Repository: pypa/setuptools

Issue:
Don't install setup_requires when run as a PEP 517 backend
PEP 517 separates the responsibilities around build requirements: the backend is responsible for saying what is required, and the frontend is responsible for ensuring they're available to the build.

In setuptools, build requirements are defined by `setup_requires`, and these get passed through to PEP 517's `get_requires_for_build_*` hooks. There is a monkeypatch to return them from these hooks and prevent setuptools trying to install them itself:

https://github.com/pypa/setuptools/blob/5e60dc50e540a942aeb558aabe7d92ab7eb13d4b/setuptools/build_meta.py#L56-L75

But something similar to that - preventing installation - should really be in place for all the PEP 517 hooks, because a PEP 517 backend isn't responsible for installing dependencies.

Why does this matter? Pip has the `--no-build-isolation` option, with which the caller can declare that they have taken care of build dependencies and pip should try to build the package in the current environment. This is useful for downstream packagers, and for experimenting with different versions of your build dependencies. But setuptools doesn't know about this, so it charges ahead and attempts to install things when that's not what you want.

The workaround I'm looking at is to only specify `setup_requires` if `'egg_info' in sys.argv`, as is the case when the `get_requires_for_build_*` hooks are called. But this is clearly not ideal.


Discussion:

Resolving patch:
diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py
--- a/setuptools/build_meta.py
+++ b/setuptools/build_meta.py
@@ -75,6 +75,22 @@ def patch(cls):
             distutils.core.Distribution = orig
 
 
+@contextlib.contextmanager
+def no_install_setup_requires():
+    """Temporarily disable installing setup_requires
+
+    Under PEP 517, the backend reports build dependencies to the frontend,
+    and the frontend is responsible for ensuring they're installed.
+    So setuptools (acting as a backend) should not try to install them.
+    """
+    orig = setuptools._install_setup_requires
+    setuptools._install_setup_requires = lambda attrs: None
+    try:
+        yield
+    finally:
+        setuptools._install_setup_requires = orig
+
+
 def _to_str(s):
     """
     Convert a filename to a string (on Python 2, explicitly
@@ -154,7 +170,8 @@ def prepare_metadata_for_build_wheel(self, metadata_directory,
                                          config_settings=None):
         sys.argv = sys.argv[:1] + ['dist_info', '--egg-base',
                                    _to_str(metadata_directory)]
-        self.run_setup()
+        with no_install_setup_requires():
+            self.run_setup()
 
         dist_info_directory = metadata_directory
         while True:
@@ -194,7 +211,8 @@ def _build_with_temp_dir(self, setup_command, result_extension,
             sys.argv = (sys.argv[:1] + setup_command +
                         ['--dist-dir', tmp_dist_dir] +
                         config_settings["--global-option"])
-            self.run_setup()
+            with no_install_setup_requires():
+                self.run_setup()
 
             result_basename = _file_with_extension(
                 tmp_dist_dir, result_extension)


Test patch:
diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py
--- a/setuptools/tests/test_build_meta.py
+++ b/setuptools/tests/test_build_meta.py
@@ -380,7 +380,7 @@ def test_setup_requires(self, setup_literal, requirements, use_wheel,
                 setup(
                     name="qux",
                     version="0.0.0",
-                    py_modules=["hello.py"],
+                    py_modules=["hello"],
                     setup_requires={setup_literal},
                 )
             """).format(setup_literal=setup_literal),
@@ -407,6 +407,35 @@ def run():
 
         assert expected == sorted(actual)
 
+    def test_dont_install_setup_requires(self, tmpdir_cwd):
+        files = {
+            'setup.py': DALS("""
+                        from setuptools import setup
+
+                        setup(
+                            name="qux",
+                            version="0.0.0",
+                            py_modules=["hello"],
+                            setup_requires=["does-not-exist >99"],
+                        )
+                    """),
+            'hello.py': DALS("""
+                    def run():
+                        print('hello')
+                    """),
+        }
+
+        build_files(files)
+
+        build_backend = self.get_build_backend()
+
+        dist_dir = os.path.abspath('pip-dist-info')
+        os.makedirs(dist_dir)
+
+        # does-not-exist can't be satisfied, so if it attempts to install
+        # setup_requires, it will fail.
+        build_backend.prepare_metadata_for_build_wheel(dist_dir)
+
     _sys_argv_0_passthrough = {
         'setup.py': DALS("""
             import os