Skip to content
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

Add warning for deprecated metadata fields #4811

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
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
18 changes: 14 additions & 4 deletions docs/references/keywords.rst
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,21 @@ extensions).
``url``
A string specifying the URL for the package homepage.

.. warning::
``url`` is deprecated as it generates the ``Home-page`` metadata.
Please use ``project_urls`` instead, with the relevant labels.
See :pep:`753`.

.. _keyword/download_url:

``download_url``
A string specifying the URL to download the package.

.. warning::
``download_url`` is deprecated as it generates the ``Download-URL`` metadata.
Please use ``project_urls`` instead, with the relevant labels.
See :pep:`753`.

.. _keyword/packages:

``packages``
Expand Down Expand Up @@ -257,14 +267,14 @@ extensions).

``requires``
.. warning::
``requires`` is superseded by ``install_requires`` and should not be used
anymore.
``requires`` is deprecated and superseded by ``install_requires``.
It should not be used anymore.

.. _keyword/obsoletes:

``obsoletes``
.. warning::
``obsoletes`` is currently ignored by ``pip``.
``obsoletes`` is deprecated and currently ignored by ``pip``.

List of strings describing packages which this package renders obsolete,
meaning that the two projects should not be installed at the same time.
Expand All @@ -283,7 +293,7 @@ extensions).

``provides``
.. warning::
``provides`` is currently ignored by ``pip``.
``provides`` is currently considered deprecated and is ignored by ``pip``.

List of strings describing package- and virtual package names contained
within this package.
Expand Down
2 changes: 2 additions & 0 deletions newsfragments/4811.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Configuration options that yield deprecated metadata fields
result now in warnings.
4 changes: 4 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,7 @@ filterwarnings=

# Ignore warnings about consider_namespace_packages (jaraco/skeleton@6ff02e0eefcd)
ignore:Unknown config option. consider_namespace_packages:pytest.PytestConfigWarning

# Suppress known config deprecations still used in tests
ignore:Deprecated config in `setup.cfg`
ignore:Deprecated usage of `tool.setuptools.(provides|obsoletes)`
10 changes: 9 additions & 1 deletion setuptools/config/_apply_pyprojecttoml.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from .._path import StrPath
from ..errors import RemovedConfigError
from ..extension import Extension
from ..warnings import SetuptoolsWarning
from ..warnings import SetuptoolsDeprecationWarning, SetuptoolsWarning

