Repository: pypa/setuptools
Pull request #1922 (2019-11-26 20:06:43)

fix possible issue with transitive build dependencies
Handle the case where a missing transitive build dependency is required by an extra for an already installed build dependency.

Example:
* a test package with a `setup.py` containing: `setup_requires=['dep[extra]']`
* `dep` is already installed, and the dependencies for `extra` are: `['extra_dep']`
* `extra_dep` is not already installed, and `fetch_build_egg('extra_dep; extra =="extra"')` is called

If the marker is not stripped, `pip wheel [...] 'extra_dep; extra =="extra"'` ignores the requirement, and does not build a wheel, but still returns with no error.

<!-- First time contributors: Take a moment to review https://setuptools.readthedocs.io/en/latest/developer-guide.html! -->
<!-- Remove sections if not applicable -->

## Summary of changes

<!-- Summary goes here -->

Closes the second issue found in #1920.

### Pull Request Checklist
- [x] Changes have tests
- [x] News fragment added in changelog.d. See [documentation](http://setuptools.readthedocs.io/en/latest/developer-guide.html#making-a-pull-request) for details


frivoire: It seems I'm impacted by this bug too, with one difference : my use case is dependencies in `tests_require`  and not in `setup_requires`.

So, the proposed fix should be ok (it's in a generic code path that is used for both `tests_require`  and `setup_requires` as far as I understand) :)

Patch:
diff --git a/changelog.d/1922.change.rst b/changelog.d/1922.change.rst
new file mode 100644
index 000000000..837ef9c9e
--- /dev/null
+++ b/changelog.d/1922.change.rst
@@ -0,0 +1,1 @@
+Build dependencies (setup_requires and tests_require) now install transitive dependencies indicated by extras.
diff --git a/setuptools/installer.py b/setuptools/installer.py
index 35bc3cc55..527b95ded 100644
--- a/setuptools/installer.py
+++ b/setuptools/installer.py
@@ -64,8 +64,8 @@ def fetch_build_egg(dist, req):
         pkg_resources.get_distribution('wheel')
     except pkg_resources.DistributionNotFound:
         dist.announce('WARNING: The wheel package is not available.', log.WARN)
-    if not isinstance(req, pkg_resources.Requirement):
-        req = pkg_resources.Requirement.parse(req)
+    # Ignore environment markers; if supplied, it is required.
+    req = strip_marker(req)
     # Take easy_install options into account, but do not override relevant
     # pip environment variables (like PIP_INDEX_URL or PIP_QUIET); they'll
     # take precedence.
@@ -127,3 +127,15 @@ def fetch_build_egg(dist, req):
         dist = pkg_resources.Distribution.from_filename(
             dist_location, metadata=dist_metadata)
         return dist
+
+
+def strip_marker(req):
+    """
+    Return a new requirement without the environment marker to avoid
+    calling pip with something like `babel; extra == "i18n"`, which
+    would always be ignored.
+    """
+    # create a copy to avoid mutating the input
+    req = pkg_resources.Requirement.parse(str(req))
+    req.marker = None
+    return req


Test patch:
diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py
index aa75899a0..f6da1b16c 100644
--- a/setuptools/tests/test_easy_install.py
+++ b/setuptools/tests/test_easy_install.py
@@ -37,6 +37,7 @@ from setuptools.tests import fail_on_ascii
 import pkg_resources
 
 from . import contexts
+from .files import build_files
 from .textwrap import DALS
 
 __metaclass__ = type
@@ -744,6 +745,49 @@ class TestSetupRequires:
         eggs = list(map(str, pkg_resources.find_distributions(os.path.join(test_pkg, '.eggs'))))
         assert eggs == ['dep 1.0']
 
+    def test_setup_requires_with_transitive_extra_dependency(self, monkeypatch):
+        # Use case: installing a package with a build dependency on
+        # an already installed `dep[extra]`, which in turn depends
+        # on `extra_dep` (whose is not already installed).
+        with contexts.save_pkg_resources_state():
+            with contexts.tempdir() as temp_dir:
+                # Create source distribution for `extra_dep`.
+                make_trivial_sdist(os.path.join(temp_dir, 'extra_dep-1.0.tar.gz'), 'extra_dep', '1.0')
+                # Create source tree for `dep`.
+                dep_pkg = os.path.join(temp_dir, 'dep')
+                os.mkdir(dep_pkg)
+                build_files({
+                    'setup.py':
+                    DALS("""
+                          import setuptools
+                          setuptools.setup(
+                              name='dep', version='2.0',
+                              extras_require={'extra': ['extra_dep']},
+                          )
+                         """),
+                    'setup.cfg': '',
+                }, prefix=dep_pkg)
+                # "Install" dep.
+                run_setup(os.path.join(dep_pkg, 'setup.py'), [str('dist_info')])
+                working_set.add_entry(dep_pkg)
+                # Create source tree for test package.
+                test_pkg = os.path.join(temp_dir, 'test_pkg')
+                test_setup_py = os.path.join(test_pkg, 'setup.py')
+                test_setup_cfg = os.path.join(test_pkg, 'setup.cfg')
+                os.mkdir(test_pkg)
+                with open(test_setup_py, 'w') as fp:
+                    fp.write(DALS(
+                        '''
+                        from setuptools import installer, setup
+                        setup(setup_requires='dep[extra]')
+                        '''))
+                # Check...
+                monkeypatch.setenv(str('PIP_FIND_LINKS'), str(temp_dir))
+                monkeypatch.setenv(str('PIP_NO_INDEX'), str('1'))
+                monkeypatch.setenv(str('PIP_RETRIES'), str('0'))
+                monkeypatch.setenv(str('PIP_TIMEOUT'), str('0'))
+                run_setup(test_setup_py, [str('--version')])
+
 
 def make_trivial_sdist(dist_path, distname, version):
     """