Coverage for src/iso_freeze/get_requirements.py: 78%
45 statements
« prev ^ index » next coverage.py v6.4.2, created at 2022-08-01 13:01 +0200
« prev ^ index » next coverage.py v6.4.2, created at 2022-08-01 13:01 +0200
1"""Getting requirements from pip install --report."""
3import json
4import sys
6from pathlib import Path
7from typing import Any, Union, Optional
9if sys.version_info >= (3, 11, 0):
10 import tomllib
11else:
12 import tomli as tomllib # type: ignore
14from iso_freeze.lib import PyPackage, run_pip
17def get_pip_report_requirements(
18 file: Path,
19 python_exec: Path,
20 pip_args: Optional[list[str]],
21 optional_dependency: Optional[str],
22) -> Optional[list[PyPackage]]:
23 """Get dependencies to pass to pip install --report.
25 Arguments:
26 file -- Input file to parse (Path)
27 python_exec -- Path to Python executable (Path)
28 pip_args -- Args to pass to pip install (Optional[list[str]])
29 optional_dependency -- Optional dependency to include (Optional[str])
31 Returns:
32 Pip packages from pip install --report output (Optional[list[PyPackage]])
33 """
34 if file.suffix == ".toml":
35 # pip_report_input type is either just list[str] (for TOML dependencies) or
36 # list[Union[str, Path]] (for requirements file).
37 pip_report_input: Union[list[str], list[Union[str, Path]]] = read_toml(
38 toml_dict=load_toml_file(file), optional_dependency=optional_dependency
39 )
40 else:
41 # Telling mypy to ignore type here because it thinks that ["-r", file]
42 # is list[object] rather than list[Union[str, Path]]
43 pip_report_input = ["-r", file] # type: ignore
44 pip_report_command: list[Union[str, Path]] = build_pip_report_command(
45 pip_report_input=pip_report_input,
46 python_exec=python_exec,
47 pip_args=pip_args,
48 )
49 pip_report: dict[str, Any] = get_pip_report(pip_report_command=pip_report_command)
50 return read_pip_report(pip_report)
53def build_pip_report_command(
54 pip_report_input: Union[list[str], list[Union[str, Path]]],
55 python_exec: Path,
56 pip_args: Optional[list[str]],
57) -> list[Union[str, Path]]:
58 """Build pip command to to generate report.
60 Arguments:
61 pip_report_input -- Packages or file to pass to pip report
62 (Union[list[str], list[Union[str, Path]]])
63 python_exec -- Path to Python interpreter to use (Path)
64 pip_args -- Arguments to be passed to pip install (Optional[list[str]])
66 Returns:
67 Pip command to pass to run_pip_report (list[Union[str, Path]])
68 """
69 pip_report_command: list[Union[str, Path]] = [
70 "env",
71 "PIP_REQUIRE_VIRTUALENV=false",
72 python_exec,
73 "-m",
74 "pip",
75 "install",
76 ]
77 # If pip_args have been provided, inject them after the 'install' keyword
78 if pip_args:
79 pip_report_command.extend(pip_args)
80 # Add necessary flags for calling pip install report
81 pip_report_command.extend(
82 ["-q", "--dry-run", "--ignore-installed", "--report", "-", *pip_report_input]
83 )
84 # # Finally, either append dependencies from TOML file or '-r requirements-file'
85 # if toml_dependencies:
86 # pip_report_command.extend([dependency for dependency in toml_dependencies])
87 # else:
88 # pip_report_command.extend(["-r", file])
89 return pip_report_command
92def load_toml_file(toml_file: Path) -> dict[str, Any]:
93 """Load TOML file and return its contents
95 Arguments:
96 toml_file -- Path to TOML file (Path)
98 Returns:
99 Contents of TOML file (dict[str, str])
100 """
101 with open(toml_file, "rb") as f:
102 return tomllib.load(f)
105def read_toml(
106 toml_dict: dict[str, Any],
107 optional_dependency: Optional[str] = None,
108) -> list[str]:
109 """Read TOML dict and return listed dependencies.
111 Includes requirements for optional dependency if any has been specified.
113 Keyword Arguments:
114 toml_file -- Path to pyproject.toml file
115 optional_dependency -- Optional dependency to include (default: None)
117 Returns:
118 List of dependency names (list[str])
119 """
120 if not toml_dict.get("project"):
121 sys.exit("TOML file does not contain a 'project' section.")
122 dependencies: list[str] = toml_dict["project"].get("dependencies")
123 if optional_dependency:
124 if not toml_dict["project"].get("optional-dependencies"):
125 sys.exit("No optional dependencies defined in TOML file.")
126 optional_dependency_reqs: Optional[list[str]] = (
127 toml_dict["project"].get("optional-dependencies").get(optional_dependency)
128 )
129 if optional_dependency_reqs:
130 dependencies.extend(optional_dependency_reqs)
131 else:
132 sys.exit(
133 f"No optional dependency '{optional_dependency}' found in TOML file."
134 )
135 return dependencies
138def get_pip_report(pip_report_command: list[Union[Path, str]]) -> dict[str, Any]:
139 """Capture pip install --report output.
141 Arguments:
142 pip_report_command -- Command for subprocess (list[Union[Path, str]])
144 Returns:
145 Json pip report response (dict[str, Any])
146 """
147 return json.loads(run_pip(command=pip_report_command, check_output=True))
150def read_pip_report(pip_report: dict[str, Any]) -> Optional[list[PyPackage]]:
151 """Extract package informations from pip report.
153 Arguments:
154 pip_report -- Json pip report (dict[str, Any])
156 Returns:
157 List of PyPackage objects containing infos to pin requirements (list[PyPackage])
158 """
159 if pip_report.get("install"):
160 dependencies: list[PyPackage] = []
161 for package in pip_report["install"]:
162 dependencies.append(
163 PyPackage(
164 name=package["metadata"]["name"],
165 version=package["metadata"]["version"],
166 requested=package["requested"],
167 # pip report provides hashes in the form 'sha256=<hash>', but pip
168 # install requires 'sha256:<hash>', so we replace '=' with ':'
169 hash=package["download_info"]["archive_info"]["hash"].replace(
170 "=", ":"
171 ),
172 )
173 )
174 return dependencies
175 else:
176 return None