diff --git a/docs/references/keywords.rst b/docs/references/keywords.rst index 41d30c33d4..6fe03c6d89 100644 --- a/docs/references/keywords.rst +++ b/docs/references/keywords.rst @@ -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`` @@ -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. @@ -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. diff --git a/newsfragments/4811.change.rst b/newsfragments/4811.change.rst new file mode 100644 index 0000000000..697ecf8570 --- /dev/null +++ b/newsfragments/4811.change.rst @@ -0,0 +1,2 @@ +Configuration options that yield deprecated metadata fields +result now in warnings. diff --git a/pytest.ini b/pytest.ini index 292b65864c..777f700d3f 100644 --- a/pytest.ini +++ b/pytest.ini @@ -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)` diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py index 331596bdd7..95fde5ddd3 100644 --- a/setuptools/config/_apply_pyprojecttoml.py +++ b/setuptools/config/_apply_pyprojecttoml.py @@ -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 @@ -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) @@ -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"), diff --git a/setuptools/config/setupcfg.py b/setuptools/config/setupcfg.py index 633aa9d45d..417531f347 100644 --- a/setuptools/config/setupcfg.py +++ b/setuptools/config/setupcfg.py @@ -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 @@ -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' @@ -561,8 +580,9 @@ 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, @@ -570,7 +590,8 @@ def parsers(self): '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, } diff --git a/setuptools/dist.py b/setuptools/dist.py index 0249651267..8ec63c83fc 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -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 @@ -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"} @@ -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. @@ -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) diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py index 20146b4a89..e7395c0e43 100644 --- a/setuptools/tests/config/test_apply_pyprojecttoml.py +++ b/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -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 @@ -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: @@ -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) diff --git a/setuptools/tests/config/test_setupcfg.py b/setuptools/tests/config/test_setupcfg.py index adadc02da3..3c1122dd01 100644 --- a/setuptools/tests/config/test_setupcfg.py +++ b/setuptools/tests/config/test_setupcfg.py @@ -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 @@ -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 == 'test@test.com' + 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 == 'test@test.com' def test_license_cfg(self, tmpdir): fake_env( @@ -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 == 'test@test.com' - 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 == 'test@test.com' + 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( @@ -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): diff --git a/setuptools/tests/test_bdist_wheel.py b/setuptools/tests/test_bdist_wheel.py index 776d21d729..8b2a2e5c59 100644 --- a/setuptools/tests/test_bdist_wheel.py +++ b/setuptools/tests/test_bdist_wheel.py @@ -89,7 +89,7 @@ long_description="Another testing distribution \N{SNOWMAN}", author="Illustrious Author", author_email="illustrious@example.org", - url="http://example.org/exemplary", + project_urls={"homepage": "http://example.org/exemplary"}, packages=["complexdist"], setup_requires=["setuptools"], install_requires=["quux", "splort"], diff --git a/setuptools/tests/test_core_metadata.py b/setuptools/tests/test_core_metadata.py index b1edb79b40..f14917c573 100644 --- a/setuptools/tests/test_core_metadata.py +++ b/setuptools/tests/test_core_metadata.py @@ -92,10 +92,11 @@ def __read_test_cases(): ], ), ), - ( + pytest.param( 'Metadata version 1.1: Download URL', - params( - download_url='https://example.com', + params(download_url='https://example.com'), + marks=pytest.mark.filterwarnings( + "ignore:Deprecated usage of .download_url" ), ), ( diff --git a/setuptools/tests/test_dist.py b/setuptools/tests/test_dist.py index e65ab310e7..e64277ebcb 100644 --- a/setuptools/tests/test_dist.py +++ b/setuptools/tests/test_dist.py @@ -5,7 +5,7 @@ import pytest -from setuptools import Distribution +from setuptools import Distribution, SetuptoolsDeprecationWarning from setuptools.dist import check_package_data, check_specifier from .test_easy_install import make_trivial_sdist @@ -276,3 +276,22 @@ def test_dist_default_name(tmp_path, dist_name, package_dir, package_files): dist.set_defaults() assert dist.py_modules or dist.packages assert dist.get_name() == dist_name + + +@pytest.mark.parametrize( + ("field", "value"), + [ + ("requires", ["setuptools"]), + ("provides", ["setuptools"]), + ("obsoletes", ["setuptools"]), + ("url", "www.setuptools.com.br"), + ("download_url", "www.setuptools.com.br/42"), + ], +) +def test_deprecated_fields(tmpdir_cwd, field, value): + """See discussion in https://github.com/pypa/setuptools/issues/4797""" + attrs = {"name": "test", "version": "0.42", field: value} + match = f"Deprecated usage of `{field}`" + with pytest.warns(SetuptoolsDeprecationWarning, match=match): + dist = Distribution(attrs) + assert getattr(dist.metadata, field, None) == value