Skip to content

Commit dc1bfa5

Browse files
committed
move det_installed_python_packages, det_pip_version, run_pip_check to Python easyblock, so they can also be used there
1 parent 41cbdb3 commit dc1bfa5

File tree

3 files changed

+137
-132
lines changed

3 files changed

+137
-132
lines changed

easybuild/easyblocks/generic/pythonpackage.py

Lines changed: 1 addition & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
@author: Jens Timmerman (Ghent University)
3333
@author: Alexander Grund (TU Dresden)
3434
"""
35-
import json
3635
import os
3736
import re
3837
import sys
@@ -43,6 +42,7 @@
4342
import easybuild.tools.environment as env
4443
from easybuild.base import fancylogger
4544
from easybuild.easyblocks.python import EXTS_FILTER_PYTHON_PACKAGES
45+
from easybuild.easyblocks.python import det_installed_python_packages, det_pip_version, run_pip_check
4646
from easybuild.framework.easyconfig import CUSTOM
4747
from easybuild.framework.easyconfig.default import DEFAULT_CONFIG
4848
from easybuild.framework.easyconfig.templates import PYPI_SOURCE
@@ -295,27 +295,6 @@ def get_pylibdirs(python_cmd):
295295
return all_pylibdirs
296296

297297

298-
def det_pip_version(python_cmd='python'):
299-
"""Determine version of currently active 'pip' module."""
300-
301-
pip_version = None
302-
log = fancylogger.getLogger('det_pip_version', fname=False)
303-
log.info("Determining pip version...")
304-
305-
res = run_shell_cmd("%s -m pip --version" % python_cmd, hidden=True)
306-
out = res.output
307-
308-
pip_version_regex = re.compile('^pip ([0-9.]+)')
309-
res = pip_version_regex.search(out)
310-
if res:
311-
pip_version = res.group(1)
312-
log.info("Found pip version: %s", pip_version)
313-
else:
314-
log.warning("Failed to determine pip version from '%s' using pattern '%s'", out, pip_version_regex.pattern)
315-
316-
return pip_version
317-
318-
319298
def det_py_install_scheme(python_cmd='python'):
320299
"""
321300
Try to determine active installation scheme used by Python.
@@ -398,108 +377,6 @@ def symlink_dist_site_packages(install_dir, pylibdirs):
398377
symlink(dist_pkgs, site_pkgs_path, use_abspath_source=False)
399378

400379

