Repository: tox-dev/tox

Issue:
TOX_CONFIG_FILE overrides local config
## Issue

TOX_CONFIG_FILE is documented to be a way to specify a user-level configuration file, to set defaults to be used. There is a default for the configuration file as well, but the behavior differs when the file is automatically discovered vs. being explicitly specified.

In the examples below, the `A` and `B` outputs are correct, but the `C` output is not, even though TOX_CONFIG_FILE is set to point to the same file that is automatically used when it is not set.

## Environment

Provide at least:

- OS: Debian Bookworm
- tox version: 4.3.5 installed using `pipx`

## Minimal example

```shell
$ mkdir foo
$ cd foo
$ cat > tox.ini << EOF
[tox]

[testenv:foo]

[testenv:bar]

[testenv:baz]
EOF

$ tox list (A)
default environments:
py  -> [no description]

additional environments:
foo -> [no description]
bar -> [no description]
baz -> [no description]

$ mkdir $HOME/.config/tox
$ cat > $HOME/.config/tox/config.ini << EOF
[tox]
workdir=/tmp
EOF

$ tox list (B)
default environments:
py  -> [no description]

additional environments:
foo -> [no description]
bar -> [no description]
baz -> [no description]

$ export TOX_CONFIG_FILE=$HOME/.config/tox/config.ini

$ tox list (C)
default environments:
py  -> [no description]
```


Discussion:

Patch:
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 5b99534c..12405ee6 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -4,6 +4,54 @@ Release History
 
 .. towncrier release notes start
 
+v4.4.1 (2023-01-25)
+-------------------
+
+Bugfixes - 4.4.1
+~~~~~~~~~~~~~~~~
+- In tox 4.4.0 ``constrain_package_deps`` was introduced with a default value of ``True``. This has been changed back to
+  ``False``, which restores the original behavior of tox 4.3.5 - by :user:`masenf`. (:issue:`2897`)
+
+
+v4.4.0 (2023-01-25)
+-------------------
+
+Features - 4.4.0
+~~~~~~~~~~~~~~~~
+- Test environments now recognize boolean config keys ``constrain_package_deps`` (default=true) and ``use_frozen_constraints`` (default=false),
+  which control how tox generates and applies constraints files when performing ``install_package_deps``.
+
+  If ``constrain_package_deps`` is true (default), then tox will write out ``{env_dir}{/}constraints.txt`` and pass it to
+  ``pip`` during ``install_package_deps``. If ``use_frozen_constraints`` is false (default), the constraints will be taken
+  from the specifications listed under ``deps`` (and inside any requirements or constraints file referenced in ``deps``).
+  Otherwise, ``list_dependencies_command`` (``pip freeze``) is used to enumerate exact package specifications which will
+  be written to the constraints file.
+
+  In previous releases, conflicting package dependencies would silently override the ``deps`` named in the configuration,
+  resulting in test runs against unexpected dependency versions, particularly when using tox factors to explicitly test
+  with different versions of dependencies - by :user:`masenf`. (:issue:`2386`)
+
+Bugfixes - 4.4.0
+~~~~~~~~~~~~~~~~
+- When parsing command lines, use ``shlex(..., posix=True)``, even on windows platforms, since non-POSIX mode does not
+  handle escape characters and quoting like a shell would. This improves cross-platform configurations without hacks or
+  esoteric quoting.
+
+  To make this transition easier, on Windows, the backslash path separator will not treated as an escape character unless
+  it preceeds a quote, whitespace, or another backslash chracter. This allows paths to mostly be written in single or
+  double backslash style.
+
+  Note that **double-backslash will no longer be escaped to a single backslash in substitutions**, instead the double
+  backslash will be consumed as part of command splitting, on either posix or windows platforms.
+
+  In some instances superfluous double or single quote characters may be stripped from arg arrays in ways that do not
+  occur in the default windows ``cmd.exe`` shell - by :user:`masenf`. (:issue:`2635`)
+
+Improved Documentation - 4.4.0
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+- Add infromation when command from ``list_dependencies_command`` configuration option is used. (:issue:`2883`)
+
+
 v4.3.5 (2023-01-18)
 -------------------
 
diff --git a/docs/changelog/2883.doc.rst b/docs/changelog/2883.doc.rst
deleted file mode 100644
index 8472f903..00000000
--- a/docs/changelog/2883.doc.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Add infromation when command from ``list_dependencies_command`` configuration option is used.
diff --git a/docs/changelog/2890.bugfix.rst b/docs/changelog/2890.bugfix.rst
new file mode 100644
index 00000000..248b36af
--- /dev/null
+++ b/docs/changelog/2890.bugfix.rst
@@ -0,0 +1,4 @@
+Allow the user configuration file (default ``<appdir>/tox/config.ini``) to be overridden via the
+``TOX_USER_CONFIG_FILE`` environment variable. Previously tox was looking at the ``TOX_CONFIG_FILE`` to override the
+user configuration, however that environment variable is already used to override the main configuration - by
+:user:`masenf`.
diff --git a/docs/config.rst b/docs/config.rst
index e80da066..adc7863f 100644
--- a/docs/config.rst
+++ b/docs/config.rst
@@ -431,6 +431,16 @@ Run
       that outside of it. Therefore ``python`` translates as the virtual environments ``python`` (having the same
       runtime version as the :ref:`base_python`), and ``pip`` translates as the virtual environments ``pip``.
 
