From c4166e3a4924f860b02c6886a955f3b51bb4f689 Mon Sep 17 00:00:00 2001 From: Alexander Schepanovski Date: Fri, 20 Jun 2014 18:10:04 +0800 Subject: First stab at multidimensional config --- tox/_config.py | 72 +++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 56 insertions(+), 16 deletions(-) diff --git a/tox/_config.py b/tox/_config.py index 4f89d00..5935f74 100644 --- a/tox/_config.py +++ b/tox/_config.py @@ -303,10 +303,18 @@ class parseini: 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 @@ class parseini: 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 @@ RE_ITEM_REF = re.compile( 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 @@ class IniReader: 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 @@ class IniReader: #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: -- cgit v1.2.1 From 8a1d420a321dbb8b8fbb1e74a72a6d0e7114e6eb Mon Sep 17 00:00:00 2001 From: Alexander Schepanovski Date: Sat, 28 Jun 2014 22:17:46 +0800 Subject: Fix regressions after adding factors --- tox/_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox/_config.py b/tox/_config.py index 5935f74..39bc375 100644 --- a/tox/_config.py +++ b/tox/_config.py @@ -636,7 +636,7 @@ class IniReader: 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 -- cgit v1.2.1 From 578c6f2e23ce585f83e27c9c5866a0e3475b965c Mon Sep 17 00:00:00 2001 From: Alexander Schepanovski Date: Sun, 29 Jun 2014 17:01:28 +0800 Subject: Parse env and args for factors, detect undefined envs --- tox/_config.py | 120 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 58 insertions(+), 62 deletions(-) diff --git a/tox/_config.py b/tox/_config.py index 39bc375..1dadc8a 100644 --- a/tox/_config.py +++ b/tox/_config.py @@ -6,6 +6,7 @@ import re import shlex import string import pkg_resources +import itertools from tox.interpreters import Interpreters @@ -280,22 +281,19 @@ class parseini: 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 @@ class parseini: 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 @@ class parseini: "'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 _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 _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): """ @@ -454,15 +440,25 @@ class parseini: 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): -- cgit v1.2.1 From b445c4e81c404b991c7b643e206562ea0dc1f6b5 Mon Sep 17 00:00:00 2001 From: Alexander Schepanovski Date: Thu, 3 Jul 2014 18:22:28 +0800 Subject: Test factors and envlist expansion --- tests/test_config.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/test_config.py b/tests/test_config.py index ff743c4..87ed36a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -832,6 +832,24 @@ class TestConfigTestEnv: 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 @@ class TestGlobalOptions: 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] -- cgit v1.2.1 From fa48190fed81c28e393c162ef7189b7cc1f62332 Mon Sep 17 00:00:00 2001 From: Alexander Schepanovski Date: Thu, 3 Jul 2014 18:34:23 +0800 Subject: Fix undefined env check --- tox/_config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox/_config.py b/tox/_config.py index 1dadc8a..0d9f8af 100644 --- a/tox/_config.py +++ b/tox/_config.py @@ -291,7 +291,7 @@ class parseini: 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 @@ class parseini: 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): -- cgit v1.2.1 From 1135a2a07207907db8cd24bd6bd5407fa1fe902f Mon Sep 17 00:00:00 2001 From: Alexander Schepanovski Date: Thu, 17 Jul 2014 15:12:02 +0800 Subject: Docs on factors and envlist expansion --- doc/config.txt | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/doc/config.txt b/doc/config.txt index 9288ee2..8837664 100644 --- a/doc/config.txt +++ b/doc/config.txt @@ -382,6 +382,115 @@ You can put default values in one section and reference them in others to avoid {[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 ===================== -- cgit v1.2.1 From d95962973c14568343cafb62be4279d4312d5975 Mon Sep 17 00:00:00 2001 From: Alexander Schepanovski Date: Thu, 17 Jul 2014 16:27:47 +0800 Subject: Reimplement defaultenvs as default factors --- tests/test_config.py | 14 ++++++++++++++ tox/_config.py | 21 ++++++++------------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/tests/test_config.py b/tests/test_config.py index 87ed36a..d7a7451 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -850,6 +850,20 @@ class TestConfigTestEnv: 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 --git a/tox/_config.py b/tox/_config.py index 0d9f8af..3a12d93 100644 --- a/tox/_config.py +++ b/tox/_config.py @@ -16,13 +16,9 @@ import tox 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 @@ class parseini: # 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 @@ class parseini: 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 @@ class parseini: 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, -- cgit v1.2.1 From 601f0907efbe85192e5c6a55d99530273687b8de Mon Sep 17 00:00:00 2001 From: holger krekel Date: Sun, 20 Jul 2014 17:36:10 +0200 Subject: some streamlining of the docs --- doc/config.txt | 60 +++++++++++++++++++++++++++++++--------------------------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/doc/config.txt b/doc/config.txt index 8837664..0721a87 100644 --- a/doc/config.txt +++ b/doc/config.txt @@ -382,7 +382,7 @@ You can put default values in one section and reference them in others to avoid {[base]deps} -Generating environments and selecting factors +Generating environments, conditional settings --------------------------------------------- .. versionadded:: 1.8 @@ -392,8 +392,7 @@ 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:: +However, a better approach looks like this:: [tox] envlist = {py26,py27}-django{15,16} @@ -409,10 +408,16 @@ dependencies this way:: !py27: unittest2 commands = py.test -Let's go through this step by step. +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 @@ like this:: 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:: + + 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 + -Factors -+++++++ +Factors and factor-conditional settings +++++++++++++++++++++++++++++++++++++++++ -A parts of environment names delimited by hyphens are called factors and could -be used to alter values of ``[testenv]`` 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 @@ optional lines with unconditional ones:: 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 @@ 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 + 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 -- cgit v1.2.1 From f481bf7849fd162968bc7e13bc4e70c37a8aba03 Mon Sep 17 00:00:00 2001 From: Thomas Khyn Date: Sat, 2 Aug 2014 16:08:48 +1200 Subject: Fixed console encoding issue The issue occured with python 3.3 on windows with dependencies needing to be compiled (e.g. markupsafe) --- tests/test_venv.py | 2 -- tox/_venv.py | 11 +++++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/test_venv.py b/tests/test_venv.py index 6482e09..f6a2f32 100644 --- a/tests/test_venv.py +++ b/tests/test_venv.py @@ -552,8 +552,6 @@ def test_run_install_command(newmocksession): assert 'install' in l[0].args env = l[0].env assert env is not None - assert 'PYTHONIOENCODING' in env - assert env['PYTHONIOENCODING'] == 'utf_8' def test_run_custom_install_command(newmocksession): mocksession = newmocksession([], """ diff --git a/tox/_venv.py b/tox/_venv.py index 7e9f45f..d5df526 100644 --- a/tox/_venv.py +++ b/tox/_venv.py @@ -1,5 +1,6 @@ from __future__ import with_statement import sys, os +import codecs import py import tox from tox._config import DepConfig @@ -276,11 +277,13 @@ class VirtualEnv(object): del os.environ[x] except KeyError: pass - env = dict(PYTHONIOENCODING='utf_8') - if extraenv is not None: - env.update(extraenv) + old_stdout = sys.stdout + sys.stdout = codecs.getwriter('utf8')(sys.stdout) + if extraenv is None: + extraenv = {} self._pcall(argv, cwd=self.envconfig.config.toxinidir, - extraenv=env, action=action) + extraenv=extraenv, action=action) + sys.stdout = old_stdout def _install(self, deps, extraopts=None, action=None): if not deps: -- cgit v1.2.1 From 7969038573c8f2e7646a048ac8c738cd208ed571 Mon Sep 17 00:00:00 2001 From: Alexander Schepanovski Date: Sat, 9 Aug 2014 20:55:53 +0800 Subject: Remove factor negation support --- tests/test_config.py | 10 +++++----- tox/_config.py | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test_config.py b/tests/test_config.py index 28afa6c..fc429a4 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -835,20 +835,20 @@ class TestConfigTestEnv: def test_factors(self, newconfig): inisource=""" [tox] - envlist = a,b + envlist = a-x,b [testenv] deps= dep-all a: dep-a b: dep-b - !a: dep-not-a + x: dep-x """ 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"] + assert [dep.name for dep in configs['a-x'].deps] == \ + ["dep-all", "dep-a", "dep-x"] + assert [dep.name for dep in configs['b'].deps] == ["dep-all", "dep-b"] def test_default_factors(self, newconfig): inisource=""" diff --git a/tox/_config.py b/tox/_config.py index 6e8e706..2503962 100644 --- a/tox/_config.py +++ b/tox/_config.py @@ -302,7 +302,7 @@ class parseini: factors = set() if section in self._cfg: for _, value in self._cfg[section].items(): - factors.update(re.findall(r'^!?(\w+)\:\s+', value, re.M)) + factors.update(re.findall(r'^(\w+)\:\s+', value, re.M)) return factors def _makeenvconfig(self, name, section, subs, config): @@ -628,12 +628,12 @@ class IniReader: 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 - negate, factor, line = m.groups() - if bool(negate) ^ (factor in self.factors): + factor, line = m.groups() + if factor in self.factors: return line lines = s.strip().splitlines() -- cgit v1.2.1 From 95c5fee5612759f9ec37e97e419776a6a359f3a5 Mon Sep 17 00:00:00 2001 From: Alexander Schepanovski Date: Sat, 9 Aug 2014 22:19:09 +0800 Subject: Support boolean ops on factors --- tests/test_config.py | 18 ++++++++++++++++++ tox/_config.py | 14 ++++++++++---- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/tests/test_config.py b/tests/test_config.py index fc429a4..7cbb656 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -850,6 +850,24 @@ class TestConfigTestEnv: ["dep-all", "dep-a", "dep-x"] assert [dep.name for dep in configs['b'].deps] == ["dep-all", "dep-b"] + def test_factor_ops(self, newconfig): + inisource=""" + [tox] + envlist = {a,b}-{x,y} + + [testenv] + deps= + a,b: dep-a-or-b + a-x: dep-a-and-x + {a,b}-y: dep-ab-and-y + """ + configs = newconfig([], inisource).envconfigs + get_deps = lambda env: [dep.name for dep in configs[env].deps] + assert get_deps("a-x") == ["dep-a-or-b", "dep-a-and-x"] + assert get_deps("a-y") == ["dep-a-or-b", "dep-ab-and-y"] + assert get_deps("b-x") == ["dep-a-or-b"] + assert get_deps("b-y") == ["dep-a-or-b", "dep-ab-and-y"] + def test_default_factors(self, newconfig): inisource=""" [tox] diff --git a/tox/_config.py b/tox/_config.py index 2503962..26603c9 100644 --- a/tox/_config.py +++ b/tox/_config.py @@ -302,7 +302,8 @@ class parseini: factors = set() if section in self._cfg: for _, value in self._cfg[section].items(): - factors.update(re.findall(r'^(\w+)\:\s+', value, re.M)) + exprs = re.findall(r'^([\w{},-]+)\:\s+', value, re.M) + factors.update(*mapcat(_split_factor_expr, exprs)) return factors def _makeenvconfig(self, name, section, subs, config): @@ -434,12 +435,17 @@ class parseini: dep2_name = pkg_resources.Requirement.parse(dep2).project_name return dep1_name == dep2_name + def _split_env(env): """if handed a list, action="append" was used for -e """ if not isinstance(env, list): env = [env] return mapcat(_expand_envstr, env) +def _split_factor_expr(expr): + partial_envs = _expand_envstr(expr) + return [set(e.split('-')) for e in partial_envs] + def _expand_envstr(envstr): # split by commas not in groups tokens = re.split(r'(\{[^}]+\})|,', envstr) @@ -628,12 +634,12 @@ class IniReader: 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 - factor, line = m.groups() - if factor in self.factors: + expr, line = m.groups() + if any(fs <= self.factors for fs in _split_factor_expr(expr)): return line lines = s.strip().splitlines() -- cgit v1.2.1 From a018e738f4e8d700fb6c8d32e6c0d6746d2c2e61 Mon Sep 17 00:00:00 2001 From: Alexander Schepanovski Date: Sat, 9 Aug 2014 22:44:41 +0800 Subject: Docs for factor expressions --- doc/config.txt | 68 +++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 44 insertions(+), 24 deletions(-) diff --git a/doc/config.txt b/doc/config.txt index 0721a87..a084246 100644 --- a/doc/config.txt +++ b/doc/config.txt @@ -405,7 +405,7 @@ However, a better approach looks like this:: pytest django15: Django>=1.5,<1.6 django16: Django>=1.6,<1.7 - !py27: unittest2 + py26: unittest2 commands = py.test This uses two new facilities of tox-1.8: @@ -417,6 +417,7 @@ This uses two new facilities of tox-1.8: Let's go through this step by step. + Generative envlist +++++++++++++++++++++++ @@ -432,7 +433,7 @@ like this:: py27-django15 py27-django16 -You can still list environments explicitely along with generated ones:: +You can still list environments explicitly along with generated ones:: envlist = {py26,py27}-django{15,16}, docs, flake @@ -441,13 +442,13 @@ You can still list environments explicitely along with generated ones:: 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 -l + py26-django15 + py26-django16 + py27-django15 + py27-django16 + docs + flake Factors and factor-conditional settings @@ -471,23 +472,14 @@ optional lines with unconditional ones:: pytest django15: Django>=1.5,<1.6 django16: Django>=1.6,<1.7 - !py27: unittest2 + py26: unittest2 -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: +Reading it line by line: -=============== ================================== -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. +- ``pytest`` will be included unconditionally, +- ``Django>=1.5,<1.6`` will be included for environments containing ``django15`` factor, +- ``Django>=1.6,<1.7`` similarly depends on ``django16`` factor, +- ``unittest`` will be loaded for Python 2.6 environments. .. note:: @@ -496,6 +488,34 @@ factors such as platform, python version and/or database. setting. +Complex factor conditions ++++++++++++++++++++++++++ + +Sometimes you need to specify same line for several factors or create a special case for +a combination of factors. Here is how you do it:: + + [tox] + envlist = py{25,26,27}-django{14,15,16}-{sqlite,mysql} + + [testenv] + deps = + py25-django14: simplejson ; use it only for this specific combination + py25,py26: unittest2 ; use it for both py25 and py26 + py{25,26}-django14: mock ; patching whatever in older python/django combo + +Take a look at first ``deps`` line. It shows how you can special case something +for a combination of factors, you just join combining factors with a hyphen. +This particular line states that ``simplejson`` will be loaded for python 2.5, django 1.4 +environments, e.g. ``py25-django14-sqlite`` and ``py25-django14-mysql``. + +The second line shows how you use same line for several factors - by listing them +delimited by commas. It's possible to list not only simple factors, but also their +combinations like ``py25-django14,py26-django14``. + +Finally, factor expressions are expanded the same way as envlist, so a last example +could be rewritten as ``py{25,26}-django14``. + + Other Rules and notes ===================== -- cgit v1.2.1 From 0a8c4513f6ea3f51b2e8861eae2fbb6faa0a3892 Mon Sep 17 00:00:00 2001 From: Alexander Schepanovski Date: Mon, 11 Aug 2014 18:17:59 +0800 Subject: Update complex factor docs --- doc/config.txt | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/doc/config.txt b/doc/config.txt index a084246..8ce64db 100644 --- a/doc/config.txt +++ b/doc/config.txt @@ -491,29 +491,42 @@ Reading it line by line: Complex factor conditions +++++++++++++++++++++++++ -Sometimes you need to specify same line for several factors or create a special case for -a combination of factors. Here is how you do it:: +Sometimes you need to specify same line for several factors or create a special +case for a combination of factors. Here is how you do it:: [tox] - envlist = py{25,26,27}-django{14,15,16}-{sqlite,mysql} + envlist = py{26,27,33}-django{15,16}-{sqlite,mysql} [testenv] deps = - py25-django14: simplejson ; use it only for this specific combination - py25,py26: unittest2 ; use it for both py25 and py26 - py{25,26}-django14: mock ; patching whatever in older python/django combo + py33-mysql: PyMySQL ; use if both py33 and mysql are in an env name + py26,py27: urllib3 ; use if any of py26 or py27 are in an env name + py{26,27}-sqlite: mock ; mocking sqlite in python 2.x Take a look at first ``deps`` line. It shows how you can special case something for a combination of factors, you just join combining factors with a hyphen. -This particular line states that ``simplejson`` will be loaded for python 2.5, django 1.4 -environments, e.g. ``py25-django14-sqlite`` and ``py25-django14-mysql``. +This particular line states that ``PyMySQL`` will be loaded for python 3.3, +mysql environments, e.g. ``py33-django15-mysql`` and ``py33-django16-mysql``. -The second line shows how you use same line for several factors - by listing them -delimited by commas. It's possible to list not only simple factors, but also their -combinations like ``py25-django14,py26-django14``. +The second line shows how you use same line for several factors - by listing +them delimited by commas. It's possible to list not only simple factors, but +also their combinations like ``py26-sqlite,py27-sqlite``. -Finally, factor expressions are expanded the same way as envlist, so a last example -could be rewritten as ``py{25,26}-django14``. +Finally, factor expressions are expanded the same way as envlist, so last +example could be rewritten as ``py{26,27}-sqlite``. + +.. note:: + + Factors don't do substring matching against env name, instead every + hyphenated expression is split by ``-`` and if ALL the factors in an + expression are also factors of an env then that condition is considered + hold. + + For example, environment ``py26-mysql``: + + - could be matched with expressions ``py26``, ``py26-mysql``, + ``mysql-py26``, + - but not with ``py2`` or ``py26-sql``. Other Rules and notes -- cgit v1.2.1 From d3be3dbf74a11ce4a0631dd4f1879c537060ace4 Mon Sep 17 00:00:00 2001 From: Ivan Larin Date: Tue, 19 Aug 2014 06:58:35 +0000 Subject: docs add pypy3 support --- doc/example/basic.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/example/basic.txt b/doc/example/basic.txt index 6b115c8..562e2bf 100644 --- a/doc/example/basic.txt +++ b/doc/example/basic.txt @@ -41,6 +41,7 @@ Available "default" test environments names are:: py34 jython pypy + pypy3 However, you can also create your own test environment names, see some of the examples in :doc:`examples <../examples>`. -- cgit v1.2.1 From c0af5a5eb243d3eb0b4cdd8e452e00838c187010 Mon Sep 17 00:00:00 2001 From: Steven Myint Date: Mon, 1 Sep 2014 08:12:16 -0700 Subject: Fix "__PYVENV_LAUNCHER__" problem This resolves the use of the non-virtualenv Python executable. This fixes #171. --- tox/_venv.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox/_venv.py b/tox/_venv.py index d5df526..fea2f0c 100644 --- a/tox/_venv.py +++ b/tox/_venv.py @@ -272,7 +272,8 @@ class VirtualEnv(object): if '{opts}' in argv: i = argv.index('{opts}') argv[i:i+1] = list(options) - for x in ('PIP_RESPECT_VIRTUALENV', 'PIP_REQUIRE_VIRTUALENV'): + for x in ('PIP_RESPECT_VIRTUALENV', 'PIP_REQUIRE_VIRTUALENV', + '__PYVENV_LAUNCHER__'): try: del os.environ[x] except KeyError: -- cgit v1.2.1 From bf52a34ed710df36143bc006fa7750f35550ef9e Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 2 Sep 2014 14:55:16 +0200 Subject: fix issue148: ignore __PYVENV_LAUNCHER__ for subproc --- CHANGELOG | 3 +++ doc/Makefile | 2 +- doc/conf.py | 3 ++- setup.py | 2 +- tests/test_venv.py | 2 ++ tox/__init__.py | 2 +- 6 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9d991c7..4597560 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,9 @@ - new multi-dimensional configuration support. Many thanks to Alexander Schepanovski for the complete PR with docs. +- fix issue148: remove "__PYVENV_LAUNCHER__" from os.environ when starting + subprocesses. Thanks Seven Myint. + 1.7.2 ----------- diff --git a/doc/Makefile b/doc/Makefile index 77b2083..d3393d6 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -37,7 +37,7 @@ clean: -rm -rf $(BUILDDIR)/* install: clean html - @rsync -avz $(BUILDDIR)/html/ testrun.org:/www/testrun.org/tox/latest + @rsync -avz $(BUILDDIR)/html/ testrun.org:/www/testrun.org/tox/dev #latexpdf #@scp $(BUILDDIR)/latex/*.pdf testrun.org:www-tox/latest diff --git a/doc/conf.py b/doc/conf.py index ceb3397..0221f3c 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -48,7 +48,8 @@ copyright = u'2013, holger krekel and others' # built documents. # # The short X.Y version. -release = version = "1.7.2" +release = "1.8" +version = "1.8.0.dev" # The full version, including alpha/beta/rc tags. # The language for content autogenerated by Sphinx. Refer to documentation diff --git a/setup.py b/setup.py index 8e2f8f0..f8390bb 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ def main(): description='virtualenv-based automation of test activities', long_description=open("README.rst").read(), url='http://tox.testrun.org/', - version='1.7.2', + version='1.8.0.dev1', license='http://opensource.org/licenses/MIT', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], author='holger krekel', diff --git a/tests/test_venv.py b/tests/test_venv.py index f6a2f32..00e5f8b 100644 --- a/tests/test_venv.py +++ b/tests/test_venv.py @@ -485,9 +485,11 @@ class TestVenvTest: py.test.raises(ZeroDivisionError, "venv._pcall([1,2,3])") monkeypatch.setenv("PIP_RESPECT_VIRTUALENV", "1") monkeypatch.setenv("PIP_REQUIRE_VIRTUALENV", "1") + monkeypatch.setenv("__PYVENV_LAUNCHER__", "1") py.test.raises(ZeroDivisionError, "venv.run_install_command(['qwe'])") assert 'PIP_RESPECT_VIRTUALENV' not in os.environ assert 'PIP_REQUIRE_VIRTUALENV' not in os.environ + assert '__PYVENV_LAUNCHER__' not in os.environ def test_setenv_added_to_pcall(tmpdir, mocksession, newconfig): pkg = tmpdir.ensure("package.tar.gz") diff --git a/tox/__init__.py b/tox/__init__.py index 530f3c4..e12f703 100644 --- a/tox/__init__.py +++ b/tox/__init__.py @@ -1,5 +1,5 @@ # -__version__ = '1.7.2' +__version__ = '1.8.0.dev1' class exception: class Error(Exception): -- cgit v1.2.1 From 809e11e42c0526fc1893e940b68c8011cf368459 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 2 Sep 2014 15:00:04 +0200 Subject: fix typo in stevens name --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 4597560..f0aadbc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,7 +5,7 @@ Alexander Schepanovski for the complete PR with docs. - fix issue148: remove "__PYVENV_LAUNCHER__" from os.environ when starting - subprocesses. Thanks Seven Myint. + subprocesses. Thanks Steven Myint. 1.7.2 -- cgit v1.2.1 From d5d42e21988896981f2028b1d986f13c3b5cbeb8 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 23 Sep 2014 16:04:03 +0200 Subject: fix issue188 make boolean config vars use the default if the config file specifies an empty string --- CHANGELOG | 2 ++ setup.py | 2 +- tests/test_config.py | 15 +++++++++++++++ tox/__init__.py | 2 +- tox/_config.py | 3 +++ 5 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f0aadbc..9e69564 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,11 +3,13 @@ - new multi-dimensional configuration support. Many thanks to Alexander Schepanovski for the complete PR with docs. + And to Mike Bayer for filing an issue wrt to setting booleans. - fix issue148: remove "__PYVENV_LAUNCHER__" from os.environ when starting subprocesses. Thanks Steven Myint. + 1.7.2 ----------- diff --git a/setup.py b/setup.py index f8390bb..a9ba348 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ def main(): description='virtualenv-based automation of test activities', long_description=open("README.rst").read(), url='http://tox.testrun.org/', - version='1.8.0.dev1', + version='1.8.0.dev2', license='http://opensource.org/licenses/MIT', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], author='holger krekel', diff --git a/tests/test_config.py b/tests/test_config.py index 7cbb656..efbbc80 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -882,6 +882,21 @@ class TestConfigTestEnv: for name, config in configs.items(): assert config.basepython == 'python%s.%s' % (name[2], name[3]) + @pytest.mark.issue188 + def test_factors_in_boolean(self, newconfig): + inisource=""" + [tox] + envlist = py{27,33} + + [testenv] + recreate = + py27: True + """ + configs = newconfig([], inisource).envconfigs + assert configs["py27"].recreate + assert not configs["py33"].recreate + + class TestGlobalOptions: def test_notest(self, newconfig): config = newconfig([], "") diff --git a/tox/__init__.py b/tox/__init__.py index e12f703..775ed11 100644 --- a/tox/__init__.py +++ b/tox/__init__.py @@ -1,5 +1,5 @@ # -__version__ = '1.8.0.dev1' +__version__ = '1.8.0.dev2' class exception: class Error(Exception): diff --git a/tox/_config.py b/tox/_config.py index 26603c9..07f2365 100644 --- a/tox/_config.py +++ b/tox/_config.py @@ -596,9 +596,12 @@ class IniReader: def getbool(self, section, name, default=None): s = self.getdefault(section, name, default) + if not s: + s = default if s is None: raise KeyError("no config value [%s] %s found" % ( section, name)) + if not isinstance(s, bool): if s.lower() == "true": s = True -- cgit v1.2.1