Hello community,

here is the log from the commit of package python3-tox for openSUSE:Factory 
checked in at 2015-12-18 21:51:35
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python3-tox (Old)
 and      /work/SRC/openSUSE:Factory/.python3-tox.new (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python3-tox"

Changes:
--------
--- /work/SRC/openSUSE:Factory/python3-tox/python3-tox.changes  2015-11-24 
22:32:29.000000000 +0100
+++ /work/SRC/openSUSE:Factory/.python3-tox.new/python3-tox.changes     
2015-12-18 21:51:36.000000000 +0100
@@ -1,0 +2,26 @@
+Sun Dec 13 20:26:34 UTC 2015 - [email protected]
+
+- update to version 2.3.0:
+  * DEPRECATE use of "indexservers" in tox.ini. It complicates the
+    internal code and it is recommended to rather use the
+    devpi system for managing indexes for pip.
+  * fix issue285: make setenv processing fully lazy to fix regressions
+    of tox-2.2.X and so that we can now have testenv attributes like
+    "basepython" depend on environment variables that are set in a
+    setenv section. Thanks Nelfin for some tests and initial work on a
+    PR.
+  * allow "#" in commands.  This is slightly incompatible with
+    commands sections that used a comment after a "\" line
+    continuation. Thanks David Stanek for the PR.
+  * fix issue289: fix build_sphinx target, thanks Barry Warsaw.
+  * fix issue252: allow environment names with special characters.
+    Thanks Julien Castets for initial PR and patience.
+  * introduce experimental tox_testenv_create(venv, action) and
+    tox_testenv_install_deps(venv, action) hooks to allow plugins to
+    do additional work on creation or installing deps.  These hooks
+    are experimental mainly because of the involved "venv" and session
+    objects whose current public API is not fully guranteed.
+  * internal: push some optional object creation into tests because
+    tox core doesn't need it.
+
+-------------------------------------------------------------------

Old:
----
  tox-2.2.1.tar.gz

New:
----
  tox-2.3.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python3-tox.spec ++++++
--- /var/tmp/diff_new_pack.QQWIpi/_old  2015-12-18 21:51:37.000000000 +0100
+++ /var/tmp/diff_new_pack.QQWIpi/_new  2015-12-18 21:51:37.000000000 +0100
@@ -17,7 +17,7 @@
 
 
 Name:           python3-tox
-Version:        2.2.1
+Version:        2.3.0
 Release:        0
 Summary:        Virtualenv-based automation of test activities
 License:        MIT

++++++ tox-2.2.1.tar.gz -> tox-2.3.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tox-2.2.1/CHANGELOG new/tox-2.3.0/CHANGELOG
--- old/tox-2.2.1/CHANGELOG     2015-11-11 15:57:13.000000000 +0100
+++ new/tox-2.3.0/CHANGELOG     2015-12-09 13:38:11.000000000 +0100
@@ -1,3 +1,35 @@
+2.3.0
+-----
+
+- DEPRECATE use of "indexservers" in tox.ini.  It complicates
+  the internal code and it is recommended to rather use the
+  devpi system for managing indexes for pip.
+
+- fix issue285: make setenv processing fully lazy to fix regressions
+  of tox-2.2.X and so that we can now have testenv attributes like 
+  "basepython" depend on environment variables that are set in 
+  a setenv section. Thanks Nelfin for some tests and initial
+  work on a PR.
+
+- allow "#" in commands.  This is slightly incompatible with commands
+  sections that used a comment after a "\" line continuation.
+  Thanks David Stanek for the PR.
+
+- fix issue289: fix build_sphinx target, thanks Barry Warsaw.
+
+- fix issue252: allow environment names with special characters.
+  Thanks Julien Castets for initial PR and patience.
+
+- introduce experimental tox_testenv_create(venv, action) and 
+  tox_testenv_install_deps(venv, action) hooks to allow
+  plugins to do additional work on creation or installing
+  deps.  These hooks are experimental mainly because of
+  the involved "venv" and session objects whose current public 
+  API is not fully guranteed.
+
+- internal: push some optional object creation into tests because
+  tox core doesn't need it.
+
 2.2.1
 -----
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tox-2.2.1/PKG-INFO new/tox-2.3.0/PKG-INFO
--- old/tox-2.2.1/PKG-INFO      2015-11-11 15:57:14.000000000 +0100
+++ new/tox-2.3.0/PKG-INFO      2015-12-09 13:38:12.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: tox
-Version: 2.2.1
+Version: 2.3.0
 Summary: virtualenv-based automation of test activities
 Home-page: http://tox.testrun.org/
 Author: holger krekel
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tox-2.2.1/doc/Makefile new/tox-2.3.0/doc/Makefile
--- old/tox-2.2.1/doc/Makefile  2015-11-11 15:57:13.000000000 +0100
+++ new/tox-2.3.0/doc/Makefile  2015-12-09 13:38:11.000000000 +0100
@@ -12,6 +12,8 @@
 PAPEROPT_letter = -D latex_paper_size=letter
 ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
 
+SITETARGET=$(shell ./_getdoctarget.py)
+
 .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp 
epub latex latexpdf text man changes linkcheck doctest
 
 help:
@@ -36,8 +38,10 @@
 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/$(SITETARGET)
        #dev
     #latexpdf
        #@scp $(BUILDDIR)/latex/*.pdf testrun.org:www-tox/latest
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tox-2.2.1/doc/_getdoctarget.py 
new/tox-2.3.0/doc/_getdoctarget.py
--- old/tox-2.2.1/doc/_getdoctarget.py  1970-01-01 01:00:00.000000000 +0100
+++ new/tox-2.3.0/doc/_getdoctarget.py  2015-12-09 13:38:11.000000000 +0100
@@ -0,0 +1,16 @@
+#!/usr/bin/env python
+
+import py
+
+def get_version_string():
+    fn = py.path.local(__file__).join("..", "..",
+                                      "tox", "__init__.py")
+    for line in fn.readlines():
+        if "version" in line and not line.strip().startswith('#'):
+            return eval(line.split("=")[-1])
+
+def get_minor_version_string():
+    return ".".join(get_version_string().split(".")[:2])
+
+if __name__ == "__main__":
+    print (get_minor_version_string())
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tox-2.2.1/doc/conf.py new/tox-2.3.0/doc/conf.py
--- old/tox-2.2.1/doc/conf.py   2015-11-11 15:57:13.000000000 +0100
+++ new/tox-2.3.0/doc/conf.py   2015-12-09 13:38:11.000000000 +0100
@@ -13,6 +13,13 @@
 
 import sys, os
 
+# The short X.Y version.
+sys.path.insert(0, os.path.dirname(__file__))
+import _getdoctarget
+
+version = _getdoctarget.get_minor_version_string()
+release = _getdoctarget.get_version_string()
+
 # If extensions (or modules to document with autodoc) are in another directory,
 # add these directories to sys.path here. If the directory is relative to the
 # documentation root, use os.path.abspath to make it absolute, like shown here.
@@ -47,9 +54,6 @@
 # |version| and |release|, also used in various other places throughout the
 # built documents.
 #
-# The short X.Y version.
-release = "2.2"
-version = "2.2.0"
 # The full version, including alpha/beta/rc tags.
 
 # The language for content autogenerated by Sphinx. Refer to documentation
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tox-2.2.1/doc/config-v2.txt 
new/tox-2.3.0/doc/config-v2.txt
--- old/tox-2.2.1/doc/config-v2.txt     2015-11-11 15:57:13.000000000 +0100
+++ new/tox-2.3.0/doc/config-v2.txt     2015-12-09 13:38:11.000000000 +0100
@@ -143,7 +143,7 @@
 
 A testenv can define a new ``platform`` setting.  If its value
 is not contained in the string obtained from calling 
-``platform.platform()`` the environment will be skipped.
+``sys.platform`` the environment will be skipped.
 
 Expanding the ``envlist`` setting
 ----------------------------------------------------------
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tox-2.2.1/doc/config.txt new/tox-2.3.0/doc/config.txt
--- old/tox-2.2.1/doc/config.txt        2015-11-11 15:57:13.000000000 +0100
+++ new/tox-2.3.0/doc/config.txt        2015-12-09 13:38:11.000000000 +0100
@@ -93,9 +93,6 @@
 
     .. versionadded:: 1.6
 
-    **WARNING**: This setting is **EXPERIMENTAL** so use with care
-    and be ready to adapt your tox.ini's with post-1.6 tox releases.
-
     the ``install_command`` setting is used for installing packages into
     the virtual environment; both the package under test
     and any defined dependencies. Must contain the substitution key
@@ -166,7 +163,8 @@
     package installation.  Each line defines a dependency, which will be
     passed to the installer command for processing.  Each line specifies a 
file,
     a URL or a package name.  You can additionally specify
-    an :confval:`indexserver` to use for installing this dependency.
+    an :confval:`indexserver` to use for installing this dependency
+    but this functionality is deprecated since tox-2.3.
     All derived dependencies (deps required by the dep) will then be
     retrieved from the specified indexserver::
 
@@ -259,8 +257,9 @@
 
    .. versionadded:: 0.9
 
-   Multi-line ``name = URL`` definitions of python package servers.
-   Dependencies can specify using a specified index server through the
+   (DEPRECATED, will be removed in a future version) Multi-line ``name =
+   URL`` definitions of python package servers.  Dependencies can
+   specify using a specified index server through the
    ``:indexservername:depname`` pattern.  The ``default`` indexserver
    definition determines where unscoped dependencies and the sdist install
    installs from.  Example::
@@ -441,7 +440,7 @@
    {[sectionname]valuename}
 
 which you can use to avoid repetition of config values.
-You can put default values in one section and reference them in others to 
avoid repeting the same values::
+You can put default values in one section and reference them in others to 
avoid repeating the same values::
 
     [base]
     deps =
@@ -455,7 +454,7 @@
         {[base]deps}
 
     [testenv:mercurial]
-    dep =
+    deps =
         mercurial
         {[base]deps}
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tox-2.2.1/doc/example/basic.txt 
new/tox-2.3.0/doc/example/basic.txt
--- old/tox-2.2.1/doc/example/basic.txt 2015-11-11 15:57:13.000000000 +0100
+++ new/tox-2.3.0/doc/example/basic.txt 2015-12-09 13:38:11.000000000 +0100
@@ -30,6 +30,7 @@
 
 Available "default" test environments names are::
 
+    py
     py24
     py25
     py26
@@ -43,6 +44,8 @@
     pypy
     pypy3
 
+The environment ``py`` uses the version of Python used to invoke tox.
+
 However, you can also create your own test environment names,
 see some of the examples in :doc:`examples <../examples>`.
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tox-2.2.1/doc/plugins.txt 
new/tox-2.3.0/doc/plugins.txt
--- old/tox-2.2.1/doc/plugins.txt       2015-11-11 15:57:13.000000000 +0100
+++ new/tox-2.3.0/doc/plugins.txt       2015-12-09 13:38:11.000000000 +0100
@@ -80,3 +80,9 @@
 
 .. autoclass:: tox.config.TestenvConfig()
     :members:
+
+.. autoclass:: tox.venv.VirtualEnv()
+    :members:
+
+.. autoclass:: tox.session.Session()
+    :members:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tox-2.2.1/setup.cfg new/tox-2.3.0/setup.cfg
--- old/tox-2.2.1/setup.cfg     2015-11-11 15:57:14.000000000 +0100
+++ new/tox-2.3.0/setup.cfg     2015-12-09 13:38:12.000000000 +0100
@@ -1,5 +1,5 @@
 [build_sphinx]
-source-dir = doc/en/
+source-dir = doc/
 build-dir = doc/build
 all_files = 1
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tox-2.2.1/setup.py new/tox-2.3.0/setup.py
--- old/tox-2.2.1/setup.py      2015-11-11 15:57:13.000000000 +0100
+++ new/tox-2.3.0/setup.py      2015-12-09 13:38:11.000000000 +0100
@@ -48,7 +48,7 @@
         description='virtualenv-based automation of test activities',
         long_description=open("README.rst").read(),
         url='http://tox.testrun.org/',
-        version='2.2.1',
+        version='2.3.0',
         license='http://opensource.org/licenses/MIT',
         platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'],
         author='holger krekel',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tox-2.2.1/tests/test_config.py 
new/tox-2.3.0/tests/test_config.py
--- old/tox-2.2.1/tests/test_config.py  2015-11-11 15:57:13.000000000 +0100
+++ new/tox-2.3.0/tests/test_config.py  2015-12-09 13:38:11.000000000 +0100
@@ -283,18 +283,9 @@
            commands =
              ls {env:TEST}
         """)
-        reader = SectionReader("testenv:py27", config._cfg)
-        x = reader.getargvlist("commands")
-        assert x == [
-            "ls testvalue".split()
-        ]
-        assert x != [
-            "ls {env:TEST}".split()
-        ]
-        y = reader.getargvlist("setenv")
-        assert y == [
-            "TEST=testvalue".split()
-        ]
+        envconfig = config.envconfigs["py27"]
+        assert envconfig.commands == [["ls", "testvalue"]]
+        assert envconfig.setenv["TEST"] == "testvalue"
 
 
 class TestIniParser:
@@ -459,7 +450,7 @@
         config = newconfig("""
             [section]
             key2=
-                cmd1 {item1} \ # a comment
+                cmd1 {item1} \
                      {item2}
         """)
         reader = SectionReader("section", config._cfg)
@@ -474,12 +465,32 @@
         config = newconfig("""
             [section]
             key1=
-                cmd1 'with space' \ # a comment
-                     'after the comment'
+                cmd1 'part one' \
+                     'part two'
+        """)
+        reader = SectionReader("section", config._cfg)
+        x = reader.getargvlist("key1")
+        assert x == [["cmd1", "part one", "part two"]]
+
+    def test_argvlist_comment_after_command(self, tmpdir, newconfig):
+        config = newconfig("""
+            [section]
+            key1=
+                cmd1 --flag  # run the flag on the command
         """)
         reader = SectionReader("section", config._cfg)
         x = reader.getargvlist("key1")
-        assert x == [["cmd1", "with space", "after the comment"]]
+        assert x == [["cmd1", "--flag"]]
+
+    def test_argvlist_command_contains_hash(self, tmpdir, newconfig):
+        config = newconfig("""
+            [section]
+            key1=
+                cmd1 --re  "use the # symbol for an arg"
+        """)
+        reader = SectionReader("section", config._cfg)
+        x = reader.getargvlist("key1")
+        assert x == [["cmd1", "--re", "use the # symbol for an arg"]]
 
     def test_argvlist_positional_substitution(self, tmpdir, newconfig):
         config = newconfig("""
@@ -633,7 +644,7 @@
         assert envconfig.usedevelop is False
         assert envconfig.ignore_errors is False
         assert envconfig.envlogdir == envconfig.envdir.join("log")
-        assert list(envconfig.setenv.keys()) == ['PYTHONHASHSEED']
+        assert list(envconfig.setenv.definitions.keys()) == ['PYTHONHASHSEED']
         hashseed = envconfig.setenv['PYTHONHASHSEED']
         assert isinstance(hashseed, str)
         # The following line checks that hashseed parses to an integer.
@@ -724,46 +735,6 @@
         if bp == "jython":
             assert envconfig.envpython == envconfig.envbindir.join(bp)
 
-    def test_setenv_overrides(self, tmpdir, newconfig):
-        config = newconfig("""
-            [testenv]
-            setenv =
-                PYTHONPATH = something
-                ANOTHER_VAL=else
-        """)
-        assert len(config.envconfigs) == 1
-        envconfig = config.envconfigs['python']
-        assert 'PYTHONPATH' in envconfig.setenv
-        assert 'ANOTHER_VAL' in envconfig.setenv
-        assert envconfig.setenv['PYTHONPATH'] == 'something'
-        assert envconfig.setenv['ANOTHER_VAL'] == 'else'
-
-    def test_setenv_with_envdir_and_basepython(self, tmpdir, newconfig):
-        config = newconfig("""
-            [testenv]
-            setenv =
-                VAL = {envdir}
-            basepython = {env:VAL}
-        """)
-        assert len(config.envconfigs) == 1
-        envconfig = config.envconfigs['python']
-        assert 'VAL' in envconfig.setenv
-        assert envconfig.setenv['VAL'] == envconfig.envdir
-        assert envconfig.basepython == envconfig.envdir
-
-    def test_setenv_ordering_1(self, tmpdir, newconfig):
-        config = newconfig("""
-            [testenv]
-            setenv=
-                VAL={envdir}
-            commands=echo {env:VAL}
-        """)
-        assert len(config.envconfigs) == 1
-        envconfig = config.envconfigs['python']
-        assert 'VAL' in envconfig.setenv
-        assert envconfig.setenv['VAL'] == envconfig.envdir
-        assert str(envconfig.envdir) in envconfig.commands[0]
-
     @pytest.mark.parametrize("plat", ["win32", "linux2"])
     def test_passenv_as_multiline_list(self, tmpdir, newconfig, monkeypatch, 
plat):
         monkeypatch.setattr(sys, "platform", plat)
@@ -1505,7 +1476,7 @@
         return envconfigs["python"]
 
     def _check_hashseed(self, envconfig, expected):
-        assert envconfig.setenv == {'PYTHONHASHSEED': expected}
+        assert envconfig.setenv['PYTHONHASHSEED'] == expected
 
     def _check_testenv(self, newconfig, expected, args=None, tox_ini=None):
         envconfig = self._get_envconfig(newconfig, args=args, tox_ini=tox_ini)
@@ -1554,7 +1525,7 @@
     def test_noset(self, tmpdir, newconfig):
         args = ['--hashseed', 'noset']
         envconfig = self._get_envconfig(newconfig, args=args)
-        assert envconfig.setenv == {}
+        assert not envconfig.setenv.definitions
 
     def test_noset_with_setenv(self, tmpdir, newconfig):
         tox_ini = """
@@ -1598,6 +1569,125 @@
         self._check_hashseed(envconfigs["hash2"], '123456789')
 
 
+class TestSetenv:
+    def test_getdict_lazy(self, tmpdir, newconfig, monkeypatch):
+        monkeypatch.setenv("X", "2")
+        config = newconfig("""
+            [testenv:X]
+            key0 =
+                key1 = {env:X}
+                key2 = {env:Y:1}
+        """)
+        envconfig = config.envconfigs["X"]
+        val = envconfig._reader.getdict_setenv("key0")
+        assert val["key1"] == "2"
+        assert val["key2"] == "1"
+
+    def test_getdict_lazy_update(self, tmpdir, newconfig, monkeypatch):
+        monkeypatch.setenv("X", "2")
+        config = newconfig("""
+            [testenv:X]
+            key0 =
+                key1 = {env:X}
+                key2 = {env:Y:1}
+        """)
+        envconfig = config.envconfigs["X"]
+        val = envconfig._reader.getdict_setenv("key0")
+        d = {}
+        d.update(val)
+        assert d == {"key1": "2", "key2": "1"}
+
+    def test_setenv_uses_os_environ(self, tmpdir, newconfig, monkeypatch):
+        monkeypatch.setenv("X", "1")
+        config = newconfig("""
+            [testenv:env1]
+            setenv =
+                X = {env:X}
+        """)
+        assert config.envconfigs["env1"].setenv["X"] == "1"
+
+    def test_setenv_default_os_environ(self, tmpdir, newconfig, monkeypatch):
+        monkeypatch.delenv("X", raising=False)
+        config = newconfig("""
+            [testenv:env1]
+            setenv =
+                X = {env:X:2}
+        """)
+        assert config.envconfigs["env1"].setenv["X"] == "2"
+
+    def test_setenv_uses_other_setenv(self, tmpdir, newconfig):
+        config = newconfig("""
+            [testenv:env1]
+            setenv =
+                Y = 5
+                X = {env:Y}
+        """)
+        assert config.envconfigs["env1"].setenv["X"] == "5"
+
+    def test_setenv_recursive_direct(self, tmpdir, newconfig):
+        config = newconfig("""
+            [testenv:env1]
+            setenv =
+                X = {env:X:3}
+        """)
+        assert config.envconfigs["env1"].setenv["X"] == "3"
+
+    def test_setenv_overrides(self, tmpdir, newconfig):
+        config = newconfig("""
+            [testenv]
+            setenv =
+                PYTHONPATH = something
+                ANOTHER_VAL=else
+        """)
+        assert len(config.envconfigs) == 1
+        envconfig = config.envconfigs['python']
+        assert 'PYTHONPATH' in envconfig.setenv
+        assert 'ANOTHER_VAL' in envconfig.setenv
+        assert envconfig.setenv['PYTHONPATH'] == 'something'
+        assert envconfig.setenv['ANOTHER_VAL'] == 'else'
+
+    def test_setenv_with_envdir_and_basepython(self, tmpdir, newconfig):
+        config = newconfig("""
+            [testenv]
+            setenv =
+                VAL = {envdir}
+            basepython = {env:VAL}
+        """)
+        assert len(config.envconfigs) == 1
+        envconfig = config.envconfigs['python']
+        assert 'VAL' in envconfig.setenv
+        assert envconfig.setenv['VAL'] == envconfig.envdir
+        assert envconfig.basepython == envconfig.envdir
+
+    def test_setenv_ordering_1(self, tmpdir, newconfig):
+        config = newconfig("""
+            [testenv]
+            setenv=
+                VAL={envdir}
+            commands=echo {env:VAL}
+        """)
+        assert len(config.envconfigs) == 1
+        envconfig = config.envconfigs['python']
+        assert 'VAL' in envconfig.setenv
+        assert envconfig.setenv['VAL'] == envconfig.envdir
+        assert str(envconfig.envdir) in envconfig.commands[0]
+
+    @pytest.mark.xfail(reason="we don't implement cross-section substitution 
for setenv")
+    def test_setenv_cross_section_subst(self, monkeypatch, newconfig):
+        """test that we can do cross-section substitution with setenv"""
+        monkeypatch.delenv('TEST', raising=False)
+        config = newconfig("""
+            [section]
+            x =
+              NOT_TEST={env:TEST:defaultvalue}
+
+            [testenv]
+            setenv = {[section]x}
+        """)
+        envconfig = config.envconfigs["python"]
+        assert envconfig.setenv["NOT_TEST"] == "defaultvalue"
+
+
 class TestIndexServer:
     def test_indexserver(self, tmpdir, newconfig):
         config = newconfig("""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tox-2.2.1/tests/test_venv.py 
new/tox-2.3.0/tests/test_venv.py
--- old/tox-2.2.1/tests/test_venv.py    2015-11-11 15:57:13.000000000 +0100
+++ new/tox-2.3.0/tests/test_venv.py    2015-12-09 13:38:11.000000000 +0100
@@ -56,7 +56,8 @@
     venv = VirtualEnv(envconfig, session=mocksession)
     assert venv.path == envconfig.envdir
     assert not venv.path.check()
-    venv.create()
+    action = mocksession.newaction(venv, "getenv")
+    tox_testenv_create(action=action, venv=venv)
     l = mocksession._pcalls
     assert len(l) >= 1
     args = l[0].args
@@ -96,7 +97,8 @@
     """)
     envconfig = config.envconfigs['site']
     venv = VirtualEnv(envconfig, session=mocksession)
-    venv.create()
+    action = mocksession.newaction(venv, "getenv")
+    tox_testenv_create(action=action, venv=venv)
     l = mocksession._pcalls
     assert len(l) >= 1
     args = l[0].args
@@ -105,7 +107,8 @@
 
     envconfig = config.envconfigs['nosite']
     venv = VirtualEnv(envconfig, session=mocksession)
-    venv.create()
+    action = mocksession.newaction(venv, "getenv")
+    tox_testenv_create(action=action, venv=venv)
     l = mocksession._pcalls
     assert len(l) >= 1
     args = l[0].args
@@ -122,14 +125,15 @@
             {distshare}/dep1-*
     """)
     venv = mocksession.getenv("py123")
