Repository: pypa/setuptools

Issue:
setuptools 36.2.2 does not allow empty 'extras_require' list
In one of my projects, I'm using the empty `extras_require` list syntax from https://setuptools.readthedocs.io/en/latest/setuptools.html#declaring-extras-optional-features-with-their-own-dependencies, but `setuptools` is throwing errors under 36.2.2 that weren't there in 36.2.0.

```
$ cat setup.py
from setuptools import setup

setup(
    name='testemptyextras',
    extras_require={'empty': []}
)
```

With `setuptools==36.2.0`:
```
$ pip install -e '.[empty]'
Obtaining file:///Users/amw/Desktop/setuptools
Installing collected packages: testemptyextras
  Running setup.py develop for testemptyextras
Successfully installed testemptyextras
```

However, under `setuptools==36.2.2`:
```
$ pip install -e '.[empty]'
Obtaining file:///Users/amw/Desktop/setuptools
  testemptyextras 0.0.0 does not provide the extra 'empty'
Installing collected packages: testemptyextras
  Running setup.py develop for testemptyextras
Successfully installed testemptyextras
```

 This results in downstream errors when I try to load related dependencies via `pkg_resources`:
```
Traceback (most recent call last):
  File "/apps/python/lib/python2.7/site-packages/stevedore/extension.py", line 163, in _load_plugins
    verify_requirements,
  File "/apps/python/lib/python2.7/site-packages/stevedore/named.py", line 123, in _load_one_plugin
    verify_requirements,
  File "/apps/python/lib/python2.7/site-packages/stevedore/extension.py", line 183, in _load_one_plugin
    ep.require()
  File "/apps/python/lib/python2.7/site-packages/pkg_resources/__init__.py", line 2326, in require
    reqs = self.dist.requires(self.extras)
  File "/apps/python/lib/python2.7/site-packages/pkg_resources/__init__.py", line 2579, in requires
    "%s has no such extra feature %r" % (self, ext)
UnknownExtra: Stethoscope 0.0.1 has no such extra feature 'atlas'
```


Discussion:

Resolving patch:
diff --git a/setuptools/dist.py b/setuptools/dist.py
--- a/setuptools/dist.py
+++ b/setuptools/dist.py
@@ -369,6 +369,8 @@ def _convert_extras_requirements(self):
         spec_ext_reqs = getattr(self, 'extras_require', None) or {}
         self._tmp_extras_require = defaultdict(list)
         for section, v in spec_ext_reqs.items():
+            # Do not strip empty sections.
+            self._tmp_extras_require[section]
             for r in pkg_resources.parse_requirements(v):
                 suffix = self._suffix_for(r)
                 self._tmp_extras_require[section + suffix].append(r)


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
@@ -274,6 +274,8 @@ def test_extras_require_with_extra_and_marker_in_req(self, tmpdir_cwd, env):
         with open(requires_txt) as fp:
             install_requires = fp.read()
         expected_requires = DALS('''
+             [extra]
+
              [extra:{marker}]
              barbazquux[test]
              ''').format(marker=self.mismatch_marker_alternate)
@@ -306,6 +308,8 @@ def test_extras_require_with_marker_in_req(self, tmpdir_cwd, env):
         with open(requires_txt) as fp:
             install_requires = fp.read()
         expected_requires = DALS('''
+             [extra]
+
              [extra:{marker}]
              barbazquux
              ''').format(marker=self.mismatch_marker_alternate)
@@ -328,6 +332,20 @@ def test_extras_require_with_invalid_marker_in_req(self, tmpdir_cwd, env):
             self._run_install_command(tmpdir_cwd, env)
         assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []
 
+    def test_extras_require_with_empty_section(self, tmpdir_cwd, env):
+        tmpl = 'extras_require={{"empty": []}},'
+        req = tmpl.format(marker=self.invalid_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('''
+             [empty]
+             ''').format(marker=self.mismatch_marker_alternate)
+        assert install_requires.lstrip() == expected_requires
+
     def test_python_requires_egg_info(self, tmpdir_cwd, env):
         self._setup_script_with_requires(
             """python_requires='>=2.7.12',""")