Coverage for pipxl/resolver.py: 97%
52 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-28 21:32 +0100
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-28 21:32 +0100
1# SPDX-FileCopyrightText: 2022-present Jeroen van Zundert <mail@jeroenvanzundert.nl>
2#
3# SPDX-License-Identifier: MIT
5from __future__ import annotations
7import json
8from pathlib import Path
9from typing import Any
11from pipxl.data import Environment, ReqFileEntry
12from pipxl.pip_cli import pip_cli
15def pip_resolve(
16 files_in: list[Path] | None = None, package_spec: list[str] | None = None, no_deps: bool = False
17) -> tuple[list[ReqFileEntry], Environment]:
18 target = _pip_install_target_arg(files_in, package_spec)
19 json_report = _pip_install_fresh_dryrun(target, no_deps)
20 return _parse_pip_install_json_report(json_report)
23def _pip_install_target_arg(files_in: list[Path] | None = None, package_spec: list[str] | None = None) -> list[str]:
24 if (files_in is None) and (package_spec is None):
25 raise Exception("At least one files and/or package specification needs to be provided")
27 file_arg = []
28 if (files_in is not None) and len(files_in):
29 for file in files_in:
30 file_arg.extend(["-r", f"{str(file)}"])
32 package_name_arg = [] if package_spec is None else package_spec
34 arg = file_arg + package_name_arg
35 return arg
38def _pip_install_fresh_dryrun(target: list[str], no_deps: bool = False) -> dict[str, Any]:
39 cmd = ["install", "--ignore-installed", "--dry-run", "--report", "-", "--quiet"]
40 if no_deps:
41 cmd += ["--no-deps"]
43 output = pip_cli(cmd + target)
44 if output.returncode != 0: 44 ↛ 45line 44 didn't jump to line 45, because the condition on line 44 was never true
45 raise Exception(output.stderr)
46 js = json.loads(output.stdout)
47 assert isinstance(js, dict)
48 return js
51def _parse_pip_install_json_report(js: dict[str, Any]) -> tuple[list[ReqFileEntry], Environment]:
52 # first collect for each package all its dependencies
53 deptree = dict()
54 for package in js["install"]:
55 # the raw version includes version and platform specifiers
56 # example from httpx: 'certifi', 'sniffio', 'rfc3986[idna2008] (<2,>=1.3)', 'httpcore (<0.16.0,>=0.15.0)'
57 # We store this as a dict, with the key being the name, for easy reference, and the value the full string
58 meta = package["metadata"]
59 if "requires_dist" in meta:
60 deptree[meta["name"]] = {_package_name_from_requires_dist_string(s): s for s in meta["requires_dist"]}
61 else:
62 deptree[meta["name"]] = dict()
64 # traverse through list of packages to get version and required_by
65 out: list[ReqFileEntry] = []
66 for package in js["install"]:
67 meta = package["metadata"]
69 if "requires_dist" in meta:
70 requires = {_package_name_from_requires_dist_string(s): s for s in meta["requires_dist"]}
71 else:
72 requires = {}
74 req_by = {
75 potential_dep: reqs[meta["name"]] for potential_dep, reqs in deptree.items() if meta["name"] in reqs.keys()
76 }
77 archive_info = package["download_info"].get("archive_info")
79 out.append(
80 ReqFileEntry(
81 name=meta["name"],
82 version=meta["version"],
83 requires=requires,
84 required_by=req_by,
85 url=package["download_info"]["url"],
86 hash=archive_info["hash"] if archive_info is not None else None,
87 requested=package["requested"],
88 license=package["metadata"].get("license", None),
89 )
90 )
92 # parse environment
93 env_keys = [
94 "platform_python_implementation",
95 "implementation_version",
96 "platform_system",
97 "platform_release",
98 "platform_machine",
99 ]
100 env = {k: v for k, v in js["environment"].items() if k in env_keys}
101 env = {"pip_version": js["pip_version"]} | env
102 return out, Environment(**env)
105def _package_name_from_requires_dist_string(s: str) -> str:
106 return s.split()[0].split("[")[0]