diff options
author | Bernát Gábor <bgabor8@bloomberg.net> | 2021-08-31 11:58:03 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-08-31 11:58:03 +0100 |
commit | 8cdcc0639d6002cb34817bad8f7050f3dc66d35b (patch) | |
tree | f29ae3c48e28538389f08bc2657d8cbe8a43020a | |
parent | eac98db5a08ee29891a20fef24894e141d84c27d (diff) | |
download | tox-git-8cdcc0639d6002cb34817bad8f7050f3dc66d35b.tar.gz |
Internal changes to support tox-gh-actions (#2191)
-rw-r--r-- | docs/changelog/2191.bugfix.rst | 1 | ||||
-rw-r--r-- | docs/changelog/2191.feature.rst | 2 | ||||
-rw-r--r-- | src/tox/config/loader/api.py | 7 | ||||
-rw-r--r-- | src/tox/config/loader/ini/__init__.py | 2 | ||||
-rw-r--r-- | src/tox/config/loader/memory.py | 2 | ||||
-rw-r--r-- | src/tox/config/main.py | 31 | ||||
-rw-r--r-- | src/tox/config/of_type.py | 41 | ||||
-rw-r--r-- | src/tox/config/sets.py | 53 | ||||
-rw-r--r-- | src/tox/config/source/api.py | 11 | ||||
-rw-r--r-- | src/tox/config/source/ini.py | 24 | ||||
-rw-r--r-- | src/tox/plugin/manager.py | 4 | ||||
-rw-r--r-- | tests/config/loader/ini/replace/conftest.py | 3 | ||||
-rw-r--r-- | tests/config/test_of_types.py | 8 | ||||
-rw-r--r-- | tests/config/test_sets.py | 29 | ||||
-rw-r--r-- | whitelist.txt | 2 |
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 |