From 1aa781cd8ee638e7b403ebbd1caa82f8c7d4e6cd Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Sat, 16 Mar 2019 12:28:17 -0400 Subject: Add failing test for setup_requires Per GH #1682, with setuptools.build_meta we are not properly handling the situation where setup_requires is actually a newline-delimited string rather than a list, which is supported by setup.py interface. This adds several failing (and some passing) tests for how setup_requires is handled by setuptools.build_meta. --- setuptools/tests/test_build_meta.py | 46 +++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) (limited to 'setuptools') diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index 74969322..d9df8b2c 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -287,6 +287,52 @@ class TestBuildMetaBackend: with pytest.raises(ImportError): build_backend.build_sdist("temp") + @pytest.mark.parametrize('setup_literal, requirements', [ + pytest.param("'foo'", ['foo'], marks=pytest.mark.xfail), + ("['foo']", ['foo']), + pytest.param(r"'foo\n'", ['foo'], marks=pytest.mark.xfail), + pytest.param(r"'foo\n\n'", ['foo'], marks=pytest.mark.xfail), + ("['foo', 'bar']", ['foo', 'bar']), + pytest.param(r"'# Has a comment line\nfoo'", + ['foo'], marks=pytest.mark.xfail), + pytest.param(r"'foo # Has an inline comment'", + ['foo'], marks=pytest.mark.xfail), + pytest.param(r"'foo \\\n >=3.0'", + ['foo>=3.0'], marks=pytest.mark.xfail), + pytest.param(r"'foo\nbar'", ['foo', 'bar'], marks=pytest.mark.xfail), + pytest.param(r"'foo\nbar\n'", ['foo', 'bar'], marks=pytest.mark.xfail), + pytest.param(r"['foo\n', 'bar\n']", + ['foo', 'bar'], marks=pytest.mark.xfail), + ]) + def test_setup_requires(self, setup_literal, requirements, 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() + + # Ensure that the build requirements are properly parsed + expected = sorted(['wheel'] + requirements) + actual = build_backend.get_requires_for_build_wheel() + + assert expected == sorted(actual) + class TestBuildMetaLegacyBackend(TestBuildMetaBackend): backend_name = 'setuptools.build_meta:__legacy__' -- cgit v1.2.3 From 318f739d14a810042e6803fa3eb4c4e140f0ef88 Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Sat, 16 Mar 2019 12:53:05 -0400 Subject: Add requirement parsing in setuptools.build_meta This fixes GH #1682 by porting the pkg_resources requirement parsing logic into setuptools.build_meta, so that all valid requirement specifiers passed to setup_requires will be added to the get_requires_for_build_* function outputs. Fixes GH #1682 --- setuptools/build_meta.py | 45 ++++++++++++++++++++++++++++++++++++- setuptools/tests/test_build_meta.py | 22 ++++++++---------- 2 files changed, 53 insertions(+), 14 deletions(-) (limited to 'setuptools') diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index 047cc07b..fb37c02a 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -36,6 +36,8 @@ import contextlib import setuptools import distutils +from setuptools._vendor import six + __all__ = ['get_requires_for_build_sdist', 'get_requires_for_build_wheel', 'prepare_metadata_for_build_wheel', @@ -51,7 +53,9 @@ class SetupRequirementsError(BaseException): class Distribution(setuptools.dist.Distribution): def fetch_build_eggs(self, specifiers): - raise SetupRequirementsError(specifiers) + specifier_list = self._parse_requirements(specifiers) + + raise SetupRequirementsError(specifier_list) @classmethod @contextlib.contextmanager @@ -68,6 +72,45 @@ class Distribution(setuptools.dist.Distribution): finally: distutils.core.Distribution = orig + def _yield_lines(self, strs): + """Yield non-empty/non-comment lines of a string or sequence""" + if isinstance(strs, six.string_types): + for s in strs.splitlines(): + s = s.strip() + # skip blank lines/comments + if s and not s.startswith('#'): + yield s + else: + for ss in strs: + for s in self._yield_lines(ss): + yield s + + def _parse_requirements(self, strs): + """Parse requirement specifiers into a list of requirement strings + + This is forked from pkg_resources.parse_requirements. + + `strs` must be a string, or a (possibly-nested) iterable thereof. + """ + # create a steppable iterator, so we can handle \-continuations + lines = iter(self._yield_lines(strs)) + + requirements = [] + + for line in lines: + # Drop comments -- a hash without a space may be in a URL. + if ' #' in line: + line = line[:line.find(' #')] + # If there is a line continuation, drop it, and append the next line. + if line.endswith('\\'): + line = line[:-2].strip() + try: + line += next(lines) + except StopIteration: + return + requirements.append(line) + + return requirements def _to_str(s): """ diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index d9df8b2c..a14a3c7a 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -288,21 +288,17 @@ class TestBuildMetaBackend: build_backend.build_sdist("temp") @pytest.mark.parametrize('setup_literal, requirements', [ - pytest.param("'foo'", ['foo'], marks=pytest.mark.xfail), + ("'foo'", ['foo']), ("['foo']", ['foo']), - pytest.param(r"'foo\n'", ['foo'], marks=pytest.mark.xfail), - pytest.param(r"'foo\n\n'", ['foo'], marks=pytest.mark.xfail), + (r"'foo\n'", ['foo']), + (r"'foo\n\n'", ['foo']), ("['foo', 'bar']", ['foo', 'bar']), - pytest.param(r"'# Has a comment line\nfoo'", - ['foo'], marks=pytest.mark.xfail), - pytest.param(r"'foo # Has an inline comment'", - ['foo'], marks=pytest.mark.xfail), - pytest.param(r"'foo \\\n >=3.0'", - ['foo>=3.0'], marks=pytest.mark.xfail), - pytest.param(r"'foo\nbar'", ['foo', 'bar'], marks=pytest.mark.xfail), - pytest.param(r"'foo\nbar\n'", ['foo', 'bar'], marks=pytest.mark.xfail), - pytest.param(r"['foo\n', 'bar\n']", - ['foo', 'bar'], marks=pytest.mark.xfail), + (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']), ]) def test_setup_requires(self, setup_literal, requirements, tmpdir_cwd): -- cgit v1.2.3 From b54d4c699fc4e1692dadd19bdd7cbcde1c844976 Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Sat, 16 Mar 2019 12:57:57 -0400 Subject: Extend requirement parsing tests to sdists --- setuptools/tests/test_build_meta.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) (limited to 'setuptools') diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index a14a3c7a..0bdea2d6 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -300,7 +300,9 @@ class TestBuildMetaBackend: (r"'foo\nbar\n'", ['foo', 'bar']), (r"['foo\n', 'bar\n']", ['foo', 'bar']), ]) - def test_setup_requires(self, setup_literal, requirements, tmpdir_cwd): + @pytest.mark.parametrize('use_wheel', [True, False]) + def test_setup_requires(self, setup_literal, requirements, use_wheel, + tmpdir_cwd): files = { 'setup.py': DALS(""" @@ -323,9 +325,16 @@ class TestBuildMetaBackend: 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(['wheel'] + requirements) - actual = build_backend.get_requires_for_build_wheel() + expected = sorted(base_requirements + requirements) + actual = get_requires() assert expected == sorted(actual) -- cgit v1.2.3 From 5efdf816fddcd8fbc9c3d1e6867a25848b1f9a06 Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Sat, 16 Mar 2019 13:24:36 -0400 Subject: Use pkg_resources.parse_requirements in build_meta Since pkg_resources is imported elsewhere anyway, we don't get much value out of porting the requirement parser locally. --- setuptools/build_meta.py | 43 ++----------------------------------------- 1 file changed, 2 insertions(+), 41 deletions(-) (limited to 'setuptools') diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index fb37c02a..47cbcbf6 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -36,7 +36,7 @@ import contextlib import setuptools import distutils -from setuptools._vendor import six +from pkg_resources import parse_requirements __all__ = ['get_requires_for_build_sdist', 'get_requires_for_build_wheel', @@ -53,7 +53,7 @@ class SetupRequirementsError(BaseException): class Distribution(setuptools.dist.Distribution): def fetch_build_eggs(self, specifiers): - specifier_list = self._parse_requirements(specifiers) + specifier_list = list(map(str, parse_requirements(specifiers))) raise SetupRequirementsError(specifier_list) @@ -72,45 +72,6 @@ class Distribution(setuptools.dist.Distribution): finally: distutils.core.Distribution = orig - def _yield_lines(self, strs): - """Yield non-empty/non-comment lines of a string or sequence""" - if isinstance(strs, six.string_types): - for s in strs.splitlines(): - s = s.strip() - # skip blank lines/comments - if s and not s.startswith('#'): - yield s - else: - for ss in strs: - for s in self._yield_lines(ss): - yield s - - def _parse_requirements(self, strs): - """Parse requirement specifiers into a list of requirement strings - - This is forked from pkg_resources.parse_requirements. - - `strs` must be a string, or a (possibly-nested) iterable thereof. - """ - # create a steppable iterator, so we can handle \-continuations - lines = iter(self._yield_lines(strs)) - - requirements = [] - - for line in lines: - # Drop comments -- a hash without a space may be in a URL. - if ' #' in line: - line = line[:line.find(' #')] - # If there is a line continuation, drop it, and append the next line. - if line.endswith('\\'): - line = line[:-2].strip() - try: - line += next(lines) - except StopIteration: - return - requirements.append(line) - - return requirements def _to_str(s): """ -- cgit v1.2.3