+   .. note::
+
+       ``shlex`` POSIX-mode quoting rules are used to split the command line into arguments on all
+       supported platforms as of tox 4.4.0.
+
+       The backslash ``\`` character can be used to escape quotes, whitespace, itself, and
+       other characters (except on Windows, where a backslash in a path will not be interpreted as an escape).
+       Unescaped single quote will disable the backslash escape until closed by another unescaped single quote.
+       For more details, please see :doc:`shlex parsing rules <python:library/shlex>`.
+
    .. note::
 
      Inline scripts can be used, however note these are discovered from the project root directory, and is not
@@ -755,12 +765,31 @@ Pip installer
    latest available pre-release of any dependencies without a specified version. If ``false``, pip will only install
    final releases of unpinned dependencies.
 
+.. conf::
+   :keys: constrain_package_deps
+   :default: true
+   :version_added: 4.4.0
+
+   If ``constrain_package_deps`` is true, then tox will create and use ``{env_dir}{/}constraints.txt`` when installing
+   package dependnecies during ``install_package_deps`` stage. When this value is set to false, any conflicting package
+   dependencies will override explicit dependencies and constraints passed to ``deps``.
+
+.. conf::
+   :keys: use_frozen_constraints
+   :default: false
+   :version_added: 4.4.0
+
+   When ``use_frozen_constraints`` is true, then tox will use the ``list_dependencies_command`` to enumerate package
+   versions in order to create ``{env_dir}{/}constraints.txt``. Otherwise the package specifications explicitly listed under
+   ``deps`` (or in requirements / constraints files referenced in ``deps``) will be used as the constraints. If
+   ``constrain_package_deps`` is false, then this setting has no effect.
+
 User configuration
 ------------------
 
 tox allows creation of user level config-file to modify default values of the CLI commands.
 It is located in the OS-specific user config directory under ``tox/config.ini`` path, see ``tox --help`` output for exact location.
-It can be changed via ``TOX_CONFIG_FILE`` environment variable.
+It can be changed via ``TOX_USER_CONFIG_FILE`` environment variable.
 Example configuration:
 
 .. code-block:: ini
@@ -776,7 +805,7 @@ through the ``{...}`` string-substitution pattern.
 
 The string inside the curly braces may reference a global or per-environment config key as described above.
 
-The backslash character ``\`` will act as an escape for a following: ``\``,
+In substitutions, the backslash character ``\`` will act as an escape when preceeding
 ``{``, ``}``, ``:``, ``[``, or ``]``, otherwise the backslash will be
 reproduced literally::
 
@@ -784,6 +813,10 @@ reproduced literally::
         python -c 'print("\{posargs} = \{}".format("{posargs}"))'
         python -c 'print("host: \{}".format("{env:HOSTNAME:host\: not set}")'
 
