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+
48import json
59import sys
610import 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+
1924def 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-
2940class 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