Skip to content

Commit

Permalink
Detect recursively referencing requirements files
Browse files Browse the repository at this point in the history
  • Loading branch information
hellozee committed Jul 26, 2024
1 parent 184390f commit 4359e34
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 4 deletions.
2 changes: 2 additions & 0 deletions news/12653.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Detect recursively referencing requirements files and help users identify
the source.
25 changes: 21 additions & 4 deletions src/pip/_internal/req/req_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,11 +324,14 @@ 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."""
filename = os.path.abspath(filename)
self._parsed_files[filename] = None # The primary requirements file passed
yield from self._parse_and_recurse(filename, constraint)

def _parse_and_recurse(
Expand All @@ -353,11 +356,25 @@ def _parse_and_recurse(
# original file and nested file are paths
elif not SCHEME_RE.search(req_path):
# do a join so relative paths work
req_path = os.path.join(
os.path.dirname(filename),
req_path,
# and then abspath so that we can identify recursive references
req_path = os.path.abspath(
os.path.join(
os.path.dirname(filename),
req_path,
)
)

if req_path in self._parsed_files.keys():
initial_file = self._parsed_files[req_path]
tail = (
f"and again in {initial_file}"
if initial_file is not None
else ""
)
raise RecursionError(
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)
else:
yield line
Expand Down
28 changes: 28 additions & 0 deletions tests/unit/test_req_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,34 @@ def test_nested_constraints_file(
assert reqs[0].name == req_name
assert reqs[0].constraint

def test_recursive_requirements_file(
self, tmpdir: Path, session: PipSession
) -> None:
req_files: list[Path] = []
req_file_count = 4
for i in range(req_file_count):
req_file = tmpdir / f"{i}.txt"
req_file.write_text(f"-r {(i+1) % req_file_count}.txt")
req_files.append(req_file)

# When the passed requirements file recursively references itself
with pytest.raises(
RecursionError,
match=f"{req_files[0]} recursively references itself"
f" in {req_files[req_file_count - 1]}",
):
list(parse_requirements(filename=str(req_files[0]), session=session))

# When one of other the requirements file recursively references itself
req_files[req_file_count - 1].write_text(f"-r {req_files[req_file_count - 2]}")
with pytest.raises(
RecursionError,
match=f"{req_files[req_file_count - 2]} recursively references itself "
f"in {req_files[req_file_count - 1]} and again in"
f" {req_files[req_file_count - 3]}",
):
list(parse_requirements(filename=str(req_files[0]), session=session))

def test_options_on_a_requirement_line(self, line_processor: LineProcessor) -> None:
line = (
'SomeProject --global-option="yo3" --global-option "yo4" '
Expand Down

0 comments on commit 4359e34

Please sign in to comment.