summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBernát Gábor <bgabor8@bloomberg.net>2021-08-31 11:58:03 +0100
committerGitHub <noreply@github.com>2021-08-31 11:58:03 +0100
commit8cdcc0639d6002cb34817bad8f7050f3dc66d35b (patch)
treef29ae3c48e28538389f08bc2657d8cbe8a43020a
parenteac98db5a08ee29891a20fef24894e141d84c27d (diff)
downloadtox-git-8cdcc0639d6002cb34817bad8f7050f3dc66d35b.tar.gz
Internal changes to support tox-gh-actions (#2191)
-rw-r--r--docs/changelog/2191.bugfix.rst1
-rw-r--r--docs/changelog/2191.feature.rst2
-rw-r--r--src/tox/config/loader/api.py7
-rw-r--r--src/tox/config/loader/ini/__init__.py2
-rw-r--r--src/tox/config/loader/memory.py2
-rw-r--r--src/tox/config/main.py31
-rw-r--r--src/tox/config/of_type.py41
-rw-r--r--src/tox/config/sets.py53
-rw-r--r--src/tox/config/source/api.py11
-rw-r--r--src/tox/config/source/ini.py24
-rw-r--r--src/tox/plugin/manager.py4
-rw-r--r--tests/config/loader/ini/replace/conftest.py3
-rw-r--r--tests/config/test_of_types.py8
-rw-r--r--tests/config/test_sets.py29
-rw-r--r--whitelist.txt2
15 files changed, 149 insertions, 71 deletions
diff --git a/docs/changelog/2191.bugfix.rst b/docs/changelog/2191.bugfix.rst
new file mode 100644
index 00000000..2d19a689
--- /dev/null
+++ b/docs/changelog/2191.bugfix.rst
@@ -0,0 +1 @@
+Fix the ``tox_configure`` plugin entrypoint is not called -- by :user:`gaborbernat`.
diff --git a/docs/changelog/2191.feature.rst b/docs/changelog/2191.feature.rst
new file mode 100644
index 00000000..2d5a5a1a
--- /dev/null
+++ b/docs/changelog/2191.feature.rst
@@ -0,0 +1,2 @@
+Expose the parsed CLI arguments on the main configuration object for plugins and allow plugins to define their own
+configuration section -- by :user:`gaborbernat`.
diff --git a/src/tox/config/loader/api.py b/src/tox/config/loader/api.py
index bdbac197..f436a1ff 100644
--- a/src/tox/config/loader/api.py
+++ b/src/tox/config/loader/api.py
@@ -49,10 +49,15 @@ V = TypeVar("V")
class Loader(Convert[T]):
"""Loader loads a configuration value and converts it."""
- def __init__(self, overrides: List[Override]) -> None:
+ def __init__(self, section: str, overrides: List[Override]) -> None:
+ self._section_name = section
self.overrides = {o.key: o for o in overrides}
self.parent: Optional["Loader[Any]"] = None
+ @property
+ def section_name(self) -> str:
+ return self._section_name
+
@abstractmethod
def load_raw(self, key: str, conf: Optional["Config"], env_name: Optional[str]) -> T: # noqa: U100
"""
diff --git a/src/tox/config/loader/ini/__init__.py b/src/tox/config/loader/ini/__init__.py
index 55d67be3..ee5cf9f9 100644
--- a/src/tox/config/loader/ini/__init__.py
+++ b/src/tox/config/loader/ini/__init__.py
@@ -26,7 +26,7 @@ class IniLoader(StrConvert, Loader[str]):
self._section: SectionProxy = parser[section]
self._parser = parser
self.core_prefix = core_prefix
- super().__init__(overrides)
+ super().__init__(section, overrides)
def load_raw(self, key: str, conf: Optional["Config"], env_name: Optional[str]) -> str:
return self.process_raw(conf, env_name, self._section[key])
diff --git a/src/tox/config/loader/memory.py b/src/tox/config/loader/memory.py
index 063dd3b7..696163f2 100644
--- a/src/tox/config/loader/memory.py
+++ b/src/tox/config/loader/memory.py
@@ -10,7 +10,7 @@ from .api import Loader
class MemoryLoader(Loader[Any]):
def __init__(self, **kwargs: Any) -> None:
- super().__init__([])
+ super().__init__("<memory>", [])
self.raw: Dict[str, Any] = {**kwargs}
def load_raw(self, key: Any, conf: Optional["Config"], env_name: Optional[str]) -> T: # noqa: U100
diff --git a/src/tox/config/main.py b/src/tox/config/main.py
index 60d4107c..2d7062fe 100644
--- a/src/tox/config/main.py
+++ b/src/tox/config/main.py
@@ -1,16 +1,18 @@
import os
from collections import OrderedDict, defaultdict
from pathlib import Path
-from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Optional, Sequence, Tuple
+from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, Optional, Sequence, Tuple, Type, TypeVar
-from tox.config.loader.api import Loader, Override, OverrideMap
+from tox.config.loader.api import Loader, OverrideMap
-from .sets import CoreConfigSet, EnvConfigSet
+from .sets import ConfigSet, CoreConfigSet, EnvConfigSet
from .source import Source
if TYPE_CHECKING:
from .cli.parser import Parsed
+T = TypeVar("T", bound=ConfigSet)
+
class Config:
"""Main configuration object for tox."""
@@ -18,7 +20,7 @@ class Config:
def __init__(
self,
config_source: Source,
- overrides: List[Override],
+ options: "Parsed",
root: Path,
pos_args: Optional[Sequence[str]],
work_dir: Path,
@@ -26,9 +28,10 @@ class Config:
self._pos_args = None if pos_args is None else tuple(pos_args)
self._work_dir = work_dir
self._root = root
+ self._options = options
self._overrides: OverrideMap = defaultdict(list)
- for override in overrides:
+ for override in options.override:
self._overrides[override.namespace].append(override)
self._src = config_source
@@ -36,6 +39,10 @@ class Config:
self._core_set: Optional[CoreConfigSet] = None
self.register_config_set: Callable[[str, EnvConfigSet], Any] = lambda n, e: None
+ from tox.plugin.manager import MANAGER
+
+ MANAGER.tox_configure(self)
+
def pos_args(self, to_path: Optional[Path]) -> Optional[Tuple[str, ...]]:
"""
:param to_path: if not None rewrite relative posargs paths from cwd to to_path
@@ -85,18 +92,22 @@ class Config:
work_dir: Path = source.path.parent if parsed.work_dir is None else parsed.work_dir
return cls(
config_source=source,
- overrides=parsed.override,
+ options=parsed,
pos_args=pos_args,
root=root,
work_dir=work_dir,
)
@property
+ def options(self) -> "Parsed":
+ return self._options
+
+ @property
def core(self) -> CoreConfigSet:
""":return: the core configuration"""
if self._core_set is not None:
return self._core_set
- core = CoreConfigSet(self, self._root)
+ core = CoreConfigSet(self, self._root, self.src_path)
for loader in self._src.get_core(self._overrides):
core.loaders.append(loader)
@@ -106,6 +117,12 @@ class Config:
self._core_set = core
return core
+ def get_section_config(self, section_name: str, of_type: Type[T]) -> T:
+ conf_set = of_type(self)
+ for loader in self._src.get_section(section_name, self._overrides):
+ conf_set.loaders.append(loader)
+ return conf_set
+
def get_env(
self, item: str, package: bool = False, loaders: Optional[Sequence[Loader[Any]]] = None
) -> EnvConfigSet:
diff --git a/src/tox/config/of_type.py b/src/tox/config/of_type.py
index c53964be..ea8012d6 100644
--- a/src/tox/config/of_type.py
+++ b/src/tox/config/of_type.py
@@ -18,19 +18,22 @@ V = TypeVar("V")
class ConfigDefinition(ABC, Generic[T]):
"""Abstract base class for configuration definitions"""
- def __init__(self, keys: Iterable[str], desc: str, env_name: Optional[str]) -> None:
+ def __init__(self, keys: Iterable[str], desc: str) -> None:
self.keys = keys
self.desc = desc
- self.env_name = env_name
@abstractmethod
def __call__(
- self, conf: "Config", key: Optional[str], loaders: List[Loader[T]], chain: List[str] # noqa: U100
+ self,
+ conf: "Config", # noqa: U100
+ loaders: List[Loader[T]], # noqa: U100
+ env_name: Optional[str], # noqa: U100
+ chain: Optional[List[str]], # noqa: U100
) -> T:
raise NotImplementedError
def __eq__(self, o: Any) -> bool:
- return type(self) == type(o) and (self.keys, self.desc, self.env_name) == (o.keys, o.desc, o.env_name)
+ return type(self) == type(o) and (self.keys, self.desc) == (o.keys, o.desc)
def __ne__(self, o: Any) -> bool:
return not (self == o)
@@ -43,14 +46,17 @@ class ConfigConstantDefinition(ConfigDefinition[T]):
self,
keys: Iterable[str],
desc: str,
- env_name: Optional[str],
value: Union[Callable[[], T], T],
) -> None:
- super().__init__(keys, desc, env_name)
+ super().__init__(keys, desc)
self.value = value
def __call__(
- self, conf: "Config", name: Optional[str], loaders: List[Loader[T]], chain: List[str] # noqa: U100
+ self,
+ conf: "Config", # noqa: U100
+ loaders: List[Loader[T]], # noqa: U100
+ env_name: Optional[str], # noqa: U100
+ chain: Optional[List[str]], # noqa: U100
) -> T:
if callable(self.value):
value = self.value()
@@ -72,13 +78,12 @@ class ConfigDynamicDefinition(ConfigDefinition[T]):
self,
keys: Iterable[str],
desc: str,
- env_name: Optional[str],
of_type: Type[T],
default: Union[Callable[["Config", Optional[str]], T], T],
post_process: Optional[Callable[[T], T]] = None,
kwargs: Optional[Mapping[str, Any]] = None,
) -> None:
- super().__init__(keys, desc, env_name)
+ super().__init__(keys, desc)
self.of_type = of_type
self.default = default
self.post_process = post_process
@@ -86,22 +91,26 @@ class ConfigDynamicDefinition(ConfigDefinition[T]):
self._cache: Union[object, T] = _PLACE_HOLDER
def __call__(
- self,
- conf: "Config",
- name: Optional[str], # noqa: U100
- loaders: List[Loader[T]],
- chain: List[str],
+ self, conf: "Config", loaders: List[Loader[T]], env_name: Optional[str], chain: Optional[List[str]]
) -> T:
+ if chain is None:
+ chain = []
if self._cache is _PLACE_HOLDER:
for key, loader in product(self.keys, loaders):
+ chain_key = f"{loader.section_name}.{key}"
+ if chain_key in chain:
+ raise ValueError(f"circular chain detected {', '.join(chain[chain.index(chain_key):])}")
+ chain.append(chain_key)
try:
- value = loader.load(key, self.of_type, self.kwargs, conf, self.env_name, chain)
+ value = loader.load(key, self.of_type, self.kwargs, conf, env_name, chain)
except KeyError:
continue
else:
break
+ finally:
+ del chain[-1]
else:
- value = self.default(conf, self.env_name) if callable(self.default) else self.default
+ value = self.default(conf, env_name) if callable(self.default) else self.default
if self.post_process is not None:
value = self.post_process(value) # noqa
self._cache = value
diff --git a/src/tox/config/sets.py b/src/tox/config/sets.py
index d9515c5e..dc8c3eea 100644
--- a/src/tox/config/sets.py
+++ b/src/tox/config/sets.py
@@ -30,8 +30,7 @@ V = TypeVar("V")
class ConfigSet:
"""A set of configuration that belong together (such as a tox environment settings, core tox settings)"""
- def __init__(self, conf: "Config", name: Optional[str]):
- self._name = name
+ def __init__(self, conf: "Config"):
self._conf = conf
self.loaders: List[Loader[Any]] = []
self._defined: Dict[str, ConfigDefinition[Any]] = {}
@@ -59,7 +58,7 @@ class ConfigSet:
:return: the new dynamic config definition
"""
keys_ = self._make_keys(keys)
- definition = ConfigDynamicDefinition(keys_, desc, self._name, of_type, default, post_process, kwargs)
+ definition = ConfigDynamicDefinition(keys_, desc, of_type, default, post_process, kwargs)
result = self._add_conf(keys_, definition)
return cast(ConfigDynamicDefinition[V], result)
@@ -73,7 +72,7 @@ class ConfigSet:
:return: the new constant config value
"""
keys_ = self._make_keys(keys)
- definition = ConfigConstantDefinition(keys_, desc, self._name, value)
+ definition = ConfigConstantDefinition(keys_, desc, value)
result = self._add_conf(keys_, definition)
return cast(ConfigConstantDefinition[V], result)
@@ -84,12 +83,7 @@ class ConfigSet:
def _add_conf(self, keys: Sequence[str], definition: ConfigDefinition[V]) -> ConfigDefinition[V]:
key = keys[0]
if key in self._defined:
- earlier = self._defined[key]
- # core definitions may be defined multiple times as long as all their options match, first defined wins
- if self._name is None and definition == earlier:
- definition = earlier
- else:
- raise ValueError(f"config {key} already defined")
+ self._on_duplicate_conf(key, definition)
else:
self._keys[key] = None
for item in keys:
@@ -98,6 +92,11 @@ class ConfigSet:
self._defined[key] = definition
return definition
+ def _on_duplicate_conf(self, key: str, definition: ConfigDefinition[V]) -> None:
+ earlier = self._defined[key]
+ if definition != earlier: # pragma: no branch
+ raise ValueError(f"config {key} already defined")
+
def __getitem__(self, item: str) -> Any:
"""
Get the config value for a given key (will materialize in case of dynamic config).
@@ -116,18 +115,14 @@ class ConfigSet:
:return: the configuration value
"""
config_definition = self._defined[item]
- if chain is None:
- chain = []
- env_name = "tox" if self._name is None else f"testenv:{self._name}"
- key = f"{env_name}.{item}"
- if key in chain:
- raise ValueError(f"circular chain detected {', '.join(chain[chain.index(key):])}")
- chain.append(key)
- return config_definition(self._conf, item, self.loaders, chain)
+ return config_definition.__call__(self._conf, self.loaders, self.name, chain)
+
+ @property
+ def name(self) -> Optional[str]:
+ return None
def __repr__(self) -> str:
- values = (v for v in (f"name={self._name!r}" if self._name else "", f"loaders={self.loaders!r}") if v)
- return f"{self.__class__.__name__}({', '.join(values)})"
+ return f"{self.__class__.__name__}(loaders={self.loaders!r})"
def __iter__(self) -> Iterator[str]:
""":return: iterate through the defined config keys (primary keys used)"""
@@ -166,8 +161,9 @@ class ConfigSet:
class CoreConfigSet(ConfigSet):
"""Configuration set for the core tox config"""
- def __init__(self, conf: "Config", root: Path) -> None:
- super().__init__(conf, name=None)
+ def __init__(self, conf: "Config", root: Path, src_path: Path) -> None:
+ super().__init__(conf)
+ self.add_constant(keys=["config_file_path"], desc="path to the configuration file", value=src_path)
self.add_config(
keys=["tox_root", "toxinidir"],
of_type=Path,
@@ -198,12 +194,16 @@ class CoreConfigSet(ConfigSet):
desc="define environments to automatically run",
)
+ def _on_duplicate_conf(self, key: str, definition: ConfigDefinition[V]) -> None: # noqa: U100
+ pass # core definitions may be defined multiple times as long as all their options match, first defined wins
+
class EnvConfigSet(ConfigSet):
"""Configuration set for a tox environment"""
- def __init__(self, conf: "Config", name: Optional[str]):
- super().__init__(conf, name=name)
+ def __init__(self, conf: "Config", name: str):
+ self._name = name
+ super().__init__(conf)
self.default_set_env_loader: Callable[[], Mapping[str, str]] = lambda: {}
def set_env_post_process(values: SetEnv) -> SetEnv:
@@ -220,7 +220,10 @@ class EnvConfigSet(ConfigSet):
@property
def name(self) -> str:
- return self._name # type: ignore
+ return self._name
+
+ def __repr__(self) -> str:
+ return f"{self.__class__.__name__}(name={self._name!r}, loaders={self.loaders!r})"
__all__ = (
diff --git a/src/tox/config/source/api.py b/src/tox/config/source/api.py
index 6855b661..bfc2381a 100644
--- a/src/tox/config/source/api.py
+++ b/src/tox/config/source/api.py
@@ -29,6 +29,17 @@ class Source(ABC):
raise NotImplementedError
@abstractmethod
+ def get_section(self, name: str, override_map: OverrideMap) -> Iterator[Loader[Any]]: # noqa: U100
+ """
+ Return a loader that loads the core configuration values.
+
+ :param name: name of the section to load
+ :param override_map: a list of overrides to apply
+ :returns: the core loader from this source
+ """
+ raise NotImplementedError
+
+ @abstractmethod
def get_env_loaders(
self, env_name: str, override_map: OverrideMap, package: bool, conf: ConfigSet # noqa: U100
) -> Iterator[Loader[Any]]:
diff --git a/src/tox/config/source/ini.py b/src/tox/config/source/ini.py
index 0008fa24..8fbe5180 100644
--- a/src/tox/config/source/ini.py
+++ b/src/tox/config/source/ini.py
@@ -29,24 +29,28 @@ class IniSource(Source, ABC):
raise ValueError
content = path.read_text()
self._parser.read_string(content, str(path))
- self._envs: Dict[Optional[str], List[IniLoader]] = {}
+ self._envs: Dict[str, List[IniLoader]] = {}
+ self._sections: Dict[str, List[IniLoader]] = {}
def get_core(self, override_map: OverrideMap) -> Iterator[IniLoader]:
- if None in self._envs:
- yield from self._envs[None]
+ yield from self.get_section(self.CORE_PREFIX, override_map)
+
+ def get_section(self, name: str, override_map: OverrideMap) -> Iterator[IniLoader]:
+ if name in self._sections:
+ yield from self._sections[name]
return
- core = []
- if self._parser.has_section(self.CORE_PREFIX):
- core.append(
+ section = []
+ if self._parser.has_section(name):
+ section.append(
IniLoader(
- section=self.CORE_PREFIX,
+ section=name,
parser=self._parser,
- overrides=override_map.get(self.CORE_PREFIX, []),
+ overrides=override_map.get(name, []),
core_prefix=self.CORE_PREFIX,
)
)
- self._envs[None] = core
- yield from core
+ self._sections[name] = section
+ yield from section
def get_env_loaders(
self, env_name: str, override_map: OverrideMap, package: bool, conf: ConfigSet
diff --git a/src/tox/plugin/manager.py b/src/tox/plugin/manager.py
index 41c7179f..786d9819 100644
--- a/src/tox/plugin/manager.py
+++ b/src/tox/plugin/manager.py
@@ -15,6 +15,7 @@ from tox.tox_env.python.virtual_env import runner
from tox.tox_env.python.virtual_env.package import api
from tox.tox_env.register import REGISTER, ToxEnvRegister
+from ..config.main import Config
from . import NAME, spec
from .inline import load_inline
@@ -54,6 +55,9 @@ class Plugin:
def tox_add_core_config(self, core: ConfigSet) -> None:
self.manager.hook.tox_add_core_config(core=core)
+ def tox_configure(self, config: Config) -> None:
+ self.manager.hook.tox_configure(config=config)
+
def tox_register_tox_env(self, register: "ToxEnvRegister") -> None:
self.manager.hook.tox_register_tox_env(register=register)
diff --git a/tests/config/loader/ini/replace/conftest.py b/tests/config/loader/ini/replace/conftest.py
index 9c66e198..75994906 100644
--- a/tests/config/loader/ini/replace/conftest.py
+++ b/tests/config/loader/ini/replace/conftest.py
@@ -4,6 +4,7 @@ from typing import List, Optional
import pytest
+from tox.config.cli.parser import Parsed
from tox.config.main import Config
from tox.config.source.tox_ini import ToxIni
@@ -24,7 +25,7 @@ def replace_one(tmp_path: Path) -> ReplaceOne:
tox_ini_file = tmp_path / "tox.ini"
tox_ini_file.write_text(f"[testenv:py]\nenv={conf}\n")
tox_ini = ToxIni(tox_ini_file)
- config = Config(tox_ini, overrides=[], root=tmp_path, pos_args=pos_args, work_dir=tmp_path)
+ config = Config(tox_ini, options=Parsed(override=[]), root=tmp_path, pos_args=pos_args, work_dir=tmp_path)
loader = config.get_env("py").loaders[0]
return loader.load(key="env", of_type=str, conf=config, env_name="a", chain=[], kwargs={})
diff --git a/tests/config/test_of_types.py b/tests/config/test_of_types.py
index 5ca83aae..8a74dda0 100644
--- a/tests/config/test_of_types.py
+++ b/tests/config/test_of_types.py
@@ -2,8 +2,8 @@ from tox.config.of_type import ConfigConstantDefinition, ConfigDynamicDefinition
def test_config_constant_eq() -> None:
- val_1 = ConfigConstantDefinition(("key",), "description", "env", "value")
- val_2 = ConfigConstantDefinition(("key",), "description", "env", "value")
+ val_1 = ConfigConstantDefinition(("key",), "description", "value")
+ val_2 = ConfigConstantDefinition(("key",), "description", "value")
assert val_1 == val_2
@@ -11,6 +11,6 @@ def test_config_dynamic_eq() -> None:
def func(name: str) -> str:
return name # pragma: no cover
- val_1 = ConfigDynamicDefinition(("key",), "description", "env", str, "default", post_process=func)
- val_2 = ConfigDynamicDefinition(("key",), "description", "env", str, "default", post_process=func)
+ val_1 = ConfigDynamicDefinition(("key",), "description", str, "default", post_process=func)
+ val_2 = ConfigDynamicDefinition(("key",), "description", str, "default", post_process=func)
assert val_1 == val_2
diff --git a/tests/config/test_sets.py b/tests/config/test_sets.py
index e0bc7dd3..9c03c71d 100644
--- a/tests/config/test_sets.py
+++ b/tests/config/test_sets.py
@@ -5,7 +5,10 @@ from typing import Callable, Dict, Optional, Set, TypeVar
import pytest
from tests.conftest import ToxIniCreator
+from tox.config.cli.parser import Parsed
+from tox.config.main import Config
from tox.config.sets import ConfigSet
+from tox.pytest import ToxProjectCreator
ConfBuilder = Callable[[str], ConfigSet]
@@ -112,14 +115,14 @@ def test_config_redefine_constant_fail(conf_builder: ConfBuilder) -> None:
config_set = conf_builder("path = path")
config_set.add_constant(keys="path", desc="desc", value="value")
with pytest.raises(ValueError, match="config path already defined"):
- config_set.add_constant(keys="path", desc="desc", value="value")
+ config_set.add_constant(keys="path", desc="desc2", value="value")
def test_config_redefine_dynamic_fail(conf_builder: ConfBuilder) -> None:
config_set = conf_builder("path = path")
- config_set.add_config(keys="path", of_type=str, default="default", desc="path")
+ config_set.add_config(keys="path", of_type=str, default="default_1", desc="path")
with pytest.raises(ValueError, match="config path already defined"):
- config_set.add_config(keys="path", of_type=str, default="default", desc="path")
+ config_set.add_config(keys="path", of_type=str, default="default_2", desc="path")
def test_config_dynamic_not_equal(conf_builder: ConfBuilder) -> None:
@@ -127,3 +130,23 @@ def test_config_dynamic_not_equal(conf_builder: ConfBuilder) -> None:
path = config_set.add_config(keys="path", of_type=Path, default=Path(), desc="path")
paths = config_set.add_config(keys="paths", of_type=Path, default=Path(), desc="path")
assert path != paths
+
+
+def test_define_custom_set(tox_project: ToxProjectCreator) -> None:
+ class MagicConfigSet(ConfigSet):
+ SECTION = "magic"
+
+ def __init__(self, conf: Config):
+ super().__init__(conf)
+ self.add_config("a", of_type=int, default=0, desc="number")
+ self.add_config("b", of_type=str, default="", desc="string")
+
+ project = tox_project({"tox.ini": "[testenv]\npackage=skip\n[magic]\na = 1\nb = ok"})
+ result = project.run()
+
+ conf = result.state.conf.get_section_config(MagicConfigSet.SECTION, MagicConfigSet)
+ assert conf["a"] == 1
+ assert conf["b"] == "ok"
+ assert repr(conf) == "MagicConfigSet(loaders=[IniLoader(section=<Section: magic>, overrides={})])"
+
+ assert isinstance(result.state.conf.options, Parsed)
diff --git a/whitelist.txt b/whitelist.txt
index 476d0d5a..fe47b850 100644
--- a/whitelist.txt
+++ b/whitelist.txt
@@ -98,7 +98,6 @@ groupdict
hookimpl
hookspec
hookspecs
-htmlhelp
ident
ign
ignorecase
@@ -180,7 +179,6 @@ rfind
rpartition
rreq
rst
-rtd
runtime
sdist
setdefault