Repository: pypa/setuptools

Issue:
setup_requires="string" not handled by PEP 517 backend
Does this need to be fixed in setuptools rather since the PEP says the return value needs to be a list of strings? https://www.python.org/dev/peps/pep-0517/#get-requires-for-build-wheel

It looks like here is the setuptools code: https://github.com/pypa/setuptools/blob/cdb5eeae678d8ccc90bf7d4348013a294f11be75/setuptools/build_meta.py#L138

_Originally posted by @cjerdonek in https://github.com/pypa/pip/issues/6255#issuecomment-462468517_


Discussion:
So my instinct here is to say that `setup_requires` is deprecated, because generally speaking the stuff you put in there is what needs to be installed *before `setup.py` is invoked* (and `setup.py` is where you get `setup_requires` from). It should be replaced by the `build-system.requires` key in `pyproject.toml`.

The biggest downside I see to doing this is that it would make `setup_requires` the most convenient place to "single source" your setup requirements, since builds that want to do "best effort" support for pre-PEP 518 build systems may want to specify `setup_requires`. Right now they have to duplicate the information, but if we add support in `setup_requires` in the PEP 517 backend, they would be able to omit the information from `pyproject.toml` in the interest of having a single canonical source for the build information - the opposite of what we want happening.

The biggest upside is that if we do this, PEP 517 builds are much more likely to "just work" out of the box, and I believe it means that for PEP 517 builds we will have *eliminated* the `easy_install` code path, which is very attractive. In this case, it might make sense to use this *only* for the `setuptools.build_meta:__legacy__` backend, since it's actually a compatibility mode enhancement. I'm mildly reluctant to do it that way, though, because I don't want to give people *more* reasons to explicitly use `setuptools.build_meta:__legacy__`.
Hm, I guess I misunderstood this issue, I didn't realize that we're *already* translating `setup_requires` requirements into build requirements in the build requirements hooks.

That's mildly embarassing. I am going to hide my previous post as irrelevant, feel free to read it if you want to know how I would have responded if I had been around when we first added this feature.

To respond to the actual issue, yes, this seems to be supported in the old system and I see no good reason to drop support for it now.
I think this is decently simple to fix, I think the one thing I'd like to avoid is duplication of the logic that handles `setup_requires` strings, so whoever fixes it please try and make it so that the `setuptools.build_meta` code receives the list of missing requirements in a consistent format.

Minimum cases to handle:

- `setup_requires=["foo"]`
- `setup_requires="foo"`
- `setup_requires="foo\n"`
- `setup_requires="foo\n\n"`
- `setup_requires="foo\nbar"`


Please add tests to `setuptools/tests/test_build_meta.py`, and if you are feeling ambitious, I think there are also no tests for this "newline-delimited requirements string" format in the general test suite.

Resolving patch:
diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py
--- a/setuptools/build_meta.py
+++ b/setuptools/build_meta.py
@@ -36,6 +36,8 @@
 import setuptools
 import distutils
 
+from pkg_resources import parse_requirements
+
 __all__ = ['get_requires_for_build_sdist',
            'get_requires_for_build_wheel',
            'prepare_metadata_for_build_wheel',
@@ -51,7 +53,9 @@ def __init__(self, specifiers):
 
 class Distribution(setuptools.dist.Distribution):
     def fetch_build_eggs(self, specifiers):
-        raise SetupRequirementsError(specifiers)
+        specifier_list = list(map(str, parse_requirements(specifiers)))
+
+        raise SetupRequirementsError(specifier_list)
 
     @classmethod
     @contextlib.contextmanager


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
@@ -287,6 +287,57 @@ def test_build_sdist_relative_path_import(self, tmpdir_cwd):
         with pytest.raises(ImportError):
             build_backend.build_sdist("temp")
 
+    @pytest.mark.parametrize('setup_literal, requirements', [
+        ("'foo'", ['foo']),
+        ("['foo']", ['foo']),
+        (r"'foo\n'", ['foo']),
+        (r"'foo\n\n'", ['foo']),
+        ("['foo', 'bar']", ['foo', 'bar']),
+        (r"'# Has a comment line\nfoo'", ['foo']),
+        (r"'foo # Has an inline comment'", ['foo']),
+        (r"'foo \\\n >=3.0'", ['foo>=3.0']),
+        (r"'foo\nbar'", ['foo', 'bar']),
+        (r"'foo\nbar\n'", ['foo', 'bar']),
+        (r"['foo\n', 'bar\n']", ['foo', 'bar']),
+    ])
+    @pytest.mark.parametrize('use_wheel', [True, False])
+    def test_setup_requires(self, setup_literal, requirements, use_wheel,
+                            tmpdir_cwd):
+
+        files = {
+            'setup.py': DALS("""
+                from setuptools import setup
+
+                setup(
+                    name="qux",
+                    version="0.0.0",
+                    py_modules=["hello.py"],
+                    setup_requires={setup_literal},
+                )
+            """).format(setup_literal=setup_literal),
+            'hello.py': DALS("""
+            def run():
+                print('hello')
+            """),
+        }
+
+        build_files(files)
+
+        build_backend = self.get_build_backend()
+
+        if use_wheel:
+            base_requirements = ['wheel']
+            get_requires = build_backend.get_requires_for_build_wheel
+        else:
+            base_requirements = []
+            get_requires = build_backend.get_requires_for_build_sdist
+
+        # Ensure that the build requirements are properly parsed
+        expected = sorted(base_requirements + requirements)
+        actual = get_requires()
+
+        assert expected == sorted(actual)
+
 
 class TestBuildMetaLegacyBackend(TestBuildMetaBackend):
     backend_name = 'setuptools.build_meta:__legacy__'