Skip to content

Commit

Permalink
Support multiple override appends (#3261)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
amitschang committed Apr 3, 2024
1 parent c2be629 commit f5850c0
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 25 deletions.
1 change: 1 addition & 0 deletions docs/changelog/3261.feature.rst
@@ -0,0 +1 @@
Add support for multiple appending override options (-x, --override) on command line - by :user:`amitschang`.
15 changes: 14 additions & 1 deletion docs/config.rst
Expand Up @@ -1037,14 +1037,27 @@ You could add additional dependencies by running:

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

.. code-block:: bash
tox --override testenv.setenv+=baz=quux
You can specify overrides multiple times on the command line to append multiple items:

.. code-block:: bash
tox -x testenv.seteenv+=foo=bar -x testenv.setenv+=baz=quux
tox -x testenv.deps+=pytest-xdist -x testenv.deps+=pytest-cov
Or reset override and append to that (note the first override is ``=`` and not ``+=``):

.. code-block:: bash
tox -x testenv.deps=pytest-xdist -x testenv.deps+=pytest-cov
Set CLI flags via environment variables
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
All CLI flags can be set via environment variables too, the naming convention here is ``TOX_<option>``. E.g.
Expand Down
50 changes: 28 additions & 22 deletions src/tox/config/loader/api.py
Expand Up @@ -81,7 +81,9 @@ class Loader(Convert[T]):

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

@property
Expand Down Expand Up @@ -130,31 +132,35 @@ def load( # noqa: PLR0913
"""
from tox.config.set_env import SetEnv # noqa: PLC0415

override = self.overrides.get(key)
if override:
converted_override = _STR_CONVERT.to(override.value, of_type, factory)
if not override.append:
return converted_override
overrides = self.overrides.get(key, [])

try:
raw = self.load_raw(key, conf, args.env_name)
except KeyError:
if override:
return converted_override
raise
converted = self.build(key, of_type, factory, conf, raw, args)
if override and override.append:
if isinstance(converted, list) and isinstance(converted_override, list):
converted += converted_override
elif isinstance(converted, dict) and isinstance(converted_override, dict):
converted.update(converted_override)
elif isinstance(converted, SetEnv) and isinstance(converted_override, SetEnv):
converted.update(converted_override, override=True)
elif isinstance(converted, PythonDeps) and isinstance(converted_override, PythonDeps):
converted += converted_override
converted = None
if not overrides:
raise
else:
converted = self.build(key, of_type, factory, conf, raw, args)

for override in overrides:
converted_override = _STR_CONVERT.to(override.value, of_type, factory)
if override.append and converted is not None:
if isinstance(converted, list) and isinstance(converted_override, list):
converted += converted_override
elif isinstance(converted, dict) and isinstance(converted_override, dict):
converted.update(converted_override)
elif isinstance(converted, SetEnv) and isinstance(converted_override, SetEnv):
converted.update(converted_override, override=True)
elif isinstance(converted, PythonDeps) and isinstance(converted_override, PythonDeps):
converted += converted_override
else:
msg = "Only able to append to lists and dicts"
raise ValueError(msg)
else:
msg = "Only able to append to lists and dicts"
raise ValueError(msg)
return converted
converted = converted_override

return converted # type: ignore[return-value]

def build( # noqa: PLR0913
self,
Expand Down
2 changes: 1 addition & 1 deletion tests/config/loader/ini/test_ini_loader.py
Expand Up @@ -21,7 +21,7 @@ def test_ini_loader_keys(mk_ini_conf: Callable[[str], ConfigParser]) -> None:
def test_ini_loader_repr(mk_ini_conf: Callable[[str], ConfigParser]) -> None:
core = IniSection(None, "tox")
loader = IniLoader(core, mk_ini_conf("\n[tox]\n\na=b\nc=d\n\n"), [Override("tox.a=1")], core_section=core)
assert repr(loader) == "IniLoader(section=tox, overrides={'a': Override('tox.a=1')})"
assert repr(loader) == "IniLoader(section=tox, overrides={'a': [Override('tox.a=1')]})"


def test_ini_loader_has_section(mk_ini_conf: Callable[[str], ConfigParser]) -> None:
Expand Down
6 changes: 6 additions & 0 deletions tests/config/loader/test_loader.py
Expand Up @@ -42,6 +42,12 @@ def test_override_append(flag: str) -> None:
assert value.append is True


@pytest.mark.parametrize("flag", ["-x", "--override"])
def test_override_multiple(flag: str) -> None:
parsed, _, __, ___, ____ = get_options(flag, "magic+=1", flag, "magic+=2")
assert len(parsed.override) == 2


def test_override_equals() -> None:
assert Override("a=b") == Override("a=b")

Expand Down
2 changes: 1 addition & 1 deletion tests/config/loader/test_memory_loader.py
Expand Up @@ -18,7 +18,7 @@ def test_memory_loader_repr() -> None:

def test_memory_loader_override() -> None:
loader = MemoryLoader(a=1)
loader.overrides["a"] = Override("a=2")
loader.overrides["a"] = [Override("a=2")]
args = ConfigLoadArgs([], "name", None)
loaded = loader.load("a", of_type=int, conf=None, factory=None, args=args)
assert loaded == 2
Expand Down
57 changes: 57 additions & 0 deletions tests/config/test_main.py
Expand Up @@ -78,6 +78,17 @@ def test_config_override_appends_to_list(tox_ini_conf: ToxIniCreator) -> None:
assert conf["passenv"] == ["foo", "bar"]


def test_config_override_sequence(tox_ini_conf: ToxIniCreator) -> None:
example = """
[testenv]
passenv = foo
"""
overrides = [Override("testenv.passenv=bar"), Override("testenv.passenv+=baz")]
conf = tox_ini_conf(example, override=overrides).get_env("testenv")
conf.add_config("passenv", of_type=List[str], default=[], desc="desc")
assert conf["passenv"] == ["bar", "baz"]


def test_config_override_appends_to_empty_list(tox_ini_conf: ToxIniCreator) -> None:
conf = tox_ini_conf("[testenv]", override=[Override("testenv.passenv+=bar")]).get_env("testenv")
conf.add_config("passenv", of_type=List[str], default=[], desc="desc")
Expand All @@ -95,6 +106,35 @@ def test_config_override_appends_to_setenv(tox_ini_conf: ToxIniCreator) -> None:
assert conf["setenv"].load("baz") == "quux"


def test_config_override_appends_to_setenv_multiple(tox_ini_conf: ToxIniCreator) -> None:
example = """
[testenv]
setenv =
foo = bar
"""
overrides = [Override("testenv.setenv+=baz=quux"), Override("testenv.setenv+=less=more")]
conf = tox_ini_conf(example, override=overrides).get_env("testenv")
assert conf["setenv"].load("foo") == "bar"
assert conf["setenv"].load("baz") == "quux"
assert conf["setenv"].load("less") == "more"


def test_config_override_sequential_processing(tox_ini_conf: ToxIniCreator) -> None:
example = """
[testenv]
setenv =
foo = bar
"""
overrides = [Override("testenv.setenv+=a=b"), Override("testenv.setenv=c=d"), Override("testenv.setenv+=e=f")]
conf = tox_ini_conf(example, override=overrides).get_env("testenv")
with pytest.raises(KeyError):
assert conf["setenv"].load("foo") == "bar"
with pytest.raises(KeyError):
assert conf["setenv"].load("a") == "b"
assert conf["setenv"].load("c") == "d"
assert conf["setenv"].load("e") == "f"


def test_config_override_appends_to_empty_setenv(tox_ini_conf: ToxIniCreator) -> None:
conf = tox_ini_conf("[testenv]", override=[Override("testenv.setenv+=foo=bar")]).get_env("testenv")
assert conf["setenv"].load("foo") == "bar"
Expand All @@ -116,6 +156,23 @@ def test_config_override_appends_to_pythondeps(tox_ini_conf: ToxIniCreator, tmp_
assert conf["deps"].lines() == ["foo", "bar"]


def test_config_multiple_overrides(tox_ini_conf: ToxIniCreator, tmp_path: Path) -> None:
example = """
[testenv]
deps = foo
"""
overrides = [Override("testenv.deps+=bar"), Override("testenv.deps+=baz")]
conf = tox_ini_conf(example, override=overrides).get_env("testenv")
conf.add_config(
"deps",
of_type=PythonDeps,
factory=partial(PythonDeps.factory, tmp_path),
default=PythonDeps("", root=tmp_path),
desc="desc",
)
assert conf["deps"].lines() == ["foo", "bar", "baz"]


def test_config_override_appends_to_empty_pythondeps(tox_ini_conf: ToxIniCreator, tmp_path: Path) -> None:
example = """
[testenv]
Expand Down

0 comments on commit f5850c0

Please sign in to comment.