Repository: tox-dev/tox (python)

Issue:
2.4.0 breaks environment variable substitution in dependency list
At the moment, I'm using a [tox.ini](https://github.com/rcbops/rpc-openstack/blob/2870f4e2d1c0571f66b0017ed984fe928e460c12/tox.ini) that has a dependency list specified like so:

```ini
deps =
   -rtest-requirements.txt
   ansible{env:ANSIBLE_VERSION:>=1.9.1,1.9.5}
   -ehacking/
```

Prior to tox 2.4.0, this worked fine. Now, however, we see [errors](https://travis-ci.org/rcbops/rpc-openstack/jobs/167071331#L188) from pip because tox is not performing substitution from what I can tell.

```
msg: getenv

cmdargs: ['/home/travis/build/rcbops/rpc-openstack/.tox/flake8/bin/pip', 'install', '-rtest-requirements.txt', 'ansible{env:ANSIBLE_VERSION:>=1.9.1,<1.9.5}', '-e./hacking']

env: {'TRAVIS_PULL_REQUEST_BRANCH': 'd34dh0r53/elk-upgrade', 'rvm_version': '1.26.8 (1.26.8)', 'LC_CTYPE': 'en_US.UTF-8', 'RUBY_VERSION': 'ruby-1.9.3-p551', 'TRAVIS_REPO_SLUG': 'rcbops/rpc-openstack', 'JRUBY_OPTS': '--client -J-XX:+TieredCompilation -J-XX:TieredStopAtLevel=1 -Xcext.enabled=false -J-Xss2m -Xcompile.invokedynamic=false', 'VIRTUAL_ENV': '/home/travis/build/rcbops/rpc-openstack/.tox/flake8', 'SHELL': '/bin/bash', 'TRAVIS_BRANCH': 'master', 'rvm_with_default_gems': 'rake=~>10.2.2 bundler=~>1.6.0', 'ANSIBLE_ROLES_PATH': '/home/travis/.ansible/roles:/home/travis/build/rcbops/rpc-openstack/rcpd/playbooks/roles:/home/travis/build/rcbops/rpc-openstack/openstack-ansible/playbooks/roles', 'NVM_BIN': '/home/travis/.nvm/v0.10.36/bin', 'VIRTUAL_ENV_DISABLE_PROMPT': 'true', 'MANPATH': '/home/travis/.nvm/v0.10.36/share/man:/usr/local/clang-3.4/share/man:/usr/local/man:/usr/local/share/man:/usr/share/man', 'JAVA_HOME': '/usr/lib/jvm/java-7-oracle', '_system_type': 'Linux', 'TRAVIS_SECURE_ENV_VARS': 'false', 'MY_RUBY_HOME': '/home/travis/.rvm/rubies/ruby-1.9.3-p551', 'rvm_max_time_flag': '5', 'ANSIBLE_LIBRARY': '/home/travis/.ansible/roles/plugins/library', 'TRAVIS': 'true', 'PIP_DISABLE_PIP_VERSION_CHECK': '1', '_system_version': '12.04', 'TRAVIS_COMMIT_RANGE': '3af2952ac6132edec9904e27d9744a7d23f4b2b1...e752e4f8e3a74cc09328711a256a0153fc738f2b', 'MAIL': '/var/mail/travis', 'rvm_loaded_flag': '1', 'CONTINUOUS_INTEGRATION': 'true', 'GOROOT': '/home/travis/.gimme/versions/go1.4.1.linux.amd64', 'rvm_path': '/home/travis/.rvm', 'RACK_ENV': 'test', 'USER': 'travis', 'ANSIBLE_FILTER_PLUGINS': '/home/travis/.ansible/roles/plugins/filter', 'NVM_IOJS_ORG_MIRROR': 'https://iojs.org/dist', 'PS4': '+', 'SHLVL': '3', 'TRAVIS_PULL_REQUEST_SHA': 'e752e4f8e3a74cc09328711a256a0153fc738f2b', 'MERB_ENV': 'test', 'GIT_ASKPASS': 'echo', 'GEM_PATH': '/home/travis/.rvm/gems/ruby-1.9.3-p551:/home/travis/.rvm/gems/ruby-1.9.3-p551@global', 'HAS_ANTARES_THREE_LITTLE_FRONZIES_BADGE': 'true', 'TRAVIS_EVENT_TYPE': 'pull_request', '_': '/home/travis/virtualenv/python2.7.9/bin/tox', 'TRAVIS_TAG': '', 'SSH_CONNECTION': '172.17.42.1 46316 172.17.2.94 22', 'rvm_without_gems': 'rubygems-bundler', 'TRAVIS_BUILD_NUMBER': '3698', '_system_name': 'Ubuntu', 'TRAVIS_BUILD_ID': '167071330', 'NVM_NODEJS_ORG_MIRROR': 'https://nodejs.org/dist', 'NVM_DIR': '/home/travis/.nvm', 'HOME': '/home/travis', 'TRAVIS_PULL_REQUEST': '1440', 'LANG': 'en_US.UTF-8', 'TRAVIS_COMMIT': 'aedd1cc29f7b7c13ec43673cd60e884cb6daf899', 'NVM_PATH': '/home/travis/.nvm/v0.10.36/lib/node', 'PYTHONHASHSEED': '567880832', '_system_arch': 'x86_64', 'CI': 'true', 'TOXENV': 'flake8', 'ANSIBLE_LOOKUP_PLUGINS': '/home/travis/.ansible/roles/plugins/lookup', 'rvm_prefix': '/home/travis', 'DEBIAN_FRONTEND': 'noninteractive', 'rvm_user_install_flag': '1', 'IRBRC': '/home/travis/.rvm/rubies/ruby-1.9.3-p551/.irbrc', 'SSH_TTY': '/dev/pts/1', 'GEM_HOME': '/home/travis/.rvm/gems/ruby-1.9.3-p551', 'HAS_JOSH_K_SEAL_OF_APPROVAL': 'true', 'PYTHON_CFLAGS': '-g -fstack-protector --param=ssp-buffer-size=4 -Wformat -Werror=format-security', 'COMPOSER_NO_INTERACTION': '1', 'SSH_CLIENT': '172.17.42.1 46316 22', 'rvm_project_rvmrc': '0', 'LOGNAME': 'travis', 'PATH': '/home/travis/build/rcbops/rpc-openstack/.tox/flake8/bin:/home/travis/virtualenv/python2.7.9/bin:/home/travis/bin:/home/travis/.local/bin:/home/travis/.gimme/versions/go1.4.1.linux.amd64/bin:/opt/python/2.7.9/bin:/opt/python/2.6.9/bin:/opt/python/3.4.2/bin:/opt/python/3.3.5/bin:/opt/python/3.2.5/bin:/opt/python/pypy-2.5.0/bin:/opt/python/pypy3-2.4.0/bin:/usr/local/phantomjs/bin:/home/travis/.nvm/v0.10.36/bin:./node_modules/.bin:/usr/local/maven-3.2.5/bin:/usr/local/clang-3.4/bin:/home/travis/.rvm/gems/ruby-1.9.3-p551/bin:/home/travis/.rvm/gems/ruby-1.9.3-p551@global/bin:/home/travis/.rvm/rubies/ruby-1.9.3-p551/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/travis/.rvm/bin', 'rvm_silence_path_mismatch_check_flag': '1', 'rvm_autoupdate_flag': '0', 'ANSIBLE_ACTION_PLUGINS': '/home/travis/.ansible/roles/plugins/action', 'ANSIBLE_CALLBACK_PLUGINS': '/home/travis/.ansible/roles/plugins/callback', 'TERM': 'xterm', 'NVM_IOJS_ORG_VERSION_LISTING': 'https://iojs.org/dist/index.tab', 'rvm_bin_path': '/home/travis/.rvm/bin', 'RAILS_ENV': 'test', 'TRAVIS_JOB_NUMBER': '3698.1', 'PYTHON_CONFIGURE_OPTS': '--enable-unicode=ucs4 --with-wide-unicode --enable-shared --enable-ipv6 --enable-loadable-sqlite-extensions --with-computed-gotos', 'rvm_stored_umask': '0002', 'LC_ALL': 'en_US.UTF-8', 'TRAVIS_JOB_ID': '167071331', 'TRAVIS_PYTHON_VERSION': '2.7', 'OLDPWD': '/home/travis/build', 'TRAVIS_LANGUAGE': 'python', 'TRAVIS_BUILD_DIR': '/home/travis/build/rcbops/rpc-openstack', 'PWD': '/home/travis/build/rcbops/rpc-openstack', 'TRAVIS_OS_NAME': 'linux'}
```

This appears to be a regression (at least from 2.3.x).

Pull request:
Fix regression in env suupport in dependency list
The fix for allowing parametrized dependencies in
174bf2da31a33deeb1985f3049b6884c21e3a9b4 broke the use case of having a
dependency whose version can be specified in an environment variable or
defaulted to a range, e.g.,

    deps =
       mydep{env:MYDEP_VERSION:>1.0,<2.0}

To fix this, we modify the regular expression to handle some
substitution value cases specifically while also handling the default
value of a env substitution explicitly.

Closes #380

Patch:
diff --git a/tox/config.py b/tox/config.py
index 8d1712f8..c1d894a1 100644
--- a/tox/config.py
+++ b/tox/config.py
@@ -1023,7 +1023,8 @@ class Replacer:
         r'''
         (?<!\\)[{]
         (?:(?P<sub_type>[^[:{}]+):)?    # optional sub_type for special rules
-        (?P<substitution_value>[^,{}]*)  # substitution key
+        (?P<substitution_value>(?:\[[^,{}]*\])?[^:,{}]*)  # substitution key
+        (?::(?P<default_value>[^{}]*))?   # default value
         [}]
         ''', re.VERBOSE)
 
