Coverage for src/lpip/reqfile.py: 84%
45 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-17 20:19 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-17 20:19 +0000
1"""Parse a pip-style requirements file for ``lpip install -r`` (ADR-0017)."""
3from __future__ import annotations
5from collections.abc import Iterator
6from pathlib import Path
8from packaging.requirements import InvalidRequirement, Requirement
9from packaging.version import InvalidVersion, Version
11from lpip.errors import (
12 CatalogExtrasNotSupportedError,
13 RequirementsFileError,
14 RequirementsParseError,
15 UnsupportedCatalogSpecifierError,
16)
19def iter_requirement_lines(text: str) -> Iterator[str]:
20 """Yield non-empty requirement lines; drop comments and blanks."""
21 for raw in text.splitlines():
22 stripped = raw.strip()
23 if not stripped or stripped.startswith("#"):
24 continue
25 if " #" in stripped:
26 stripped = stripped.split(" #", 1)[0].strip()
27 if not stripped: 27 ↛ 28line 27 didn't jump to line 28 because the condition on line 27 was never true
28 continue
29 yield stripped
32def requirement_name(line: str) -> str:
33 """Distribution name from a PEP 508 line. Reject pip option lines."""
34 if line.startswith("-"):
35 raise RequirementsFileError(
36 f"Unsupported requirements option: {line}",
37 hint="v1 install -r accepts PEP 508 requirement lines only "
38 "(no -r, -e, or other pip flags).",
39 )
40 try:
41 return Requirement(line).name
42 except InvalidRequirement as exc:
43 raise RequirementsParseError(line, detail=str(exc)) from exc
46def parse_catalog_request(line: str) -> tuple[str, Version | None]:
47 """Name plus optional == version for a catalog install (ADR-0018)."""
48 name = requirement_name(line)
49 try:
50 req = Requirement(line)
51 except InvalidRequirement as exc:
52 raise RequirementsParseError(line, detail=str(exc)) from exc
53 if req.extras: 53 ↛ 54line 53 didn't jump to line 54 because the condition on line 53 was never true
54 raise CatalogExtrasNotSupportedError(name)
55 specs = list(req.specifier)
56 if not specs:
57 return name, None
58 if len(specs) == 1 and specs[0].operator in {"==", "==="}:
59 try:
60 return name, Version(specs[0].version)
61 except InvalidVersion as exc:
62 raise RequirementsParseError(line, detail=str(exc)) from exc
63 raise UnsupportedCatalogSpecifierError(line)
66def load_requirement_lines(path: Path) -> tuple[str, ...]:
67 file = path.expanduser()
68 if not file.is_file(): 68 ↛ 69line 68 didn't jump to line 69 because the condition on line 68 was never true
69 raise RequirementsFileError(
70 f"Requirements file was not found: {file}",
71 hint="Pass -r lpip-requirements.txt (or another existing path).",
72 path=file,
73 )
74 return tuple(iter_requirement_lines(file.read_text(encoding="utf-8")))