Repository: pypa/pip

Issue:
Wrong conflict is reported when both compatible and incompatible python_requires exist
**Environment**

* pip version: 20.3.3, 21.0.1
* Python version: 3.9.1, 3.8.7
* OS: macOS Catalina, Docker python:3.9, Docker python:3.8

**Description**
Getting this weird error on one of our internal libraries (git+ssh) that has a `python_requires=">=3.6"`:

```
ERROR: Package 'xxxxx' requires a different Python: 3.9.1 not in '>=3.6'
```

Installing the package directly works (`pip install git+ssh://...` as well as `pip install -e .`), but it fails on the requirements file (which has other git+ssh calls, but those other ones don't have `python_requires`).

**Expected behavior**
Successfully installs the package.

**How to Reproduce**
Having trouble creating an "isolated" testcase. The three factors seem to be: (1) requirements.txt; (2) git+ssh; (3) python_requires in the setup.py of the dependency (Django, for example, has python_requires >= 3.6 in their setup.cfg, but it installs successfully).

**Output**

```
Paste the output of the steps above, including the commands themselves and
pip's output/traceback etc.
```

```pip install -r requirements.txt
# ...
Collecting xxxxx==0.0.3
  Cloning ssh://****@github.com/yyyyy/xxxxx (to revision 0.0.3) to /tmp/pip-install-ndn6x7f3/xxxxx_4765c045eeb2421f85443ae590d6fa3b
# ... 
INFO: pip is looking at multiple versions of xxxxx to determine which version is compatible with other requirements. This could take a while.
ERROR: Package 'xxxxx' requires a different Python: 3.9.1 not in '>=3.6'
```

Have removed the package info, but should not be relevant in this case since it _is_ possible to install this package.


Discussion:
What happens if you remove `python_requires` but install with your existing requirements.txt? I suspect the error message is misleading and the real issue lies elsewhere.
Change in branch:
![image](https://user-images.githubusercontent.com/2257119/106501868-7c591c80-6478-11eb-93d7-cae21d2dd485.png)

Without changing anything else, got: 
```
INFO: pip is looking at multiple versions of zzzzz to determine which version is compatible with other requirements. This could take a while.
INFO: pip is looking at multiple versions of yyyyy to determine which version is compatible with other requirements. This could take a while.
INFO: pip is looking at multiple versions of xxxxx to determine which version is compatible with other requirements. This could take a while.
ERROR: Package 'ppppp' requires a different Python: 3.9.1 not in '<3.7,>=2.7'
```
(where xxxxx is the package that we removed python_requires on, and ppppp is a package that was part of the same requirements file. it is a valid error this time)

Looks like pip was reporting the wrong `python_requires` error in the error message. The next step would be to strip the failure as much as possible while keeping it fail, until we get something that contains no sensitive information. Would you be able to do itfor us? This would be much easier than trying to come up with an example from scratch (an has risk to not actually match your situation).
Got pretty lazy, but here's a reproducible example:

1) clone https://github.com/pachewise/exampleA and https://github.com/pachewise/exampleB
2) `pip install ./exampleA ./exampleB`

![image](https://user-images.githubusercontent.com/2257119/106698600-f7642500-6595-11eb-9b73-e257a6a3797b.png)


The order of the dependencies doesn't seem to matter (note that, for these repos, git+ssh install will not work. Didn't try to figure out why)
Awesome! It’s perfectly fine they don’t work via git+ssh; they’ll need to be converted into a local direcotry to put in the test suite eventually anyway.

Resolving patch:
diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py
--- 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.


Test patch:
diff --git a/tests/functional/test_new_resolver_errors.py b/tests/functional/test_new_resolver_errors.py
--- 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)