Repository: pex-tool/pex

Issue:
`Failure to resolve compatible distributions` with requirements file generated by pip-tools
Hello fellow PEX maintainers,

First a big thank for your project that is widely used in `Criteo` company to distribute our Python applications :)

Let me present you the issue I'm facing when using both PEX and pip-tools on some projects.
It did create a dummy project to highlight my issue and to allow to reproduce my bug : https://github.com/Djailla/pex_bug

Env :
- python 3.9
- latest pex release
- latest pip-tools release

Thanks in advance for the support

Bastien (alias @Djailla)


Discussion:
Hrmph. Jaroco is just having fun flouting quirks of the rules for package names. Note the dash vs dot:
```
jaraco-functools==3.5.0 but jaraco.functools
```

I'll dig a bit more to see exactly how Pex is getting this wrong. I think it deals with `[_.-]` normalization in project names at least some places.
@Djailla to work around you can pass `--ignore-errors`, but this also looks like a quick fix. I'm pretty sure I can get out a 2.1.81 release with the proper fix today.
@jsirois my local fix was to replace `.` by `-` for the jaroco related packages, there is no rush, I can wait for a future release. Thanks a lot for the quick answer !

Resolving patch:
diff --git a/pex/resolver.py b/pex/resolver.py
--- a/pex/resolver.py
+++ b/pex/resolver.py
@@ -623,6 +623,7 @@ def __init__(
         use_pep517=None,  # type: Optional[bool]
         build_isolation=True,  # type: bool
         verify_wheels=True,  # type: bool
+        allow_prereleases=False,  # type: bool
     ):
         # type: (...) -> None
         self._build_requests = tuple(build_requests)
@@ -638,6 +639,7 @@ def __init__(
             build_isolation=build_isolation,
             verify_wheels=verify_wheels,
         )
+        self._allow_prereleases = allow_prereleases
 
     @staticmethod
     def _categorize_install_requests(
@@ -796,7 +798,9 @@ def add_installation(install_result):
 
         if not ignore_errors:
             with TRACER.timed("Checking install"):
-                self._check_install(installations)
+                self._check_install(
+                    installations,
+                )
 
         installed_distributions = OrderedSet()  # type: OrderedSet[InstalledDistribution]
         for installed_distribution in installations:
@@ -812,8 +816,7 @@ def add_installation(install_result):
             )
         return installed_distributions
 
-    @staticmethod
-    def _check_install(installed_distributions):
+    def _check_install(self, installed_distributions):
         # type: (Iterable[InstalledDistribution]) -> None
         installed_distribution_by_project_name = OrderedDict(
             (ProjectName(resolved_distribution.distribution), resolved_distribution)
@@ -839,7 +842,9 @@ def _check_install(installed_distributions):
                     )
                 else:
                     installed_dist = installed_requirement_dist.distribution
-                    if installed_dist not in requirement:
+                    if not requirement.specifier.contains(
+                        installed_dist.version, prereleases=self._allow_prereleases
+                    ):
                         unsatisfied.append(
                             "{dist} requires {requirement} but {resolved_dist} was resolved".format(
                                 dist=dist.as_requirement(),
@@ -1022,6 +1027,7 @@ def resolve(
         use_pep517=use_pep517,
         build_isolation=build_isolation,
         verify_wheels=verify_wheels,
+        allow_prereleases=allow_prereleases,
     )
 
     ignore_errors = ignore_errors or not transitive


Test patch:
diff --git a/tests/integration/test_issue_1726.py b/tests/integration/test_issue_1726.py
new file mode 100644
--- /dev/null
+++ b/tests/integration/test_issue_1726.py
@@ -0,0 +1,67 @@
+# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md).
+# Licensed under the Apache License, Version 2.0 (see LICENSE).
+
+from __future__ import absolute_import
+
+import os
+import subprocess
+import sys
+from textwrap import dedent
+
+import pytest
+
+from pex.common import safe_open
+from pex.testing import run_pex_command
+from pex.typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from typing import Any
+
+
+@pytest.mark.skipif(
+    sys.version_info[:2] < (3, 7),
+    reason="The jaraco-collections 3.5.1 distribution requires Python >=3.7",
+)
+def test_check_install_issue_1726(tmpdir):
+    # type: (Any) -> None
+
+    src = os.path.join(str(tmpdir), "src")
+    with safe_open(os.path.join(src, "setup.py"), "w") as fp:
+        fp.write(
+            dedent(
+                """\
+                from setuptools import setup
+
+                setup(
+                    name="pex-test",
+                    version='0.1',
+                    install_requires=[
+                        "jaraco-collections==3.5.1",
+                    ]
+                )
+                """
+            )
+        )
+
+    pex_root = os.path.join(str(tmpdir), "pex_root")
+    pex_args = [
+        "--pex-root",
+        pex_root,
+        "--runtime-pex-root",
+        pex_root,
+        src,
+        "--",
+        "-c",
+        "from jaraco import collections; print(collections.__file__)",
+    ]
+    old_result = run_pex_command(args=["pex==2.1.80", "-c", "pex", "--"] + pex_args)
+    old_result.assert_failure()
+    assert (
+        "Failed to resolve compatible distributions:\n"
+        "1: pex-test==0.1 requires jaraco-collections==3.5.1 but jaraco.collections 3.5.1 was "
+        "resolved" in old_result.error
+    )
+
+    new_result = run_pex_command(args=pex_args)
+    new_result.assert_success()
+    assert new_result.output.startswith(pex_root)