-    venv.create()
+    action = mocksession.newaction(venv, "getenv")
+    tox_testenv_create(action=action, venv=venv)
     l = mocksession._pcalls
     assert len(l) == 1
     distshare = venv.session.config.distshare
     distshare.ensure("dep1-1.0.zip")
     distshare.ensure("dep1-1.1.zip")
 
-    venv.install_deps()
+    tox_testenv_install_deps(action=action, venv=venv)
     assert len(l) == 2
     args = l[-1].args
     assert l[-1].cwd == venv.envconfig.config.toxinidir
@@ -154,11 +158,12 @@
             dep2
     """)
     venv = mocksession.getenv("py123")
-    venv.create()
+    action = mocksession.newaction(venv, "getenv")
+    tox_testenv_create(action=action, venv=venv)
     l = mocksession._pcalls
     assert len(l) == 1
 
-    venv.install_deps()
+    tox_testenv_install_deps(action=action, venv=venv)
     assert len(l) == 2
     args = l[-1].args
     assert l[-1].cwd == venv.envconfig.config.toxinidir
@@ -183,12 +188,13 @@
             :abc2:dep3
     """)
     venv = mocksession.getenv('py123')
-    venv.create()
+    action = mocksession.newaction(venv, "getenv")
+    tox_testenv_create(action=action, venv=venv)
     l = mocksession._pcalls
     assert len(l) == 1
     l[:] = []
 
