|
1 | 1 | # -*- coding: utf-8 -*-
|
2 | 2 | # require python 3.8+ because of importlib.metadata
|
| 3 | +# revamped via github/gpt-4o free then gemini flash 2 free |
3 | 4 | import json
|
4 | 5 | import sys
|
5 | 6 | import re
|
@@ -43,20 +44,21 @@ def __init__(self, target=None):
|
43 | 44 | self.raw = {}
|
44 | 45 | self.environment = self._get_environment()
|
45 | 46 |
|
46 |
| - target = target or sys.executable |
| 47 | + search_path = target or sys.executable |
47 | 48 |
|
48 |
| - if sys.executable==target: |
| 49 | + if sys.executable==search_path: |
49 | 50 | # self-Distro inspection case (use all packages reachable per sys.path I presume )
|
50 | 51 | packages=Distribution.discover()
|
51 | 52 | else:
|
52 | 53 | # not self-Distro inspection case , look at site-packages only)
|
53 |
| - packages=distributions(path=[str(Path(target).parent /'lib'/'site-packages'),]) |
| 54 | + packages=distributions(path=[str(Path(search_path).parent /'lib'/'site-packages'),]) |
| 55 | + |
54 | 56 |
|
55 | 57 | for package in packages:
|
56 | 58 | self._process_package(package)
|
57 | 59 |
|
58 | 60 | # On a second pass, complement dependancies in reverse mode with 'wanted-per':
|
59 |
| - self._populate_wanted_per() |
| 61 | + self._populate_reverse_dependencies() |
60 | 62 |
|
61 | 63 | def _get_environment(self):
|
62 | 64 | """Get the current environment details."""
|
@@ -141,7 +143,7 @@ def _get_provides(self, package):
|
141 | 143 | provides[req_marker.split('extra == ')[1].translate(remove_list)] = None
|
142 | 144 | return provides
|
143 | 145 |
|
144 |
| - def _populate_wanted_per(self): |
| 146 | + def _populate_reverse_dependencies(self): |
145 | 147 | """Populate the wanted_per field for each package."""
|
146 | 148 | # - get all downward links in 'requires_dist' of each package
|
147 | 149 | # - feed the required packages 'wanted_per' as a reverse dict of dict
|
@@ -171,125 +173,111 @@ def _populate_wanted_per(self):
|
171 | 173 | self.distro[r["req_key"]]["provided"][r["req_marker"].split('extra == ')[1].translate(remove_list)] = None
|
172 | 174 | self.distro[r["req_key"]]["wanted_per"].append(want_add)
|
173 | 175 |
|
| 176 | + def _get_dependency_tree(self, package_name, extra="", version_req="", depth=20, path=None, verbose=False, upward=False): |
| 177 | + """Recursive function to build dependency tree.""" |
| 178 | + path = path or [] |
| 179 | + extras = extra.split(",") |
| 180 | + package_key = normalize(package_name) |
| 181 | + ret_all = [] |
| 182 | + #pe = normalize(f'{package_key}[{extras}]') |
| 183 | + if package_key + "[" + extra + "]" in path: |
| 184 | + print("cycle!", "->".join(path + [package_key + "[" + extra + "]"])) |
| 185 | + return [] # Return empty list to avoid further recursion |
174 | 186 |
|
| 187 | + package_data = self.distro.get(package_key) |
| 188 | + if package_data and len(path) <= depth: |
| 189 | + for extra in extras: |
| 190 | + environment = {"extra": extra, **self.environment} |
| 191 | + summary = f' {package_data["summary"]}' if verbose else '' |
| 192 | + base_name = f'{package_name}[{extra}]' if extra else package_name |
| 193 | + ret = [f'{base_name}=={package_data["version"]} {version_req}{summary}'] |
175 | 194 |
|
176 |
| - def _downraw(self, pp, extra="", version_req="", depth=20, path=[], verbose=False): |
177 |
| - """build a nested list of needed packages with given extra and depth""" |
178 |
| - envi = {"extra": extra, **self.environment} |
179 |
| - p = normalize(pp) |
180 |
| - extras = extra.split(",") # to handle several extras, example: dask[array,diagnostics] |
181 |
| - ret_all = [] |
182 |
| - if p + "[" + extra + "]" in path: # for dask[complete]->dask[array,test,..] |
183 |
| - print("cycle!", "->".join(path + [p + "[" + extra + "]"])) |
184 |
| - elif p in self.distro and len(path) <= depth: |
185 |
| - for extra in extras: # several extras request management |
186 |
| - envi = {"extra": extra, **self.environment} |
187 |
| - summary = f' {self.distro[p]["summary"]}' if verbose else '' |
188 |
| - if extra == "": |
189 |
| - ret = [f'{p}=={self.distro[p]["version"]} {version_req}{summary}'] |
190 |
| - else: |
191 |
| - ret = [f'{p}[{extra}]=={self.distro[p]["version"]} {version_req}{summary}'] |
192 |
| - for r in self.distro[p]["requires_dist"]: |
193 |
| - if r["req_key"] in self.distro: |
194 |
| - if "req_marker" not in r or Marker(r["req_marker"]).evaluate(environment=envi): |
195 |
| - ret += self._downraw( |
196 |
| - r["req_key"], |
197 |
| - r["req_extra"], |
198 |
| - r["req_version"], |
199 |
| - depth, |
200 |
| - path + [p + "[" +extra + "]"], |
201 |
| - verbose=verbose, |
202 |
| - ) |
203 |
| - ret_all.append(ret) |
204 |
| - return ret_all |
| 195 | + dependencies = package_data["requires_dist"] if not upward else package_data["wanted_per"] |
205 | 196 |
|
206 |
| - def _upraw(self, pp, extra="", version_req="", depth=20, path=[], verbose=False): |
207 |
| - """build a nested list of user packages with given extra and depth |
208 |
| - from direct dependancies like dask-image <--dask['array'] |
209 |
| - or indirect like Pytest['test'] <-- pandas['test']""" |
| 197 | + for dependency in dependencies: |
| 198 | + if dependency["req_key"] in self.distro: |
| 199 | + if not dependency.get("req_marker") or Marker(dependency["req_marker"]).evaluate(environment=environment): |
| 200 | + next_path = path + [base_name] |
| 201 | + if upward: |
| 202 | + up_req = (dependency.get("req_marker", "").split('extra == ')+[""])[1].strip("'\"") |
| 203 | + # 2024-06-30 example of langchain <- numpy. pip.distro['numpy']['wanted_per'] has: |
| 204 | + # {'req_key': 'langchain', 'req_version': '(>=1,<2)', 'req_extra': '', 'req_marker': ' python_version < "3.12"'}, |
| 205 | + # {'req_key': 'langchain', 'req_version': '(>=1.26.0,<2.0.0)', 'req_extra': '', 'req_marker': ' python_version >= "3.12"'} |
| 206 | + # must be no extra dependancy, optionnal extra in the package, or provided extra per upper packages |
| 207 | + if dependency["req_key"] in self.distro and dependency["req_key"]+"["+up_req+"]" not in path: # avoids circular links on dask[array] |
| 208 | + if (not dependency.get("req_marker") and extra =="") or (extra !="" and extra==up_req and dependency["req_key"]!=package_key) or (extra !="" and "req_marker" in dependency and extra+',' in dependency["req_extra"]+',' #bingo1346 contourpy[test-no-images] |
| 209 | + or "req_marker" in dependency and extra+',' in dependency["req_extra"]+',' and Marker(dependency["req_marker"]).evaluate(environment=environment) |
| 210 | + ): |
| 211 | + ret += self._get_dependency_tree( |
| 212 | + dependency["req_key"], |
| 213 | + up_req, # pydask[array] going upwards will look for pydask[dataframe] |
| 214 | + f"[requires: {package_name}" |
| 215 | + + ( |
| 216 | + "[" + dependency["req_extra"] + "]" |
| 217 | + if dependency["req_extra"] != "" |
| 218 | + else "" |
| 219 | + ) |
| 220 | + + f'{dependency["req_version"]}]', |
| 221 | + depth, |
| 222 | + next_path, |
| 223 | + verbose=verbose, |
| 224 | + upward=upward, |
| 225 | + ) |
| 226 | + else: |
| 227 | + ret += self._get_dependency_tree( |
| 228 | + dependency["req_key"], |
| 229 | + dependency["req_extra"], |
| 230 | + dependency["req_version"], |
| 231 | + depth, |
| 232 | + next_path, |
| 233 | + verbose=verbose, |
| 234 | + upward=upward, |
| 235 | + ) |
210 | 236 |
|
211 |
| - remove_list = {ord("'"):None, ord('"'):None} # to clean-up req_extra |
212 |
| - envi = {"extra": extra, **self.environment} |
213 |
| - p = normalize(pp) |
214 |
| - pe = normalize(f'{pp}[{extra}]') |
215 |
| - ret_all = [] |
216 |
| - if pe in path: |
217 |
| - print("cycle!", "->".join(path + [pe])) |
218 |
| - elif p in self.distro and len(path) <= depth: |
219 |
| - summary = f' {self.distro[p]["summary"]}' if verbose else '' |
220 |
| - if extra == "": |
221 |
| - ret_all = [f'{p}=={self.distro[p]["version"]} {version_req}{summary}'] |
222 |
| - elif extra in set(self.distro[p]["provided"]).union(set(self.distro[p]["provides"])): # so that -r pytest[test] gives |
223 |
| - ret_all = [f'{p}[{extra}]=={self.distro[p]["version"]} {version_req}{summary}'] |
224 |
| - else: |
225 |
| - return [] |
226 |
| - ret = [] |
227 |
| - for r in self.distro[p]["wanted_per"]: |
228 |
| - up_req = (r.get("req_marker", "").split('extra == ')+[""])[1].translate(remove_list) |
229 |
| - if r["req_key"] in self.distro and r["req_key"]+"["+up_req+"]" not in path: # avoids circular links on dask[array] |
230 |
| - # 2024-06-30 example of langchain <- numpy. pip.distro['numpy']['wanted_per'] has: |
231 |
| - # {'req_key': 'langchain', 'req_version': '(>=1,<2)', 'req_extra': '', 'req_marker': ' python_version < "3.12"'}, |
232 |
| - # {'req_key': 'langchain', 'req_version': '(>=1.26.0,<2.0.0)', 'req_extra': '', 'req_marker': ' python_version >= "3.12"'} |
233 |
| - # must be no extra dependancy, optionnal extra in the package, or provided extra per upper packages |
234 |
| - if ("req_marker" not in r and extra =="") or (extra !="" and extra==up_req and r["req_key"]!=p) or (extra !="" and "req_marker" in r and extra+',' in r["req_extra"]+',' #bingo1346 contourpy[test-no-images] |
235 |
| - or "req_marker" in r and extra+',' in r["req_extra"]+',' and Marker(r["req_marker"]).evaluate(environment=envi) |
236 |
| - ): |
237 |
| - ret += self._upraw( |
238 |
| - r["req_key"], |
239 |
| - up_req, # pydask[array] going upwards will look for pydask[dataframe] |
240 |
| - f"[requires: {p}" |
241 |
| - + ( |
242 |
| - "[" + r["req_extra"] + "]" |
243 |
| - if r["req_extra"] != "" |
244 |
| - else "" |
245 |
| - ) |
246 |
| - + f'{r["req_version"]}]', |
247 |
| - depth, |
248 |
| - path + [pe], |
249 |
| - verbose=verbose, |
250 |
| - ) |
251 |
| - if not ret == []: |
252 | 237 | ret_all.append(ret)
|
253 | 238 | return ret_all
|
254 | 239 |
|
255 |
| - def down(self, pp="", extra="", depth=99, indent=5, version_req="", verbose=False): |
256 |
| - """print the downward requirements for the package or all packages""" |
257 |
| - if pp != ".": |
258 |
| - if extra != ".": |
259 |
| - if pp in self.distro: |
260 |
| - extras = [s for s in extra.split(',') if s in sorted(self.distro[pp]["provides"])] |
261 |
| - if extras == []: return '' |
262 |
| - rawtext = json.dumps(self._downraw(pp, extra, version_req, depth, verbose=verbose), indent=indent) |
263 |
| - lines = [l for l in rawtext.split("\n") if len(l.strip()) > 2] |
264 |
| - return ("\n".join(lines).replace('"', "")) |
265 |
| - else: |
266 |
| - if pp in self.distro: |
267 |
| - results = [self.down(pp, one_extra, depth, indent, version_req, verbose=verbose) |
268 |
| - for one_extra in sorted(self.distro[pp]["provides"])] |
269 |
| - return '\n'.join(filter(None, results)) |
270 |
| - else: |
271 |
| - results = [self.down(one_pp, extra, depth, indent, version_req, verbose=verbose) |
272 |
| - for one_pp in sorted(self.distro)] |
| 240 | + def down(self, pp="", extra="", depth=20, indent=5, version_req="", verbose=False): |
| 241 | + """Print the downward requirements for the package or all packages.""" |
| 242 | + if pp == ".": |
| 243 | + results = [self.down(one_pp, extra, depth, indent, version_req, verbose=verbose) for one_pp in sorted(self.distro)] |
273 | 244 | return '\n'.join(filter(None, results))
|
274 |
| - |
275 |
| - def up(self, pp, extra="", depth=99, indent=5, version_req="", verbose=False): |
| 245 | + |
| 246 | + if extra == ".": |
| 247 | + if pp in self.distro: |
| 248 | + results = [self.down(pp, one_extra, depth, indent, version_req, verbose=verbose) |
| 249 | + for one_extra in sorted(self.distro[pp]["provides"])] |
| 250 | + return '\n'.join(filter(None, results)) |
| 251 | + return "" # Handle cases where extra is "." and package_name is not found. |
| 252 | + |
| 253 | + if pp not in self.distro: |
| 254 | + return "" # Handle cases where package_name is not found. |
| 255 | + |
| 256 | + rawtext = json.dumps(self._get_dependency_tree(pp, extra, version_req, depth, verbose=verbose), indent=indent) |
| 257 | + lines = [l for l in rawtext.split("\n") if len(l.strip()) > 2] |
| 258 | + return "\n".join(lines).replace('"', "") |
| 259 | + |
| 260 | + def up(self, pp, extra="", depth=20, indent=5, version_req="", verbose=False): |
276 | 261 | """Print the upward needs for the package."""
|
277 |
| - r = [] |
278 |
| - if pp != ".": |
279 |
| - if extra != ".": |
280 |
| - rawtext = json.dumps(self._upraw(pp, extra, version_req, depth, verbose=verbose), indent=indent) |
281 |
| - lines = [l for l in rawtext.split("\n") if len(l.strip()) > 2] |
282 |
| - return ('\n'.join(filter(None, lines)).replace('"', "") ) |
283 |
| - else: |
284 |
| - if pp in self.distro: |
285 |
| - # get 'extra' tags from direct and from upward packages |
286 |
| - results = [self.up(pp, one_extra, depth, indent, version_req, verbose=verbose) |
287 |
| - for one_extra in sorted(set(self.distro[pp]["provided"]).union(set(self.distro[pp]["provides"])))] |
288 |
| - return '\n'.join(filter(None, results)) |
289 |
| - else: |
290 |
| - results = [self.up(one_pp, extra, depth, indent, version_req, verbose=verbose) for one_pp in sorted(self.distro)] |
| 262 | + |
| 263 | + if pp == ".": |
| 264 | + results = [self.up(one_pp, extra, depth, indent, version_req, verbose) for one_pp in sorted(self.distro)] |
291 | 265 | return '\n'.join(filter(None, results))
|
292 | 266 |
|
| 267 | + if extra == ".": |
| 268 | + if pp in self.distro: |
| 269 | + extras = set(self.distro[pp]["provided"]).union(set(self.distro[pp]["provides"])) |
| 270 | + results = [self.up(pp, one_extra, depth, indent, version_req, verbose=verbose) for one_extra in sorted(extras)] |
| 271 | + return '\n'.join(filter(None, results)) |
| 272 | + return "" |
| 273 | + |
| 274 | + if pp not in self.distro: |
| 275 | + return "" |
| 276 | + |
| 277 | + rawtext = json.dumps(self._get_dependency_tree(pp, extra, version_req, depth, verbose=verbose, upward=True), indent=indent) |
| 278 | + lines = [l for l in rawtext.split("\n") if len(l.strip()) > 2] |
| 279 | + return '\n'.join(filter(None, lines)).replace('"', "") |
| 280 | + |
293 | 281 | def description(self, pp):
|
294 | 282 | """Return description of the package."""
|
295 | 283 | if pp in self.distro:
|
|
0 commit comments