if TYPE_CHECKING:
from typing_extensions import TypeAlias
Expand Down Expand Up @@ -98,6 +98,12 @@ def _apply_tool_table(dist: Distribution, config: dict, filename: StrPath):
and has been removed from `pyproject.toml`.
"""
raise RemovedConfigError("\n".join([cleandoc(msg), suggestion]))
elif norm_key in TOOL_TABLE_DEPRECATIONS:
SetuptoolsDeprecationWarning.emit(
f"Deprecated usage of `tool.setuptools.{field}` in `pyproject.toml`.",
see_docs=f"references/keywords.html#keyword-{field}",
due_date=(2027, 1, 25), # introduced in 20 Jan 2025
)

norm_key = TOOL_TABLE_RENAMES.get(norm_key, norm_key)
corresp = TOOL_TABLE_CORRESPONDENCE.get(norm_key, norm_key)
Expand Down Expand Up @@ -401,11 +407,13 @@ def _acessor(obj):

TOOL_TABLE_RENAMES = {"script_files": "scripts"}
TOOL_TABLE_REMOVALS = {
"requires": "Please use `[project] dependencies` for Python requirements.",
"namespace_packages": """
Please migrate to implicit native namespaces instead.
See https://packaging.python.org/en/latest/guides/packaging-namespace-packages/.
""",
}
TOOL_TABLE_DEPRECATIONS = ("obsoletes", "provides")
TOOL_TABLE_CORRESPONDENCE = {
# Fields with corresponding core metadata need to be marked as static:
"obsoletes": partial(_set_static_list_metadata, "obsoletes"),
Expand Down
29 changes: 25 additions & 4 deletions setuptools/config/setupcfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

from .. import _static
from .._path import StrPath
from ..errors import FileError, OptionError
from ..errors import FileError, OptionError, RemovedConfigError
from ..warnings import SetuptoolsDeprecationWarning
from . import expand

Expand Down Expand Up @@ -516,6 +516,25 @@ def config_handler(*args, **kwargs):

return config_handler

def _deprecated(self, field, func):
anchor = f"keyword-{field.replace('_', '-')}"
return self._deprecated_config_handler(
func,
f"Deprecated usage of `{field}` in `setup.cfg`.",
see_docs=f"references/keywords.html#{anchor}",
due_date=(2027, 1, 25), # introduced in 20 Jan 2025
)

def _removed(self, field, **kwargs):
def config_handler(*args, **kwargs):
raise RemovedConfigError(
f"Invalid use of `{field}` in `setup.cfg`.\nSee: "
"https://setuptools.pypa.io/en/latest/"
f"references/keywords.html#keyword-{field.replace('_', '-')}"
)

return config_handler


class ConfigMetadataHandler(ConfigHandler["DistributionMetadata"]):
section_prefix = 'metadata'
Expand Down Expand Up @@ -561,16 +580,18 @@ def parsers(self):
'maintainer_email': _static.Str,
'platforms': parse_list_static,
'keywords': parse_list_static,
'provides': parse_list_static,
'obsoletes': parse_list_static,
'provides': self._deprecated('provides', parse_list_static),
'obsoletes': self._deprecated('obsoletes', parse_list_static),
'requires': self._removed('requires'), # 2023-Nov-20
'classifiers': self._get_parser_compound(parse_file, parse_list_static),
'license': exclude_files_parser('license'),
'license_files': parse_list_static,
'description': parse_file,
'long_description': parse_file,
'long_description_content_type': _static.Str,
'version': self._parse_version, # Cannot be marked as dynamic
'url': _static.Str,
'url': self._deprecated('url', _static.Str),
'download_url': self._deprecated('download_url', _static.Str),
'project_urls': parse_dict_static,
}

Expand Down
25 changes: 22 additions & 3 deletions setuptools/dist.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,14 @@ class Distribution(_Distribution):
'extras_require': dict,
}

_DEPRECATED_FIELDS = (
"url",
"download_url",
"requires",
"provides",
"obsoletes",
)

# Used by build_py, editable_wheel and install_lib commands for legacy namespaces
namespace_packages: list[str] #: :meta private: DEPRECATED

Expand Down Expand Up @@ -326,9 +334,9 @@ def __init__(self, attrs: MutableMapping[str, Any] | None = None) -> None:
self.set_defaults = ConfigDiscovery(self)

self._set_metadata_defaults(attrs)

self.metadata.version = self._normalize_version(self.metadata.version)
self._finalize_requires()
self._check_deprecated_metadata_fields()

def _validate_metadata(self):
required = {"name"}
Expand All @@ -343,6 +351,16 @@ def _validate_metadata(self):
msg = f"Required package metadata is missing: {missing}"
raise DistutilsSetupError(msg)

def _check_deprecated_metadata_fields(self) -> None:
for attr in self._DEPRECATED_FIELDS:
if getattr(self.metadata, attr, None):
anchor = f"keyword-{attr.replace('_', '-')}"
SetuptoolsDeprecationWarning.emit(
f"Deprecated usage of `{attr}` in setuptools configuration.",
see_docs=f"references/keywords.html#{anchor}",
due_date=(2027, 1, 25), # introduced in 20 Jan 2025
)

def _set_metadata_defaults(self, attrs):
"""
Fill-in missing metadata fields not supported by distutils.
Expand Down Expand Up @@ -993,8 +1011,9 @@ def handle_display_options(self, option_order):

def run_command(self, command) -> None:
self.set_defaults()
# Postpone defaults until all explicit configuration is considered
# (setup() args, config files, command line and plugins)
self._check_deprecated_metadata_fields()
# Postpone defaults and validations until all explicit configuration is
# considered (setup() args, config files, command line and plugins)

super().run_command(command)

Expand Down
43 changes: 38 additions & 5 deletions setuptools/tests/config/test_apply_pyprojecttoml.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from setuptools.config._apply_pyprojecttoml import _MissingDynamic, _some_attrgetter
from setuptools.dist import Distribution
from setuptools.errors import RemovedConfigError
from setuptools.warnings import SetuptoolsDeprecationWarning

from .downloads import retrieve_file, urls_from_file

Expand Down Expand Up @@ -347,18 +348,47 @@ def test_pyproject_sets_attribute(self, tmp_path, monkeypatch):


class TestDeprecatedFields:
def test_namespace_packages(self, tmp_path):
@pytest.mark.parametrize(
("field", "value"),
[
("provides", "['setuptools']"),
("obsoletes", "['obsoletes']"),
],
)
def test_still_valid(self, tmp_path, field, value):
pyproject = tmp_path / "pyproject.toml"
config = f"""
[project]
name = "myproj"
version = "42"
[tool.setuptools]
{field} = {value}
"""
pyproject.write_text(cleandoc(config), encoding="utf-8")
match = f"Deprecated usage of `tool.setuptools.{field}`"
with pytest.warns(SetuptoolsDeprecationWarning, match=match):
pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)

@pytest.mark.parametrize(
("field", "value"),
[
("namespace-packages", "['myproj.pkg']"),
("requires", "['setuptools']"),
],
)
def test_removed(self, tmp_path, field, value):
pyproject = tmp_path / "pyproject.toml"
config = """
config = f"""
[project]
name = "myproj"
version = "42"
[tool.setuptools]
namespace-packages = ["myproj.pkg"]
{field} = {value}
"""
pyproject.write_text(cleandoc(config), encoding="utf-8")
with pytest.raises(RemovedConfigError, match="namespace-packages"):
with pytest.raises((RemovedConfigError, ValueError)) as exc:
pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
assert f"tool.setuptools.{field}" in str(exc.value)


class TestPresetField:
Expand Down Expand Up @@ -498,7 +528,10 @@ def test_mark_static_fields(self, tmp_path, monkeypatch):
"""
pyproject = Path(tmp_path, "pyproject.toml")
pyproject.write_text(cleandoc(toml_config), encoding="utf-8")
dist = pyprojecttoml.apply_configuration(Distribution({}), pyproject)

