diff --git a/changelog.d/3253.change.rst b/changelog.d/3253.change.rst new file mode 100644 index 00000000000..0c29e2a60f5 --- /dev/null +++ b/changelog.d/3253.change.rst @@ -0,0 +1 @@ +Enabled using ``file:`` for requirements in setup.cfg -- by :user:`akx` diff --git a/docs/userguide/declarative_config.rst b/docs/userguide/declarative_config.rst index 2a65e6e3677..da27184118a 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-6]_ +install_requires file:, list-semi [#opt-6]_ +extras_require file:, section [#opt-2]_, [#opt-6]_ 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 [#opt-5]_ -py_modules list-comma 34.4.0 +py_modules list-comma 34.4.0 data_files section 40.6.0 [#opt-4]_ -======================= =================================== =============== ========= +======================= =================================== =============== ==================== **Notes**: @@ -247,6 +247,11 @@ data_files section 40.6.0 [# namespaces (:pep:`420`). Check :doc:`the Python Packaging User Guide ` for more information. +.. [#opt-6] ``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 b2d5c34609a..4b2d07ffab0 100644 --- a/setuptools/config/setupcfg.py +++ b/setuptools/config/setupcfg.py @@ -568,15 +568,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, @@ -591,11 +603,11 @@ def parsers(self): "consider using implicit namespaces instead (PEP 420).", SetuptoolsDeprecationWarning, ), - '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, @@ -682,8 +694,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 904b1ef80a1..2384d632aab 100644 --- a/setuptools/tests/config/test_setupcfg.py +++ b/setuptools/tests/config/test_setupcfg.py @@ -884,6 +884,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__