From fca16159c78cc225d955fa8064f7de378294cd88 Mon Sep 17 00:00:00 2001 From: J Lorieau Date: Sat, 29 Jul 2023 14:40:32 -0500 Subject: [PATCH] Simplify mechanism to get package version by using importlib --- geomancy/checks/python.py | 71 ++++++------------------------------- tests/checks/test_python.py | 42 ++-------------------- 2 files changed, 13 insertions(+), 100 deletions(-) diff --git a/geomancy/checks/python.py b/geomancy/checks/python.py index f813bcb..a97f4a0 100644 --- a/geomancy/checks/python.py +++ b/geomancy/checks/python.py @@ -2,10 +2,8 @@ Checks for python environment and packages """ import typing as t -import sys -import subprocess import logging -import re +import importlib.metadata # python >= 3.8 from .version import CheckVersion from .utils import version_to_tuple @@ -19,15 +17,6 @@ class CheckPythonPackage(CheckVersion): """Check the availability and version of a python package""" - # use the pip freeze method - use_pip_freeze: bool = True - - # The regex to use for parsing python package names - pip_pkg_str = r"^(?P{pkg_name})\s*==\s*(?P[\d\w.]+)$" - - # The results of pip freeze - pip_freeze: t.Union[str, None] - # The message for checking python packages msg = Parameter( "CHECKPYTHONPACKAGE.MSG", @@ -37,58 +26,20 @@ class CheckPythonPackage(CheckVersion): aliases = ("checkPythonPackage", "checkPythonPkg", "CheckPythonPkg") def get_current_version(self) -> t.Union[None, t.Tuple[int]]: + # Get the package name, operator and version to check against (the last + # 2 aren't used here) pkg_name, op, version = self.value - python = sys.executable # current python interpreter # Nothing to do if the package name was not found if pkg_name is None: return None - # Method 1 -- try pip freeze - # First, try loading the freeze. - if self.use_pip_freeze and not hasattr(self, "pip_freeze"): - # "pip list" is used instead of "pip freeze" because "pip list" - # will not show paths--just package names--for packages installed - # from a local repository - args = (python, "-m", "pip", "list", "--format=freeze") - proc = subprocess.run(args=args, capture_output=True) - - if proc.returncode != 0: - # This command didn't work. Set the class attribute for all - # instances - CheckPythonPackage.pip_freeze = None - logger.debug(f"Trying to use pip_freeze but couldn't run " - f"'{args}'") - else: - CheckPythonPackage.pip_freeze = proc.stdout.decode("UTF-8") - - # Parse the pip freeze - if getattr(self, "pip_freeze", None) is not None: - pattern = self.pip_pkg_str.format(pkg_name=pkg_name) - match = re.search(pattern, self.pip_freeze, re.MULTILINE) - - # Convert the regex match to a version tuple - version = match.groupdict()["ver"] if match is not None else None - version = version_to_tuple(version) if version is not None else None + # Method 1 -- importlib.metadata + try: + # Returns a version string--e.g. '0.9.3' + version_string = importlib.metadata.version(pkg_name) - logger.debug(f"Found '{pkg_name}' package version '{version}' with " - f"pip freeze.") - if version is not None: - return version - - # Method 2 -- try importing and getting it from the __version__ string - code = f"import {pkg_name}; print({pkg_name}.__version__)" - proc = subprocess.run(args=(python, "-c", code), capture_output=True) - - if proc.returncode == 0: - version_str = ( - proc.stdout.decode("UTF-8").strip() if proc.stdout is not None else None - ) - version = version_to_tuple(version_str) if version_str is not None else None - - logger.debug(f"Found '{pkg_name}' package version '{version}' with " - f"{pkg_name}.__version__") - return version - - # I'm out of ideas. Version couldn't be parsed - return None + # Return the version tuple--e.g. (0, 9, 3) + return version_to_tuple(version_string) if version_string else None + except importlib.metadata.PackageNotFoundError: + return None diff --git a/tests/checks/test_python.py b/tests/checks/test_python.py index a82ef84..eb6134e 100644 --- a/tests/checks/test_python.py +++ b/tests/checks/test_python.py @@ -9,45 +9,7 @@ pip = None -@pytest.fixture -def reset(): - """Reset class caches""" - # Reset cached version, if populated by another test - if hasattr(CheckPythonPackage, "pip_freeze"): - del CheckPythonPackage.pip_freeze - return CheckPythonPackage - - -@pytest.mark.skipif(pip is None, reason="pip must be installed") -def test_check_python_package_get_current_version_pip(reset): - """Test the CheckPythonPackage get_current_version method with pip""" - # Check that pip_freeze can get populated - check = CheckPythonPackage(name="Python package", value="mypkg>=3.0") - check.use_pip_freeze = True - - assert not hasattr(check, "pip_freeze") - check.get_current_version() - assert hasattr(check, "pip_freeze") and isinstance(check.pip_freeze, str) - - # Substitute the pip freeze and see if the package version is correctly parsed - check.pip_freeze = "" - assert check.get_current_version() is None - check.pip_freeze = "\n".join(("more-itertools==9.1.0", "mypkg==3.0")) - assert check.get_current_version() == (3, 0) - - -def test_check_python_package_get_current_version_no_pip(reset): - """Test the CheckPythonPackage get_current_version method without pip""" - check = CheckPythonPackage(name="Python package", value="pytest") - check.use_pip_freeze = False - - version = check.get_current_version() - assert not hasattr(check, "pip_freeze") - assert version is not None # A version should have been found for 'pytest' - assert isinstance(version, tuple) - - -def test_check_python_package_exists(reset): +def test_check_python_package_exists(): """Tests CheckPythonPackage checking for an existing and a missing package""" # Should exist @@ -59,7 +21,7 @@ def test_check_python_package_exists(reset): assert not check.check().passed -def test_check_python_package_version(reset): +def test_check_python_package_version(): """Tests CheckPythonPackage checking with version number""" # Should be greater than version 1.0 check = CheckPythonPackage(name="Check pytest", value="pytest>=1.0")