diff --git a/news/13046.bugfix.rst b/news/13046.bugfix.rst new file mode 100644 index 00000000000..41aae012414 --- /dev/null +++ b/news/13046.bugfix.rst @@ -0,0 +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 55c0726f370..eb2a1f69921 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -324,19 +324,20 @@ def __init__( ) -> 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 @@ def _parse_and_recurse( 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 @@ def _parse_and_recurse( 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 diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index 7593e55d679..1cc030681db 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -347,6 +347,19 @@ def test_nested_constraints_file( 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: