Repository: tox-dev/tox

Issue:
tox4: pymssql InterpolationSyntaxError
There is a problem when the new tox parses tox.ini file from pymssql project: https://github.com/pymssql/pymssql/blob/master/tox.ini

The parsing raises:
```
configparser.InterpolationSyntaxError: '%' must be followed by '%' or '(', found: '%r" % pymssql.__version__)\'\npython -c \'from pymssql import _mssql; print(_mssql); print("_mssql.__version__ = %r" % _mssql.__version__)\'\npy.test {posargs:-v {toxinidir}/tests/}'
```
But tox 3 has no problem with that:
```
$ tox --version
3.24.5 imported from /usr/lib/python3.10/site-packages/tox/__init__.py
$ tox -e py310
GLOB sdist-make: /home/lbalhar/Software/pymssql/setup.py
py310 inst-nodeps: /home/lbalhar/Software/pymssql/.tox/.tmp/package/1/pymssql-2.2.4.zip
py310 installed: asttokens==2.0.5,attrs==21.4.0,backcall==0.2.0,black==22.1.0,click==8.0.3,Cython==0.29.27,decorator==5.1.1,executing==0.8.2,greenlet==1.1.2,iniconfig==1.1.1,ipdb==0.13.9,ipython==8.0.1,jedi==0.18.1,matplotlib-inline==0.1.3,mypy-extensions==0.4.3,packaging==21.3,parso==0.8.3,pathspec==0.9.0,pexpect==4.8.0,pickleshare==0.7.5,platformdirs==2.4.1,pluggy==1.0.0,prompt-toolkit==3.0.27,ptyprocess==0.7.0,pure-eval==0.2.2,py==1.11.0,Pygments==2.11.2,pymssql @ file:///home/lbalhar/Software/pymssql/.tox/.tmp/package/1/pymssql-2.2.4.zip,pyparsing==3.0.7,pytest==7.0.0,six==1.16.0,SQLAlchemy==1.4.31,stack-data==0.1.4,toml==0.10.2,tomli==2.0.1,traitlets==5.1.1,wcwidth==0.2.5
py310 run-test-pre: PYTHONHASHSEED='3467337242'
py310 run-test: commands[0] | python -c 'import pymssql; print(pymssql); print("pymssql.__version__ = %r" % pymssql.__version__)'
<module 'pymssql' from '/home/lbalhar/Software/pymssql/.tox/py310/lib64/python3.10/site-packages/pymssql/__init__.py'>
pymssql.__version__ = '2.2.4'
py310 run-test: commands[1] | python -c 'from pymssql import _mssql; print(_mssql); print("_mssql.__version__ = %r" % _mssql.__version__)'
<module 'pymssql._mssql' from '/home/lbalhar/Software/pymssql/.tox/py310/lib64/python3.10/site-packages/pymssql/_mssql.cpython-310-x86_64-linux-gnu.so'>
_mssql.__version__ = '2.2.4'
…
```
I honestly don't know whether this is an issue here or an unsupported config file there.

Discussion:

Patch:
diff --git a/docs/changelog/2350.bugfix.rst b/docs/changelog/2350.bugfix.rst
new file mode 100644
index 00000000..4d42dee6
--- /dev/null
+++ b/docs/changelog/2350.bugfix.rst
@@ -0,0 +1,1 @@
+Do not interpolate values when parsing ``tox.ini`` configuration files - by :user:`gaborbernat`.
diff --git a/src/tox/config/cli/ini.py b/src/tox/config/cli/ini.py
index f832b116..8839f430 100644
--- a/src/tox/config/cli/ini.py
+++ b/src/tox/config/cli/ini.py
@@ -34,7 +34,7 @@ class IniConfig:
             self.config_file = self.config_file.absolute()
             try:
 
-                parser = ConfigParser()
+                parser = ConfigParser(interpolation=None)
                 with self.config_file.open() as file_handler:
                     parser.read_file(file_handler)
                 self.has_tox_section = parser.has_section(CORE.key)
diff --git a/src/tox/config/source/ini.py b/src/tox/config/source/ini.py
index c5ea7299..eea006d9 100644
--- a/src/tox/config/source/ini.py
+++ b/src/tox/config/source/ini.py
@@ -24,7 +24,7 @@ class IniSource(Source, ABC):
 
     def __init__(self, path: Path, content: str | None = None) -> None:
         super().__init__(path)
-        self._parser = ConfigParser()
+        self._parser = ConfigParser(interpolation=None)
         if content is None:
             if not path.exists():
                 raise ValueError


