Skip to content

Commit 795170c

Browse files
authored
Merge pull request winpython#1476 from stonebig/master
use Grok.
2 parents 0888558 + 1790261 commit 795170c

File tree

1 file changed

+71
-73
lines changed

1 file changed

+71
-73
lines changed

winpython/piptree.py

Lines changed: 71 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# -*- coding: utf-8 -*-
2-
# require python 3.8+ because of importlib.metadata
3-
# revamped via github/gpt-4o free then gemini flash 2 free
2+
"""
3+
This script provides functionality to inspect and display package dependencies
4+
in a Python environment, including both downward and upward dependencies.
5+
Requires Python 3.8+ due to importlib.metadata.
6+
"""
7+
48
import json
59
import sys
610
import re
@@ -16,43 +20,39 @@ def normalize(name):
1620
"""Normalize package name according to PEP 503."""
1721
return re.sub(r"[-_.]+", "-", name).lower()
1822

23+
1924
def sum_up(text, max_length=144, stop_at=". "):
20-
"""Summarize text to a single line of max_length characters."""
25+
"""
26+
Summarize text to fit within max_length characters, ending at the last complete sentence if possible.
27+
28+
:param text: The text to summarize
29+
:param max_length: Maximum length for summary
30+
:param stop_at: String to stop summarization at
31+
:return: Summarized text
32+
"""
2133
summary = (text + os.linesep).splitlines()[0]
2234
if len(summary) > max_length and len(stop_at) > 1:
2335
summary = (summary + stop_at).split(stop_at)[0]
2436
if len(summary) > max_length:
2537
summary = summary[:max_length]
2638
return summary
2739

28-
2940
class pipdata:
30-
"""Wrapper around Distribution.discover() or Distribution.distributions()"""
41+
"""
42+
Wrapper around Distribution.discover() or Distribution.distributions() to manage package metadata.
43+
"""
3144

3245
def __init__(self, target=None):
33-
34-
# create a distro{} dict of Packages
35-
# key = normalised package name
36-
# string_elements = 'name', 'version', 'summary'
37-
# requires = list of dict with 1 level need downward
38-
# req_key = package_key requires
39-
# req_extra = extra branch needed of the package_key ('all' or '')
40-
# req_version = version needed
41-
# req_marker = marker of the requirement (if any)
42-
4346
self.distro = {}
4447
self.raw = {}
4548
self.environment = self._get_environment()
4649

47-
search_path = target or sys.executable
50+
search_path = target or sys.executable
4851

49-
if sys.executable==search_path:
50-
# self-Distro inspection case (use all packages reachable per sys.path I presume )
51-
packages=Distribution.discover()
52+
if sys.executable == search_path:
53+
packages = Distribution.discover()
5254
else:
53-
# not self-Distro inspection case , look at site-packages only)
54-
packages=distributions(path=[str(Path(search_path).parent /'lib'/'site-packages'),])
55-
55+
packages = distributions(path=[str(Path(search_path).parent / 'lib' / 'site-packages')])
5656

5757
for package in packages:
5858
self._process_package(package)
@@ -61,7 +61,11 @@ def __init__(self, target=None):
6161
self._populate_reverse_dependencies()
6262

6363
def _get_environment(self):
64-
"""Get the current environment details."""
64+
"""
65+
Collect environment details for dependency evaluation.
66+
67+
:return: Dictionary containing system and Python environment information
68+
"""
6569
return {
6670
"implementation_name": sys.implementation.name,
6771
"implementation_version": "{0.major}.{0.minor}.{0.micro}".format(sys.implementation.version),
@@ -77,63 +81,63 @@ def _get_environment(self):
7781
}
7882

7983
def _process_package(self, package):
80-
"""Process a single package and add it to the distro dictionary."""
84+
"""Process package metadata and store it in the distro dictionary."""
8185
meta = package.metadata
8286
name = meta['Name']
8387
version = package.version
8488
key = normalize(name)
8589
self.raw[key] = meta
86-
provided = {'': None}
87-
88-
requires = self._get_requires(package)
89-
provides = self._get_provides(package)
9090

