Skip to content

Commit

Permalink
Allow {posargs} in setenv (tox-dev#1697)
Browse files Browse the repository at this point in the history
  • Loading branch information
jayvdb authored Oct 18, 2020
1 parent f0d8ae2 commit e4d0d60
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 22 deletions.
1 change: 1 addition & 0 deletions docs/changelog/1695.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow {posargs} in setenv. - by :user:`jayvdb`
62 changes: 40 additions & 22 deletions src/tox/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -1558,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:
Expand All @@ -1569,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:
Expand Down Expand Up @@ -1661,6 +1671,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)
Expand All @@ -1685,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)
Expand Down Expand Up @@ -1771,11 +1801,8 @@ 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)
if sub_value == "posargs":
return self.reader.getposargs(match.group("default_value"))

try:
sub_type = g["sub_type"]
Expand All @@ -1790,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),
Expand Down Expand Up @@ -1883,28 +1912,17 @@ 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
# shlex.
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)
Expand Down
15 changes: 15 additions & 0 deletions tests/unit/config/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down

0 comments on commit e4d0d60

Please sign in to comment.