From 000b1963c0679814e6de144125daee581dd0090a Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 20 Jan 2025 12:11:59 +0000 Subject: [PATCH 1/4] Add warning for deprecated metadata fields --- docs/references/keywords.rst | 18 ++++++++++++++---- setuptools/dist.py | 23 +++++++++++++++++++++++ setuptools/tests/test_dist.py | 21 ++++++++++++++++++++- 3 files changed, 57 insertions(+), 5 deletions(-) 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/setuptools/dist.py b/setuptools/dist.py index 0249651267..97f6859c0f 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -231,6 +231,18 @@ def check_packages(dist, attr, value): ) +def _check_deprecated_metadata_field( + dist: Distribution, attr: str, value: object +) -> None: + if value: + SetuptoolsDeprecationWarning.emit( + f"Deprecated usage of `{attr}` in setuptools configuration.", + see_docs=f"references/keywords.html#keyword-{attr.replace('_', '-')}", + due_date=(2027, 1, 25), + # Warning initially introduced in 14 Jan 2025 + ) + + if TYPE_CHECKING: # Work around a mypy issue where type[T] can't be used as a base: https://github.com/python/mypy/issues/10962 from distutils.core import Distribution as _Distribution @@ -294,6 +306,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 @@ -318,6 +338,9 @@ def __init__(self, attrs: MutableMapping[str, Any] | None = None) -> None: dist_attrs = {k: v for k, v in attrs.items() if k not in metadata_only} _Distribution.__init__(self, dist_attrs) + for attr in self._DEPRECATED_FIELDS: + _check_deprecated_metadata_field(self, attr, dist_attrs.get(attr)) + # Private API (setuptools-use only, not restricted to Distribution) # Stores files that are referenced by the configuration and need to be in the # sdist (e.g. `version = file: VERSION.txt`) diff --git a/setuptools/tests/test_dist.py b/setuptools/tests/test_dist.py index 533eb9f45e..2524a92e93 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_nspkg_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 From eef853baba1450c67679f04c14e9a9f417739638 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 20 Jan 2025 12:33:16 +0000 Subject: [PATCH 2/4] Prevent errors with deprecation warnings in tests --- setuptools/tests/test_bdist_wheel.py | 2 +- setuptools/tests/test_core_metadata.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/setuptools/tests/test_bdist_wheel.py b/setuptools/tests/test_bdist_wheel.py index d51dfbeb6d..7d9d51f1d4 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" ), ), ( From 7792faca3d19ee9009c2ccb03268a08297b6abe3 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 20 Jan 2025 17:13:17 +0000 Subject: [PATCH 3/4] Refactor check for deprecations in 'setup.py' --- setuptools/dist.py | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index 97f6859c0f..8ec63c83fc 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -231,18 +231,6 @@ def check_packages(dist, attr, value): ) -def _check_deprecated_metadata_field( - dist: Distribution, attr: str, value: object -) -> None: - if value: - SetuptoolsDeprecationWarning.emit( - f"Deprecated usage of `{attr}` in setuptools configuration.", - see_docs=f"references/keywords.html#keyword-{attr.replace('_', '-')}", - due_date=(2027, 1, 25), - # Warning initially introduced in 14 Jan 2025 - ) - - if TYPE_CHECKING: # Work around a mypy issue where type[T] can't be used as a base: https://github.com/python/mypy/issues/10962 from distutils.core import Distribution as _Distribution @@ -338,9 +326,6 @@ def __init__(self, attrs: MutableMapping[str, Any] | None = None) -> None: dist_attrs = {k: v for k, v in attrs.items() if k not in metadata_only} _Distribution.__init__(self, dist_attrs) - for attr in self._DEPRECATED_FIELDS: - _check_deprecated_metadata_field(self, attr, dist_attrs.get(attr)) - # Private API (setuptools-use only, not restricted to Distribution) # Stores files that are referenced by the configuration and need to be in the # sdist (e.g. `version = file: VERSION.txt`) @@ -349,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"} @@ -366,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. @@ -1016,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) From 6b6736cffea4905832ff651c013e503ceea1e52e Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 20 Jan 2025 17:14:00 +0000 Subject: [PATCH 4/4] Warn deprecated metadata fields in setup.cfg --- setuptools/config/setupcfg.py | 29 +++++++-- setuptools/tests/config/test_setupcfg.py | 80 +++++++++++++++++------- setuptools/tests/test_core_metadata.py | 1 + 3 files changed, 84 insertions(+), 26 deletions(-) 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/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_core_metadata.py b/setuptools/tests/test_core_metadata.py index f14917c573..04eed528eb 100644 --- a/setuptools/tests/test_core_metadata.py +++ b/setuptools/tests/test_core_metadata.py @@ -381,6 +381,7 @@ def dist(self, request, monkeypatch, tmp_path): yield setupcfg.apply_configuration(Distribution({}), config) @pytest.mark.uses_network + @pytest.mark.filterwarnings('ignore:Deprecated config') def test_equivalent_output(self, tmp_path, dist): """Ensure output from setuptools is equivalent to the one from `pypa/wheel`""" # Generate a METADATA file using pypa/wheel for comparison