9191
self.distro[key] = {
92-
"name": name,
93-
"version": version,
94-
"summary": meta.get("Summary", ""),
95-
"requires_dist": requires,
96-
"wanted_per": [],
97-
"description": meta.get("Description", ""),
98-
"provides": provides,
99-
"provided": provided, # being extras from other packages: 'test' for pytest because dask['test'] wants pytest
92+
"name": name,
93+
"version": version,
94+
"summary": meta.get("Summary", ""),
95+
"requires_dist": self._get_requires(package),
96+
"reverse_dependencies": [],
97+
"description": meta.get("Description", ""),
98+
"provides": self._get_provides(package),
99+
"provided": {'': None} # Placeholder for extras provided by this package
100100
}
101101

102102
def _get_requires(self, package):
103-
"""Get the requirements of a package."""
103+
"""Extract and normalize requirements for a package."""
104+
# requires = list of dict with 1 level need downward
105+
# req_key = package_key requires
106+
# req_extra = extra branch needed of the package_key ('all' or '')
107+
# req_version = version needed
108+
# req_marker = marker of the requirement (if any)
104109
requires = []
105-
replacements = str.maketrans({" ": " ", "[": "", "]": "", "'": "", '"': ""}) # space not ' or '
106-
further_replacements=((' == ', '=='),('= ', '='), (' !=', '!='), (' ~=', '~='),
107-
(' <', '<'),('< ', '<'), (' >', '>'), ('> ', '>'),
108-
('; ', ';'), (' ;', ';'), ('( ', '('),
109-
(' and (',' andZZZZZ('), (' (', '('), (' andZZZZZ(',' and (' ))
110+
replacements = str.maketrans({" ": " ", "[": "", "]": "", "'": "", '"': ""})
111+
further_replacements = [
112+
(' == ', '=='), ('= ', '='), (' !=', '!='), (' ~=', '~='),
113+
(' <', '<'), ('< ', '<'), (' >', '>'), ('> ', '>'),
114+
('; ', ';'), (' ;', ';'), ('( ', '('),
115+
(' and (', ' andZZZZZ('), (' (', '('), (' andZZZZZ(', ' and (')
116+
]
110117

111118
if package.requires:
112119
for req in package.requires:
113-
# req_nameextra is "python-jose[cryptography]"
114-
# from fastapi "python-jose[cryptography]<4.0.0,>=3.3.0
115-
# req_nameextra is "google-cloud-storage"
116-
# from "google-cloud-storage (<2.0.0,>=1.26.0)
117120
req_nameextra, req_marker = (req + ";").split(";")[:2]
118-
req_nameextra = normalize(re.split(" |;|==|!|>|<", req_nameextra+";")[0])
121+
req_nameextra = normalize(re.split(r" |;|==|!|>|<", req_nameextra + ";")[0])
119122
req_key = normalize((req_nameextra + "[").split("[")[0])
120123
req_key_extra = req_nameextra[len(req_key) + 1:].split("]")[0]
121124
req_version = req[len(req_nameextra):].translate(replacements)
122-
for other in further_replacements: # before we stop this cosmetic...
123-
req_version = req_version.replace(*other)
125+
126+
for old, new in further_replacements:
127+
req_version = req_version.replace(old, new)
128+
124129
req_add = {
125130
"req_key": req_key,
126131
"req_version": req_version,
127132
"req_extra": req_key_extra,
128133
}
129-
if not req_marker == "":
134+
if req_marker != "":
130135
req_add["req_marker"] = req_marker
131136
requires.append(req_add)
132137
return requires
133138

134139
def _get_provides(self, package):
135-
"""Get the extended list of dependant packages, from extra options."""
136-
# 'array' is an added dependancy package of dask, if you install dask['array']
140+
"""Get the list of extras provided by this package."""
137141
provides = {'': None}
138142
if package.requires:
139143
for req in package.requires:
@@ -144,34 +148,28 @@ def _get_provides(self, package):
144148
return provides
145149