+Note that any backslashes remaining after substitution may be processed by ``shlex`` during command parsing. On POSIX
+platforms, the backslash will escape any following character; on windows, the backslash will escape any following quote,
+whitespace, or backslash character (since it normally acts as a path delimiter).
+
 Special substitutions that accept additional colon-delimited ``:`` parameters
 cannot have a space after the ``:`` at the beginning of line (e.g.  ``{posargs:
 magic}`` would be parsed as factorial ``{posargs``, having value magic).
diff --git a/docs/faq.rst b/docs/faq.rst
index aa47ff0c..9c948d8c 100644
--- a/docs/faq.rst
+++ b/docs/faq.rst
@@ -124,21 +124,16 @@ install. While creating a test environment tox will invoke pip multiple times, i
    1. install the dependencies of the package.
    2. install the package itself.
 
-Some solutions and their drawbacks:
-
-- specify the constraint files within :ref:`deps` (these constraints will not be applied when installing package
-  dependencies),
-- use ``PIP_CONSTRAINT`` inside :ref:`set_env` (tox will not know about the content of the constraint file and such
-  will not trigger a rebuild of the environment when its content changes),
-- specify the constraint file by extending the :ref:`install_command` as in the following example
-  (tox will not know about the content of the constraint file and such will not trigger a rebuild of the environment
-  when its content changes).
-
-.. code-block:: ini
-
-    [testenv:py39]
-    install_command = python -m pip install {opts} {packages} -c constraints.txt
-    extras = test
+Starting in tox 4.4.0, ``{env_dir}{/}constraints.txt`` is generated by default during ``install_deps`` based on the
+package specifications listed under ``deps``. These constraints are subsequently passed to pip during the
+``install_package_deps`` stage, causing an error to be raised when the package dependencies conflict with the test
+environment dependencies. For stronger guarantees, set ``use_frozen_constraints = true`` in the test environment to
+generate the constraints file based on the exact versions enumerated by the ``list_dependencies_command`` (``pip
+freeze``).  When using frozen constraints, if the package deps are incompatible with any previously installed
+dependency, an error will be raised.
+
+Ensure that ``constrain_package_deps = true`` is set in the test environment in order to use the constraints file
+generated by processing the ``deps`` section when performing ``package_deps``.
 
 Note constraint files are a subset of requirement files. Therefore, it's valid to pass a constraint file wherever you
 can specify a requirement file.
diff --git a/src/tox/config/cli/ini.py b/src/tox/config/cli/ini.py
index 8839f430..c1823a1d 100644
--- a/src/tox/config/cli/ini.py
+++ b/src/tox/config/cli/ini.py
@@ -19,7 +19,7 @@ DEFAULT_CONFIG_FILE = Path(user_config_dir("tox")) / "config.ini"
 
 
 class IniConfig:
-    TOX_CONFIG_FILE_ENV_VAR = "TOX_CONFIG_FILE"
+    TOX_CONFIG_FILE_ENV_VAR = "TOX_USER_CONFIG_FILE"
     STATE = {None: "failed to parse", True: "active", False: "missing"}
 
     def __init__(self) -> None:
diff --git a/src/tox/config/loader/ini/replace.py b/src/tox/config/loader/ini/replace.py
index 40730ca5..41672780 100644
--- a/src/tox/config/loader/ini/replace.py
+++ b/src/tox/config/loader/ini/replace.py
@@ -30,7 +30,7 @@ LOGGER = logging.getLogger(__name__)
 ARG_DELIMITER = ":"
 REPLACE_START = "{"
 REPLACE_END = "}"
-BACKSLASH_ESCAPE_CHARS = ["\\", ARG_DELIMITER, REPLACE_START, REPLACE_END, "[", "]"]
+BACKSLASH_ESCAPE_CHARS = [ARG_DELIMITER, REPLACE_START, REPLACE_END, "[", "]"]
 MAX_REPLACE_DEPTH = 100
 
 
@@ -115,11 +115,18 @@ class MatchExpression:
         pos = 0
 
         while pos < len(value):
-            if len(value) > pos + 1 and value[pos] == "\\" and value[pos + 1] in BACKSLASH_ESCAPE_CHARS:
-                # backslash escapes the next character from a special set
-                last_arg.append(value[pos + 1])
-                pos += 2
-                continue
+            if len(value) > pos + 1 and value[pos] == "\\":
+                if value[pos + 1] in BACKSLASH_ESCAPE_CHARS:
+                    # backslash escapes the next character from a special set
+                    last_arg.append(value[pos + 1])
+                    pos += 2
+                    continue
+                if value[pos + 1] == "\\":
+                    # backlash doesn't escape a backslash, but does prevent it from affecting the next char
+                    # a subsequent `shlex` pass will eat the double backslash during command splitting
+                    last_arg.append(value[pos : pos + 2])
+                    pos += 2
+                    continue
             fragment = value[pos:]
             if terminator and fragment.startswith(terminator):
                 pos += len(terminator)
diff --git a/src/tox/config/loader/str_convert.py b/src/tox/config/loader/str_convert.py
index f07545f5..ad7ffa5d 100644
--- a/src/tox/config/loader/str_convert.py
+++ b/src/tox/config/loader/str_convert.py
@@ -45,11 +45,43 @@ class StrConvert(Convert[str]):
                 else:
                     raise TypeError(f"dictionary lines must be of form key=value, found {row!r}")
 
+    @staticmethod
+    def _win32_process_path_backslash(value: str, escape: str, special_chars: str) -> str:
+        """
+        Escape backslash in value that is not followed by a special character.
+
+        This allows windows paths to be written without double backslash, while
+        retaining the POSIX backslash escape semantics for quotes and escapes.
+        """
+        result = []
+        for ix, char in enumerate(value):
+            result.append(char)
+            if char == escape:
+                last_char = value[ix - 1 : ix]
+                if last_char == escape:
+                    continue
+                next_char = value[ix + 1 : ix + 2]
+                if next_char not in (escape, *special_chars):
+                    result.append(escape)  # escape escapes that are not themselves escaping a special character
+        return "".join(result)
+
     @staticmethod
     def to_command(value: str) -> Command:
-        is_win = sys.platform == "win32"
+        """
+        At this point, ``value`` has already been substituted out, and all punctuation / escapes are final.
+
+        Value will typically be stripped of whitespace when coming from an ini file.
+        """
         value = value.replace(r"\#", "#")
-        splitter = shlex.shlex(value, posix=not is_win)
+        is_win = sys.platform == "win32"
+        if is_win:  # pragma: win32 cover
+            s = shlex.shlex(posix=True)
+            value = StrConvert._win32_process_path_backslash(
+                value,
+                escape=s.escape,
+                special_chars=s.quotes + s.whitespace,
+            )
+        splitter = shlex.shlex(value, posix=True)
         splitter.whitespace_split = True
         splitter.commenters = ""  # comments handled earlier, and the shlex does not know escaped comment characters
         args: list[str] = []
diff --git a/src/tox/tox_env/python/pip/pip_install.py b/src/tox/tox_env/python/pip/pip_install.py
index 2136e862..55f95f65 100644
--- a/src/tox/tox_env/python/pip/pip_install.py
+++ b/src/tox/tox_env/python/pip/pip_install.py
@@ -2,6 +2,7 @@ from __future__ import annotations
 
 import logging
 from collections import defaultdict
+from pathlib import Path
 from typing import Any, Callable, Sequence
 
 from packaging.requirements import Requirement
@@ -38,6 +39,18 @@ class Pip(Installer[Python]):
             post_process=self.post_process_install_command,
             desc="command used to install packages",
         )
+        self._env.conf.add_config(
+            keys=["constrain_package_deps"],
+            of_type=bool,
+            default=False,
+            desc="If true, apply constraints during install_package_deps.",
+        )
+        self._env.conf.add_config(
+            keys=["use_frozen_constraints"],
+            of_type=bool,
+            default=False,
+            desc="Use the exact versions of installed deps as constraints, otherwise use the listed deps.",
+        )
         if self._with_list_deps:  # pragma: no branch
             self._env.conf.add_config(
                 keys=["list_dependencies_command"],
@@ -81,6 +94,17 @@ class Pip(Installer[Python]):
             logging.warning(f"pip cannot install {arguments!r}")
             raise SystemExit(1)
 
+    def constraints_file(self) -> Path:
+        return Path(self._env.env_dir) / "constraints.txt"
+
+    @property
+    def constrain_package_deps(self) -> bool:
+        return bool(self._env.conf["constrain_package_deps"])
+
+    @property
+    def use_frozen_constraints(self) -> bool:
+        return bool(self._env.conf["use_frozen_constraints"])
+
     def _install_requirement_file(self, arguments: PythonDeps, section: str, of_type: str) -> None:
         try:
             new_options, new_reqs = arguments.unroll()
@@ -90,7 +114,16 @@ class Pip(Installer[Python]):
         new_constraints: list[str] = []
         for req in new_reqs:
             (new_constraints if req.startswith("-c ") else new_requirements).append(req)
-        new = {"options": new_options, "requirements": new_requirements, "constraints": new_constraints}
+        constraint_options = {
+            "constrain_package_deps": self.constrain_package_deps,
+            "use_frozen_constraints": self.use_frozen_constraints,
+        }
+        new = {
+            "options": new_options,
+            "requirements": new_requirements,
+            "constraints": new_constraints,
+            "constraint_options": constraint_options,
+        }
         # if option or constraint change in any way recreate, if the requirements change only if some are removed
         with self._env.cache.compare(new, section, of_type) as (eq, old):
             if not eq:  # pragma: no branch
@@ -100,9 +133,16 @@ class Pip(Installer[Python]):
                     missing_requirement = set(old["requirements"]) - set(new_requirements)
                     if missing_requirement:
                         raise Recreate(f"requirements removed: {' '.join(missing_requirement)}")
+                    old_constraint_options = old.get("constraint_options")
+                    if old_constraint_options != constraint_options:
+                        msg = f"constraint options changed: old={old_constraint_options} new={constraint_options}"
+                        raise Recreate(msg)
                 args = arguments.as_root_args
                 if args:  # pragma: no branch
                     self._execute_installer(args, of_type)
+                    if self.constrain_package_deps and not self.use_frozen_constraints:
+                        combined_constraints = new_requirements + [c.lstrip("-c ") for c in new_constraints]
+                        self.constraints_file().write_text("\n".join(combined_constraints))
 
     @staticmethod
     def _recreate_if_diff(of_type: str, new_opts: list[str], old_opts: list[str], fmt: Callable[[str], str]) -> None:
@@ -155,10 +195,19 @@ class Pip(Installer[Python]):
             self._execute_installer(install_args, of_type)
 
     def _execute_installer(self, deps: Sequence[Any], of_type: str) -> None:
+        if of_type == "package_deps" and self.constrain_package_deps:
+            constraints_file = self.constraints_file()
+            if constraints_file.exists():
+                deps = [*deps, f"-c{constraints_file}"]
+
         cmd = self.build_install_cmd(deps)
         outcome = self._env.execute(cmd, stdin=StdinSource.OFF, run_id=f"install_{of_type}")
         outcome.assert_success()
 
+        if of_type == "deps" and self.constrain_package_deps and self.use_frozen_constraints:
+            # freeze installed deps for use as constraints
+            self.constraints_file().write_text("\n".join(self.installed()))
+
     def build_install_cmd(self, args: Sequence[str]) -> list[str]:
         try:
             cmd: Command = self._env.conf["install_command"]


Test patch:
diff --git a/src/tox/pytest.py b/src/tox/pytest.py
index ae211252..fb83721c 100644
--- a/src/tox/pytest.py
+++ b/src/tox/pytest.py
@@ -525,6 +525,7 @@ __all__ = (
     "LogCaptureFixture",
     "TempPathFactory",
     "MonkeyPatch",
+    "SubRequest",
     "ToxRunOutcome",
     "ToxProject",
     "ToxProjectCreator",
diff --git a/tests/config/cli/test_cli_ini.py b/tests/config/cli/test_cli_ini.py
index 1022312a..c7324e1a 100644
--- a/tests/config/cli/test_cli_ini.py
+++ b/tests/config/cli/test_cli_ini.py
@@ -44,7 +44,7 @@ def exhaustive_ini(tmp_path: Path, monkeypatch: MonkeyPatch) -> Path:
         """,
         ),
     )
-    monkeypatch.setenv("TOX_CONFIG_FILE", str(to))
+    monkeypatch.setenv("TOX_USER_CONFIG_FILE", str(to))
     return to
 
 
@@ -58,7 +58,7 @@ def test_ini_empty(
     content: str,
 ) -> None:
     to = tmp_path / "tox.ini"
-    monkeypatch.setenv("TOX_CONFIG_FILE", str(to))
+    monkeypatch.setenv("TOX_USER_CONFIG_FILE", str(to))
     to.write_text(content)
     mocker.patch("tox.config.cli.parse.discover_source", return_value=mocker.MagicMock(path=Path()))
     options = get_options("r")
@@ -73,7 +73,7 @@ def test_ini_empty(
 
 
 @pytest.fixture()
-def default_options(tmp_path: Path) -> dict[str, Any]:
+def default_options() -> dict[str, Any]:
     return {
         "colored": "no",
         "command": "r",
@@ -97,7 +97,7 @@ def default_options(tmp_path: Path) -> dict[str, Any]:
         "verbose": 2,
         "work_dir": None,
         "root_dir": None,
-        "config_file": (tmp_path / "tox.ini").absolute(),
+        "config_file": None,
         "factors": [],
         "labels": [],
         "exit_and_dump_after": 0,
@@ -105,7 +105,8 @@ def default_options(tmp_path: Path) -> dict[str, Any]:
     }
 
 
-def test_ini_exhaustive_parallel_values(exhaustive_ini: Path, core_handlers: dict[str, Callable[[State], int]]) -> None:
+@pytest.mark.usefixtures("exhaustive_ini")
+def test_ini_exhaustive_parallel_values(core_handlers: dict[str, Callable[[State], int]]) -> None:
     options = get_options("p")
     assert vars(options.parsed) == {
         "colored": "yes",
@@ -133,7 +134,7 @@ def test_ini_exhaustive_parallel_values(exhaustive_ini: Path, core_handlers: dic
         "verbose": 5,
         "work_dir": None,
         "root_dir": None,
-        "config_file": exhaustive_ini,
+        "config_file": None,
         "factors": [],
         "labels": [],
         "exit_and_dump_after": 0,
@@ -149,7 +150,7 @@ def test_ini_help(exhaustive_ini: Path, capsys: CaptureFixture) -> None:
     assert context.value.code == 0
     out, err = capsys.readouterr()
     assert not err
-    assert f"config file '{exhaustive_ini}' active (changed via env var TOX_CONFIG_FILE)"
+    assert f"config file '{exhaustive_ini}' active (changed via env var TOX_USER_CONFIG_FILE)"
 
 
 def test_bad_cli_ini(
@@ -161,7 +162,7 @@ def test_bad_cli_ini(
 ) -> None:
     mocker.patch("tox.config.cli.parse.discover_source", return_value=mocker.MagicMock(path=Path()))
     caplog.set_level(logging.WARNING)
-    monkeypatch.setenv("TOX_CONFIG_FILE", str(tmp_path))
+    monkeypatch.setenv("TOX_USER_CONFIG_FILE", str(tmp_path))
     options = get_options("r")
     msg = (
         "PermissionError(13, 'Permission denied')"
@@ -169,7 +170,6 @@ def test_bad_cli_ini(
         else "IsADirectoryError(21, 'Is a directory')"
     )
     assert caplog.messages == [f"failed to read config file {tmp_path} because {msg}"]
-    default_options["config_file"] = tmp_path
     assert vars(options.parsed) == default_options
 
 
@@ -191,7 +191,7 @@ def test_bad_option_cli_ini(
         """,
         ),
     )
-    monkeypatch.setenv("TOX_CONFIG_FILE", str(to))
+    monkeypatch.setenv("TOX_USER_CONFIG_FILE", str(to))
     parsed, _, __, ___, ____ = get_options("r")
     assert caplog.messages == [
         "{} key verbose as type <class 'int'> failed with {}".format(
@@ -205,7 +205,7 @@ def test_bad_option_cli_ini(
 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))
+    monkeypatch.setenv("TOX_USER_CONFIG_FILE", str(to))
     conf = IniConfig()
     assert conf.get("a", str)
 
diff --git a/tests/config/loader/ini/replace/test_replace_env_var.py b/tests/config/loader/ini/replace/test_replace_env_var.py
index 54438b0d..c5aca717 100644
--- a/tests/config/loader/ini/replace/test_replace_env_var.py
+++ b/tests/config/loader/ini/replace/test_replace_env_var.py
@@ -17,24 +17,24 @@ def test_replace_env_set(replace_one: ReplaceOne, monkeypatch: MonkeyPatch) -> N
 
 
 def test_replace_env_set_double_bs(replace_one: ReplaceOne, monkeypatch: MonkeyPatch) -> None:
-    """Double backslash should escape to single backslash and not affect surrounding replacements."""
+    """Double backslash should remain but not affect surrounding replacements."""
     monkeypatch.setenv("MAGIC", "something good")
     result = replace_one(r"{env:MAGIC}\\{env:MAGIC}")
-    assert result == r"something good\something good"
+    assert result == r"something good\\something good"
 
 
 def test_replace_env_set_triple_bs(replace_one: ReplaceOne, monkeypatch: MonkeyPatch) -> None:
-    """Triple backslash should escape to single backslash also escape subsequent replacement."""
+    """Triple backslash should retain two slashes with the third escaping subsequent replacement."""
     monkeypatch.setenv("MAGIC", "something good")
     result = replace_one(r"{env:MAGIC}\\\{env:MAGIC}")
-    assert result == r"something good\{env:MAGIC}"
+    assert result == r"something good\\{env:MAGIC}"
 
 
 def test_replace_env_set_quad_bs(replace_one: ReplaceOne, monkeypatch: MonkeyPatch) -> None:
-    """Quad backslash should escape to two backslashes and not affect surrounding replacements."""
+    """Quad backslash should remain but not affect surrounding replacements."""
     monkeypatch.setenv("MAGIC", "something good")
-    result = replace_one(r"\\{env:MAGIC}\\\\{env:MAGIC}\\")
-    assert result == r"\something good\\something good" + "\\"
+    result = replace_one(r"\\{env:MAGIC}\\\\{env:MAGIC}" + "\\")
+    assert result == r"\\something good\\\\something good" + "\\"
 
 
 def test_replace_env_when_value_is_backslash(replace_one: ReplaceOne, monkeypatch: MonkeyPatch) -> None:
diff --git a/tests/config/loader/test_str_convert.py b/tests/config/loader/test_str_convert.py
index 4fe0aaff..1619056d 100644
--- a/tests/config/loader/test_str_convert.py
+++ b/tests/config/loader/test_str_convert.py
@@ -2,12 +2,14 @@ from __future__ import annotations
 
 import sys
 from pathlib import Path
+from textwrap import dedent
 from typing import Any, Dict, List, Optional, Set, TypeVar, Union
 
 import pytest
 
 from tox.config.loader.str_convert import StrConvert
 from tox.config.types import Command, EnvList
+from tox.pytest import MonkeyPatch, SubRequest, ToxProjectCreator
 
 if sys.version_info >= (3, 8):  # pragma: no cover (py38+)
     from typing import Literal
@@ -80,3 +82,127 @@ def test_str_convert_nok(raw: str, of_type: type[Any], msg: str, exc_type: type[
 def test_invalid_shell_expression(value: str, expected: list[str]) -> None:
     result = StrConvert().to_command(value).args
     assert result == expected
+
+
+SIMPLE_ARGS = [
+    ('foo "bar baz"', ["foo", "bar baz"]),
+    ('foo "bar baz"ext', ["foo", "bar bazext"]),
+    ('foo="bar baz"', ["foo=bar baz"]),
+    ("foo 'bar baz'", ["foo", "bar baz"]),
+    ("foo 'bar baz'ext", ["foo", "bar bazext"]),
+    ("foo='bar baz'", ["foo=bar baz"]),
+    (r"foo=\"bar baz\"", ['foo="bar', 'baz"']),
+    (r'foo="bar baz\"', ['foo="bar baz\\"']),
+    ("foo='bar baz' quuc", ["foo=bar baz", "quuc"]),
+    (r"foo='bar baz\' quuc", ["foo=bar baz\\", "quuc"]),
+    (r"foo=\"bar baz\' quuc", ['foo="bar', "baz'", "quuc"]),
+    (r"foo=\\\"bar baz\"", ['foo=\\"bar', 'baz"']),
+    (r'foo=\\"bar baz\"', [r'foo=\\"bar baz\"']),
+]
+NEWLINE_ARGS = [
+    ('foo\n"bar\nbaz"', ["foo", "bar\nbaz"]),
+]
+INI_CONFIG_NEWLINE_ARGS = [
+    ('foo\\\n    "bar\\\n    baz"', ["foobarbaz"]),  # behavior change from tox 3
+    ('foo\\\n    "bar \\\n    baz"', ["foobar baz"]),  # behavior change from tox 3
+    ('foo \\\n    "bar\\\n    baz"', ["foo", "barbaz"]),
+    ('foo \\\n    "bar \\\n    baz"', ["foo", "bar baz"]),
+    ('foo \\\n    \\"bar \\\n    baz"', ["foo", '"bar', 'baz"']),
+    ("foo \\\n    bar \\\n    baz", ["foo", "bar", "baz"]),
+]
+WINDOWS_PATH_ARGS = [
+    (r"SPECIAL:\foo\bar --quuz='baz atan'", [r"SPECIAL:\foo\bar", "--quuz=baz atan"]),
+    (r"X:\\foo\\bar --quuz='baz atan'", [r"X:\foo\bar", "--quuz=baz atan"]),
+    ("/foo/bar --quuz='baz atan'", ["/foo/bar", "--quuz=baz atan"]),
+    ('cc --arg "C:\\\\Users\\""', ["cc", "--arg", 'C:\\Users"']),
+    ('cc --arg "C:\\\\Users\\"', ["cc", "--arg", '"C:\\\\Users\\"']),
+    ('cc --arg "C:\\\\Users"', ["cc", "--arg", "C:\\Users"]),
+    ('cc --arg \\"C:\\\\Users"', ["cc", "--arg", '\\"C:\\\\Users"']),
+    ('cc --arg "C:\\\\Users\\ "', ["cc", "--arg", "C:\\Users\\ "]),
+    # ('cc --arg "C:\\\\Users\\ ', ["cc", "--arg", '"C:\\\\Users\\ ']),
+    ('cc --arg "C:\\\\Users\\\\"', ["cc", "--arg", "C:\\Users\\"]),
+    ('cc --arg "C:\\\\Users\\\\ "', ["cc", "--arg", "C:\\Users\\ "]),
+    # ('cc --arg "C:\\\\Users\\\\ ', ["cc", "--arg", '"C:\\\\Users\\\\ ']),
+    (
+        r'cc --arg C:\\Users\\ --arg2 "SPECIAL:\Temp\f o o" --arg3="\\FOO\share\Path name" --arg4 SPECIAL:\Temp\ '[:-1],
+        [
+            "cc",
+            "--arg",
+            "C:\\Users\\",
+            "--arg2",
+            "SPECIAL:\\Temp\\f o o",
+            "--arg3=\\FOO\\share\\Path name",
+            "--arg4",
+            "SPECIAL:\\Temp\\",
+        ],
+    ),
+]
+WACKY_SLASH_ARGS = [
+    ("\\\\\\", ["\\\\\\"]),
+    (" \\'\\'\\ '", [" \\'\\'\\ '"]),
+    ("\\'\\'\\ ", ["'' "]),
+    ("\\'\\ \\\\", ["' \\"]),
+    ("\\'\\ ", ["' "]),
+    ('''"\\'\\"''', ['"\\\'\\"']),
+    ("'\\' \\\\", ["\\", "\\"]),
+    ('"\\\\" \\\\', ["\\", "\\"]),
+]
+
+
+@pytest.fixture(params=["win32", "linux2"])
+def sys_platform(request: SubRequest, monkeypatch: MonkeyPatch) -> str:
+    monkeypatch.setattr(sys, "platform", request.param)
+    return str(request.param)
+
+
+@pytest.mark.parametrize(
+    ("value", "expected"),
+    [
+        *SIMPLE_ARGS,
+        *NEWLINE_ARGS,
+        *WINDOWS_PATH_ARGS,
+        *WACKY_SLASH_ARGS,
+    ],
+)
+def test_shlex_platform_specific(sys_platform: str, value: str, expected: list[str]) -> None:
+    if sys_platform != "win32" and value.startswith("SPECIAL:"):
+        # on non-windows platform, backslash is always an escape, not path separator
+        expected = [exp.replace("\\", "") for exp in expected]
+    result = StrConvert().to_command(value).args
+    assert result == expected
+
+
+@pytest.mark.parametrize(
+    ("value", "expected"),
+    [
+        *SIMPLE_ARGS,
+        *INI_CONFIG_NEWLINE_ARGS,
+        *WINDOWS_PATH_ARGS,
+        #        *WACKY_SLASH_ARGS,
+    ],
+)
+def test_shlex_platform_specific_ini(
+    tox_project: ToxProjectCreator,
+    sys_platform: str,
+    value: str,
+    expected: list[str],
+) -> None:
+    if sys_platform != "win32" and value.startswith("SPECIAL:"):
+        # on non-windows platform, backslash is always an escape, not path separator
+        expected = [exp.replace("\\", "") for exp in expected]
+    project = tox_project(
+        {
+            "tox.ini": dedent(
+                """
+                [testenv]
+                commands =
+                    %s""",
+            )
+            % value,
+        },
+    )
+    outcome = project.run("c")
+    outcome.assert_success()
+    env_config = outcome.env_conf("py")
+    result = env_config["commands"]
+    assert result == [Command(args=expected)]
diff --git a/tests/tox_env/python/pip/test_pip_install.py b/tests/tox_env/python/pip/test_pip_install.py
index 8fb70a5f..d6bcdd3c 100644
--- a/tests/tox_env/python/pip/test_pip_install.py
+++ b/tests/tox_env/python/pip/test_pip_install.py
@@ -8,7 +8,7 @@ from unittest.mock import Mock
 import pytest
 from packaging.requirements import Requirement
 
-from tox.pytest import CaptureFixture, ToxProjectCreator
+from tox.pytest import CaptureFixture, SubRequest, ToxProject, ToxProjectCreator
 from tox.tox_env.errors import Fail
 
 
@@ -270,3 +270,139 @@ def test_pip_install_constraint_file_new(tox_project: ToxProjectCreator) -> None
     assert "py: recreate env because changed constraint(s) added a" in result_second.out, result_second.out
     assert execute_calls.call_count == 1
     assert execute_calls.call_args[0][3].cmd == ["python", "-I", "-m", "pip", "install", "a", "-c", "c.txt"]
+
+
+@pytest.fixture(params=[True, False])
+def constrain_package_deps(request: SubRequest) -> bool:
+    return bool(request.param)
+
+
+@pytest.fixture(params=[True, False])
+def use_frozen_constraints(request: SubRequest) -> bool:
+    return bool(request.param)
+
+
+@pytest.fixture(
+    params=[
+        "explicit",
+        "requirements",
+        "constraints",
+        "explicit+requirements",
+        "requirements_indirect",
+        "requirements_constraints_indirect",
+    ],
+)
+def constrained_mock_project(
+    request: SubRequest,
+    tox_project: ToxProjectCreator,
+    demo_pkg_inline: Path,
+    constrain_package_deps: bool,
+    use_frozen_constraints: bool,
+) -> tuple[ToxProject, list[str]]:
+    toml = (demo_pkg_inline / "pyproject.toml").read_text()
+    files = {
+        "pyproject.toml": toml.replace("requires = []", 'requires = ["setuptools"]')
+        + '\n[project]\nname = "demo"\nversion = "0.1"\ndependencies = ["foo > 2"]',
+        "build.py": (demo_pkg_inline / "build.py").read_text(),
+    }
+    exp_constraints: list[str] = []
+    requirement = "foo==1.2.3"
+    constraint = "foo<2"
+    if request.param == "explicit":
+        deps = requirement
+        exp_constraints.append(requirement)
+    elif request.param == "requirements":
+        files["requirements.txt"] = f"--pre\n{requirement}"
+        deps = "-rrequirements.txt"
+        exp_constraints.append(requirement)
+    elif request.param == "constraints":
+        files["constraints.txt"] = constraint
+        deps = "-cconstraints.txt"
+        exp_constraints.append(constraint)
+    elif request.param == "explicit+requirements":
+        files["requirements.txt"] = f"--pre\n{requirement}"
+        deps = "\n\t-rrequirements.txt\n\tfoo"
+        exp_constraints.extend(["foo", requirement])
+    elif request.param == "requirements_indirect":
+        files["foo.requirements.txt"] = f"--pre\n{requirement}"
+        files["requirements.txt"] = "-r foo.requirements.txt"
+        deps = "-rrequirements.txt"
+        exp_constraints.append(requirement)
+    elif request.param == "requirements_constraints_indirect":
+        files["foo.requirements.txt"] = f"--pre\n{requirement}"
+        files["foo.constraints.txt"] = f"{constraint}"
+        files["requirements.txt"] = "-r foo.requirements.txt\n-c foo.constraints.txt"
+        deps = "-rrequirements.txt"
+        exp_constraints.extend([requirement, constraint])
+    else:  # pragma: no cover
+        pytest.fail(f"Missing case: {request.param}")
+    files["tox.ini"] = (
+        "[testenv]\npackage=wheel\n"
+        f"constrain_package_deps = {constrain_package_deps}\n"
+        f"use_frozen_constraints = {use_frozen_constraints}\n"
+        f"deps = {deps}"
+    )
+    return tox_project(files), exp_constraints if constrain_package_deps else []
+
+
+def test_constrain_package_deps(
+    constrained_mock_project: tuple[ToxProject, list[str]],
+    constrain_package_deps: bool,
+    use_frozen_constraints: bool,
+) -> None:
+    proj, exp_constraints = constrained_mock_project
+    execute_calls = proj.patch_execute(lambda r: 0 if "install" in r.run_id else None)
+    result_first = proj.run("r")
+    result_first.assert_success()
+    exp_run_ids = ["install_deps"]
+    if constrain_package_deps and use_frozen_constraints:
+        exp_run_ids.append("freeze")
+    exp_run_ids.extend(
+        [
+            "install_requires",
+            "_optional_hooks",
+            "get_requires_for_build_wheel",
+            "build_wheel",
+            "install_package_deps",
+            "install_package",
+            "_exit",
+        ],
+    )
+    run_ids = [i[0][3].run_id for i in execute_calls.call_args_list]
+    assert run_ids == exp_run_ids
+    constraints_file = proj.path / ".tox" / "py" / "constraints.txt"
+    if constrain_package_deps:
+        constraints = constraints_file.read_text().splitlines()
+        for call in execute_calls.call_args_list:
+            if call[0][3].run_id == "install_package_deps":
+                assert f"-c{constraints_file}" in call[0][3].cmd
+        if use_frozen_constraints:
+            for c in exp_constraints:
+                # when using frozen constraints with this mock, the mock package does NOT
+                # actually end up in the constraints, so assert it's not there
+                assert c not in constraints
+            for c in constraints:
+                assert c.partition("==")[0] in ["pip", "setuptools", "wheel"]
+        else:
+            for c in constraints:
+                assert c in exp_constraints
+            for c in exp_constraints:
+                assert c in constraints
+    else:
+        assert not constraints_file.exists()
+
+
+@pytest.mark.parametrize("conf_key", ["constrain_package_deps", "use_frozen_constraints"])
+def test_change_constraint_options_recreates(tox_project: ToxProjectCreator, conf_key: str) -> None:
+    tox_ini_content = "[testenv:py]\ndeps=a\nskip_install=true"
+    proj = tox_project({"tox.ini": f"{tox_ini_content}\n{conf_key} = true"})
+    proj.patch_execute(lambda r: 0 if "install" in r.run_id else None)
+
+    result = proj.run("r")
+    result.assert_success()
+
+    (proj.path / "tox.ini").write_text(f"{tox_ini_content}\n{conf_key} = false")
+    result_second = proj.run("r")
+    result_second.assert_success()
+    assert "recreate env because constraint options changed" in result_second.out
+    assert conf_key in result_second.out