11#!/usr/bin/env python
22
3+ """
4+ Find Python dependencies for a given Python package after loading dependencies specified in an EasyConfig.
5+ This is intended for writing or updating PythonBundle EasyConfigs:
6+ 1. Create a EasyConfig with at least 'Python' as a dependency.
7+ When updating to a new toolchain it is a good idea to reduce the dependencies to a minimum
8+ as e.g. the new "Python" module might have different packages included.
9+ 2. Run this script
10+ 3. For each dependency found by this script search existing EasyConfigs for ones providing that Python package.
11+ E.g many are contained in Python-bundle-PyPI. Some can be updated from an earlier toolchain.
12+ 4. Add those EasyConfigs as dependencies to your new EasyConfig.
13+ 5. Rerun this script so it takes the newly provided packages into account.
14+ You can do steps 3-5 iteratively adding EasyConfig-dependencies one-by-one.
15+ 6. Finally you copy the packages found by this script as "exts_list" into the new EasyConfig.
16+ You usually want the list printed as "in install order", the format is already suitable to be copied as-is.
17+ """
18+
319import argparse
420import json
521import os
824import subprocess
925import sys
1026import tempfile
27+ import textwrap
1128from contextlib import contextmanager
1229from pprint import pprint
1330try :
1431 import pkg_resources
1532except ImportError as e :
16- print ('pkg_resources could not be imported: %s \n You might need to install setuptools!' % e )
33+ print (f 'pkg_resources could not be imported: { e } \n You might need to install setuptools!' )
1734 sys .exit (1 )
1835
1936try :
2239 _canonicalize_regex = re .compile (r"[-_.]+" )
2340
2441 def canonicalize_name (name ):
42+ """Fallback if the import doesn't work with same behavior."""
2543 return _canonicalize_regex .sub ("-" , name ).lower ()
2644
2745
@@ -36,16 +54,16 @@ def temporary_directory(*args, **kwargs):
3654
3755
3856def extract_pkg_name (package_spec ):
39- return re .split ('<|>|=|~' , args .package , 1 )[0 ]
57+ """Get the package name from a specification such as 'package>=3.42'"""
58+ return re .split ('<|>|=|~' , package_spec , 1 )[0 ]
4059
4160
42- def can_run (cmd , argument ):
61+ def can_run (cmd , * arguments ):
4362 """Check if the given cmd and argument can be run successfully"""
44- with open (os .devnull , 'w' ) as FNULL :
45- try :
46- return subprocess .call ([cmd , argument ], stdout = FNULL , stderr = subprocess .STDOUT ) == 0
47- except (subprocess .CalledProcessError , OSError ):
48- return False
63+ try :
64+ return subprocess .call ([cmd , * arguments ], stdout = subprocess .DEVNULL , stderr = subprocess .STDOUT ) == 0
65+ except (subprocess .CalledProcessError , OSError ):
66+ return False
4967
5068
5169def run_shell_cmd (arguments , action_desc , capture_stderr = True , ** kwargs ):
@@ -59,13 +77,13 @@ def run_shell_cmd(arguments, action_desc, capture_stderr=True, **kwargs):
5977 if p .returncode != 0 :
6078 if err :
6179 err = "\n STDERR:\n " + err
62- raise RuntimeError ('Failed to %s: %s%s' % ( action_desc , out , err ) )
80+ raise RuntimeError (f 'Failed to { action_desc } : { out } { err } ' )
6381 return out
6482
6583
6684def run_in_venv (cmd , venv_path , action_desc ):
6785 """Run the given command in the virtualenv at the given path"""
68- cmd = 'source %s /bin/activate && %s' % ( venv_path , cmd )
86+ cmd = f 'source { venv_path } /bin/activate && { cmd } '
6987 return run_shell_cmd (cmd , action_desc , shell = True , executable = '/bin/bash' )
7088
7189
@@ -78,17 +96,19 @@ def get_dep_tree(package_spec, verbose):
7896 venv_dir = os .path .join (tmp_dir , 'venv' )
7997 if verbose :
8098 print ('Creating virtualenv at ' + venv_dir )
81- run_shell_cmd (['virtualenv' , '--system-site-packages' , venv_dir ], action_desc = 'create virtualenv' )
99+ run_shell_cmd (
100+ [sys .executable , '-m' , 'venv' , '--system-site-packages' , venv_dir ], action_desc = 'create virtualenv'
101+ )
82102 if verbose :
83103 print ('Updating pip in virtualenv' )
84104 run_in_venv ('pip install --upgrade pip' , venv_dir , action_desc = 'update pip' )
85105 if verbose :
86- print ('Installing %s into virtualenv' % package_spec )
87- out = run_in_venv ('pip install "%s"' % package_spec , venv_dir , action_desc = 'install ' + package_spec )
88- print ('%s installed: %s' % ( package_spec , out ) )
106+ print (f 'Installing { package_spec } into virtualenv' )
107+ out = run_in_venv (f 'pip install "{ package_spec } "' , venv_dir , action_desc = 'install ' + package_spec )
108+ print (f' { package_spec } installed: { out } ' )
89109 # install pipdeptree, figure out dependency tree for installed package
90110 run_in_venv ('pip install pipdeptree' , venv_dir , action_desc = 'install pipdeptree' )
91- dep_tree = run_in_venv ('pipdeptree -j -p "%s"' % package_name ,
111+ dep_tree = run_in_venv (f 'pipdeptree -j -p "{ package_name } "' ,
92112 venv_dir , action_desc = 'collect dependencies' )
93113 return json .loads (dep_tree )
94114
@@ -108,26 +128,27 @@ def find_deps(pkgs, dep_tree):
108128 for orig_pkg in cur_pkgs :
109129 count += 1
110130 if count > MAX_PACKAGES :
111- raise RuntimeError ("Aborting after checking %s packages. Possibly cycle detected!" % MAX_PACKAGES )
131+ raise RuntimeError (f "Aborting after checking { MAX_PACKAGES } packages. Possibly cycle detected!" )
112132 pkg = canonicalize_name (orig_pkg )
113133 matching_entries = [entry for entry in dep_tree
114134 if pkg in (entry ['package' ]['package_name' ], entry ['package' ]['key' ])]
115135 if not matching_entries :
116136 matching_entries = [entry for entry in dep_tree
117137 if orig_pkg in (entry ['package' ]['package_name' ], entry ['package' ]['key' ])]
118138 if not matching_entries :
119- raise RuntimeError ("Found no installed package for '%s ' in %s" % ( pkg , dep_tree ) )
139+ raise RuntimeError (f "Found no installed package for '{ pkg } ' in { dep_tree } " )
120140 if len (matching_entries ) > 1 :
121- raise RuntimeError ("Found multiple installed packages for '%s ' in %s" % ( pkg , dep_tree ) )
141+ raise RuntimeError (f "Found multiple installed packages for '{ pkg } ' in { dep_tree } " )
122142 entry = matching_entries [0 ]
123- res .append (( entry ['package' ][ 'package_name' ], entry [ 'package' ][ 'installed_version' ]) )
143+ res .append (entry ['package' ])
124144 # Add dependencies to list of packages to check next
125145 # Could call this function recursively but that might exceed the max recursion depth
126146 next_pkgs .update (dep ['package_name' ] for dep in entry ['dependencies' ])
127147 return res
128148
129149
130150def print_deps (package , verbose ):
151+ """Print dependencies of the given package that are not installed yet in a format usable as 'exts_list'"""
131152 if verbose :
132153 print ('Getting dep tree of ' + package )
133154 dep_tree = get_dep_tree (package , verbose )
@@ -144,82 +165,92 @@ def print_deps(package, verbose):
144165 res = []
145166 handled = set ()
146167 for dep in reversed (deps ):
147- if dep not in handled :
148- handled .add (dep )
149- if dep [0 ] in installed_modules :
168+ # Tuple as we need it for exts_list
169+ dep_entry = (dep ['package_name' ], dep ['installed_version' ])
170+ if dep_entry not in handled :
171+ handled .add (dep_entry )
172+ # Need to check for key and package_name as naming is not consistent. E.g.:
173+ # "PyQt5-sip": 'key': 'pyqt5-sip', 'package_name': 'PyQt5-sip'
174+ # "jupyter-core": 'key': 'jupyter-core', 'package_name': 'jupyter_core'
175+ if dep ['key' ] in installed_modules or dep ['package_name' ] in installed_modules :
150176 if verbose :
151- print ("Skipping installed module '%s'" % dep [0 ] )
177+ print (f "Skipping installed module '{ dep ['package_name' ] } '" )
152178 else :
153- res .append (dep )
179+ res .append (dep_entry )
154180
155181 print ("List of dependencies in (likely) install order:" )
156182 pprint (res , indent = 4 )
157183 print ("Sorted list of dependencies:" )
158184 pprint (sorted (res ), indent = 4 )
159185
160186
161- examples = [
162- 'Example usage with EasyBuild (after installing dependency modules):' ,
163- '\t ' + sys .argv [0 ] + ' --ec TensorFlow-2.3.4.eb tensorflow==2.3.4' ,
164- 'Which is the same as:' ,
165- '\t ' + ' && ' .join (['eb TensorFlow-2.3.4.eb --dump-env' ,
166- 'source TensorFlow-2.3.4.env' ,
167- sys .argv [0 ] + ' tensorflow==2.3.4' ,
168- ]),
169- ]
170- parser = argparse .ArgumentParser (
171- description = 'Find dependencies of Python packages by installing it in a temporary virtualenv. ' ,
172- epilog = '\n ' .join (examples ),
173- formatter_class = argparse .RawDescriptionHelpFormatter
174- )
175- parser .add_argument ('package' , metavar = 'python-pkg-spec' ,
176- help = 'Python package spec, e.g. tensorflow==2.3.4' )
177- parser .add_argument ('--ec' , metavar = 'easyconfig' , help = 'EasyConfig to use as the build environment. '
178- 'You need to have dependency modules installed already!' )
179- parser .add_argument ('--verbose' , help = 'Verbose output' , action = 'store_true' )
180- args = parser .parse_args ()
181-
182- if args .ec :
183- if not can_run ('eb' , '--version' ):
184- print ('EasyBuild not found or executable. Make sure it is in your $PATH when using --ec!' )
185- sys .exit (1 )
186- if args .verbose :
187- print ('Checking with EasyBuild for missing dependencies' )
188- missing_dep_out = run_shell_cmd (['eb' , args .ec , '--missing' ],
189- capture_stderr = False ,
190- action_desc = 'Get missing dependencies' )
191- excluded_dep = '(%s)' % os .path .basename (args .ec )
192- missing_deps = [dep for dep in missing_dep_out .split ('\n ' )
193- if dep .startswith ('*' ) and excluded_dep not in dep
194- ]
195- if missing_deps :
196- print ('You need to install all modules on which %s depends first!' % args .ec )
197- print ('\n \t ' .join (['Missing:' ] + missing_deps ))
198- sys .exit (1 )
199-
200- # If the --ec argument is a (relative) existing path make it absolute so we can find it after the chdir
201- ec_arg = os .path .abspath (args .ec ) if os .path .exists (args .ec ) else args .ec
202- with temporary_directory () as tmp_dir :
203- old_dir = os .getcwd ()
204- os .chdir (tmp_dir )
205- if args .verbose :
206- print ('Running EasyBuild to get build environment' )
207- run_shell_cmd (['eb' , ec_arg , '--dump-env' , '--force' ], action_desc = 'Dump build environment' )
208- os .chdir (old_dir )
187+ def main ():
188+ """Entrypoint of the script"""
189+ examples = textwrap .dedent (f"""
190+ Example usage with EasyBuild (after installing dependency modules):
191+ { sys .argv [0 ]} --ec TensorFlow-2.3.4.eb tensorflow==2.3.4
192+ Which is the same as:
193+ eb TensorFlow-2.3.4.eb --dump-env && source TensorFlow-2.3.4.env && { sys .argv [0 ]} tensorflow==2.3.4
194+ Using the '--ec' parameter is recommended as the latter requires manually updating the .env file
195+ after each change to the EasyConfig.
196+ """ )
197+ parser = argparse .ArgumentParser (
198+ description = 'Find dependencies of Python packages by installing it in a temporary virtualenv. ' ,
199+ epilog = '\n ' .join (examples ),
200+ formatter_class = argparse .RawDescriptionHelpFormatter
201+ )
202+ parser .add_argument ('package' , metavar = 'python-pkg-spec' ,
203+ help = 'Python package spec, e.g. tensorflow==2.3.4' )
204+ parser .add_argument ('--ec' , metavar = 'easyconfig' , help = 'EasyConfig to use as the build environment. '
205+ 'You need to have dependency modules installed already!' )
206+ parser .add_argument ('--verbose' , help = 'Verbose output' , action = 'store_true' )
207+ args = parser .parse_args ()
209208
210- cmd = "source %s/*.env && python %s '%s'" % (tmp_dir , sys .argv [0 ], args .package )
209+ if args .ec :
210+ if not can_run ('eb' , '--version' ):
211+ print ('EasyBuild not found or executable. Make sure it is in your $PATH when using --ec!' )
212+ sys .exit (1 )
211213 if args .verbose :
212- cmd += ' --verbose'
213- print ('Restarting script in new build environment' )
214-
215- out = run_shell_cmd (cmd , action_desc = 'Run in new environment' , shell = True , executable = '/bin/bash' )
216- print (out )
217- else :
218- if not can_run ('virtualenv' , '--version' ):
219- print ('Virtualenv not found or executable. ' +
220- 'Make sure it is installed (e.g. in the currently loaded Python module)!' )
221- sys .exit (1 )
222- if 'PIP_PREFIX' in os .environ :
223- print ("$PIP_PREFIX is set. Unsetting it as it doesn't work well with virtualenv." )
224- del os .environ ['PIP_PREFIX' ]
225- print_deps (args .package , args .verbose )
214+ print ('Checking with EasyBuild for missing dependencies' )
215+ missing_dep_out = run_shell_cmd (['eb' , args .ec , '--missing' ],
216+ capture_stderr = False ,
217+ action_desc = 'Get missing dependencies' )
218+ excluded_dep = f'({ os .path .basename (args .ec )} )'
219+ missing_deps = [dep for dep in missing_dep_out .split ('\n ' )
220+ if dep .startswith ('*' ) and excluded_dep not in dep
221+ ]
222+ if missing_deps :
223+ print (f'You need to install all modules on which { args .ec } depends first!' )
224+ print ('\n \t ' .join (['Missing:' ] + missing_deps ))
225+ sys .exit (1 )
226+
227+ # If the --ec argument is a (relative) existing path make it absolute so we can find it after the chdir
228+ ec_arg = os .path .abspath (args .ec ) if os .path .exists (args .ec ) else args .ec
229+ with temporary_directory () as tmp_dir :
230+ old_dir = os .getcwd ()
231+ os .chdir (tmp_dir )
232+ if args .verbose :
233+ print ('Running EasyBuild to get build environment' )
234+ run_shell_cmd (['eb' , ec_arg , '--dump-env' , '--force' ], action_desc = 'Dump build environment' )
235+ os .chdir (old_dir )
236+
237+ cmd = f"source { tmp_dir } /*.env && python { sys .argv [0 ]} '{ args .package } '"
238+ if args .verbose :
239+ cmd += ' --verbose'
240+ print ('Restarting script in new build environment' )
241+
242+ out = run_shell_cmd (cmd , action_desc = 'Run in new environment' , shell = True , executable = '/bin/bash' )
243+ print (out )
244+ else :
245+ if not can_run (sys .executable , '-m' , 'venv' , '-h' ):
246+ print ("'venv' module not found. This should be available in Python 3.3+." )
247+ sys .exit (1 )
248+ if 'PIP_PREFIX' in os .environ :
249+ print ("$PIP_PREFIX is set. Unsetting it as it doesn't work well with virtualenv." )
250+ del os .environ ['PIP_PREFIX' ]
251+ os .environ ['PYTHONNOUSERSITE' ] = '1'
252+ print_deps (args .package , args .verbose )
253+
254+
255+ if __name__ == "__main__" :
256+ main ()
0 commit comments