From 88bee158e43a8534aac08cadf3b45f393eadb462 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sun, 9 Mar 2025 16:14:10 +0000 Subject: [PATCH 1/6] Disalow deprecated dash-separated and uppercase options in setup.cfg --- setuptools/dist.py | 96 +++++++++++------------- setuptools/tests/config/test_setupcfg.py | 62 +++++++-------- 2 files changed, 76 insertions(+), 82 deletions(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index 0249651267..bcfe9c23f2 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -1,5 +1,6 @@ from __future__ import annotations +import functools import io import itertools import numbers @@ -27,6 +28,7 @@ from ._reqs import _StrOrIter from .config import pyprojecttoml, setupcfg from .discovery import ConfigDiscovery +from .errors import InvalidConfigError from .monkey import get_unpatched from .warnings import InformationOnly, SetuptoolsDeprecationWarning @@ -490,8 +492,8 @@ def _parse_config_files(self, filenames=None): # noqa: C901 continue val = parser.get(section, opt) - opt = self.warn_dash_deprecation(opt, section) - opt = self.make_option_lowercase(opt, section) + opt = self._enforce_underscore(opt, section) + opt = self._enforce_option_lowercase(opt, section) opt_dict[opt] = (filename, val) # Make the ConfigParser forget everything (so we retain @@ -516,64 +518,42 @@ def _parse_config_files(self, filenames=None): # noqa: C901 except ValueError as e: raise DistutilsOptionError(e) from e - def warn_dash_deprecation(self, opt: str, section: str) -> str: - if section in ( - 'options.extras_require', - 'options.data_files', - ): + def _enforce_underscore(self, opt: str, section: str) -> str: + if "-" not in opt or not self._config_requires_normalization(section): return opt - underscore_opt = opt.replace('-', '_') - commands = list( - itertools.chain( - distutils.command.__all__, - self._setuptools_commands(), - ) + raise InvalidConfigError( + f"Invalid dash-separated key {opt!r} in {section!r} (setup.cfg), " + f"please use the underscore name {opt.replace('-', '_')!r} instead." + # Warning initially introduced in 3 Mar 2021 ) - if ( - not section.startswith('options') - and section != 'metadata' - and section not in commands - ): - return underscore_opt - - if '-' in opt: - SetuptoolsDeprecationWarning.emit( - "Invalid dash-separated options", - f""" - Usage of dash-separated {opt!r} will not be supported in future - versions. Please use the underscore name {underscore_opt!r} instead. - """, - see_docs="userguide/declarative_config.html", - due_date=(2025, 3, 3), - # Warning initially introduced in 3 Mar 2021 - ) - return underscore_opt - def _setuptools_commands(self): - try: - entry_points = metadata.distribution('setuptools').entry_points - return {ep.name for ep in entry_points} # Avoid newer API for compatibility - except metadata.PackageNotFoundError: - # during bootstrapping, distribution doesn't exist - return [] - - def make_option_lowercase(self, opt: str, section: str) -> str: - if section != 'metadata' or opt.islower(): + def _enforce_option_lowercase(self, opt: str, section: str) -> str: + if opt.islower() or not self._config_requires_normalization(section): return opt - lowercase_opt = opt.lower() - SetuptoolsDeprecationWarning.emit( - "Invalid uppercase configuration", - f""" - Usage of uppercase key {opt!r} in {section!r} will not be supported in - future versions. Please use lowercase {lowercase_opt!r} instead. - """, - see_docs="userguide/declarative_config.html", - due_date=(2025, 3, 3), + raise InvalidConfigError( + f"Invalid uppercase key {opt!r} in {section!r} (setup.cfg), " + f"please use lowercase {opt.lower()!r} instead." # Warning initially introduced in 6 Mar 2021 ) - return lowercase_opt + + def _config_requires_normalization(self, section: str) -> bool: + skip = ( + 'options.extras_require', + 'options.data_files', + 'options.entry_points', + 'options.package_data', + 'options.exclude_package_data', + ) + return section not in skip and self._is_setuptools_section(section) + + def _is_setuptools_section(self, section: str) -> bool: + return ( + section == "metadata" + or section.startswith("option") + or section in _setuptools_commands() + ) # FIXME: 'Distribution._set_command_options' is too complex (14) def _set_command_options(self, command_obj, option_dict=None): # noqa: C901 @@ -999,6 +979,18 @@ def run_command(self, command) -> None: super().run_command(command) +@functools.cache +def _setuptools_commands() -> set[str]: + try: + # Use older API for importlib.metadata compatibility + entry_points = metadata.distribution('setuptools').entry_points + eps = (ep.name for ep in entry_points) + except metadata.PackageNotFoundError: + # during bootstrapping, distribution doesn't exist + return set(distutils.command.__all__) + return {*distutils.command.__all__, *eps} + + class DistDeprecationWarning(SetuptoolsDeprecationWarning): """Class for warning about deprecations in dist in setuptools. Not ignored by default, unlike DeprecationWarning.""" diff --git a/setuptools/tests/config/test_setupcfg.py b/setuptools/tests/config/test_setupcfg.py index d356d2b77c..a199871ffd 100644 --- a/setuptools/tests/config/test_setupcfg.py +++ b/setuptools/tests/config/test_setupcfg.py @@ -1,6 +1,7 @@ import configparser import contextlib import inspect +import re from pathlib import Path from unittest.mock import Mock, patch @@ -9,6 +10,7 @@ from setuptools.config.setupcfg import ConfigHandler, Target, read_configuration from setuptools.dist import Distribution, _Distribution +from setuptools.errors import InvalidConfigError from setuptools.warnings import SetuptoolsDeprecationWarning from ..textwrap import DALS @@ -420,36 +422,36 @@ def test_not_utf8(self, tmpdir): with get_dist(tmpdir): pass - @pytest.mark.xfail(reason="#4864") - def test_warn_dash_deprecation(self, tmpdir): - # warn_dash_deprecation() is a method in setuptools.dist - # remove this test and the method when no longer needed - fake_env( - tmpdir, - '[metadata]\n' - 'author-email = test@test.com\n' - 'maintainer_email = foo@foo.com\n', - ) - msg = "Usage of dash-separated 'author-email' will not be supported" - with pytest.warns(SetuptoolsDeprecationWarning, match=msg): - with get_dist(tmpdir) as dist: - metadata = dist.metadata - - assert metadata.author_email == 'test@test.com' - assert metadata.maintainer_email == 'foo@foo.com' - - @pytest.mark.xfail(reason="#4864") - def test_make_option_lowercase(self, tmpdir): - # remove this test and the method make_option_lowercase() in setuptools.dist - # when no longer needed - fake_env(tmpdir, '[metadata]\nName = foo\ndescription = Some description\n') - msg = "Usage of uppercase key 'Name' in 'metadata' will not be supported" - with pytest.warns(SetuptoolsDeprecationWarning, match=msg): - with get_dist(tmpdir) as dist: - metadata = dist.metadata - - assert metadata.name == 'foo' - assert metadata.description == 'Some description' + @pytest.mark.parametrize( + ("error_msg", "config"), + [ + ( + "Invalid dash-separated key 'author-email' in 'metadata' (setup.cfg)", + DALS( + """ + [metadata] + author-email = test@test.com + maintainer_email = foo@foo.com + """ + ), + ), + ( + "Invalid uppercase key 'Name' in 'metadata' (setup.cfg)", + DALS( + """ + [metadata] + Name = foo + description = Some description + """ + ), + ), + ], + ) + def test_invalid_options_previously_deprecated(self, tmpdir, error_msg, config): + # this test and related methods can be removed when no longer needed + fake_env(tmpdir, config) + with pytest.raises(InvalidConfigError, match=re.escape(error_msg)): + get_dist(tmpdir).__enter__() class TestOptions: From 3a0596f0c4f9d26d2a307333796681f0b54b3fd5 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sun, 9 Mar 2025 17:45:20 +0000 Subject: [PATCH 2/6] Add news fragment --- newsfragments/4870.removal.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 newsfragments/4870.removal.rst diff --git a/newsfragments/4870.removal.rst b/newsfragments/4870.removal.rst new file mode 100644 index 0000000000..dd21a13c22 --- /dev/null +++ b/newsfragments/4870.removal.rst @@ -0,0 +1,10 @@ +Setuptools no longer accepts options containing uppercase or dash characters in ``setup.cfg``. +Please ensure to write the options in ``setup.cfg`` using the :wiki:`lower_snake_case ` convention +(e.g. ``Name => name``, ``install-requires => install_requires``). +This is a follow-up on deprecations introduced in +`v54.1.0 `_ (see #1608) and +`v54.1.1 `_ (see #2592). + +.. note:: + This change *does not affect configurations in* ``pyproject.toml`` + (which uses the :wiki:`lower-kebab-case ` convention following the precedent in :pep:`517`). From a67f998998ff3f19bf4c9dceb60cbc07a47c7abe Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sun, 9 Mar 2025 17:47:23 +0000 Subject: [PATCH 3/6] Avoid duplication in setuptools.dist --- setuptools/dist.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index bcfe9c23f2..e140c86851 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -984,10 +984,10 @@ def _setuptools_commands() -> set[str]: try: # Use older API for importlib.metadata compatibility entry_points = metadata.distribution('setuptools').entry_points - eps = (ep.name for ep in entry_points) + eps: Iterable[str] = (ep.name for ep in entry_points) except metadata.PackageNotFoundError: # during bootstrapping, distribution doesn't exist - return set(distutils.command.__all__) + eps = [] return {*distutils.command.__all__, *eps} From 5a9b4b53e26ea174001f83c590cd37ed9da1f221 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 10 Mar 2025 09:09:57 +0000 Subject: [PATCH 4/6] Update mentions to PEP in newsfragment --- newsfragments/4870.removal.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/newsfragments/4870.removal.rst b/newsfragments/4870.removal.rst index dd21a13c22..5b713032d0 100644 --- a/newsfragments/4870.removal.rst +++ b/newsfragments/4870.removal.rst @@ -7,4 +7,4 @@ This is a follow-up on deprecations introduced in .. note:: This change *does not affect configurations in* ``pyproject.toml`` - (which uses the :wiki:`lower-kebab-case ` convention following the precedent in :pep:`517`). + (which uses the :wiki:`lower-kebab-case ` convention following the precedent set in :pep:`517`/:pep:`518`). From 6b71893d27de2fc3da3ddd1092269bb0a3085f80 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 10 Mar 2025 09:19:02 +0000 Subject: [PATCH 5/6] Simplify negative conditions by applying DeMorgan's theorem --- setuptools/dist.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index e140c86851..e8af957aff 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -519,7 +519,7 @@ def _parse_config_files(self, filenames=None): # noqa: C901 raise DistutilsOptionError(e) from e def _enforce_underscore(self, opt: str, section: str) -> str: - if "-" not in opt or not self._config_requires_normalization(section): + if "-" not in opt or self._skip_setupcfg_normalization(section): return opt raise InvalidConfigError( @@ -529,7 +529,7 @@ def _enforce_underscore(self, opt: str, section: str) -> str: ) def _enforce_option_lowercase(self, opt: str, section: str) -> str: - if opt.islower() or not self._config_requires_normalization(section): + if opt.islower() or self._skip_setupcfg_normalization(section): return opt raise InvalidConfigError( @@ -538,7 +538,7 @@ def _enforce_option_lowercase(self, opt: str, section: str) -> str: # Warning initially introduced in 6 Mar 2021 ) - def _config_requires_normalization(self, section: str) -> bool: + def _skip_setupcfg_normalization(self, section: str) -> bool: skip = ( 'options.extras_require', 'options.data_files', @@ -546,7 +546,7 @@ def _config_requires_normalization(self, section: str) -> bool: 'options.package_data', 'options.exclude_package_data', ) - return section not in skip and self._is_setuptools_section(section) + return section in skip or not self._is_setuptools_section(section) def _is_setuptools_section(self, section: str) -> bool: return ( From d3640575a483d8c2bf00d45d4249738a4968fb08 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 13 Mar 2025 11:53:26 +0000 Subject: [PATCH 6/6] Fix error in `setuptools/dist.py` --- setuptools/dist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index e8af957aff..8663077bb3 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -551,7 +551,7 @@ def _skip_setupcfg_normalization(self, section: str) -> bool: def _is_setuptools_section(self, section: str) -> bool: return ( section == "metadata" - or section.startswith("option") + or section.startswith("options") or section in _setuptools_commands() )