Skip to content

Commit

Permalink
Support multiple override appends
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 c3eef39
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 25 deletions.
15 changes: 14 additions & 1 deletion docs/config.rst
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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 c3eef39

Please sign in to comment.