Repository: tox-dev/tox

Issue:
{env:} macro is not expanded in setenv if the default value contains {envdir}
- Bitbucket: https://bitbucket.org/hpk42/tox/issue/301
- Originally reported by: @booxter
- Originally created at: 2016-01-05T19:28:00.290

The following tox target will not expand {env:} macro with the default value (the substitution for {envdir}):

```
[testenv:dsvm-functional]
setenv = OS_ROOTWRAP_CMD={env:OS_ROOTWRAP_CMD:{envdir}}
commands =
  env
```

```
$ tox -e dsvm-functional
...
OS_ROOTWRAP_CMD={env:OS_ROOTWRAP_CMD:/home/vagrant/git/neutron-vpnaas/.tox/dsvm-functional}
...
```

Once I replace {envdir} with a hardcoded value, it expands {env:} correctly using the default value.

```
[testenv:dsvm-functional]
setenv = OS_ROOTWRAP_CMD={env:OS_ROOTWRAP_CMD:XXX}
commands =
  env
```

```
$ tox -e dsvm-functional
...
OS_ROOTWRAP_CMD=XXX
...
```

Discussion:

Patch:
diff --git a/CONTRIBUTORS b/CONTRIBUTORS
index aa65cccd..84ccf207 100644
--- a/CONTRIBUTORS
+++ b/CONTRIBUTORS
@@ -48,3 +48,4 @@ Selim Belhaouane
 Nick Douma
 Cyril Roelandt
 Bartolome Sanchez Salado
+Laszlo Vasko
diff --git a/doc/config.txt b/doc/config.txt
index 2a7a1465..8880123c 100644
--- a/doc/config.txt
+++ b/doc/config.txt
@@ -435,6 +435,14 @@ then the value will be retrieved as ``os.environ['KEY']``
 and replace with and empty string if the environment variable does not
 exist.
 
+Substitutions can also be nested. In that case they are expanded starting
+from the innermost expression::
+
+    {env:KEY:{env:DEFAULT_OF_KEY}}
+
+the above example is roughly equivalent to
+``os.environ.get('KEY', os.environ['DEFAULT_OF_KEY'])``
+
 .. _`command positional substitution`:
 .. _`positional substitution`:
 
diff --git a/tox/config.py b/tox/config.py
index 33bf14f2..d797951c 100755
--- a/tox/config.py
+++ b/tox/config.py
@@ -1059,8 +1059,20 @@ class Replacer:
         self.reader = reader
         self.crossonly = crossonly
 
-    def do_replace(self, x):
-        return self.RE_ITEM_REF.sub(self._replace_match, x)
+    def do_replace(self, value):
+        '''
+        Recursively expand substitutions starting from the innermost expression
+        '''
+        def substitute_once(x):
+            return self.RE_ITEM_REF.sub(self._replace_match, x)
+
+        expanded = substitute_once(value)
+
+        while expanded != value:  # substitution found
+            value = expanded
+            expanded = substitute_once(value)
+
+        return expanded
 
     def _replace_match(self, match):
         g = match.groupdict()


Test patch:
diff --git a/tests/test_config.py b/tests/test_config.py
index 1f57dcec..9254e992 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -1088,8 +1088,7 @@ class TestConfigTestEnv:
         assert 'FOO' in env
         assert 'BAR' in env
 
-    @pytest.mark.xfail(raises=AssertionError, reason="issue #301")
-    def test_substitution_env_defaults_issue301(tmpdir, newconfig, monkeypatch):
+    def test_substitution_nested_env_defaults_issue301(tmpdir, newconfig, monkeypatch):
         monkeypatch.setenv("IGNORE_STATIC_DEFAULT", "env")
         monkeypatch.setenv("IGNORE_DYNAMIC_DEFAULT", "env")
         config = newconfig("""