diff --git a/docs/changelog/3087.feature.rst b/docs/changelog/3087.feature.rst new file mode 100644 index 0000000000..e8ad4877b5 --- /dev/null +++ b/docs/changelog/3087.feature.rst @@ -0,0 +1,3 @@ +``--override`` can now take options in the form of ``foo+=bar`` which +will append ``bar`` to the end of an existing list, rather than +replacing it. diff --git a/src/tox/config/loader/api.py b/src/tox/config/loader/api.py index c8fb61f0ef..3edfb49c13 100644 --- a/src/tox/config/loader/api.py +++ b/src/tox/config/loader/api.py @@ -24,6 +24,12 @@ def __init__(self, value: str) -> None: if not equal: msg = f"override {value} has no = sign in it" raise ArgumentTypeError(msg) + + self.append = False + if key.endswith("+"): # key += value appends to a list + key = key[:-1] + self.append = True + self.namespace, _, self.key = key.rpartition(".") def __repr__(self) -> str: @@ -117,10 +123,14 @@ def load( # noqa: PLR0913 :param args: the config load arguments :return: the converted type """ - if key in self.overrides: - return _STR_CONVERT.to(self.overrides[key].value, of_type, factory) + override = self.overrides.get(key) + if override and not override.append: + return _STR_CONVERT.to(override.value, of_type, factory) raw = self.load_raw(key, conf, args.env_name) - return self.build(key, of_type, factory, conf, raw, args) + converted = self.build(key, of_type, factory, conf, raw, args) + if override and override.append: + converted += _STR_CONVERT.to(override.value, of_type, factory) + return converted def build( # noqa: PLR0913 self, diff --git a/tests/config/loader/test_loader.py b/tests/config/loader/test_loader.py index 44e649a5df..b00a5a24c1 100644 --- a/tests/config/loader/test_loader.py +++ b/tests/config/loader/test_loader.py @@ -28,6 +28,18 @@ def test_override_add(flag: str) -> None: assert value.key == "magic" assert value.value == "true" assert not value.namespace + assert value.append is False + + +@pytest.mark.parametrize("flag", ["-x", "--override"]) +def test_override_append(flag: str) -> None: + parsed, _, __, ___, ____ = get_options(flag, "magic+=true") + assert len(parsed.override) == 1 + value = parsed.override[0] + assert value.key == "magic" + assert value.value == "true" + assert not value.namespace + assert value.append is True def test_override_equals() -> None: diff --git a/tests/config/test_main.py b/tests/config/test_main.py index 46107db738..78c4e7f7c6 100644 --- a/tests/config/test_main.py +++ b/tests/config/test_main.py @@ -2,7 +2,7 @@ import os from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List from tox.config.loader.api import Override from tox.config.loader.memory import MemoryLoader @@ -64,6 +64,16 @@ def test_config_override_wins_memory_loader(tox_ini_conf: ToxIniCreator) -> None assert conf["c"] == "ok" +def test_config_override_appends(tox_ini_conf: ToxIniCreator) -> None: + example = """ + [testenv] + passenv = foo + """ + conf = tox_ini_conf(example, override=[Override("testenv.passenv+=bar")]).get_env("testenv") + conf.add_config("passenv", of_type=List[str], default=[], desc="desc") + assert conf["passenv"] == ["foo", "bar"] + + def test_args_are_paths_when_disabled(tox_project: ToxProjectCreator) -> None: ini = "[testenv]\npackage=skip\ncommands={posargs}\nargs_are_paths=False" project = tox_project({"tox.ini": ini, "w": {"a.txt": "a"}})