summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBernát Gábor <bgabor8@bloomberg.net>2021-01-11 11:08:30 +0000
committerGitHub <noreply@github.com>2021-01-11 11:08:30 +0000
commit4cf619a95b2ab14632d2fb6e62335d792da75c37 (patch)
tree49d705b392dea0ee00bd1f4163aeb780b7bb26ea /src
parent68cbc356ebc041f700c3c49a0f7f1ce522f7e66c (diff)
downloadtox-git-4cf619a95b2ab14632d2fb6e62335d792da75c37.tar.gz
Better handling of set_env (#1784)
Diffstat (limited to 'src')
-rw-r--r--src/tox/config/cli/ini.py2
-rw-r--r--src/tox/config/loader/api.py26
-rw-r--r--src/tox/config/loader/ini/__init__.py49
-rw-r--r--src/tox/config/loader/ini/factor.py5
-rw-r--r--src/tox/config/loader/ini/replace.py48
-rw-r--r--src/tox/config/loader/str_convert.py22
-rw-r--r--src/tox/config/loader/stringify.py3
-rw-r--r--src/tox/config/main.py12
-rw-r--r--src/tox/config/of_type.py8
-rw-r--r--src/tox/config/set_env.py71
-rw-r--r--src/tox/config/sets.py37
-rw-r--r--src/tox/execute/local_sub_process/__init__.py1
-rw-r--r--src/tox/provision.py2
-rw-r--r--src/tox/pytest.py4
-rw-r--r--src/tox/session/cmd/show_config.py9
-rw-r--r--src/tox/session/state.py6
-rw-r--r--src/tox/tox_env/api.py29
-rw-r--r--src/tox/tox_env/package.py4
-rw-r--r--src/tox/tox_env/python/api.py14
-rw-r--r--src/tox/tox_env/python/runner.py6
-rw-r--r--src/tox/tox_env/python/virtual_env/api.py7
-rw-r--r--src/tox/tox_env/python/virtual_env/package/api.py24
-rw-r--r--src/tox/tox_env/runner.py4
-rw-r--r--src/tox/util/pep517/via_fresh_subprocess.py4
24 files changed, 298 insertions, 99 deletions
diff --git a/src/tox/config/cli/ini.py b/src/tox/config/cli/ini.py
index 9c98e470..c7bdae34 100644
--- a/src/tox/config/cli/ini.py
+++ b/src/tox/config/cli/ini.py
@@ -51,7 +51,7 @@ class IniConfig:
result = None
else:
source = "file"
- value = self.ini.load(key, of_type=of_type, conf=None, env_name="tox")
+ value = self.ini.load(key, of_type=of_type, conf=None, env_name="tox", chain=[key])
result = value, source
except KeyError: # just not found
result = None
diff --git a/src/tox/config/loader/api.py b/src/tox/config/loader/api.py
index b1eb433f..3a542653 100644
--- a/src/tox/config/loader/api.py
+++ b/src/tox/config/loader/api.py
@@ -1,6 +1,8 @@
from abc import abstractmethod
from argparse import ArgumentTypeError
-from typing import TYPE_CHECKING, Any, List, Mapping, Optional, Set, Type, TypeVar
+from concurrent.futures import Future
+from contextlib import contextmanager
+from typing import TYPE_CHECKING, Any, Generator, List, Mapping, Optional, Set, Type, TypeVar
from tox.plugin.impl import impl
@@ -69,7 +71,9 @@ class Loader(Convert[T]):
def __repr__(self) -> str:
return f"{type(self).__name__}"
- def load(self, key: str, of_type: Type[V], conf: Optional["Config"], env_name: Optional[str]) -> V:
+ def load(
+ self, key: str, of_type: Type[V], conf: Optional["Config"], env_name: Optional[str], chain: List[str]
+ ) -> V:
"""
Load a value.
@@ -82,9 +86,25 @@ class Loader(Convert[T]):
if key in self.overrides:
return _STR_CONVERT.to(self.overrides[key].value, of_type)
raw = self.load_raw(key, conf, env_name)
- converted = self.to(raw, of_type)
+ future: "Future[V]" = Future()
+ with self.build(future, key, of_type, conf, env_name, raw, chain) as prepared:
+ converted = self.to(prepared, of_type)
+ future.set_result(converted)
return converted
+ @contextmanager
+ def build(
+ self,
+ future: "Future[V]",
+ key: str,
+ of_type: Type[V],
+ conf: Optional["Config"],
+ env_name: Optional[str],
+ raw: T,
+ chain: List[str],
+ ) -> Generator[T, None, None]:
+ yield raw
+
@impl
def tox_add_option(parser: "ToxParser") -> None:
diff --git a/src/tox/config/loader/ini/__init__.py b/src/tox/config/loader/ini/__init__.py
index 0b013557..46042fd6 100644
--- a/src/tox/config/loader/ini/__init__.py
+++ b/src/tox/config/loader/ini/__init__.py
@@ -1,11 +1,15 @@
+import inspect
+from concurrent.futures import Future
from configparser import ConfigParser, SectionProxy
-from typing import List, Optional, Set, TypeVar
+from contextlib import contextmanager
+from typing import Generator, List, Optional, Set, Type, TypeVar
from tox.config.loader.api import Loader, Override
from tox.config.loader.ini.factor import filter_for_env
from tox.config.loader.ini.replace import replace
from tox.config.loader.str_convert import StrConvert
from tox.config.main import Config
+from tox.config.set_env import SetEnv
from tox.report import HandledError
V = TypeVar("V")
@@ -28,15 +32,42 @@ class IniLoader(StrConvert, Loader[str]):
value = self._section[key]
collapsed_newlines = value.replace("\\\r\n", "").replace("\\\n", "") # collapse explicit new-line escape
if conf is None: # conf is None when we're loading the global tox configuration file for the CLI
- replaced = collapsed_newlines # we don't support factor and replace functionality there
+ factor_filtered = collapsed_newlines # we don't support factor and replace functionality there
else:
- factor_selected = filter_for_env(collapsed_newlines, env_name) # select matching factors
- try:
- replaced = replace(factor_selected, conf, env_name, self) # do replacements
- except Exception as exception:
- msg = f"replace failed in {'tox' if env_name is None else env_name}.{key} with {exception!r}"
- raise HandledError(msg)
- return replaced
+ factor_filtered = filter_for_env(collapsed_newlines, env_name) # select matching factors
+ return factor_filtered
+
+ @contextmanager
+ def build(
+ self,
+ future: "Future[V]",
+ key: str,
+ of_type: Type[V],
+ conf: Optional["Config"],
+ env_name: Optional[str],
+ raw: str,
+ chain: List[str],
+ ) -> Generator[str, None, None]:
+ delay_replace = inspect.isclass(of_type) and issubclass(of_type, SetEnv)
+
+ def replacer(raw_: str, chain_: List[str]) -> str:
+ if conf is None:
+ replaced = raw_ # no replacement supported in the core section
+ else:
+ try:
+ replaced = replace(conf, env_name, self, raw_, chain_) # do replacements
+ except Exception as exception:
+ msg = f"replace failed in {'tox' if env_name is None else env_name}.{key} with {exception!r}"
+ raise HandledError(msg) from exception
+ return replaced
+
+ if not delay_replace:
+ raw = replacer(raw, chain)
+ yield raw
+ if delay_replace:
+ converted = future.result()
+ if hasattr(converted, "replacer"): # pragma: no branch
+ converted.replacer = replacer # type: ignore[attr-defined]
def found_keys(self) -> Set[str]:
return set(self._section.keys())
diff --git a/src/tox/config/loader/ini/factor.py b/src/tox/config/loader/ini/factor.py
index 017749ff..bd1b5e63 100644
--- a/src/tox/config/loader/ini/factor.py
+++ b/src/tox/config/loader/ini/factor.py
@@ -15,12 +15,13 @@ def filter_for_env(value: str, name: Optional[str]) -> str:
overall = []
for factors, content in expand_factors(value):
if factors is None:
- overall.append(content)
+ if content:
+ overall.append(content)
else:
for group in factors:
for a_name, negate in group:
contains = a_name in current
- if contains == negate:
+ if not ((contains is True and negate is False) or (contains is False and negate is True)):
break
else:
overall.append(content)
diff --git a/src/tox/config/loader/ini/replace.py b/src/tox/config/loader/ini/replace.py
index 3a6bb98f..f590b3dd 100644
--- a/src/tox/config/loader/ini/replace.py
+++ b/src/tox/config/loader/ini/replace.py
@@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Iterator, List, Optional, Sequence, Tuple, Uni
from tox.config.loader.stringify import stringify
from tox.config.main import Config
+from tox.config.set_env import SetEnv
from tox.config.sets import ConfigSet
from tox.execute.request import shell_cmd
@@ -18,18 +19,19 @@ if TYPE_CHECKING:
CORE_PREFIX = "tox"
BASE_TEST_ENV = "testenv"
-ARGS_GROUP = re.compile(r"(?<!\\):")
+# split alongside :, unless it's esscaped, or it's preceded by a single capital letter (Windows drive letter in paths)
+ARGS_GROUP = re.compile(r"(?<!\\\\|:[A-Z]):")
-def replace(value: str, conf: Config, name: Optional[str], loader: "IniLoader") -> str:
+def replace(conf: Config, name: Optional[str], loader: "IniLoader", value: str, chain: List[str]) -> str:
# perform all non-escaped replaces
start, end = 0, 0
while True:
- start, end, match = _find_replace_part(value, start, end)
+ start, end, match = find_replace_part(value, start, end)
if not match:
break
to_replace = value[start + 1 : end]
- replaced = _replace_match(conf, name, loader, to_replace)
+ replaced = _replace_match(conf, name, loader, to_replace, chain.copy())
if replaced is None:
# if we cannot replace, keep what was there, and continue looking for additional replaces following
# note, here we cannot raise because the content may be a factorial expression, and in those case we don't
@@ -47,7 +49,7 @@ def replace(value: str, conf: Config, name: Optional[str], loader: "IniLoader")
return value
-def _find_replace_part(value: str, start: int, end: int) -> Tuple[int, int, bool]:
+def find_replace_part(value: str, start: int, end: int) -> Tuple[int, int, bool]:
match = False
while end != -1:
end = value.find("}", end)
@@ -68,16 +70,20 @@ def _find_replace_part(value: str, start: int, end: int) -> Tuple[int, int, bool
return start, end, match
-def _replace_match(conf: Config, current_env: Optional[str], loader: "IniLoader", value: str) -> Optional[str]:
+def _replace_match(
+ conf: Config, current_env: Optional[str], loader: "IniLoader", value: str, chain: List[str]
+) -> Optional[str]:
of_type, *args = ARGS_GROUP.split(value)
- if of_type == "env":
- replace_value: Optional[str] = replace_env(args)
+ if of_type == "/":
+ replace_value: Optional[str] = os.sep
+ elif of_type == "env":
+ replace_value = replace_env(conf, current_env, args, chain)
elif of_type == "tty":
replace_value = replace_tty(args)
elif of_type == "posargs":
replace_value = replace_pos_args(args, conf.pos_args)
else:
- replace_value = replace_reference(conf, current_env, loader, value)
+ replace_value = replace_reference(conf, current_env, loader, value, chain)
return replace_value
@@ -96,6 +102,7 @@ def replace_reference(
current_env: Optional[str],
loader: "IniLoader",
value: str,
+ chain: List[str],
) -> Optional[str]:
# a return value of None indicates could not replace
match = _REPLACE_REF.match(value)
@@ -112,7 +119,7 @@ def replace_reference(
try:
if isinstance(src, SectionProxy):
return src[key]
- value = src[key]
+ value = src.load(key, chain)
as_str, _ = stringify(value)
return as_str
except KeyError as exc: # if fails, keep trying maybe another source can satisfy
@@ -172,10 +179,24 @@ def replace_pos_args(args: List[str], pos_args: Optional[Sequence[str]]) -> str:
return replace_value
-def replace_env(args: List[str]) -> str:
+def replace_env(conf: Config, env_name: Optional[str], args: List[str], chain: List[str]) -> str:
key = args[0]
- default = "" if len(args) == 1 else args[1]
- return os.environ.get(key, default)
+ new_key = f"env:{key}"
+
+ if env_name is not None: # on core no set env support # pragma: no branch
+ if new_key not in chain: # check if set env
+ chain.append(new_key)
+ env_conf = conf.get_env(env_name)
+ set_env: SetEnv = env_conf["set_env"]
+ if key in set_env:
+ return set_env.load(key, chain)
+ elif chain[-1] != new_key: # if there's a chain but only self-refers than use os.environ
+ raise ValueError(f"circular chain between set env {', '.join(i[4:] for i in chain[chain.index(new_key):])}")
+
+ if key in os.environ:
+ return os.environ[key]
+
+ return "" if len(args) == 1 else args[1]
def replace_tty(args: List[str]) -> str:
@@ -190,4 +211,5 @@ __all__ = (
"CORE_PREFIX",
"BASE_TEST_ENV",
"replace",
+ "find_replace_part",
)
diff --git a/src/tox/config/loader/str_convert.py b/src/tox/config/loader/str_convert.py
index f5c5acd0..7635b5c5 100644
--- a/src/tox/config/loader/str_convert.py
+++ b/src/tox/config/loader/str_convert.py
@@ -1,10 +1,9 @@
"""Convert string configuration values to tox python configuration objects."""
-import re
import shlex
import sys
from itertools import chain
from pathlib import Path
-from typing import Any, Iterator, Tuple, Type
+from typing import Any, Iterator, List, Tuple, Type
from tox.config.loader.convert import Convert
from tox.config.types import Command, EnvList
@@ -51,18 +50,21 @@ class StrConvert(Convert[str]):
@staticmethod
def to_command(value: str) -> Command:
- win = sys.platform == "win32"
- splitter = shlex.shlex(value, posix=not win)
+ is_win = sys.platform == "win32"
+ splitter = shlex.shlex(value, posix=not is_win)
splitter.whitespace_split = True
- if win: # pragma: win32 cover
- args = []
+ if is_win: # pragma: win32 cover
+ args: List[str] = []
for arg in splitter:
- if arg[0] == "'" and arg[-1] == "'": # remove outer quote - the arg is passed as one, so no need for it
+ # on Windows quoted arguments will remain quoted, strip it
+ if (
+ len(arg) > 1
+ and (arg.startswith('"') and arg.endswith('"'))
+ or (arg.startswith("'") and arg.endswith("'"))
+ ):
arg = arg[1:-1]
- if "/" in arg: # normalize posix paths to nt paths
- arg = "\\".join(re.split(pattern=r"[\\/]", string=arg))
args.append(arg)
- else: # pragma: win32 no cover
+ else:
args = list(splitter)
return Command(args)
diff --git a/src/tox/config/loader/stringify.py b/src/tox/config/loader/stringify.py
index 85dddac6..da432252 100644
--- a/src/tox/config/loader/stringify.py
+++ b/src/tox/config/loader/stringify.py
@@ -2,6 +2,7 @@ from enum import Enum
from pathlib import Path
from typing import Any, Mapping, Sequence, Set, Tuple
+from tox.config.set_env import SetEnv
from tox.config.types import Command, EnvList
@@ -26,6 +27,8 @@ def stringify(value: Any) -> Tuple[str, bool]:
return "\n".join(e for e in value.envs), True
if isinstance(value, Command):
return value.shell, True
+ if isinstance(value, SetEnv):
+ return stringify({k: value.load(k) for k in sorted(list(value))})
return str(value), False
diff --git a/src/tox/config/main.py b/src/tox/config/main.py
index 790c6e4e..150002c9 100644
--- a/src/tox/config/main.py
+++ b/src/tox/config/main.py
@@ -5,7 +5,7 @@ from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence
from tox.config.loader.api import Loader, Override, OverrideMap
from tox.config.source import Source
-from .sets import ConfigSet, CoreConfigSet
+from .sets import CoreConfigSet, EnvConfigSet
class Config:
@@ -26,9 +26,9 @@ class Config:
self._overrides[override.namespace].append(override)
self._src = config_source
- self._env_to_set: Dict[str, ConfigSet] = OrderedDict()
+ self._env_to_set: Dict[str, EnvConfigSet] = OrderedDict()
self._core_set: Optional[CoreConfigSet] = None
- self.register_config_set: Callable[[str, ConfigSet], Any] = lambda n, e: None
+ self.register_config_set: Callable[[str, EnvConfigSet], Any] = lambda n, e: None
@property
def core(self) -> CoreConfigSet:
@@ -44,11 +44,13 @@ class Config:
self._core_set = core
return core
- def get_env(self, item: str, package: bool = False, loaders: Optional[Sequence[Loader[Any]]] = None) -> ConfigSet:
+ def get_env(
+ self, item: str, package: bool = False, loaders: Optional[Sequence[Loader[Any]]] = None
+ ) -> EnvConfigSet:
try:
return self._env_to_set[item]
except KeyError:
- env = ConfigSet(self, item)
+ env = EnvConfigSet(self, item)
self._env_to_set[item] = env
if loaders is not None:
env.loaders.extend(loaders)
diff --git a/src/tox/config/of_type.py b/src/tox/config/of_type.py
index 2322dfa4..f82b5cc8 100644
--- a/src/tox/config/of_type.py
+++ b/src/tox/config/of_type.py
@@ -23,7 +23,7 @@ class ConfigDefinition(ABC, Generic[T]):
self.env_name = env_name
@abstractmethod
- def __call__(self, conf: "Config", key: Optional[str], loaders: List[Loader[T]]) -> T:
+ def __call__(self, conf: "Config", key: Optional[str], loaders: List[Loader[T]], chain: List[str]) -> T:
raise NotImplementedError
def __eq__(self, o: Any) -> bool:
@@ -46,7 +46,7 @@ class ConfigConstantDefinition(ConfigDefinition[T]):
super().__init__(keys, desc, env_name)
self.value = value
- def __call__(self, conf: "Config", name: Optional[str], loaders: List[Loader[T]]) -> T:
+ def __call__(self, conf: "Config", name: Optional[str], loaders: List[Loader[T]], chain: List[str]) -> T:
if callable(self.value):
value = self.value()
else:
@@ -78,13 +78,13 @@ class ConfigDynamicDefinition(ConfigDefinition[T]):
self.post_process = post_process
self._cache: Union[object, T] = _PLACE_HOLDER
- def __call__(self, conf: "Config", name: Optional[str], loaders: List[Loader[T]]) -> T:
+ def __call__(self, conf: "Config", name: Optional[str], loaders: List[Loader[T]], chain: List[str]) -> T:
if self._cache is _PLACE_HOLDER:
found = False
for key in self.keys:
for loader in loaders:
try:
- value = loader.load(key, self.of_type, conf, self.env_name)
+ value = loader.load(key, self.of_type, conf, self.env_name, chain)
found = True
except KeyError:
continue
diff --git a/src/tox/config/set_env.py b/src/tox/config/set_env.py
new file mode 100644
index 00000000..68b365ad
--- /dev/null
+++ b/src/tox/config/set_env.py
@@ -0,0 +1,71 @@
+from typing import Callable, Dict, Iterator, List, Mapping, Optional, Tuple
+
+Replacer = Callable[[str, List[str]], str]
+
+
+class SetEnv:
+ def __init__(self, raw: str) -> None:
+ self.replacer: Replacer = lambda s, c: s
+ lines = raw.splitlines()
+ self._later: List[str] = []
+ self._raw: Dict[str, str] = {}
+ from .loader.ini.replace import find_replace_part
+
+ for line in lines:
+ if line.strip():
+ try:
+ key, value = self._extract_key_value(line)
+ if "{" in key:
+ raise ValueError(f"invalid line {line!r} in set_env")
+ except ValueError:
+ _, __, match = find_replace_part(line, 0, 0)
+ if match:
+ self._later.append(line)
+ else:
+ raise
+ else:
+ self._raw[key] = value
+ self._materialized: Dict[str, str] = {}
+
+ @staticmethod
+ def _extract_key_value(line: str) -> Tuple[str, str]:
+ try:
+ at = line.index("=")
+ except ValueError:
+ raise ValueError(f"invalid line {line!r} in set_env")
+ key, value = line[:at], line[at + 1 :]
+ return key.strip(), value.strip()
+
+ def load(self, item: str, chain: Optional[List[str]] = None) -> str:
+ if chain is None:
+ chain = [f"env:{item}"]
+ if item in self._materialized:
+ return self._materialized[item]
+ raw = self._raw[item]
+ result = self.replacer(raw, chain) # apply any replace options
+ self._materialized[item] = result
+ del self._raw[item]
+ return result
+
+ def __contains__(self, item: object) -> bool:
+ return isinstance(item, str) and item in self.__iter__()
+
+ def __iter__(self) -> Iterator[str]:
+ # start with the materialized ones, maybe we don't need to materialize the raw ones
+ yield from self._materialized.keys()
+ yield from list(self._raw.keys()) # iterating over this may trigger materialization and change the dict
+ while self._later:
+ line = self._later.pop(0)
+ expanded_line = self.replacer(line, [])
+ sub_raw = {}
+ for sub_line in expanded_line.splitlines():
+ key, value = self._extract_key_value(sub_line)
+ sub_raw[key] = value
+ self._raw.update(sub_raw)
+ yield from sub_raw.keys()
+
+ def update(self, param: Mapping[str, str]) -> None:
+ self._materialized.update(param)
+
+
+__all__ = ("SetEnv",)
diff --git a/src/tox/config/sets.py b/src/tox/config/sets.py
index 95fdb925..2784872c 100644
--- a/src/tox/config/sets.py
+++ b/src/tox/config/sets.py
@@ -7,6 +7,7 @@ from typing import (
Dict,
Iterator,
List,
+ Mapping,
Optional,
Sequence,
Set,
@@ -17,6 +18,7 @@ from typing import (
)
from .of_type import ConfigConstantDefinition, ConfigDefinition, ConfigDynamicDefinition
+from .set_env import SetEnv
from .types import EnvList
if TYPE_CHECKING:
@@ -79,8 +81,16 @@ class ConfigSet:
return definition
def __getitem__(self, item: str) -> Any:
+ return self.load(item)
+
+ def load(self, item: str, chain: Optional[List[str]] = None) -> Any:
config_definition = self._defined[item]
- return config_definition(self._conf, item, self.loaders)
+ if chain is None:
+ chain = []
+ if item in chain:
+ raise ValueError(f"circular chain detected {', '.join(chain[chain.index(item):])}")
+ chain.append(item)
+ return config_definition(self._conf, item, self.loaders, chain)
def __repr__(self) -> str:
values = (v for v in (f"name={self.name!r}" if self.name else "", f"loaders={self.loaders!r}") if v)
@@ -129,3 +139,28 @@ class CoreConfigSet(ConfigSet):
default=EnvList([]),
desc="define environments to automatically run",
)
+
+
+class EnvConfigSet(ConfigSet):
+ def __init__(self, conf: "Config", name: Optional[str]):
+ super().__init__(conf, name=name)
+ self.default_set_env_loader: Callable[[], Mapping[str, str]] = lambda: {}
+
+ def set_env_post_process(values: SetEnv, config: "Config") -> SetEnv:
+ values.update(self.default_set_env_loader())
+ return values
+
+ self.add_config(
+ keys=["set_env", "setenv"],
+ of_type=SetEnv,
+ default=SetEnv(""),
+ desc="environment variables to set when running commands in the tox environment",
+ post_process=set_env_post_process,
+ )
+
+
+__all__ = (
+ "ConfigSet",
+ "CoreConfigSet",
+ "EnvConfigSet",
+)
diff --git a/src/tox/execute/local_sub_process/__init__.py b/src/tox/execute/local_sub_process/__init__.py
index a8af51a8..65f15f12 100644
--- a/src/tox/execute/local_sub_process/__init__.py
+++ b/src/tox/execute/local_sub_process/__init__.py
@@ -190,7 +190,6 @@ class LocalSubProcessExecuteInstance(ExecuteInstance):
self.request.env["LINES"] = str(lines)
stdout, stderr = self.get_stream_file_no("stdout"), self.get_stream_file_no("stderr")
-
try:
self.process = process = Popen(
self.cmd,
diff --git a/src/tox/provision.py b/src/tox/provision.py
index 48b00266..9996d248 100644
--- a/src/tox/provision.py
+++ b/src/tox/provision.py
@@ -73,7 +73,7 @@ def provision(state: State) -> Union[int, bool]:
for package in requires:
package_name = canonicalize_name(package.name)
try:
- dist = distribution(package_name)
+ dist = distribution(package_name) # type: ignore[no-untyped-call]
if not package.specifier.contains(dist.version, prereleases=True):
missing.append((package, dist.version))
except PackageNotFoundError:
diff --git a/src/tox/pytest.py b/src/tox/pytest.py
index 84676dd6..a5bdd508 100644
--- a/src/tox/pytest.py
+++ b/src/tox/pytest.py
@@ -32,6 +32,7 @@ from virtualenv.discovery.py_info import PythonInfo
from virtualenv.info import IS_WIN, fs_supports_symlink
import tox.run
+from tox.config.sets import EnvConfigSet
from tox.execute.api import Execute, ExecuteInstance, ExecuteStatus, Outcome
from tox.execute.request import ExecuteRequest, shell_cmd
from tox.execute.stream import SyncWrite
@@ -299,6 +300,9 @@ class ToxRunOutcome:
raise RuntimeError("no state")
return self._state
+ def env_conf(self, name: str) -> EnvConfigSet:
+ return self.state.conf.get_env(name)
+
@property
def success(self) -> bool:
return self.code == Outcome.OK
diff --git a/src/tox/session/cmd/show_config.py b/src/tox/session/cmd/show_config.py
index 759505db..8849b470 100644
--- a/src/tox/session/cmd/show_config.py
+++ b/src/tox/session/cmd/show_config.py
@@ -27,17 +27,20 @@ def tox_add_option(parser: ToxParser) -> None:
def show_config(state: State) -> int:
+ show_core = state.options.env.all or state.options.show_core
keys: List[str] = state.options.list_keys_only
# environments may define core configuration flags, so we must exhaust first the environments to tell the core part
- for name in state.env_list(everything=False):
+ envs = list(state.env_list(everything=False))
+ for at, name in enumerate(envs):
tox_env = state.tox_env(name)
print(f"[testenv:{name}]")
if not keys:
print(f"type = {type(tox_env).__name__}")
print_conf(tox_env.conf, keys)
- print("")
+ if show_core or at + 1 != len(envs):
+ print("")
# no print core
- if state.options.env.all or state.options.show_core:
+ if show_core:
print("[tox]")
print_conf(state.conf.core, keys)
return 0
diff --git a/src/tox/session/state.py b/src/tox/session/state.py
index 05928765..dab94b1c 100644
--- a/src/tox/session/state.py
+++ b/src/tox/session/state.py
@@ -1,7 +1,7 @@
from typing import TYPE_CHECKING, Dict, Iterator, Optional, Sequence, Set, Tuple, cast
from tox.config.main import Config
-from tox.config.sets import ConfigSet
+from tox.config.sets import EnvConfigSet
from tox.journal import Journal
from tox.plugin.impl import impl
from tox.report import HandledError, ToxHandler
@@ -64,7 +64,7 @@ class State:
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, config_set: ConfigSet) -> None:
+ def register_config_set(self, name: str, config_set: EnvConfigSet) -> None:
"""Ensure the config set with the given name has been registered with configuration values"""
# during the creation of hte 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
@@ -75,7 +75,7 @@ class State:
# runtime environments are created upon lookup via the tox_env method, call it
self._build_run_env(config_set)
- def _build_run_env(self, env_conf: ConfigSet) -> None:
+ def _build_run_env(self, env_conf: EnvConfigSet) -> None:
env_conf.add_config(
keys="runner",
desc="the tox execute used to evaluate this environment",
diff --git a/src/tox/tox_env/api.py b/src/tox/tox_env/api.py
index ae678557..1ba40a17 100644
--- a/src/tox/tox_env/api.py
+++ b/src/tox/tox_env/api.py
@@ -13,7 +13,8 @@ from pathlib import Path
from typing import TYPE_CHECKING, Dict, Iterator, List, Optional, Sequence, Tuple, Union, cast
from tox.config.main import Config
-from tox.config.sets import ConfigSet
+from tox.config.set_env import SetEnv
+from tox.config.sets import CoreConfigSet, EnvConfigSet
from tox.execute.api import Execute, ExecuteStatus, Outcome, StdinSource
from tox.execute.request import ExecuteRequest
from tox.journal import EnvJournal
@@ -30,11 +31,11 @@ LOGGER = logging.getLogger(__name__)
class ToxEnv(ABC):
def __init__(
- self, conf: ConfigSet, core: ConfigSet, options: "Parsed", journal: EnvJournal, log_handler: ToxHandler
+ self, conf: EnvConfigSet, core: CoreConfigSet, options: "Parsed", journal: EnvJournal, log_handler: ToxHandler
) -> None:
self.journal = journal
- self.conf: ConfigSet = conf
- self.core: ConfigSet = core
+ self.conf: EnvConfigSet = conf
+ self.core: CoreConfigSet = core
self.options = options
self._executor: Optional[Execute] = None
self.register_config()
@@ -90,19 +91,7 @@ class ToxEnv(ABC):
default=lambda conf, name: cast(Path, conf.core["work_dir"]) / cast(str, self.conf["env_name"]) / "tmp",
desc="a folder that is always reset at the start of the run",
)
-
- def set_env_post_process(values: Dict[str, str], config: Config) -> Dict[str, str]:
- env = self.default_set_env()
- env.update(values)
- return env
-
- self.conf.add_config(
- keys=["set_env", "setenv"],
- of_type=Dict[str, str],
- default={},
- desc="environment variables to set when running commands in the tox environment",
- post_process=set_env_post_process,
- )
+ self.conf.default_set_env_loader = self.default_set_env
def pass_env_post_process(values: List[str], config: Config) -> List[str]:
values.extend(self.default_pass_env())
@@ -199,7 +188,6 @@ class ToxEnv(ABC):
if self._env_vars is not None:
return self._env_vars
result: Dict[str, str] = {}
-
pass_env: List[str] = self.conf["pass_env"]
glob_pass_env = [re.compile(e.replace("*", ".*")) for e in pass_env if "*" in e]
literal_pass_env = [e for e in pass_env if "*" not in e]
@@ -210,8 +198,9 @@ class ToxEnv(ABC):
for env, value in os.environ.items():
if any(g.match(env) is not None for g in glob_pass_env):
result[env] = value
- set_env: Dict[str, str] = self.conf["set_env"]
- result.update(set_env)
+ set_env: SetEnv = self.conf["set_env"]
+ for key in set_env:
+ result[key] = set_env.load(key)
result["PATH"] = os.pathsep.join([str(i) for i in self._paths] + os.environ.get("PATH", "").split(os.pathsep))
self._env_vars = result
return result
diff --git a/src/tox/tox_env/package.py b/src/tox/tox_env/package.py
index b32b2c40..88861174 100644
--- a/src/tox/tox_env/package.py
+++ b/src/tox/tox_env/package.py
@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, List, Set
from packaging.requirements import Requirement
-from tox.config.sets import ConfigSet
+from tox.config.sets import CoreConfigSet, EnvConfigSet
from tox.journal import EnvJournal
from tox.report import ToxHandler
from tox.util.threading import AtomicCounter
@@ -20,7 +20,7 @@ if TYPE_CHECKING:
class PackageToxEnv(ToxEnv, ABC):
def __init__(
- self, conf: ConfigSet, core: ConfigSet, options: "Parsed", journal: EnvJournal, log_handler: ToxHandler
+ self, conf: EnvConfigSet, core: CoreConfigSet, options: "Parsed", journal: EnvJournal, log_handler: ToxHandler
) -> None:
super().__init__(conf, core, options, journal, log_handler)
self.recreate_package = options.no_recreate_pkg is False if options.recreate else False
diff --git a/src/tox/tox_env/python/api.py b/src/tox/tox_env/python/api.py
index 28fb5d79..ed7dd27d 100644
--- a/src/tox/tox_env/python/api.py
+++ b/src/tox/tox_env/python/api.py
@@ -12,7 +12,7 @@ from virtualenv.discovery.py_spec import PythonSpec
from tox.config.cli.parser import Parsed
from tox.config.main import Config
-from tox.config.sets import ConfigSet
+from tox.config.sets import CoreConfigSet, EnvConfigSet
from tox.journal import EnvJournal
from tox.report import ToxHandler
from tox.tox_env.api import ToxEnv
@@ -63,7 +63,7 @@ PythonDeps = Sequence[PythonDep]
class Python(ToxEnv, ABC):
def __init__(
- self, conf: ConfigSet, core: ConfigSet, options: Parsed, journal: EnvJournal, log_handler: ToxHandler
+ self, conf: EnvConfigSet, core: CoreConfigSet, options: Parsed, journal: EnvJournal, log_handler: ToxHandler
) -> None:
self._base_python: Optional[PythonInfo] = None
self._base_python_searched: bool = False
@@ -83,6 +83,11 @@ class Python(ToxEnv, ABC):
value=lambda: self.env_site_package_dir(),
)
self.conf.add_constant(
+ keys=["env_bin_dir", "envbindir"],
+ desc="the python environments site package",
+ value=lambda: self.env_bin_dir(),
+ )
+ self.conf.add_constant(
["env_python", "envpython"],
desc="python executable from within the tox environment",
value=lambda: self.env_python(),
@@ -123,6 +128,11 @@ class Python(ToxEnv, ABC):
"""The python executable within the tox environment"""
raise NotImplementedError
+ @abstractmethod
+ def env_bin_dir(self) -> Path:
+ """The binary folder within the tox environment"""
+ raise NotImplementedError
+
def setup(self) -> None:
"""setup a virtual python environment"""
conf = self.python_cache()
diff --git a/src/tox/tox_env/python/runner.py b/src/tox/tox_env/python/runner.py
index bdc5d797..f9b0f1af 100644
--- a/src/tox/tox_env/python/runner.py
+++ b/src/tox/tox_env/python/runner.py
@@ -8,7 +8,7 @@ from pathlib import Path
from typing import Any, Dict, List, Set
from tox.config.cli.parser import Parsed
-from tox.config.sets import ConfigSet
+from tox.config.sets import CoreConfigSet, EnvConfigSet
from tox.journal import EnvJournal
from tox.report import ToxHandler
from tox.tox_env.errors import Recreate
@@ -19,7 +19,9 @@ from .req_file import RequirementsFile
class PythonRun(Python, RunToxEnv, ABC):
- def __init__(self, conf: ConfigSet, core: ConfigSet, options: Parsed, journal: EnvJournal, log_handler: ToxHandler):
+ def __init__(
+ self, conf: EnvConfigSet, core: CoreConfigSet, options: Parsed, journal: EnvJournal, log_handler: ToxHandler
+ ):
super().__init__(conf, core, options, journal, log_handler)
self._packages: List[PythonDep] = []
diff --git a/src/tox/tox_env/python/virtual_env/api.py b/src/tox/tox_env/python/virtual_env/api.py
index dd5692c8..5af0a504 100644
--- a/src/tox/tox_env/python/virtual_env/api.py
+++ b/src/tox/tox_env/python/virtual_env/api.py
@@ -10,7 +10,7 @@ from virtualenv.create.creator import Creator
from virtualenv.run.session import Session
from tox.config.cli.parser import DEFAULT_VERBOSITY, Parsed
-from tox.config.sets import ConfigSet
+from tox.config.sets import CoreConfigSet, EnvConfigSet
from tox.execute.api import Execute, Outcome, StdinSource
from tox.execute.local_sub_process import LocalSubProcessExecutor
from tox.journal import EnvJournal
@@ -23,7 +23,7 @@ class VirtualEnv(Python, ABC):
"""A python executor that uses the virtualenv project with pip"""
def __init__(
- self, conf: ConfigSet, core: ConfigSet, options: Parsed, journal: EnvJournal, log_handler: ToxHandler
+ self, conf: EnvConfigSet, core: CoreConfigSet, options: Parsed, journal: EnvJournal, log_handler: ToxHandler
) -> None:
self._virtualenv_session: Optional[Session] = None # type: ignore[no-any-unimported]
super().__init__(conf, core, options, journal, log_handler)
@@ -90,6 +90,9 @@ class VirtualEnv(Python, ABC):
def env_python(self) -> Path:
return cast(Path, self.creator.exe)
+ def env_bin_dir(self) -> Path:
+ return cast(Path, self.creator.script_dir)
+
def install_python_packages(
self,
packages: PythonDeps,
diff --git a/src/tox/tox_env/python/virtual_env/package/api.py b/src/tox/tox_env/python/virtual_env/package/api.py
index 03ea06e8..3df7bb33 100644
--- a/src/tox/tox_env/python/virtual_env/package/api.py
+++ b/src/tox/tox_env/python/virtual_env/package/api.py
@@ -11,7 +11,7 @@ from packaging.markers import Variable
from packaging.requirements import Requirement
from tox.config.cli.parser import Parsed
-from tox.config.sets import ConfigSet
+from tox.config.sets import CoreConfigSet, EnvConfigSet
from tox.execute.api import ExecuteStatus
from tox.execute.pep517_backend import LocalSubProcessPep517Executor
from tox.execute.request import StdinSource
@@ -83,7 +83,7 @@ class Pep517VirtualEnvPackage(VirtualEnv, PythonPackage, Frontend, ABC):
"""local file system python virtual environment via the virtualenv package"""
def __init__(
- self, conf: ConfigSet, core: ConfigSet, options: Parsed, journal: EnvJournal, log_handler: ToxHandler
+ self, conf: EnvConfigSet, core: CoreConfigSet, options: Parsed, journal: EnvJournal, log_handler: ToxHandler
) -> None:
VirtualEnv.__init__(self, conf, core, options, journal, log_handler)
Frontend.__init__(self, *Frontend.create_args_from_folder(core["tox_root"]))
@@ -122,7 +122,7 @@ class Pep517VirtualEnvPackage(VirtualEnv, PythonPackage, Frontend, ABC):
return # pragma: no cover
self.ensure_setup()
dist_info = self.prepare_metadata_for_build_wheel(self.meta_folder).metadata
- self._distribution_meta = Distribution.at(str(dist_info))
+ self._distribution_meta = Distribution.at(str(dist_info)) # type: ignore[no-untyped-call]
@abstractmethod
def _build_artifact(self) -> Path:
@@ -163,18 +163,16 @@ class Pep517VirtualEnvPackage(VirtualEnv, PythonPackage, Frontend, ABC):
):
if marker_key.value == "extra" and op.value == "==": # pragma: no branch
extra = marker_value.value
+ del markers[_at]
+ _at -= 1
+ if _at > 0 and (isinstance(markers[_at], str) and markers[_at] in ("and", "or")):
+ del markers[_at]
+ if len(markers) == 0:
+ req.marker = None
break
# continue only if this extra should be included
if not (extra is None or extra in extras):
continue
- # delete the extra marker if present
- if _at is not None:
- del markers[_at]
- _at -= 1
- if _at > 0 and (isinstance(markers[_at], str) and markers[_at] in ("and", "or")):
- del markers[_at]
- if len(markers) == 0:
- req.marker = None
result.append(req)
return result
@@ -201,7 +199,9 @@ class Pep517VirtualEnvPackage(VirtualEnv, PythonPackage, Frontend, ABC):
@property
def environment_variables(self) -> Dict[str, str]:
env = super().environment_variables
- env["PYTHONPATH"] = os.pathsep.join(str(i) for i in self._backend_paths)
+ backend = os.pathsep.join(str(i) for i in self._backend_paths).strip()
+ if backend:
+ env["PYTHONPATH"] = backend
return env
def teardown(self) -> None:
diff --git a/src/tox/tox_env/runner.py b/src/tox/tox_env/runner.py
index cce84f18..5d5d1872 100644
--- a/src/tox/tox_env/runner.py
+++ b/src/tox/tox_env/runner.py
@@ -3,7 +3,7 @@ from abc import ABC, abstractmethod
from pathlib import Path
from typing import TYPE_CHECKING, Dict, Generator, List, Optional, Tuple, cast
-from tox.config.sets import ConfigSet
+from tox.config.sets import CoreConfigSet, EnvConfigSet
from tox.config.types import Command, EnvList
from tox.journal import EnvJournal
from tox.report import ToxHandler
@@ -17,7 +17,7 @@ if TYPE_CHECKING:
class RunToxEnv(ToxEnv, ABC):
def __init__(
- self, conf: ConfigSet, core: ConfigSet, options: "Parsed", journal: EnvJournal, log_handler: ToxHandler
+ self, conf: EnvConfigSet, core: CoreConfigSet, options: "Parsed", journal: EnvJournal, log_handler: ToxHandler
) -> None:
self.has_package = False
self.package_env: Optional[PackageToxEnv] = None
diff --git a/src/tox/util/pep517/via_fresh_subprocess.py b/src/tox/util/pep517/via_fresh_subprocess.py
index 1f83a557..11858e85 100644
--- a/src/tox/util/pep517/via_fresh_subprocess.py
+++ b/src/tox/util/pep517/via_fresh_subprocess.py
@@ -45,7 +45,9 @@ class SubprocessFrontend(Frontend):
self, cmd: str, result_file: Path, msg: str
) -> Iterator[SubprocessCmdStatus]:
env = os.environ.copy()
- env["PYTHONPATH"] = os.pathsep.join(str(i) for i in self._backend_paths)
+ backend = os.pathsep.join(str(i) for i in self._backend_paths).strip()
+ if backend:
+ env["PYTHONPATH"] = backend
process = Popen(
args=[sys.executable] + self.backend_args,
stdout=PIPE,