Skip to content

Commit fb4df29

Browse files
committed
fix(config): resolve overrides with alias config keys
When using TOX_OVERRIDE with += (append) and the override key differs from the config file key (e.g., pass_env vs passenv), the config value was lost. Now Loader.load() checks all alias keys for both raw config values and overrides. Fixes #3127, fixes #3348.
1 parent c4552f6 commit fb4df29

4 files changed

Lines changed: 51 additions & 16 deletions

File tree

docs/changelog/3127.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
``TOX_OVERRIDE`` with ``+=`` (append) now works correctly when the override key name differs from the config file key
2+
name (e.g., overriding ``pass_env`` when config uses ``passenv``, or vice versa) - by :user:`gaborbernat`.

src/tox/config/loader/api.py

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from abc import abstractmethod
44
from argparse import ArgumentTypeError
5-
from collections.abc import Mapping
5+
from collections.abc import Iterable, Mapping
66
from typing import TYPE_CHECKING, Any, TypeVar, cast
77

88
from tox.plugin import impl
@@ -120,13 +120,14 @@ def __repr__(self) -> str:
120120
def __contains__(self, item: str) -> bool:
121121
return item in self.found_keys()
122122

123-
def load(
123+
def load( # noqa: PLR0913
124124
self,
125125
key: str,
126126
of_type: type[V] | UnionType,
127127
factory: Factory[V],
128128
conf: Config | None,
129129
args: ConfigLoadArgs,
130+
all_keys: Iterable[str] = (),
130131
) -> V:
131132
"""Load a value (raw and then convert).
132133
@@ -135,22 +136,18 @@ def load(
135136
:param factory: factory method to build the object
136137
:param conf: the configuration object of this tox session (needed to manifest the value)
137138
:param args: the config load arguments
139+
:param all_keys: all alias keys for this config entry (to collect overrides from any alias)
138140
139141
:returns: the converted type
140142
141143
"""
142144
from tox.config.set_env import SetEnv # noqa: PLC0415
143145

144-
overrides = self.overrides.get(key, [])
145-
146-
try:
147-
raw = self.load_raw(key, conf, args.env_name)
148-
except KeyError:
149-
converted = None
150-
if not overrides:
151-
raise
152-
else:
153-
converted = self.build(key, of_type, factory, conf, raw, args)
146+
overrides = [o for alias in (key, *all_keys) for o in self.overrides.get(alias, [])]
147+
if (
148+
converted := self._load_raw_with_aliases(key, of_type, factory, conf, args, all_keys)
149+
) is None and not overrides:
150+
raise KeyError(key)
154151

155152
for override in overrides:
156153
converted_override = _STR_CONVERT.to(override.value, of_type, factory)
@@ -171,6 +168,23 @@ def load(
171168

172169
return cast("V", converted) # guaranteed non-None: either build() succeeded or overrides set it
173170

171+
def _load_raw_with_aliases( # noqa: PLR0913
172+
self,
173+
key: str,
174+
of_type: type[V] | UnionType,
175+
factory: Factory[V],
176+
conf: Config | None,
177+
args: ConfigLoadArgs,
178+
all_keys: Iterable[str],
179+
) -> V | None:
180+
for alias in (key, *all_keys):
181+
try:
182+
raw = self.load_raw(alias, conf, args.env_name)
183+
except KeyError:
184+
continue
185+
return self.build(alias, of_type, factory, conf, raw, args)
186+
return None
187+
174188
def build( # noqa: PLR0913
175189
self,
176190
key: str, # noqa: ARG002

src/tox/config/of_type.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from __future__ import annotations
44

55
from abc import ABC, abstractmethod
6-
from itertools import product
76
from typing import TYPE_CHECKING, Generic, TypeVar, cast
87

98
from tox.config.loader.api import ConfigLoadArgs, Loader
@@ -100,8 +99,9 @@ def __call__(
10099
args: ConfigLoadArgs,
101100
) -> T:
102101
if self._cache is _PLACE_HOLDER:
103-
for key, loader in product(self.keys, loaders):
104-
chain_key = f"{loader.section.key}.{key}"
102+
primary_key, *alias_keys = self.keys
103+
for loader in loaders:
104+
chain_key = f"{loader.section.key}.{primary_key}"
105105
try:
106106
if chain_key in args.chain:
107107
values = args.chain[args.chain.index(chain_key) :]
@@ -110,7 +110,7 @@ def __call__(
110110
finally:
111111
args.chain.append(chain_key)
112112
try:
113-
value = loader.load(key, self.of_type, self.factory, conf, args)
113+
value = loader.load(primary_key, self.of_type, self.factory, conf, args, all_keys=alias_keys)
114114
except KeyError:
115115
continue
116116
else:

tests/config/test_main.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,25 @@ def test_config_override_appends_to_empty_list(tox_ini_conf: ToxIniCreator) -> N
9595
assert conf["passenv"] == ["bar"]
9696

9797

98+
@pytest.mark.parametrize(
99+
("ini_key", "override_key"),
100+
[
101+
pytest.param("passenv", "pass_env", id="ini_old_override_new"),
102+
pytest.param("pass_env", "passenv", id="ini_new_override_old"),
103+
],
104+
)
105+
def test_config_override_append_alias_key(tox_ini_conf: ToxIniCreator, ini_key: str, override_key: str) -> None:
106+
example = f"""
107+
[testenv]
108+
{ini_key} = foo
109+
"""
110+
conf = tox_ini_conf(example, override=[Override(f"testenv.{override_key}+=bar")]).get_env("testenv")
111+
conf.add_config(["pass_env", "passenv"], of_type=list[str], default=[], desc="desc")
112+
result = conf["pass_env"]
113+
assert "foo" in result
114+
assert "bar" in result
115+
116+
98117
def test_config_override_appends_to_setenv(tox_ini_conf: ToxIniCreator) -> None:
99118
example = """
100119
[testenv]

0 commit comments

Comments
 (0)