From 2a5e84e1c4d9c8e4c4236e1eccfa580406a29b6b Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Wed, 3 Feb 2021 16:23:04 +0800 Subject: [PATCH 1/2] Add failing test --- tests/functional/test_new_resolver_errors.py | 30 +++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_new_resolver_errors.py b/tests/functional/test_new_resolver_errors.py index e263f4206b8..b4d63a99695 100644 --- a/tests/functional/test_new_resolver_errors.py +++ b/tests/functional/test_new_resolver_errors.py @@ -1,4 +1,6 @@ -from tests.lib import create_basic_wheel_for_package +import sys + +from tests.lib import create_basic_wheel_for_package, create_test_package_with_setup def test_new_resolver_conflict_requirements_file(tmpdir, script): @@ -45,3 +47,29 @@ def test_new_resolver_conflict_constraints_file(tmpdir, script): message = "The user requested (constraint) pkg!=1.0" assert message in result.stdout, str(result) + + +def test_new_resolver_requires_python_error(script): + compatible_python = ">={0.major}.{0.minor}".format(sys.version_info) + incompatible_python = "<{0.major}.{0.minor}".format(sys.version_info) + + pkga = create_test_package_with_setup( + script, + name="pkga", + version="1.0", + python_requires=compatible_python, + ) + pkgb = create_test_package_with_setup( + script, + name="pkgb", + version="1.0", + python_requires=incompatible_python, + ) + + # This always fails because pkgb can never be satisfied. + result = script.pip("install", "--no-index", pkga, pkgb, expect_error=True) + + # The error message should mention the Requires-Python: value causing the + # conflict, not the compatible one. + assert incompatible_python in result.stderr, str(result) + assert compatible_python not in result.stderr, str(result) From d87abdfdfe5175d76019ec2ce52b77656f3a3779 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Wed, 3 Feb 2021 16:47:22 +0800 Subject: [PATCH 2/2] Fix error reporting on Requires-Python conflicts --- news/9541.bugfix.rst | 1 + .../resolution/resolvelib/factory.py | 57 ++++++++++++------- 2 files changed, 37 insertions(+), 21 deletions(-) create mode 100644 news/9541.bugfix.rst diff --git a/news/9541.bugfix.rst b/news/9541.bugfix.rst new file mode 100644 index 00000000000..88180198c07 --- /dev/null +++ b/news/9541.bugfix.rst @@ -0,0 +1 @@ +Fix incorrect reporting on ``Requires-Python`` conflicts. diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 3181d575336..13ab7e904a0 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -1,6 +1,7 @@ import functools import logging from typing import ( + TYPE_CHECKING, Dict, FrozenSet, Iterable, @@ -60,6 +61,14 @@ UnsatisfiableRequirement, ) +if TYPE_CHECKING: + from typing import Protocol + + class ConflictCause(Protocol): + requirement: RequiresPythonRequirement + parent: Candidate + + logger = logging.getLogger(__name__) C = TypeVar("C") @@ -387,21 +396,25 @@ def get_dist_to_uninstall(self, candidate): ) return None - def _report_requires_python_error( - self, - requirement, # type: RequiresPythonRequirement - template, # type: Candidate - ): - # type: (...) -> UnsupportedPythonVersion - message_format = ( - "Package {package!r} requires a different Python: " - "{version} not in {specifier!r}" - ) - message = message_format.format( - package=template.name, - version=self._python_candidate.version, - specifier=str(requirement.specifier), - ) + def _report_requires_python_error(self, causes): + # type: (Sequence[ConflictCause]) -> UnsupportedPythonVersion + assert causes, "Requires-Python error reported with no cause" + + version = self._python_candidate.version + + if len(causes) == 1: + specifier = str(causes[0].requirement.specifier) + message = ( + f"Package {causes[0].parent.name!r} requires a different " + f"Python: {version} not in {specifier!r}" + ) + return UnsupportedPythonVersion(message) + + message = f"Packages require a different Python. {version} not in:" + for cause in causes: + package = cause.parent.format_for_error() + specifier = str(cause.requirement.specifier) + message += f"\n{specifier!r} (required by {package})" return UnsupportedPythonVersion(message) def _report_single_requirement_conflict(self, req, parent): @@ -427,12 +440,14 @@ def get_installation_error( # If one of the things we can't solve is "we need Python X.Y", # that is what we report. - for cause in e.causes: - if isinstance(cause.requirement, RequiresPythonRequirement): - return self._report_requires_python_error( - cause.requirement, - cause.parent, - ) + requires_python_causes = [ + cause + for cause in e.causes + if isinstance(cause.requirement, RequiresPythonRequirement) + and not cause.requirement.is_satisfied_by(self._python_candidate) + ] + if requires_python_causes: + return self._report_requires_python_error(requires_python_causes) # Otherwise, we have a set of causes which can't all be satisfied # at once.