Repository: pypa/setuptools

Issue:
36.2.1 appears to ignore install_requires with extras
Travis, Appveyor, and local builds of magic-wormhole started failing about an hour ago, when 36.2.1 was released. Our setup.py has:

```python
      install_requires=[
          "spake2==0.7", "pynacl",
          "six",
          "twisted[tls] >= 17.5.0", # 17.5.0 adds failAfterFailures=
          "autobahn[twisted] >= 0.14.1",
          "automat",
          "hkdf",
          "tqdm >= 4.13.0", # 4.13.0 fixes crash on NetBSD
          "click",
          "humanize",
          "ipaddress",
          "txtorcon >= 0.19.3",
      ],
```

When I run `pip install -e .` in a virtualenv with setuptools-36.2.0, I see Autobahn-17.7.1 installed as expected. If I have setuptools-36.2.1, then Autobahn does *not* get installed. (Twisted *does* get installed, but it's a dependency of txtorcon).



Discussion:
Should that be `pip install -e '.[twisted]'`?
Because of the extra specifiers:
```python
setup(install_requires=["autobahn[twisted] >= 0.14.1"])
```
is equivalent to
```python
setup(extras_require={'twisted': ["autobahn >= 0.14.1"]})
```
Previously those were ignored.
Nope, in this case, the un-extra-ed magic-wormhole installation requires both the base `autobahn` and autobahn's twisted support. (magic-wormhole depends upon twisted directly, but when you ask autobahn to give you its twisted support, it could conceivably depend upon something beyond just twisted, maybe some tx*foo package).

Depending upon `autobahn` instead of `autobahn[twisted]` gets you a subset of the functionality that we need.

The only extras that magic-wormhole supports are `pip install magic-wormhole[dev]`, and a `:sys_platform=="win32"` that gets us some extra windows support.

Oh, what? Have I been mis-expressing this all this time?

What's the syntax to say "I want dependency X and also X's extra Y"?
No, I think I'm the one being wrong, and I have introduced a regression...
https://www.python.org/dev/peps/pep-0508/#extras
Whew, ok, good :). I looked at https://setuptools.readthedocs.io/en/latest/setuptools.html#declaring-extras-optional-features-with-their-own-dependencies , and it seems to match the way magic-wormhole's setup.py is doing things.

Resolving patch:
diff --git a/setuptools/dist.py b/setuptools/dist.py
--- a/setuptools/dist.py
+++ b/setuptools/dist.py
@@ -376,7 +376,7 @@ def _convert_extras_requirements(self):
     def _move_install_requirements_markers(self):
         """
         Move requirements in `install_requires` that are using environment
-        markers or extras to `extras_require`.
+        markers `extras_require`.
         """
 
         # divide the install_requires into two sets, simple ones still
@@ -384,7 +384,7 @@ def _move_install_requirements_markers(self):
         # by extras_require.
 
         def is_simple_req(req):
-            return not req.marker and not req.extras
+            return not req.marker
 
         spec_inst_reqs = getattr(self, 'install_requires', None) or ()
         simple_reqs = filter(
@@ -398,9 +398,7 @@ def is_simple_req(req):
         self.install_requires = list(map(str, simple_reqs))
 
         for r in complex_reqs:
-            suffix = ':' + str(r.marker) if r.marker else ''
-            for section in r.extras or ('',):
-                self._tmp_extras_require[section + suffix].append(r)
+            self._tmp_extras_require[':' + str(r.marker)].append(r)
         self.extras_require = dict(
             (k, [str(r) for r in map(self._clean_req, v)])
             for k, v in self._tmp_extras_require.items()
@@ -408,9 +406,8 @@ def is_simple_req(req):
 
     def _clean_req(self, req):
         """
-        Given a Requirement, remove extras and markers and return it.
+        Given a Requirement, remove environment markers and return it.
         """
-        req.extras = ()
         req.marker = None
         return req
 


Test patch:
diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py
--- a/setuptools/tests/test_egg_info.py
+++ b/setuptools/tests/test_egg_info.py
@@ -207,14 +207,13 @@ def test_install_requires_with_marker(self, tmpdir_cwd, env):
     def test_install_requires_with_extra(self, tmpdir_cwd, env):
         req = 'install_requires=["barbazquux [test]"],'
         self._setup_script_with_requires(req)
-        self._run_install_command(tmpdir_cwd, env)
-        egg_info_dir = self._find_egg_info_files(env.paths['lib']).base
+        self._run_install_command(tmpdir_cwd, env, cmd=['egg_info'])
+        egg_info_dir = os.path.join('.', 'foo.egg-info')
         requires_txt = os.path.join(egg_info_dir, 'requires.txt')
         with open(requires_txt) as fp:
             install_requires = fp.read()
         expected_requires = DALS('''
-             [test]
-             barbazquux
+             barbazquux[test]
              ''')
         assert install_requires.lstrip() == expected_requires
         assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []
@@ -229,8 +228,8 @@ def test_install_requires_with_extra_and_marker(self, tmpdir_cwd, env):
         with open(requires_txt) as fp:
             install_requires = fp.read()
         expected_requires = DALS('''
-             [test:{marker}]
-             barbazquux
+             [:{marker}]
+             barbazquux[test]
              ''').format(marker=self.mismatch_marker_alternate)
         assert install_requires.lstrip() == expected_requires
         assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []
@@ -250,6 +249,37 @@ def test_tests_require_with_markers(self, tmpdir_cwd, env):
             tmpdir_cwd, env, cmd=['test'], output="Ran 0 tests in")
         assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []
 
+    def test_extras_require_with_extra(self, tmpdir_cwd, env):
+        req = 'extras_require={"extra": ["barbazquux [test]"]},'
+        self._setup_script_with_requires(req)
+        self._run_install_command(tmpdir_cwd, env, cmd=['egg_info'])
+        egg_info_dir = os.path.join('.', 'foo.egg-info')
+        requires_txt = os.path.join(egg_info_dir, 'requires.txt')
+        with open(requires_txt) as fp:
+            install_requires = fp.read()
+        expected_requires = DALS('''
+             [extra]
+             barbazquux[test]
+             ''')
+        assert install_requires.lstrip() == expected_requires
+        assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []
+
+    def test_extras_require_with_extra_and_marker_in_req(self, tmpdir_cwd, env):
+        tmpl = 'extras_require={{"extra": ["barbazquux [test]; {marker}"]}},'
+        req = tmpl.format(marker=self.mismatch_marker)
+        self._setup_script_with_requires(req)
+        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')
+        with open(requires_txt) as fp:
+            install_requires = fp.read()
+        expected_requires = DALS('''
+             [extra:{marker}]
+             barbazquux[test]
+             ''').format(marker=self.mismatch_marker_alternate)
+        assert install_requires.lstrip() == expected_requires
+        assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []
+
     def test_extras_require_with_marker(self, tmpdir_cwd, env):
         tmpl = 'extras_require={{":{marker}": ["barbazquux"]}},'
         req = tmpl.format(marker=self.mismatch_marker)