diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7b53d790..7a68a789 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: exclude: docs/.*|tests/.*|noxfile.py - id: mypy name: mypy (Python 2) - additional_dependencies: ["pathlib2"] + additional_dependencies: ["pathlib2", "build", "packaging<21.0"] exclude: docs/.*|tests/.*|tools/.*|noxfile.py args: ["-2"] diff --git a/pyproject.toml b/pyproject.toml index acb67b38..6c911fc6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,8 @@ requires-python = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" requires = [ "configparser >= 3.5; python_version < '3'", "importlib-resources; python_version < '3.7'", + "build >= 0.2.0", # not a hard runtime requirement -- we can softfail + "packaging", # not a hard runtime requirement -- we can softfail ] [tool.flit.scripts] diff --git a/src/installer/__main__.py b/src/installer/__main__.py index 15fcd379..f0643501 100644 --- a/src/installer/__main__.py +++ b/src/installer/__main__.py @@ -5,9 +5,11 @@ import distutils.dist import os import os.path +import platform import py_compile import sys import sysconfig +import warnings import installer import installer.destinations @@ -16,6 +18,7 @@ from installer._compat.typing import TYPE_CHECKING if TYPE_CHECKING: + from email.message import Message from typing import Any, Dict, List, Optional, Sequence, Union from installer._compat.typing import Text @@ -51,6 +54,12 @@ def main_parser(): # type: () -> argparse.ArgumentParser action="store_true", help="enable optimization", ) + parser.add_argument( + "--skip-dependency-check", + "-s", + action="store_true", + help="don't check if the wheel dependencies are met", + ) return parser @@ -102,6 +111,47 @@ def generate_bytecode(record_dict, scheme_dict, levels, stripdir=None): _compile_records(record_dict, scheme_dict, compile_args) +def check_python_version(metadata): # type: (Message) -> None + """Check if the project support the current interpreter.""" + try: + import packaging.specifiers + except ImportError: + warnings.warn( + "'packaging' module not available, " + "skiping python version compatibility check" + ) + + requirement = metadata["Requires-Python"] + if not requirement: + return + + versions = requirement.split(",") + for version in versions: + specifier = packaging.specifiers.Specifier(version) + if platform.python_version() not in specifier: + raise RuntimeError( + "Incompatible python version, needed: {}".format(version) + ) + + +def check_dependencies(metadata): # type: (Message) -> None + """Check if the project dependencies are met.""" + try: + import build + except ImportError: + warnings.warn("'build' module not available, skiping dependency check") + + missing = { + unmet + for requirement in metadata.get_all("Requires-Dist") or [] + for unmet_list in build.check_dependency(requirement) + for unmet in unmet_list + } + if missing: + missing_list = ", ".join(missing) + raise RuntimeError("Missing requirements: {}".format(missing_list)) + + def main(cli_args, program=None): # type: (Sequence[str], Optional[str]) -> None """Process arguments and perform the install.""" @@ -113,6 +163,13 @@ def main(cli_args, program=None): bytecode_stripdir = None # type: Optional[str] with installer.sources.WheelFile.open(args.wheel) as source: + # compability checks + metadata_contents = source.read_dist_info("METADATA") + metadata = installer.utils.parse_metadata_file(metadata_contents) + check_python_version(metadata) + if not args.skip_dependency_check: + check_dependencies(metadata) + scheme_dict = get_scheme_dict(source.distribution) # prepend DESTDIR to scheme paths