at master 3.0 kB view raw
1#!/usr/bin/env python3 2""" 3The runtimeDependenciesHook validates, that all dependencies specified 4in wheel metadata are available in the local environment. 5 6In case that does not hold, it will print missing dependencies and 7violated version constraints. 8""" 9 10 11import importlib.metadata 12import re 13import sys 14import tempfile 15from argparse import ArgumentParser 16from zipfile import ZipFile 17 18from packaging.metadata import Metadata, parse_email 19from packaging.requirements import Requirement 20 21argparser = ArgumentParser() 22argparser.add_argument("wheel", help="Path to the .whl file to test") 23 24 25def error(msg: str) -> None: 26 print(f" - {msg}", file=sys.stderr) 27 28 29def normalize_name(name: str) -> str: 30 """ 31 Normalize package names according to PEP503 32 """ 33 return re.sub(r"[-_.]+", "-", name).lower() 34 35 36def get_manifest_text_from_wheel(wheel: str) -> str: 37 """ 38 Given a path to a wheel, this function will try to extract the 39 METADATA file in the wheels .dist-info directory. 40 """ 41 with ZipFile(wheel) as zipfile: 42 for zipinfo in zipfile.infolist(): 43 if zipinfo.filename.endswith(".dist-info/METADATA"): 44 with tempfile.TemporaryDirectory() as tmp: 45 path = zipfile.extract(zipinfo, path=tmp) 46 with open(path, encoding="utf-8") as fd: 47 return fd.read() 48 49 raise RuntimeError("No METADATA file found in wheel") 50 51 52def get_metadata(wheel: str) -> Metadata: 53 """ 54 Given a path to a wheel, returns a parsed Metadata object. 55 """ 56 text = get_manifest_text_from_wheel(wheel) 57 raw, _ = parse_email(text) 58 metadata = Metadata.from_raw(raw, validate=False) 59 60 return metadata 61 62 63def test_requirement(requirement: Requirement) -> bool: 64 """ 65 Given a requirement specification, tests whether the dependency can 66 be resolved in the local environment, and whether it satisfies the 67 specified version constraints. 68 """ 69 if requirement.marker and not requirement.marker.evaluate(): 70 # ignore requirements with incompatible markers 71 return True 72 73 package_name = normalize_name(requirement.name) 74 75 try: 76 package = importlib.metadata.distribution(requirement.name) 77 except importlib.metadata.PackageNotFoundError: 78 error(f"{package_name} not installed") 79 return False 80 81 # Allow prereleases, to give to give us some wiggle-room 82 requirement.specifier.prereleases = True 83 84 if requirement.specifier and package.version not in requirement.specifier: 85 error( 86 f"{package_name}{requirement.specifier} not satisfied by version {package.version}" 87 ) 88 return False 89 90 return True 91 92 93if __name__ == "__main__": 94 args = argparser.parse_args() 95 96 metadata = get_metadata(args.wheel) 97 requirements = metadata.requires_dist 98 99 if not requirements: 100 sys.exit(0) 101 102 tests = [test_requirement(requirement) for requirement in requirements] 103 104 if not all(tests): 105 sys.exit(1)