Repository: pex-tool/pex

Issue:
PEX direct requirement metadata for resolves via Pip is incorrect.
For example:
```
$ python -mpex sanic.routing==0.5.0 -o pip-resolve.pex
^jsirois@gill ~/dev/pantsbuild/pex (main) $ ./pip-resolve.pex -c 'import sanic_routing'
Traceback (most recent call last):
  File "/home/jsirois/dev/pantsbuild/pex/pip-resolve.pex/.bootstrap/pex/pex.py", line 483, in execute
  File "/home/jsirois/dev/pantsbuild/pex/pip-resolve.pex/.bootstrap/pex/pex.py", line 400, in _wrap_coverage
  File "/home/jsirois/dev/pantsbuild/pex/pip-resolve.pex/.bootstrap/pex/pex.py", line 431, in _wrap_profiling
  File "/home/jsirois/dev/pantsbuild/pex/pip-resolve.pex/.bootstrap/pex/pex.py", line 542, in _execute
  File "/home/jsirois/dev/pantsbuild/pex/pip-resolve.pex/.bootstrap/pex/pex.py", line 582, in execute_interpreter
  File "/home/jsirois/dev/pantsbuild/pex/pip-resolve.pex/.bootstrap/pex/pex.py", line 649, in execute_content
  File "/home/jsirois/dev/pantsbuild/pex/pip-resolve.pex/.bootstrap/pex/compatibility.py", line 93, in exec_function
  File "-c <cmd>", line 1, in <module>
ModuleNotFoundError: No module named 'sanic_routing'
$ python -mpex sanic.routing==0.5.0 --pex-repository pip-resolve.pex -o pex-resolve.pex
$ ./pex-resolve.pex -c 'import sanic_routing'
$
```

In other words - PEX files resolved via either Pip or a --pex-repository both have the same distributions but not the same direct requirements:
```diff
$ diff <(pex-tools pip-resolve.pex info -i4) <(pex-tools pex-resolve.pex info -i4)
21c21
<     "pex_hash": "8afa05bc6600a8cac0e42ad2493e371a0659c5c4",
---
>     "pex_hash": "e52d16e483c67467a895e567e027819b78e78fc1",
23c23,25
<     "requirements": [],
---
>     "requirements": [
>         "sanic.routing==0.5.0"
>     ],

```

Where the distributions embedded in both are:
```
$ pex-tools pip-resolve.pex info | jq .distributions
{
  "sanic_routing-0.5.0-py3-none-any.whl": "1acac3815f82bcb444980f7a3a4d24e8d6df2f56"
}
$ pex-tools pex-resolve.pex info | jq .distributions
{
  "sanic_routing-0.5.0-py3-none-any.whl": "1acac3815f82bcb444980f7a3a4d24e8d6df2f56"
}
```


Discussion:

Resolving patch:
diff --git a/pex/environment.py b/pex/environment.py
--- a/pex/environment.py
+++ b/pex/environment.py
@@ -17,7 +17,7 @@
 from pex.distribution_target import DistributionTarget
 from pex.inherit_path import InheritPath
 from pex.orderedset import OrderedSet
-from pex.pep_503 import ProjectName
+from pex.pep_503 import ProjectName, distribution_satisfies_requirement
 from pex.pex_info import PexInfo
 from pex.third_party.packaging import specifiers, tags
 from pex.third_party.pkg_resources import Distribution, Requirement
@@ -75,12 +75,7 @@ def maximum(cls, distribution=None):
 
     def satisfies(self, requirement):
         # type: (Requirement) -> bool
-        # N.B.: Although Requirement.__contains__ handles Distributions directly, it compares the
-        # Distribution key with the Requirement key and these keys are not properly canonicalized
-        # per PEP-503; so we compare project names here on our own.
-        if ProjectName(self.distribution) != ProjectName(requirement):
-            return False
-        return self.distribution.version in requirement
+        return distribution_satisfies_requirement(self.distribution, requirement)
 
 
 @attr.s(frozen=True)
