Repository: pypa/pip

Issue:
pip mistakenly reports recursion error even when there is no recursion
### Description

I have three requirements files, as follows:

```
# requirements.txt
...
```
```
# test-requirements.txt
-r requirements.txt
...
```
```
# lint-requirements.txt
-r requirements.txt
-r test-requirements.txt
...
```
When attempting to `pip install -r lint-requirements.txt`, pip complains:
```console
ERROR: .../requirements.txt recursively references itself in .../test-requirements.txt and again in .../lint-requirements.txt
```
even though there is no recursive reference here. This used to work fine prior to pip 24.3.

### Expected behavior

pip should not report an error and instead should proceed with installation as usual.

### pip version

24.3

### Python version

3.9

### OS

Linux

### How to Reproduce

1. Place three example requirements files in a directory as above.
2. Run `pip install -r lint-requirements.txt`
3. Observe error.

### Output

```console
$ python3.9 -m pip install -r lint-requirements.txt -c constraints.txt
Defaulting to user installation because normal site-packages is not writeable
ERROR: <omitted>/requirements.txt recursively references itself in <omitted>/test-requirements.txt and again in <omitted>/lint-requirements.txt
```

### Code of Conduct

- [X] I agree to follow the [PSF Code of Conduct](https://www.python.org/psf/conduct/).

Discussion:
notatallshaw: I tried a few different variations on my MRE, including both ones that should and should not throw a recursive error, and this branch produced the correct result for all of them.


Patch:
diff --git a/news/13046.bugfix.rst b/news/13046.bugfix.rst
new file mode 100644
index 000000000..41aae0124
--- /dev/null
+++ b/news/13046.bugfix.rst
@@ -0,0 +1,1 @@
+Allow multiple inclusion of the same requirements file again.
diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py
index 55c0726f3..eb2a1f699 100644
--- a/src/pip/_internal/req/req_file.py
+++ b/src/pip/_internal/req/req_file.py
@@ -324,19 +324,20 @@ class RequirementsFileParser:
     ) -> None:
         self._session = session
         self._line_parser = line_parser
-        self._parsed_files: dict[str, Optional[str]] = {}
 
     def parse(
         self, filename: str, constraint: bool
     ) -> Generator[ParsedLine, None, None]:
         """Parse a given file, yielding parsed lines."""
-        self._parsed_files[os.path.abspath(filename)] = (
-            None  # The primary requirements file passed
+        yield from self._parse_and_recurse(
+            filename, constraint, [{os.path.abspath(filename): None}]
         )
-        yield from self._parse_and_recurse(filename, constraint)
 
     def _parse_and_recurse(
-        self, filename: str, constraint: bool
+        self,
+        filename: str,
+        constraint: bool,
+        parsed_files_stack: List[Dict[str, Optional[str]]],
     ) -> Generator[ParsedLine, None, None]:
         for line in self._parse_file(filename, constraint):
             if not line.is_requirement and (
@@ -364,8 +365,9 @@ class RequirementsFileParser:
                             req_path,
                         )
                     )
-                if req_path in self._parsed_files:
-                    initial_file = self._parsed_files[req_path]
+                parsed_files = parsed_files_stack[0]
+                if req_path in parsed_files:
+                    initial_file = parsed_files[req_path]
                     tail = (
                         f" and again in {initial_file}"
                         if initial_file is not None
@@ -375,8 +377,11 @@ class RequirementsFileParser:
                         f"{req_path} recursively references itself in {filename}{tail}"
                     )
                 # Keeping a track where was each file first included in
-                self._parsed_files[req_path] = filename
-                yield from self._parse_and_recurse(req_path, nested_constraint)
+                new_parsed_files = parsed_files.copy()
+                new_parsed_files[req_path] = filename
+                yield from self._parse_and_recurse(
+                    req_path, nested_constraint, [new_parsed_files, *parsed_files_stack]
+                )
             else:
                 yield line
 


Test patch:
diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py
index 7593e55d6..1cc030681 100644
--- a/tests/unit/test_req_file.py
+++ b/tests/unit/test_req_file.py
@@ -347,6 +347,19 @@ class TestProcessLine:
         assert reqs[0].name == req_name
         assert reqs[0].constraint
 
+    def test_repeated_requirement_files(
+        self, tmp_path: Path, session: PipSession
+    ) -> None:
+        # Test that the same requirements file can be included multiple times
+        # as long as there is no recursion. https://github.com/pypa/pip/issues/13046
+        tmp_path.joinpath("a.txt").write_text("requests")
+        tmp_path.joinpath("b.txt").write_text("-r a.txt")
+        tmp_path.joinpath("c.txt").write_text("-r a.txt\n-r b.txt")
+        parsed = parse_requirements(
+            filename=os.fspath(tmp_path.joinpath("c.txt")), session=session
+        )
+        assert [r.requirement for r in parsed] == ["requests", "requests"]
+
     def test_recursive_requirements_file(
         self, tmpdir: Path, session: PipSession
     ) -> None: