Skip to content

Commit 698ff34

Browse files
authored
Merge pull request winpython#1479 from stonebig/master
piptree docstring via mistral , and fix upward
2 parents d757b47 + 3f0a7af commit 698ff34

File tree

1 file changed

+181
-60
lines changed

1 file changed

+181
-60
lines changed

winpython/piptree.py

Lines changed: 181 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,31 @@
1212
import os
1313
from collections import OrderedDict
1414
from typing import Dict, List, Optional, Tuple, Union
15-
from pip._vendor.packaging.markers import Marker, InvalidMarker
15+
from pip._vendor.packaging.markers import Marker
1616
from importlib.metadata import Distribution, distributions
1717
from pathlib import Path
1818

1919
def normalize(name: str) -> str:
20-
"""Normalize package name according to PEP 503."""
20+
"""
21+
Normalize package name according to PEP 503.
22+
23+
This function converts a package name to its canonical form by replacing
24+
any sequence of dashes, underscores, or dots with a single dash and
25+
converting the result to lowercase.
26+
27+
:param name: The package name to normalize
28+
:return: The normalized package name
29+
"""
2130
return re.sub(r"[-_.]+", "-", name).lower()
2231

2332
def sum_up(text: str, max_length: int = 144, stop_at: str = ". ") -> str:
2433
"""
2534
Summarize text to fit within max_length characters, ending at the last complete sentence if possible.
2635
36+
This function attempts to create a summary of the given text that fits within
37+
the specified maximum length. It tries to end the summary at a complete sentence
38+
if possible.
39+
2740
:param text: The text to summarize
2841
:param max_length: Maximum length for summary
2942
:param stop_at: String to stop summarization at
@@ -39,19 +52,23 @@ def sum_up(text: str, max_length: int = 144, stop_at: str = ". ") -> str:
3952
class PipData:
4053
"""
4154
Wrapper around Distribution.discover() or Distribution.distributions() to manage package metadata.
55+
56+
This class provides methods to inspect and display package dependencies
57+
in a Python environment, including both downward and upward dependencies.
4258
"""
4359

4460
def __init__(self, target: Optional[str] = None):
61+
"""
62+
Initialize the PipData instance.
63+
64+
:param target: Optional target path to search for packages
65+
"""
4566
self.distro: Dict[str, Dict] = {}
4667
self.raw: Dict[str, Dict] = {}
4768
self.environment = self._get_environment()
4869

4970
search_path = target or sys.executable
50-
51-
if sys.executable == search_path:
52-
packages = Distribution.discover()
53-
else:
54-
packages = distributions(path=[str(Path(search_path).parent / 'lib' / 'site-packages')])
71+
packages = self._get_packages(search_path)
5572

5673
for package in packages:
5774
self._process_package(package)
@@ -63,6 +80,9 @@ def _get_environment(self) -> Dict[str, str]:
6380
"""
6481
Collect environment details for dependency evaluation.
6582
83+
This method gathers information about the system and Python environment,
84+
which is used to evaluate package dependencies.
85+
6686
:return: Dictionary containing system and Python environment information
6787
"""
6888
return {
@@ -79,8 +99,33 @@ def _get_environment(self) -> Dict[str, str]:
7999
"sys_platform": sys.platform,
80100
}
81101

102+
def _get_packages(self, search_path: str) -> List[Distribution]:
103+
"""
104+
Get the list of packages based on the search path.
105+
106+
This method retrieves the list of installed packages in the specified
107+
search path. If the search path is the current executable, it uses
108+
Distribution.discover(). Otherwise, it uses distributions() with the
109+
specified path.
110+
111+
:param search_path: Path to search for packages
112+
:return: List of Distribution objects
113+
"""
114+
if sys.executable == search_path:
115+
return Distribution.discover()
116+
else:
117+
return distributions(path=[str(Path(search_path).parent / 'lib' / 'site-packages')])
118+
82119
def _process_package(self, package: Distribution) -> None:
83-
"""Process package metadata and store it in the distro dictionary."""
120+
"""
121+
Process package metadata and store it in the distro dictionary.
122+
123+
This method extracts metadata from a Distribution object and stores it
124+
in the distro dictionary. It also initializes the reverse dependencies
125+
and provided extras for the package.
126+
127+
:param package: The Distribution object to process
128+
"""
84129
meta = package.metadata
85130
name = meta['Name']
86131
version = package.version
@@ -99,12 +144,16 @@ def _process_package(self, package: Distribution) -> None:
99144
}
100145

101146
def _get_requires(self, package: Distribution) -> List[Dict[str, str]]:
102-
"""Extract and normalize requirements for a package."""
103-
# requires = list of dict with 1 level need downward
104-
# req_key = package_key requires
105-
# req_extra = extra branch needed of the package_key ('all' or '')
106-
# req_version = version needed
107-
# req_marker = marker of the requirement (if any)
147+
"""
148+
Extract and normalize requirements for a package.
149+
150+
This method parses the requirements of a package and normalizes them
151+
into a list of dictionaries. Each dictionary contains the required
152+
package key, version, extra, and marker (if any).
153+
154+
:param package: The Distribution object to extract requirements from
155+
:return: List of dictionaries containing normalized requirements
156+
"""
108157
requires = []
109158
replacements = str.maketrans({" ": " ", "[": "", "]": "", "'": "", '"': ""})
110159
further_replacements = [
@@ -136,7 +185,16 @@ def _get_requires(self, package: Distribution) -> List[Dict[str, str]]:
136185
return requires
137186

138187
def _get_provides(self, package: Distribution) -> Dict[str, None]:
139-
"""Get the list of extras provided by this package."""
188+
"""
189+
Get the list of extras provided by this package.
190+
191+
This method extracts the extras provided by a package from its requirements.
192+
It returns a dictionary where the keys are the provided extras and the values
193+
are None.
194+
195+
:param package: The Distribution object to extract provided extras from
196+
:return: Dictionary containing provided extras
197+
"""
140198
provides = {'': None}
141199
if package.requires:
142200
for req in package.requires:
@@ -147,14 +205,13 @@ def _get_provides(self, package: Distribution) -> Dict[str, None]:
147205
return provides
148206

149207
def _populate_reverse_dependencies(self) -> None:
150-
"""Add reverse dependencies to each package."""
151-
# - get all downward links in 'requires_dist' of each package
152-
# - feed the required packages 'reverse_dependencies' as a reverse dict of dict
153-
# contains =
154-
# req_key = upstream package_key
155-
# req_version = downstream package version wanted
156-
# req_extra = extra option of the demanding package that wants this dependancy
157-
# req_marker = marker of the downstream package requirement (if any)
208+
"""
209+
Add reverse dependencies to each package.
210+
211+
This method populates the reverse dependencies for each package in the
212+
distro dictionary. It iterates over the requirements of each package
213+
and adds the package as a reverse dependency to the required packages.
214+
"""
158215
for package in self.distro:
159216
for requirement in self.distro[package]["requires_dist"]:
160217
if requirement["req_key"] in self.distro:
@@ -171,12 +228,28 @@ def _populate_reverse_dependencies(self) -> None:
171228
self.distro[requirement["req_key"]]["reverse_dependencies"].append(want_add)
172229

173230
def _get_dependency_tree(self, package_name: str, extra: str = "", version_req: str = "", depth: int = 20, path: Optional[List[str]] = None, verbose: bool = False, upward: bool = False) -> List[List[str]]:
174-
"""Recursive function to build dependency tree."""
231+
"""
232+
Recursive function to build dependency tree.
233+
234+
This method builds a dependency tree for the specified package. It can
235+
build the tree for downward dependencies (default) or upward dependencies
236+
(if upward is True). The tree is built recursively up to the specified
237+
depth.
238+
239+
:param package_name: The name of the package to build the tree for
240+
:param extra: The extra to include in the dependency tree
241+
:param version_req: The version requirement for the package
242+
:param depth: The maximum depth of the dependency tree
243+
:param path: The current path in the dependency tree (used for cycle detection)
244+
:param verbose: Whether to include verbose output in the tree
245+
:param upward: Whether to build the tree for upward dependencies
246+
:return: List of lists containing the dependency tree
247+
"""
175248
path = path or []
176249
extras = extra.split(",")
177250
package_key = normalize(package_name)
178251
ret_all = []
179-
#pe = normalize(f'{package_key}[{extras}]')
252+
180253
if package_key + "[" + extra + "]" in path:
181254
print("cycle!", "->".join(path + [package_key + "[" + extra + "]"]))
182255
return [] # Return empty list to avoid further recursion
@@ -193,49 +266,60 @@ def _get_dependency_tree(self, package_name: str, extra: str = "", version_req:
193266

194267
for dependency in dependencies:
195268
if dependency["req_key"] in self.distro:
196-
if not dependency.get("req_marker") or Marker(dependency["req_marker"]).evaluate(environment=environment):
197-
next_path = path + [base_name]
198-
if upward:
199-
up_req = (dependency.get("req_marker", "").split('extra == ')+[""])[1].strip("'\"")
200-
# 2024-06-30 example of langchain <- numpy. pip.distro['numpy']['reverse_dependencies'] has:
201-
# {'req_key': 'langchain', 'req_version': '(>=1,<2)', 'req_extra': '', 'req_marker': ' python_version < "3.12"'},
202-
# {'req_key': 'langchain', 'req_version': '(>=1.26.0,<2.0.0)', 'req_extra': '', 'req_marker': ' python_version >= "3.12"'}
203-
# must be no extra dependancy, optionnal extra in the package, or provided extra per upper packages
204-
if dependency["req_key"] in self.distro and dependency["req_key"]+"["+up_req+"]" not in path: # avoids circular links on dask[array]
205-
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]
206-
or "req_marker" in dependency and extra+',' in dependency["req_extra"]+',' and Marker(dependency["req_marker"]).evaluate(environment=environment)
207-
):
269+
next_path = path + [base_name]
270+
if upward:
271+
up_req = (dependency.get("req_marker", "").split('extra == ')+[""])[1].strip("'\"")
272+
# avoids circular links on dask[array]
273+
if dependency["req_key"] in self.distro and dependency["req_key"]+"["+up_req+"]" not in path:
274+
# upward dependancy taken if:
275+
# - if extra "" demanded, and no marker from upward package: like pandas[] ==> numpy
276+
# - if an extra "array" is demanded, and indeed in the req_extra list: array,dataframe,diagnostics,distributer
277+
# - or the extra is in the upward package, like pandas[test] ==> pytest, for 'test' extra
278+
if (not dependency.get("req_marker") and extra ==""
279+
) or (extra !="" and extra==up_req and dependency["req_key"]!=package_key
280+
) or (extra !="" and "req_marker" in dependency and extra+',' in dependency["req_extra"]+','
281+
) or ("req_marker" in dependency and extra+',' in dependency["req_extra"]+',' and Marker(dependency["req_marker"]).evaluate(environment=environment)):
208282
ret += self._get_dependency_tree(
209283
dependency["req_key"],
210-
up_req, # pydask[array] going upwards will look for pydask[dataframe]
284+
up_req, # dask[array] going upwards continues as dask[dataframe]
211285
f"[requires: {package_name}"
212-
+ (
213-
"[" + dependency["req_extra"] + "]"
214-
if dependency["req_extra"] != ""
215-
else ""
216-
)
286+
+ (f"[{dependency['req_extra']}]" if dependency["req_extra"] != "" else "")
217287
+ f'{dependency["req_version"]}]',
218288
depth,
219289
next_path,
220290
verbose=verbose,
221291
upward=upward,
222292
)
223-
else:
224-
ret += self._get_dependency_tree(
225-
dependency["req_key"],
226-
dependency["req_extra"],
227-
dependency["req_version"],
228-
depth,
229-
next_path,
230-
verbose=verbose,
231-
upward=upward,
232-
)
293+
elif not dependency.get("req_marker") or Marker(dependency["req_marker"]).evaluate(environment=environment):
294+
ret += self._get_dependency_tree(
295+
dependency["req_key"],
296+
dependency["req_extra"],
297+
dependency["req_version"],
298+
depth,
299+
next_path,
300+
verbose=verbose,
301+
upward=upward,
302+
)
233303

234304
ret_all.append(ret)
235305
return ret_all
236306

237307
def down(self, pp: str = "", extra: str = "", depth: int = 20, indent: int = 5, version_req: str = "", verbose: bool = False) -> str:
238-
"""Print the downward requirements for the package or all packages."""
308+
"""
309+
Print the downward requirements for the package or all packages.
310+
311+
This method prints the downward dependencies for the specified package
312+
or all packages if pp is ".". It uses the _get_dependency_tree method
313+
to build the dependency tree and formats the output as a JSON string.
314+
315+
:param pp: The package name or "." to print dependencies for all packages
316+
:param extra: The extra to include in the dependency tree
317+
:param depth: The maximum depth of the dependency tree
318+
:param indent: The indentation level for the JSON output
319+
:param version_req: The version requirement for the package
320+
:param verbose: Whether to include verbose output in the tree
321+
:return: JSON string containing the downward dependencies
322+
"""
239323
if pp == ".":
240324
results = [self.down(one_pp, extra, depth, indent, version_req, verbose=verbose) for one_pp in sorted(self.distro)]
241325
return '\n'.join(filter(None, results))
@@ -255,7 +339,21 @@ def down(self, pp: str = "", extra: str = "", depth: int = 20, indent: int = 5,
255339
return "\n".join(lines).replace('"', "")
256340

257341
def up(self, pp: str, extra: str = "", depth: int = 20, indent: int = 5, version_req: str = "", verbose: bool = False) -> str:
258-
"""Print the upward needs for the package."""
342+
"""
343+
Print the upward needs for the package.
344+
345+
This method prints the upward dependencies for the specified package.
346+
It uses the _get_dependency_tree method to build the dependency tree
347+
and formats the output as a JSON string.
348+
349+
:param pp: The package name
350+
:param extra: The extra to include in the dependency tree
351+
:param depth: The maximum depth of the dependency tree
352+
:param indent: The indentation level for the JSON output
353+
:param version_req: The version requirement for the package
354+
:param verbose: Whether to include verbose output in the tree
355+
:return: JSON string containing the upward dependencies
356+
"""
259357
if pp == ".":
260358
results = [self.up(one_pp, extra, depth, indent, version_req, verbose) for one_pp in sorted(self.distro)]
261359
return '\n'.join(filter(None, results))
@@ -275,18 +373,41 @@ def up(self, pp: str, extra: str = "", depth: int = 20, indent: int = 5, version
275373
return "\n".join(filter(None, lines)).replace('"', "")
276374

277375
def description(self, pp: str) -> None:
278-
"""Return description of the package."""
376+
"""
377+
Return description of the package.
378+
379+
This method prints the description of the specified package.
380+
381+
:param pp: The package name
382+
"""
279383
if pp in self.distro:
280384
return print("\n".join(self.distro[pp]["description"].split(r"\n")))
281-
282-
def summary(self, pp):
283-
"""Return summary of the package."""
385+
386+
def summary(self, pp: str) -> str:
387+
"""
388+
Return summary of the package.
389+
390+
This method returns the summary of the specified package.
391+
392+
:param pp: The package name
393+
:return: The summary of the package
394+
"""
284395
if pp in self.distro:
285396
return self.distro[pp]["summary"]
286397
return ""
287398

288399
def pip_list(self, full: bool = False, max_length: int = 144) -> List[Tuple[str, Union[str, Tuple[str, str]]]]:
289-
"""List installed packages similar to pip list."""
400+
"""
401+
List installed packages similar to pip list.
402+
403+
This method lists the installed packages in a format similar to the
404+
output of the `pip list` command. If full is True, it includes the
405+
package version and summary.
406+
407+
:param full: Whether to include the package version and summary
408+
:param max_length: The maximum length for the summary
409+
:return: List of tuples containing package information
410+
"""
290411
if full:
291412
return [(p, self.distro[p]["version"], sum_up(self.distro[p]["summary"], max_length)) for p in sorted(self.distro)]
292413
else:

0 commit comments

Comments
 (0)