3 new commits in tox: https://bitbucket.org/hpk42/tox/commits/f6fc0af4ceb5/ Changeset: f6fc0af4ceb5 Branch: issue285 User: hpk42 Date: 2015-11-13 09:48:43+00:00 Summary: add various xfailing test cases for env substitution in setenv Affected #: 1 file
diff -r f6cca79ba7f6522893ab720e1a5d09ab38fd3543 -r f6fc0af4ceb5e0b313008ffe7f1a523ffe8d7fc1 tests/test_config.py --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1598,6 +1598,47 @@ self._check_hashseed(envconfigs["hash2"], '123456789') +class TestSetenv: + @pytest.mark.xfail(reason="fix pending") + def test_setenv_uses_os_environ(self, tmpdir, newconfig, monkeypatch): + monkeypatch.setenv("X", "1") + config = newconfig(""" + [testenv:env1] + setenv = + X = {env:X} + """) + assert config.envconfigs["env1"].setenv["X"] == "1" + + @pytest.mark.xfail(reason="fix pending") + def test_setenv_default_os_environ(self, tmpdir, newconfig, monkeypatch): + monkeypatch.delenv("X", raising=False) + config = newconfig(""" + [testenv:env1] + setenv = + X = {env:X:2} + """) + assert config.envconfigs["env1"].setenv["X"] == "2" + + @pytest.mark.xfail(reason="fix pending") + def test_setenv_uses_other_setenv(self, tmpdir, newconfig): + config = newconfig(""" + [testenv:env1] + setenv = + Y = 5 + X = {env:Y} + """) + assert config.envconfigs["env1"].setenv["X"] == "5" + + @pytest.mark.xfail(reason="fix pending") + def test_setenv_recursive_direct(self, tmpdir, newconfig): + config = newconfig(""" + [testenv:env1] + setenv = + X = {env:X:3} + """) + assert config.envconfigs["env1"].setenv["X"] == "3" + + class TestIndexServer: def test_indexserver(self, tmpdir, newconfig): config = newconfig(""" https://bitbucket.org/hpk42/tox/commits/fd4af25ed954/ Changeset: fd4af25ed954 Branch: issue285 User: hpk42 Date: 2015-11-13 09:48:45+00:00 Summary: put replacing/substitution into own class Affected #: 1 file diff -r f6fc0af4ceb5e0b313008ffe7f1a523ffe8d7fc1 -r fd4af25ed9541539b969146305375064320666cd tox/config.py --- a/tox/config.py +++ b/tox/config.py @@ -799,16 +799,6 @@ is_section_substitution = re.compile("{\[[^{}\s]+\]\S+?}").match -RE_ITEM_REF = re.compile( - r''' - (?<!\\)[{] - (?:(?P<sub_type>[^[:{}]+):)? # optional sub_type for special rules - (?P<substitution_value>[^{}]*) # substitution key - [}] - ''', - re.VERBOSE) - - class SectionReader: def __init__(self, section_name, cfgparser, fallbacksections=None, factors=()): self.section_name = section_name @@ -888,11 +878,7 @@ x = self._apply_factors(x) if replace and x and hasattr(x, 'replace'): - self._subststack.append((self.section_name, name)) - try: - x = self._replace(x) - finally: - assert self._subststack.pop() == (self.section_name, name) + x = self._replace(x, name=name) # print "getstring", self.section_name, name, "returned", repr(x) return x @@ -909,8 +895,61 @@ lines = s.strip().splitlines() return '\n'.join(filter(None, map(factor_line, lines))) + def _replace(self, value, name=None, section_name=None): + if '{' not in value: + return value + + section_name = section_name if section_name else self.section_name + self._subststack.append((section_name, name)) + try: + return Replacer(self).do_replace(value) + finally: + assert self._subststack.pop() == (section_name, name) + + +class Replacer: + RE_ITEM_REF = re.compile( + r''' + (?<!\\)[{] + (?:(?P<sub_type>[^[:{}]+):)? # optional sub_type for special rules + (?P<substitution_value>[^{}]*) # substitution key + [}] + ''', + re.VERBOSE) + + + def __init__(self, reader, opt_replace_env=True): + self.reader = reader + self.opt_replace_env = opt_replace_env + + def do_replace(self, x): + return self.RE_ITEM_REF.sub(self._replace_match, x) + + def _replace_match(self, match): + g = match.groupdict() + + # special case: opts and packages. Leave {opts} and + # {packages} intact, they are replaced manually in + # _venv.VirtualEnv.run_install_command. + sub_value = g['substitution_value'] + if sub_value in ('opts', 'packages'): + return '{%s}' % sub_value + + try: + sub_type = g['sub_type'] + except KeyError: + raise tox.exception.ConfigError( + "Malformed substitution; no substitution type provided") + + if sub_type == "env": + assert self.opt_replace_env + return self._replace_env(match) + if sub_type != None: + raise tox.exception.ConfigError("No support for the %s substitution type" % sub_type) + return self._replace_substitution(match) + def _replace_env(self, match): - env_list = self.getdict('setenv') + env_list = self.reader.getdict('setenv') match_value = match.group('substitution_value') if not match_value: raise tox.exception.ConfigError( @@ -938,60 +977,26 @@ if key.startswith("[") and "]" in key: i = key.find("]") section, item = key[1:i], key[i + 1:] - if section in self._cfg and item in self._cfg[section]: - if (section, item) in self._subststack: + cfg = self.reader._cfg + if section in cfg and item in cfg[section]: + if (section, item) in self.reader._subststack: raise ValueError('%s already in %s' % ( - (section, item), self._subststack)) - x = str(self._cfg[section][item]) - self._subststack.append((section, item)) - try: - return self._replace(x) - finally: - self._subststack.pop() + (section, item), self.reader._subststack)) + x = str(cfg[section][item]) + return self.reader._replace(x, name=item, section_name=section) raise tox.exception.ConfigError( "substitution key %r not found" % key) def _replace_substitution(self, match): sub_key = match.group('substitution_value') - val = self._subs.get(sub_key, None) + val = self.reader._subs.get(sub_key, None) if val is None: val = self._substitute_from_other_section(sub_key) if py.builtin.callable(val): val = val() return str(val) - def _replace_match(self, match): - g = match.groupdict() - - # special case: opts and packages. Leave {opts} and - # {packages} intact, they are replaced manually in - # _venv.VirtualEnv.run_install_command. - sub_value = g['substitution_value'] - if sub_value in ('opts', 'packages'): - return '{%s}' % sub_value - - handlers = { - 'env': self._replace_env, - None: self._replace_substitution, - } - try: - sub_type = g['sub_type'] - except KeyError: - raise tox.exception.ConfigError( - "Malformed substitution; no substitution type provided") - - try: - handler = handlers[sub_type] - except KeyError: - raise tox.exception.ConfigError("No support for the %s substitution type" % sub_type) - - return handler(match) - - def _replace(self, x): - if '{' in x: - return RE_ITEM_REF.sub(self._replace_match, x) - return x class _ArgvlistReader: https://bitbucket.org/hpk42/tox/commits/a6d3e1956d85/ Changeset: a6d3e1956d85 Branch: issue285 User: hpk42 Date: 2015-11-13 09:49:05+00:00 Summary: address issue285 but not fully resolve it: setenv processing now works, making all config tests pass except for one substitution issue which requires yet some more logic, probably. Affected #: 2 files diff -r fd4af25ed9541539b969146305375064320666cd -r a6d3e1956d8516889b1ee3275257e397fe1dfd51 tests/test_config.py --- a/tests/test_config.py +++ b/tests/test_config.py @@ -283,18 +283,9 @@ commands = ls {env:TEST} """) - reader = SectionReader("testenv:py27", config._cfg) - x = reader.getargvlist("commands") - assert x == [ - "ls testvalue".split() - ] - assert x != [ - "ls {env:TEST}".split() - ] - y = reader.getargvlist("setenv") - assert y == [ - "TEST=testvalue".split() - ] + envconfig = config.envconfigs["py27"] + assert envconfig.commands == [["ls", "testvalue"]] + assert envconfig.setenv["TEST"] == "testvalue" class TestIniParser: @@ -1599,7 +1590,19 @@ class TestSetenv: - @pytest.mark.xfail(reason="fix pending") + def test_getdict_lazy(self, tmpdir, newconfig): + config = newconfig(""" + [testenv:X] + key0 = + key1 = {env:X} + key2 = {env:X:1} + """) + envconfig = config.envconfigs["X"] + val = envconfig._reader.getdict_lazy("key0") + assert val == {"key1": "{env:X}", + "key2": "{env:X:1}"} + + def test_setenv_uses_os_environ(self, tmpdir, newconfig, monkeypatch): monkeypatch.setenv("X", "1") config = newconfig(""" @@ -1609,7 +1612,6 @@ """) assert config.envconfigs["env1"].setenv["X"] == "1" - @pytest.mark.xfail(reason="fix pending") def test_setenv_default_os_environ(self, tmpdir, newconfig, monkeypatch): monkeypatch.delenv("X", raising=False) config = newconfig(""" @@ -1619,7 +1621,6 @@ """) assert config.envconfigs["env1"].setenv["X"] == "2" - @pytest.mark.xfail(reason="fix pending") def test_setenv_uses_other_setenv(self, tmpdir, newconfig): config = newconfig(""" [testenv:env1] @@ -1629,7 +1630,6 @@ """) assert config.envconfigs["env1"].setenv["X"] == "5" - @pytest.mark.xfail(reason="fix pending") def test_setenv_recursive_direct(self, tmpdir, newconfig): config = newconfig(""" [testenv:env1] diff -r fd4af25ed9541539b969146305375064320666cd -r a6d3e1956d8516889b1ee3275257e397fe1dfd51 tox/config.py --- a/tox/config.py +++ b/tox/config.py @@ -323,11 +323,39 @@ parser.add_argument("args", nargs="*", help="additional arguments available to command positional substitution") - # add various core venv interpreter attributes parser.add_testenv_attribute( name="envdir", type="path", default="{toxworkdir}/{envname}", help="venv directory") + # add various core venv interpreter attributes + def setenv(testenv_config, value): + setenv = value + reader = testenv_config._reader + + # we need to resolve environment variable substitution + + replacing = [] # for detecting direct recursion + def setenv_reader(name): + if name in setenv and name not in replacing: + return setenv[name] + return os.environ.get(name) + reader.set_envreader(setenv_reader) + + for name, value in setenv.items(): + replacing.append(name) + setenv[name] = reader._replace(value) + replacing.pop() + + config = testenv_config.config + if "PYTHONHASHSEED" not in setenv and config.hashseed is not None: + setenv['PYTHONHASHSEED'] = config.hashseed + return setenv + + parser.add_testenv_attribute( + name="setenv", type="dict_lazy", postprocess=setenv, + help="list of X=Y lines with environment variable settings") + + def basepython_default(testenv_config, value): if value is None: for f in testenv_config.factors: @@ -385,17 +413,6 @@ name="recreate", type="bool", default=False, postprocess=recreate, help="always recreate this test environment.") - def setenv(testenv_config, value): - setenv = value - config = testenv_config.config - if "PYTHONHASHSEED" not in setenv and config.hashseed is not None: - setenv['PYTHONHASHSEED'] = config.hashseed - return setenv - - parser.add_testenv_attribute( - name="setenv", type="dict", postprocess=setenv, - help="list of X=Y lines with environment variable settings") - def passenv(testenv_config, value): # Flatten the list to deal with space-separated values. value = list( @@ -518,21 +535,15 @@ @property def envbindir(self): """ path to directory where scripts/binaries reside. """ - if (sys.platform == "win32" - and "jython" not in self.basepython - and "pypy" not in self.basepython): + if sys.platform == "win32": return self.envdir.join("Scripts") else: return self.envdir.join("bin") @property def envpython(self): - """ path to python/jython executable. """ - if "jython" in str(self.basepython): - name = "jython" - else: - name = "python" - return self.envbindir.join(name) + """ path to python executable. """ + return self.envbindir.join(self.basepython) # no @property to avoid early calling (see callable(subst[key]) checks) def envsitepackagesdir(self): @@ -699,7 +710,7 @@ for env_attr in config._testenv_attr: atype = env_attr.type - if atype in ("bool", "path", "string", "dict", "argv", "argvlist"): + if atype in ("bool", "path", "string", "dict", "dict_lazy", "argv", "argvlist"): meth = getattr(reader, "get" + atype) res = meth(env_attr.name, env_attr.default) elif atype == "space-separated-list": @@ -807,6 +818,13 @@ self.factors = factors self._subs = {} self._subststack = [] + self._envreader = os.environ.get + + def set_envreader(self, envreader): + self._envreader = envreader + + def get_environ_value(self, name): + return self._envreader(name) def addsubstitutions(self, _posargs=None, **kw): self._subs.update(kw) @@ -826,17 +844,24 @@ return [x.strip() for x in s.split(sep) if x.strip()] def getdict(self, name, default=None, sep="\n"): - s = self.getstring(name, None) - if s is None: + value = self.getstring(name, None) + return self._getdict(value, default=default, sep=sep) + + def getdict_lazy(self, name, default=None, sep="\n"): + value = self.getstring(name, None, replace="noenv") + return self._getdict(value, default=default, sep=sep) + + def _getdict(self, value, default, sep): + if value is None: return default or {} - value = {} - for line in s.split(sep): + d = {} + for line in value.split(sep): if line.strip(): name, rest = line.split('=', 1) - value[name.strip()] = rest.strip() + d[name.strip()] = rest.strip() - return value + return d def getbool(self, name, default=None): s = self.getstring(name, default) @@ -878,7 +903,7 @@ x = self._apply_factors(x) if replace and x and hasattr(x, 'replace'): - x = self._replace(x, name=name) + x = self._replace(x, name=name, opt_replace_env=(replace!="noenv")) # print "getstring", self.section_name, name, "returned", repr(x) return x @@ -895,14 +920,14 @@ lines = s.strip().splitlines() return '\n'.join(filter(None, map(factor_line, lines))) - def _replace(self, value, name=None, section_name=None): + def _replace(self, value, name=None, section_name=None, opt_replace_env=True): if '{' not in value: return value section_name = section_name if section_name else self.section_name self._subststack.append((section_name, name)) try: - return Replacer(self).do_replace(value) + return Replacer(self, opt_replace_env=opt_replace_env).do_replace(value) finally: assert self._subststack.pop() == (section_name, name) @@ -918,7 +943,7 @@ re.VERBOSE) - def __init__(self, reader, opt_replace_env=True): + def __init__(self, reader, opt_replace_env): self.reader = reader self.opt_replace_env = opt_replace_env @@ -942,14 +967,14 @@ "Malformed substitution; no substitution type provided") if sub_type == "env": - assert self.opt_replace_env - return self._replace_env(match) + if self.opt_replace_env: + return self._replace_env(match) + return "{env:%s}" %(g["substitution_value"]) if sub_type != None: raise tox.exception.ConfigError("No support for the %s substitution type" % sub_type) return self._replace_substitution(match) def _replace_env(self, match): - env_list = self.reader.getdict('setenv') match_value = match.group('substitution_value') if not match_value: raise tox.exception.ConfigError( @@ -963,15 +988,14 @@ else: envkey = match_value - if envkey not in os.environ and default is None: - if envkey not in env_list and default is None: + envvalue = self.reader.get_environ_value(envkey) + if envvalue is None: + if default is None: raise tox.exception.ConfigError( "substitution env:%r: unknown environment variable %r" % (envkey, envkey)) - if envkey in os.environ: - return os.environ.get(envkey, default) - else: - return env_list.get(envkey, default) + return default + return envvalue def _substitute_from_other_section(self, key): if key.startswith("[") and "]" in key: @@ -983,7 +1007,8 @@ raise ValueError('%s already in %s' % ( (section, item), self.reader._subststack)) x = str(cfg[section][item]) - return self.reader._replace(x, name=item, section_name=section) + return self.reader._replace(x, name=item, section_name=section, + opt_replace_env=self.opt_replace_env) raise tox.exception.ConfigError( "substitution key %r not found" % key) Repository URL: https://bitbucket.org/hpk42/tox/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. _______________________________________________ pytest-commit mailing list pytest-commit@python.org https://mail.python.org/mailman/listinfo/pytest-commit