@@ -1064,18 +1065,12 @@ class Replacer:
         return self._replace_substitution(match)
 
     def _replace_env(self, match):
-        match_value = match.group('substitution_value')
-        if not match_value:
+        envkey = match.group('substitution_value')
+        if not envkey:
             raise tox.exception.ConfigError(
                 'env: requires an environment variable name')
 
-        default = None
-        envkey_split = match_value.split(':', 1)
-
-        if len(envkey_split) is 2:
-            envkey, default = envkey_split
-        else:
-            envkey = match_value
+        default = match.group('default_value')
 
         envvalue = self.reader.get_environ_value(envkey)
         if envvalue is None:


Test patch:
diff --git a/tests/test_config.py b/tests/test_config.py
index 2b3352ca..9c10c387 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -1177,13 +1177,14 @@ class TestConfigTestEnv:
             deps=
                 {{[testenv]deps}}
                 fun
+                frob{{env:ENV_VAR:>1.0,<2.0}}
         """.format(
             envlist=','.join(envlist),
             deps='\n' + '\n'.join([' ' * 17 + d for d in deps])
         )
         conf = newconfig([], inisource).envconfigs['py27']
         packages = [dep.name for dep in conf.deps]
-        assert packages == list(deps) + ['fun']
+        assert packages == list(deps) + ['fun', 'frob>1.0,<2.0']
 
     def test_take_dependencies_from_other_section(self, newconfig):
         inisource = """