summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBernát Gábor <bgabor8@bloomberg.net>2022-01-04 09:15:39 +0000
committerGitHub <noreply@github.com>2022-01-04 09:15:39 +0000
commitc88d497c535f62540637a437d5a0c71c335b86d2 (patch)
tree802209644b46e788aa6e656a0c2243d79d950908
parente62a717b8033a8f3ae556a7ea9183933f2d65a66 (diff)
downloadtox-git-c88d497c535f62540637a437d5a0c71c335b86d2.tar.gz
Better selection support (#2290)
-rw-r--r--.github/workflows/check.yml26
-rw-r--r--.pre-commit-config.yaml8
-rw-r--r--docs/changelog/2275.removal.rst3
-rw-r--r--docs/changelog/2290.feature.rst2
-rw-r--r--docs/changelog/238.feature.rst3
-rw-r--r--docs/conf.py11
-rw-r--r--docs/config.rst27
-rw-r--r--docs/plugins_api.rst14
-rw-r--r--src/tox/config/cli/parse.py23
-rw-r--r--src/tox/config/cli/parser.py21
-rw-r--r--src/tox/config/loader/memory.py8
-rw-r--r--src/tox/config/main.py41
-rw-r--r--src/tox/config/sets.py8
-rw-r--r--src/tox/config/types.py4
-rw-r--r--src/tox/plugin/manager.py15
-rw-r--r--src/tox/plugin/spec.py10
-rw-r--r--src/tox/provision.py59
-rw-r--r--src/tox/report.py4
-rw-r--r--src/tox/run.py15
-rw-r--r--src/tox/session/cmd/depends.py19
-rw-r--r--src/tox/session/cmd/devenv.py32
-rw-r--r--src/tox/session/cmd/exec_.py13
-rw-r--r--src/tox/session/cmd/legacy.py11
-rw-r--r--src/tox/session/cmd/list_env.py30
-rw-r--r--src/tox/session/cmd/quickstart.py2
-rw-r--r--src/tox/session/cmd/run/common.py61
-rw-r--r--src/tox/session/cmd/run/parallel.py11
-rw-r--r--src/tox/session/cmd/run/sequential.py4
-rw-r--r--src/tox/session/cmd/show_config.py48
-rw-r--r--src/tox/session/common.py50
-rw-r--r--src/tox/session/env_select.py336
-rw-r--r--src/tox/session/state.py147
-rw-r--r--src/tox/tox_env/api.py13
-rw-r--r--src/tox/tox_env/info.py11
-rw-r--r--src/tox/tox_env/package.py13
-rw-r--r--src/tox/tox_env/python/package.py7
-rw-r--r--src/tox/tox_env/python/pip/req/file.py2
-rw-r--r--src/tox/tox_env/python/virtual_env/package/pep517.py10
-rw-r--r--src/tox/tox_env/runner.py30
-rw-r--r--tests/config/cli/test_cli_env_var.py14
-rw-r--r--tests/config/cli/test_cli_ini.py30
-rw-r--r--tests/config/loader/ini/replace/conftest.py10
-rw-r--r--tests/config/source/test_discover.py2
-rw-r--r--tests/config/test_sets.py2
-rw-r--r--tests/conftest.py2
-rw-r--r--tests/execute/local_subprocess/bad_process.py2
-rw-r--r--tests/execute/local_subprocess/local_subprocess_sigint.py2
-rw-r--r--tests/execute/local_subprocess/test_local_subprocess.py2
-rw-r--r--tests/plugin/test_plugin.py26
-rw-r--r--tests/plugin/test_plugin_custom_config_set.py6
-rw-r--r--tests/pytest_/test_init.py2
-rw-r--r--tests/session/cmd/test_depends.py67
-rw-r--r--tests/session/cmd/test_devenv.py5
-rw-r--r--tests/session/cmd/test_exec_.py5
-rw-r--r--tests/session/cmd/test_legacy.py23
-rw-r--r--tests/session/cmd/test_list_envs.py41
-rw-r--r--tests/session/cmd/test_parallel.py5
-rw-r--r--tests/session/cmd/test_quickstart.py5
-rw-r--r--tests/session/cmd/test_sequential.py7
-rw-r--r--tests/session/cmd/test_show_config.py7
-rw-r--r--tests/session/cmd/test_state.py4
-rw-r--r--tests/session/test_env_select.py72
-rw-r--r--tests/session/test_session_common.py2
-rw-r--r--tests/test_run.py2
-rw-r--r--tests/tox_env/python/pip/test_pip_install.py4
-rw-r--r--tests/tox_env/python/virtual_env/test_setuptools.py4
-rw-r--r--tox.ini6
-rw-r--r--whitelist.txt9
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)
diff --git a/tox.ini b/tox.ini
index c4aff6ea..558d1c51 100644
--- a/tox.ini
+++ b/tox.ini
@@ -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