diff --git a/pex/pep_503.py b/pex/pep_503.py
--- a/pex/pep_503.py
+++ b/pex/pep_503.py
@@ -26,7 +26,7 @@ def _canonicalize_project_name(project_nameable):
 
 @attr.s(frozen=True)
 class ProjectName(object):
-    """Encodes a canonicalized project name as per PEP-502.
+    """Encodes a canonicalized project name as per PEP-503.
 
     See: https://www.python.org/dev/peps/pep-0503/#normalized-names
     """
@@ -36,3 +36,22 @@ class ProjectName(object):
     def __str__(self):
         # type: () -> str
         return self.project_name
+
+
+def distribution_satisfies_requirement(
+    distribution,  # type: Distribution
+    requirement,  # type: Requirement
+):
+    # type: (...) -> bool
+    """Determines if the given distribution satisfies the given requirement.
+
+    N.B.: Any environment markers present in the requirement are not evaluated. The requirement is
+    considered satisfied if project names match and the distribution version is in the requirement's
+    specified range, if any.
+    """
+    # N.B.: Although Requirement.__contains__ handles Distributions directly, it compares the
+    # Distribution key with the Requirement key and these keys are not properly canonicalized
+    # per PEP-503; so we compare project names here on our own.
+    if ProjectName(distribution) != ProjectName(requirement):
+        return False
+    return distribution.version in requirement
diff --git a/pex/resolver.py b/pex/resolver.py
--- a/pex/resolver.py
+++ b/pex/resolver.py
@@ -17,7 +17,7 @@
 from pex.jobs import Raise, SpawnedJob, execute_parallel
 from pex.network_configuration import NetworkConfiguration
 from pex.orderedset import OrderedSet
-from pex.pep_503 import ProjectName
+from pex.pep_503 import ProjectName, distribution_satisfies_requirement
 from pex.pex_info import PexInfo
 from pex.pip import PackageIndexConfiguration, get_pip
 from pex.platforms import Platform
@@ -708,9 +708,8 @@ def add_installation(install_result):
             distribution = installed_distribution.distribution
             direct_reqs = [
                 req
-                for req in direct_requirements_by_project_name.get(ProjectName(distribution), ())
-                if req
-                and distribution in req
+                for req in direct_requirements_by_project_name[ProjectName(distribution)]
+                if distribution_satisfies_requirement(distribution, req)
                 and installed_distribution.target.requirement_applies(req)
             ]
             if len(direct_reqs) > 1:


Test patch:
diff --git a/tests/test_integration.py b/tests/test_integration.py
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -3381,12 +3381,28 @@ def test_pex_repository_pep503_issues_1302(tmpdir):
                 "Foo._-BAR==1.0.0",
                 "-o",
                 repository_pex,
+                "--include-tools",
             ]
         ).assert_success()
 
+    repository_info = subprocess.check_output(
+        args=[repository_pex, "info"], env=make_env(PEX_TOOLS=1)
+    )
+    assert ["Foo._-BAR==1.0.0"] == json.loads(repository_info.decode("utf-8"))["requirements"]
+
     foo_bar_pex = os.path.join(str(tmpdir), "foo-bar.pex")
     run_pex_command(
-        args=["--pex-repository", repository_pex, "Foo._-BAR==1.0.0", "-o", foo_bar_pex]
+        args=[
+            "--pex-repository",
+            repository_pex,
+            "Foo._-BAR==1.0.0",
+            "-o",
+            foo_bar_pex,
+            "--include-tools",
+        ]
     ).assert_success()
 
+    foo_bar_info = subprocess.check_output(args=[foo_bar_pex, "info"], env=make_env(PEX_TOOLS=1))
+    assert ["Foo._-BAR==1.0.0"] == json.loads(foo_bar_info.decode("utf-8"))["requirements"]
+
     subprocess.check_call(args=[foo_bar_pex, "-c", "import foo_bar"])