Test patch:
diff --git a/tests/config/cli/test_cli_ini.py b/tests/config/cli/test_cli_ini.py
index f97927d4..efe3776d 100644
--- a/tests/config/cli/test_cli_ini.py
+++ b/tests/config/cli/test_cli_ini.py
@@ -9,6 +9,7 @@ from typing import Any, Callable
 import pytest
 from pytest_mock import MockerFixture
 
+from tox.config.cli.ini import IniConfig
 from tox.config.cli.parse import get_options
 from tox.config.loader.api import Override
 from tox.pytest import CaptureFixture, LogCaptureFixture, MonkeyPatch
@@ -190,3 +191,11 @@ def test_bad_option_cli_ini(
         ),
     ]
     assert vars(parsed) == default_options
+
+
+def test_cli_ini_with_interpolated(tmp_path: Path, monkeypatch: MonkeyPatch) -> None:
+    to = tmp_path / "tox.ini"
+    to.write_text("[tox]\na = %(b)s")
+    monkeypatch.setenv("TOX_CONFIG_FILE", str(to))
+    conf = IniConfig()
+    assert conf.get("a", str)
diff --git a/tests/config/loader/ini/conftest.py b/tests/config/loader/ini/conftest.py
index 53c6dfd2..52e066e7 100644
--- a/tests/config/loader/ini/conftest.py
+++ b/tests/config/loader/ini/conftest.py
@@ -12,7 +12,7 @@ def mk_ini_conf(tmp_path: Path) -> Callable[[str], ConfigParser]:
     def _func(raw: str) -> ConfigParser:
         filename = tmp_path / "demo.ini"
         filename.write_bytes(raw.encode("utf-8"))  # win32: avoid CR normalization - what you pass is what you get
-        parser = ConfigParser()
+        parser = ConfigParser(interpolation=None)
         with filename.open() as file_handler:
             parser.read_file(file_handler)
         return parser
diff --git a/tests/config/source/test_source_ini.py b/tests/config/source/test_source_ini.py
new file mode 100644
index 00000000..dcf727d7
--- /dev/null
+++ b/tests/config/source/test_source_ini.py
@@ -0,0 +1,10 @@
+from pathlib import Path
+
+from tox.config.loader.section import Section
+from tox.config.source.ini import IniSource
+
+
+def test_source_ini_with_interpolated(tmp_path: Path) -> None:
+    loader = IniSource(tmp_path, content="[tox]\na = %(c)s").get_loader(Section(None, "tox"), {})
+    assert loader is not None
+    loader.load_raw("a", None, None)
diff --git a/tests/session/cmd/test_show_config.py b/tests/session/cmd/test_show_config.py
index 55e4acb4..4becedb5 100644
--- a/tests/session/cmd/test_show_config.py
+++ b/tests/session/cmd/test_show_config.py
@@ -24,7 +24,7 @@ def test_show_config_default_run_env(tox_project: ToxProjectCreator, monkeypatch
     outcome = list(state.envs.iter(only_active=False))
     assert outcome == [name]
     monkeypatch.delenv("TERM", raising=False)  # disable conditionally set flag
-    parser = ConfigParser()
+    parser = ConfigParser(interpolation=None)
     parser.read_string(result.out)
     assert list(parser.sections()) == [f"testenv:{name}", "tox"]
 
@@ -122,7 +122,7 @@ def test_show_config_pkg_env_once(
     )
     result = project.run("c")
     result.assert_success()
-    parser = ConfigParser()
+    parser = ConfigParser(interpolation=None)
     parser.read_string(result.out)
     sections = set(parser.sections())
     assert sections == {"testenv:.pkg", f"testenv:.pkg-{impl}{prev_ver}", f"testenv:py{prev_ver}", "testenv:py", "tox"}
@@ -138,7 +138,7 @@ def test_show_config_pkg_env_skip(
     )
     result = project.run("c")
     result.assert_success()
-    parser = ConfigParser()
+    parser = ConfigParser(interpolation=None)
     parser.read_string(result.out)
     sections = set(parser.sections())
     assert sections == {"testenv:.pkg", "tox", "testenv:py", f"testenv:py{prev_ver}"}
@@ -148,7 +148,7 @@ def test_show_config_select_only(tox_project: ToxProjectCreator) -> None:
     project = tox_project({"tox.ini": "[tox]\nenv_list=\n a\n b", "pyproject.toml": ""})
     result = project.run("c", "-e", ".pkg,b,.pkg")
     result.assert_success()
-    parser = ConfigParser()
+    parser = ConfigParser(interpolation=None)
     parser.read_string(result.out)
     sections = list(parser.sections())
     assert sections == ["testenv:.pkg", "testenv:b"]