-
Notifications
You must be signed in to change notification settings - Fork 311
run pip check only once for PythonBundle
#3432
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 4 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
a598091
run `pip check` only once for PythonBundle
Flamefire 0177e4b
Fix failing sanity check due to (fake) module not being loaded
Flamefire 4506c50
tweak det_installed_python_packages function provided with PythonPack…
boegel a916a98
tweak run_pip_check function provided with PythonPackage easyblock + …
boegel 12febcc
add dedicated test for run_pip_check
boegel File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -51,7 +51,7 @@ | |
| from easybuild.tools.config import build_option, PYTHONPATH, EBPYTHONPREFIXES | ||
| from easybuild.tools.filetools import change_dir, mkdir, remove_dir, symlink, which | ||
| from easybuild.tools.modules import ModEnvVarType, get_software_root | ||
| from easybuild.tools.run import run_shell_cmd, subprocess_popen_text | ||
| from easybuild.tools.run import run_shell_cmd | ||
| from easybuild.tools.utilities import nub | ||
| from easybuild.tools.hooks import CONFIGURE_STEP, BUILD_STEP, TEST_STEP, INSTALL_STEP | ||
|
|
||
|
|
@@ -398,6 +398,108 @@ def symlink_dist_site_packages(install_dir, pylibdirs): | |
| symlink(dist_pkgs, site_pkgs_path, use_abspath_source=False) | ||
|
|
||
|
|
||
| def det_installed_python_packages(names_only=True, python_cmd=None): | ||
| """ | ||
| Return list of Python packages that are installed | ||
|
|
||
| Note that the names are reported by pip and might be different to the name that need to be used to import it. | ||
|
|
||
| :param names_only: boolean indicating whether only names or full info from `pip list` should be returned | ||
| :param python_cmd: Python command to use (if None, 'python' is used) | ||
| """ | ||
| log = fancylogger.getLogger('det_installed_python_packages', fname=False) | ||
|
|
||
| if python_cmd is None: | ||
| python_cmd = 'python' | ||
|
|
||
| # Check installed Python packages | ||
| cmd = ' '.join([ | ||
| python_cmd, '-m', 'pip', | ||
| 'list', | ||
| '--isolated', | ||
| '--disable-pip-version-check', | ||
| '--format', 'json', | ||
| ]) | ||
| res = run_shell_cmd(cmd, fail_on_error=False, hidden=True) | ||
| if res.exit_code: | ||
| raise EasyBuildError(f'Failed to determine installed python packages: {res.output}') | ||
|
|
||
| # only check stdout, not stderr which might contain user facing warnings | ||
| log.info(f'Got list of installed Python packages: {res.output}') | ||
| pkgs = json.loads(res.output.strip()) | ||
| return [pkg['name'] for pkg in pkgs] if names_only else pkgs | ||
|
|
||
|
|
||
| def run_pip_check(python_cmd=None, unversioned_packages=None): | ||
| """ | ||
| Check installed Python packages using 'pip check' | ||
|
|
||
| :param unversioned_packages: list of Python packages to exclude in the version existence check | ||
| :param python_cmd: Python command to use (if None, 'python' is used) | ||
| """ | ||
| log = fancylogger.getLogger('det_installed_python_packages', fname=False) | ||
|
|
||
| if python_cmd is None: | ||
| python_cmd = 'python' | ||
| if unversioned_packages is None: | ||
| unversioned_packages = [] | ||
|
|
||
| pip_check_cmd = f"{python_cmd} -m pip check" | ||
|
|
||
| pip_version = det_pip_version(python_cmd=python_cmd) | ||
| if not pip_version: | ||
| raise EasyBuildError("Failed to determine pip version!") | ||
| min_pip_version = LooseVersion('9.0.0') | ||
| if LooseVersion(pip_version) < min_pip_version: | ||
| raise EasyBuildError(f"pip >= {min_pip_version} is required for '{pip_check_cmd}', found {pip_version}") | ||
|
|
||
| pip_check_errors = [] | ||
|
|
||
| res = run_shell_cmd(pip_check_cmd, fail_on_error=False) | ||
| if res.exit_code: | ||
| pip_check_errors.append(f"`{pip_check_cmd}` failed:\n{res.output}") | ||
| else: | ||
| log.info(f"`{pip_check_cmd}` passed successfully") | ||
|
|
||
| # Also check for a common issue where the package version shows up as 0.0.0 often caused | ||
| # by using setup.py as the installation method for a package which is released as a generic wheel | ||
| # named name-version-py2.py3-none-any.whl. `tox` creates those from version controlled source code | ||
| # so it will contain a version, but the raw tar.gz does not. | ||
| pkgs = det_installed_python_packages(names_only=False, python_cmd=python_cmd) | ||
| faulty_version = '0.0.0' | ||
| faulty_pkg_names = [pkg['name'] for pkg in pkgs if pkg['version'] == faulty_version] | ||
|
|
||
| for unversioned_package in unversioned_packages: | ||
| try: | ||
| faulty_pkg_names.remove(unversioned_package) | ||
| log.debug(f"Excluding unversioned package '{unversioned_package}' from check") | ||
| except ValueError: | ||
| try: | ||
| version = next(pkg['version'] for pkg in pkgs if pkg['name'] == unversioned_package) | ||
| except StopIteration: | ||
| msg = f"Package '{unversioned_package}' in unversioned_packages was not found in " | ||
| msg += "the installed packages. Check that the name from `python -m pip list` is used " | ||
| msg += "which may be different than the module name." | ||
| else: | ||
| msg = f"Package '{unversioned_package}' in unversioned_packages has a version of {version} " | ||
| msg += "which is valid. Please remove it from unversioned_packages." | ||
| pip_check_errors.append(msg) | ||
|
|
||
| log.info("Found %s invalid packages out of %s packages", len(faulty_pkg_names), len(pkgs)) | ||
| if faulty_pkg_names: | ||
| faulty_pkg_names_str = '\n'.join(faulty_pkg_names) | ||
| msg = "The following Python packages were likely not installed correctly because they show a " | ||
| msg += f"version of '{faulty_version}':\n{faulty_pkg_names_str}\n" | ||
| msg += "This may be solved by using a *-none-any.whl file as the source instead. " | ||
| msg += "See e.g. the SOURCE*_WHL templates.\n" | ||
| msg += "Otherwise you could check if the package provides a version at all or if e.g. poetry is " | ||
| msg += "required (check the source for a pyproject.toml and see PEP517 for details on that)." | ||
| pip_check_errors.append(msg) | ||
|
|
||
| if pip_check_errors: | ||
| raise EasyBuildError('\n'.join(pip_check_errors)) | ||
|
|
||
|
|
||
| class PythonPackage(ExtensionEasyBlock): | ||
| """Builds and installs a Python package, and provides a dedicated module file.""" | ||
|
|
||
|
|
@@ -611,25 +713,7 @@ def get_installed_python_packages(self, names_only=True, python_cmd=None): | |
| """ | ||
| if python_cmd is None: | ||
| python_cmd = self.python_cmd | ||
| # Check installed python packages but only check stdout, not stderr which might contain user facing warnings | ||
| cmd_list = [python_cmd, '-m', 'pip', 'list', '--isolated', '--disable-pip-version-check', | ||
| '--format', 'json'] | ||
| full_cmd = ' '.join(cmd_list) | ||
| self.log.info("Running command '%s'" % full_cmd) | ||
| proc = subprocess_popen_text(cmd_list, env=os.environ) | ||
| (stdout, stderr) = proc.communicate() | ||
| ec = proc.returncode | ||
| msg = "Command '%s' returned with %s: stdout: %s; stderr: %s" % (full_cmd, ec, stdout, stderr) | ||
| if ec: | ||
| self.log.info(msg) | ||
| raise EasyBuildError('Failed to determine installed python packages: %s', stderr) | ||
|
|
||
| self.log.debug(msg) | ||
| pkgs = json.loads(stdout.strip()) | ||
| if names_only: | ||
| return [pkg['name'] for pkg in pkgs] | ||
| else: | ||
| return pkgs | ||
| return det_installed_python_packages(names_only=names_only, python_cmd=python_cmd) | ||
|
|
||
| def using_pip_install(self): | ||
| """ | ||
|
|
@@ -1019,10 +1103,10 @@ def sanity_check_step(self, *args, **kwargs): | |
| # load module early ourselves rather than letting parent sanity_check_step method do so, | ||
| # since custom actions taken below require that environment is set up properly already | ||
| # (especially when using --sanity-check-only) | ||
| if hasattr(self, 'sanity_check_module_loaded') and not self.sanity_check_module_loaded: | ||
| if not self.sanity_check_module_loaded: | ||
| extension = self.is_extension or kwargs.get('extension', False) | ||
| extra_modules = kwargs.get('extra_modules', None) | ||
| self.fake_mod_data = self.sanity_check_load_module(extension=extension, extra_modules=extra_modules) | ||
| self.sanity_check_load_module(extension=extension, extra_modules=extra_modules) | ||
|
|
||
| # don't add user site directory to sys.path (equivalent to python -s) | ||
| # see https://www.python.org/dev/peps/pep-0370/; | ||
|
|
@@ -1076,78 +1160,31 @@ def sanity_check_step(self, *args, **kwargs): | |
| exts_filter = (orig_exts_filter[0].replace('python', self.python_cmd), orig_exts_filter[1]) | ||
| kwargs.update({'exts_filter': exts_filter}) | ||
|
|
||
| if self.cfg.get('sanity_pip_check', True): | ||
| pip_version = det_pip_version(python_cmd=python_cmd) | ||
|
|
||
| if pip_version: | ||
| pip_check_command = "%s -m pip check" % python_cmd | ||
|
|
||
| if LooseVersion(pip_version) >= LooseVersion('9.0.0'): | ||
|
|
||
| if not self.is_extension: | ||
| # for stand-alone Python package installations (not part of a bundle of extensions), | ||
| # the (fake or real) module file must be loaded at this point, | ||
| # otherwise the Python package being installed is not "in view", | ||
| # and we will overlook missing dependencies... | ||
| loaded_modules = [x['mod_name'] for x in self.modules_tool.list()] | ||
| if self.short_mod_name not in loaded_modules: | ||
| self.log.debug("Currently loaded modules: %s", loaded_modules) | ||
| raise EasyBuildError("%s module is not loaded, this should never happen...", | ||
| self.short_mod_name) | ||
|
|
||
| pip_check_errors = [] | ||
|
|
||
| res = run_shell_cmd(pip_check_command, fail_on_error=False) | ||
| pip_check_msg = res.output | ||
| if res.exit_code: | ||
| pip_check_errors.append('`%s` failed:\n%s' % (pip_check_command, pip_check_msg)) | ||
| else: | ||
| self.log.info('`%s` completed successfully' % pip_check_command) | ||
|
|
||
| # Also check for a common issue where the package version shows up as 0.0.0 often caused | ||
| # by using setup.py as the installation method for a package which is released as a generic wheel | ||
| # named name-version-py2.py3-none-any.whl. `tox` creates those from version controlled source code | ||
| # so it will contain a version, but the raw tar.gz does not. | ||
| pkgs = self.get_installed_python_packages(names_only=False, python_cmd=python_cmd) | ||
| faulty_version = '0.0.0' | ||
| faulty_pkg_names = [pkg['name'] for pkg in pkgs if pkg['version'] == faulty_version] | ||
|
|
||
| for unversioned_package in self.cfg.get('unversioned_packages', []): | ||
| try: | ||
| faulty_pkg_names.remove(unversioned_package) | ||
| self.log.debug('Excluding unversioned package %s from check', unversioned_package) | ||
| except ValueError: | ||
| try: | ||
| version = next(pkg['version'] for pkg in pkgs if pkg['name'] == unversioned_package) | ||
| except StopIteration: | ||
| msg = ('Package %s in unversioned_packages was not found in the installed packages. ' | ||
| 'Check that the name from `python -m pip list` is used which may be different ' | ||
| 'than the module name.' % unversioned_package) | ||
| else: | ||
| msg = ('Package %s in unversioned_packages has a version of %s which is valid. ' | ||
| 'Please remove it from unversioned_packages.' % (unversioned_package, version)) | ||
| pip_check_errors.append(msg) | ||
|
|
||
| self.log.info('Found %s invalid packages out of %s packages', len(faulty_pkg_names), len(pkgs)) | ||
| if faulty_pkg_names: | ||
| msg = ( | ||
| "The following Python packages were likely not installed correctly because they show a " | ||
| "version of '%s':\n%s\n" | ||
| "This may be solved by using a *-none-any.whl file as the source instead. " | ||
| "See e.g. the SOURCE*_WHL templates.\n" | ||
| "Otherwise you could check if the package provides a version at all or if e.g. poetry is " | ||
| "required (check the source for a pyproject.toml and see PEP517 for details on that)." | ||
| ) % (faulty_version, '\n'.join(faulty_pkg_names)) | ||
| pip_check_errors.append(msg) | ||
|
|
||
| if pip_check_errors: | ||
| raise EasyBuildError('\n'.join(pip_check_errors)) | ||
| else: | ||
| raise EasyBuildError("pip >= 9.0.0 is required for running '%s', found %s", | ||
| pip_check_command, | ||
| pip_version) | ||
| else: | ||
| raise EasyBuildError("Failed to determine pip version!") | ||
| sanity_pip_check = self.cfg.get('sanity_pip_check', True) | ||
| if self.is_extension: | ||
| sanity_pip_check_main = self.master.cfg.get('sanity_pip_check') | ||
| if sanity_pip_check_main is not None: | ||
| # If the main easyblock (e.g. PythonBundle) defines the variable | ||
| # we trust it does the pip check if requested and checks for mismatches | ||
| sanity_pip_check = False | ||
| msg = "Sanity 'pip check' disabled for {self.name} extension, " | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @boegel Related to the above: If we don't define |
||
| msg += "assuming that parent will take care of it" | ||
| self.log.info(msg) | ||
|
|
||
| if sanity_pip_check: | ||
| if not self.is_extension: | ||
| # for stand-alone Python package installations (not part of a bundle of extensions), | ||
| # the (fake or real) module file must be loaded at this point, | ||
| # otherwise the Python package being installed is not "in view", | ||
| # and we will overlook missing dependencies... | ||
| loaded_modules = [x['mod_name'] for x in self.modules_tool.list()] | ||
| if self.short_mod_name not in loaded_modules: | ||
| self.log.debug("Currently loaded modules: %s", loaded_modules) | ||
| raise EasyBuildError("%s module is not loaded, this should never happen...", | ||
| self.short_mod_name) | ||
|
|
||
| unversioned_packages = self.cfg.get('unversioned_packages', []) | ||
| run_pip_check(python_cmd=python_cmd, unversioned_packages=unversioned_packages) | ||
|
|
||
| # ExtensionEasyBlock handles loading modules correctly for multi_deps, so we clean up fake_mod_data | ||
| # and let ExtensionEasyBlock do its job | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@boegel What was wrong with the original syntax? I find the repetition of the variable name overly verbose and IMO introduces a new source for errors. It seems like PEP even recommends this, although I have only found a secondary source