Skip to content

Disallow deprecated dash-separated and uppercase options in setup.cfg #4870

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Mar 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions newsfragments/4870.removal.rst
Original file line number Diff line number Diff line change
@@ -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 <Snake_case>` convention
(e.g. ``Name => name``, ``install-requires => install_requires``).
This is a follow-up on deprecations introduced in
`v54.1.0 <https://setuptools.pypa.io/en/latest/history.html#v54-1-0>`_ (see #1608) and
`v54.1.1 <https://setuptools.pypa.io/en/latest/history.html#v54-1-1>`_ (see #2592).

.. note::
This change *does not affect configurations in* ``pyproject.toml``
(which uses the :wiki:`lower-kebab-case <Letter_case#Kebab_case>` convention following the precedent set in :pep:`517`/:pep:`518`).
96 changes: 44 additions & 52 deletions setuptools/dist.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import functools
import io
import itertools
import numbers
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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 self._skip_setupcfg_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 self._skip_setupcfg_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 _skip_setupcfg_normalization(self, section: str) -> bool:
skip = (
'options.extras_require',
'options.data_files',
'options.entry_points',
'options.package_data',
'options.exclude_package_data',
)
return section in skip or not self._is_setuptools_section(section)

def _is_setuptools_section(self, section: str) -> bool:
return (
section == "metadata"
or section.startswith("options")
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
Expand Down Expand Up @@ -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: Iterable[str] = (ep.name for ep in entry_points)
except metadata.PackageNotFoundError:
# during bootstrapping, distribution doesn't exist
eps = []
return {*distutils.command.__all__, *eps}


Copy link
Contributor Author

@abravalheri abravalheri Mar 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved this function out of the class to allow caching without memory leaks (otherwise it would keep a reference to self).

class DistDeprecationWarning(SetuptoolsDeprecationWarning):
"""Class for warning about deprecations in dist in
setuptools. Not ignored by default, unlike DeprecationWarning."""
62 changes: 32 additions & 30 deletions setuptools/tests/config/test_setupcfg.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import configparser
import contextlib
import inspect
import re
from pathlib import Path
from unittest.mock import Mock, patch

Expand All @@ -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
Expand Down Expand Up @@ -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 = [email protected]\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 == '[email protected]'
assert metadata.maintainer_email == '[email protected]'

@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 = [email protected]
"""
),
),
(
"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:
Expand Down
Loading