From ae4c0ace31a5d134b4d5a1de8875a9d40fbadb10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 27 Oct 2024 14:52:16 +0100 Subject: [PATCH] Don't fail when the same req file is included more than once --- src/pip/_internal/req/req_file.py | 23 ++++++++++++++--------- tests/unit/test_req_file.py | 15 +++++++++++++++ 2 files changed, 29 insertions(+), 9 deletions(-) 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..ccb10e939fe 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -347,6 +347,21 @@ 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 = list( + parse_requirements( + filename=str(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: