Repository: tox-dev/tox

Issue:
Generated constraints.txt eats the first `c` letter of packages
## Issue

We use `constrain_package_deps = true` and to our surprise, our dependencies that are in our `constraints.txt` that start with the letter `c`, that letter gets removed.

## Environment

Provide at least:

- OS: linux

## Minimal example

Use these files to reproduce it:

```ini
[tox]
min_version = 4.14.1
envlist =
    test

[testenv:test]
allowlist_externals = echo
skip_install = true
constrain_package_deps = true
deps =
    zope.interface
    -c constraints.txt
commands =
    echo 1
```

Download locally: https://dist.plone.org/release/6.0.7/requirements.txt

```shell
wget https://dist.plone.org/release/6.0.7/requirements.txt
```

And run tox:

```shell
tox -e test
```

After `tox` fails (on purpose), look at the generated `.tox/test/constraints.txt` and you will see that there is no `collective.XXX` version pins but rather `ollective.XXX` version pins.



Discussion:
Actually, if one uses

```ini
deps =
   zope.interface
   -c https://dist.plone.org/release/6.0.7/constraints.txt
```

The same happens 😕 
@gforcada Thanks for the bug report!

I was able to reproduce it and identify the issue. I have created a PR which hopefully gets reviewed by @masenf as both he worked on that feature and @gaborbernat is off for a bit.



Patch:
diff --git a/docs/changelog/3247.bugfix.rst b/docs/changelog/3247.bugfix.rst
new file mode 100644
index 000000000..f94bcc9e7
--- /dev/null
+++ b/docs/changelog/3247.bugfix.rst
@@ -0,0 +1,1 @@
+Fix issue that the leading character ``c`` was dropped from packages in constraints files - by :user:`jugmac00`.
diff --git a/src/tox/tox_env/python/pip/pip_install.py b/src/tox/tox_env/python/pip/pip_install.py
index 4b75c2b3c..f09d98d03 100644
--- a/src/tox/tox_env/python/pip/pip_install.py
+++ b/src/tox/tox_env/python/pip/pip_install.py
@@ -107,7 +107,7 @@ def constrain_package_deps(self) -> bool:
     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:
+    def _install_requirement_file(self, arguments: PythonDeps, section: str, of_type: str) -> None:  # noqa: C901
         try:
             new_options, new_reqs = arguments.unroll()
         except ValueError as exception:
@@ -145,7 +145,15 @@ def _install_requirement_file(self, arguments: PythonDeps, section: str, of_type
                 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]
+                        # when we drop Python 3.8 we can use the builtin `.removeprefix`
+                        def remove_prefix(text: str, prefix: str) -> str:
+                            if text.startswith(prefix):
+                                return text[len(prefix) :]
+                            return text
+
+                        combined_constraints = new_requirements + [
+                            remove_prefix(text=c, prefix="-c ") for c in new_constraints
+                        ]
                         self.constraints_file().write_text("\n".join(combined_constraints))
 
     @staticmethod


Test patch:
diff --git a/tests/tox_env/python/pip/test_pip_install.py b/tests/tox_env/python/pip/test_pip_install.py
index 9f90a0f02..ca5aa6ca5 100644
--- a/tests/tox_env/python/pip/test_pip_install.py
+++ b/tests/tox_env/python/pip/test_pip_install.py
@@ -309,8 +309,8 @@ def constrained_mock_project(
         "build.py": (demo_pkg_inline / "build.py").read_text(),
     }
     exp_constraints: list[str] = []
-    requirement = "foo==1.2.3"
-    constraint = "foo<2"
+    requirement = "coo==1.2.3"
+    constraint = "coo<2"
     if request.param == "explicit":
         deps = requirement
         exp_constraints.append(requirement)