diff options
author | Bernát Gábor <bgabor8@bloomberg.net> | 2022-01-04 09:15:39 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-01-04 09:15:39 +0000 |
commit | c88d497c535f62540637a437d5a0c71c335b86d2 (patch) | |
tree | 802209644b46e788aa6e656a0c2243d79d950908 | |
parent | e62a717b8033a8f3ae556a7ea9183933f2d65a66 (diff) | |
download | tox-git-c88d497c535f62540637a437d5a0c71c335b86d2.tar.gz |
Better selection support (#2290)
68 files changed, 944 insertions, 576 deletions
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 2cfe04ac..dbd19061 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -15,11 +15,11 @@ jobs: steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - - uses: pre-commit/action@v2.0.0 + - uses: pre-commit/action@v2.0.3 test: - name: test ${{ matrix.py }} - ${{ matrix.os }} - runs-on: ${{ matrix.os }}-latest + name: test ${{ matrix.py }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: @@ -29,9 +29,9 @@ jobs: - "3.8" - "3.7" os: - - Ubuntu - - Windows - - MacOs + - ubuntu-20.04 + - windows-2022 + - macos-11 steps: - name: Setup python for tox uses: actions/setup-python@v2 @@ -78,21 +78,21 @@ jobs: name: ${{ matrix.py }} - ${{ matrix.os }} check: - name: check ${{ matrix.tox_env }} - ${{ matrix.os }} - runs-on: ${{ matrix.os }}-latest + name: tox env ${{ matrix.tox_env }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - os: - - Windows - - Ubuntu tox_env: - type - dev - docs - pkg_meta + os: + - ubuntu-20.04 + - windows-2022 exclude: - - { os: windows, tox_env: pkg_meta } + - { os: windows-2022, tox_env: pkg_meta } steps: - uses: actions/checkout@v2 with: @@ -111,7 +111,7 @@ jobs: publish: if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') needs: [check, test, pre_commit] - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Setup python to build package uses: actions/setup-python@v2 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5c388591..4b35d1d5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 + rev: v4.1.0 hooks: - id: check-builtin-literals - id: check-docstring-first @@ -17,7 +17,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.29.1 + rev: v2.30.1 hooks: - id: pyupgrade args: ["--py37-plus"] @@ -43,7 +43,7 @@ repos: - id: prettier additional_dependencies: - prettier@2.5.1 - - "@prettier/plugin-xml@1.1.0" + - "@prettier/plugin-xml@1.2.0" args: ["--print-width=120", "--prose-wrap=always"] - repo: https://github.com/asottile/blacken-docs rev: v1.12.0 @@ -55,7 +55,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/tox-dev/tox-ini-fmt - rev: "0.5.1" + rev: "0.5.2" hooks: - id: tox-ini-fmt args: ["-p", "fix"] diff --git a/docs/changelog/2275.removal.rst b/docs/changelog/2275.removal.rst new file mode 100644 index 00000000..11d08480 --- /dev/null +++ b/docs/changelog/2275.removal.rst @@ -0,0 +1,3 @@ +``tox_add_core_config`` and ``tox_add_env_config`` now take a ``state: State`` argument instead of a configuration one, +and ``Config`` not longer provides the ``envs`` property (instead users should migrate to ``State.envs``) - by +:user:`gaborbernat`. diff --git a/docs/changelog/2290.feature.rst b/docs/changelog/2290.feature.rst new file mode 100644 index 00000000..5eb0002b --- /dev/null +++ b/docs/changelog/2290.feature.rst @@ -0,0 +1,2 @@ +Support for selecting target environments with a given factor via the :ref:`-m <tox-run--f>` CLI environment flag - by +:user:`gaborbernat`. diff --git a/docs/changelog/238.feature.rst b/docs/changelog/238.feature.rst new file mode 100644 index 00000000..4e424e5d --- /dev/null +++ b/docs/changelog/238.feature.rst @@ -0,0 +1,3 @@ +Support for grouping environment values together by applying labels to them either at :ref:`core <labels>` and +:ref:`environment <labels-env>` level, and allow selecting them via the :ref:`-m <tox-run--m>` flag from the CLI - by +:user:`gaborbernat`. diff --git a/docs/conf.py b/docs/conf.py index 620361b9..f18cd176 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -142,13 +142,16 @@ def setup(app: Sphinx) -> None: "tox.tox_env.installer.T": "typing.TypeVar", "ToxParserT": "typing.TypeVar", "_Section": "Section", + "ArgumentParser": "argparse.ArgumentParser", + "Factory": "tox.config.loader.convert.Factory", } if target in mapping: + if target == "Factory": + type = "attr" node["reftarget"] = mapping[target] - if target == "_Section": - target = "Section" - contnode = Text(target, target) - + if target == "_Section": + target = "Section" + contnode = Text(target, target) return super().resolve_xref(env, fromdocname, builder, type, target, node, contnode) app.connect("autodoc-skip-member", skip_member) diff --git a/docs/config.rst b/docs/config.rst index d9fbb94f..3de36cf8 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -195,6 +195,19 @@ Core Indicates where the packaging root file exists (historically setup.py file or pyproject.toml now). +.. conf:: + :keys: labels + :default: <empty dictionary> + + A mapping of label names to environments it applies too. For example: + + .. code-block:: ini + + [tox] + labels = + test = py310, py39 + static = flake8, mypy + Python language core options ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -304,6 +317,20 @@ Base options ``allowlist_externals=make`` or ``allowlist_externals=/usr/bin/make``. If you want to allow all external commands you can use ``allowlist_externals=*`` which will match all commands (not recommended). +.. conf:: + :keys: labels + :default: <empty list> + :ref_suffix: env + + A list of labels to apply for this environment. For example: + + .. code-block:: ini + + [testenv] + labels = test, core + [testenv:flake8] + labels = mypy + Execute ~~~~~~~ diff --git a/docs/plugins_api.rst b/docs/plugins_api.rst index 69e44ee2..99f8094c 100644 --- a/docs/plugins_api.rst +++ b/docs/plugins_api.rst @@ -73,6 +73,9 @@ config .. autoclass:: tox.config.types.Command :members: +.. autoclass:: tox.config.loader.convert.Factory + :members: + environments ------------ .. autoclass:: tox.tox_env.api.ToxEnv @@ -128,3 +131,14 @@ installer .. autoclass:: tox.tox_env.installer.Installer :members: + +session +------- +.. autoclass:: tox.session.state.State + :members: + +.. autoclass:: tox.session.env_select.EnvSelector + :members: + +.. autoclass:: tox.tox_env.info.Info + :members: diff --git a/src/tox/config/cli/parse.py b/src/tox/config/cli/parse.py index 237710ea..19003c8f 100644 --- a/src/tox/config/cli/parse.py +++ b/src/tox/config/cli/parse.py @@ -3,17 +3,26 @@ This module pulls together this package: create and parse CLI arguments for tox. """ from __future__ import annotations -from typing import Dict, Sequence, cast +from typing import TYPE_CHECKING, Callable, NamedTuple, Sequence, cast from tox.config.source import Source, discover_source from tox.report import ToxHandler, setup_report -from .parser import Handler, Parsed, ToxParser +from .parser import Parsed, ToxParser -Handlers = Dict[str, Handler] +if TYPE_CHECKING: + from tox.session.state import State -def get_options(*args: str) -> tuple[Parsed, Handlers, Sequence[str] | None, ToxHandler, Source]: +class Options(NamedTuple): + parsed: Parsed + pos_args: Sequence[str] | None + source: Source + cmd_handlers: dict[str, Callable[[State], int]] + log_handler: ToxHandler + + +def get_options(*args: str) -> Options: pos_args: tuple[str, ...] | None = None try: # remove positional arguments passed to parser if specified, they are pulled directly from sys.argv pos_arg_at = args.index("--") @@ -27,7 +36,7 @@ def get_options(*args: str) -> tuple[Parsed, Handlers, Sequence[str] | None, Tox parsed, cmd_handlers = _get_all(args) if guess_verbosity != parsed.verbosity: setup_report(parsed.verbosity, parsed.is_colored) # pragma: no cover - return parsed, cmd_handlers, pos_args, log_handler, source + return Options(parsed, pos_args, source, cmd_handlers, log_handler) def _get_base(args: Sequence[str]) -> tuple[int, ToxHandler, Source]: @@ -45,7 +54,7 @@ def _get_base(args: Sequence[str]) -> tuple[int, ToxHandler, Source]: return guess_verbosity, handler, source -def _get_all(args: Sequence[str]) -> tuple[Parsed, Handlers]: +def _get_all(args: Sequence[str]) -> tuple[Parsed, dict[str, Callable[[State], int]]]: """Parse all the options.""" tox_parser = _get_parser() parsed = cast(Parsed, tox_parser.parse_args(args)) @@ -75,5 +84,5 @@ def _get_parser_doc() -> ToxParser: __all__ = ( "get_options", - "Handlers", + "Options", ) diff --git a/src/tox/config/cli/parser.py b/src/tox/config/cli/parser.py index be830ce3..a7a4e548 100644 --- a/src/tox/config/cli/parser.py +++ b/src/tox/config/cli/parser.py @@ -9,11 +9,10 @@ import os import sys from argparse import SUPPRESS, Action, ArgumentDefaultsHelpFormatter, ArgumentParser, Namespace from pathlib import Path -from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Type, TypeVar, cast +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Sequence, Tuple, Type, TypeVar, cast from tox.config.loader.str_convert import StrConvert from tox.plugin import NAME -from tox.session.state import State from .env_var import get_env_var from .ini import IniConfig @@ -23,6 +22,9 @@ if sys.version_info >= (3, 8): # pragma: no cover (py38+) else: # pragma: no cover (py38+) from typing_extensions import Literal +if TYPE_CHECKING: + from tox.session.state import State + class ArgumentParserWithEnvAndConfig(ArgumentParser): """ @@ -93,8 +95,6 @@ class HelpFormatter(ArgumentDefaultsHelpFormatter): return text -Handler = Callable[[State], int] - ToxParserT = TypeVar("ToxParserT", bound="ToxParser") DEFAULT_VERBOSITY = 2 @@ -122,7 +122,7 @@ class ToxParser(ArgumentParserWithEnvAndConfig): def __init__(self, *args: Any, root: bool = False, add_cmd: bool = False, **kwargs: Any) -> None: self.of_cmd: str | None = None - self.handlers: dict[str, tuple[Any, Handler]] = {} + self.handlers: dict[str, tuple[Any, Callable[[State], int]]] = {} self._arguments: list[ArgumentArgs] = [] self._groups: list[tuple[Any, dict[str, Any], list[tuple[dict[str, Any], list[ArgumentArgs]]]]] = [] super().__init__(*args, **kwargs) @@ -136,7 +136,13 @@ class ToxParser(ArgumentParserWithEnvAndConfig): else: self._cmd = None - def add_command(self, cmd: str, aliases: Sequence[str], help_msg: str, handler: Handler) -> ArgumentParser: + def add_command( + self, + cmd: str, + aliases: Sequence[str], + help_msg: str, + handler: Callable[[State], int], + ) -> ArgumentParser: if self._cmd is None: raise RuntimeError("no sub-command group allowed") sub_parser: ToxParser = self._cmd.add_parser( @@ -217,7 +223,7 @@ class ToxParser(ArgumentParserWithEnvAndConfig): def parse_known_args( # type: ignore[override] self, - args: Sequence[str] | None, + args: Sequence[str] | None = None, namespace: Parsed | None = None, ) -> tuple[Parsed, list[str]]: if args is None: @@ -315,5 +321,4 @@ __all__ = ( "DEFAULT_VERBOSITY", "Parsed", "ToxParser", - "Handler", ) diff --git a/src/tox/config/loader/memory.py b/src/tox/config/loader/memory.py index c28deef0..4cbe4580 100644 --- a/src/tox/config/loader/memory.py +++ b/src/tox/config/loader/memory.py @@ -1,16 +1,18 @@ from __future__ import annotations from pathlib import Path -from typing import Any, Iterator, cast +from typing import TYPE_CHECKING, Any, Iterator, TypeVar, cast -from tox.config.loader.convert import T -from tox.config.main import Config from tox.config.types import Command, EnvList from .api import Loader from .section import Section from .str_convert import StrConvert +if TYPE_CHECKING: + from tox.config.main import Config +T = TypeVar("T") + class MemoryLoader(Loader[Any]): def __init__(self, **kwargs: Any) -> None: diff --git a/src/tox/config/main.py b/src/tox/config/main.py index 0fe110bf..d122d598 100644 --- a/src/tox/config/main.py +++ b/src/tox/config/main.py @@ -3,11 +3,10 @@ from __future__ import annotations import os from collections import OrderedDict, defaultdict from pathlib import Path -from typing import TYPE_CHECKING, Any, Callable, Iterator, Sequence, TypeVar +from typing import TYPE_CHECKING, Any, Iterator, Sequence, TypeVar from tox.config.loader.api import Loader, OverrideMap -from ..session.common import CliEnv from .loader.section import Section from .sets import ConfigSet, CoreConfigSet, EnvConfigSet from .source import Source @@ -15,6 +14,7 @@ from .source import Source if TYPE_CHECKING: from .cli.parser import Parsed + T = TypeVar("T", bound=ConfigSet) @@ -29,6 +29,7 @@ class Config: pos_args: Sequence[str] | None, work_dir: Path, ) -> None: + self._pos_args = None if pos_args is None else tuple(pos_args) self._work_dir = work_dir self._root = root @@ -42,9 +43,6 @@ class Config: self._key_to_conf_set: dict[tuple[str, str], ConfigSet] = OrderedDict() self._core_set: CoreConfigSet | None = None - def register_config_set(self, name: str, env_config_set: EnvConfigSet) -> None: # noqa: U100 - raise NotImplementedError # this should be overwritten by the state object before called - def pos_args(self, to_path: Path | None) -> tuple[str, ...] | None: """ :param to_path: if not None rewrite relative posargs paths from cwd to to_path @@ -116,9 +114,6 @@ class Config: core = CoreConfigSet(self, core_section, self._root, self.src_path) core.loaders.extend(self._src.get_loaders(core_section, base=[], override_map=self._overrides, conf=core)) self._core_set = core - from tox.plugin.manager import MANAGER - - MANAGER.tox_add_core_config(core, self) return core def get_section_config( @@ -128,7 +123,6 @@ class Config: of_type: type[T], for_env: str | None, loaders: Sequence[Loader[Any]] | None = None, - initialize: Callable[[T], None] | None = None, ) -> T: key = section.key, for_env or "" try: @@ -140,8 +134,6 @@ class Config: conf_set.loaders.append(loader) if loaders is not None: conf_set.loaders.extend(loaders) - if initialize is not None: - initialize(conf_set) return conf_set def get_env( @@ -165,33 +157,12 @@ class Config: of_type=EnvConfigSet, for_env=item, loaders=loaders, - initialize=lambda e: self.register_config_set(item, e), ) - from tox.plugin.manager import MANAGER - - MANAGER.tox_add_env_config(conf_set, self) return conf_set - def env_list(self, everything: bool = False) -> Iterator[str]: - """ - :param everything: if ``True`` returns all discovered tox environment names from the configuration - - :return: Return the tox environment names, by default only the default env list entries. - """ - fallback_env = "py" - use_env_list: CliEnv | None = getattr(self._options, "env", None) - if everything or (use_env_list is not None and use_env_list.all): - _at = 0 - for _at, env in enumerate(self, start=1): - yield env - if _at == 0: # if we discovered no other env, inject the default - yield fallback_env - return - if use_env_list is not None and use_env_list.use_default_list: - use_env_list = self.core["env_list"] - if use_env_list is None or bool(use_env_list) is False: - use_env_list = CliEnv([fallback_env]) - yield from use_env_list + def clear_env(self, name: str) -> None: + section, _ = self._src.get_tox_env_section(name) + del self._key_to_conf_set[(section.key, name)] ___all__ = [ diff --git a/src/tox/config/sets.py b/src/tox/config/sets.py index c51ff462..46864030 100644 --- a/src/tox/config/sets.py +++ b/src/tox/config/sets.py @@ -166,6 +166,8 @@ class CoreConfigSet(ConfigSet): self._root = root self._src_path = src_path super().__init__(conf, section=section, env_name=None) + desc = "define environments to automatically run" + self.add_config(keys=["env_list", "envlist"], of_type=EnvList, default=EnvList([]), desc=desc) def register_config(self) -> None: self.add_constant(keys=["config_file_path"], desc="path to the configuration file", value=self._src_path) @@ -192,12 +194,6 @@ class CoreConfigSet(ConfigSet): default=lambda conf, _: cast(Path, self["tox_root"]) / ".temp", # noqa: U100, U101 desc="temporary directory cleaned at start", ) - self.add_config( - keys=["env_list", "envlist"], - of_type=EnvList, - default=EnvList([]), - 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 diff --git a/src/tox/config/types.py b/src/tox/config/types.py index 419c095d..36dd4378 100644 --- a/src/tox/config/types.py +++ b/src/tox/config/types.py @@ -57,10 +57,6 @@ class EnvList: """:return: iterator that goes through the defined env-list""" return iter(self.envs) - def __bool__(self) -> bool: - """:return: ``True`` if there are any environments defined""" - return bool(self.envs) - __all__ = ( "Command", diff --git a/src/tox/plugin/manager.py b/src/tox/plugin/manager.py index c10c8282..55e92d0b 100644 --- a/src/tox/plugin/manager.py +++ b/src/tox/plugin/manager.py @@ -10,16 +10,14 @@ from tox import provision from tox.config.cli.parser import ToxParser from tox.config.loader import api as loader_api from tox.config.sets import ConfigSet, EnvConfigSet -from tox.session import state -from tox.session.cmd import depends, devenv, exec_, legacy, list_env, quickstart, show_config, version_flag from tox.session.cmd.run import parallel, sequential from tox.tox_env import package as package_api from tox.tox_env.python.virtual_env import runner from tox.tox_env.python.virtual_env.package import cmd_builder, pep517 from tox.tox_env.register import REGISTER, ToxEnvRegister -from ..config.main import Config from ..execute import Outcome +from ..session.state import State from ..tox_env.api import ToxEnv from . import NAME, spec from .inline import load_inline @@ -30,6 +28,9 @@ class Plugin: self.manager: pluggy.PluginManager = pluggy.PluginManager(NAME) self.manager.add_hookspecs(spec) + from tox.session import state + from tox.session.cmd import depends, devenv, exec_, legacy, list_env, quickstart, show_config, version_flag + internal_plugins = ( loader_api, provision, @@ -58,11 +59,11 @@ class Plugin: def tox_add_option(self, parser: ToxParser) -> None: self.manager.hook.tox_add_option(parser=parser) - def tox_add_core_config(self, core_conf: ConfigSet, config: Config) -> None: - self.manager.hook.tox_add_core_config(core_conf=core_conf, config=config) + def tox_add_core_config(self, core_conf: ConfigSet, state: State) -> None: + self.manager.hook.tox_add_core_config(core_conf=core_conf, state=state) - def tox_add_env_config(self, env_conf: EnvConfigSet, config: Config) -> None: - self.manager.hook.tox_add_env_config(env_conf=env_conf, config=config) + def tox_add_env_config(self, env_conf: EnvConfigSet, state: State) -> None: + self.manager.hook.tox_add_env_config(env_conf=env_conf, state=state) def tox_register_tox_env(self, register: ToxEnvRegister) -> None: self.manager.hook.tox_register_tox_env(register=register) diff --git a/src/tox/plugin/spec.py b/src/tox/plugin/spec.py index 4eec8aa6..502ece71 100644 --- a/src/tox/plugin/spec.py +++ b/src/tox/plugin/spec.py @@ -4,12 +4,12 @@ from typing import Any, Callable, TypeVar, cast import pluggy -from tox.config.main import Config from tox.config.sets import ConfigSet, EnvConfigSet from tox.tox_env.register import ToxEnvRegister from ..config.cli.parser import ToxParser from ..execute import Outcome +from ..session.state import State from ..tox_env.api import ToxEnv from . import NAME @@ -44,22 +44,22 @@ def tox_add_option(parser: ToxParser) -> None: # noqa: U100 @_spec -def tox_add_core_config(core_conf: ConfigSet, config: Config) -> None: # noqa: U100 +def tox_add_core_config(core_conf: ConfigSet, state: State) -> None: # noqa: U100 """ Called when the core configuration is built for a tox environment. :param core_conf: the core configuration object - :param config: the global tox config object + :param state: the global tox state object """ @_spec -def tox_add_env_config(env_conf: EnvConfigSet, config: Config) -> None: # noqa: U100 +def tox_add_env_config(env_conf: EnvConfigSet, state: State) -> None: # noqa: U100 """ Called when configuration is built for a tox environment. :param env_conf: the core configuration object - :param config: the global tox config object + :param state: the global tox state object """ diff --git a/src/tox/provision.py b/src/tox/provision.py index 1728c1b8..9b43ce2b 100644 --- a/src/tox/provision.py +++ b/src/tox/provision.py @@ -8,19 +8,16 @@ import logging import sys from argparse import ArgumentParser from pathlib import Path -from typing import List, cast +from typing import TYPE_CHECKING, List, cast from packaging.requirements import Requirement from packaging.utils import canonicalize_name from packaging.version import Version from tox.config.loader.memory import MemoryLoader -from tox.config.main import Config -from tox.config.sets import CoreConfigSet from tox.execute.api import StdinSource from tox.plugin import impl from tox.report import HandledError -from tox.session.state import State from tox.tox_env.errors import Skip from tox.tox_env.python.pip.req_file import PythonDeps from tox.tox_env.python.runner import PythonRun @@ -31,6 +28,9 @@ if sys.version_info >= (3, 8): # pragma: no cover (py38+) else: # pragma: no cover (py38+) from importlib_metadata import PackageNotFoundError, distribution +if TYPE_CHECKING: + from tox.session.state import State + @impl def tox_add_option(parser: ArgumentParser) -> None: @@ -57,16 +57,15 @@ def tox_add_option(parser: ArgumentParser) -> None: ) -@impl -def tox_add_core_config(core_conf: CoreConfigSet, config: Config) -> None: # noqa: U100 - core_conf.add_config( +def provision(state: State) -> int | bool: + state.conf.core.add_config( keys=["min_version", "minversion"], of_type=Version, # do not include local version specifier (because it's not allowed in version spec per PEP-440) default=Version(current_version.split("+")[0]), desc="Define the minimal tox version required to run", ) - core_conf.add_config( + state.conf.core.add_config( keys="provision_tox_env", of_type=str, default=".tox", @@ -74,29 +73,42 @@ def tox_add_core_config(core_conf: CoreConfigSet, config: Config) -> None: # no ) def add_tox_requires_min_version(requires: list[Requirement]) -> list[Requirement]: - min_version: Version = core_conf["min_version"] + min_version: Version = state.conf.core["min_version"] requires.append(Requirement(f"tox >= {min_version.public}")) return requires - core_conf.add_config( + state.conf.core.add_config( keys="requires", of_type=List[Requirement], default=[], desc="Name of the virtual environment used to provision a tox.", post_process=add_tox_requires_min_version, ) - - -def provision(state: State) -> int | bool: requires: list[Requirement] = state.conf.core["requires"] missing = _get_missing(requires) + + deps = ", ".join(f"{p}{'' if v is None else f' ({v})'}" for p, v in missing) + loader = MemoryLoader( # these configuration values are loaded from in-memory always (no file conf) + base=[], # disable inheritance for provision environments + package="skip", # no packaging for this please + # use our own dependency specification + deps=PythonDeps("\n".join(str(r) for r in requires), root=state.conf.core["tox_root"]), + pass_env=["*"], # do not filter environment variables, will be handled by provisioned tox + recreate=state.conf.options.recreate and not state.conf.options.no_recreate_provision, + ) + provision_tox_env: str = state.conf.core["provision_tox_env"] + state.envs._mark_provision(bool(missing), provision_tox_env, loader) + + from tox.plugin.manager import MANAGER + + MANAGER.tox_add_core_config(state.conf.core, state) + if not missing: return False - deps = ", ".join(f"{p}{'' if v is None else f' ({v})'}" for p, v in missing) miss_msg = f"is missing [requires (has)]: {deps}" - no_provision: bool | str = state.options.no_provision + no_provision: bool | str = state.conf.options.no_provision if no_provision: msg = f"provisioning explicitly disabled within {sys.executable}, but {miss_msg}" if isinstance(no_provision, str): @@ -109,7 +121,7 @@ def provision(state: State) -> int | bool: raise HandledError(msg) logging.warning("will run in automatically provisioned tox, host %s %s", sys.executable, miss_msg) - return run_provision(requires, state) + return run_provision(provision_tox_env, state) def _get_missing(requires: list[Requirement]) -> list[tuple[Requirement, str | None]]: @@ -126,19 +138,8 @@ def _get_missing(requires: list[Requirement]) -> list[tuple[Requirement, str | N return missing -def run_provision(deps: list[Requirement], state: State) -> int: - """ """ - loader = MemoryLoader( # these configuration values are loaded from in-memory always (no file conf) - base=[], # disable inheritance for provision environments - package="skip", # no packaging for this please - # use our own dependency specification - deps=PythonDeps("\n".join(str(d) for d in deps), root=state.conf.core["tox_root"]), - pass_env=["*"], # do not filter environment variables, will be handled by provisioned tox - recreate=state.options.recreate and not state.options.no_recreate_provision, - ) - provision_tox_env: str = state.conf.core["provision_tox_env"] - state.conf.get_env(provision_tox_env, loaders=[loader]) - tox_env = cast(PythonRun, state.tox_env(provision_tox_env)) +def run_provision(name: str, state: State) -> int: + tox_env: PythonRun = cast(PythonRun, state.envs[name]) env_python = tox_env.env_python() logging.info("will run in a automatically provisioned python environment under %s", env_python) try: diff --git a/src/tox/report.py b/src/tox/report.py index 55d4a562..f15e1a5a 100644 --- a/src/tox/report.py +++ b/src/tox/report.py @@ -94,7 +94,7 @@ class NamedBytesIO(BytesIO): self.name: str = name -class ToxHandler(logging.StreamHandler): +class ToxHandler(logging.StreamHandler): # type: ignore[type-arg] # is generic but at runtime doesn't take a type arg # """Controls tox output.""" def __init__(self, level: int, is_colored: bool, out_err: OutErr) -> None: @@ -217,7 +217,7 @@ def setup_report(verbosity: int, is_colored: bool) -> ToxHandler: logger = logging.getLogger(name) logger.filters.clear() logger.addFilter(lower_info_level) - out_err: OutErr = (sys.stdout, sys.stderr) # type: ignore[arg-type,assignment] + out_err: OutErr = (sys.stdout, sys.stderr) # type: ignore[assignment] handler = ToxHandler(level, is_colored, out_err) LOGGER.addHandler(handler) diff --git a/src/tox/run.py b/src/tox/run.py index 7631e46f..544e35ac 100644 --- a/src/tox/run.py +++ b/src/tox/run.py @@ -8,8 +8,6 @@ import time from typing import Sequence from tox.config.cli.parse import get_options -from tox.config.main import Config -from tox.provision import provision from tox.report import HandledError, ToxHandler from tox.session.state import State @@ -37,11 +35,12 @@ def run(args: Sequence[str] | None = None) -> None: def main(args: Sequence[str]) -> int: state = setup_state(args) + from tox.provision import provision + result = provision(state) if result is not False: return result - command = state.options.command - handler = state.cmd_handlers[command] + handler = state._options.cmd_handlers[state.conf.options.command] result = handler(state) return result @@ -50,10 +49,8 @@ def setup_state(args: Sequence[str]) -> State: """Setup the state object of this run.""" start = time.monotonic() # parse CLI arguments - parsed, handlers, pos_args, log_handler, source = get_options(*args) - parsed.start = start - # parse configuration file - config = Config.make(parsed, pos_args, source) + options = get_options(*args) + options.parsed.start = start # build tox environment config objects - state = State(config, (parsed, handlers), args, log_handler) + state = State(options, args) return state diff --git a/src/tox/session/cmd/depends.py b/src/tox/session/cmd/depends.py index ca107ea1..44c173fd 100644 --- a/src/tox/session/cmd/depends.py +++ b/src/tox/session/cmd/depends.py @@ -1,23 +1,27 @@ from __future__ import annotations +from typing import cast + from tox.config.cli.parser import ToxParser from tox.plugin import impl -from tox.session.cmd.run.common import run_order +from tox.session.cmd.run.common import env_run_create_flags, run_order from tox.session.state import State +from tox.tox_env.runner import RunToxEnv @impl def tox_add_option(parser: ToxParser) -> None: - parser.add_command( + our = parser.add_command( "depends", ["de"], "visualize tox environment dependencies", depends, ) + env_run_create_flags(our, mode="depends") def depends(state: State) -> int: - to_run_list = list(state.all_run_envs(with_skip=False)) + to_run_list = list(state.envs.iter(only_active=False)) order, todo = run_order(state, to_run_list) print(f"Execution order: {', '.join(order)}") @@ -28,7 +32,14 @@ def depends(state: State) -> int: print(" " * at, end="") print(env, end="") if env != "ALL": - names = " | ".join(e.conf.name for e in state.tox_env(env).package_envs) + run_env = cast(RunToxEnv, state.envs[env]) + packager_list: list[str] = [] + try: + for pkg_env in run_env.package_envs: + packager_list.append(pkg_env.name) + except Exception as exception: + packager_list.append(f"... ({exception})") + names = " | ".join(packager_list) if names: print(f" ~ {names}", end="") print("") diff --git a/src/tox/session/cmd/devenv.py b/src/tox/session/cmd/devenv.py index 514d97b2..b9188ab4 100644 --- a/src/tox/session/cmd/devenv.py +++ b/src/tox/session/cmd/devenv.py @@ -8,7 +8,7 @@ from tox.plugin import impl from tox.report import HandledError from tox.session.cmd.run.common import env_run_create_flags from tox.session.cmd.run.sequential import run_sequential -from tox.session.common import CliEnv, env_list_flag +from tox.session.env_select import CliEnv, register_env_select_flags from tox.session.state import State @@ -17,28 +17,28 @@ def tox_add_option(parser: ToxParser) -> None: help_msg = "sets up a development environment at ENVDIR based on the tox configuration specified " our = parser.add_command("devenv", ["d"], help_msg, devenv) our.add_argument("devenv_path", metavar="path", default=Path("venv").absolute(), nargs="?") - env_list_flag(our, default=CliEnv("py"), multiple=False) + register_env_select_flags(our, default=CliEnv("py"), multiple=False) env_run_create_flags(our, mode="devenv") def devenv(state: State) -> int: - opt = state.options - opt.skip_missing_interpreters = False - opt.no_test = False + opt = state.conf.options + + opt.skip_missing_interpreters = False # the target python must exist + opt.no_test = False # do not run the test suite opt.package_only = False - opt.install_pkg = None - opt.skip_pkg_install = False + opt.install_pkg = None # no explicit packages to install + opt.skip_pkg_install = False # always install a package in this case - env_list = list(state.conf.env_list(everything=False)) - if len(env_list) != 1: - raise HandledError(f"exactly one target environment allowed in devenv mode but found {', '.join(env_list)}") + state.envs.ensure_only_run_env_is_active() + envs = list(state.envs.iter()) + if len(envs) != 1: + raise HandledError(f"exactly one target environment allowed in devenv mode but found {', '.join(envs)}") loader = MemoryLoader( # these configuration values are loaded from in-memory always (no file conf) - # dev environments must be of type dev - usedevelop=True, - # move it in source - env_dir=Path(state.options.devenv_path), + usedevelop=True, # dev environments must be of type dev + env_dir=Path(opt.devenv_path), # move it in source ) - state.options.no_test = True # do not run the test phase - state.conf.get_env(env_list[0], loaders=[loader]) + opt.no_test = True # do not run the test phase + state.conf.get_env(envs[0], loaders=[loader]) return run_sequential(state) diff --git a/src/tox/session/cmd/exec_.py b/src/tox/session/cmd/exec_.py index 48ed4841..891189b7 100644 --- a/src/tox/session/cmd/exec_.py +++ b/src/tox/session/cmd/exec_.py @@ -12,7 +12,7 @@ from tox.plugin import impl from tox.report import HandledError from tox.session.cmd.run.common import env_run_create_flags from tox.session.cmd.run.sequential import run_sequential -from tox.session.common import CliEnv, env_list_flag +from tox.session.env_select import CliEnv, register_env_select_flags from tox.session.state import State @@ -20,20 +20,21 @@ from tox.session.state import State def tox_add_option(parser: ToxParser) -> None: our = parser.add_command("exec", ["e"], "execute an arbitrary command within a tox environment", exec_) our.epilog = "For example: tox exec -e py39 -- python --version" - env_list_flag(our, default=CliEnv("py"), multiple=False) + register_env_select_flags(our, default=CliEnv("py"), multiple=False) env_run_create_flags(our, mode="exec") def exec_(state: State) -> int: - env_list = list(state.conf.env_list(everything=False)) - if len(env_list) != 1: - raise HandledError(f"exactly one target environment allowed in exec mode but found {', '.join(env_list)}") + envs = list(state.envs.iter()) + if len(envs) != 1: + raise HandledError(f"exactly one target environment allowed in exec mode but found {', '.join(envs)}") loader = MemoryLoader( # these configuration values are loaded from in-memory always (no file conf) commands_pre=[], commands=[], commands_post=[], ) - conf = state.conf.get_env(env_list[0], loaders=[loader]) + conf = state.envs[envs[0]].conf + conf.loaders.insert(0, loader) to_path: Path | None = conf["change_dir"] if conf["args_are_paths"] else None pos_args = state.conf.pos_args(to_path) if not pos_args: diff --git a/src/tox/session/cmd/legacy.py b/src/tox/session/cmd/legacy.py index 9da5f810..ed14e023 100644 --- a/src/tox/session/cmd/legacy.py +++ b/src/tox/session/cmd/legacy.py @@ -7,9 +7,9 @@ from tox.plugin import impl from tox.session.cmd.run.common import env_run_create_flags from tox.session.cmd.run.parallel import OFF_VALUE, parallel_flags, run_parallel from tox.session.cmd.run.sequential import run_sequential -from tox.session.common import env_list_flag from tox.session.state import State +from ..env_select import CliEnv, register_env_select_flags from .devenv import devenv from .list_env import list_env from .show_config import show_config @@ -48,7 +48,7 @@ def tox_add_option(parser: ToxParser) -> None: default=None, of_type=Path, ) - env_list_flag(our) + register_env_select_flags(our, default=CliEnv()) env_run_create_flags(our, mode="legacy") parallel_flags(our, default_parallel=OFF_VALUE) our.add_argument( @@ -88,12 +88,13 @@ def tox_add_option(parser: ToxParser) -> None: def legacy(state: State) -> int: - option = state.options + option = state.conf.options if option.show_config: - state.options.list_keys_only = [] - state.options.show_core = True + option.list_keys_only = [] + option.show_core = True return show_config(state) if option.list_envs or option.list_envs_all: + state.envs.on_empty_fallback_py = False option.list_no_description = option.verbosity <= DEFAULT_VERBOSITY option.list_default_only = not option.list_envs_all option.show_core = False diff --git a/src/tox/session/cmd/list_env.py b/src/tox/session/cmd/list_env.py index 1a7b19d1..6bc34ad9 100644 --- a/src/tox/session/cmd/list_env.py +++ b/src/tox/session/cmd/list_env.py @@ -3,33 +3,37 @@ Print available tox environments. """ from __future__ import annotations +from itertools import chain + from tox.config.cli.parser import ToxParser from tox.plugin import impl +from tox.session.env_select import register_env_select_flags from tox.session.state import State @impl def tox_add_option(parser: ToxParser) -> None: our = parser.add_command("list", ["l"], "list environments", list_env) - our.add_argument("-d", action="store_true", help="list just default envs", dest="list_default_only") our.add_argument("--no-desc", action="store_true", help="do not show description", dest="list_no_description") + d = register_env_select_flags(our, default=None, group_only=True) + d.add_argument("-d", action="store_true", help="list just default envs", dest="list_default_only") def list_env(state: State) -> int: - core = state.conf.core - option = state.options - - default = core["env_list"] # this should be something not affected by env-vars :-| + option = state.conf.options + has_group_select = bool(option.factors or option.labels) + active_only = has_group_select or option.list_default_only - extra = [] if option.list_default_only else [e for e in state.all_run_envs(with_skip=True) if e not in default] + active = dict.fromkeys(state.envs.iter()) + inactive = {} if active_only else {env: None for env in state.envs.iter(only_active=False) if env not in active} - if not option.list_no_description and default: + if not has_group_select and not option.list_no_description and active: print("default environments:") - max_length = max((len(env) for env in (default.envs + extra)), default=0) + max_length = max((len(env) for env in chain(active, inactive)), default=0) def report_env(name: str) -> None: if not option.list_no_description: - tox_env = state.tox_env(name) + tox_env = state.envs[name] text = tox_env.conf["description"] if not text.strip(): text = "[no description]" @@ -39,14 +43,14 @@ def list_env(state: State) -> int: msg = env print(msg) - for env in default: + for env in active: report_env(env) - if not option.list_default_only and extra: + if not has_group_select and not option.list_default_only and inactive: if not option.list_no_description: - if default: + if active: # pragma: no branch print("") print("additional environments:") - for env in extra: + for env in inactive: report_env(env) return 0 diff --git a/src/tox/session/cmd/quickstart.py b/src/tox/session/cmd/quickstart.py index 785696a3..bba00bc5 100644 --- a/src/tox/session/cmd/quickstart.py +++ b/src/tox/session/cmd/quickstart.py @@ -30,7 +30,7 @@ def tox_add_option(parser: ToxParser) -> None: def quickstart(state: State) -> int: - root = state.options.quickstart_root.absolute() + root = state.conf.options.quickstart_root.absolute() tox_ini = root / "tox.ini" if tox_ini.exists(): print(f"{tox_ini} already exist, refusing to overwrite") diff --git a/src/tox/session/cmd/run/common.py b/src/tox/session/cmd/run/common.py index 5139c7e9..00939f82 100644 --- a/src/tox/session/cmd/run/common.py +++ b/src/tox/session/cmd/run/common.py @@ -58,7 +58,7 @@ class InstallPackageAction(Action): def env_run_create_flags(parser: ArgumentParser, mode: str) -> None: # mode can be one of: run, run-parallel, legacy, devenv, config - if mode != "config": + if mode not in ("config", "depends"): parser.add_argument( "--result-json", dest="result_json", @@ -67,7 +67,7 @@ def env_run_create_flags(parser: ArgumentParser, mode: str) -> None: default=None, help="write a JSON file with detailed information about all commands and results involved", ) - if mode != "devenv": + if mode not in ("devenv", "depends"): parser.add_argument( "-s", "--skip-missing-interpreters", @@ -77,7 +77,7 @@ def env_run_create_flags(parser: ArgumentParser, mode: str) -> None: action=SkipMissingInterpreterAction, help="don't fail tests for missing interpreters: {config,true,false} choice", ) - if mode not in ("devenv", "config"): + if mode not in ("devenv", "config", "depends"): parser.add_argument( "-n", "--notest", @@ -101,14 +101,14 @@ def env_run_create_flags(parser: ArgumentParser, mode: str) -> None: action=InstallPackageAction, dest="install_pkg", ) - if mode != "devenv": + if mode not in ("devenv", "depends"): parser.add_argument( "--develop", action="store_true", help="install package in development mode", dest="develop", ) - if mode != "config": + if mode not in ("config", "depends"): parser.add_argument( "--hashseed", metavar="SEED", @@ -126,13 +126,14 @@ def env_run_create_flags(parser: ArgumentParser, mode: str) -> None: help="for Python discovery first try the Python executables under these paths", default=[], ) - parser.add_argument( - "--no-recreate-pkg", - dest="no_recreate_pkg", - help="if recreate is set do not recreate packaging tox environment(s)", - action="store_true", - ) - if mode not in ("devenv", "config"): + if mode not in ("depends",): + parser.add_argument( + "--no-recreate-pkg", + dest="no_recreate_pkg", + help="if recreate is set do not recreate packaging tox environment(s)", + action="store_true", + ) + if mode not in ("devenv", "config", "depends"): parser.add_argument( "--skip-pkg-install", dest="skip_pkg_install", @@ -185,10 +186,10 @@ def execute(state: State, max_workers: int | None, has_spinner: bool, live: bool interrupt, done = Event(), Event() results: list[ToxEnvRunResult] = [] future_to_env: dict[Future[ToxEnvRunResult], ToxEnv] = {} - to_run_list: list[str] = [] - for env in state.conf.env_list(): # ensure envs can be constructed - state.tox_env(env) - to_run_list.append(env) + state.envs.ensure_only_run_env_is_active() + to_run_list: list[str] = list(state.envs.iter()) + for name in to_run_list: + cast(RunToxEnv, state.envs[name]).mark_active() previous, has_previous = None, False try: spinner = ToxSpinner(has_spinner, state, len(to_run_list)) @@ -224,9 +225,9 @@ def execute(state: State, max_workers: int | None, has_spinner: bool, live: bool for env in to_run_list: ordered_results.append(name_to_run[env]) # write the journal - write_journal(getattr(state.options, "result_json", None), state.journal) + write_journal(getattr(state.conf.options, "result_json", None), state._journal) # report the outcome - exit_code = report(state.options.start, ordered_results, state.options.is_colored) + exit_code = report(state.conf.options.start, ordered_results, state.conf.options.is_colored) if has_previous: signal(SIGINT, previous) return exit_code @@ -236,8 +237,8 @@ class ToxSpinner(Spinner): def __init__(self, enabled: bool, state: State, total: int) -> None: super().__init__( enabled=enabled, - colored=state.options.is_colored, - stream=state.log_handler.stdout, + colored=state.conf.options.is_colored, + stream=state._options.log_handler.stdout, total=total, ) @@ -261,7 +262,7 @@ def _queue_and_wait( live: bool, ) -> None: try: - options = state.options + options = state._options with spinner: max_workers = len(to_run_list) if max_workers is None else max_workers completed: set[str] = set() @@ -269,14 +270,14 @@ def _queue_and_wait( def _run(tox_env: RunToxEnv) -> ToxEnvRunResult: spinner.add(tox_env.conf.name) - return run_one(tox_env, options.no_test, suspend_display=live is False) + return run_one(tox_env, options.parsed.no_test, suspend_display=live is False) try: executor = ThreadPoolExecutor(max_workers=max_workers, thread_name_prefix="tox-driver") env_list: list[str] = [] while True: for env in env_list: # queue all available - tox_env_to_run = state.tox_env(env) + tox_env_to_run = cast(RunToxEnv, state.envs[env]) if interrupt.is_set(): # queue the rest as failed upfront tox_env_to_run.teardown() future: Future[ToxEnvRunResult] = Future() @@ -324,8 +325,8 @@ def _queue_and_wait( finally: try: # call teardown - configuration only environments for example could not be finished - for _, tox_env in state.created_run_envs(): - tox_env.teardown() + for name in to_run_list: + state.envs[name].teardown() finally: done.set() @@ -333,14 +334,14 @@ def _queue_and_wait( def _handle_one_run_done(result: ToxEnvRunResult, spinner: ToxSpinner, state: State, live: bool) -> None: success = result.code == Outcome.OK spinner.update_spinner(result, success) - tox_env = state.tox_env(result.name) + tox_env = cast(RunToxEnv, state.envs[result.name]) if tox_env.journal: # add overall journal entry tox_env.journal["result"] = { "success": success, "exit_code": result.code, "duration": result.duration, } - if live is False and state.options.parallel_live is False: # teardown background run + if live is False and state.conf.options.parallel_live is False: # teardown background run out_err = tox_env.close_and_read_out_err() # sync writes from buffer to stdout/stderr pkg_out_err_list = [] for package_env in tox_env.package_envs: @@ -349,9 +350,9 @@ def _handle_one_run_done(result: ToxEnvRunResult, spinner: ToxSpinner, state: St pkg_out_err_list.append(pkg_out_err) if not success or tox_env.conf["parallel_show_output"]: for pkg_out_err in pkg_out_err_list: - state.log_handler.write_out_err(pkg_out_err) # pragma: no cover + state._options.log_handler.write_out_err(pkg_out_err) # pragma: no cover if out_err is not None: # pragma: no branch # first show package build - state.log_handler.write_out_err(out_err) + state._options.log_handler.write_out_err(out_err) def ready_to_run_envs(state: State, to_run: list[str], completed: set[str]) -> Iterator[list[str]]: @@ -373,7 +374,7 @@ def run_order(state: State, to_run: list[str]) -> tuple[list[str], dict[str, set to_run_set = set(to_run) todo: dict[str, set[str]] = {} for env in to_run: - run_env = state.tox_env(env) + run_env = cast(RunToxEnv, state.envs[env]) depends = set(cast(EnvList, run_env.conf["depends"]).envs) todo[env] = to_run_set & depends order = stable_topological_sort(todo) diff --git a/src/tox/session/cmd/run/parallel.py b/src/tox/session/cmd/run/parallel.py index 7c19d383..53cca051 100644 --- a/src/tox/session/cmd/run/parallel.py +++ b/src/tox/session/cmd/run/parallel.py @@ -8,10 +8,10 @@ from argparse import ArgumentParser, ArgumentTypeError from tox.config.cli.parser import ToxParser from tox.plugin import impl -from tox.session.common import env_list_flag from tox.session.state import State from tox.util.cpu import auto_detect_cpus +from ...env_select import CliEnv, register_env_select_flags from .common import env_run_create_flags, execute logger = logging.getLogger(__name__) @@ -24,7 +24,7 @@ DEFAULT_PARALLEL = OFF_VALUE @impl def tox_add_option(parser: ToxParser) -> None: our = parser.add_command("run-parallel", ["p"], "run environments in parallel", run_parallel) - env_list_flag(our) + register_env_select_flags(our, default=CliEnv()) env_run_create_flags(our, mode="run-parallel") parallel_flags(our, default_parallel=auto_detect_cpus()) @@ -72,9 +72,10 @@ def parallel_flags(our: ArgumentParser, default_parallel: int) -> None: def run_parallel(state: State) -> int: """here we'll just start parallel sub-processes""" + option = state.conf.options return execute( state, - max_workers=state.options.parallel, - has_spinner=state.options.parallel_no_spinner is False and state.options.parallel_live is False, - live=state.options.parallel_live, + max_workers=option.parallel, + has_spinner=option.parallel_no_spinner is False and option.parallel_live is False, + live=option.parallel_live, ) diff --git a/src/tox/session/cmd/run/sequential.py b/src/tox/session/cmd/run/sequential.py index fcc71d46..682b35e8 100644 --- a/src/tox/session/cmd/run/sequential.py +++ b/src/tox/session/cmd/run/sequential.py @@ -5,16 +5,16 @@ from __future__ import annotations from tox.config.cli.parser import ToxParser from tox.plugin import impl -from tox.session.common import env_list_flag from tox.session.state import State +from ...env_select import CliEnv, register_env_select_flags from .common import env_run_create_flags, execute @impl def tox_add_option(parser: ToxParser) -> None: our = parser.add_command("run", ["r"], "run environments", run_sequential) - env_list_flag(our) + register_env_select_flags(our, default=CliEnv()) env_run_create_flags(our, mode="run") diff --git a/src/tox/session/cmd/show_config.py b/src/tox/session/cmd/show_config.py index b6e867ab..82181e3c 100644 --- a/src/tox/session/cmd/show_config.py +++ b/src/tox/session/cmd/show_config.py @@ -13,12 +13,9 @@ from tox.config.loader.stringify import stringify from tox.config.sets import ConfigSet from tox.plugin import impl from tox.session.cmd.run.common import env_run_create_flags -from tox.session.common import CliEnv, env_list_flag +from tox.session.env_select import CliEnv, register_env_select_flags from tox.session.state import State from tox.tox_env.api import ToxEnv -from tox.tox_env.errors import Skip -from tox.tox_env.package import PackageToxEnv -from tox.tox_env.runner import RunToxEnv @impl @@ -38,13 +35,13 @@ def tox_add_option(parser: ToxParser) -> None: help="show core options too when selecting an env with -e", dest="show_core", ) - env_list_flag(our, default=CliEnv("ALL")) + register_env_select_flags(our, default=CliEnv("ALL")) env_run_create_flags(our, mode="config") def show_config(state: State) -> int: - is_colored = state.options.is_colored - keys: list[str] = state.options.list_keys_only + is_colored = state.conf.options.is_colored + keys: list[str] = state.conf.options.list_keys_only is_first = True def _print_env(tox_env: ToxEnv) -> None: @@ -58,39 +55,14 @@ def show_config(state: State) -> int: print_key_value(is_colored, "type", type(tox_env).__name__) print_conf(is_colored, tox_env.conf, keys) - def _get_run_env(env_name: str) -> RunToxEnv: - try: - return state.tox_env(env_name) - except Skip: - return state.tox_env(env_name) # get again to get the temporary state - - # because the target env could be a packaging one we first need to discover all defined ones - run_envs: dict[str, RunToxEnv] = {} - pkg_envs: dict[str, PackageToxEnv] = {} - for name in state.conf.env_list(everything=True): - run_env = _get_run_env(name) - run_envs[name] = run_env - for pkg_env in run_env.package_envs: - pkg_envs[pkg_env.conf.name] = pkg_env - - show_everything = state.options.env.all - done_pkg_envs: set[str] = set() - for name in state.conf.env_list(): # now go through selected ones - if name in pkg_envs: - if name not in done_pkg_envs: - _print_env(pkg_envs[name]) - done_pkg_envs.add(name) - else: - run_env = run_envs[name] if name in run_envs else _get_run_env(name) # an on-demand env, construct it now - _print_env(run_env) - if show_everything: - for pkg_env in run_env.package_envs: - if pkg_env.name not in done_pkg_envs: - _print_env(pkg_env) - done_pkg_envs.add(pkg_env.name) + show_everything = state.conf.options.env.is_all + done: set[str] = set() + for name in state.envs.iter(package=True): # now go through selected ones + done.add(name) + _print_env(state.envs[name]) # environments may define core configuration flags, so we must exhaust first the environments to tell the core part - if show_everything or state.options.show_core: + if show_everything or state.conf.options.show_core: print("") print_section_header(is_colored, "[tox]") print_conf(is_colored, state.conf.core, keys) diff --git a/src/tox/session/common.py b/src/tox/session/common.py deleted file mode 100644 index a2b2bd8a..00000000 --- a/src/tox/session/common.py +++ /dev/null @@ -1,50 +0,0 @@ -from __future__ import annotations - -from argparse import ArgumentParser -from typing import Any, Iterator, List - -from tox.config.loader.str_convert import StrConvert - - -class CliEnv: - def __init__(self, value: None | list[str] | str = None): - if isinstance(value, str): - value = StrConvert().to(value, of_type=List[str], factory=None) - self._names = value - - def __iter__(self) -> Iterator[str]: - if self._names is not None: # pragma: no branch - yield from self._names - - def __str__(self) -> str: - if self.all: - return "ALL" - if self.use_default_list: - return "<env_list>" - return ",".join(self) - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({'' if self.use_default_list else repr(str(self))})" - - def __eq__(self, other: Any) -> bool: - return type(self) == type(other) and self._names == other._names - - def __ne__(self, other: Any) -> bool: - return not (self == other) - - @property - def all(self) -> bool: - return "ALL" in self - - @property - def use_default_list(self) -> bool: - return len(list(self)) == 0 - - -def env_list_flag(parser: ArgumentParser, default: CliEnv | None = None, multiple: bool = True) -> None: - help_msg = ( - "tox environment(s) to run (ALL -> all environments, not set -> <env_list>)" - if multiple - else "tox environment to run" - ) - parser.add_argument("-e", dest="env", help=help_msg, default=CliEnv() if default is None else default, type=CliEnv) diff --git a/src/tox/session/env_select.py b/src/tox/session/env_select.py new file mode 100644 index 00000000..07ecf33d --- /dev/null +++ b/src/tox/session/env_select.py @@ -0,0 +1,336 @@ +from __future__ import annotations + +from argparse import ArgumentParser +from collections import Counter +from dataclasses import dataclass +from itertools import chain +from typing import TYPE_CHECKING, Any, Dict, Iterable, Iterator, List, cast + +from tox.config.loader.str_convert import StrConvert +from tox.tox_env.api import ToxEnvCreateArgs +from tox.tox_env.register import REGISTER +from tox.tox_env.runner import RunToxEnv + +from ..config.loader.memory import MemoryLoader +from ..config.types import EnvList +from ..report import HandledError +from ..tox_env.errors import Skip +from ..tox_env.package import PackageToxEnv + +if TYPE_CHECKING: + from tox.session.state import State + + +class CliEnv: + """CLI tox env selection""" + + def __init__(self, value: None | list[str] | str = None): + if isinstance(value, str): + value = StrConvert().to(value, of_type=List[str], factory=None) + self._names: list[str] | None = value + + def __iter__(self) -> Iterator[str]: + if not self.is_all and self._names is not None: # pragma: no branch + yield from self._names + + def __str__(self) -> str: + return "ALL" if self.is_all else ("<env_list>" if self.is_default_list else ",".join(self)) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({'' if self.is_default_list else repr(str(self))})" + + def __eq__(self, other: Any) -> bool: + return type(self) == type(other) and self._names == other._names + + def __ne__(self, other: Any) -> bool: + return not (self == other) + + @property + def is_all(self) -> bool: + return self._names is not None and "ALL" in self._names + + @property + def is_default_list(self) -> bool: + return not (self._names or []) + + +def register_env_select_flags( + parser: ArgumentParser, + default: CliEnv | None, + multiple: bool = True, + group_only: bool = False, +) -> ArgumentParser: + """ + Register environment selection flags. + + :param parser: the parser to register to + :param default: the default value for env selection + :param multiple: allow selecting multiple environments + :param group_only: + :return: + """ + if multiple: + group = parser.add_argument_group("select target environment(s)") + add_to: ArgumentParser = group.add_mutually_exclusive_group(required=False) # type: ignore + else: + add_to = parser + if not group_only: + if multiple: + help_msg = "enumerate (ALL -> all environments, not set -> use <env_list> from config)" + else: + help_msg = "environment to run" + add_to.add_argument("-e", dest="env", help=help_msg, default=default, type=CliEnv) + if multiple: + help_msg = "labels to evaluate" + add_to.add_argument("-m", dest="labels", metavar="label", help=help_msg, default=[], type=str, nargs="+") + help_msg = "factors to evaluate" + add_to.add_argument("-f", dest="factors", metavar="factor", help=help_msg, default=[], type=str, nargs="+") + return add_to + + +@dataclass +class _ToxEnvInfo: + """tox environment information""" + + env: PackageToxEnv | RunToxEnv #: the tox environment + is_active: bool #: a flag indicating if the environment is marked as active in the current run + package_skip: tuple[str, Skip] | None = None #: if set the creation of the packaging environment failed + + +class EnvSelector: + def __init__(self, state: State) -> None: + # needs core to load the default tox environment list + # to load the package environments of a run environments we need the run environment builder + # to load labels we need core + the run environment + self.on_empty_fallback_py = True + self._state = state + self._cli_envs: CliEnv | None = getattr(self._state.conf.options, "env", None) + self._defined_envs_: None | dict[str, _ToxEnvInfo] = None + self._pkg_env_counter: Counter[str] = Counter() + from tox.plugin.manager import MANAGER + + self._manager = MANAGER + self._log_handler = self._state._options.log_handler + self._journal = self._state._journal + self._provision: None | tuple[bool, str, MemoryLoader] = None + + self._state.conf.core.add_config("labels", Dict[str, EnvList], {}, "core labels") + + def _collect_names(self) -> Iterator[tuple[Iterable[str], bool]]: + """:return: sources of tox environments defined with name and if is marked as target to run""" + if self._provision is not None: # pragma: no branch + yield (self._provision[1],), False + env_list, everything_active = self._state.conf.core["env_list"], False + if self._cli_envs is None or self._cli_envs.is_default_list: + yield env_list, True + elif self._cli_envs.is_all: + everything_active = True + else: + yield self._cli_envs, True + yield self._state.conf, everything_active + label_envs = dict.fromkeys(chain.from_iterable(self._state.conf.core["labels"].values())) + if label_envs: + yield label_envs.keys(), False + + def _env_name_to_active(self) -> dict[str, bool]: + env_name_to_active_map = {} + for a_collection, is_active in self._collect_names(): + for name in a_collection: + if name not in env_name_to_active_map: + env_name_to_active_map[name] = is_active + # for factor/label selection update the active flag + if not (getattr(self._state.conf.options, "labels", []) or getattr(self._state.conf.options, "factors", [])): + # if no active environment is defined fallback to py + if self.on_empty_fallback_py and not any(env_name_to_active_map.values()): + env_name_to_active_map["py"] = True + return env_name_to_active_map + + @property + def _defined_envs(self) -> dict[str, _ToxEnvInfo]: + # The problem of classifying run/package environments: + # There can be two type of tox environments: run or package. Given a tox environment name there's no easy way to + # find out which it is. Intuitively a run environment is any environment that's not used for packaging by + # another run environment. To find out what are the packaging environments for a run environment you have to + # first construct it. This implies a two phase solution: construct all environments and query their packaging + # environments. The run environments are the ones not marked as of packaging type. This requires being able + # to change tox environments type, if it was earlier discovered as a run environment and is marked as packaging + # we need to redefine it, e.g. when it shows up in config as [testenv:.package] and afterwards by a run env is + # marked as package_env. + + if self._defined_envs_ is None: + self._defined_envs_ = {} + failed: dict[str, Exception] = {} + env_name_to_active = self._env_name_to_active() + for name, is_active in env_name_to_active.items(): + if name in self._pkg_env_counter: # already marked as packaging, nothing to do here + continue + with self._log_handler.with_context(name): + run_env = self._build_run_env(name) + if run_env is None: + continue + self._defined_envs_[name] = _ToxEnvInfo(run_env, is_active) + pkg_name_type = run_env.get_package_env_types() + if pkg_name_type is not None: + # build package env and assign it, then register the run environment which can trigger generation + # of additional run environments + start_package_env_use_counter = self._pkg_env_counter.copy() + try: + run_env.package_env = self._build_pkg_env(pkg_name_type, name, env_name_to_active) + except Exception as exception: + # if it's not a run environment, wait to see if ends up being a packaging one -> rollback + failed[name] = exception + for key in self._pkg_env_counter - start_package_env_use_counter: + del self._defined_envs_[key] + self._state.conf.clear_env(key) + self._pkg_env_counter = start_package_env_use_counter + del self._defined_envs_[name] + self._state.conf.clear_env(name) + else: + try: + for env in run_env.package_envs: + # check if any packaging envs are already run and remove them + other_env_info = self._defined_envs_.get(env.name) + if other_env_info is not None and isinstance(other_env_info.env, RunToxEnv): + del self._defined_envs_[env.name] # pragma: no cover + for _pkg_env in other_env_info.env.package_envs: # pragma: no cover + self._pkg_env_counter[_pkg_env.name] -= 1 # pragma: no cover + except Exception: + assert self._defined_envs_[name].package_skip is not None + failed_to_create = failed.keys() - self._defined_envs_.keys() + if failed_to_create: + raise failed[next(iter(failed_to_create))] + for name, count in self._pkg_env_counter.items(): + if not count: + self._defined_envs_.pop(name) # pragma: no cover + + # reorder to as defined rather as found + order = chain(env_name_to_active, (i for i in self._defined_envs_ if i not in env_name_to_active)) + self._defined_envs_ = {name: self._defined_envs_[name] for name in order if name in self._defined_envs_} + self._mark_active() + return self._defined_envs_ + + def _build_run_env(self, name: str) -> RunToxEnv | None: + if self._provision is not None and self._provision[0] is False and name == self._provision[1]: + return None + env_conf = self._state.conf.get_env( + name, + package=False, + loaders=[self._provision[2]] if self._provision is not None and self._provision[1] == name else None, + ) + desc = "the tox execute used to evaluate this environment" + env_conf.add_config(keys="runner", desc=desc, of_type=str, default=self._state.conf.options.default_runner) + runner = REGISTER.runner(cast(str, env_conf["runner"])) + journal = self._journal.get_env_journal(name) + args = ToxEnvCreateArgs(env_conf, self._state.conf.core, self._state.conf.options, journal, self._log_handler) + run_env = runner(args) + self._manager.tox_add_env_config(env_conf, self._state) + return run_env + + def _build_pkg_env(self, name_type: tuple[str, str], run_env_name: str, active: dict[str, bool]) -> PackageToxEnv: + name, core_type = name_type + with self._log_handler.with_context(name): + if run_env_name == name: + raise HandledError(f"{run_env_name} cannot self-package") + missing_active = self._cli_envs is not None and self._cli_envs.is_all + try: + package_tox_env = self._get_package_env(core_type, name, active.get(name, missing_active)) + self._pkg_env_counter[name] += 1 + run_env: RunToxEnv = self._defined_envs_[run_env_name].env # type: ignore + child_package_envs = package_tox_env.register_run_env(run_env) + try: + name_type = next(child_package_envs) + while True: + child_pkg_env = self._build_pkg_env(name_type, run_env_name, active) + self._pkg_env_counter[name_type[0]] += 1 + name_type = child_package_envs.send(child_pkg_env) + except StopIteration: + pass + except Skip as exception: + assert self._defined_envs_ is not None + self._defined_envs_[run_env_name].package_skip = (name_type[0], exception) + return package_tox_env + + def _get_package_env(self, packager: str, name: str, is_active: bool) -> PackageToxEnv: + assert self._defined_envs_ is not None + if name in self._defined_envs_: + env = self._defined_envs_[name].env + if isinstance(env, PackageToxEnv): + if env.id() != packager: # pragma: no branch # same env name is used by different packaging + msg = f"{name} is already defined as a {env.id()}, cannot be {packager} too" # pragma: no cover + raise HandledError(msg) # pragma: no cover + return env + else: + self._state.conf.clear_env(name) + package_type = REGISTER.package(packager) + pkg_conf = self._state.conf.get_env(name, package=True) + journal = self._journal.get_env_journal(name) + args = ToxEnvCreateArgs(pkg_conf, self._state.conf.core, self._state.conf.options, journal, self._log_handler) + pkg_env: PackageToxEnv = package_type(args) + self._defined_envs_[name] = _ToxEnvInfo(pkg_env, is_active) + self._manager.tox_add_env_config(pkg_conf, self._state) + return pkg_env + + def _mark_active(self) -> None: + labels = set(getattr(self._state.conf.options, "labels", [])) + factors = set(getattr(self._state.conf.options, "factors", [])) + assert self._defined_envs_ is not None + if labels or factors: + for env_info in self._defined_envs_.values(): + env_info.is_active = False # if any was selected reset + if labels: + for label in labels: + for env_name in self._state.conf.core["labels"].get(label, []): + self._defined_envs_[env_name].is_active = True + for env_info in self._defined_envs_.values(): + if labels.intersection(env_info.env.conf["labels"]): + env_info.is_active = True + if self._state.conf.options.factors: # if matches mark it active + for name, env_info in self._defined_envs_.items(): + if factors.issubset(set(name.split("-"))): + env_info.is_active = True + + def __getitem__(self, item: str) -> RunToxEnv | PackageToxEnv: + """ + :param item: the name of the environment + :return: the tox environment + """ + return self._defined_envs[item].env + + def iter( + self, + *, + only_active: bool = True, + package: bool = False, + ) -> Iterator[str]: + """ + Get tox environments. + + :param only_active: active environments are marked to be executed in the current target + :param package: return package environments + + :return: an iteration of tox environments + """ + ignore_envs: set[str] = set() + for name, env_info in self._defined_envs.items(): + if only_active and not env_info.is_active: + continue + if not package and not isinstance(env_info.env, RunToxEnv): + continue + yield name + ignore_envs.add(name) + + def ensure_only_run_env_is_active(self) -> None: + envs, active = self._defined_envs, self._env_name_to_active() + invalid = [n for n, a in active.items() if a and isinstance(envs[n].env, PackageToxEnv)] + if invalid: + raise HandledError(f"cannot run packaging environment(s) {','.join(invalid)}") + + def _mark_provision(self, on: bool, provision_tox_env: str, loader: MemoryLoader) -> None: + self._provision = on, provision_tox_env, loader + + +__all__ = [ + "register_env_select_flags", + "EnvSelector", + "CliEnv", +] diff --git a/src/tox/session/state.py b/src/tox/session/state.py index 692aa596..5bc7f2a7 100644 --- a/src/tox/session/state.py +++ b/src/tox/session/state.py @@ -1,144 +1,35 @@ from __future__ import annotations -from itertools import chain -from typing import TYPE_CHECKING, Iterator, Sequence, cast +from typing import TYPE_CHECKING, Sequence from tox.config.main import Config -from tox.config.sets import EnvConfigSet from tox.journal import Journal from tox.plugin import impl -from tox.report import HandledError, ToxHandler -from tox.tox_env.api import ToxEnvCreateArgs -from tox.tox_env.errors import Skip -from tox.tox_env.package import PackageToxEnv -from tox.tox_env.runner import RunToxEnv + +from .env_select import EnvSelector if TYPE_CHECKING: - from tox.config.cli.parse import Handlers - from tox.config.cli.parser import Parsed, ToxParser + from tox.config.cli.parse import Options + from tox.config.cli.parser import ToxParser class State: - def __init__( - self, - conf: Config, - opt_parse: tuple[Parsed, Handlers], - args: Sequence[str], - log_handler: ToxHandler, - ) -> None: - self.conf = conf - self.conf.register_config_set = self.register_config_set # type: ignore[assignment] - options, cmd_handlers = opt_parse - self.options = options - self.cmd_handlers = cmd_handlers - self.log_handler = log_handler - self.args = args - - self._run_env: dict[str, RunToxEnv] = {} - self._pkg_env: dict[str, tuple[str, PackageToxEnv]] = {} - self._pkg_env_discovered: set[str] = set() - - self.journal: Journal = Journal(getattr(options, "result_json", None) is not None) - - def tox_env(self, name: str) -> RunToxEnv: - if name in self._pkg_env_discovered: - raise HandledError(f"cannot run packaging environment {name}") - with self.log_handler.with_context(name): - tox_env = self._run_env.get(name) - if tox_env is not None: - return tox_env - self.conf.get_env(name) # the lookup here will trigger register_config_set, which will build it - return self._run_env[name] - - def register_config_set(self, name: str, env_config_set: EnvConfigSet) -> None: - """Ensure the config set with the given name has been registered with configuration values""" - # during the creation of the tox environment we automatically register configurations, so to ensure - # config sets have a set of defined values in it we have to ensure the tox environment is created - if name in self._pkg_env_discovered: - return # packaging environments are created explicitly, nothing to do here - if name in self._run_env: # pragma: no branch - raise ValueError(f"{name} run tox env already defined") # pragma: no cover - # runtime environments are created upon lookup via the tox_env method, call it - self._build_run_env(env_config_set) - - def _build_run_env(self, env_conf: EnvConfigSet) -> None: - env_conf.add_config( - keys="runner", - desc="the tox execute used to evaluate this environment", - of_type=str, - default=self.options.default_runner, - ) - runner = cast(str, env_conf["runner"]) - from tox.tox_env.register import REGISTER - - builder = REGISTER.runner(runner) - name = env_conf.name - journal = self.journal.get_env_journal(name) - args = ToxEnvCreateArgs(env_conf, self.conf.core, self.options, journal, self.log_handler) - env: RunToxEnv = builder(args) - self._run_env[name] = env - self._build_package_env(env) + """Runtime state holder.""" - def _build_package_env(self, env: RunToxEnv) -> None: - pkg_info = env.get_package_env_types() - if pkg_info is not None: - name, core_type = pkg_info - env.package_env = self._build_pkg_env(name, core_type, env) - - def _build_pkg_env(self, name: str, core_type: str, env: RunToxEnv) -> PackageToxEnv: - with self.log_handler.with_context(name): - package_tox_env = self._get_package_env(core_type, name) - - child_package_envs = package_tox_env.register_run_env(env) - try: - child_name, child_type = next(child_package_envs) - while True: - child_pkg_env = self._build_pkg_env(child_name, child_type, env) - child_name, child_type = child_package_envs.send(child_pkg_env) - except StopIteration: - pass - return package_tox_env - - def _get_package_env(self, packager: str, name: str) -> PackageToxEnv: - if name in self._pkg_env: # if already created reuse - old, pkg_tox_env = self._pkg_env[name] - if old != packager: # pragma: no branch # same env name is used by different packaging: dpkg vs virtualenv - msg = f"{name} is already defined as a {old}, cannot be {packager} too" # pragma: no cover - raise HandledError(msg) # pragma: no cover - else: - from tox.tox_env.register import REGISTER - - package_type = REGISTER.package(packager) - self._pkg_env_discovered.add(name) - if name in self._run_env: - raise HandledError(f"{name} is already defined as a run environment, cannot be packaging too") - pkg_conf = self.conf.get_env(name, package=True) - journal = self.journal.get_env_journal(name) - args = ToxEnvCreateArgs(pkg_conf, self.conf.core, self.options, journal, self.log_handler) - pkg_tox_env = package_type(args) - self._pkg_env[name] = packager, pkg_tox_env - return pkg_tox_env - - def created_run_envs(self) -> Iterator[tuple[str, RunToxEnv]]: - yield from self._run_env.items() - - def all_run_envs(self, *, with_skip: bool = True) -> Iterator[str]: - default_env_list = self.conf.core["env_list"] - ignore = {self.conf.core["provision_tox_env"]} - for env in chain(default_env_list.envs, self.conf.env_list(everything=True)): - if env in ignore: - continue - ignore.add(env) # ignore self - skip = False - try: - tox_env = self.tox_env(env) - except Skip: - skip = True - tox_env = self.tox_env(env) - ignore.update(i.name for i in tox_env.package_envs) # ignore package environments - if not skip or with_skip: - yield env + def __init__(self, options: Options, args: Sequence[str]) -> None: + self.conf = Config.make(options.parsed, options.pos_args, options.source) + self._options = options + self.args = args + self._journal: Journal = Journal(getattr(options.parsed, "result_json", None) is not None) + self._selector: EnvSelector | None = None + + @property + def envs(self) -> EnvSelector: + """:return: provides access to the tox environments""" + if self._selector is None: + self._selector = EnvSelector(self) + return self._selector @impl diff --git a/src/tox/tox_env/api.py b/src/tox/tox_env/api.py index b84bd42f..b008a3c4 100644 --- a/src/tox/tox_env/api.py +++ b/src/tox/tox_env/api.py @@ -12,7 +12,7 @@ from abc import ABC, abstractmethod from contextlib import contextmanager from io import BytesIO from pathlib import Path -from typing import TYPE_CHECKING, Any, Iterator, List, NamedTuple, Sequence, cast +from typing import TYPE_CHECKING, Any, Iterator, List, NamedTuple, Sequence, Set, cast from tox.config.main import Config from tox.config.set_env import SetEnv @@ -68,7 +68,10 @@ class ToxEnv(ABC): self._log_id = 0 self.register_config() - self.cache = Info(self.env_dir) + + @property + def cache(self) -> Info: + return Info(self.env_dir) @staticmethod @abstractmethod @@ -95,6 +98,12 @@ class ToxEnv(ABC): value=self.conf.name, ) self.conf.add_config( + keys=["labels"], + of_type=Set[str], + default=set(), + desc="labels attached to the tox environment", + ) + self.conf.add_config( keys=["env_dir", "envdir"], of_type=Path, default=lambda conf, name: cast(Path, conf.core["work_dir"]) / self.name, # noqa: U100 diff --git a/src/tox/tox_env/info.py b/src/tox/tox_env/info.py index 5ba8a8c3..1371a7a2 100644 --- a/src/tox/tox_env/info.py +++ b/src/tox/tox_env/info.py @@ -11,6 +11,8 @@ from typing import Any, Iterator class Info: + """Stores metadata about the tox environment.""" + def __init__(self, path: Path) -> None: self._path = path / ".tox-info.json" try: @@ -29,7 +31,14 @@ class Info: section: str, sub_section: str | None = None, ) -> Iterator[tuple[bool, Any | None]]: - """Cache""" + """ + Compare new information with the existing one and update if differs. + + :param value: the value stored + :param section: the primary key of the information + :param sub_section: the secondary key of the information + :return: a tuple where the first value is if it differs and the second is the old value + """ old = self._content.get(section) if sub_section is not None and old is not None: old = old.get(sub_section) diff --git a/src/tox/tox_env/package.py b/src/tox/tox_env/package.py index d8b3aab8..a697e19f 100644 --- a/src/tox/tox_env/package.py +++ b/src/tox/tox_env/package.py @@ -58,6 +58,13 @@ class PackageToxEnv(ToxEnv, ABC): def perform_packaging(self, for_env: EnvConfigSet) -> list[Package]: raise NotImplementedError + def register_run_env(self, run_env: RunToxEnv) -> Generator[tuple[str, str], PackageToxEnv, None]: # noqa: U100 + yield from () # empty generator by default + + def mark_active_run_env(self, run_env: RunToxEnv) -> None: + with self._lock: + self._envs.add(run_env.conf.name) + def teardown_env(self, conf: EnvConfigSet) -> None: with self._lock: self._envs.remove(conf.name) @@ -65,12 +72,6 @@ class PackageToxEnv(ToxEnv, ABC): if not has_envs: self._teardown() - def register_run_env(self, run_env: RunToxEnv) -> Generator[tuple[str, str], PackageToxEnv, None]: - with self._lock: - self._envs.add(run_env.conf.name) - return - yield # make this a generator - @abstractmethod def child_pkg_envs(self, run_conf: EnvConfigSet) -> Iterator[PackageToxEnv]: raise NotImplementedError diff --git a/src/tox/tox_env/python/package.py b/src/tox/tox_env/python/package.py index a31cf830..ca833339 100644 --- a/src/tox/tox_env/python/package.py +++ b/src/tox/tox_env/python/package.py @@ -11,6 +11,7 @@ from packaging.requirements import Requirement from ...config.sets import EnvConfigSet from ..api import ToxEnvCreateArgs +from ..errors import Skip from ..package import Package, PackageToxEnv, PathPackage from ..runner import RunToxEnv from .api import Python @@ -47,9 +48,6 @@ class PythonPackageToxEnv(Python, PackageToxEnv, ABC): self._wheel_build_envs: dict[str, PythonPackageToxEnv] = {} super().__init__(create_args) - def register_config(self) -> None: - super().register_config() - def _setup_env(self) -> None: """setup the tox environment""" super()._setup_env() @@ -74,7 +72,8 @@ class PythonPackageToxEnv(Python, PackageToxEnv, ABC): # https://github.com/pypa/wheel/blob/master/src/wheel/bdist_wheel.py#L234-L280 run_py = cast(Python, run_env).base_python if run_py is None: - raise ValueError(f"could not resolve base python for {self.conf.name}") + base = ",".join(run_env.conf["base_python"]) + raise Skip(f"could not resolve base python with {base}") default_pkg_py = self.base_python if ( diff --git a/src/tox/tox_env/python/pip/req/file.py b/src/tox/tox_env/python/pip/req/file.py index 0118b3dc..4ad4ae06 100644 --- a/src/tox/tox_env/python/pip/req/file.py +++ b/src/tox/tox_env/python/pip/req/file.py @@ -263,7 +263,7 @@ class RequirementsFile: def _handle_requirement_line(line: ParsedLine) -> ParsedRequirement: # For editable requirements, we don't support per-requirement options, so just return the parsed requirement. # get the options that apply to requirements - req_options = {} + req_options: dict[str, Any] = {} if line.is_editable: req_options["is_editable"] = line.is_editable if line.constraint: diff --git a/src/tox/tox_env/python/virtual_env/package/pep517.py b/src/tox/tox_env/python/virtual_env/package/pep517.py index caf021f9..6bf5aa88 100644 --- a/src/tox/tox_env/python/virtual_env/package/pep517.py +++ b/src/tox/tox_env/python/virtual_env/package/pep517.py @@ -74,12 +74,12 @@ class Pep517VirtualEnvPackager(PythonPackageToxEnv, VirtualEnv): def __init__(self, create_args: ToxEnvCreateArgs) -> None: super().__init__(create_args) - self.root: Path = self.conf["package_root"] - self._frontend_private: Pep517VirtualEnvFrontend | None = None + self._frontend_: Pep517VirtualEnvFrontend | None = None self.builds: set[str] = set() self._distribution_meta: PathDistribution | None = None self._package_dependencies: list[Requirement] | None = None self._pkg_lock = RLock() # can build only one package at a time + self.root = self.conf["package_root"] @staticmethod def id() -> str: @@ -87,9 +87,9 @@ class Pep517VirtualEnvPackager(PythonPackageToxEnv, VirtualEnv): @property def _frontend(self) -> Pep517VirtualEnvFrontend: - if self._frontend_private is None: - self._frontend_private = Pep517VirtualEnvFrontend(self.root, self) - return self._frontend_private + if self._frontend_ is None: + self._frontend_ = Pep517VirtualEnvFrontend(self.root, self) + return self._frontend_ def register_config(self) -> None: super().register_config() diff --git a/src/tox/tox_env/runner.py b/src/tox/tox_env/runner.py index adde85a4..ff3051a6 100644 --- a/src/tox/tox_env/runner.py +++ b/src/tox/tox_env/runner.py @@ -20,6 +20,7 @@ class RunToxEnv(ToxEnv, ABC): self.package_env: PackageToxEnv | None = None self._packages: list[Package] = [] super().__init__(create_args) + self._package_envs: list[PackageToxEnv | Exception] | None = None def register_config(self) -> None: def ensure_one_line(value: str) -> str: @@ -81,6 +82,16 @@ class RunToxEnv(ToxEnv, ABC): default=False, desc="if set to true a failing result of this testenv will not make tox fail (instead just warn)", ) + + def _teardown(self) -> None: + super()._teardown() + self._call_pkg_envs("teardown_env", self.conf) + + def interrupt(self) -> None: + super().interrupt() + self._call_pkg_envs("interrupt") + + def get_package_env_types(self) -> tuple[str, str] | None: has_external_pkg = getattr(self.options, "install_pkg", None) is not None if self._register_package_conf() or has_external_pkg: has_external_pkg = has_external_pkg or self.conf["package"] == "external" @@ -102,19 +113,8 @@ class RunToxEnv(ToxEnv, ABC): desc="tox package type used to generate the package", value=self._external_pkg_tox_env_type if is_external else self._package_tox_env_type, ) - - def _teardown(self) -> None: - super()._teardown() - self._call_pkg_envs("teardown_env", self.conf) - - def interrupt(self) -> None: - super().interrupt() - self._call_pkg_envs("interrupt") - - def get_package_env_types(self) -> tuple[str, str] | None: - if "package_env" not in self.conf: - return None - return self.conf["package_env"], self.conf["package_tox_env_type"] + return self.conf["package_env"], self.conf["package_tox_env_type"] + return None def _call_pkg_envs(self, method_name: str, *args: Any) -> None: for package_env in self.package_envs: @@ -209,3 +209,7 @@ class RunToxEnv(ToxEnv, ABC): if self.package_env is not None: yield self.package_env yield from self.package_env.child_pkg_envs(self.conf) + + def mark_active(self) -> None: + for pkg_env in self.package_envs: + pkg_env.mark_active_run_env(self) diff --git a/tests/config/cli/test_cli_env_var.py b/tests/config/cli/test_cli_env_var.py index c69ce238..4106e104 100644 --- a/tests/config/cli/test_cli_env_var.py +++ b/tests/config/cli/test_cli_env_var.py @@ -7,7 +7,7 @@ import pytest from tox.config.cli.parse import get_options from tox.config.loader.api import Override from tox.pytest import CaptureFixture, LogCaptureFixture, MonkeyPatch -from tox.session.common import CliEnv +from tox.session.env_select import CliEnv from tox.session.state import State @@ -59,6 +59,8 @@ def test_verbose_no_test() -> None: "parallel_no_spinner": False, "pre": False, "index_url": [], + "factors": [], + "labels": [], } @@ -77,8 +79,8 @@ def test_env_var_exhaustive_parallel_values( monkeypatch.setenv("TOX_PARALLEL_LIVE", "no") monkeypatch.setenv("TOX_OVERRIDE", "a=b\nc=d") - parsed, handlers, _, __, ___ = get_options() - assert vars(parsed) == { + options = get_options() + assert vars(options.parsed) == { "always_copy": False, "colored": "no", "command": "legacy", @@ -114,9 +116,11 @@ def test_env_var_exhaustive_parallel_values( "work_dir": None, "root_dir": None, "config_file": None, + "factors": [], + "labels": [], } - assert parsed.verbosity == 4 - assert handlers == core_handlers + assert options.parsed.verbosity == 4 + assert options.cmd_handlers == core_handlers def test_ini_help(monkeypatch: MonkeyPatch, capsys: CaptureFixture) -> None: diff --git a/tests/config/cli/test_cli_ini.py b/tests/config/cli/test_cli_ini.py index 0d72d249..f97927d4 100644 --- a/tests/config/cli/test_cli_ini.py +++ b/tests/config/cli/test_cli_ini.py @@ -12,7 +12,7 @@ from pytest_mock import MockerFixture from tox.config.cli.parse import get_options from tox.config.loader.api import Override from tox.pytest import CaptureFixture, LogCaptureFixture, MonkeyPatch -from tox.session.common import CliEnv +from tox.session.env_select import CliEnv from tox.session.state import State @@ -56,14 +56,14 @@ def test_ini_empty( monkeypatch.setenv("TOX_CONFIG_FILE", str(to)) to.write_text(content) mocker.patch("tox.config.cli.parse.discover_source", return_value=mocker.MagicMock(path=Path())) - parsed, handlers, _, __, ___ = get_options("r") - assert vars(parsed) == default_options - assert parsed.verbosity == 2 - assert handlers == core_handlers + options = get_options("r") + assert vars(options.parsed) == default_options + assert options.parsed.verbosity == 2 + assert options.cmd_handlers == core_handlers to.unlink() - missing_parsed, ____, _, __, ___ = get_options("r") - assert vars(missing_parsed) == vars(parsed) + missing_options = get_options("r") + assert vars(missing_options.parsed) == vars(options.parsed) @pytest.fixture() @@ -92,12 +92,14 @@ def default_options(tmp_path: Path) -> dict[str, Any]: "work_dir": None, "root_dir": None, "config_file": (tmp_path / "tox.ini").absolute(), + "factors": [], + "labels": [], } def test_ini_exhaustive_parallel_values(exhaustive_ini: Path, core_handlers: dict[str, Callable[[State], int]]) -> None: - parsed, handlers, _, __, ___ = get_options("p") - assert vars(parsed) == { + options = get_options("p") + assert vars(options.parsed) == { "colored": "yes", "command": "p", "default_runner": "virtualenv", @@ -124,9 +126,11 @@ def test_ini_exhaustive_parallel_values(exhaustive_ini: Path, core_handlers: dic "work_dir": None, "root_dir": None, "config_file": exhaustive_ini, + "factors": [], + "labels": [], } - assert parsed.verbosity == 4 - assert handlers == core_handlers + assert options.parsed.verbosity == 4 + assert options.cmd_handlers == core_handlers def test_ini_help(exhaustive_ini: Path, capsys: CaptureFixture) -> None: @@ -148,7 +152,7 @@ def test_bad_cli_ini( mocker.patch("tox.config.cli.parse.discover_source", return_value=mocker.MagicMock(path=Path())) caplog.set_level(logging.WARNING) monkeypatch.setenv("TOX_CONFIG_FILE", str(tmp_path)) - parsed, _, __, ___, ____ = get_options("r") + options = get_options("r") msg = ( "PermissionError(13, 'Permission denied')" if sys.platform == "win32" @@ -156,7 +160,7 @@ def test_bad_cli_ini( ) assert caplog.messages == [f"failed to read config file {tmp_path} because {msg}"] default_options["config_file"] = tmp_path - assert vars(parsed) == default_options + assert vars(options.parsed) == default_options def test_bad_option_cli_ini( diff --git a/tests/config/loader/ini/replace/conftest.py b/tests/config/loader/ini/replace/conftest.py index 1ef6d3cf..6025f839 100644 --- a/tests/config/loader/ini/replace/conftest.py +++ b/tests/config/loader/ini/replace/conftest.py @@ -27,8 +27,14 @@ 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, options=Parsed(override=[]), root=tmp_path, pos_args=pos_args, work_dir=tmp_path) - config.register_config_set = lambda name, env_config_set: None # type: ignore[assignment] # noqa: U100 + + 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] args = ConfigLoadArgs(chain=[], name="a", env_name="a") return loader.load(key="env", of_type=str, conf=config, factory=None, args=args) diff --git a/tests/config/source/test_discover.py b/tests/config/source/test_discover.py index 3ed16ba0..1ef7073d 100644 --- a/tests/config/source/test_discover.py +++ b/tests/config/source/test_discover.py @@ -8,7 +8,7 @@ from tox.pytest import ToxProjectCreator def out_no_src(path: Path) -> str: return ( f"ROOT: No tox.ini or setup.cfg or pyproject.toml found, assuming empty tox.ini at {path}\n" - f"additional environments:\npy -> [no description]\n" + f"default environments:\npy -> [no description]\n" ) diff --git a/tests/config/test_sets.py b/tests/config/test_sets.py index 8e72b636..ca577875 100644 --- a/tests/config/test_sets.py +++ b/tests/config/test_sets.py @@ -158,7 +158,7 @@ def test_define_custom_set(tox_project: ToxProjectCreator) -> None: exp = "MagicConfigSet(loaders=[IniLoader(section=magic, overrides={}), " "IniLoader(section=A, overrides={})])" assert repr(conf) == exp - assert isinstance(result.state.conf.options, Parsed) + assert isinstance(result.state.conf._options, Parsed) def test_do_not_allow_create_config_set(mocker: MockerFixture) -> None: diff --git a/tests/conftest.py b/tests/conftest.py index 8ceb4c66..81c4a493 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -54,12 +54,12 @@ def tox_ini_conf(tmp_path: Path, monkeypatch: MonkeyPatch) -> ToxIniCreator: with monkeypatch.context() as context: context.chdir(tmp_path) source = discover_source(config_file, None) + config = Config.make( Parsed(work_dir=dest, override=override or [], config_file=config_file, root_dir=None), pos_args=[], source=source, ) - config.register_config_set = lambda name, env_config_set: None # type: ignore[assignment] # noqa: U100 return config return func diff --git a/tests/execute/local_subprocess/bad_process.py b/tests/execute/local_subprocess/bad_process.py index babc6ad6..4870d058 100644 --- a/tests/execute/local_subprocess/bad_process.py +++ b/tests/execute/local_subprocess/bad_process.py @@ -12,7 +12,7 @@ from types import FrameType out = sys.stdout -def handler(signum: signal.Signals, _: FrameType) -> None: # noqa: U101 +def handler(signum: int, _: FrameType | None) -> None: # noqa: U101 _p(f"how about no signal {signum!r}") diff --git a/tests/execute/local_subprocess/local_subprocess_sigint.py b/tests/execute/local_subprocess/local_subprocess_sigint.py index b4e2ee72..8bee5fe2 100644 --- a/tests/execute/local_subprocess/local_subprocess_sigint.py +++ b/tests/execute/local_subprocess/local_subprocess_sigint.py @@ -37,7 +37,7 @@ def show_outcome(outcome: Outcome | None) -> None: print("done show outcome", file=sys.stderr) -def handler(s: signal.Signals, f: FrameType) -> None: +def handler(s: int, f: FrameType | None) -> None: logging.info(f"signal {s} at {f}") global interrupt_done if interrupt_done is False: # pragma: no branch diff --git a/tests/execute/local_subprocess/test_local_subprocess.py b/tests/execute/local_subprocess/test_local_subprocess.py index a6f406e6..fa3b43a3 100644 --- a/tests/execute/local_subprocess/test_local_subprocess.py +++ b/tests/execute/local_subprocess/test_local_subprocess.py @@ -187,12 +187,14 @@ def test_local_execute_basic_fail(capsys: CaptureFixture, caplog: LogCaptureFixt record = caplog.records[0] assert record.levelno == logging.CRITICAL assert record.msg == "exit %s (%.2f seconds) %s> %s%s" + assert record.args is not None _code, _duration, _cwd, _cmd, _metadata = record.args assert _code == 3 assert _cwd == cwd assert _cmd == request.shell_cmd assert isinstance(_duration, float) assert _duration > 0 + assert isinstance(_metadata, str) assert _metadata.startswith(" pid=") diff --git a/tests/plugin/test_plugin.py b/tests/plugin/test_plugin.py index b05afa8a..358cec54 100644 --- a/tests/plugin/test_plugin.py +++ b/tests/plugin/test_plugin.py @@ -10,11 +10,11 @@ from pytest_mock import MockerFixture from tox.config.cli.parser import ToxParser from tox.config.loader.memory import MemoryLoader -from tox.config.main import Config from tox.config.sets import CoreConfigSet, EnvConfigSet from tox.execute import Outcome from tox.plugin import impl from tox.pytest import ToxProjectCreator, register_inline_plugin +from tox.session.state import State from tox.tox_env.api import ToxEnv from tox.tox_env.register import ToxEnvRegister @@ -31,15 +31,15 @@ def test_plugin_hooks_and_order(tox_project: ToxProjectCreator, mocker: MockerFi logging.warning("tox_add_option") @impl - def tox_add_core_config(core_conf: CoreConfigSet, config: Config) -> None: + def tox_add_core_config(core_conf: CoreConfigSet, state: State) -> None: assert isinstance(core_conf, CoreConfigSet) - assert isinstance(config, Config) + assert isinstance(state, State) logging.warning("tox_add_core_config") @impl - def tox_add_env_config(env_conf: EnvConfigSet, config: Config) -> None: + def tox_add_env_config(env_conf: EnvConfigSet, state: State) -> None: assert isinstance(env_conf, EnvConfigSet) - assert isinstance(config, Config) + assert isinstance(state, State) logging.warning("tox_add_env_config") @impl @@ -86,9 +86,9 @@ def test_plugin_hooks_and_order(tox_project: ToxProjectCreator, mocker: MockerFi def test_plugin_can_read_env_list(tox_project: ToxProjectCreator, mocker: MockerFixture) -> None: @impl - def tox_add_core_config(core_conf: CoreConfigSet, config: Config) -> None: # noqa: U100 - logging.warning("All envs: %s", ", ".join(config.env_list(everything=True))) - logging.warning("Default envs: %s", ", ".join(config.env_list())) + def tox_add_core_config(core_conf: CoreConfigSet, state: State) -> None: # noqa: U100 + logging.warning("All envs: %s", ", ".join(state.envs.iter(only_active=False))) + logging.warning("Default envs: %s", ", ".join(state.envs.iter(only_active=True))) register_inline_plugin(mocker, tox_add_core_config) ini = """ @@ -108,8 +108,8 @@ def test_plugin_can_read_env_list(tox_project: ToxProjectCreator, mocker: Mocker def test_plugin_can_read_sections(tox_project: ToxProjectCreator, mocker: MockerFixture) -> None: @impl - def tox_add_core_config(core_conf: CoreConfigSet, config: Config) -> None: # noqa: U100 - logging.warning("Sections: %s", ", ".join(i.key for i in config.sections())) + def tox_add_core_config(core_conf: CoreConfigSet, state: State) -> None: # noqa: U100 + logging.warning("Sections: %s", ", ".join(i.key for i in state.conf.sections())) register_inline_plugin(mocker, tox_add_core_config) ini = """ @@ -127,7 +127,7 @@ def test_plugin_can_read_sections(tox_project: ToxProjectCreator, mocker: Mocker def test_plugin_injects_invalid_python_run(tox_project: ToxProjectCreator, mocker: MockerFixture) -> None: @impl - def tox_add_env_config(env_conf: EnvConfigSet, config: Config) -> None: # noqa: U100 + def tox_add_env_config(env_conf: EnvConfigSet, state: State) -> None: # noqa: U100 env_conf.loaders.insert(0, MemoryLoader(deps=[1])) with pytest.raises(TypeError, match="1"): assert env_conf["deps"] @@ -141,7 +141,7 @@ def test_plugin_injects_invalid_python_run(tox_project: ToxProjectCreator, mocke def test_plugin_extend_pass_env(tox_project: ToxProjectCreator, mocker: MockerFixture) -> None: @impl - def tox_add_env_config(env_conf: EnvConfigSet, config: Config) -> None: # noqa: U100 + def tox_add_env_config(env_conf: EnvConfigSet, state: State) -> None: # noqa: U100 env_conf["pass_env"].append("MAGIC_*") register_inline_plugin(mocker, tox_add_env_config) @@ -164,7 +164,7 @@ def test_plugin_extend_pass_env(tox_project: ToxProjectCreator, mocker: MockerFi def test_plugin_extend_set_env(tox_project: ToxProjectCreator, mocker: MockerFixture) -> None: @impl - def tox_add_env_config(env_conf: EnvConfigSet, config: Config) -> None: # noqa: U100 + def tox_add_env_config(env_conf: EnvConfigSet, state: State) -> None: # noqa: U100 env_conf["set_env"].update({"MAGI_CAL": "magi_cal"}) register_inline_plugin(mocker, tox_add_env_config) diff --git a/tests/plugin/test_plugin_custom_config_set.py b/tests/plugin/test_plugin_custom_config_set.py index 7d0783b2..dc197469 100644 --- a/tests/plugin/test_plugin_custom_config_set.py +++ b/tests/plugin/test_plugin_custom_config_set.py @@ -8,10 +8,10 @@ import pytest from pytest_mock import MockerFixture from tox.config.loader.section import Section -from tox.config.main import Config from tox.config.sets import ConfigSet, EnvConfigSet from tox.plugin import impl from tox.pytest import ToxProjectCreator, register_inline_plugin +from tox.session.state import State from tox.tox_env.api import ToxEnv @@ -22,11 +22,11 @@ def _custom_config_set(mocker: MockerFixture) -> None: self.add_config(keys="A", of_type=int, default=0, desc="a config") @impl - def tox_add_env_config(env_conf: EnvConfigSet, config: Config) -> None: + def tox_add_env_config(env_conf: EnvConfigSet, state: State) -> None: def factory(for_env: str, raw: object) -> DockerConfigSet: assert isinstance(raw, str) section = Section("docker", raw) - conf_set = config.get_section_config(section, base=["docker"], of_type=DockerConfigSet, for_env=for_env) + conf_set = state.conf.get_section_config(section, base=["docker"], of_type=DockerConfigSet, for_env=for_env) return conf_set env_conf.add_config( diff --git a/tests/pytest_/test_init.py b/tests/pytest_/test_init.py index a433a048..1bb4eed4 100644 --- a/tests/pytest_/test_init.py +++ b/tests/pytest_/test_init.py @@ -100,7 +100,7 @@ def test_tox_run_outcome_repr(tox_project: ToxProjectCreator) -> None: cmd: {sys.executable} -m tox l cwd: {project.path} standard output - additional environments: + default environments: py -> [no description] """, ).lstrip() diff --git a/tests/session/cmd/test_depends.py b/tests/session/cmd/test_depends.py index b6c7b44a..6b27d3bc 100644 --- a/tests/session/cmd/test_depends.py +++ b/tests/session/cmd/test_depends.py @@ -1,20 +1,14 @@ from __future__ import annotations import sys +from textwrap import dedent from typing import Callable -import pytest - from tox.pytest import ToxProjectCreator -@pytest.mark.parametrize("has_prev", [True, False]) -def test_depends( - tox_project: ToxProjectCreator, - patch_prev_py: Callable[[bool], tuple[str, str]], - has_prev: bool, -) -> None: - prev_ver, impl = patch_prev_py(has_prev) +def test_depends(tox_project: ToxProjectCreator, patch_prev_py: Callable[[bool], tuple[str, str]]) -> None: + prev_ver, impl = patch_prev_py(True) # has previous python ver = sys.version_info[0:2] py = f"py{''.join(str(i) for i in ver)}" prev_py = f"py{prev_ver}" @@ -33,32 +27,29 @@ def test_depends( project = tox_project({"tox.ini": ini, "pyproject.toml": ""}) outcome = project.run("de") outcome.assert_success() - lines = outcome.out.splitlines() - assert lines[0] == f"Execution order: py, {py},{f' {prev_py},' if has_prev else '' } cov, cov2" - expected_lines = [ - "ALL", - " py ~ .pkg", - f" {py} ~ .pkg", - ] - if has_prev: - expected_lines.append(f" {prev_py} ~ .pkg | .pkg-{impl}{prev_ver}") - expected_lines.extend( - [ - " cov2", - " cov", - " py ~ .pkg", - f" {py} ~ .pkg", - ], - ) - if has_prev: - expected_lines.append(f" {prev_py} ~ .pkg | .pkg-{impl}{prev_ver}") - expected_lines.extend( - [ - " cov", - " py ~ .pkg", - f" {py} ~ .pkg", - ], - ) - if has_prev: - expected_lines.append(f" {prev_py} ~ .pkg | .pkg-{impl}{prev_ver}") - assert lines[1:] == expected_lines + + expected = f""" + Execution order: py, {py}, {prev_py}, py31, cov, cov2 + ALL + py ~ .pkg + {py} ~ .pkg + {prev_py} ~ .pkg | .pkg-{impl}{prev_ver} + py31 ~ .pkg | ... (could not resolve base python with py31) + cov2 + cov + py ~ .pkg + {py} ~ .pkg + {prev_py} ~ .pkg | .pkg-{impl}{prev_ver} + py31 ~ .pkg | ... (could not resolve base python with py31) + cov + py ~ .pkg + {py} ~ .pkg + {prev_py} ~ .pkg | .pkg-{impl}{prev_ver} + py31 ~ .pkg | ... (could not resolve base python with py31) + """ + assert outcome.out == dedent(expected).lstrip() + + +def test_depends_help(tox_project: ToxProjectCreator) -> None: + outcome = tox_project({"tox.ini": ""}).run("de", "-h") + outcome.assert_success() diff --git a/tests/session/cmd/test_devenv.py b/tests/session/cmd/test_devenv.py index 4b8c4d04..42a3090d 100644 --- a/tests/session/cmd/test_devenv.py +++ b/tests/session/cmd/test_devenv.py @@ -17,3 +17,8 @@ def test_devenv_ok(tox_project: ToxProjectCreator, enable_pip_pypi_access: str | content = {"setup.py": "from setuptools import setup\nsetup(name='demo', version='1.0')"} outcome = tox_project(content).run("d", "-e", "py") outcome.assert_success() + + +def test_devenv_help(tox_project: ToxProjectCreator) -> None: + outcome = tox_project({"tox.ini": ""}).run("d", "-h") + outcome.assert_success() diff --git a/tests/session/cmd/test_exec_.py b/tests/session/cmd/test_exec_.py index 0c6a18b8..1011ab61 100644 --- a/tests/session/cmd/test_exec_.py +++ b/tests/session/cmd/test_exec_.py @@ -32,3 +32,8 @@ def test_exec(tox_project: ToxProjectCreator, exit_code: int) -> None: else: outcome.assert_success() assert sys.version in outcome.out + + +def test_exec_help(tox_project: ToxProjectCreator) -> None: + outcome = tox_project({"tox.ini": ""}).run("e", "-h") + outcome.assert_success() diff --git a/tests/session/cmd/test_legacy.py b/tests/session/cmd/test_legacy.py index fc9b1219..8ec82939 100644 --- a/tests/session/cmd/test_legacy.py +++ b/tests/session/cmd/test_legacy.py @@ -14,8 +14,8 @@ def test_legacy_show_config(tox_project: ToxProjectCreator, mocker: MockerFixtur outcome = tox_project({"tox.ini": ""}).run("le", "--showconfig") assert show_config.call_count == 1 - assert outcome.state.options.list_keys_only == [] - assert outcome.state.options.show_core is True + assert outcome.state.conf.options.list_keys_only == [] + assert outcome.state.conf.options.show_core is True @pytest.mark.parametrize("verbose", range(3)) @@ -25,9 +25,9 @@ def test_legacy_list_default(tox_project: ToxProjectCreator, mocker: MockerFixtu outcome = tox_project({"tox.ini": ""}).run("le", "-l", *(["-v"] * verbose)) assert list_env.call_count == 1 - assert outcome.state.options.list_no_description is (verbose < 1) - assert outcome.state.options.list_default_only is True - assert outcome.state.options.show_core is False + assert outcome.state.conf.options.list_no_description is (verbose < 1) + assert outcome.state.conf.options.list_default_only is True + assert outcome.state.conf.options.show_core is False @pytest.mark.parametrize( @@ -62,9 +62,9 @@ def test_legacy_list_all(tox_project: ToxProjectCreator, mocker: MockerFixture, outcome = tox_project({"tox.ini": ""}).run("le", "-a", *(["-v"] * verbose)) assert list_env.call_count == 1 - assert outcome.state.options.list_no_description is (verbose < 1) - assert outcome.state.options.list_default_only is False - assert outcome.state.options.show_core is False + assert outcome.state.conf.options.list_no_description is (verbose < 1) + assert outcome.state.conf.options.list_default_only is False + assert outcome.state.conf.options.show_core is False def test_legacy_devenv(tox_project: ToxProjectCreator, mocker: MockerFixture, tmp_path: Path) -> None: @@ -74,7 +74,7 @@ def test_legacy_devenv(tox_project: ToxProjectCreator, mocker: MockerFixture, tm outcome = tox_project({"tox.ini": ""}).run("le", "--devenv", str(into), "-e", "py") assert devenv.call_count == 1 - assert outcome.state.options.devenv_path == into + assert outcome.state.conf.options.devenv_path == into def test_legacy_run_parallel(tox_project: ToxProjectCreator, mocker: MockerFixture) -> None: @@ -91,3 +91,8 @@ def test_legacy_run_sequential(tox_project: ToxProjectCreator, mocker: MockerFix tox_project({"tox.ini": ""}).run("le", "-e", "py") assert run_sequential.call_count == 1 + + +def test_legacy_help(tox_project: ToxProjectCreator) -> None: + outcome = tox_project({"tox.ini": ""}).run("le", "-h") + outcome.assert_success() diff --git a/tests/session/cmd/test_list_envs.py b/tests/session/cmd/test_list_envs.py index 27eef749..3d9b693a 100644 --- a/tests/session/cmd/test_list_envs.py +++ b/tests/session/cmd/test_list_envs.py @@ -76,3 +76,44 @@ def test_list_env_quiet_default(project: ToxProject) -> None: py """ outcome.assert_out_err(expected, "") + + +def test_list_env_package_env_before_run(tox_project: ToxProjectCreator) -> None: + ini = """ + [testenv:pkg] + [testenv:run] + package = wheel + wheel_build_env = pkg + """ + project = tox_project({"tox.ini": ini}) + outcome = project.run("l") + + outcome.assert_success() + expected = """ + default environments: + py -> [no description] + + additional environments: + run -> [no description] + """ + outcome.assert_out_err(expected, "") + + +def test_list_env_package_self(tox_project: ToxProjectCreator) -> None: + ini = """ + [tox] + env_list = pkg + [testenv:pkg] + package = wheel + wheel_build_env = pkg + """ + project = tox_project({"tox.ini": ini}) + outcome = project.run("l") + + outcome.assert_failed() + assert outcome.out.splitlines() == ["ROOT: HandledError| pkg cannot self-package"] + + +def test_list_envs_help(tox_project: ToxProjectCreator) -> None: + outcome = tox_project({"tox.ini": ""}).run("l", "-h") + outcome.assert_success() diff --git a/tests/session/cmd/test_parallel.py b/tests/session/cmd/test_parallel.py index dc4e21e6..f3152291 100644 --- a/tests/session/cmd/test_parallel.py +++ b/tests/session/cmd/test_parallel.py @@ -146,3 +146,8 @@ def test_keyboard_interrupt(tox_project: ToxProjectCreator, demo_pkg_inline: Pat assert "send signal SIGINT" in out, out assert "interrupt finished with success" in out, out assert "interrupt tox environment: .pkg" in out, out + + +def test_parallels_help(tox_project: ToxProjectCreator) -> None: + outcome = tox_project({"tox.ini": ""}).run("p", "-h") + outcome.assert_success() diff --git a/tests/session/cmd/test_quickstart.py b/tests/session/cmd/test_quickstart.py index 67087059..2ccc290d 100644 --- a/tests/session/cmd/test_quickstart.py +++ b/tests/session/cmd/test_quickstart.py @@ -45,3 +45,8 @@ def test_quickstart_refuse(tox_project: ToxProjectCreator) -> None: outcome = project.run("q", str(project.path)) outcome.assert_failed(code=1) assert "tox.ini already exist, refusing to overwrite" in outcome.out + + +def test_quickstart_help(tox_project: ToxProjectCreator) -> None: + outcome = tox_project({"tox.ini": ""}).run("q", "-h") + outcome.assert_success() diff --git a/tests/session/cmd/test_sequential.py b/tests/session/cmd/test_sequential.py index dc97db0f..9fb29578 100644 --- a/tests/session/cmd/test_sequential.py +++ b/tests/session/cmd/test_sequential.py @@ -251,7 +251,7 @@ def test_env_name_change_recreate(tox_project: ToxProjectCreator) -> None: result_first = proj.run("r") result_first.assert_success() - tox_env = result_first.state.tox_env("py") + tox_env = result_first.state.envs["py"] assert repr(tox_env) == "VirtualEnvRunner(name=py)" path = tox_env.env_dir with Info(path).compare({"name": "p", "type": "magical"}, ToxEnv.__name__): @@ -406,3 +406,8 @@ def test_virtualenv_cache(tox_project: ToxProjectCreator) -> None: result_second = proj.run("r", "-v", "-v") result_second.assert_success() assert " create virtual environment via " not in result_second.out + + +def test_sequential_help(tox_project: ToxProjectCreator) -> None: + outcome = tox_project({"tox.ini": ""}).run("r", "-h") + outcome.assert_success() diff --git a/tests/session/cmd/test_show_config.py b/tests/session/cmd/test_show_config.py index 0d751ba3..55e4acb4 100644 --- a/tests/session/cmd/test_show_config.py +++ b/tests/session/cmd/test_show_config.py @@ -21,7 +21,7 @@ def test_show_config_default_run_env(tox_project: ToxProjectCreator, monkeypatch result = project.run("c", "-e", name, "--core", "--", "magic") state = result.state assert state.args == ("c", "-e", name, "--core", "--", "magic") - outcome = list(state.conf.env_list(everything=True)) + outcome = list(state.envs.iter(only_active=False)) assert outcome == [name] monkeypatch.delenv("TERM", raising=False) # disable conditionally set flag parser = ConfigParser() @@ -211,3 +211,8 @@ def test_show_config_timeout_custom(tox_project: ToxProjectCreator) -> None: result = project.run("c", "-e", "py", "-k", "suicide_timeout", "interrupt_timeout", "terminate_timeout") expected = "[testenv:py]\nsuicide_timeout = 1.0\ninterrupt_timeout = 2.222\nterminate_timeout = 3.0\n" assert result.out == expected + + +def test_show_config_help(tox_project: ToxProjectCreator) -> None: + outcome = tox_project({"tox.ini": ""}).run("c", "-h") + outcome.assert_success() diff --git a/tests/session/cmd/test_state.py b/tests/session/cmd/test_state.py index 1a4c5081..d05ad875 100644 --- a/tests/session/cmd/test_state.py +++ b/tests/session/cmd/test_state.py @@ -12,11 +12,11 @@ def test_env_already_packaging(tox_project: ToxProjectCreator) -> None: ) result = proj.run("r", "-e", "py,.pkg") result.assert_failed(code=-2) - assert "cannot run packaging environment .pkg" in result.out, result.out + assert "cannot run packaging environment(s) .pkg" in result.out, result.out def test_env_run_cannot_be_packaging_too(tox_project: ToxProjectCreator) -> None: proj = tox_project({"tox.ini": "[testenv]\npackage=wheel\npackage_env=py", "pyproject.toml": ""}) result = proj.run("r", "-e", "py") result.assert_failed(code=-2) - assert " py is already defined as a run environment, cannot be packaging too" in result.out, result.out + assert " py cannot self-package" in result.out, result.out diff --git a/tests/session/test_env_select.py b/tests/session/test_env_select.py new file mode 100644 index 00000000..90d72a43 --- /dev/null +++ b/tests/session/test_env_select.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +from tox.pytest import ToxProjectCreator + + +def test_label_core_can_define(tox_project: ToxProjectCreator) -> None: + ini = """ + [tox] + labels = + test = py3{10,9} + static = flake8, type + """ + project = tox_project({"tox.ini": ini}) + outcome = project.run("l", "--no-desc") + outcome.assert_success() + outcome.assert_out_err("py\npy310\npy39\nflake8\ntype\n", "") + + +def test_label_core_select(tox_project: ToxProjectCreator) -> None: + ini = """ + [tox] + labels = + test = py3{10,9} + static = flake8, type + """ + project = tox_project({"tox.ini": ini}) + outcome = project.run("l", "--no-desc", "-m", "test") + outcome.assert_success() + outcome.assert_out_err("py310\npy39\n", "") + + +def test_label_select_trait(tox_project: ToxProjectCreator) -> None: + ini = """ + [tox] + env_list = py310, py39, flake8, type + [testenv] + labels = test + [testenv:flake8] + labels = static + [testenv:type] + labels = static + """ + project = tox_project({"tox.ini": ini}) + outcome = project.run("l", "--no-desc", "-m", "test") + outcome.assert_success() + outcome.assert_out_err("py310\npy39\n", "") + + +def test_label_core_and_trait(tox_project: ToxProjectCreator) -> None: + ini = """ + [tox] + env_list = py310, py39, flake8, type + labels = + static = flake8, type + [testenv] + labels = test + """ + project = tox_project({"tox.ini": ini}) + outcome = project.run("l", "--no-desc", "-m", "test", "static") + outcome.assert_success() + outcome.assert_out_err("py310\npy39\nflake8\ntype\n", "") + + +def test_factor_select(tox_project: ToxProjectCreator) -> None: + ini = """ + [tox] + env_list = py3{10,9}-{django20,django21}{-cov,} + """ + project = tox_project({"tox.ini": ini}) + outcome = project.run("l", "--no-desc", "-f", "cov", "django20") + outcome.assert_success() + outcome.assert_out_err("py310-django20-cov\npy39-django20-cov\n", "") diff --git a/tests/session/test_session_common.py b/tests/session/test_session_common.py index 8ad72bbc..7d8df642 100644 --- a/tests/session/test_session_common.py +++ b/tests/session/test_session_common.py @@ -2,7 +2,7 @@ from __future__ import annotations import pytest -from tox.session.common import CliEnv +from tox.session.env_select import CliEnv @pytest.mark.parametrize( diff --git a/tests/test_run.py b/tests/test_run.py index 62ce77d8..ca36f41f 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -25,4 +25,4 @@ def test_re_raises_on_unexpected_exit(mocker: MockerFixture) -> None: def test_custom_work_dir(tox_project: ToxProjectCreator) -> None: project = tox_project({}) outcome = project.run("c", "--workdir", str(project.path.parent)) - assert outcome.state.options.work_dir == project.path.parent + assert outcome.state.conf.options.work_dir == project.path.parent diff --git a/tests/tox_env/python/pip/test_pip_install.py b/tests/tox_env/python/pip/test_pip_install.py index 4126c796..71dccd87 100644 --- a/tests/tox_env/python/pip/test_pip_install.py +++ b/tests/tox_env/python/pip/test_pip_install.py @@ -15,7 +15,7 @@ def test_pip_install_bad_type(tox_project: ToxProjectCreator, capfd: CaptureFixt proj = tox_project({"tox.ini": ""}) result = proj.run("l") result.assert_success() - pip = result.state.tox_env("py").installer + pip = result.state.envs["py"].installer with pytest.raises(SystemExit, match="1"): pip.install(arg, "section", "type") @@ -29,7 +29,7 @@ def test_pip_install_empty_list(tox_project: ToxProjectCreator) -> None: result = proj.run("l") result.assert_success() - pip = result.state.tox_env("py").installer + pip = result.state.envs["py"].installer execute_calls = proj.patch_execute(Mock()) pip.install([], "section", "type") assert execute_calls.call_count == 0 diff --git a/tests/tox_env/python/virtual_env/test_setuptools.py b/tests/tox_env/python/virtual_env/test_setuptools.py index 41e74dc7..71b981bb 100644 --- a/tests/tox_env/python/virtual_env/test_setuptools.py +++ b/tests/tox_env/python/virtual_env/test_setuptools.py @@ -3,12 +3,14 @@ from __future__ import annotations import os import sys from pathlib import Path +from typing import cast import pytest from tox.pytest import ToxProjectCreator from tox.tox_env.python.package import WheelPackage from tox.tox_env.python.virtual_env.package.pep517 import Pep517VirtualEnvPackager +from tox.tox_env.runner import RunToxEnv @pytest.mark.integration() @@ -30,7 +32,7 @@ def test_setuptools_package( outcome.assert_success() assert f"\ngreetings from demo_pkg_setuptools{os.linesep}" in outcome.out - tox_env = outcome.state.tox_env("py") + tox_env = cast(RunToxEnv, outcome.state.envs["py"]) (package_env,) = list(tox_env.package_envs) assert isinstance(package_env, Pep517VirtualEnvPackager) @@ -9,12 +9,10 @@ envlist = type docs pkg_meta -isolated_build = true skip_missing_interpreters = true -minversion = 3.21 [testenv] -description = run the tests with pytest +description = run the tests with pytest under {envname} passenv = PYTEST_* SSL_CERT_FILE @@ -51,7 +49,7 @@ description = run type check on code base setenv = {tty:MYPY_FORCE_COLOR = 1} deps = - mypy==0.910 + mypy==0.930 types-cachetools types-chardet types-freezegun diff --git a/whitelist.txt b/whitelist.txt index 67574999..ca1fffd1 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -59,7 +59,6 @@ doc2path docname docutils dotall -dpkg e3 e4 ebadf @@ -114,10 +113,8 @@ intersphinx isalpha isatty isspace +issubset iterdir -iwgrp -iwoth -iwusr kernel32 levelname levelno @@ -161,7 +158,6 @@ prog proj psutil purelib -py37 py38 py39 pygments @@ -219,13 +215,10 @@ trylast tty typehints typeshed -unbuffered unescaped -unimported unittest unlink unregister -untyped url2pathname usedevelop usefixtures |