401-
def det_installed_python_packages(names_only=True, python_cmd=None):
402-
"""
403-
Return list of Python packages that are installed
404-
405-
Note that the names are reported by pip and might be different to the name that need to be used to import it.
406-
407-
:param names_only: boolean indicating whether only names or full info from `pip list` should be returned
408-
:param python_cmd: Python command to use (if None, 'python' is used)
409-
"""
410-
log = fancylogger.getLogger('det_installed_python_packages', fname=False)
411-
412-
if python_cmd is None:
413-
python_cmd = 'python'
414-
415-
# Check installed Python packages
416-
cmd = ' '.join([
417-
python_cmd, '-m', 'pip',
418-
'list',
419-
'--isolated',
420-
'--disable-pip-version-check',
421-
'--format', 'json',
422-
])
423-
res = run_shell_cmd(cmd, fail_on_error=False, hidden=True)
424-
if res.exit_code:
425-
raise EasyBuildError(f'Failed to determine installed python packages: {res.output}')
426-
427-
# only check stdout, not stderr which might contain user facing warnings
428-
log.info(f'Got list of installed Python packages: {res.output}')
429-
pkgs = json.loads(res.output.strip())
430-
return [pkg['name'] for pkg in pkgs] if names_only else pkgs
431-
432-
433-
def run_pip_check(python_cmd=None, unversioned_packages=None):
434-
"""
435-
Check installed Python packages using 'pip check'
436-
437-
:param unversioned_packages: list of Python packages to exclude in the version existence check
438-
:param python_cmd: Python command to use (if None, 'python' is used)
439-
"""
440-
log = fancylogger.getLogger('det_installed_python_packages', fname=False)
441-
442-
if python_cmd is None:
443-
python_cmd = 'python'
444-
if unversioned_packages is None:
445-
unversioned_packages = []
446-
447-
pip_check_cmd = f"{python_cmd} -m pip check"
448-
449-
pip_version = det_pip_version(python_cmd=python_cmd)
450-
if not pip_version:
451-
raise EasyBuildError("Failed to determine pip version!")
452-
min_pip_version = LooseVersion('9.0.0')
453-
if LooseVersion(pip_version) < min_pip_version:
454-
raise EasyBuildError(f"pip >= {min_pip_version} is required for '{pip_check_cmd}', found {pip_version}")
455-
456-
pip_check_errors = []
457-
458-
res = run_shell_cmd(pip_check_cmd, fail_on_error=False)
459-
if res.exit_code:
460-
pip_check_errors.append(f"`{pip_check_cmd}` failed:\n{res.output}")
461-
else:
462-
log.info(f"`{pip_check_cmd}` passed successfully")
463-
464-
# Also check for a common issue where the package version shows up as 0.0.0 often caused
465-
# by using setup.py as the installation method for a package which is released as a generic wheel
466-
# named name-version-py2.py3-none-any.whl. `tox` creates those from version controlled source code
467-
# so it will contain a version, but the raw tar.gz does not.
468-
pkgs = det_installed_python_packages(names_only=False, python_cmd=python_cmd)
469-
faulty_version = '0.0.0'
470-
faulty_pkg_names = [pkg['name'] for pkg in pkgs if pkg['version'] == faulty_version]
471-
472-
for unversioned_package in unversioned_packages:
473-
try:
474-
faulty_pkg_names.remove(unversioned_package)
475-
log.debug(f"Excluding unversioned package '{unversioned_package}' from check")
476-
except ValueError:
477-
try:
478-
version = next(pkg['version'] for pkg in pkgs if pkg['name'] == unversioned_package)
479-
except StopIteration:
480-
msg = f"Package '{unversioned_package}' in unversioned_packages was not found in "
481-
msg += "the installed packages. Check that the name from `python -m pip list` is used "
482-
msg += "which may be different than the module name."
483-
else:
484-
msg = f"Package '{unversioned_package}' in unversioned_packages has a version of {version} "
485-
msg += "which is valid. Please remove it from unversioned_packages."
486-
pip_check_errors.append(msg)
487-
488-
log.info("Found %s invalid packages out of %s packages", len(faulty_pkg_names), len(pkgs))
489-
if faulty_pkg_names:
490-
faulty_pkg_names_str = '\n'.join(faulty_pkg_names)
491-
msg = "The following Python packages were likely not installed correctly because they show a "
492-
msg += f"version of '{faulty_version}':\n{faulty_pkg_names_str}\n"
493-
msg += "This may be solved by using a *-none-any.whl file as the source instead. "
494-
msg += "See e.g. the SOURCE*_WHL templates.\n"
495-
msg += "Otherwise you could check if the package provides a version at all or if e.g. poetry is "
496-
msg += "required (check the source for a pyproject.toml and see PEP517 for details on that)."
497-
pip_check_errors.append(msg)
498-
499-
if pip_check_errors:
500-
raise EasyBuildError('\n'.join(pip_check_errors))
501-
502-
503380
class PythonPackage(ExtensionEasyBlock):
504381
"""Builds and installs a Python package, and provides a dedicated module file."""
505382

easybuild/easyblocks/p/python.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
@author: Bart Oldeman (McGill University, Calcul Quebec, Compute Canada)
3434
"""
3535
import glob
36+
import json
3637
import os
3738
import re
3839
import fileinput
@@ -41,6 +42,7 @@
4142
from easybuild.tools import LooseVersion
4243

4344
import easybuild.tools.environment as env
45+
from easybuild.base import fancylogger
4446
from easybuild.easyblocks.generic.configuremake import ConfigureMake
4547
from easybuild.framework.easyconfig import CUSTOM
4648
from easybuild.framework.easyconfig.templates import PYPI_SOURCE
@@ -107,6 +109,129 @@
107109
""" % {'EBPYTHONPREFIXES': EBPYTHONPREFIXES}
108110

109111

