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

Reply via email to