Skip to content

Commit f5850c0

Browse files
authored
Support multiple override appends (#3261)
Currently only the last override supplied is considered, in conflict with now supported append overrides ("+="). This now enables appending multiple times via the command line.
1 parent c2be629 commit f5850c0

File tree

7 files changed

+108
-25
lines changed

7 files changed

+108
-25
lines changed

docs/changelog/3261.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add support for multiple appending override options (-x, --override) on command line - by :user:`amitschang`.

docs/config.rst

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1037,14 +1037,27 @@ You could add additional dependencies by running:
10371037

10381038
.. code-block:: bash
10391039
1040-
tox --override testenv.deps+=pytest-xdist,pytest-cov
1040+
tox --override testenv.deps+=pytest-xdist
10411041
10421042
You could set additional environment variables by running:
10431043

10441044
.. code-block:: bash
10451045
10461046
tox --override testenv.setenv+=baz=quux
10471047
1048+
You can specify overrides multiple times on the command line to append multiple items:
1049+
1050+
.. code-block:: bash
1051+
1052+
tox -x testenv.seteenv+=foo=bar -x testenv.setenv+=baz=quux
1053+
tox -x testenv.deps+=pytest-xdist -x testenv.deps+=pytest-cov
1054+
1055+
Or reset override and append to that (note the first override is ``=`` and not ``+=``):
1056+
1057+
.. code-block:: bash
1058+
1059+
tox -x testenv.deps=pytest-xdist -x testenv.deps+=pytest-cov
1060+
10481061
Set CLI flags via environment variables
10491062
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
10501063
All CLI flags can be set via environment variables too, the naming convention here is ``TOX_<option>``. E.g.

src/tox/config/loader/api.py

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ class Loader(Convert[T]):
8181

8282
def __init__(self, section: Section, overrides: list[Override]) -> None:
8383
self._section = section
84-
self.overrides: dict[str, Override] = {o.key: o for o in overrides}
84+
self.overrides: dict[str, list[Override]] = {}
85+
for override in overrides:
86+
self.overrides.setdefault(override.key, []).append(override)
8587
self.parent: Loader[Any] | None = None
8688

8789
@property
@@ -130,31 +132,35 @@ def load( # noqa: PLR0913
130132
"""
131133
from tox.config.set_env import SetEnv # noqa: PLC0415
132134

133-
override = self.overrides.get(key)
134-
if override:
135-
converted_override = _STR_CONVERT.to(override.value, of_type, factory)
136-
if not override.append:
137-
return converted_override
135+
overrides = self.overrides.get(key, [])
136+
138137
try:
139138
raw = self.load_raw(key, conf, args.env_name)
140139
except KeyError:
141-
if override:
142-
return converted_override
143-
raise
144-
converted = self.build(key, of_type, factory, conf, raw, args)
145-
if override and override.append:
146-
if isinstance(converted, list) and isinstance(converted_override, list):
147-
converted += converted_override
148-
elif isinstance(converted, dict) and isinstance(converted_override, dict):
149-
converted.update(converted_override)
150-
elif isinstance(converted, SetEnv) and isinstance(converted_override, SetEnv):
151-
converted.update(converted_override, override=True)
152-
elif isinstance(converted, PythonDeps) and isinstance(converted_override, PythonDeps):
153-
converted += converted_override
140+
converted = None
141+
if not overrides:
142+
raise
143+
else:
144+
converted = self.build(key, of_type, factory, conf, raw, args)
145+
146+
for override in overrides:
147+
converted_override = _STR_CONVERT.to(override.value, of_type, factory)
148+
if override.append and converted is not None:
149+
if isinstance(converted, list) and isinstance(converted_override, list):
150+
converted += converted_override
151+
elif isinstance(converted, dict) and isinstance(converted_override, dict):
152+
converted.update(converted_override)
153+
elif isinstance(converted, SetEnv) and isinstance(converted_override, SetEnv):
154+
converted.update(converted_override, override=True)
155+
elif isinstance(converted, PythonDeps) and isinstance(converted_override, PythonDeps):
156+
converted += converted_override
157+
else:
158+
msg = "Only able to append to lists and dicts"
159+
raise ValueError(msg)
154160
else:
155-
msg = "Only able to append to lists and dicts"
156-
raise ValueError(msg)
157-
return converted
161+
converted = converted_override
162+
163+
return converted # type: ignore[return-value]
158164

159165
def build( # noqa: PLR0913
160166
self,

tests/config/loader/ini/test_ini_loader.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def test_ini_loader_keys(mk_ini_conf: Callable[[str], ConfigParser]) -> None:
2121
def test_ini_loader_repr(mk_ini_conf: Callable[[str], ConfigParser]) -> None:
2222
core = IniSection(None, "tox")
2323
loader = IniLoader(core, mk_ini_conf("\n[tox]\n\na=b\nc=d\n\n"), [Override("tox.a=1")], core_section=core)
24-
assert repr(loader) == "IniLoader(section=tox, overrides={'a': Override('tox.a=1')})"
24+
assert repr(loader) == "IniLoader(section=tox, overrides={'a': [Override('tox.a=1')]})"
2525

2626

2727
def test_ini_loader_has_section(mk_ini_conf: Callable[[str], ConfigParser]) -> None:

tests/config/loader/test_loader.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ def test_override_append(flag: str) -> None:
4242
assert value.append is True
4343

4444

45+
@pytest.mark.parametrize("flag", ["-x", "--override"])
46+
def test_override_multiple(flag: str) -> None:
47+
parsed, _, __, ___, ____ = get_options(flag, "magic+=1", flag, "magic+=2")
48+
assert len(parsed.override) == 2
49+
50+
4551
def test_override_equals() -> None:
4652
assert Override("a=b") == Override("a=b")
4753

tests/config/loader/test_memory_loader.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def test_memory_loader_repr() -> None:
1818

1919
def test_memory_loader_override() -> None:
2020
loader = MemoryLoader(a=1)
21-
loader.overrides["a"] = Override("a=2")
21+
loader.overrides["a"] = [Override("a=2")]
2222
args = ConfigLoadArgs([], "name", None)
2323
loaded = loader.load("a", of_type=int, conf=None, factory=None, args=args)
2424
assert loaded == 2

tests/config/test_main.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,17 @@ def test_config_override_appends_to_list(tox_ini_conf: ToxIniCreator) -> None:
7878
assert conf["passenv"] == ["foo", "bar"]
7979

8080

81+
def test_config_override_sequence(tox_ini_conf: ToxIniCreator) -> None:
82+
example = """
83+
[testenv]
84+
passenv = foo
85+
"""
86+
overrides = [Override("testenv.passenv=bar"), Override("testenv.passenv+=baz")]
87+
conf = tox_ini_conf(example, override=overrides).get_env("testenv")
88+
conf.add_config("passenv", of_type=List[str], default=[], desc="desc")
89+
assert conf["passenv"] == ["bar", "baz"]
90+
91+
8192
def test_config_override_appends_to_empty_list(tox_ini_conf: ToxIniCreator) -> None:
8293
conf = tox_ini_conf("[testenv]", override=[Override("testenv.passenv+=bar")]).get_env("testenv")
8394
conf.add_config("passenv", of_type=List[str], default=[], desc="desc")
@@ -95,6 +106,35 @@ def test_config_override_appends_to_setenv(tox_ini_conf: ToxIniCreator) -> None:
95106
assert conf["setenv"].load("baz") == "quux"
96107

97108

109+
def test_config_override_appends_to_setenv_multiple(tox_ini_conf: ToxIniCreator) -> None:
110+
example = """
111+
[testenv]
112+
setenv =
113+
foo = bar
114+
"""
115+
overrides = [Override("testenv.setenv+=baz=quux"), Override("testenv.setenv+=less=more")]
116+
conf = tox_ini_conf(example, override=overrides).get_env("testenv")
117+
assert conf["setenv"].load("foo") == "bar"
118+
assert conf["setenv"].load("baz") == "quux"
119+
assert conf["setenv"].load("less") == "more"
120+
121+
122+
def test_config_override_sequential_processing(tox_ini_conf: ToxIniCreator) -> None:
123+
example = """
124+
[testenv]
125+
setenv =
126+
foo = bar
127+
"""
128+
overrides = [Override("testenv.setenv+=a=b"), Override("testenv.setenv=c=d"), Override("testenv.setenv+=e=f")]
129+
conf = tox_ini_conf(example, override=overrides).get_env("testenv")
130+
with pytest.raises(KeyError):
131+
assert conf["setenv"].load("foo") == "bar"
132+
with pytest.raises(KeyError):
133+
assert conf["setenv"].load("a") == "b"
134+
assert conf["setenv"].load("c") == "d"
135+
assert conf["setenv"].load("e") == "f"
136+
137+
98138
def test_config_override_appends_to_empty_setenv(tox_ini_conf: ToxIniCreator) -> None:
99139
conf = tox_ini_conf("[testenv]", override=[Override("testenv.setenv+=foo=bar")]).get_env("testenv")
100140
assert conf["setenv"].load("foo") == "bar"
@@ -116,6 +156,23 @@ def test_config_override_appends_to_pythondeps(tox_ini_conf: ToxIniCreator, tmp_
116156
assert conf["deps"].lines() == ["foo", "bar"]
117157

118158

159+
def test_config_multiple_overrides(tox_ini_conf: ToxIniCreator, tmp_path: Path) -> None:
160+
example = """
161+
[testenv]
162+
deps = foo
163+
"""
164+
overrides = [Override("testenv.deps+=bar"), Override("testenv.deps+=baz")]
165+
conf = tox_ini_conf(example, override=overrides).get_env("testenv")
166+
conf.add_config(
167+
"deps",
168+
of_type=PythonDeps,
169+
factory=partial(PythonDeps.factory, tmp_path),
170+
default=PythonDeps("", root=tmp_path),
171+
desc="desc",
172+
)
173+
assert conf["deps"].lines() == ["foo", "bar", "baz"]
174+
175+
119176
def test_config_override_appends_to_empty_pythondeps(tox_ini_conf: ToxIniCreator, tmp_path: Path) -> None:
120177
example = """
121178
[testenv]

0 commit comments

Comments
 (0)