|
2 | 2 |
|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
5 | | -import contextlib |
6 | | -import re |
7 | | -from importlib import metadata |
8 | 5 | from pathlib import Path |
9 | | -from typing import Any |
10 | 6 |
|
11 | 7 | import tomli |
12 | | -from packaging.requirements import Requirement |
13 | | -from packaging.utils import canonicalize_name |
14 | 8 |
|
15 | 9 | from licensecheck import license_matrix, packageinfo |
16 | | -from licensecheck.session import session |
| 10 | +from licensecheck.resolvers import native as res_native |
| 11 | +from licensecheck.resolvers import uv as res_uv |
17 | 12 | from licensecheck.types import JOINS, License, PackageInfo, ucstr |
18 | 13 |
|
19 | 14 | USINGS = ["requirements", "poetry", "PEP631"] |
@@ -46,143 +41,35 @@ def getReqs(using: str, skipDependencies: list[ucstr]) -> set[ucstr]: |
46 | 41 | if using not in USINGS: |
47 | 42 | using = "poetry" |
48 | 43 |
|
49 | | - pyproject = {} |
50 | | - requirementsPaths = [] |
51 | | - |
52 | | - pyprojectPath = Path("pyproject.toml") |
53 | | - if pyprojectPath.exists(): |
54 | | - pyproject = tomli.loads(pyprojectPath.read_text(encoding="utf-8")) |
55 | | - |
56 | | - # Requirements |
57 | | - if using == "requirements": |
58 | | - requirementsPaths = [Path("requirements.txt")] |
59 | | - if len(extras) > 0: |
60 | | - requirementsPaths = [Path(x) for x in (extras)] |
61 | | - |
62 | | - return do_get_reqs(using, skipDependencies, extras, pyproject, requirementsPaths) |
63 | | - |
64 | | - |
65 | | -def do_get_reqs( |
66 | | - using: str, |
67 | | - skipDependencies: list[ucstr], |
68 | | - extras: list[str], |
69 | | - pyproject: dict[str, Any], |
70 | | - requirementsPaths: list[Path], |
71 | | -) -> set[ucstr]: |
72 | | - """Underlying machineary to get requirements. |
73 | | -
|
74 | | - Args: |
75 | | - ---- |
76 | | - using (str): use requirements, poetry or PEP631. |
77 | | - skipDependencies (list[str]): list of dependencies to skip. |
78 | | - extras (str | None): to-do |
79 | | - pyproject (dict[str, Any]): to-do |
80 | | - requirementsPaths (list[Path]): to-do |
81 | | -
|
82 | | - Returns: |
83 | | - ------- |
84 | | - set[str]: set of requirement packages |
85 | | -
|
86 | | - """ |
87 | | - reqs = set() |
88 | | - extrasReqs = {} |
89 | | - |
90 | | - def resolveReq(req: str, *, extra: bool = True) -> ucstr: |
91 | | - requirement = Requirement(req) |
92 | | - extras = {ucstr(extra) for extra in requirement.extras} |
93 | | - name = ucstr(canonicalize_name(requirement.name)) |
94 | | - canonicalName = name |
95 | | - if len(extras) > 0: |
96 | | - canonicalName = ucstr(f"{name}[{next(iter(extras))}]") |
97 | | - # Avoid overwriting the initial mapping in extrasReqs, only overwrite when extra is True |
98 | | - if extra: |
99 | | - extrasReqs[name] = extras |
100 | | - return canonicalName if extra else name |
101 | | - |
102 | | - def resolveExtraReq(extraReq: str) -> ucstr | None: |
103 | | - match = re.search(r"extra\s*==\s*[\"'](.*?)[\"']", extraReq) |
104 | | - if match is None: |
105 | | - return None |
106 | | - return ucstr(match.group(1)) |
107 | | - |
108 | | - if using == "poetry": |
109 | | - try: |
110 | | - project = pyproject["tool"]["poetry"] |
111 | | - reqLists = [project["dependencies"]] |
112 | | - except KeyError as error: |
113 | | - msg = "Could not find specification of requirements (pyproject.toml)." |
114 | | - raise RuntimeError(msg) from error |
115 | | - for extra in extras: |
116 | | - reqLists.append( |
117 | | - project.get("group", {extra: {"dependencies": {}}})[extra]["dependencies"] |
118 | | - ) |
119 | | - reqLists.append(project.get("dev-dependencies", {})) |
120 | | - for reqList in reqLists: |
121 | | - for req in reqList: |
122 | | - reqs.add(resolveReq(req)) |
123 | | - # PEP631 |
124 | | - if using == "PEP631": |
125 | | - try: |
126 | | - project = pyproject["project"] |
127 | | - reqLists = [project["dependencies"]] |
128 | | - except KeyError as error: |
129 | | - msg = "Could not find specification of requirements (pyproject.toml)." |
130 | | - raise RuntimeError(msg) from error |
131 | | - for extra in extras: |
132 | | - reqLists.append(project["optional-dependencies"][extra]) |
133 | | - for reqList in reqLists: |
134 | | - for req in reqList: |
135 | | - reqs.add(resolveReq(req)) |
| 44 | + # if using poetry or pep621 |
| 45 | + requirementsPaths = ["pyproject.toml"] |
136 | 46 |
|
137 | 47 | # Requirements |
138 | 48 | if using == "requirements": |
139 | | - for reqPath in requirementsPaths: |
140 | | - if not reqPath.exists(): |
141 | | - msg = f"Could not find specification of requirements ({reqPath})." |
142 | | - raise RuntimeError(msg) |
143 | | - |
144 | | - for _line in reqPath.read_text(encoding="utf-8").splitlines(): |
145 | | - line = _line.rstrip("\\").strip() |
146 | | - if not line or line[0] in {"#", "-"}: |
147 | | - continue |
148 | | - reqs.add(resolveReq(line)) |
149 | | - |
150 | | - # Remove PYTHON if define as requirement |
151 | | - with contextlib.suppress(KeyError): |
152 | | - reqs.remove("PYTHON") |
153 | | - # Remove skip dependencies |
154 | | - for skipDependency in skipDependencies: |
155 | | - with contextlib.suppress(KeyError): |
156 | | - reqs.remove(skipDependency) |
157 | | - |
158 | | - # Get Dependencies, 1 deep |
159 | | - requirementsWithDeps = reqs.copy() |
160 | | - |
161 | | - def update_dependencies(dependency: str) -> None: |
162 | | - dep = resolveReq(dependency, extra=False) |
163 | | - req = resolveReq(requirement, extra=False) |
164 | | - extra = resolveExtraReq(dependency) |
165 | | - if extra is not None: |
166 | | - if req in extrasReqs and extra in extrasReqs.get(req, []): |
167 | | - requirementsWithDeps.add(dep) |
168 | | - else: |
169 | | - requirementsWithDeps.add(dep) |
170 | | - |
171 | | - for requirement in reqs: |
172 | | - try: |
173 | | - pkgMetadata = metadata.metadata(requirement) |
174 | | - for dependency in pkgMetadata.get_all("Requires-Dist") or []: |
175 | | - update_dependencies(dependency) |
176 | | - except metadata.PackageNotFoundError: # noqa: PERF203 |
177 | | - request = session.get( |
178 | | - f"https://pypi.org/pypi/{requirement.split('[')[0]}/json", timeout=60 |
179 | | - ) |
180 | | - response: dict = request.json() |
181 | | - requires_dist: list = response.get("info", {}).get("requires_dist", []) or [] |
182 | | - for dependency in requires_dist: |
183 | | - update_dependencies(dependency) |
184 | | - |
185 | | - return {r.split("[")[0] for r in requirementsWithDeps} |
| 49 | + requirementsPaths = ["requirements.txt"] if len(extras) > 0 else extras |
| 50 | + extras = [] |
| 51 | + |
| 52 | + try: |
| 53 | + return res_uv.get_reqs( |
| 54 | + using=using, |
| 55 | + skipDependencies=skipDependencies, |
| 56 | + extras=extras, |
| 57 | + requirementsPaths=requirementsPaths, |
| 58 | + ) |
| 59 | + |
| 60 | + except RuntimeError: |
| 61 | + pyproject = {} |
| 62 | + if "pyproject.toml" in requirementsPaths: |
| 63 | + pyproject = tomli.loads(Path("pyproject.toml").read_text("utf-8")) |
| 64 | + |
| 65 | + # Fallback to the old resolver (hopefully we can deprecate this asap!) |
| 66 | + return res_native.get_reqs( |
| 67 | + using=using, |
| 68 | + skipDependencies=skipDependencies, |
| 69 | + extras=extras, |
| 70 | + pyproject=pyproject, |
| 71 | + requirementsPaths=[Path(x) for x in requirementsPaths], |
| 72 | + ) |
186 | 73 |
|
187 | 74 |
|
188 | 75 | def getDepsWithLicenses( |
|
0 commit comments