Repository: pex-tool/pex

Issue:
Pex skipping pandas activation
Running `pex pandas==1.0.5 -v` using python 3.6.2 outputs.
 ``pex: Skipping activation of `pandas==1.0.5; python_version >= "3.6.1"` due to environment marker de-selection``
Thus `import pandas` results in :
`Traceback (most recent call last):
  File "<console>", line 1, in <module>
ModuleNotFoundError: No module named 'pandas'`


Discussion:
It works if i use pex==2.1.6 and fails for everything about that version
In pex 2.1.7 we started honoring python_requires metadata in #923 fixing #898 which explains your observation that this problem does not exist in pex 2.1.6.

It looks like that fix has a bug when converting `Requires-Python` wheel metadata into an equivalent environment marker:
https://github.com/pantsbuild/pex/blob/e5b62caddbf4070521980f54984a01ed55251bf9/pex/resolver.py#L103-L117

Whereas [`Requires-Python`](https://packaging.python.org/specifications/core-metadata/#requires-python) allows for full version specifiers as per [PEP-440](https://www.python.org/dev/peps/pep-0440/), The `python_version` environment marker just uses the major and minor versions (https://www.python.org/dev/peps/pep-0508/#environment-markers) so converting all `Requires-Python` constraints to `python_version` constraints is incorrect. For any constraint that includes a patch level in the version constraint the `python_full_version` environment marker should be used.

Thanks for pinning down the Pex version where things went wrong, that was helpful pinpointing this.

Resolving patch:
diff --git a/pex/resolver.py b/pex/resolver.py
--- a/pex/resolver.py
+++ b/pex/resolver.py
@@ -23,6 +23,8 @@
 from pex.platforms import Platform
 from pex.requirements import local_project_from_requirement, local_projects_from_requirement_file
 from pex.third_party.packaging.markers import Marker
+from pex.third_party.packaging.version import Version
+from pex.third_party.packaging.version import parse as parse_version
 from pex.third_party.pkg_resources import Distribution, Environment, Requirement
 from pex.tracer import TRACER
 from pex.util import CacheHelper
@@ -130,11 +132,22 @@ def to_requirement(self, dist):
         # + https://www.python.org/dev/peps/pep-0508/#environment-markers
         python_requires = dist_metadata.requires_python(dist)
         if python_requires:
+
+            def choose_marker(version):
+                # type: (str) -> str
+                parsed_version = parse_version(version)
+                if type(parsed_version) != Version or len(parsed_version.release) > 2:
+                    return "python_full_version"
+                else:
+                    return "python_version"
+
             markers.update(
                 Marker(python_version)
                 for python_version in sorted(
-                    "python_version {operator} {version!r}".format(
-                        operator=specifier.operator, version=specifier.version
+                    "{marker} {operator} {version!r}".format(
+                        marker=choose_marker(specifier.version),
+                        operator=specifier.operator,
+                        version=specifier.version,
                     )
                     for specifier in python_requires
                 )


Test patch:
diff --git a/tests/test_integration.py b/tests/test_integration.py
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -2413,3 +2413,20 @@ def test_resolve_arbitrary_equality_issues_940():
         stdout, returncode = run_simple_pex(pex_file, args=["-c", "import foo"])
         assert returncode == 0
         assert stdout == b""
+
+
+def test_resolve_python_requires_full_version_issues_1017():
+    # type: () -> None
+    python36 = ensure_python_interpreter(PY36)
+    result = run_pex_command(
+        python=python36,
+        args=[
+            "pandas==1.0.5",
+            "--",
+            "-c",
+            "import pandas; print(pandas._version.get_versions()['version'])",
+        ],
+        quiet=True,
+    )
+    result.assert_success()
+    assert "1.0.5" == result.output.strip()