Repository: pypa/setuptools
Pull request #1081 (2017-07-10 01:11:37)

Fix environment markers handling
While the [documentation](https://github.com/pypa/setuptools/blob/master/docs/setuptools.txt#L767) now points to using environment markers with `install_requires` for declaring platform dependencies, this result in invalid wheels because those are stripped from the final requirements in the embedded metadata.

E.g, using something like this:
```python
from setuptools import setup
setup(
    name='foo',
    install_requires='barbazquux; "foobar" in sys_platform',
)
```
result in `foo.egg-info/requires.txt` containing:
```
barbazquux; "foobar" in sys_platform
```
instead of:
```python
import sys

from setuptools import setup

setup(
    name='foo',
    extras_require={':"foobar" in sys_platform': 'barbazquux'},
)
```
which yields:
```
[:"foobar" in sys_platform]
barbazquux
```

This PR automatically move requirements in `install_requires` that use environment markers to `extras_require` so the egg info is valid and the generated wheels work as expected.

Additionally, I added a check to error out if an `extras_require` requirement is itself using an environment marker, as it's not supported.

~~Note: first commit is a fix for the same issue as #1063, so I'll rebase once this is merged.~~


Patch:
diff --git a/.travis.yml b/.travis.yml
index 4a9a861f3..6d6333f87 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -27,9 +27,6 @@ script:
  #- python -m tox
  - tox
 
- # Check that setuptools can be installed in a clean environment
- - tests/clean_install.sh
-
 deploy:
   provider: pypi
   # Also update server in setup.cfg
diff --git a/setuptools/dist.py b/setuptools/dist.py
index 6b97ed33c..cb887ea61 100644
--- a/setuptools/dist.py
+++ b/setuptools/dist.py
@@ -8,6 +8,7 @@ import distutils.log
 import distutils.core
 import distutils.cmd
 import distutils.dist
+from collections import defaultdict
 from distutils.errors import (DistutilsOptionError, DistutilsPlatformError,
     DistutilsSetupError)
 from distutils.util import rfc822_escape
@@ -134,7 +135,13 @@ def check_extras(dist, attr, value):
                 k, m = k.split(':', 1)
                 if pkg_resources.invalid_marker(m):
                     raise DistutilsSetupError("Invalid environment marker: " + m)
-            list(pkg_resources.parse_requirements(v))
+            for r in pkg_resources.parse_requirements(v):
+                if r.marker:
+                    tmpl = (
+                        "'extras_require' requirements cannot include "
+                        "environment markers, in {section!r}: '{req!s}'"
+                    )
+                    raise DistutilsSetupError(tmpl.format(section=k, req=r))
     except (TypeError, ValueError, AttributeError):
         raise DistutilsSetupError(
             "'extras_require' must be a dictionary whose values are "
@@ -346,6 +353,31 @@ class Distribution(Distribution_parse_config_files, _Distribution):
                 )
         if getattr(self, 'python_requires', None):
             self.metadata.python_requires = self.python_requires
+        self._finalize_requires()
+
+    def _finalize_requires(self):
+        """Move requirements in `install_requires` that
+        are using environment markers to `extras_require`.
+        """
+        if not self.install_requires:
+            return
+        extras_require = defaultdict(list, (
+            (k, list(pkg_resources.parse_requirements(v)))
+            for k, v in (self.extras_require or {}).items()
+        ))
+        install_requires = []
+        for r in pkg_resources.parse_requirements(self.install_requires):
+            marker = r.marker
+            if not marker:
+                install_requires.append(r)
+                continue
+            r.marker = None
+            extras_require[':'+str(marker)].append(r)
+        self.extras_require = dict(
+            (k, [str(r) for r in v])
+            for k, v in extras_require.items()
+        )
+        self.install_requires = [str(r) for r in install_requires]
 
     def parse_config_files(self, filenames=None):
         """Parses configuration files from various levels


Test patch:
diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py
index a32b981d6..55df44897 100644
--- a/setuptools/tests/test_egg_info.py
+++ b/setuptools/tests/test_egg_info.py
@@ -185,8 +185,12 @@ class TestEggInfo(object):
         self._run_install_command(tmpdir_cwd, env)
         egg_info_dir = self._find_egg_info_files(env.paths['lib']).base
         requires_txt = os.path.join(egg_info_dir, 'requires.txt')
-        assert "barbazquux;python_version<'2'" in open(
-            requires_txt).read().split('\n')
+        with open(requires_txt) as fp:
+            install_requires = fp.read()
+        assert install_requires.lstrip() == DALS('''
+                                                 [:python_version < "2"]
+                                                 barbazquux
+                                                 ''')
         assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []
 
     def test_setup_requires_with_markers(self, tmpdir_cwd, env):
@@ -202,10 +206,11 @@ class TestEggInfo(object):
             tmpdir_cwd, env, cmd=['test'], output="Ran 0 tests in")
         assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []
 
-    def test_extra_requires_with_markers(self, tmpdir_cwd, env):
+    def test_extras_require_with_markers(self, tmpdir_cwd, env):
         self._setup_script_with_requires(
-            """extra_requires={":python_version<'2'": ["barbazquux"]},""")
-        self._run_install_command(tmpdir_cwd, env)
+            """extras_require={"extra": ["barbazquux; python_version<'2'"]},""")
+        with pytest.raises(AssertionError):
+            self._run_install_command(tmpdir_cwd, env)
         assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []
 
     def test_python_requires_egg_info(self, tmpdir_cwd, env):
diff --git a/tests/clean_install.sh b/tests/clean_install.sh
deleted file mode 100755
index f8f80dc7c..000000000
--- a/tests/clean_install.sh
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/env bash
-
-# This test was created in
-# https://github.com/pypa/setuptools/pull/1050
-# but it really should be incorporated into the test suite
-# such that it runs on Windows and doesn't depend on
-# virtualenv. Moving to test_integration will likely address
-# those concerns.
-
-set -o errexit
-set -o xtrace
-
-# Create a temporary directory to install the virtualenv in
-VENV_DIR="$(mktemp -d)"
-function cleanup() {
-  rm -rf "$VENV_DIR"
-}
-trap cleanup EXIT
-
-# Create a virtualenv that doesn't have pip or setuptools installed
-wget https://raw.githubusercontent.com/pypa/virtualenv/master/virtualenv.py
-python virtualenv.py --no-wheel --no-pip --no-setuptools "$VENV_DIR"
-source "$VENV_DIR/bin/activate"
-
-# Now try to install setuptools
-python bootstrap.py
-python setup.py install