Skip to content

Commit 4cf619a

Browse files
authored
Better handling of set_env (#1784)
1 parent 68cbc35 commit 4cf619a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+478
-134
lines changed

docs/changelog.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,30 @@ Release History
55

66
.. towncrier release notes start
77
8+
v4.0.0a2 (2021-01-09)
9+
---------------------
10+
11+
Features - 4.0.0a2
12+
~~~~~~~~~~~~~~~~~~
13+
- Add option to disable colored output, and support ``NO_COLOR`` and ``FORCE_COLOR`` environment variables - by
14+
:user:`gaborbernat`. (`#1630 <https://github.com/tox-dev/tox/issues/1630>`_)
15+
16+
Bugfixes - 4.0.0a2
17+
~~~~~~~~~~~~~~~~~~
18+
- Fix coverage generation in CI - by :user:`gaborbernat`. (`#1551 <https://github.com/tox-dev/tox/issues/1551>`_)
19+
- Fix the CI failures:
20+
21+
- drop Python 3.5 support as it's not expected to get to a release before EOL,
22+
- fix test using ``\n`` instead of ``os.linesep``,
23+
- Windows Python 3.6 does not contain ``_overlapped.ReadFileInto``
24+
25+
- by :user:`gaborbernat`. (`#1556 <https://github.com/tox-dev/tox/issues/1556>`_)
26+
27+
Improved Documentation - 4.0.0a2
28+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
29+
- Add base documentation by merging virtualenv structure with tox 3 - by :user:`gaborbernat`. (`#1551 <https://github.com/tox-dev/tox/issues/1551>`_)
30+
31+
832
v4.0.0a1
933
--------
1034
* First version all is brand new.

docs/changelog/1551.bugfix.rst

Lines changed: 0 additions & 1 deletion
This file was deleted.

docs/changelog/1551.doc.rst

Lines changed: 0 additions & 1 deletion
This file was deleted.

docs/changelog/1556.bugfix.rst

Lines changed: 0 additions & 7 deletions
This file was deleted.

docs/changelog/1630.feature.rst

Lines changed: 0 additions & 2 deletions
This file was deleted.

docs/changelog/1776.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Entries in the ``set_env`` does not reference environments from ``set_env`` - by :user:`gaborbernat`.

docs/changelog/1779.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
``env`` substitution does not uses values from ``set_env`` - by :user:`gaborbernat`.

docs/changelog/1779.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Raise exception when set env enters into a circular reference - by :user:`gaborbernat`.

docs/changelog/1784.bugfix.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
- Python version markers are stripped in package dependencies (after wrongfully being detected as an extra marker).
2+
- In packaging APIs do not set ``PYTHONPATH`` (to empty string) if ``backend-path`` is empty.
3+
- Fix commands parsing on Windows (do not auto-escape ``\`` - instead users should use the new ``{\}``, and on parsed
4+
arguments strip both ``'`` and ``"`` quoted outcomes).
5+
- Allow windows paths in substitution set/default (the ``:`` character used to separate substitution arguments may
6+
also be present in paths on Windows - do not support single capital letter values as substitution arguments) -
7+
by :user:`gaborbernat`.

docs/changelog/1784.feature.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- Raise exception when variable substitution enters into a circle.
2+
- Add ``{/}`` as substitution for os specific path separator.
3+
- Add ``{env_bin_dir}`` constant substitution.
4+
- Implement support for ``--discover`` flag - by :user:`gaborbernat`.

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ install_requires =
4242
packaging>=20.3
4343
pluggy>=0.13.1
4444
toml>=0.10
45-
virtualenv>=20.0.20
45+
virtualenv>=20.3
4646
importlib-metadata>=1.6.0;python_version<"3.8"
4747
typing-extensions>=3.7.4.2;python_version<"3.8"
4848
python_requires = >=3.6

src/tox/config/cli/ini.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def get(self, key: str, of_type: Type[Any]) -> Any:
5151
result = None
5252
else:
5353
source = "file"
54-
value = self.ini.load(key, of_type=of_type, conf=None, env_name="tox")
54+
value = self.ini.load(key, of_type=of_type, conf=None, env_name="tox", chain=[key])
5555
result = value, source
5656
except KeyError: # just not found
5757
result = None

src/tox/config/loader/api.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from abc import abstractmethod
22
from argparse import ArgumentTypeError
3-
from typing import TYPE_CHECKING, Any, List, Mapping, Optional, Set, Type, TypeVar
3+
from concurrent.futures import Future
4+
from contextlib import contextmanager
5+
from typing import TYPE_CHECKING, Any, Generator, List, Mapping, Optional, Set, Type, TypeVar
46

57
from tox.plugin.impl import impl
68

@@ -69,7 +71,9 @@ def found_keys(self) -> Set[str]:
6971
def __repr__(self) -> str:
7072
return f"{type(self).__name__}"
7173

72-
def load(self, key: str, of_type: Type[V], conf: Optional["Config"], env_name: Optional[str]) -> V:
74+
def load(
75+
self, key: str, of_type: Type[V], conf: Optional["Config"], env_name: Optional[str], chain: List[str]
76+
) -> V:
7377
"""
7478
Load a value.
7579
@@ -82,9 +86,25 @@ def load(self, key: str, of_type: Type[V], conf: Optional["Config"], env_name: O
8286
if key in self.overrides:
8387
return _STR_CONVERT.to(self.overrides[key].value, of_type)
8488
raw = self.load_raw(key, conf, env_name)
85-
converted = self.to(raw, of_type)
89+
future: "Future[V]" = Future()
90+
with self.build(future, key, of_type, conf, env_name, raw, chain) as prepared:
91+
converted = self.to(prepared, of_type)
92+
future.set_result(converted)
8693
return converted
8794

95+
@contextmanager
96+
def build(
97+
self,
98+
future: "Future[V]",
99+
key: str,
100+
of_type: Type[V],
101+
conf: Optional["Config"],
102+
env_name: Optional[str],
103+
raw: T,
104+
chain: List[str],
105+
) -> Generator[T, None, None]:
106+
yield raw
107+
88108

89109
@impl
90110
def tox_add_option(parser: "ToxParser") -> None:

src/tox/config/loader/ini/__init__.py

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
import inspect
2+
from concurrent.futures import Future
13
from configparser import ConfigParser, SectionProxy
2-
from typing import List, Optional, Set, TypeVar
4+
from contextlib import contextmanager
5+
from typing import Generator, List, Optional, Set, Type, TypeVar
36

47
from tox.config.loader.api import Loader, Override
58
from tox.config.loader.ini.factor import filter_for_env
69
from tox.config.loader.ini.replace import replace
710
from tox.config.loader.str_convert import StrConvert
811
from tox.config.main import Config
12+
from tox.config.set_env import SetEnv
913
from tox.report import HandledError
1014

1115
V = TypeVar("V")
@@ -28,15 +32,42 @@ def load_raw(self, key: str, conf: Optional[Config], env_name: Optional[str]) ->
2832
value = self._section[key]
2933
collapsed_newlines = value.replace("\\\r\n", "").replace("\\\n", "") # collapse explicit new-line escape
3034
if conf is None: # conf is None when we're loading the global tox configuration file for the CLI
31-
replaced = collapsed_newlines # we don't support factor and replace functionality there
35+
factor_filtered = collapsed_newlines # we don't support factor and replace functionality there
3236
else:
33-
factor_selected = filter_for_env(collapsed_newlines, env_name) # select matching factors
34-
try:
35-
replaced = replace(factor_selected, conf, env_name, self) # do replacements
36-
except Exception as exception:
37-
msg = f"replace failed in {'tox' if env_name is None else env_name}.{key} with {exception!r}"
38-
raise HandledError(msg)
39-
return replaced
37+
factor_filtered = filter_for_env(collapsed_newlines, env_name) # select matching factors
38+
return factor_filtered
39+
40+
@contextmanager
41+
def build(
42+
self,
43+
future: "Future[V]",
44+
key: str,
45+
of_type: Type[V],
46+
conf: Optional["Config"],
47+
env_name: Optional[str],
48+
raw: str,
49+
chain: List[str],
50+
) -> Generator[str, None, None]:
51+
delay_replace = inspect.isclass(of_type) and issubclass(of_type, SetEnv)
52+
53+
def replacer(raw_: str, chain_: List[str]) -> str:
54+
if conf is None:
55+
replaced = raw_ # no replacement supported in the core section
56+
else:
57+
try:
58+
replaced = replace(conf, env_name, self, raw_, chain_) # do replacements
59+
except Exception as exception:
60+
msg = f"replace failed in {'tox' if env_name is None else env_name}.{key} with {exception!r}"
61+
raise HandledError(msg) from exception
62+
return replaced
63+
64+
if not delay_replace:
65+
raw = replacer(raw, chain)
66+
yield raw
67+
if delay_replace:
68+
converted = future.result()
69+
if hasattr(converted, "replacer"): # pragma: no branch
70+
converted.replacer = replacer # type: ignore[attr-defined]
4071

4172
def found_keys(self) -> Set[str]:
4273
return set(self._section.keys())

src/tox/config/loader/ini/factor.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@ def filter_for_env(value: str, name: Optional[str]) -> str:
1515
overall = []
1616
for factors, content in expand_factors(value):
1717
if factors is None:
18-
overall.append(content)
18+
if content:
19+
overall.append(content)
1920
else:
2021
for group in factors:
2122
for a_name, negate in group:
2223
contains = a_name in current
23-
if contains == negate:
24+
if not ((contains is True and negate is False) or (contains is False and negate is True)):
2425
break
2526
else:
2627
overall.append(content)

src/tox/config/loader/ini/replace.py

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from tox.config.loader.stringify import stringify
1111
from tox.config.main import Config
12+
from tox.config.set_env import SetEnv
1213
from tox.config.sets import ConfigSet
1314
from tox.execute.request import shell_cmd
1415

@@ -18,18 +19,19 @@
1819
CORE_PREFIX = "tox"
1920
BASE_TEST_ENV = "testenv"
2021

21-
ARGS_GROUP = re.compile(r"(?<!\\):")
22+
# split alongside :, unless it's esscaped, or it's preceded by a single capital letter (Windows drive letter in paths)
23+
ARGS_GROUP = re.compile(r"(?<!\\\\|:[A-Z]):")
2224

2325

24-
def replace(value: str, conf: Config, name: Optional[str], loader: "IniLoader") -> str:
26+
def replace(conf: Config, name: Optional[str], loader: "IniLoader", value: str, chain: List[str]) -> str:
2527
# perform all non-escaped replaces
2628
start, end = 0, 0
2729
while True:
28-
start, end, match = _find_replace_part(value, start, end)
30+
start, end, match = find_replace_part(value, start, end)
2931
if not match:
3032
break
3133
to_replace = value[start + 1 : end]
32-
replaced = _replace_match(conf, name, loader, to_replace)
34+
replaced = _replace_match(conf, name, loader, to_replace, chain.copy())
3335
if replaced is None:
3436
# if we cannot replace, keep what was there, and continue looking for additional replaces following
3537
# 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")
4749
return value
4850

4951

50-
def _find_replace_part(value: str, start: int, end: int) -> Tuple[int, int, bool]:
52+
def find_replace_part(value: str, start: int, end: int) -> Tuple[int, int, bool]:
5153
match = False
5254
while end != -1:
5355
end = value.find("}", end)
@@ -68,16 +70,20 @@ def _find_replace_part(value: str, start: int, end: int) -> Tuple[int, int, bool
6870
return start, end, match
6971

7072

71-
def _replace_match(conf: Config, current_env: Optional[str], loader: "IniLoader", value: str) -> Optional[str]:
73+
def _replace_match(
74+
conf: Config, current_env: Optional[str], loader: "IniLoader", value: str, chain: List[str]
75+
) -> Optional[str]:
7276
of_type, *args = ARGS_GROUP.split(value)
73-
if of_type == "env":
74-
replace_value: Optional[str] = replace_env(args)
77+
if of_type == "/":
78+
replace_value: Optional[str] = os.sep
79+
elif of_type == "env":
80+
replace_value = replace_env(conf, current_env, args, chain)
7581
elif of_type == "tty":
7682
replace_value = replace_tty(args)
7783
elif of_type == "posargs":
7884
replace_value = replace_pos_args(args, conf.pos_args)
7985
else:
80-
replace_value = replace_reference(conf, current_env, loader, value)
86+
replace_value = replace_reference(conf, current_env, loader, value, chain)
8187
return replace_value
8288

8389

@@ -96,6 +102,7 @@ def replace_reference(
96102
current_env: Optional[str],
97103
loader: "IniLoader",
98104
value: str,
105+
chain: List[str],
99106
) -> Optional[str]:
100107
# a return value of None indicates could not replace
101108
match = _REPLACE_REF.match(value)
@@ -112,7 +119,7 @@ def replace_reference(
112119
try:
113120
if isinstance(src, SectionProxy):
114121
return src[key]
115-
value = src[key]
122+
value = src.load(key, chain)
116123
as_str, _ = stringify(value)
117124
return as_str
118125
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:
172179
return replace_value
173180

174181

175-
def replace_env(args: List[str]) -> str:
182+
def replace_env(conf: Config, env_name: Optional[str], args: List[str], chain: List[str]) -> str:
176183
key = args[0]
177-
default = "" if len(args) == 1 else args[1]
178-
return os.environ.get(key, default)
184+
new_key = f"env:{key}"
185+
186+
if env_name is not None: # on core no set env support # pragma: no branch
187+
if new_key not in chain: # check if set env
188+
chain.append(new_key)
189+
env_conf = conf.get_env(env_name)
190+
set_env: SetEnv = env_conf["set_env"]
191+
if key in set_env:
192+
return set_env.load(key, chain)
193+
elif chain[-1] != new_key: # if there's a chain but only self-refers than use os.environ
194+
raise ValueError(f"circular chain between set env {', '.join(i[4:] for i in chain[chain.index(new_key):])}")
195+
196+
if key in os.environ:
197+
return os.environ[key]
198+
199+
return "" if len(args) == 1 else args[1]
179200

180201

181202
def replace_tty(args: List[str]) -> str:
@@ -190,4 +211,5 @@ def replace_tty(args: List[str]) -> str:
190211
"CORE_PREFIX",
191212
"BASE_TEST_ENV",
192213
"replace",
214+
"find_replace_part",
193215
)

src/tox/config/loader/str_convert.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
"""Convert string configuration values to tox python configuration objects."""
2-
import re
32
import shlex
43
import sys
54
from itertools import chain
65
from pathlib import Path
7-
from typing import Any, Iterator, Tuple, Type
6+
from typing import Any, Iterator, List, Tuple, Type
87

98
from tox.config.loader.convert import Convert
109
from tox.config.types import Command, EnvList
@@ -51,18 +50,21 @@ def to_dict(value: str, of_type: Tuple[Type[Any], Type[Any]]) -> Iterator[Tuple[
5150

5251
@staticmethod
5352
def to_command(value: str) -> Command:
54-
win = sys.platform == "win32"
55-
splitter = shlex.shlex(value, posix=not win)
53+
is_win = sys.platform == "win32"
54+
splitter = shlex.shlex(value, posix=not is_win)
5655
splitter.whitespace_split = True
57-
if win: # pragma: win32 cover
58-
args = []
56+
if is_win: # pragma: win32 cover
57+
args: List[str] = []
5958
for arg in splitter:
60-
if arg[0] == "'" and arg[-1] == "'": # remove outer quote - the arg is passed as one, so no need for it
59+
# on Windows quoted arguments will remain quoted, strip it
60+
if (
61+
len(arg) > 1
62+
and (arg.startswith('"') and arg.endswith('"'))
63+
or (arg.startswith("'") and arg.endswith("'"))
64+
):
6165
arg = arg[1:-1]
62-
if "/" in arg: # normalize posix paths to nt paths
63-
arg = "\\".join(re.split(pattern=r"[\\/]", string=arg))
6466
args.append(arg)
65-
else: # pragma: win32 no cover
67+
else:
6668
args = list(splitter)
6769
return Command(args)
6870

src/tox/config/loader/stringify.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from pathlib import Path
33
from typing import Any, Mapping, Sequence, Set, Tuple
44

5+
from tox.config.set_env import SetEnv
56
from tox.config.types import Command, EnvList
67

78

@@ -26,6 +27,8 @@ def stringify(value: Any) -> Tuple[str, bool]:
2627
return "\n".join(e for e in value.envs), True
2728
if isinstance(value, Command):
2829
return value.shell, True
30+
if isinstance(value, SetEnv):
31+
return stringify({k: value.load(k) for k in sorted(list(value))})
2932
return str(value), False
3033

3134

0 commit comments

Comments
 (0)