Skip to content

Commit

Permalink
Simplify mechanism to get package version by using importlib
Browse files Browse the repository at this point in the history
  • Loading branch information
jlorieau committed Jul 29, 2023
1 parent 4aa6708 commit fca1615
Show file tree
Hide file tree
Showing 2 changed files with 13 additions and 100 deletions.
71 changes: 11 additions & 60 deletions geomancy/checks/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<name>{pkg_name})\s*==\s*(?P<ver>[\d\w.]+)$"

# The results of pip freeze
pip_freeze: t.Union[str, None]

# The message for checking python packages
msg = Parameter(
"CHECKPYTHONPACKAGE.MSG",
Expand All @@ -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
42 changes: 2 additions & 40 deletions tests/checks/test_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
Expand Down

0 comments on commit fca1615

Please sign in to comment.