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

1"""Parse a pip-style requirements file for ``lpip install -r`` (ADR-0017).""" 

2 

3from __future__ import annotations 

4 

5from collections.abc import Iterator 

6from pathlib import Path 

7 

8from packaging.requirements import InvalidRequirement, Requirement 

9from packaging.version import InvalidVersion, Version 

10 

11from lpip.errors import ( 

12 CatalogExtrasNotSupportedError, 

13 RequirementsFileError, 

14 RequirementsParseError, 

15 UnsupportedCatalogSpecifierError, 

16) 

17 

18 

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 

30 

31 

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 

44 

45 

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) 

64 

65 

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")))