1212import os
1313from collections import OrderedDict
1414from typing import Dict , List , Optional , Tuple , Union
15- from pip ._vendor .packaging .markers import Marker , InvalidMarker
15+ from pip ._vendor .packaging .markers import Marker
1616from importlib .metadata import Distribution , distributions
1717from pathlib import Path
1818
1919def 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
2332def 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:
3952class 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