146150
def _populate_reverse_dependencies(self):
147-
"""Populate the wanted_per field for each package."""
151+
"""Add reverse dependencies to each package."""
148152
# - get all downward links in 'requires_dist' of each package
149-
# - feed the required packages 'wanted_per' as a reverse dict of dict
153+
# - feed the required packages 'reverse_dependencies' as a reverse dict of dict
150154
# contains =
151155
# req_key = upstream package_key
152156
# req_version = downstream package version wanted
153157
# req_extra = extra option of the demanding package that wants this dependancy
154158
# req_marker = marker of the downstream package requirement (if any)
155-
for p in self.distro:
156-
for r in self.distro[p]["requires_dist"]:
157-
if r["req_key"] in self.distro:
159+
for package in self.distro:
160+
for requirement in self.distro[package]["requires_dist"]:
161+
if requirement["req_key"] in self.distro:
158162
want_add = {
159-
"req_key": p,
160-
"req_version": r["req_version"],
161-
"req_extra": r["req_extra"],
163+
"req_key": package,
164+
"req_version": requirement["req_version"],
165+
"req_extra": requirement["req_extra"],
162166
}
163-
if "req_marker" in r:
164-
want_add["req_marker"] = r["req_marker"] # req_key_extra
165-
166-
# provided = extras in upper packages that triggers the need for this package,
167-
# like 'pandas[test]->Pytest', so 'test' in distro['pytest']['provided']['test']
168-
# corner-cases: 'dask[dataframe]' -> dask[array]'
169-
# 'dask-image ->dask[array]
170-
171-
if 'extra == ' in r["req_marker"]:
167+
if "req_marker" in requirement:
168+
want_add["req_marker"] = requirement["req_marker"]
169+
if 'extra == ' in requirement["req_marker"]:
172170
remove_list = {ord("'"):None, ord('"'):None}
173-
self.distro[r["req_key"]]["provided"][r["req_marker"].split('extra == ')[1].translate(remove_list)] = None
174-
self.distro[r["req_key"]]["wanted_per"].append(want_add)
171+
self.distro[requirement["req_key"]]["provided"][requirement["req_marker"].split('extra == ')[1].translate(remove_list)] = None
172+
self.distro[requirement["req_key"]]["reverse_dependencies"].append(want_add)
175173

176174
def _get_dependency_tree(self, package_name, extra="", version_req="", depth=20, path=None, verbose=False, upward=False):
177175
"""Recursive function to build dependency tree."""
@@ -192,15 +190,15 @@ def _get_dependency_tree(self, package_name, extra="", version_req="", depth=20,
192190
base_name = f'{package_name}[{extra}]' if extra else package_name
193191
ret = [f'{base_name}=={package_data["version"]} {version_req}{summary}']
194192

195-
dependencies = package_data["requires_dist"] if not upward else package_data["wanted_per"]
193+
dependencies = package_data["requires_dist"] if not upward else package_data["reverse_dependencies"]
196194

197195
for dependency in dependencies:
198196
if dependency["req_key"] in self.distro:
199197
if not dependency.get("req_marker") or Marker(dependency["req_marker"]).evaluate(environment=environment):
200198
next_path = path + [base_name]
201199
if upward:
202200
up_req = (dependency.get("req_marker", "").split('extra == ')+[""])[1].strip("'\"")
203-
# 2024-06-30 example of langchain <- numpy. pip.distro['numpy']['wanted_per'] has:
201+
# 2024-06-30 example of langchain <- numpy. pip.distro['numpy']['reverse_dependencies'] has:
204202
# {'req_key': 'langchain', 'req_version': '(>=1,<2)', 'req_extra': '', 'req_marker': ' python_version < "3.12"'},
205203
# {'req_key': 'langchain', 'req_version': '(>=1.26.0,<2.0.0)', 'req_extra': '', 'req_marker': ' python_version >= "3.12"'}
206204
# must be no extra dependancy, optionnal extra in the package, or provided extra per upper packages

0 commit comments

Comments
 (0)