112+
def det_pip_version(python_cmd='python'):
113+
"""Determine version of currently active 'pip' module."""
114+
115+
pip_version = None
116+
log = fancylogger.getLogger('det_pip_version', fname=False)
117+
log.info("Determining pip version...")
118+
119+
res = run_shell_cmd("%s -m pip --version" % python_cmd, hidden=True)
120+
out = res.output
121+
122+
pip_version_regex = re.compile('^pip ([0-9.]+)')
123+
res = pip_version_regex.search(out)
124+
if res:
125+
pip_version = res.group(1)
126+
log.info("Found pip version: %s", pip_version)
127+
else:
128+
log.warning("Failed to determine pip version from '%s' using pattern '%s'", out, pip_version_regex.pattern)
129+
130+
return pip_version
131+
132+
133+
def det_installed_python_packages(names_only=True, python_cmd=None):
134+
"""
135+
Return list of Python packages that are installed
136+
137+
Note that the names are reported by pip and might be different to the name that need to be used to import it.
138+
139+
:param names_only: boolean indicating whether only names or full info from `pip list` should be returned
140+
:param python_cmd: Python command to use (if None, 'python' is used)
141+
"""
142+
log = fancylogger.getLogger('det_installed_python_packages', fname=False)
143+
144+
if python_cmd is None:
145+
python_cmd = 'python'
146+
147+
# Check installed Python packages
148+
cmd = ' '.join([
149+
python_cmd, '-m', 'pip',
150+
'list',
151+
'--isolated',
152+
'--disable-pip-version-check',
153+
'--format', 'json',
154+
])
155+
res = run_shell_cmd(cmd, fail_on_error=False, hidden=True)
156+
if res.exit_code:
157+
raise EasyBuildError(f'Failed to determine installed python packages: {res.output}')
158+
159+
# only check stdout, not stderr which might contain user facing warnings
160+
log.info(f'Got list of installed Python packages: {res.output}')
161+
pkgs = json.loads(res.output.strip())
162+
return [pkg['name'] for pkg in pkgs] if names_only else pkgs
163+
164+
165+
def run_pip_check(python_cmd=None, unversioned_packages=None):
166+
"""
167+
Check installed Python packages using 'pip check'
168+
169+
:param unversioned_packages: list of Python packages to exclude in the version existence check
170+
:param python_cmd: Python command to use (if None, 'python' is used)
171+
"""
172+
log = fancylogger.getLogger('det_installed_python_packages', fname=False)
173+
174+
if python_cmd is None:
175+
python_cmd = 'python'
176+
if unversioned_packages is None:
177+
unversioned_packages = []
178+
179+
pip_check_cmd = f"{python_cmd} -m pip check"
180+
181+
pip_version = det_pip_version(python_cmd=python_cmd)
182+
if not pip_version:
183+
raise EasyBuildError("Failed to determine pip version!")
184+
min_pip_version = LooseVersion('9.0.0')
185+
if LooseVersion(pip_version) < min_pip_version:
186+
raise EasyBuildError(f"pip >= {min_pip_version} is required for '{pip_check_cmd}', found {pip_version}")
187+
188+
pip_check_errors = []
189+
190+
res = run_shell_cmd(pip_check_cmd, fail_on_error=False)
191+
if res.exit_code:
192+
pip_check_errors.append(f"`{pip_check_cmd}` failed:\n{res.output}")
193+
else:
194+
log.info(f"`{pip_check_cmd}` passed successfully")
195+
196+
# Also check for a common issue where the package version shows up as 0.0.0 often caused
197+
# by using setup.py as the installation method for a package which is released as a generic wheel
198+
# named name-version-py2.py3-none-any.whl. `tox` creates those from version controlled source code
199+
# so it will contain a version, but the raw tar.gz does not.
200+
pkgs = det_installed_python_packages(names_only=False, python_cmd=python_cmd)
201+
faulty_version = '0.0.0'
202+
faulty_pkg_names = [pkg['name'] for pkg in pkgs if pkg['version'] == faulty_version]
203+
204+
for unversioned_package in unversioned_packages:
205+
try:
206+
faulty_pkg_names.remove(unversioned_package)
207+
log.debug(f"Excluding unversioned package '{unversioned_package}' from check")
208+
except ValueError:
209+
try:
210+
version = next(pkg['version'] for pkg in pkgs if pkg['name'] == unversioned_package)
211+
except StopIteration:
212+
msg = f"Package '{unversioned_package}' in unversioned_packages was not found in "
213+
msg += "the installed packages. Check that the name from `python -m pip list` is used "
214+
msg += "which may be different than the module name."
215+
else:
216+
msg = f"Package '{unversioned_package}' in unversioned_packages has a version of {version} "
217+
msg += "which is valid. Please remove it from unversioned_packages."
218+
pip_check_errors.append(msg)
219+
220+
log.info("Found %s invalid packages out of %s packages", len(faulty_pkg_names), len(pkgs))
221+
if faulty_pkg_names:
222+
faulty_pkg_names_str = '\n'.join(faulty_pkg_names)
223+
msg = "The following Python packages were likely not installed correctly because they show a "
224+
msg += f"version of '{faulty_version}':\n{faulty_pkg_names_str}\n"
225+
msg += "This may be solved by using a *-none-any.whl file as the source instead. "
226+
msg += "See e.g. the SOURCE*_WHL templates.\n"
227+
msg += "Otherwise you could check if the package provides a version at all or if e.g. poetry is "
228+
msg += "required (check the source for a pyproject.toml and see PEP517 for details on that)."
229+
pip_check_errors.append(msg)
230+
231+
if pip_check_errors:
232+
raise EasyBuildError('\n'.join(pip_check_errors))
233+
234+
110235
class EB_Python(ConfigureMake):
111236
"""Support for building/installing Python
112237
- default configure/build_step/make install works fine

test/easyblocks/easyblock_specific.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040

4141
import easybuild.tools.options as eboptions
4242
import easybuild.easyblocks.generic.pythonpackage as pythonpackage
43+
import easybuild.easyblocks.p.python as python
4344
from easybuild.base.testing import TestCase
4445
from easybuild.easyblocks.generic.cmakemake import det_cmake_version
4546
from easybuild.easyblocks.generic.toolchain import Toolchain
@@ -275,8 +276,8 @@ def test_det_installed_python_packages(self):
275276
Test det_installed_python_packages function providyed by PythonPackage easyblock
276277
"""
277278
pkg1 = None
278-
# we can't make too much assumptions on which installed Python packages are found
279279
res = pythonpackage.det_installed_python_packages(python_cmd=sys.executable)
280+
# we can't make too much assumptions on which installed Python packages are found
280281
self.assertTrue(isinstance(res, list))
281282
if res:
282283
pkg1_name = res[0]
@@ -358,8 +359,9 @@ def mocked_run_shell_cmd_pip(cmd, **kwargs):
358359
return RunShellCmdResult(cmd=cmd, exit_code=0, output=output, stderr=None, work_dir=None,
359360
out_file=None, err_file=None, cmd_sh=None, thread_id=None, task_id=None)
360361

