From 2dbbabb2488af3a295b620b163b97354358656de Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 6 Apr 2022 16:06:08 +0300 Subject: [PATCH] Allow `file:` for `requires` statements in setup.cfg Refs #1951 --- docs/userguide/declarative_config.rst | 21 ++++++++++------- setuptools/config/setupcfg.py | 30 +++++++++++++++++------- setuptools/tests/config/test_setupcfg.py | 21 +++++++++++++++++ 3 files changed, 56 insertions(+), 16 deletions(-) diff --git a/docs/userguide/declarative_config.rst b/docs/userguide/declarative_config.rst index 52379dbf1c6..d6d4d4d4ded 100644 --- a/docs/userguide/declarative_config.rst +++ b/docs/userguide/declarative_config.rst @@ -192,28 +192,28 @@ obsoletes list-comma Options ------- -======================= =================================== =============== ========= +======================= =================================== =============== ==================== Key Type Minimum Version Notes -======================= =================================== =============== ========= +======================= =================================== =============== ==================== zip_safe bool -setup_requires list-semi 36.7.0 -install_requires list-semi -extras_require section [#opt-2]_ +setup_requires file:, list-semi 36.7.0 [#opt-5]_ +install_requires file:, list-semi [#opt-5]_ +extras_require file:, section [#opt-2]_, [#opt-5]_ python_requires str 34.4.0 entry_points file:, section 51.0.0 scripts list-comma eager_resources list-comma dependency_links list-comma -tests_require list-semi +tests_require file:, list-semi [#opt-5]_ include_package_data bool packages find:, find_namespace:, list-comma [#opt-3]_ package_dir dict package_data section [#opt-1]_ exclude_package_data section namespace_packages list-comma -py_modules list-comma 34.4.0 +py_modules list-comma 34.4.0 data_files section 40.6.0 [#opt-4]_ -======================= =================================== =============== ========= +======================= =================================== =============== ==================== **Notes**: @@ -243,6 +243,11 @@ data_files section 40.6.0 [# .. [#opt-4] ``data_files`` is deprecated and should be avoided. Please check :doc:`/userguide/datafiles` for more information. +.. [#opt-5] ``file:`` directives for reading requirements are supported since version 63.0. + The format for the file is basically the same as for a ``requirements.txt`` file. + Library developers should avoid tightly pinning their dependencies to a specific + version (e.g. via a "locked" requirements file). + Compatibility with other tools ============================== diff --git a/setuptools/config/setupcfg.py b/setuptools/config/setupcfg.py index d485a8bba88..fd95ff91bf6 100644 --- a/setuptools/config/setupcfg.py +++ b/setuptools/config/setupcfg.py @@ -567,15 +567,27 @@ def __init__( self.root_dir = target_obj.src_root self.package_dir: Dict[str, str] = {} # To be filled by `find_packages` + @classmethod + def _parse_list_semicolon(cls, value): + return cls._parse_list(value, separator=';') + + def _parse_file_in_root(self, value): + return self._parse_file(value, root_dir=self.root_dir) + + def _parse_requirements_list(self, value): + # Parse a requirements list, either by reading in a `file:`, or a list. + parsed = self._parse_list_semicolon(self._parse_file_in_root(value)) + # Filter it to only include lines that are not comments. `parse_list` + # will have stripped each line and filtered out empties. + return [line for line in parsed if not line.startswith("#")] + @property def parsers(self): """Metadata item name to parser function mapping.""" parse_list = self._parse_list - parse_list_semicolon = partial(self._parse_list, separator=';') parse_bool = self._parse_bool parse_dict = self._parse_dict parse_cmdclass = self._parse_cmdclass - parse_file = partial(self._parse_file, root_dir=self.root_dir) return { 'zip_safe': parse_bool, @@ -585,11 +597,11 @@ def parsers(self): 'eager_resources': parse_list, 'dependency_links': parse_list, 'namespace_packages': parse_list, - 'install_requires': parse_list_semicolon, - 'setup_requires': parse_list_semicolon, - 'tests_require': parse_list_semicolon, + 'install_requires': self._parse_requirements_list, + 'setup_requires': self._parse_requirements_list, + 'tests_require': self._parse_requirements_list, 'packages': self._parse_packages, - 'entry_points': parse_file, + 'entry_points': self._parse_file_in_root, 'py_modules': parse_list, 'python_requires': SpecifierSet, 'cmdclass': parse_cmdclass, @@ -676,8 +688,10 @@ def parse_section_extras_require(self, section_options): :param dict section_options: """ - parse_list = partial(self._parse_list, separator=';') - parsed = self._parse_section_to_dict(section_options, parse_list) + parsed = self._parse_section_to_dict( + section_options, + self._parse_requirements_list, + ) self['extras_require'] = parsed def parse_section_data_files(self, section_options): diff --git a/setuptools/tests/config/test_setupcfg.py b/setuptools/tests/config/test_setupcfg.py index 1f35f836308..0ae75626d06 100644 --- a/setuptools/tests/config/test_setupcfg.py +++ b/setuptools/tests/config/test_setupcfg.py @@ -881,6 +881,27 @@ def test_cmdclass(self, tmpdir): assert cmdclass.__module__ == "custom_build" assert module_path.samefile(inspect.getfile(cmdclass)) + def test_requirements_file(self, tmpdir): + fake_env( + tmpdir, + DALS(""" + [options] + install_requires = file:requirements.txt + tests_require = file:requirements-test.txt + [options.extras_require] + colors = file:requirements-extra.txt + """) + ) + + tmpdir.join('requirements.txt').write('\ndocutils>=0.3\n\n') + tmpdir.join('requirements-test.txt').write(' # comment\npytest\n# comment\n') + tmpdir.join('requirements-extra.txt').write('colorama') + + with get_dist(tmpdir) as dist: + assert dist.install_requires == ['docutils>=0.3'] + assert dist.tests_require == ['pytest'] + assert dist.extras_require == {'colors': ['colorama']} + saved_dist_init = _Distribution.__init__