Repository: pypa/setuptools
Pull request #530 (2016-04-04 06:29:02)

Restore evaluating environment markers in WorkingSet
Correctly deal with parsed requirements that include extras as a
marker inside WorkingSet that are populated from METADATA inside
wheels, like we get from pip>=7.

This partially reverts commit 04d10ff025e1cbef7ec93a2008c930e856045c8a.

Closes: #523


Patch:
diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py
index eb84f4ba1..4f0a487e0 100644
--- a/pkg_resources/__init__.py
+++ b/pkg_resources/__init__.py
@@ -791,6 +791,8 @@ class WorkingSet(object):
         # key -> dist
         best = {}
         to_activate = []
+        # Map requirement to the extras that require it
+        extra_req_mapping = {}
 
         # Mapping of requirement to set of distributions that required it;
         # useful for reporting info about conflicts.
@@ -804,9 +806,14 @@ class WorkingSet(object):
                 continue
             # If the req has a marker, evaluate it -- skipping the req if
             # it evaluates to False.
-            # https://github.com/pypa/setuptools/issues/523
-            _issue_523_bypass = True
-            if not _issue_523_bypass and req.marker and not req.marker.evaluate():
+            if req.marker:
+                result = []
+                if req in extra_req_mapping:
+                    for extra in extra_req_mapping[req] or ['']:
+                        result.append(req.marker.evaluate({'extra': extra}))
+                else:
+                    result.append(req.marker.evaluate())
+                if not any(result):
                     continue
             dist = best.get(req.key)
             if dist is None:
@@ -840,6 +847,7 @@ class WorkingSet(object):
             # Register the new requirements needed by req
             for new_requirement in new_requirements:
                 required_by[new_requirement].add(req.project_name)
+                extra_req_mapping[new_requirement] = req.extras
 
             processed[req] = True
 


Test patch:
diff --git a/pkg_resources/tests/test_resources.py b/pkg_resources/tests/test_resources.py
index 98b8dcd7b..7f86c7978 100644
--- a/pkg_resources/tests/test_resources.py
+++ b/pkg_resources/tests/test_resources.py
@@ -171,16 +171,85 @@ class TestDistro:
         msg = 'Foo 0.9 is installed but Foo==1.2 is required'
         assert vc.value.report() == msg
 
-    @pytest.mark.xfail(reason="Functionality disabled; see #523")
-    def test_environment_markers(self):
-        """
-        Environment markers are evaluated at resolution time.
-        """
+    def test_environment_marker_evaluation_negative(self):
+        """Environment markers are evaluated at resolution time."""
         ad = pkg_resources.Environment([])
         ws = WorkingSet([])
         res = ws.resolve(parse_requirements("Foo;python_version<'2'"), ad)
         assert list(res) == []
 
+    def test_environment_marker_evaluation_positive(self):
+        ad = pkg_resources.Environment([])
+        ws = WorkingSet([])
+        Foo = Distribution.from_filename("/foo_dir/Foo-1.2.dist-info")
+        ad.add(Foo)
+        res = ws.resolve(parse_requirements("Foo;python_version>='2'"), ad)
+        assert list(res) == [Foo]
+
+    def test_marker_evaluation_with_extras(self):
+        """Extras are also evaluated as markers at resolution time."""
+        ad = pkg_resources.Environment([])
+        ws = WorkingSet([])
+        # Metadata needs to be native strings due to cStringIO behaviour in
+        # 2.6, so use str().
+        Foo = Distribution.from_filename(
+            "/foo_dir/Foo-1.2.dist-info",
+            metadata=Metadata(("METADATA", str("Provides-Extra: baz\n"
+                               "Requires-Dist: quux; extra=='baz'")))
+        )
+        ad.add(Foo)
+        assert list(ws.resolve(parse_requirements("Foo"), ad)) == [Foo]
+        quux = Distribution.from_filename("/foo_dir/quux-1.0.dist-info")
+        ad.add(quux)
+        res = list(ws.resolve(parse_requirements("Foo[baz]"), ad))
+        assert res == [Foo,quux]
+
+    def test_marker_evaluation_with_multiple_extras(self):
+        ad = pkg_resources.Environment([])
+        ws = WorkingSet([])
+        # Metadata needs to be native strings due to cStringIO behaviour in
+        # 2.6, so use str().
+        Foo = Distribution.from_filename(
+            "/foo_dir/Foo-1.2.dist-info",
+            metadata=Metadata(("METADATA", str("Provides-Extra: baz\n"
+                               "Requires-Dist: quux; extra=='baz'\n"
+                               "Provides-Extra: bar\n"
+                               "Requires-Dist: fred; extra=='bar'\n")))
+        )
+        ad.add(Foo)
+        quux = Distribution.from_filename("/foo_dir/quux-1.0.dist-info")
+        ad.add(quux)
+        fred = Distribution.from_filename("/foo_dir/fred-0.1.dist-info")
+        ad.add(fred)
+        res = list(ws.resolve(parse_requirements("Foo[baz,bar]"), ad))
+        assert sorted(res) == [fred,quux,Foo]
+
+    def test_marker_evaluation_with_extras_loop(self):
+        ad = pkg_resources.Environment([])
+        ws = WorkingSet([])
+        # Metadata needs to be native strings due to cStringIO behaviour in
+        # 2.6, so use str().
+        a = Distribution.from_filename(
+            "/foo_dir/a-0.2.dist-info",
+            metadata=Metadata(("METADATA", str("Requires-Dist: c[a]")))
+        )
+        b = Distribution.from_filename(
+            "/foo_dir/b-0.3.dist-info",
+            metadata=Metadata(("METADATA", str("Requires-Dist: c[b]")))
+        )
+        c = Distribution.from_filename(
+            "/foo_dir/c-1.0.dist-info",
+            metadata=Metadata(("METADATA", str("Provides-Extra: a\n"
+                               "Requires-Dist: b;extra=='a'\n"
+                               "Provides-Extra: b\n"
+                               "Requires-Dist: foo;extra=='b'")))
+        )
+        foo = Distribution.from_filename("/foo_dir/foo-0.1.dist-info")
+        for dist in (a, b, c, foo):
+            ad.add(dist)
+        res = list(ws.resolve(parse_requirements("a"), ad))
+        assert res == [a, c, b, foo]
+
     def testDistroDependsOptions(self):
         d = self.distRequires("""
             Twisted>=1.5
diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py
index d37567b48..fd5f26fc2 100644
--- a/setuptools/tests/test_egg_info.py
+++ b/setuptools/tests/test_egg_info.py
@@ -104,7 +104,6 @@ class TestEggInfo(object):
             'setup.py': setup_script,
             })
 
-    @pytest.mark.xfail(reason="Functionality disabled; see #523")
     def test_install_requires_with_markers(self, tmpdir_cwd, env):
         self._setup_script_with_requires(
             """install_requires=["barbazquux;python_version<'2'"],""")
@@ -115,14 +114,12 @@ class TestEggInfo(object):
             requires_txt).read().split('\n')
         assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []
 
-    @pytest.mark.xfail(reason="Functionality disabled; see #523")
     def test_setup_requires_with_markers(self, tmpdir_cwd, env):
         self._setup_script_with_requires(
             """setup_requires=["barbazquux;python_version<'2'"],""")
         self._run_install_command(tmpdir_cwd, env)
         assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []
 
-    @pytest.mark.xfail(reason="Functionality disabled; see #523")
     def test_tests_require_with_markers(self, tmpdir_cwd, env):
         self._setup_script_with_requires(
             """tests_require=["barbazquux;python_version<'2'"],""")