361-
pythonpackage.run_shell_cmd = mocked_run_shell_cmd_pip
362-
pythonpackage.run_pip_check(python_cmd=sys.executable)
362+
python.run_shell_cmd = mocked_run_shell_cmd_pip
363+
with self.mocked_stdout_stderr():
364+
python.run_pip_check(python_cmd=sys.executable)
363365

364366
# inject all possible errors
365367
def mocked_run_shell_cmd_pip(cmd, **kwargs):
@@ -379,7 +381,7 @@ def mocked_run_shell_cmd_pip(cmd, **kwargs):
379381
return RunShellCmdResult(cmd=cmd, exit_code=exit_code, output=output, stderr=None, work_dir=None,
380382
out_file=None, err_file=None, cmd_sh=None, thread_id=None, task_id=None)
381383

382-
pythonpackage.run_shell_cmd = mocked_run_shell_cmd_pip
384+
python.run_shell_cmd = mocked_run_shell_cmd_pip
383385
error_pattern = '\n'.join([
384386
"pip check.*failed.*",
385387
"foo.*requires.*bar.*not installed.*",
@@ -388,17 +390,18 @@ def mocked_run_shell_cmd_pip(cmd, **kwargs):
388390
r".*not installed correctly.*version of '0\.0\.0':",
389391
"wrong",
390392
])
391-
self.assertErrorRegex(EasyBuildError, error_pattern, pythonpackage.run_pip_check,
392-
python_cmd=sys.executable, unversioned_packages=['example', 'nosuchpkg'])
393+
with self.mocked_stdout_stderr():
394+
self.assertErrorRegex(EasyBuildError, error_pattern, python.run_pip_check,
395+
python_cmd=sys.executable, unversioned_packages=['example', 'nosuchpkg'])
393396

394397
# invalid pip version
395398
def mocked_run_shell_cmd_pip(cmd, **kwargs):
396399
return RunShellCmdResult(cmd=cmd, exit_code=0, output="1.2.3", stderr=None, work_dir=None,
397400
out_file=None, err_file=None, cmd_sh=None, thread_id=None, task_id=None)
398401

399-
pythonpackage.run_shell_cmd = mocked_run_shell_cmd_pip
402+
python.run_shell_cmd = mocked_run_shell_cmd_pip
400403
error_pattern = "Failed to determine pip version!"
401-
self.assertErrorRegex(EasyBuildError, error_pattern, pythonpackage.run_pip_check, python_cmd=sys.executable)
404+
self.assertErrorRegex(EasyBuildError, error_pattern, python.run_pip_check, python_cmd=sys.executable)
402405

403406
def test_symlink_dist_site_packages(self):
404407
"""Test symlink_dist_site_packages provided by PythonPackage easyblock."""

0 commit comments

Comments
 (0)