Skip to content

Commit e4d0d60

Browse files
authored
Allow {posargs} in setenv (tox-dev#1697)
1 parent f0d8ae2 commit e4d0d60

File tree

3 files changed

+56
-22
lines changed

3 files changed

+56
-22
lines changed

docs/changelog/1695.feature.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow {posargs} in setenv. - by :user:`jayvdb`

src/tox/config/__init__.py

+40-22
Original file line numberDiff line numberDiff line change
@@ -240,8 +240,8 @@ def postprocess(self, testenv_config, value):
240240

241241
class InstallcmdOption:
242242
name = "install_command"
243-
type = "argv"
244-
default = "python -m pip install {opts} {packages}"
243+
type = "argv_install_command"
244+
default = r"python -m pip install \{opts\} \{packages\}"
245245
help = "install command for dependencies and package under test."
246246

247247
def postprocess(self, testenv_config, value):
@@ -1374,6 +1374,7 @@ def make_envconfig(self, name, section, subs, config, replace=True):
13741374
"dict_setenv",
13751375
"argv",
13761376
"argvlist",
1377+
"argv_install_command",
13771378
):
13781379
meth = getattr(reader, "get{}".format(atype))
13791380
res = meth(env_attr.name, env_attr.default, replace=replace)
@@ -1558,7 +1559,15 @@ def __repr__(self):
15581559

15591560

15601561
class SectionReader:
1561-
def __init__(self, section_name, cfgparser, fallbacksections=None, factors=(), prefix=None):
1562+
def __init__(
1563+
self,
1564+
section_name,
1565+
cfgparser,
1566+
fallbacksections=None,
1567+
factors=(),
1568+
prefix=None,
1569+
posargs="",
1570+
):
15621571
if prefix is None:
15631572
self.section_name = section_name
15641573
else:
@@ -1569,6 +1578,7 @@ def __init__(self, section_name, cfgparser, fallbacksections=None, factors=(), p
15691578
self._subs = {}
15701579
self._subststack = []
15711580
self._setenv = None
1581+
self.posargs = posargs
15721582

15731583
def get_environ_value(self, name):
15741584
if self._setenv is None:
@@ -1661,6 +1671,15 @@ def getargvlist(self, name, default="", replace=True):
16611671
def getargv(self, name, default="", replace=True):
16621672
return self.getargvlist(name, default, replace=replace)[0]
16631673

1674+
def getargv_install_command(self, name, default="", replace=True):
1675+
s = self.getstring(name, default, replace=False)
1676+
if "{packages}" in s:
1677+
s = s.replace("{packages}", r"\{packages\}")
1678+
if "{opts}" in s:
1679+
s = s.replace("{opts}", r"\{opts\}")
1680+
1681+
return _ArgvlistReader.getargvlist(self, s, replace=replace)[0]
1682+
16641683
def getstring(self, name, default=None, replace=True, crossonly=False, no_fallback=False):
16651684
x = None
16661685
sections = [self.section_name] + ([] if no_fallback else self.fallbacksections)
@@ -1685,6 +1704,17 @@ def getstring(self, name, default=None, replace=True, crossonly=False, no_fallba
16851704
x = self._replace_if_needed(x, name, replace, crossonly)
16861705
return x
16871706

1707+
def getposargs(self, default=None):
1708+
if self.posargs:
1709+
posargs = self.posargs
1710+
if sys.platform.startswith("win"):
1711+
posargs_string = list2cmdline([x for x in posargs if x])
1712+
else:
1713+
posargs_string = " ".join([shlex_quote(x) for x in posargs if x])
1714+
return posargs_string
1715+
else:
1716+
return default or ""
1717+
16881718
def _replace_if_needed(self, x, name, replace, crossonly):
16891719
if replace and x and hasattr(x, "replace"):
16901720
x = self._replace(x, name=name, crossonly=crossonly)
@@ -1771,11 +1801,8 @@ def _replace_match(self, match):
17711801
if not any(g.values()):
17721802
return os.pathsep
17731803

1774-
# special case: opts and packages. Leave {opts} and
1775-
# {packages} intact, they are replaced manually in
1776-
# _venv.VirtualEnv.run_install_command.
1777-
if sub_value in ("opts", "packages"):
1778-
return "{{{}}}".format(sub_value)
1804+
if sub_value == "posargs":
1805+
return self.reader.getposargs(match.group("default_value"))
17791806

17801807
try:
17811808
sub_type = g["sub_type"]
@@ -1790,6 +1817,8 @@ def _replace_match(self, match):
17901817
if is_interactive():
17911818
return match.group("substitution_value")
17921819
return match.group("default_value")
1820+
if sub_type == "posargs":
1821+
return self.reader.getposargs(match.group("substitution_value"))
17931822
if sub_type is not None:
17941823
raise tox.exception.ConfigError(
17951824
"No support for the {} substitution type".format(sub_type),
@@ -1883,28 +1912,17 @@ def getargvlist(cls, reader, value, replace=True):
18831912

18841913
@classmethod
18851914
def processcommand(cls, reader, command, replace=True):
1886-
posargs = getattr(reader, "posargs", "")
1887-
if sys.platform.startswith("win"):
1888-
posargs_string = list2cmdline([x for x in posargs if x])
1889-
else:
1890-
posargs_string = " ".join([shlex_quote(x) for x in posargs if x])
1891-
18921915
# Iterate through each word of the command substituting as
18931916
# appropriate to construct the new command string. This
18941917
# string is then broken up into exec argv components using
18951918
# shlex.
18961919
if replace:
18971920
newcommand = ""
18981921
for word in CommandParser(command).words():
1899-
if word == "{posargs}" or word == "[]":
1900-
newcommand += posargs_string
1922+
if word == "[]":
1923+
newcommand += reader.getposargs()
19011924
continue
1902-
elif word.startswith("{posargs:") and word.endswith("}"):
1903-
if posargs:
1904-
newcommand += posargs_string
1905-
continue
1906-
else:
1907-
word = word[9:-1]
1925+
19081926
new_arg = ""
19091927
new_word = reader._replace(word)
19101928
new_word = reader._replace(new_word)

tests/unit/config/test_config.py

+15
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,21 @@ def test_command_env_substitution(self, newconfig):
441441
assert envconfig.commands == [["ls", "testvalue"]]
442442
assert envconfig.setenv["TEST"] == "testvalue"
443443

444+
def test_command_env_substitution_posargs(self, newconfig):
445+
"""Ensure {posargs} values are substituted correctly."""
446+
config = newconfig(
447+
"""
448+
[testenv:py27]
449+
setenv =
450+
TEST={posargs:default}
451+
commands =
452+
ls {env:TEST}
453+
""",
454+
)
455+
envconfig = config.envconfigs["py27"]
456+
assert envconfig.commands == [["ls", "default"]]
457+
assert envconfig.setenv["TEST"] == "default"
458+
444459
def test_command_env_substitution_global(self, newconfig):
445460
"""Ensure referenced {env:key:default} values are substituted correctly."""
446461
config = newconfig(

0 commit comments

Comments
 (0)