Repository: pex-tool/pex

Issue:
Pex does not use the pip 2020-resolver when resolving for Python 2.7
It turns out Pip defaults to the legacy resolver under Python 2.7 and the 2020 resolver under anything else. As such, we need to actively pass --use-feature=2020-resolver when our resolve target is a Python 2.7 interpreter.


Discussion:
👍 

Nit: I think in pip parlance it would be --use-feature=2020-resolver.
Fixed.

Resolving patch:
diff --git a/pex/pip.py b/pex/pip.py
--- a/pex/pip.py
+++ b/pex/pip.py
@@ -28,20 +28,15 @@
 
 class ResolverVersion(object):
     class Value(object):
-        def __init__(
-            self,
-            value,  # type: str
-            pip_args=None,  # type: Optional[Iterable[str]]
-        ):
-            # type: (...) -> None
+        def __init__(self, value):
+            # type: (str) -> None
             self.value = value
-            self.pip_args = tuple(pip_args) if pip_args else ()  # type: Tuple[str, ...]
 
         def __repr__(self):
             # type: () -> str
             return repr(self.value)
 
-    PIP_LEGACY = Value("pip-legacy-resolver", pip_args=("--use-deprecated", "legacy-resolver"))
+    PIP_LEGACY = Value("pip-legacy-resolver")
     PIP_2020 = Value("pip-2020-resolver")
 
     values = PIP_LEGACY, PIP_2020
@@ -233,6 +228,28 @@ def _calculate_resolver_version(package_index_configuration=None):
             else ResolverVersion.PIP_LEGACY
         )
 
+    @classmethod
+    def _calculate_resolver_version_args(
+        cls,
+        interpreter,  # type: PythonInterpreter
+        package_index_configuration=None,  # type: Optional[PackageIndexConfiguration]
+    ):
+        # type: (...) -> Iterator[str]
+        resolver_version = cls._calculate_resolver_version(
+            package_index_configuration=package_index_configuration
+        )
+        # N.B.: The pip default resolver depends on the python it is invoked with. For Python 2.7
+        # Pip defaults to the legacy resolver and for Python 3 Pip defaults to the 2020 resolver.
+        # Further, Pip warns when you do not use the default resolver version for the interpreter
+        # in play. To both avoid warnings and set the correct resolver version, we need
+        # to only set the resolver version when it's not the default for the interpreter in play:
+        if resolver_version == ResolverVersion.PIP_2020 and interpreter.version[0] == 2:
+            yield "--use-feature"
+            yield "2020-resolver"
+        elif resolver_version == ResolverVersion.PIP_LEGACY and interpreter.version[0] == 3:
+            yield "--use-deprecated"
+            yield "legacy-resolver"
+
     def _spawn_pip_isolated(
         self,
         args,  # type: Iterable[str]
@@ -257,7 +274,12 @@ def _spawn_pip_isolated(
             "--exists-action",
             "a",
         ]
-        pip_args.extend(self._calculate_resolver_version(package_index_configuration).pip_args)
+        python_interpreter = interpreter or PythonInterpreter.get()
+        pip_args.extend(
+            self._calculate_resolver_version_args(
+                python_interpreter, package_index_configuration=package_index_configuration
+            )
+        )
         if not package_index_configuration or package_index_configuration.isolated:
             # Don't read PIP_ environment variables or pip configuration files like
             # `~/.config/pip/pip.conf`.
@@ -298,7 +320,7 @@ def _spawn_pip_isolated(
 
             from pex.pex import PEX
 
-            pip = PEX(pex=self._pip_pex_path, interpreter=interpreter)
+            pip = PEX(pex=self._pip_pex_path, interpreter=python_interpreter)
             return pip.cmdline(command), pip.run(
                 args=command, env=env, blocking=False, **popen_kwargs
             )
@@ -438,7 +460,9 @@ def spawn_download_distributions(
         # information and surface it on stderr. See: https://github.com/pypa/pip/issues/9420.
         log = None
         if (
-            self._calculate_resolver_version(package_index_configuration)
+            self._calculate_resolver_version(
+                package_index_configuration=package_index_configuration
+            )
             == ResolverVersion.PIP_2020
         ):
             log = os.path.join(safe_mkdtemp(), "pip.log")


Test patch:
diff --git a/tests/test_integration.py b/tests/test_integration.py
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -2847,3 +2847,18 @@ def test_top_level_environment_markers_issues_899(tmpdir):
     assert (
         1 == returncode
     ), "Expected subprocess32 to be present in the PEX file but not activated for Python 3."
+
+
+def test_2020_resolver_engaged_issues_1179():
+    # type: () -> None
+
+    # The Pip legacy resolver cannot solve the following requirements but the 2020 resolver can.
+    # Use this fact to prove we're plumbing Pip resolver version arguments correctly.
+    pex_args = ["boto3==1.15.6", "botocore>1.17<1.18.7", "--", "-c", "import boto3"]
+
+    results = run_pex_command(args=["--resolver-version", "pip-legacy-resolver"] + pex_args)
+    results.assert_failure()
+    assert "Failed to resolve compatible distributions:" in results.error
+    assert "1: boto3==1.15.6 requires botocore<1.19.0,>=1.18.6 but " in results.error
+
+    run_pex_command(args=["--resolver-version", "pip-2020-resolver"] + pex_args).assert_success()