9 new commits in tox: https://bitbucket.org/hpk42/tox/commits/de361f0a345c/ Changeset: de361f0a345c User: suor Date: 2014-06-20 12:10:04 Summary: First stab at multidimensional config Affected #: 1 file
diff -r cefc0fd28dda72ac76a9170b4c586e0eb3f1d124 -r de361f0a345ce1843425fa3128eab4ef8e437977 tox/_config.py --- a/tox/_config.py +++ b/tox/_config.py @@ -303,10 +303,18 @@ config.skipsdist = reader.getbool(toxsection, "skipsdist", all_develop) - def _makeenvconfig(self, name, section, subs, config): + # interpolate missing configs + for name in config.envlist: + if name not in config.envconfigs: + config.envconfigs[name] = \ + self._makeenvconfig(name, "testenv", reader._subs, config, + factors=name.split('-')) + + def _makeenvconfig(self, name, section, subs, config, factors=()): vc = VenvConfig(envname=name) vc.config = config - reader = IniReader(self._cfg, fallbacksections=["testenv"]) + reader = IniReader(self._cfg, fallbacksections=["testenv"], + factors=factors) reader.addsubstitutions(**subs) vc.develop = not config.option.installpkg and \ reader.getbool(section, "usedevelop", config.option.develop) @@ -391,16 +399,33 @@ if not env: env = os.environ.get("TOXENV", None) if not env: - envlist = reader.getlist(toxsection, "envlist", sep=",") + envstr = reader.getdefault(toxsection, "envlist", default="", + replace=False) + envlist = self._expand_envstr(envstr) if not envlist: envlist = self.config.envconfigs.keys() return envlist + # TODO: move envsplit magic to _split_env() envlist = _split_env(env) if "ALL" in envlist: envlist = list(self.config.envconfigs) envlist.sort() return envlist + def _expand_envstr(self, envstr): + from itertools import groupby, product, chain + + # split by commas not in groups + tokens = re.split(r'(\{[^}]+\})|,', envstr) + envlist = [''.join(g).strip() for k, g in groupby(tokens, key=bool) if k] + + def expand(env): + tokens = re.split(r'\{([^}]+)\}', env) + parts = [token.split(',') for token in tokens] + return [''.join(variant) for variant in product(*parts)] + + return list(chain(*map(expand, envlist))) + def _replace_forced_dep(self, name, config): """ Override the given dependency config name taking --force-dep-version @@ -468,9 +493,10 @@ class IniReader: - def __init__(self, cfgparser, fallbacksections=None): + def __init__(self, cfgparser, fallbacksections=None, factors=()): self._cfg = cfgparser self.fallbacksections = fallbacksections or [] + self.factors = factors self._subs = {} self._subststack = [] @@ -586,18 +612,19 @@ return s def getdefault(self, section, name, default=None, replace=True): - try: - x = self._cfg[section][name] - except KeyError: - for fallbacksection in self.fallbacksections: - try: - x = self._cfg[fallbacksection][name] - except KeyError: - pass - else: - break - else: - x = default + x = None + for s in [section] + self.fallbacksections: + try: + x = self._cfg[s][name] + break + except KeyError: + continue + + if x is None: + x = default + else: + x = self._apply_factors(x) + if replace and x and hasattr(x, 'replace'): self._subststack.append((section, name)) try: @@ -607,6 +634,19 @@ #print "getdefault", section, name, "returned", repr(x) return x + def _apply_factors(self, s): + def factor_line(line): + m = re.search(r'^(!)?(\w+)?\:\s*(.+)', line) + if not m: + return line + + negate, factor, line = m.groups() + if bool(negate) ^ (factor in self.factors): + return line + + lines = s.strip().splitlines() + return '\n'.join(filter(None, map(factor_line, lines))) + def _replace_env(self, match): match_value = match.group('substitution_value') if not match_value: https://bitbucket.org/hpk42/tox/commits/26612b04a9fd/ Changeset: 26612b04a9fd User: suor Date: 2014-06-28 16:17:46 Summary: Fix regressions after adding factors Affected #: 1 file diff -r de361f0a345ce1843425fa3128eab4ef8e437977 -r 26612b04a9fd86702309469182b16f47ae56fc8d tox/_config.py --- a/tox/_config.py +++ b/tox/_config.py @@ -636,7 +636,7 @@ def _apply_factors(self, s): def factor_line(line): - m = re.search(r'^(!)?(\w+)?\:\s*(.+)', line) + m = re.search(r'^(!)?(\w+)\:\s+(.+)', line) if not m: return line https://bitbucket.org/hpk42/tox/commits/c3c0b8cf5e00/ Changeset: c3c0b8cf5e00 User: suor Date: 2014-06-29 11:01:28 Summary: Parse env and args for factors, detect undefined envs Affected #: 1 file diff -r 26612b04a9fd86702309469182b16f47ae56fc8d -r c3c0b8cf5e00e99761e892c113cbfe95150de3cb tox/_config.py --- a/tox/_config.py +++ b/tox/_config.py @@ -6,6 +6,7 @@ import shlex import string import pkg_resources +import itertools from tox.interpreters import Interpreters @@ -280,22 +281,19 @@ config.sdistsrc = reader.getpath(toxsection, "sdistsrc", None) config.setupdir = reader.getpath(toxsection, "setupdir", "{toxinidir}") config.logdir = config.toxworkdir.join("log") - for sectionwrapper in self._cfg: - section = sectionwrapper.name - if section.startswith(testenvprefix): - name = section[len(testenvprefix):] - envconfig = self._makeenvconfig(name, section, reader._subs, - config) - config.envconfigs[name] = envconfig - if not config.envconfigs: - config.envconfigs['python'] = \ - self._makeenvconfig("python", "_xz_9", reader._subs, config) - config.envlist = self._getenvlist(reader, toxsection) - for name in config.envlist: - if name not in config.envconfigs: - if name in defaultenvs: - config.envconfigs[name] = \ - self._makeenvconfig(name, "_xz_9", reader._subs, config) + + config.envlist, all_envs = self._getenvdata(reader, toxsection) + + # configure testenvs + known_factors = self._list_section_factors("testenv") + known_factors.update(defaultenvs) + known_factors.add("python") + for name in all_envs: + section = testenvprefix + name + factors = set(name.split('-')) + if section in self._cfg or factors & known_factors: + config.envconfigs[name] = \ + self._makeenvconfig(name, section, reader._subs, config) all_develop = all(name in config.envconfigs and config.envconfigs[name].develop @@ -303,18 +301,18 @@ config.skipsdist = reader.getbool(toxsection, "skipsdist", all_develop) - # interpolate missing configs - for name in config.envlist: - if name not in config.envconfigs: - config.envconfigs[name] = \ - self._makeenvconfig(name, "testenv", reader._subs, config, - factors=name.split('-')) + def _list_section_factors(self, section): + factors = set() + if section in self._cfg: + for _, value in self._cfg[section].items(): + factors.update(re.findall(r'^(!)?(\w+)\:\s+(.+)', value)) + return factors - def _makeenvconfig(self, name, section, subs, config, factors=()): + def _makeenvconfig(self, name, section, subs, config): vc = VenvConfig(envname=name) vc.config = config reader = IniReader(self._cfg, fallbacksections=["testenv"], - factors=factors) + factors=name.split('-')) reader.addsubstitutions(**subs) vc.develop = not config.option.installpkg and \ reader.getbool(section, "usedevelop", config.option.develop) @@ -394,37 +392,25 @@ "'install_command' must contain '{packages}' substitution") return vc - def _getenvlist(self, reader, toxsection): - env = self.config.option.env - if not env: - env = os.environ.get("TOXENV", None) - if not env: - envstr = reader.getdefault(toxsection, "envlist", default="", - replace=False) - envlist = self._expand_envstr(envstr) - if not envlist: - envlist = self.config.envconfigs.keys() - return envlist - # TODO: move envsplit magic to _split_env() - envlist = _split_env(env) - if "ALL" in envlist: - envlist = list(self.config.envconfigs) - envlist.sort() - return envlist + def _getenvdata(self, reader, toxsection): + envstr = self.config.option.env \ + or os.environ.get("TOXENV") \ + or reader.getdefault(toxsection, "envlist", replace=False) \ + or [] + envlist = _split_env(envstr) - def _expand_envstr(self, envstr): - from itertools import groupby, product, chain + # collect section envs + all_envs = set(envlist) - set(["ALL"]) + for section in self._cfg: + if section.name.startswith(testenvprefix): + all_envs.add(section.name[len(testenvprefix):]) + if not all_envs: + all_envs.add("python") - # split by commas not in groups - tokens = re.split(r'(\{[^}]+\})|,', envstr) - envlist = [''.join(g).strip() for k, g in groupby(tokens, key=bool) if k] + if not envlist or "ALL" in envlist: + envlist = sorted(all_envs) - def expand(env): - tokens = re.split(r'\{([^}]+)\}', env) - parts = [token.split(',') for token in tokens] - return [''.join(variant) for variant in product(*parts)] - - return list(chain(*map(expand, envlist))) + return envlist, all_envs def _replace_forced_dep(self, name, config): """ @@ -454,15 +440,25 @@ def _split_env(env): """if handed a list, action="append" was used for -e """ - envlist = [] if not isinstance(env, list): env = [env] - for to_split in env: - for single_env in to_split.split(","): - # "remove True or", if not allowing multiple same runs, update tests - if True or single_env not in envlist: - envlist.append(single_env) - return envlist + return mapcat(_expand_envstr, env) + +def _expand_envstr(envstr): + # split by commas not in groups + tokens = re.split(r'(\{[^}]+\})|,', envstr) + envlist = [''.join(g).strip() + for k, g in itertools.groupby(tokens, key=bool) if k] + + def expand(env): + tokens = re.split(r'\{([^}]+)\}', env) + parts = [token.split(',') for token in tokens] + return [''.join(variant) for variant in itertools.product(*parts)] + + return mapcat(expand, envlist) + +def mapcat(f, seq): + return list(itertools.chain.from_iterable(map(f, seq))) class DepConfig: def __init__(self, name, indexserver=None): https://bitbucket.org/hpk42/tox/commits/7a0dd904a4fb/ Changeset: 7a0dd904a4fb User: suor Date: 2014-07-03 12:22:28 Summary: Test factors and envlist expansion Affected #: 1 file diff -r c3c0b8cf5e00e99761e892c113cbfe95150de3cb -r 7a0dd904a4fbacfe93c223f4eec746ef9f9b4f07 tests/test_config.py --- a/tests/test_config.py +++ b/tests/test_config.py @@ -832,6 +832,24 @@ assert conf.changedir.basename == 'testing' assert conf.changedir.dirpath().realpath() == tmpdir.realpath() + def test_factors(self, newconfig): + inisource=""" + [tox] + envlist = a,b + + [testenv] + deps= + dep-all + a: dep-a + b: dep-b + !a: dep-not-a + """ + conf = newconfig([], inisource) + configs = conf.envconfigs + assert [dep.name for dep in configs['a'].deps] == ["dep-all", "dep-a"] + assert [dep.name for dep in configs['b'].deps] == \ + ["dep-all", "dep-b", "dep-not-a"] + class TestGlobalOptions: def test_notest(self, newconfig): config = newconfig([], "") @@ -935,6 +953,23 @@ bp = "python%s.%s" %(name[2], name[3]) assert env.basepython == bp + def test_envlist_expansion(self, newconfig): + inisource = """ + [tox] + envlist = py{26,27},docs + """ + config = newconfig([], inisource) + assert config.envlist == ["py26", "py27", "docs"] + + def test_envlist_cross_product(self, newconfig): + inisource = """ + [tox] + envlist = py{26,27}-dep{1,2} + """ + config = newconfig([], inisource) + assert config.envlist == \ + ["py26-dep1", "py26-dep2", "py27-dep1", "py27-dep2"] + def test_minversion(self, tmpdir, newconfig, monkeypatch): inisource = """ [tox] https://bitbucket.org/hpk42/tox/commits/a6ef74cfe446/ Changeset: a6ef74cfe446 User: suor Date: 2014-07-03 12:34:23 Summary: Fix undefined env check Affected #: 1 file diff -r 7a0dd904a4fbacfe93c223f4eec746ef9f9b4f07 -r a6ef74cfe44662feff5bda94bdaf83c362e505d3 tox/_config.py --- a/tox/_config.py +++ b/tox/_config.py @@ -291,7 +291,7 @@ for name in all_envs: section = testenvprefix + name factors = set(name.split('-')) - if section in self._cfg or factors & known_factors: + if section in self._cfg or factors <= known_factors: config.envconfigs[name] = \ self._makeenvconfig(name, section, reader._subs, config) @@ -305,7 +305,7 @@ factors = set() if section in self._cfg: for _, value in self._cfg[section].items(): - factors.update(re.findall(r'^(!)?(\w+)\:\s+(.+)', value)) + factors.update(re.findall(r'^!?(\w+)\:\s+', value, re.M)) return factors def _makeenvconfig(self, name, section, subs, config): https://bitbucket.org/hpk42/tox/commits/7130bc740587/ Changeset: 7130bc740587 User: suor Date: 2014-07-17 09:12:02 Summary: Docs on factors and envlist expansion Affected #: 1 file diff -r a6ef74cfe44662feff5bda94bdaf83c362e505d3 -r 7130bc7405879fa2254cfcb618d07ff7aec9a7d5 doc/config.txt --- a/doc/config.txt +++ b/doc/config.txt @@ -382,6 +382,115 @@ {[base]deps} +Generating environments and selecting factors +--------------------------------------------- + +.. versionadded:: 1.8 + +Suppose you want to test your package against python2.6, python2.7 and against +several versions of a dependency, say Django 1.5 and Django 1.6. You can +accomplish that by writing down 2*2 = 4 ``[testenv:*]`` sections and then +listing all of them in ``envlist``. + +However, a better approach would be generating ``envlist`` and then selecting +dependencies this way:: + + [tox] + envlist = {py26,py27}-django{15,16} + + [testenv] + basepython = + py26: python2.6 + py27: python2.7 + deps = + pytest + django15: Django>=1.5,<1.6 + django16: Django>=1.6,<1.7 + !py27: unittest2 + commands = py.test + +Let's go through this step by step. + + +Generating environments ++++++++++++++++++++++++ + +:: + + envlist = {py26,py27}-django{15,16} + +This is bash-style syntax and will create ``2*2=4`` environment names +like this:: + + py26-django15 + py26-django16 + py27-django15 + py27-django16 + +You can still list explicit environments along with generated ones:: + + envlist = {py26,py27}-django{15,16}, docs, flake + + +Factors ++++++++ + +A parts of environment names delimited by hyphens are called factors and could +be used to alter values of ``[testenv]`` settings:: + + basepython = + py26: python2.6 + py27: python2.7 + +This conditional setting will lead to either ``python2.6`` or +``python2.7`` used as base python, e.g. ``python2.6`` is selected if current +environment contains ``py26`` factor. + +In list settings such as ``deps`` or ``commands`` you can freely intermix +optional lines with unconditional ones:: + + deps = + pytest + django15: Django>=1.5,<1.6 + django16: Django>=1.6,<1.7 + !py27: unittest2 + +A last line here uses negation of a factor, this means ``unittest2`` will be +in ``deps`` for all pythons except python2.7. The whole effect of this setting +definition could be described with a table: + +=============== ================================== +environment deps +=============== ================================== +py26-django15 pytest, Django>=1.5,<1.6, unitest2 +py26-django16 pytest, Django>=1.6,<1.7, unitest2 +py27-django15 pytest, Django>=1.5,<1.6 +py27-django16 pytest, Django>=1.6,<1.7 +=============== ================================== + +And this table can significantly grow as you have more dependencies and other +factors such as platform, python version and/or database. + +.. note:: + + Tox provides good defaults for basepython setting, so the above ini-file can be + further reduced by omitting it. + + +Showing all expanded sections ++++++++++++++++++++++++++++++ + +To help with understanding how the variants will produce section values, +you can ask tox to show their expansion with a new option:: + + $ tox -l + py26-django15 + py26-django16 + py27-django15 + py27-django16 + docs + flake + Other Rules and notes ===================== https://bitbucket.org/hpk42/tox/commits/34e8cf7abd61/ Changeset: 34e8cf7abd61 User: suor Date: 2014-07-17 10:27:47 Summary: Reimplement defaultenvs as default factors Affected #: 2 files diff -r 7130bc7405879fa2254cfcb618d07ff7aec9a7d5 -r 34e8cf7abd61ea9b7c6cee4d4a02918a03880c09 tests/test_config.py --- a/tests/test_config.py +++ b/tests/test_config.py @@ -850,6 +850,20 @@ assert [dep.name for dep in configs['b'].deps] == \ ["dep-all", "dep-b", "dep-not-a"] + def test_default_factors(self, newconfig): + inisource=""" + [tox] + envlist = py{26,27,33,34}-dep + + [testenv] + deps= + dep: dep + """ + conf = newconfig([], inisource) + configs = conf.envconfigs + for name, config in configs.items(): + assert config.basepython == 'python%s.%s' % (name[2], name[3]) + class TestGlobalOptions: def test_notest(self, newconfig): config = newconfig([], "") diff -r 7130bc7405879fa2254cfcb618d07ff7aec9a7d5 -r 34e8cf7abd61ea9b7c6cee4d4a02918a03880c09 tox/_config.py --- a/tox/_config.py +++ b/tox/_config.py @@ -16,13 +16,9 @@ iswin32 = sys.platform == "win32" -defaultenvs = {'jython': 'jython', 'pypy': 'pypy'} -for _name in "py,py24,py25,py26,py27,py30,py31,py32,py33,py34".split(","): - if _name == "py": - basepython = sys.executable - else: - basepython = "python" + ".".join(_name[2:4]) - defaultenvs[_name] = basepython +default_factors = {'jython': 'jython', 'pypy': 'pypy', 'py': sys.executable} +for version in '24,25,26,27,30,31,32,33,34'.split(','): + default_factors['py' + version] = 'python%s.%s' % tuple(version) def parseconfig(args=None, pkg=None): if args is None: @@ -286,7 +282,7 @@ # configure testenvs known_factors = self._list_section_factors("testenv") - known_factors.update(defaultenvs) + known_factors.update(default_factors) known_factors.add("python") for name in all_envs: section = testenvprefix + name @@ -311,8 +307,9 @@ def _makeenvconfig(self, name, section, subs, config): vc = VenvConfig(envname=name) vc.config = config + factors = set(name.split('-')) reader = IniReader(self._cfg, fallbacksections=["testenv"], - factors=name.split('-')) + factors=factors) reader.addsubstitutions(**subs) vc.develop = not config.option.installpkg and \ reader.getbool(section, "usedevelop", config.option.develop) @@ -321,10 +318,8 @@ if reader.getdefault(section, "python", None): raise tox.exception.ConfigError( "'python=' key was renamed to 'basepython='") - if name in defaultenvs: - bp = defaultenvs[name] - else: - bp = sys.executable + bp = next((default_factors[f] for f in factors if f in default_factors), + sys.executable) vc.basepython = reader.getdefault(section, "basepython", bp) vc._basepython_info = config.interpreters.get_info(vc.basepython) reader.addsubstitutions(envdir=vc.envdir, envname=vc.envname, https://bitbucket.org/hpk42/tox/commits/ffec51fcde95/ Changeset: ffec51fcde95 User: hpk42 Date: 2014-07-20 11:27:59 Summary: merge Alexander's "multi-dimensional" PR. Affected #: 5 files diff -r 27b38ca7904a514f5b9e51e3570fac218b522737 -r ffec51fcde950f4f662b4d8528570f17e047ed8d CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,10 @@ +1.8.0.dev1 +----------- + +- new multi-dimensional configuration support. Many thanks to + Alexander Schepanovski for the complete PR with docs. + + 1.7.2 ----------- diff -r 27b38ca7904a514f5b9e51e3570fac218b522737 -r ffec51fcde950f4f662b4d8528570f17e047ed8d CONTRIBUTORS --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -3,6 +3,7 @@ Krisztian Fekete Marc Abramowitz +Aleaxner Schepanovski Sridhar Ratnakumar Barry Warsaw Chris Rose diff -r 27b38ca7904a514f5b9e51e3570fac218b522737 -r ffec51fcde950f4f662b4d8528570f17e047ed8d doc/config.txt --- a/doc/config.txt +++ b/doc/config.txt @@ -382,6 +382,115 @@ {[base]deps} +Generating environments and selecting factors +--------------------------------------------- + +.. versionadded:: 1.8 + +Suppose you want to test your package against python2.6, python2.7 and against +several versions of a dependency, say Django 1.5 and Django 1.6. You can +accomplish that by writing down 2*2 = 4 ``[testenv:*]`` sections and then +listing all of them in ``envlist``. + +However, a better approach would be generating ``envlist`` and then selecting +dependencies this way:: + + [tox] + envlist = {py26,py27}-django{15,16} + + [testenv] + basepython = + py26: python2.6 + py27: python2.7 + deps = + pytest + django15: Django>=1.5,<1.6 + django16: Django>=1.6,<1.7 + !py27: unittest2 + commands = py.test + +Let's go through this step by step. + + +Generating environments ++++++++++++++++++++++++ + +:: + + envlist = {py26,py27}-django{15,16} + +This is bash-style syntax and will create ``2*2=4`` environment names +like this:: + + py26-django15 + py26-django16 + py27-django15 + py27-django16 + +You can still list explicit environments along with generated ones:: + + envlist = {py26,py27}-django{15,16}, docs, flake + + +Factors ++++++++ + +A parts of environment names delimited by hyphens are called factors and could +be used to alter values of ``[testenv]`` settings:: + + basepython = + py26: python2.6 + py27: python2.7 + +This conditional setting will lead to either ``python2.6`` or +``python2.7`` used as base python, e.g. ``python2.6`` is selected if current +environment contains ``py26`` factor. + +In list settings such as ``deps`` or ``commands`` you can freely intermix +optional lines with unconditional ones:: + + deps = + pytest + django15: Django>=1.5,<1.6 + django16: Django>=1.6,<1.7 + !py27: unittest2 + +A last line here uses negation of a factor, this means ``unittest2`` will be +in ``deps`` for all pythons except python2.7. The whole effect of this setting +definition could be described with a table: + +=============== ================================== +environment deps +=============== ================================== +py26-django15 pytest, Django>=1.5,<1.6, unitest2 +py26-django16 pytest, Django>=1.6,<1.7, unitest2 +py27-django15 pytest, Django>=1.5,<1.6 +py27-django16 pytest, Django>=1.6,<1.7 +=============== ================================== + +And this table can significantly grow as you have more dependencies and other +factors such as platform, python version and/or database. + +.. note:: + + Tox provides good defaults for basepython setting, so the above ini-file can be + further reduced by omitting it. + + +Showing all expanded sections ++++++++++++++++++++++++++++++ + +To help with understanding how the variants will produce section values, +you can ask tox to show their expansion with a new option:: + + $ tox -l + py26-django15 + py26-django16 + py27-django15 + py27-django16 + docs + flake + Other Rules and notes ===================== diff -r 27b38ca7904a514f5b9e51e3570fac218b522737 -r ffec51fcde950f4f662b4d8528570f17e047ed8d tests/test_config.py --- a/tests/test_config.py +++ b/tests/test_config.py @@ -832,6 +832,38 @@ assert conf.changedir.basename == 'testing' assert conf.changedir.dirpath().realpath() == tmpdir.realpath() + def test_factors(self, newconfig): + inisource=""" + [tox] + envlist = a,b + + [testenv] + deps= + dep-all + a: dep-a + b: dep-b + !a: dep-not-a + """ + conf = newconfig([], inisource) + configs = conf.envconfigs + assert [dep.name for dep in configs['a'].deps] == ["dep-all", "dep-a"] + assert [dep.name for dep in configs['b'].deps] == \ + ["dep-all", "dep-b", "dep-not-a"] + + def test_default_factors(self, newconfig): + inisource=""" + [tox] + envlist = py{26,27,33,34}-dep + + [testenv] + deps= + dep: dep + """ + conf = newconfig([], inisource) + configs = conf.envconfigs + for name, config in configs.items(): + assert config.basepython == 'python%s.%s' % (name[2], name[3]) + class TestGlobalOptions: def test_notest(self, newconfig): config = newconfig([], "") @@ -935,6 +967,23 @@ bp = "python%s.%s" %(name[2], name[3]) assert env.basepython == bp + def test_envlist_expansion(self, newconfig): + inisource = """ + [tox] + envlist = py{26,27},docs + """ + config = newconfig([], inisource) + assert config.envlist == ["py26", "py27", "docs"] + + def test_envlist_cross_product(self, newconfig): + inisource = """ + [tox] + envlist = py{26,27}-dep{1,2} + """ + config = newconfig([], inisource) + assert config.envlist == \ + ["py26-dep1", "py26-dep2", "py27-dep1", "py27-dep2"] + def test_minversion(self, tmpdir, newconfig, monkeypatch): inisource = """ [tox] diff -r 27b38ca7904a514f5b9e51e3570fac218b522737 -r ffec51fcde950f4f662b4d8528570f17e047ed8d tox/_config.py --- a/tox/_config.py +++ b/tox/_config.py @@ -6,6 +6,7 @@ import shlex import string import pkg_resources +import itertools from tox.interpreters import Interpreters @@ -15,13 +16,10 @@ iswin32 = sys.platform == "win32" -defaultenvs = {'jython': 'jython', 'pypy': 'pypy', 'pypy3': 'pypy3'} -for _name in "py,py24,py25,py26,py27,py30,py31,py32,py33,py34".split(","): - if _name == "py": - basepython = sys.executable - else: - basepython = "python" + ".".join(_name[2:4]) - defaultenvs[_name] = basepython +default_factors = {'jython': 'jython', 'pypy': 'pypy', 'pypy3': 'pypy3', + 'py': sys.executable} +for version in '24,25,26,27,30,31,32,33,34'.split(','): + default_factors['py' + version] = 'python%s.%s' % tuple(version) def parseconfig(args=None, pkg=None): if args is None: @@ -280,22 +278,19 @@ config.sdistsrc = reader.getpath(toxsection, "sdistsrc", None) config.setupdir = reader.getpath(toxsection, "setupdir", "{toxinidir}") config.logdir = config.toxworkdir.join("log") - for sectionwrapper in self._cfg: - section = sectionwrapper.name - if section.startswith(testenvprefix): - name = section[len(testenvprefix):] - envconfig = self._makeenvconfig(name, section, reader._subs, - config) - config.envconfigs[name] = envconfig - if not config.envconfigs: - config.envconfigs['python'] = \ - self._makeenvconfig("python", "_xz_9", reader._subs, config) - config.envlist = self._getenvlist(reader, toxsection) - for name in config.envlist: - if name not in config.envconfigs: - if name in defaultenvs: - config.envconfigs[name] = \ - self._makeenvconfig(name, "_xz_9", reader._subs, config) + + config.envlist, all_envs = self._getenvdata(reader, toxsection) + + # configure testenvs + known_factors = self._list_section_factors("testenv") + known_factors.update(default_factors) + known_factors.add("python") + for name in all_envs: + section = testenvprefix + name + factors = set(name.split('-')) + if section in self._cfg or factors <= known_factors: + config.envconfigs[name] = \ + self._makeenvconfig(name, section, reader._subs, config) all_develop = all(name in config.envconfigs and config.envconfigs[name].develop @@ -303,10 +298,19 @@ config.skipsdist = reader.getbool(toxsection, "skipsdist", all_develop) + def _list_section_factors(self, section): + factors = set() + if section in self._cfg: + for _, value in self._cfg[section].items(): + factors.update(re.findall(r'^!?(\w+)\:\s+', value, re.M)) + return factors + def _makeenvconfig(self, name, section, subs, config): vc = VenvConfig(envname=name) vc.config = config - reader = IniReader(self._cfg, fallbacksections=["testenv"]) + factors = set(name.split('-')) + reader = IniReader(self._cfg, fallbacksections=["testenv"], + factors=factors) reader.addsubstitutions(**subs) vc.develop = not config.option.installpkg and \ reader.getbool(section, "usedevelop", config.option.develop) @@ -315,10 +319,8 @@ if reader.getdefault(section, "python", None): raise tox.exception.ConfigError( "'python=' key was renamed to 'basepython='") - if name in defaultenvs: - bp = defaultenvs[name] - else: - bp = sys.executable + bp = next((default_factors[f] for f in factors if f in default_factors), + sys.executable) vc.basepython = reader.getdefault(section, "basepython", bp) vc._basepython_info = config.interpreters.get_info(vc.basepython) reader.addsubstitutions(envdir=vc.envdir, envname=vc.envname, @@ -386,20 +388,25 @@ "'install_command' must contain '{packages}' substitution") return vc - def _getenvlist(self, reader, toxsection): - env = self.config.option.env - if not env: - env = os.environ.get("TOXENV", None) - if not env: - envlist = reader.getlist(toxsection, "envlist", sep=",") - if not envlist: - envlist = self.config.envconfigs.keys() - return envlist - envlist = _split_env(env) - if "ALL" in envlist: - envlist = list(self.config.envconfigs) - envlist.sort() - return envlist + def _getenvdata(self, reader, toxsection): + envstr = self.config.option.env \ + or os.environ.get("TOXENV") \ + or reader.getdefault(toxsection, "envlist", replace=False) \ + or [] + envlist = _split_env(envstr) + + # collect section envs + all_envs = set(envlist) - set(["ALL"]) + for section in self._cfg: + if section.name.startswith(testenvprefix): + all_envs.add(section.name[len(testenvprefix):]) + if not all_envs: + all_envs.add("python") + + if not envlist or "ALL" in envlist: + envlist = sorted(all_envs) + + return envlist, all_envs def _replace_forced_dep(self, name, config): """ @@ -429,15 +436,25 @@ def _split_env(env): """if handed a list, action="append" was used for -e """ - envlist = [] if not isinstance(env, list): env = [env] - for to_split in env: - for single_env in to_split.split(","): - # "remove True or", if not allowing multiple same runs, update tests - if True or single_env not in envlist: - envlist.append(single_env) - return envlist + return mapcat(_expand_envstr, env) + +def _expand_envstr(envstr): + # split by commas not in groups + tokens = re.split(r'(\{[^}]+\})|,', envstr) + envlist = [''.join(g).strip() + for k, g in itertools.groupby(tokens, key=bool) if k] + + def expand(env): + tokens = re.split(r'\{([^}]+)\}', env) + parts = [token.split(',') for token in tokens] + return [''.join(variant) for variant in itertools.product(*parts)] + + return mapcat(expand, envlist) + +def mapcat(f, seq): + return list(itertools.chain.from_iterable(map(f, seq))) class DepConfig: def __init__(self, name, indexserver=None): @@ -468,9 +485,10 @@ class IniReader: - def __init__(self, cfgparser, fallbacksections=None): + def __init__(self, cfgparser, fallbacksections=None, factors=()): self._cfg = cfgparser self.fallbacksections = fallbacksections or [] + self.factors = factors self._subs = {} self._subststack = [] @@ -586,18 +604,19 @@ return s def getdefault(self, section, name, default=None, replace=True): - try: - x = self._cfg[section][name] - except KeyError: - for fallbacksection in self.fallbacksections: - try: - x = self._cfg[fallbacksection][name] - except KeyError: - pass - else: - break - else: - x = default + x = None + for s in [section] + self.fallbacksections: + try: + x = self._cfg[s][name] + break + except KeyError: + continue + + if x is None: + x = default + else: + x = self._apply_factors(x) + if replace and x and hasattr(x, 'replace'): self._subststack.append((section, name)) try: @@ -607,6 +626,19 @@ #print "getdefault", section, name, "returned", repr(x) return x + def _apply_factors(self, s): + def factor_line(line): + m = re.search(r'^(!)?(\w+)\:\s+(.+)', line) + if not m: + return line + + negate, factor, line = m.groups() + if bool(negate) ^ (factor in self.factors): + return line + + lines = s.strip().splitlines() + return '\n'.join(filter(None, map(factor_line, lines))) + def _replace_env(self, match): match_value = match.group('substitution_value') if not match_value: https://bitbucket.org/hpk42/tox/commits/30fcc8529698/ Changeset: 30fcc8529698 User: hpk42 Date: 2014-07-20 17:36:10 Summary: some streamlining of the docs Affected #: 1 file diff -r ffec51fcde950f4f662b4d8528570f17e047ed8d -r 30fcc85296981a9fc3f5b8782c91817d5a569357 doc/config.txt --- a/doc/config.txt +++ b/doc/config.txt @@ -382,7 +382,7 @@ {[base]deps} -Generating environments and selecting factors +Generating environments, conditional settings --------------------------------------------- .. versionadded:: 1.8 @@ -392,8 +392,7 @@ accomplish that by writing down 2*2 = 4 ``[testenv:*]`` sections and then listing all of them in ``envlist``. -However, a better approach would be generating ``envlist`` and then selecting -dependencies this way:: +However, a better approach looks like this:: [tox] envlist = {py26,py27}-django{15,16} @@ -409,10 +408,16 @@ !py27: unittest2 commands = py.test +This uses two new facilities of tox-1.8: + +- generative envlist declarations where each envname + consists of environment parts or "factors" + +- "factor" specific settings + Let's go through this step by step. - -Generating environments +Generative envlist +++++++++++++++++++++++ :: @@ -427,16 +432,29 @@ py27-django15 py27-django16 -You can still list explicit environments along with generated ones:: +You can still list environments explicitely along with generated ones:: envlist = {py26,py27}-django{15,16}, docs, flake +.. note:: -Factors -+++++++ + To help with understanding how the variants will produce section values, + you can ask tox to show their expansion with a new option:: -A parts of environment names delimited by hyphens are called factors and could -be used to alter values of ``[testenv]`` settings:: + $ tox -l + py26-django15 + py26-django16 + py27-django15 + py27-django16 + docs + flake + + +Factors and factor-conditional settings +++++++++++++++++++++++++++++++++++++++++ + +Parts of an environment name delimited by hyphens are called factors and can +be used to set values conditionally:: basepython = py26: python2.6 @@ -455,7 +473,7 @@ django16: Django>=1.6,<1.7 !py27: unittest2 -A last line here uses negation of a factor, this means ``unittest2`` will be +The last line here uses negation of a factor, this means ``unittest2`` will be in ``deps`` for all pythons except python2.7. The whole effect of this setting definition could be described with a table: @@ -473,23 +491,9 @@ .. note:: - Tox provides good defaults for basepython setting, so the above ini-file can be - further reduced by omitting it. - - -Showing all expanded sections -+++++++++++++++++++++++++++++ - -To help with understanding how the variants will produce section values, -you can ask tox to show their expansion with a new option:: - - $ tox -l - py26-django15 - py26-django16 - py27-django15 - py27-django16 - docs - flake + Tox provides good defaults for basepython setting, so the above + ini-file can be further reduced by omitting the ``basepython`` + setting. Other Rules and notes 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