with pytest.warns(SetuptoolsDeprecationWarning, match="Deprecated usage of"):
dist = pyprojecttoml.apply_configuration(Distribution({}), pyproject)

assert is_static(dist.install_requires)
assert is_static(dist.metadata.keywords)
assert is_static(dist.metadata.classifiers)
Expand Down
80 changes: 58 additions & 22 deletions setuptools/tests/config/test_setupcfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from setuptools.config.setupcfg import ConfigHandler, Target, read_configuration
from setuptools.dist import Distribution, _Distribution
from setuptools.errors import RemovedConfigError
from setuptools.warnings import SetuptoolsDeprecationWarning

from ..textwrap import DALS
Expand Down Expand Up @@ -136,19 +137,20 @@ def test_basic(self, tmpdir):
'license': 'BSD 3-Clause License',
}

with get_dist(tmpdir, meta_initial) as dist:
metadata = dist.metadata
with pytest.warns(SetuptoolsDeprecationWarning, match="Deprecated config"):
with get_dist(tmpdir, meta_initial) as dist:
metadata = dist.metadata

assert metadata.version == '10.1.1'
assert metadata.description == 'Some description'
assert metadata.long_description_content_type == 'text/something'
assert metadata.long_description == 'readme contents\nline2'
assert metadata.provides == ['package', 'package.sub']
assert metadata.license == 'BSD 3-Clause License'
assert metadata.name == 'fake_name'
assert metadata.keywords == ['one', 'two']
assert metadata.download_url == 'http://test.test.com/test/'
assert metadata.maintainer_email == '[email protected]'
assert metadata.version == '10.1.1'
assert metadata.description == 'Some description'
assert metadata.long_description_content_type == 'text/something'
assert metadata.long_description == 'readme contents\nline2'
assert metadata.provides == ['package', 'package.sub']
assert metadata.license == 'BSD 3-Clause License'
assert metadata.name == 'fake_name'
assert metadata.keywords == ['one', 'two']
assert metadata.download_url == 'http://test.test.com/test/'
assert metadata.maintainer_email == '[email protected]'

def test_license_cfg(self, tmpdir):
fake_env(
Expand Down Expand Up @@ -207,16 +209,17 @@ def test_aliases(self, tmpdir):
' Programming Language :: Python :: 3.5\n',
)

with get_dist(tmpdir) as dist:
metadata = dist.metadata
assert metadata.author_email == '[email protected]'
assert metadata.url == 'http://test.test.com/test/'
assert metadata.description == 'Short summary'
assert metadata.platforms == ['a', 'b']
assert metadata.classifiers == [
'Framework :: Django',
'Programming Language :: Python :: 3.5',
]
with pytest.warns(match='Deprecated usage of .url.'):
with get_dist(tmpdir) as dist:
metadata = dist.metadata
assert metadata.author_email == '[email protected]'
assert metadata.url == 'http://test.test.com/test/'
assert metadata.description == 'Short summary'
assert metadata.platforms == ['a', 'b']
assert metadata.classifiers == [
'Framework :: Django',
'Programming Language :: Python :: 3.5',
]

def test_multiline(self, tmpdir):
fake_env(
Expand Down Expand Up @@ -449,6 +452,39 @@ def test_make_option_lowercase(self, tmpdir):
assert metadata.name == 'foo'
assert metadata.description == 'Some description'

@pytest.mark.parametrize(
("field", "value"),
[
("provides", "setuptools"),
("obsoletes", "setuptools"),
("url", "www.setuptools.com.br"),
("download_url", "www.setuptools.com.br/42"),
],
)
def test_deprecated(self, tmpdir, field, value):
fake_env(
tmpdir,
f'[metadata]\nname = foo\ndescription = Desc\n{field} = {value}',
)
match = f"Deprecated usage of `{field}`"
with pytest.warns(SetuptoolsDeprecationWarning, match=match):
get_dist(tmpdir).__enter__()

@pytest.mark.parametrize(
("field", "value"),
[
("requires", "setuptools"),
],
)
def test_removed(self, tmpdir, field, value):
fake_env(
tmpdir,
f'[metadata]\nname = foo\ndescription = Desc\n{field} = {value}',
)
match = f"Invalid use of `{field}`"
with pytest.raises(RemovedConfigError, match=match):
get_dist(tmpdir).__enter__()


class TestOptions:
def test_basic(self, tmpdir):
Expand Down
2 changes: 1 addition & 1 deletion setuptools/tests/test_bdist_wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
long_description="Another testing distribution \N{SNOWMAN}",
author="Illustrious Author",
author_email="[email protected]",
url="http://example.org/exemplary",
project_urls={"homepage": "http://example.org/exemplary"},
packages=["complexdist"],
setup_requires=["setuptools"],
install_requires=["quux", "splort"],
Expand Down
Loading
Loading