From 4e1e0ed505c2451f91202e1a40e580aeae0276b7 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Sun, 18 Oct 2020 17:57:22 +0700 Subject: [PATCH 1/2] Remove special substitutions {packages} & {opts} Escape {packages} and {opts} so they are literals allowed only in `install_command`. --- src/tox/config/__init__.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/tox/config/__init__.py b/src/tox/config/__init__.py index 197d14c34..cea998268 100644 --- a/src/tox/config/__init__.py +++ b/src/tox/config/__init__.py @@ -240,8 +240,8 @@ def postprocess(self, testenv_config, value): class InstallcmdOption: name = "install_command" - type = "argv" - default = "python -m pip install {opts} {packages}" + type = "argv_install_command" + default = r"python -m pip install \{opts\} \{packages\}" help = "install command for dependencies and package under test." def postprocess(self, testenv_config, value): @@ -1374,6 +1374,7 @@ def make_envconfig(self, name, section, subs, config, replace=True): "dict_setenv", "argv", "argvlist", + "argv_install_command", ): meth = getattr(reader, "get{}".format(atype)) res = meth(env_attr.name, env_attr.default, replace=replace) @@ -1661,6 +1662,15 @@ def getargvlist(self, name, default="", replace=True): def getargv(self, name, default="", replace=True): return self.getargvlist(name, default, replace=replace)[0] + def getargv_install_command(self, name, default="", replace=True): + s = self.getstring(name, default, replace=False) + if "{packages}" in s: + s = s.replace("{packages}", r"\{packages\}") + if "{opts}" in s: + s = s.replace("{opts}", r"\{opts\}") + + return _ArgvlistReader.getargvlist(self, s, replace=replace)[0] + def getstring(self, name, default=None, replace=True, crossonly=False, no_fallback=False): x = None sections = [self.section_name] + ([] if no_fallback else self.fallbacksections) @@ -1771,12 +1781,6 @@ def _replace_match(self, match): if not any(g.values()): return os.pathsep - # special case: opts and packages. Leave {opts} and - # {packages} intact, they are replaced manually in - # _venv.VirtualEnv.run_install_command. - if sub_value in ("opts", "packages"): - return "{{{}}}".format(sub_value) - try: sub_type = g["sub_type"] except KeyError: From d3ef0b4e77e5f32a842bcb4cef19f491d1ebd9b7 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Sun, 18 Oct 2020 21:35:15 +0700 Subject: [PATCH 2/2] Allow {posargs} in setenv Fixes https://github.com/tox-dev/tox/issues/1695 --- CONTRIBUTORS | 1 + docs/changelog/1695.feature.rst | 1 + src/tox/config/__init__.py | 44 +++++++++++++++++++++----------- tests/unit/config/test_config.py | 15 +++++++++++ 4 files changed, 46 insertions(+), 15 deletions(-) create mode 100644 docs/changelog/1695.feature.rst diff --git a/CONTRIBUTORS b/CONTRIBUTORS index c98ce617e..1f9b8e387 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -48,6 +48,7 @@ Jake Windle Jannis Leidel Joachim Brandon LeBlanc Johannes Christ +John Mark Vandenberg Jon Dufresne Josh Smeaton Josh Snyder diff --git a/docs/changelog/1695.feature.rst b/docs/changelog/1695.feature.rst new file mode 100644 index 000000000..b80d763b3 --- /dev/null +++ b/docs/changelog/1695.feature.rst @@ -0,0 +1 @@ +Allow {posargs} in setenv. - by :user:`jayvdb` diff --git a/src/tox/config/__init__.py b/src/tox/config/__init__.py index cea998268..96c51f212 100644 --- a/src/tox/config/__init__.py +++ b/src/tox/config/__init__.py @@ -1559,7 +1559,15 @@ def __repr__(self): class SectionReader: - def __init__(self, section_name, cfgparser, fallbacksections=None, factors=(), prefix=None): + def __init__( + self, + section_name, + cfgparser, + fallbacksections=None, + factors=(), + prefix=None, + posargs="", + ): if prefix is None: self.section_name = section_name else: @@ -1570,6 +1578,7 @@ def __init__(self, section_name, cfgparser, fallbacksections=None, factors=(), p self._subs = {} self._subststack = [] self._setenv = None + self.posargs = posargs def get_environ_value(self, name): if self._setenv is None: @@ -1695,6 +1704,17 @@ def getstring(self, name, default=None, replace=True, crossonly=False, no_fallba x = self._replace_if_needed(x, name, replace, crossonly) return x + def getposargs(self, default=None): + if self.posargs: + posargs = self.posargs + if sys.platform.startswith("win"): + posargs_string = list2cmdline([x for x in posargs if x]) + else: + posargs_string = " ".join([shlex_quote(x) for x in posargs if x]) + return posargs_string + else: + return default or "" + def _replace_if_needed(self, x, name, replace, crossonly): if replace and x and hasattr(x, "replace"): x = self._replace(x, name=name, crossonly=crossonly) @@ -1781,6 +1801,9 @@ def _replace_match(self, match): if not any(g.values()): return os.pathsep + if sub_value == "posargs": + return self.reader.getposargs(match.group("default_value")) + try: sub_type = g["sub_type"] except KeyError: @@ -1794,6 +1817,8 @@ def _replace_match(self, match): if is_interactive(): return match.group("substitution_value") return match.group("default_value") + if sub_type == "posargs": + return self.reader.getposargs(match.group("substitution_value")) if sub_type is not None: raise tox.exception.ConfigError( "No support for the {} substitution type".format(sub_type), @@ -1887,12 +1912,6 @@ def getargvlist(cls, reader, value, replace=True): @classmethod def processcommand(cls, reader, command, replace=True): - posargs = getattr(reader, "posargs", "") - if sys.platform.startswith("win"): - posargs_string = list2cmdline([x for x in posargs if x]) - else: - posargs_string = " ".join([shlex_quote(x) for x in posargs if x]) - # Iterate through each word of the command substituting as # appropriate to construct the new command string. This # string is then broken up into exec argv components using @@ -1900,15 +1919,10 @@ def processcommand(cls, reader, command, replace=True): if replace: newcommand = "" for word in CommandParser(command).words(): - if word == "{posargs}" or word == "[]": - newcommand += posargs_string + if word == "[]": + newcommand += reader.getposargs() continue - elif word.startswith("{posargs:") and word.endswith("}"): - if posargs: - newcommand += posargs_string - continue - else: - word = word[9:-1] + new_arg = "" new_word = reader._replace(word) new_word = reader._replace(new_word) diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index 7eb2ffbc8..7e6a71b87 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -441,6 +441,21 @@ def test_command_env_substitution(self, newconfig): assert envconfig.commands == [["ls", "testvalue"]] assert envconfig.setenv["TEST"] == "testvalue" + def test_command_env_substitution_posargs(self, newconfig): + """Ensure {posargs} values are substituted correctly.""" + config = newconfig( + """ + [testenv:py27] + setenv = + TEST={posargs:default} + commands = + ls {env:TEST} + """, + ) + envconfig = config.envconfigs["py27"] + assert envconfig.commands == [["ls", "default"]] + assert envconfig.setenv["TEST"] == "default" + def test_command_env_substitution_global(self, newconfig): """Ensure referenced {env:key:default} values are substituted correctly.""" config = newconfig(