diff --git a/news/11054.feature.rst b/news/11054.feature.rst new file mode 100644 index 00000000000..335468c12f9 --- /dev/null +++ b/news/11054.feature.rst @@ -0,0 +1 @@ +Check unsupported packages for the current platform. diff --git a/src/pip/_internal/commands/check.py b/src/pip/_internal/commands/check.py index 4a0297edd97..f54a16dc0a1 100644 --- a/src/pip/_internal/commands/check.py +++ b/src/pip/_internal/commands/check.py @@ -4,10 +4,13 @@ from pip._internal.cli.base_command import Command from pip._internal.cli.status_codes import ERROR, SUCCESS +from pip._internal.metadata import get_default_environment from pip._internal.operations.check import ( check_package_set, + check_unsupported, create_package_set_from_installed, ) +from pip._internal.utils.compatibility_tags import get_supported from pip._internal.utils.misc import write_output logger = logging.getLogger(__name__) @@ -23,6 +26,12 @@ class CheckCommand(Command): def run(self, options: Values, args: List[str]) -> int: package_set, parsing_probs = create_package_set_from_installed() missing, conflicting = check_package_set(package_set) + unsupported = list( + check_unsupported( + get_default_environment().iter_installed_distributions(), + get_supported(), + ) + ) for project_name in missing: version = package_set[project_name].version @@ -45,8 +54,13 @@ def run(self, options: Values, args: List[str]) -> int: dep_name, dep_version, ) - - if missing or conflicting or parsing_probs: + for package in unsupported: + write_output( + "%s %s is not supported on this platform", + package.raw_name, + package.version, + ) + if missing or conflicting or parsing_probs or unsupported: return ERROR else: write_output("No broken requirements found.") diff --git a/src/pip/_internal/operations/check.py b/src/pip/_internal/operations/check.py index 623db76e229..4b6fbc4c375 100644 --- a/src/pip/_internal/operations/check.py +++ b/src/pip/_internal/operations/check.py @@ -2,14 +2,30 @@ """ import logging -from typing import Callable, Dict, List, NamedTuple, Optional, Set, Tuple +from contextlib import suppress +from email.parser import Parser +from functools import reduce +from typing import ( + Callable, + Dict, + FrozenSet, + Generator, + Iterable, + List, + NamedTuple, + Optional, + Set, + Tuple, +) from pip._vendor.packaging.requirements import Requirement +from pip._vendor.packaging.tags import Tag, parse_tag from pip._vendor.packaging.utils import NormalizedName, canonicalize_name from pip._vendor.packaging.version import Version from pip._internal.distributions import make_distribution_for_install_requirement from pip._internal.metadata import get_default_environment +from pip._internal.metadata.base import BaseDistribution from pip._internal.req.req_install import InstallRequirement logger = logging.getLogger(__name__) @@ -113,6 +129,22 @@ def check_install_conflicts(to_install: List[InstallRequirement]) -> ConflictDet ) +def check_unsupported( + packages: Iterable[BaseDistribution], + supported_tags: Iterable[Tag], +) -> Generator[BaseDistribution, None, None]: + for p in packages: + with suppress(FileNotFoundError): + wheel_file = p.read_text("WHEEL") + wheel_tags: FrozenSet[Tag] = reduce( + frozenset.union, + map(parse_tag, Parser().parsestr(wheel_file).get_all("Tag", [])), + frozenset(), + ) + if wheel_tags.isdisjoint(supported_tags): + yield p + + def _simulate_installation_of( to_install: List[InstallRequirement], package_set: PackageSet ) -> Set[NormalizedName]: diff --git a/tests/functional/test_check.py b/tests/functional/test_check.py index 46ecdcc6457..f50f5593e5c 100644 --- a/tests/functional/test_check.py +++ b/tests/functional/test_check.py @@ -1,6 +1,10 @@ from typing import Collection -from tests.lib import PipTestEnvironment, create_test_package_with_setup +from tests.lib import ( + PipTestEnvironment, + create_really_basic_wheel, + create_test_package_with_setup, +) def matches_expected_lines(string: str, expected_lines: Collection[str]) -> bool: @@ -321,3 +325,26 @@ def test_check_include_work_dir_pkg(script: PipTestEnvironment) -> None: expected_lines = ("simple 1.0 requires missing, which is not installed.",) assert matches_expected_lines(result.stdout, expected_lines) assert result.returncode == 1 + + +def test_check_unsupported( + script: PipTestEnvironment, +) -> None: + script.scratch_path.joinpath("base-0.1.0-py2.py3-none-any.whl").write_bytes( + create_really_basic_wheel("base", "0.1.0") + ) + script.pip( + "install", + "--no-cache-dir", + "--no-index", + "--find-links", + script.scratch_path, + "base==0.1.0", + ) + with open( + script.site_packages_path.joinpath("base-0.1.0.dist-info/WHEEL"), "a" + ) as f: + f.write("\nTag: cp310-cp310-musllinux_1_1_x86_64\n") + result = script.pip("check", expect_error=True) + assert "base 0.1.0 is not supported on this platform" in result.stdout + assert result.returncode == 1