python3.pkgs.pythonRuntimeDepsCheckHook: init

Implements a hook, that checks whether all dependencies, as specified by
the wheel manifest, are present in the current environment.

Complains about missing packages, as well as version specifier
mismatches.

+168
+10
pkgs/development/interpreters/python/hooks/default.nix
··· 172 172 }; 173 173 } ./python-remove-tests-dir-hook.sh) {}; 174 174 175 + pythonRuntimeDepsCheckHook = callPackage ({ makePythonHook, packaging }: 176 + makePythonHook { 177 + name = "python-runtime-deps-check-hook.sh"; 178 + propagatedBuildInputs = [ packaging ]; 179 + substitutions = { 180 + inherit pythonInterpreter pythonSitePackages; 181 + hook = ./python-runtime-deps-check-hook.py; 182 + }; 183 + } ./python-runtime-deps-check-hook.sh) {}; 184 + 175 185 setuptoolsBuildHook = callPackage ({ makePythonHook, setuptools, wheel }: 176 186 makePythonHook { 177 187 name = "setuptools-setup-hook";
+97
pkgs/development/interpreters/python/hooks/python-runtime-deps-check-hook.py
··· 1 + #!/usr/bin/env python3 2 + """ 3 + The runtimeDependenciesHook validates, that all dependencies specified 4 + in wheel metadata are available in the local environment. 5 + 6 + In case that does not hold, it will print missing dependencies and 7 + violated version constraints. 8 + """ 9 + 10 + 11 + import importlib.metadata 12 + import re 13 + import sys 14 + import tempfile 15 + from argparse import ArgumentParser 16 + from zipfile import ZipFile 17 + 18 + from packaging.metadata import Metadata, parse_email 19 + from packaging.requirements import Requirement 20 + 21 + argparser = ArgumentParser() 22 + argparser.add_argument("wheel", help="Path to the .whl file to test") 23 + 24 + 25 + def error(msg: str) -> None: 26 + print(f" - {msg}", file=sys.stderr) 27 + 28 + 29 + def normalize_name(name: str) -> str: 30 + """ 31 + Normalize package names according to PEP503 32 + """ 33 + return re.sub(r"[-_.]+", "-", name).lower() 34 + 35 + 36 + def 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 + 52 + def 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) 59 + 60 + return metadata 61 + 62 + 63 + def 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 + if package.version not in requirement.specifier: 82 + error( 83 + f"{package_name}{requirement.specifier} not satisfied by version {package.version}" 84 + ) 85 + return False 86 + 87 + return True 88 + 89 + 90 + if __name__ == "__main__": 91 + args = argparser.parse_args() 92 + 93 + metadata = get_metadata(args.wheel) 94 + tests = [test_requirement(requirement) for requirement in metadata.requires_dist] 95 + 96 + if not all(tests): 97 + sys.exit(1)
+20
pkgs/development/interpreters/python/hooks/python-runtime-deps-check-hook.sh
··· 1 + # Setup hook for PyPA installer. 2 + echo "Sourcing python-runtime-deps-check-hook" 3 + 4 + pythonRuntimeDepsCheckHook() { 5 + echo "Executing pythonRuntimeDepsCheck" 6 + 7 + export PYTHONPATH="$out/@pythonSitePackages@:$PYTHONPATH" 8 + 9 + for wheel in dist/*.whl; do 10 + echo "Checking runtime dependencies for $(basename $wheel)" 11 + @pythonInterpreter@ @hook@ "$wheel" 12 + done 13 + 14 + echo "Finished executing pythonRuntimeDepsCheck" 15 + } 16 + 17 + if [ -z "${dontCheckRuntimeDeps-}" ]; then 18 + echo "Using pythonRuntimeDepsCheckHook" 19 + preInstallPhases+=" pythonRuntimeDepsCheckHook" 20 + fi
+8
pkgs/development/interpreters/python/mk-python-derivation.nix
··· 19 19 , pythonOutputDistHook 20 20 , pythonRemoveBinBytecodeHook 21 21 , pythonRemoveTestsDirHook 22 + , pythonRuntimeDepsCheckHook 22 23 , setuptoolsBuildHook 23 24 , setuptoolsCheckHook 24 25 , wheelUnpackHook ··· 229 230 } 230 231 else 231 232 pypaBuildHook 233 + ) ( 234 + if isBootstrapPackage then 235 + pythonRuntimeDepsCheckHook.override { 236 + inherit (python.pythonOnBuildForHost.pkgs.bootstrap) packaging; 237 + } 238 + else 239 + pythonRuntimeDepsCheckHook 232 240 )] ++ lib.optionals (format' == "wheel") [ 233 241 wheelUnpackHook 234 242 ] ++ lib.optionals (format' == "egg") [
+30
pkgs/development/python-modules/bootstrap/packaging/default.nix
··· 1 + { stdenv 2 + , python 3 + , flit-core 4 + , installer 5 + , packaging 6 + }: 7 + 8 + stdenv.mkDerivation { 9 + pname = "${python.libPrefix}-bootstrap-${packaging.pname}"; 10 + inherit (packaging) version src meta; 11 + 12 + buildPhase = '' 13 + runHook preBuild 14 + 15 + PYTHONPATH="${flit-core}/${python.sitePackages}" \ 16 + ${python.interpreter} -m flit_core.wheel 17 + 18 + runHook postBuild 19 + ''; 20 + 21 + installPhase = '' 22 + runHook preInstall 23 + 24 + PYTHONPATH="${installer}/${python.sitePackages}" \ 25 + ${python.interpreter} -m installer \ 26 + --destdir "$out" --prefix "" dist/*.whl 27 + 28 + runHook postInstall 29 + ''; 30 + }
+3
pkgs/top-level/python-packages.nix
··· 16 16 build = toPythonModule (callPackage ../development/python-modules/bootstrap/build { 17 17 inherit (bootstrap) flit-core installer; 18 18 }); 19 + packaging = toPythonModule (callPackage ../development/python-modules/bootstrap/packaging { 20 + inherit (bootstrap) flit-core installer; 21 + }); 19 22 }; 20 23 21 24 setuptools = callPackage ../development/python-modules/setuptools { };