From 7341ac40fd76328718b5edc3a7f73efc77887373 Mon Sep 17 00:00:00 2001 From: Stefano Rivera Date: Fri, 11 Aug 2023 22:27:16 +0200 Subject: [PATCH] Allow extending lists with --override foo+=bar Allow appending to a list with += syntax, instead of replacing the existing value. Fixes: #3087 --- docs/changelog/3087.feature.rst | 3 +++ docs/config.rst | 22 ++++++++++++++++++++++ src/tox/config/loader/api.py | 21 ++++++++++++++++++--- tests/config/loader/test_loader.py | 12 ++++++++++++ tests/config/test_main.py | 25 ++++++++++++++++++++++++- 5 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 docs/changelog/3087.feature.rst 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/docs/config.rst b/docs/config.rst index 50e6aa2429..0dd18111b8 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -958,3 +958,25 @@ Other Substitutions * ``{}`` - replaced as ``os.pathsep`` * ``{/}`` - replaced as ``os.sep`` + +Overriding configuration from the command line +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can override options in the configuration file, from the command +line. + +For example, given this config: + +.. code-block:: ini + + [testenv] + deps = pytest + commands = pytest tests + +You could enable ``ignore_errors`` by running:: + + tox --override testenv.ignore_errors=True + +You could add additional dependencies by running:: + + tox --override testenv.deps+=pytest-xdist,pytest-cov diff --git a/src/tox/config/loader/api.py b/src/tox/config/loader/api.py index c8fb61f0ef..0349216e0a 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,19 @@ 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: + appends = _STR_CONVERT.to(override.value, of_type, factory) + if isinstance(converted, list) and isinstance(appends, list): + converted += appends + else: + msg = "Only able to append to lists" + raise ValueError(msg) + 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..fd21daf472 100644 --- a/tests/config/test_main.py +++ b/tests/config/test_main.py @@ -2,7 +2,9 @@ import os from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List + +import pytest from tox.config.loader.api import Override from tox.config.loader.memory import MemoryLoader @@ -64,6 +66,27 @@ 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_config_override_cannot_append(tox_ini_conf: ToxIniCreator) -> None: + example = """ + [testenv] + foo = 1 + """ + conf = tox_ini_conf(example, override=[Override("testenv.foo+=2")]).get_env("testenv") + conf.add_config("foo", of_type=int, default=0, desc="desc") + with pytest.raises(ValueError): + conf["foo"] + + 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"}})