diff options
Diffstat (limited to 'pkgs/development/interpreters/python/hooks/python-runtime-deps-check-hook.py')
-rw-r--r-- | pkgs/development/interpreters/python/hooks/python-runtime-deps-check-hook.py | 97 |
1 files changed, 97 insertions, 0 deletions
diff --git a/pkgs/development/interpreters/python/hooks/python-runtime-deps-check-hook.py b/pkgs/development/interpreters/python/hooks/python-runtime-deps-check-hook.py new file mode 100644 index 0000000000000..5a3a919391753 --- /dev/null +++ b/pkgs/development/interpreters/python/hooks/python-runtime-deps-check-hook.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +""" +The runtimeDependenciesHook validates, that all dependencies specified +in wheel metadata are available in the local environment. + +In case that does not hold, it will print missing dependencies and +violated version constraints. +""" + + +import importlib.metadata +import re +import sys +import tempfile +from argparse import ArgumentParser +from zipfile import ZipFile + +from packaging.metadata import Metadata, parse_email +from packaging.requirements import Requirement + +argparser = ArgumentParser() +argparser.add_argument("wheel", help="Path to the .whl file to test") + + +def error(msg: str) -> None: + print(f" - {msg}", file=sys.stderr) + + +def normalize_name(name: str) -> str: + """ + Normalize package names according to PEP503 + """ + return re.sub(r"[-_.]+", "-", name).lower() + + +def get_manifest_text_from_wheel(wheel: str) -> str: + """ + Given a path to a wheel, this function will try to extract the + METADATA file in the wheels .dist-info directory. + """ + with ZipFile(wheel) as zipfile: + for zipinfo in zipfile.infolist(): + if zipinfo.filename.endswith(".dist-info/METADATA"): + with tempfile.TemporaryDirectory() as tmp: + path = zipfile.extract(zipinfo, path=tmp) + with open(path, encoding="utf-8") as fd: + return fd.read() + + raise RuntimeError("No METADATA file found in wheel") + + +def get_metadata(wheel: str) -> Metadata: + """ + Given a path to a wheel, returns a parsed Metadata object. + """ + text = get_manifest_text_from_wheel(wheel) + raw, _ = parse_email(text) + metadata = Metadata.from_raw(raw) + + return metadata + + +def test_requirement(requirement: Requirement) -> bool: + """ + Given a requirement specification, tests whether the dependency can + be resolved in the local environment, and whether it satisfies the + specified version constraints. + """ + if requirement.marker and not requirement.marker.evaluate(): + # ignore requirements with incompatible markers + return True + + package_name = normalize_name(requirement.name) + + try: + package = importlib.metadata.distribution(requirement.name) + except importlib.metadata.PackageNotFoundError: + error(f"{package_name} not installed") + return False + + if package.version not in requirement.specifier: + error( + f"{package_name}{requirement.specifier} not satisfied by version {package.version}" + ) + return False + + return True + + +if __name__ == "__main__": + args = argparser.parse_args() + + metadata = get_metadata(args.wheel) + tests = [test_requirement(requirement) for requirement in metadata.requires_dist] + + if not all(tests): + sys.exit(1) |