1010import re
1111import platform
1212import os
13+ import logging
1314from functools import lru_cache
1415from collections import OrderedDict
1516from typing import Dict , List , Optional , Tuple , Union
1617from pip ._vendor .packaging .markers import Marker
1718from importlib .metadata import Distribution , distributions
1819from pathlib import Path
1920
21+ logging .basicConfig (level = logging .INFO )
22+ logger = logging .getLogger (__name__ )
23+
24+ class PipDataError (Exception ):
25+ """Custom exception for PipData related errors."""
26+ pass
2027
2128def sum_up (text : str , max_length : int = 144 , stop_at : str = ". " ) -> str :
2229 """Summarize text to fit within max_length, ending at last complete sentence."""
2330 summary = (text + os .linesep ).splitlines ()[0 ]
2431 if len (summary ) <= max_length :
25- return summary
32+ return summary
2633 if stop_at and stop_at in summary [:max_length ]:
2734 return summary [:summary .rfind (stop_at , 0 , max_length )] + stop_at .rstrip ()
2835 return summary [:max_length ].rstrip ()
2936
30-
3137class PipData :
3238 """Manages package metadata and dependency relationships in a Python environment."""
3339
@@ -40,21 +46,19 @@ def __init__(self, target: Optional[str] = None):
4046 self .distro : Dict [str , Dict ] = {}
4147 self .raw : Dict [str , Dict ] = {}
4248 self .environment = self ._get_environment ()
43-
4449 try :
4550 packages = self ._get_packages (target or sys .executable )
4651 self ._process_packages (packages )
4752 self ._populate_reverse_dependencies ()
4853 except Exception as e :
49- raise RuntimeError (f"Failed to initialize package data: { str (e )} " ) from e
50-
54+ raise PipDataError (f"Failed to initialize package data: { str (e )} " ) from e
5155
5256 @staticmethod
5357 @lru_cache (maxsize = None )
5458 def normalize (name : str ) -> str :
5559 """Normalize package name per PEP 503."""
5660 return re .sub (r"[-_.]+" , "-" , name ).lower ()
57-
61+
5862 def _get_environment (self ) -> Dict [str , str ]:
5963 """Collect system and Python environment details."""
6064 return {
@@ -99,20 +103,10 @@ def _process_packages(self, packages: List[Distribution]) -> None:
99103 "provided" : {'' : None } # Placeholder for extras provided by this package
100104 }
101105 except Exception as e :
102- print (f"Warning: Failed to process package { name } : { str (e )} " , file = sys .stderr )
103-
106+ logger .warning (f"Failed to process package { name } : { str (e )} " , exc_info = True )
104107
105108 def _get_requires (self , package : Distribution ) -> List [Dict [str , str ]]:
106- """
107- Extract and normalize requirements for a package.
108-
109- This method parses the requirements of a package and normalizes them
110- into a list of dictionaries. Each dictionary contains the required
111- package key, version, extra, and marker (if any).
112-
113- :param package: The Distribution object to extract requirements from
114- :return: List of dictionaries containing normalized requirements
115- """
109+ """Extract and normalize requirements for a package."""
116110 requires = []
117111 replacements = str .maketrans ({" " : " " , "[" : "" , "]" : "" , "'" : "" , '"' : "" })
118112 further_replacements = [
@@ -155,11 +149,7 @@ def _get_provides(self, package: Distribution) -> Dict[str, None]:
155149 return provides
156150
157151 def _populate_reverse_dependencies (self ) -> None :
158- """Populate reverse dependencies.
159-
160- It iterates over the requirements of each package
161- and adds the package as a reverse dependency to the required packages.
162- """
152+ """Populate reverse dependencies."""
163153 for pkg_key , pkg_data in self .distro .items ():
164154 for req in pkg_data ["requires_dist" ]:
165155 target_key = req ["req_key" ]
@@ -173,32 +163,16 @@ def _populate_reverse_dependencies(self) -> None:
173163 self .distro [target_key ]["reverse_dependencies" ].append (rev_dep )
174164
175165 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 ]]:
176- """
177- Recursive function to build dependency tree.
178-
179- This method builds a dependency tree for the specified package. It can
180- build the tree for downward dependencies (default) or upward dependencies
181- (if upward is True). The tree is built recursively up to the specified
182- depth.
183-
184- :param package_name: The name of the package to build the tree for
185- :param extra: The extra to include in the dependency tree
186- :param version_req: The version requirement for the package
187- :param depth: The maximum depth of the dependency tree
188- :param path: The current path in the dependency tree (used for cycle detection)
189- :param verbose: Whether to include verbose output in the tree
190- :param upward: Whether to build the tree for upward dependencies
191- :return: List of lists containing the dependency tree
192- """
166+ """Recursive function to build dependency tree."""
193167 path = path or []
194168 extras = extra .split ("," )
195169 pkg_key = self .normalize (package_name )
196170 ret_all = []
197171
198172 full_name = f"{ package_name } [{ extra } ]" if extra else package_name
199173 if full_name in path :
200- print (f"Cycle detected: { ' -> ' .join (path + [full_name ])} " )
201- return [] # Return empty list to avoid further recursion
174+ logger . warning (f"Cycle detected: { ' -> ' .join (path + [full_name ])} " )
175+ return []
202176
203177 pkg_data = self .distro [pkg_key ]
204178 if pkg_data and len (path ) <= depth :
@@ -215,19 +189,22 @@ def _get_dependency_tree(self, package_name: str, extra: str = "", version_req:
215189 next_path = path + [base_name ]
216190 if upward :
217191 up_req = (dependency .get ("req_marker" , "" ).split ('extra == ' )+ ["" ])[1 ].strip ("'\" " )
218- # avoids circular links on dask[array]
219192 if dependency ["req_key" ] in self .distro and dependency ["req_key" ]+ "[" + up_req + "]" not in path :
220193 # upward dependancy taken if:
221194 # - if extra "" demanded, and no marker from upward package: like pandas[] ==> numpy
222195 # - or the extra is in the upward package, like pandas[test] ==> pytest, for 'test' extra
223196 # - or an extra "array" is demanded, and indeed in the req_extra list: array,dataframe,diagnostics,distributer
224- if (not dependency .get ("req_marker" ) and extra == ""
225- ) or ("req_marker" in dependency and extra == up_req and dependency ["req_key" ]!= pkg_key and Marker (dependency ["req_marker" ]).evaluate (environment = environment )
226- ) or ("req_marker" in dependency and extra != "" and extra + ',' in dependency ["req_extra" ]+ ',' and Marker (dependency ["req_marker" ]).evaluate (environment = environment | {"extra" : up_req })
227- ):
197+ if (not dependency .get ("req_marker" ) and extra == "" ) or \
198+ ("req_marker" in dependency and extra == up_req and \
199+ dependency ["req_key" ] != pkg_key and \
200+ Marker (dependency ["req_marker" ]).evaluate (environment = environment )) or \
201+ ("req_marker" in dependency and extra != "" and \
202+ extra + ',' in dependency ["req_extra" ] + ',' and \
203+ Marker (dependency ["req_marker" ]).evaluate (environment = environment | {"extra" : up_req })):
204+ # IA risk error: # dask[array] go upwards as dask[dataframe], so {"extra": up_req} , not {"extra": extra}
228205 ret += self ._get_dependency_tree (
229206 dependency ["req_key" ],
230- up_req , # dask[array] going upwards continues as dask[dataframe]
207+ up_req ,
231208 f"[requires: { package_name } "
232209 + (f"[{ dependency ['req_extra' ]} ]" if dependency ["req_extra" ] != "" else "" )
233210 + f'{ dependency ["req_version" ]} ]' ,
@@ -251,16 +228,7 @@ def _get_dependency_tree(self, package_name: str, extra: str = "", version_req:
251228 return ret_all
252229
253230 def down (self , pp : str = "" , extra : str = "" , depth : int = 20 , indent : int = 5 , version_req : str = "" , verbose : bool = False ) -> str :
254- """
255- Generate downward dependency tree as formatted string.
256-
257- :param pp: The package name or "." to print dependencies for all packages
258- :param extra: The extra to include in the dependency tree
259- :param depth: The maximum depth of the dependency tree
260- :param indent: The indentation level for the JSON output
261- :param version_req: The version requirement for the package
262- :param verbose: Whether to include verbose output in the tree
263- """
231+ """Generate downward dependency tree as formatted string."""
264232 if pp == "." :
265233 results = [self .down (p , extra , depth , indent , version_req , verbose = verbose ) for p in sorted (self .distro )]
266234 return '\n ' .join (filter (None , results ))
@@ -270,27 +238,17 @@ def down(self, pp: str = "", extra: str = "", depth: int = 20, indent: int = 5,
270238 results = [self .down (pp , one_extra , depth , indent , version_req , verbose = verbose )
271239 for one_extra in sorted (self .distro [pp ]["provides" ])]
272240 return '\n ' .join (filter (None , results ))
273- return "" # Handle cases where extra is "." and package_name is not found.
241+ return ""
274242
275243 if pp not in self .distro :
276- return "" # Handle cases where package_name is not found.
244+ return ""
277245
278246 rawtext = json .dumps (self ._get_dependency_tree (pp , extra , version_req , depth , verbose = verbose ), indent = indent )
279247 lines = [l for l in rawtext .split ("\n " ) if len (l .strip ()) > 2 ]
280248 return "\n " .join (lines ).replace ('"' , "" )
281249
282- def up (self , pp : str , extra : str = "" , depth : int = 20 , indent : int = 5 ,
283- version_req : str = "" , verbose : bool = False ) -> str :
284- """
285- Generate upward dependency tree as formatted string.
286-
287- :param pp: The package name
288- :param extra: The extra to include in the dependency tree
289- :param depth: The maximum depth of the dependency tree
290- :param indent: The indentation level for the JSON output
291- :param version_req: The version requirement for the package
292- :param verbose: Whether to include verbose output in the tree
293- """
250+ def up (self , pp : str , extra : str = "" , depth : int = 20 , indent : int = 5 , version_req : str = "" , verbose : bool = False ) -> str :
251+ """Generate upward dependency tree as formatted string."""
294252 if pp == "." :
295253 results = [self .up (p , extra , depth , indent , version_req , verbose ) for p in sorted (self .distro )]
296254 return '\n ' .join (filter (None , results ))
0 commit comments