-    venv.install_deps()
+    tox_testenv_install_deps(action=action, venv=venv)
     # two different index servers, two calls
     assert len(l) == 3
     args = " ".join(l[0].args)
@@ -211,12 +217,13 @@
             dep1
     """)
     venv = mocksession.getenv('python')
-    venv.create()
+    action = mocksession.newaction(venv, "getenv")
+    tox_testenv_create(action=action, venv=venv)
     l = mocksession._pcalls
     assert len(l) == 1
     l[:] = []
 
-    venv.install_deps()
+    tox_testenv_install_deps(action=action, venv=venv)
     assert len(l) == 1
     args = " ".join(l[0].args)
     assert "--pre " in args
@@ -246,10 +253,12 @@
         deps=xyz
     """)
     venv = mocksession.getenv('python')
-    venv.update()
+
+    action = mocksession.newaction(venv, "update")
+    venv.update(action)
     mocksession.installpkg(venv, pkg)
     mocksession.report.expect("verbosity0", "*create*")
-    venv.update()
+    venv.update(action)
     mocksession.report.expect("verbosity0", "*recreate*")
 
 
@@ -263,7 +272,8 @@
     finally:
         tox.config.make_hashseed = original_make_hashseed
     venv = mocksession.getenv('python')
-    venv.update()
+    action = mocksession.newaction(venv, "update")
+    venv.update(action)
     venv.test()
     mocksession.report.expect("verbosity0", "python runtests: 
PYTHONHASHSEED='123456789'")
 
@@ -274,7 +284,8 @@
         commands = echo foo bar
     ''')
     venv = mocksession.getenv('python')
-    venv.update()
+    action = mocksession.newaction(venv, "update")
+    venv.update(action)
     venv.test()
     mocksession.report.expect("verbosity0", "*runtests*commands?0? | echo foo 
bar")
 
@@ -343,7 +354,8 @@
             dep2
     """)
     venv = mocksession.getenv('py123')
-    venv.create()
+    action = mocksession.newaction(venv, "getenv")
+    tox_testenv_create(action=action, venv=venv)
     l = mocksession._pcalls
     assert len(l) == 1
     args = l[0].args
@@ -429,7 +441,8 @@
         envconfig = config.envconfigs['python']
         venv = VirtualEnv(envconfig, session=mocksession)
         cconfig = venv._getliveconfig()
-        venv.update()
+        action = mocksession.newaction(venv, "update")
+        venv.update(action)
         assert not venv.path_config.check()
         mocksession.installpkg(venv, pkg)
         assert venv.path_config.check()
@@ -439,36 +452,42 @@
         mocksession.report.expect("*", "*create*")
         # modify config and check that recreation happens
         mocksession._clearmocks()
-        venv.update()
+        action = mocksession.newaction(venv, "update")
+        venv.update(action)
         mocksession.report.expect("*", "*reusing*")
         mocksession._clearmocks()
+        action = mocksession.newaction(venv, "update")
         cconfig.python = py.path.local("balla")
         cconfig.writeconfig(venv.path_config)
-        venv.update()
+        venv.update(action)
         mocksession.report.expect("verbosity0", "*recreate*")
 
     def test_dep_recreation(self, newconfig, mocksession):
         config = newconfig([], "")
         envconfig = config.envconfigs['python']
         venv = VirtualEnv(envconfig, session=mocksession)
-        venv.update()
+        action = mocksession.newaction(venv, "update")
+        venv.update(action)
         cconfig = venv._getliveconfig()
         cconfig.deps[:] = [("1" * 32, "xyz.zip")]
         cconfig.writeconfig(venv.path_config)
         mocksession._clearmocks()
-        venv.update()
+        action = mocksession.newaction(venv, "update")
+        venv.update(action)
         mocksession.report.expect("*", "*recreate*")
 
     def test_develop_recreation(self, newconfig, mocksession):
         config = newconfig([], "")
         envconfig = config.envconfigs['python']
         venv = VirtualEnv(envconfig, session=mocksession)
-        venv.update()
+        action = mocksession.newaction(venv, "update")
+        venv.update(action)
         cconfig = venv._getliveconfig()
         cconfig.usedevelop = True
         cconfig.writeconfig(venv.path_config)
         mocksession._clearmocks()
-        venv.update()
+        action = mocksession.newaction(venv, "update")
+        venv.update(action)
         mocksession.report.expect("verbosity0", "*recreate*")
 
 
@@ -481,21 +500,25 @@
             commands=abc
         """)
         venv = mocksession.getenv("python")
+        action = mocksession.newaction(venv, "getenv")
         monkeypatch.setenv("PATH", "xyz")
         l = []
         monkeypatch.setattr("py.path.local.sysfind", classmethod(
                             lambda *args, **kwargs: l.append(kwargs) or 0 / 0))
 
-        py.test.raises(ZeroDivisionError, "venv._install(list('123'))")
+        with pytest.raises(ZeroDivisionError):
+            venv._install(list('123'), action=action)
         assert l.pop()["paths"] == [venv.envconfig.envbindir]
-        py.test.raises(ZeroDivisionError, "venv.test()")
+        with pytest.raises(ZeroDivisionError):
+            venv.test(action)
         assert l.pop()["paths"] == [venv.envconfig.envbindir]
-        py.test.raises(ZeroDivisionError, "venv.run_install_command(['qwe'])")
+        with pytest.raises(ZeroDivisionError):
+            venv.run_install_command(['qwe'], action=action)
         assert l.pop()["paths"] == [venv.envconfig.envbindir]
         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'])")
+        py.test.raises(ZeroDivisionError, "venv.run_install_command(['qwe'], 
action=action)")
         assert 'PIP_RESPECT_VIRTUALENV' not in os.environ
         assert 'PIP_REQUIRE_VIRTUALENV' not in os.environ
         assert '__PYVENV_LAUNCHER__' not in os.environ
@@ -626,3 +649,26 @@
     assert venv.status == "ignored failed command"
     mocksession.report.expect("warning", "*command failed but result from "
                                          "testenv is ignored*")
+
+
+def test_tox_testenv_create(newmocksession):
+    l = []
+
+    class Plugin:
+        @hookimpl
+        def tox_testenv_create(self, action, venv):
+            l.append(1)
+
+        @hookimpl
+        def tox_testenv_install_deps(self, action, venv):
+            l.append(2)
+
+    mocksession = newmocksession([], """
+        [testenv]
+        commands=testenv_fail
+        ignore_outcome=True
+    """, plugins=[Plugin()])
+
+    venv = mocksession.getenv('python')
+    venv.update(action=mocksession.newaction(venv, "getenv"))
+    assert l == [1, 2]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tox-2.2.1/tests/test_z_cmdline.py 
new/tox-2.3.0/tests/test_z_cmdline.py
--- old/tox-2.2.1/tests/test_z_cmdline.py       2015-11-11 15:57:13.000000000 
+0100
+++ new/tox-2.3.0/tests/test_z_cmdline.py       2015-12-09 13:38:11.000000000 
+0100
@@ -34,7 +34,8 @@
     report = session.report
     report.expect("using")
     venv = session.getvenv("mypython")
-    venv.update()
+    action = session.newaction(venv, "update")
+    venv.update(action)
     report.expect("logpopen")
 
 
@@ -301,6 +302,23 @@
     ])
 
 
+def test_venv_special_chars_issue252(cmd, initproj):
+    initproj("pkg123-0.7", filedefs={
+        'tests': {'test_hello.py': "def test_hello(): pass"},
+        'tox.ini': '''
+            [tox]
+            envlist = special&&1
+            [testenv:special&&1]
+            changedir=tests
+        '''
+    })
+    result = cmd.run("tox", )
+    assert result.ret == 0
+    result.stdout.fnmatch_lines([
+        "*installed*pkg123*"
+    ])
+
+
 def test_unknown_environment(cmd, initproj):
     initproj("env123-0.7", filedefs={
         'tox.ini': ''
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tox-2.2.1/tox/__init__.py 
new/tox-2.3.0/tox/__init__.py
--- old/tox-2.2.1/tox/__init__.py       2015-11-11 15:57:13.000000000 +0100
+++ new/tox-2.3.0/tox/__init__.py       2015-12-09 13:38:11.000000000 +0100
@@ -1,5 +1,5 @@
 #
-__version__ = '2.2.1'
+__version__ = '2.3.0'
 
 from .hookspecs import hookspec, hookimpl  # noqa
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tox-2.2.1/tox/_pytestplugin.py 
new/tox-2.3.0/tox/_pytestplugin.py
--- old/tox-2.2.1/tox/_pytestplugin.py  2015-11-11 15:57:13.000000000 +0100
+++ new/tox-2.3.0/tox/_pytestplugin.py  2015-12-09 13:38:11.000000000 +0100
@@ -31,7 +31,7 @@
 
 @pytest.fixture
 def newconfig(request, tmpdir):
-    def newconfig(args, source=None):
+    def newconfig(args, source=None, plugins=()):
         if source is None:
             source = args
             args = []
@@ -40,7 +40,7 @@
         p.write(s)
         old = tmpdir.chdir()
         try:
-            return parseconfig(args)
+            return parseconfig(args, plugins=plugins)
         finally:
             old.chdir()
     return newconfig
@@ -168,9 +168,8 @@
     mocksession = request.getfuncargvalue("mocksession")
     newconfig = request.getfuncargvalue("newconfig")
 
-    def newmocksession(args, source):
-        config = newconfig(args, source)
-        mocksession.config = config
+    def newmocksession(args, source, plugins=()):
+        mocksession.config = newconfig(args, source, plugins=plugins)
         return mocksession
     return newmocksession
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tox-2.2.1/tox/config.py new/tox-2.3.0/tox/config.py
--- old/tox-2.2.1/tox/config.py 2015-11-11 15:57:13.000000000 +0100
+++ new/tox-2.3.0/tox/config.py 2015-12-09 13:38:11.000000000 +0100
@@ -26,14 +26,20 @@
 
 hookimpl = pluggy.HookimplMarker("tox")
 
+_dummy = object()
 
-def get_plugin_manager():
+
+def get_plugin_manager(plugins=()):
     # initialize plugin manager
+    import tox.venv
     pm = pluggy.PluginManager("tox")
     pm.add_hookspecs(hookspecs)
     pm.register(tox.config)
     pm.register(tox.interpreters)
+    pm.register(tox.venv)
     pm.load_setuptools_entrypoints("tox")
+    for plugin in plugins:
+        pm.register(plugin)
     pm.check_pending()
     return pm
 
@@ -184,7 +190,7 @@
         return value
 
 
-def parseconfig(args=None):
+def parseconfig(args=None, plugins=()):
     """
     :param list[str] args: Optional list of arguments.
     :type pkg: str
@@ -192,7 +198,7 @@
     :raise SystemExit: toxinit file is not found
     """
 
-    pm = get_plugin_manager()
+    pm = get_plugin_manager(plugins)
 
     if args is None:
         args = sys.argv[1:]
@@ -253,6 +259,47 @@
             setattr(namespace, self.dest, 0)
 
 
+class SetenvDict:
+    def __init__(self, dict, reader):
+        self.reader = reader
+        self.definitions = dict
+        self.resolved = {}
+        self._lookupstack = []
+
+    def __contains__(self, name):
+        return name in self.definitions
+
+    def get(self, name, default=None):
+        try:
+            return self.resolved[name]
+        except KeyError:
+            try:
+                if name in self._lookupstack:
+                    raise KeyError(name)
+                val = self.definitions[name]
+            except KeyError:
+                return os.environ.get(name, default)
+            self._lookupstack.append(name)
+            try:
+                self.resolved[name] = res = self.reader._replace(val)
+            finally:
+                self._lookupstack.pop()
+            return res
+
+    def __getitem__(self, name):
+        x = self.get(name, _dummy)
+        if x is _dummy:
+            raise KeyError(name)
+        return x
+
+    def keys(self):
+        return self.definitions.keys()
+
+    def __setitem__(self, name, value):
+        self.definitions[name] = value
+        self.resolved[name] = value
+
+
 @hookimpl
 def tox_addoption(parser):
     # formatter_class=argparse.ArgumentDefaultsHelpFormatter)
@@ -323,11 +370,22 @@
     parser.add_argument("args", nargs="*",
                         help="additional arguments available to command 
positional substitution")
 
-    # add various core venv interpreter attributes
     parser.add_testenv_attribute(
         name="envdir", type="path", default="{toxworkdir}/{envname}",
         help="venv directory")
 
+    # add various core venv interpreter attributes
+    def setenv(testenv_config, value):
+        setenv = value
+        config = testenv_config.config
+        if "PYTHONHASHSEED" not in setenv and config.hashseed is not None:
+            setenv['PYTHONHASHSEED'] = config.hashseed
+        return setenv
+
+    parser.add_testenv_attribute(
+        name="setenv", type="dict_setenv", postprocess=setenv,
+        help="list of X=Y lines with environment variable settings")
+
     def basepython_default(testenv_config, value):
         if value is None:
             for f in testenv_config.factors:
@@ -385,17 +443,6 @@
         name="recreate", type="bool", default=False, postprocess=recreate,
         help="always recreate this test environment.")
 
-    def setenv(testenv_config, value):
-        setenv = value
-        config = testenv_config.config
-        if "PYTHONHASHSEED" not in setenv and config.hashseed is not None:
-            setenv['PYTHONHASHSEED'] = config.hashseed
-        return setenv
-
-    parser.add_testenv_attribute(
-        name="setenv", type="dict", postprocess=setenv,
-        help="list of X=Y lines with environment variable settings")
-
     def passenv(testenv_config, value):
         # Flatten the list to deal with space-separated values.
         value = list(
@@ -515,8 +562,7 @@
         self.factors = factors
         self._reader = reader
 
-    @property
-    def envbindir(self):
+    def get_envbindir(self):
         """ path to directory where scripts/binaries reside. """
         if (sys.platform == "win32"
                 and "jython" not in self.basepython
@@ -526,7 +572,15 @@
             return self.envdir.join("bin")
 
     @property
+    def envbindir(self):
+        return self.get_envbindir()
+
+    @property
     def envpython(self):
+        """ path to python executable. """
+        return self.get_envpython()
+
+    def get_envpython(self):
         """ path to python/jython executable. """
         if "jython" in str(self.basepython):
             name = "jython"
@@ -534,8 +588,7 @@
             name = "python"
         return self.envbindir.join(name)
 
-    # no @property to avoid early calling (see callable(subst[key]) checks)
-    def envsitepackagesdir(self):
+    def get_envsitepackagesdir(self):
         """ return sitepackagesdir of the virtualenv environment.
         (only available during execution, not parsing)
         """
@@ -696,10 +749,13 @@
         vc = TestenvConfig(config=config, envname=name, factors=factors, 
reader=reader)
         reader.addsubstitutions(**subs)
         reader.addsubstitutions(envname=name)
+        reader.addsubstitutions(envbindir=vc.get_envbindir,
+                                envsitepackagesdir=vc.get_envsitepackagesdir,
+                                envpython=vc.get_envpython)
 
         for env_attr in config._testenv_attr:
             atype = env_attr.type
-            if atype in ("bool", "path", "string", "dict", "argv", "argvlist"):
+            if atype in ("bool", "path", "string", "dict", "dict_setenv", 
"argv", "argvlist"):
                 meth = getattr(reader, "get" + atype)
                 res = meth(env_attr.name, env_attr.default)
             elif atype == "space-separated-list":
@@ -716,9 +772,6 @@
             if atype == "path":
                 reader.addsubstitutions(**{env_attr.name: res})
 
-            if env_attr.name == "basepython":
-                reader.addsubstitutions(envbindir=vc.envbindir, 
envpython=vc.envpython,
-                                        
envsitepackagesdir=vc.envsitepackagesdir)
         return vc
 
     def _getenvdata(self, reader):
@@ -799,16 +852,6 @@
 is_section_substitution = re.compile("{\[[^{}\s]+\]\S+?}").match
 
 
-RE_ITEM_REF = re.compile(
-    r'''
-    (?<!\\)[{]
-    (?:(?P<sub_type>[^[:{}]+):)?    # optional sub_type for special rules
-    (?P<substitution_value>[^{}]*)  # substitution key
-    [}]
-    ''',
-    re.VERBOSE)
-
-
 class SectionReader:
     def __init__(self, section_name, cfgparser, fallbacksections=None, 
factors=()):
         self.section_name = section_name
@@ -817,6 +860,12 @@
         self.factors = factors
         self._subs = {}
         self._subststack = []
+        self._setenv = None
+
+    def get_environ_value(self, name):
+        if self._setenv is None:
+            return os.environ.get(name)
+        return self._setenv.get(name)
 
     def addsubstitutions(self, _posargs=None, **kw):
         self._subs.update(kw)
@@ -836,17 +885,26 @@
         return [x.strip() for x in s.split(sep) if x.strip()]
 
     def getdict(self, name, default=None, sep="\n"):
-        s = self.getstring(name, None)
-        if s is None:
+        value = self.getstring(name, None)
+        return self._getdict(value, default=default, sep=sep)
+
+    def getdict_setenv(self, name, default=None, sep="\n"):
+        value = self.getstring(name, None, replace=False)
+        definitions = self._getdict(value, default=default, sep=sep)
+        self._setenv = SetenvDict(definitions, reader=self)
+        return self._setenv
+
+    def _getdict(self, value, default, sep):
+        if value is None:
             return default or {}
 
-        value = {}
-        for line in s.split(sep):
+        d = {}
+        for line in value.split(sep):
             if line.strip():
                 name, rest = line.split('=', 1)
-                value[name.strip()] = rest.strip()
+                d[name.strip()] = rest.strip()
 
-        return value
+        return d
 
     def getbool(self, name, default=None):
         s = self.getstring(name, default)
@@ -888,11 +946,7 @@
             x = self._apply_factors(x)
 
         if replace and x and hasattr(x, 'replace'):
-            self._subststack.append((self.section_name, name))
-            try:
-                x = self._replace(x)
-            finally:
-                assert self._subststack.pop() == (self.section_name, name)
+            x = self._replace(x, name=name)
         # print "getstring", self.section_name, name, "returned", repr(x)
         return x
 
@@ -909,8 +963,58 @@
         lines = s.strip().splitlines()
         return '\n'.join(filter(None, map(factor_line, lines)))
 
+    def _replace(self, value, name=None, section_name=None):
+        if '{' not in value:
+            return value
+
+        section_name = section_name if section_name else self.section_name
+        self._subststack.append((section_name, name))
+        try:
+            return Replacer(self).do_replace(value)
+        finally:
+            assert self._subststack.pop() == (section_name, name)
+
+
+class Replacer:
+    RE_ITEM_REF = re.compile(
+        r'''
+        (?<!\\)[{]
+        (?:(?P<sub_type>[^[:{}]+):)?    # optional sub_type for special rules
+        (?P<substitution_value>[^{}]*)  # substitution key
+        [}]
+        ''',
+        re.VERBOSE)
+
+    def __init__(self, reader):
+        self.reader = reader
+
+    def do_replace(self, x):
+        return self.RE_ITEM_REF.sub(self._replace_match, x)
+
+    def _replace_match(self, match):
+        g = match.groupdict()
+
+        # special case: opts and packages. Leave {opts} and
+        # {packages} intact, they are replaced manually in
+        # _venv.VirtualEnv.run_install_command.
+        sub_value = g['substitution_value']
+        if sub_value in ('opts', 'packages'):
+            return '{%s}' % sub_value
+
+        try:
+            sub_type = g['sub_type']
+        except KeyError:
+            raise tox.exception.ConfigError(
+                "Malformed substitution; no substitution type provided")
+
+        if sub_type == "env":
+            return self._replace_env(match)
+        if sub_type is not None:
+            raise tox.exception.ConfigError(
+                "No support for the %s substitution type" % sub_type)
+        return self._replace_substitution(match)
+
     def _replace_env(self, match):
-        env_list = self.getdict('setenv')
         match_value = match.group('substitution_value')
         if not match_value:
             raise tox.exception.ConfigError(
@@ -924,75 +1028,40 @@
         else:
             envkey = match_value
 
-        if envkey not in os.environ and default is None:
-            if envkey not in env_list and default is None:
+        envvalue = self.reader.get_environ_value(envkey)
+        if envvalue is None:
+            if default is None:
                 raise tox.exception.ConfigError(
-                    "substitution env:%r: unknown environment variable %r" %
+                    "substitution env:%r: unknown environment variable %r "
+                    " or recursive definition." %
                     (envkey, envkey))
-        if envkey in os.environ:
-            return os.environ.get(envkey, default)
-        else:
-            return env_list.get(envkey, default)
+            return default
+        return envvalue
 
     def _substitute_from_other_section(self, key):
         if key.startswith("[") and "]" in key:
             i = key.find("]")
             section, item = key[1:i], key[i + 1:]
-            if section in self._cfg and item in self._cfg[section]:
-                if (section, item) in self._subststack:
+            cfg = self.reader._cfg
+            if section in cfg and item in cfg[section]:
+                if (section, item) in self.reader._subststack:
                     raise ValueError('%s already in %s' % (
-                        (section, item), self._subststack))
-                x = str(self._cfg[section][item])
-                self._subststack.append((section, item))
-                try:
-                    return self._replace(x)
-                finally:
-                    self._subststack.pop()
+                        (section, item), self.reader._subststack))
+                x = str(cfg[section][item])
+                return self.reader._replace(x, name=item, section_name=section)
 
         raise tox.exception.ConfigError(
             "substitution key %r not found" % key)
 
     def _replace_substitution(self, match):
         sub_key = match.group('substitution_value')
-        val = self._subs.get(sub_key, None)
+        val = self.reader._subs.get(sub_key, None)
         if val is None:
             val = self._substitute_from_other_section(sub_key)
         if py.builtin.callable(val):
             val = val()
         return str(val)
 
-    def _replace_match(self, match):
-        g = match.groupdict()
-
-        # special case: opts and packages. Leave {opts} and
-        # {packages} intact, they are replaced manually in
-        # _venv.VirtualEnv.run_install_command.
-        sub_value = g['substitution_value']
-        if sub_value in ('opts', 'packages'):
-            return '{%s}' % sub_value
-
-        handlers = {
-            'env': self._replace_env,
-            None: self._replace_substitution,
-        }
-        try:
-            sub_type = g['sub_type']
-        except KeyError:
-            raise tox.exception.ConfigError(
-                "Malformed substitution; no substitution type provided")
-
-        try:
-            handler = handlers[sub_type]
-        except KeyError:
-            raise tox.exception.ConfigError("No support for the %s 
substitution type" % sub_type)
-
-        return handler(match)
-
-    def _replace(self, x):
-        if '{' in x:
-            return RE_ITEM_REF.sub(self._replace_match, x)
-        return x
-
 
 class _ArgvlistReader:
     @classmethod
@@ -1010,9 +1079,6 @@
         current_command = ""
         for line in value.splitlines():
             line = line.rstrip()
-            i = line.find("#")
-            if i != -1:
-                line = line[:i].rstrip()
             if not line:
                 continue
             if line.endswith("\\"):
@@ -1064,7 +1130,6 @@
         shlexer = shlex.shlex(newcommand, posix=True)
         shlexer.whitespace_split = True
         shlexer.escape = ''
-        shlexer.commenters = ''
         argv = list(shlexer)
         return argv
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tox-2.2.1/tox/hookspecs.py 
new/tox-2.3.0/tox/hookspecs.py
--- old/tox-2.2.1/tox/hookspecs.py      2015-11-11 15:57:13.000000000 +0100
+++ new/tox-2.3.0/tox/hookspecs.py      2015-12-09 13:38:11.000000000 +0100
@@ -30,3 +30,14 @@
     per-testenv configuration, notably the ``.envname`` and ``.basepython``
     setting.
     """
+
+
+@hookspec
+def tox_testenv_create(venv, action):
+    """ [experimental] perform creation action for this venv.
+    """
+
+
+@hookspec
+def tox_testenv_install_deps(venv, action):
+    """ [experimental] perform install dependencies action for this venv.  """
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tox-2.2.1/tox/session.py new/tox-2.3.0/tox/session.py
--- old/tox-2.2.1/tox/session.py        2015-11-11 15:57:13.000000000 +0100
+++ new/tox-2.3.0/tox/session.py        2015-12-09 13:38:11.000000000 +0100
@@ -51,7 +51,7 @@
             "(overridable by '-e')")
     tw.line("TOX_TESTENV_PASSENV: space-separated list of extra "
             "environment variables to be passed into test command "
-            "environemnts")
+            "environments")
 
 
 def show_help_ini(config):
@@ -211,7 +211,7 @@
         if sys.platform == "win32":
             ext = os.path.splitext(str(newargs[0]))[1].lower()
             if ext == '.py' and self.venv:
-                newargs = [str(self.venv.getcommandpath())] + newargs
+                newargs = [str(self.envconfig.envpython)] + newargs
 
         return newargs
 
@@ -313,6 +313,8 @@
 
 
 class Session:
+    """ (unstable API).  the session object that ties
+    together configuration, reporting, venv creation, testing. """
 
     def __init__(self, config, popen=subprocess.Popen, Report=Reporter):
         self.config = config
@@ -533,11 +535,11 @@
                 action = self.newaction(venv, "envreport")
                 with action:
                     pip = venv.getcommandpath("pip")
-                    # we can't really call internal helpers here easily :/
-                    # output = venv._pcall([str(pip), "freeze"],
-                    #                      cwd=self.config.toxinidir,
-                    #                      action=action)
-                    output = py.process.cmdexec("%s freeze" % (pip))
+                    output = venv._pcall([str(pip), "freeze"],
+                                         cwd=self.config.toxinidir,
+                                         action=action)
+                    # the output contains a mime-header, skip it
+                    output = output.split("\n\n")[-1]
                     packages = output.strip().split("\n")
                     action.setactivity("installed", ",".join(packages))
                     envlog = self.resultlog.get_envlog(venv.name)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tox-2.2.1/tox/venv.py new/tox-2.3.0/tox/venv.py
--- old/tox-2.2.1/tox/venv.py   2015-11-11 15:57:13.000000000 +0100
+++ new/tox-2.3.0/tox/venv.py   2015-12-09 13:38:11.000000000 +0100
@@ -5,7 +5,7 @@
 import codecs
 import py
 import tox
-from .config import DepConfig
+from .config import DepConfig, hookimpl
 
 
 class CreationConfig:
@@ -56,19 +56,34 @@
     def __init__(self, envconfig=None, session=None):
         self.envconfig = envconfig
         self.session = session
-        self.path = envconfig.envdir
-        self.path_config = self.path.join(".tox-config1")
+
+    @property
+    def hook(self):
+        return self.envconfig.config.pluginmanager.hook
+
+    @property
+    def path(self):
+        """ Path to environment base dir. """
+        return self.envconfig.envdir
+
+    @property
+    def path_config(self):
+        return self.path.join(".tox-config1")
 
     @property
     def name(self):
+        """ test environment name. """
         return self.envconfig.envname
 
     def __repr__(self):
         return "<VirtualEnv at %r>" % (self.path)
 
-    def getcommandpath(self, name=None, venv=True, cwd=None):
-        if name is None:
-            return self.envconfig.envpython
+    def getcommandpath(self, name, venv=True, cwd=None):
+        """ return absolute path (str or localpath) for specified
+        command name.  If it's a localpath we will rewrite it as
+        as a relative path.  If venv is True we will check if the
+        command is coming from the venv or is whitelisted to come
+        from external. """
         name = str(name)
         if os.path.isabs(name):
             return name
@@ -114,12 +129,10 @@
     def _ispython3(self):
         return "python3" in str(self.envconfig.basepython)
 
-    def update(self, action=None):
+    def update(self, action):
         """ return status string for updating actual venv to match 
configuration.
             if status string is empty, all is ok.
         """
-        if action is None:
-            action = self.session.newaction(self, "update")
         rconfig = CreationConfig.readconfig(self.path_config)
         if not self.envconfig.recreate and rconfig and \
            rconfig.matches(self._getliveconfig()):
@@ -130,13 +143,14 @@
         else:
             action.setactivity("recreate", self.envconfig.envdir)
         try:
-            self.create(action)
+            self.hook.tox_testenv_create(action=action, venv=self)
+            self.just_created = True
         except tox.exception.UnsupportedInterpreter:
             return sys.exc_info()[1]
         except tox.exception.InterpreterNotFound:
             return sys.exc_info()[1]
         try:
-            self.install_deps(action)
+            self.hook.tox_testenv_install_deps(action=action, venv=self)
         except tox.exception.InvocationError:
             v = sys.exc_info()[1]
             return "could not install deps %s; v = %r" % (
@@ -172,31 +186,6 @@
     def matching_platform(self):
         return re.match(self.envconfig.platform, sys.platform)
 
-    def create(self, action=None):
-        # if self.getcommandpath("activate").dirpath().check():
-        #    return
-        if action is None:
-            action = self.session.newaction(self, "create")
-
-        config_interpreter = self.getsupportedinterpreter()
-        args = [sys.executable, '-m', 'virtualenv']
-        if self.envconfig.sitepackages:
-            args.append('--system-site-packages')
-        # add interpreter explicitly, to prevent using
-        # default (virtualenv.ini)
-        args.extend(['--python', str(config_interpreter)])
-        # if sys.platform == "win32":
-        #    f, path, _ = py.std.imp.find_module("virtualenv")
-        #    f.close()
-        #    args[:1] = [str(config_interpreter), str(path)]
-        # else:
-        self.session.make_emptydir(self.path)
-        basepath = self.path.dirpath()
-        basepath.ensure(dir=1)
-        args.append(self.path.basename)
-        self._pcall(args, venv=False, action=action, cwd=basepath)
-        self.just_created = True
-
     def finish(self):
         self._getliveconfig().writeconfig(self.path_config)
 
@@ -239,15 +228,6 @@
             extraopts = ['-U', '--no-deps']
         self._install([sdistpath], extraopts=extraopts, action=action)
 
-    def install_deps(self, action=None):
-        if action is None:
-            action = self.session.newaction(self, "install_deps")
-        deps = self._getresolvedeps()
-        if deps:
-            depinfo = ", ".join(map(str, deps))
-            action.setactivity("installdeps", "%s" % depinfo)
-            self._install(deps, action=action)
-
     def _installopts(self, indexserver):
         l = []
         if indexserver:
@@ -259,7 +239,7 @@
             l.append("--pre")
         return l
 
-    def run_install_command(self, packages, options=(), action=None):
+    def run_install_command(self, packages, action, options=()):
         argv = self.envconfig.install_command[:]
         # use pip-script on win32 to avoid the executable locking
         i = argv.index('{packages}')
@@ -387,3 +367,35 @@
     if not path.check(file=1):
         return "0" * 32
     return path.computehash()
+
+
+@hookimpl
+def tox_testenv_create(venv, action):
+    # if self.getcommandpath("activate").dirpath().check():
+    #    return
+    config_interpreter = venv.getsupportedinterpreter()
+    args = [sys.executable, '-m', 'virtualenv']
+    if venv.envconfig.sitepackages:
+        args.append('--system-site-packages')
+    # add interpreter explicitly, to prevent using
+    # default (virtualenv.ini)
+    args.extend(['--python', str(config_interpreter)])
+    # if sys.platform == "win32":
+    #    f, path, _ = py.std.imp.find_module("virtualenv")
+    #    f.close()
+    #    args[:1] = [str(config_interpreter), str(path)]
+    # else:
+    venv.session.make_emptydir(venv.path)
+    basepath = venv.path.dirpath()
+    basepath.ensure(dir=1)
+    args.append(venv.path.basename)
+    venv._pcall(args, venv=False, action=action, cwd=basepath)
+
+
+@hookimpl
+def tox_testenv_install_deps(venv, action):
+    deps = venv._getresolvedeps()
+    if deps:
+        depinfo = ", ".join(map(str, deps))
+        action.setactivity("installdeps", "%s" % depinfo)
+        venv._install(deps, action=action)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tox-2.2.1/tox.egg-info/PKG-INFO 
new/tox-2.3.0/tox.egg-info/PKG-INFO
--- old/tox-2.2.1/tox.egg-info/PKG-INFO 2015-11-11 15:57:14.000000000 +0100
+++ new/tox-2.3.0/tox.egg-info/PKG-INFO 2015-12-09 13:38:12.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: tox
-Version: 2.2.1
+Version: 2.3.0
 Summary: virtualenv-based automation of test activities
 Home-page: http://tox.testrun.org/
 Author: holger krekel
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tox-2.2.1/tox.egg-info/SOURCES.txt 
new/tox-2.3.0/tox.egg-info/SOURCES.txt
--- old/tox-2.2.1/tox.egg-info/SOURCES.txt      2015-11-11 15:57:14.000000000 
+0100
+++ new/tox-2.3.0/tox.egg-info/SOURCES.txt      2015-12-09 13:38:12.000000000 
+0100
@@ -8,6 +8,7 @@
 setup.py
 tox.ini
 doc/Makefile
+doc/_getdoctarget.py
 doc/changelog.txt
 doc/check_sphinx.py
 doc/conf.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tox-2.2.1/tox.ini new/tox-2.3.0/tox.ini
--- old/tox-2.2.1/tox.ini       2015-11-11 15:57:13.000000000 +0100
+++ new/tox-2.3.0/tox.ini       2015-12-09 13:38:11.000000000 +0100
@@ -1,11 +1,11 @@
 [tox]
-envlist=py27,py26,py34,py33,pypy,flakes,py26-bare
+envlist=py27,py26,py34,py33,py35,pypy,flakes,py26-bare
 
 [testenv:X]
 commands=echo {posargs}
 
 [testenv]
-commands= py.test --timeout=180 {posargs}
+commands= py.test --timeout=180 {posargs:tests}
 
 deps=pytest>=2.3.5
     pytest-timeout


Reply via email to