From 60e01c75692879fbf0fc2bed23316a93442b1f7e Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 28 Sep 2023 17:52:37 +0100 Subject: [PATCH 01/20] Re-enable deprecation warning enforcement --- tox.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index b7f50ea89f..ced3e00ddf 100644 --- a/tox.ini +++ b/tox.ini @@ -6,8 +6,7 @@ deps = # ^-- use dev version while we wait for the new release setenv = PYTHONWARNDEFAULTENCODING = 1 - SETUPTOOLS_ENFORCE_DEPRECATION = 0 - # ^-- Temporarily disable, until overdue deprecations are handled + SETUPTOOLS_ENFORCE_DEPRECATION = 1 commands = pytest {posargs} usedevelop = True From 81a3547847fb8baa25c6413ab6ad596c04bc02e2 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 28 Sep 2023 17:13:24 +0100 Subject: [PATCH 02/20] Remove deprecation warning from _normalization.best_effort_version --- setuptools/_normalization.py | 41 +++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/setuptools/_normalization.py b/setuptools/_normalization.py index 3e94e662ef..e1a3080617 100644 --- a/setuptools/_normalization.py +++ b/setuptools/_normalization.py @@ -7,7 +7,6 @@ from typing import Union from .extern import packaging -from .warnings import SetuptoolsDeprecationWarning _Path = Union[str, Path] @@ -15,6 +14,7 @@ _VALID_NAME = re.compile(r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", re.I) _UNSAFE_NAME_CHARS = re.compile(r"[^A-Z0-9.]+", re.I) _NON_ALPHANUMERIC = re.compile(r"[^A-Z0-9]+", re.I) +_PEP440_FALLBACK = re.compile(r"^v?(?P(?:[0-9]+!)?[0-9]+(?:\.[0-9]+)*)", re.I) def safe_identifier(name: str) -> str: @@ -65,32 +65,35 @@ def safe_version(version: str) -> str: def best_effort_version(version: str) -> str: """Convert an arbitrary string into a version-like string. + Fallback when ``safe_version`` is not safe enough. >>> best_effort_version("v0.2 beta") '0.2b0' - - >>> import warnings - >>> warnings.simplefilter("ignore", category=SetuptoolsDeprecationWarning) >>> best_effort_version("ubuntu lts") - 'ubuntu.lts' + '0.dev0+sanitized.ubuntu.lts' + >>> best_effort_version("0.23ubuntu1") + '0.23.dev0+sanitized.ubuntu1' + >>> best_effort_version("0.23-") + '0.23.dev0+sanitized' + >>> best_effort_version("0.-_") + '0.dev0+sanitized' + >>> best_effort_version("42.+?1") + '42.dev0+sanitized.1' """ - # See pkg_resources.safe_version + # See pkg_resources._forgiving_version try: return safe_version(version) except packaging.version.InvalidVersion: - SetuptoolsDeprecationWarning.emit( - f"Invalid version: {version!r}.", - f""" - Version {version!r} is not valid according to PEP 440. - - Please make sure to specify a valid version for your package. - Also note that future releases of setuptools may halt the build process - if an invalid version is given. - """, - see_url="https://peps.python.org/pep-0440/", - due_date=(2023, 9, 26), # See setuptools/dist _validate_version - ) v = version.replace(' ', '.') - return safe_name(v) + match = _PEP440_FALLBACK.search(v) + if match: + safe = match["safe"] + rest = v[len(safe) :] + else: + safe = "0" + rest = version + safe_rest = _NON_ALPHANUMERIC.sub(".", rest).strip(".") + local = f"sanitized.{safe_rest}".strip(".") + return safe_version(f"{safe}.dev0+{local}") def safe_extra(extra: str) -> str: From 7760a7a1d4c2be4b397ecf7716d3e4b7f4aae810 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 28 Sep 2023 17:13:56 +0100 Subject: [PATCH 03/20] Remove egg_base option from dist_info --- setuptools/command/dist_info.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/setuptools/command/dist_info.py b/setuptools/command/dist_info.py index 9df625cee7..5ef322168c 100644 --- a/setuptools/command/dist_info.py +++ b/setuptools/command/dist_info.py @@ -12,7 +12,6 @@ from pathlib import Path from .. import _normalization -from ..warnings import SetuptoolsDeprecationWarning class dist_info(Command): @@ -24,13 +23,6 @@ class dist_info(Command): description = "DO NOT CALL DIRECTLY, INTERNAL ONLY: create .dist-info directory" user_options = [ - ( - 'egg-base=', - 'e', - "directory containing .egg-info directories" - " (default: top of the source tree)" - " DEPRECATED: use --output-dir.", - ), ( 'output-dir=', 'o', @@ -47,7 +39,6 @@ class dist_info(Command): negative_opt = {'no-date': 'tag-date'} def initialize_options(self): - self.egg_base = None self.output_dir = None self.name = None self.dist_info_dir = None @@ -56,13 +47,6 @@ def initialize_options(self): self.keep_egg_info = False def finalize_options(self): - if self.egg_base: - msg = "--egg-base is deprecated for dist_info command. Use --output-dir." - SetuptoolsDeprecationWarning.emit(msg, due_date=(2023, 9, 26)) - # This command is internal to setuptools, therefore it should be safe - # to remove the deprecated support soon. - self.output_dir = self.egg_base or self.output_dir - dist = self.distribution project_dir = dist.src_root or os.curdir self.output_dir = Path(self.output_dir or project_dir) From 2c96f00660b7710542e4a6c34e15a7aaa2522c70 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 28 Sep 2023 17:14:34 +0100 Subject: [PATCH 04/20] Ensure tags generated by egg_info are valid --- setuptools/command/egg_info.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index a5199deb33..7c7f57aaf8 100644 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -127,7 +127,7 @@ def name(self): def tagged_version(self): tagged = self._maybe_tag(self.distribution.get_version()) - return _normalization.best_effort_version(tagged) + return _normalization.safe_version(tagged) def _maybe_tag(self, version): """ @@ -148,7 +148,10 @@ def _already_tagged(self, version: str) -> bool: def _safe_tags(self) -> str: # To implement this we can rely on `safe_version` pretending to be version 0 # followed by tags. Then we simply discard the starting 0 (fake version number) - return _normalization.best_effort_version(f"0{self.vtags}")[1:] + try: + return _normalization.safe_version(f"0{self.vtags}")[1:] + except packaging.version.InvalidVersion: + return _normalization.safe_name(self.vtags.replace(' ', '.')) def tags(self) -> str: version = '' From db987aa6aace6df20abb0be6699c9492d60de5e4 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 28 Sep 2023 17:18:07 +0100 Subject: [PATCH 05/20] Remove 'requires' and 'license_file' from setup.cfg A search in [`grep.app`](https://grep.app/search?q=%5Erequires%5Cs%2A%3D®exp=true&filter%5Bpath.pattern%5D%5B0%5D=setup.cfg) suggests that `requires` is not utilised in `setup.cfg`, so it should be safe to remove. `license_file` is not fundamental to make distributions work, so it should be safe to remove. --- setuptools/config/setupcfg.py | 14 -------------- setuptools/tests/config/test_setupcfg.py | 17 ----------------- 2 files changed, 31 deletions(-) diff --git a/setuptools/config/setupcfg.py b/setuptools/config/setupcfg.py index bb35559069..1a0e4154b9 100644 --- a/setuptools/config/setupcfg.py +++ b/setuptools/config/setupcfg.py @@ -556,23 +556,9 @@ def parsers(self): 'platforms': parse_list, 'keywords': parse_list, 'provides': parse_list, - 'requires': self._deprecated_config_handler( - parse_list, - "The requires parameter is deprecated, please use " - "install_requires for runtime dependencies.", - due_date=(2023, 10, 30), - # Warning introduced in 27 Oct 2018 - ), 'obsoletes': parse_list, 'classifiers': self._get_parser_compound(parse_file, parse_list), 'license': exclude_files_parser('license'), - 'license_file': self._deprecated_config_handler( - exclude_files_parser('license_file'), - "The license_file parameter is deprecated, " - "use license_files instead.", - due_date=(2023, 10, 30), - # Warning introduced in 23 May 2021 - ), 'license_files': parse_list, 'description': parse_file, 'long_description': parse_file, diff --git a/setuptools/tests/config/test_setupcfg.py b/setuptools/tests/config/test_setupcfg.py index fa16728ea7..23fc0d0b47 100644 --- a/setuptools/tests/config/test_setupcfg.py +++ b/setuptools/tests/config/test_setupcfg.py @@ -388,23 +388,6 @@ def test_classifiers(self, tmpdir): with get_dist(tmpdir) as dist: assert set(dist.metadata.classifiers) == expected - def test_deprecated_config_handlers(self, tmpdir): - fake_env( - tmpdir, - '[metadata]\n' - 'version = 10.1.1\n' - 'description = Some description\n' - 'requires = some, requirement\n', - ) - - with pytest.warns(SetuptoolsDeprecationWarning, match="requires"): - with get_dist(tmpdir) as dist: - metadata = dist.metadata - - assert metadata.version == '10.1.1' - assert metadata.description == 'Some description' - assert metadata.requires == ['some', 'requirement'] - def test_interpolation(self, tmpdir): fake_env(tmpdir, '[metadata]\n' 'description = %(message)s\n') with pytest.raises(configparser.InterpolationMissingOptionError): From 091008425fc5a22edf31d1eabeb06fd1bdbd4873 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 28 Sep 2023 17:21:15 +0100 Subject: [PATCH 06/20] Improve explanation of difference between safe_version and best_effort_version --- setuptools/_normalization.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setuptools/_normalization.py b/setuptools/_normalization.py index e1a3080617..eee4fb7746 100644 --- a/setuptools/_normalization.py +++ b/setuptools/_normalization.py @@ -42,6 +42,8 @@ def safe_name(component: str) -> str: def safe_version(version: str) -> str: """Convert an arbitrary string into a valid version string. + Can still raise an ``InvalidVersion`` exception. + To avoid exceptions use ``best_effort_version``. >>> safe_version("1988 12 25") '1988.12.25' >>> safe_version("v0.2.1") From e247d21b271f9daa8a5103306bed2da6434ebc1a Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 28 Sep 2023 17:22:04 +0100 Subject: [PATCH 07/20] Remove deprecation warning for config_settings --global-option This interface is not super stable/usable yet, so it should be fine to remove. --- setuptools/build_meta.py | 14 +++----------- setuptools/tests/test_build_meta.py | 19 ------------------- 2 files changed, 3 insertions(+), 30 deletions(-) diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index 9267cf312f..3696658f4d 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -284,10 +284,9 @@ def _arbitrary_args(self, config_settings: _ConfigSettings) -> Iterator[str]: ['foo'] >>> list(fn({'--build-option': 'foo bar'})) ['foo', 'bar'] - >>> warnings.simplefilter('error', SetuptoolsDeprecationWarning) >>> list(fn({'--global-option': 'foo'})) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): - SetuptoolsDeprecationWarning: ...arguments given via `--global-option`... + ValueError: Incompatible .config_settings. ...'foo'... """ args = self._get_config("--global-option", config_settings) global_opts = self._valid_global_options() @@ -301,15 +300,8 @@ def _arbitrary_args(self, config_settings: _ConfigSettings) -> Iterator[str]: yield from self._get_config("--build-option", config_settings) if bad_args: - SetuptoolsDeprecationWarning.emit( - "Incompatible `config_settings` passed to build backend.", - f""" - The arguments {bad_args!r} were given via `--global-option`. - Please use `--build-option` instead, - `--global-option` is reserved for flags like `--verbose` or `--quiet`. - """, - due_date=(2023, 9, 26), # Warning introduced in v64.0.1, 11/Aug/2022. - ) + msg = f"Incompatible `config_settings`: {bad_args!r} ({config_settings!r})" + raise ValueError(msg) class _BuildMetaBackend(_ConfigSettingsTranslator): diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index f36119eb9c..778aedf573 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -706,25 +706,6 @@ def _assert_link_tree(self, parent_dir): for file in files: assert file.is_symlink() or os.stat(file).st_nlink > 0 - @pytest.mark.filterwarnings("ignore::setuptools.SetuptoolsDeprecationWarning") - # Since the backend is running via a process pool, in some operating systems - # we may have problems to make assertions based on warnings/stdout/stderr... - # So the best is to ignore them for the time being. - def test_editable_with_global_option_still_works(self, tmpdir_cwd): - """The usage of --global-option is now discouraged in favour of --build-option. - This is required to make more sense of the provided scape hatch and align with - previous pip behaviour. See pypa/setuptools#1928. - """ - path.build({**self._simple_pyproject_example, '_meta': {}}) - build_backend = self.get_build_backend() - assert not Path("build").exists() - - cfg = {"--global-option": ["--mode", "strict"]} - build_backend.prepare_metadata_for_build_editable("_meta", cfg) - build_backend.build_editable("temp", cfg, "_meta") - - self._assert_link_tree(next(Path("build").glob("__editable__.*"))) - def test_editable_without_config_settings(self, tmpdir_cwd): """ Sanity check to ensure tests with --mode=strict are different from the ones From 9433e907ca4a8a45e72ff877b939d88e09807ff8 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 28 Sep 2023 17:24:53 +0100 Subject: [PATCH 08/20] Remove deprecation warning for invalid versions in setuptools.dist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … and post-pone deprecations for `setup.cfg` regarding hifens instead of underscores and upper-cased fields. --- setuptools/dist.py | 42 ++++++++---------------------------------- 1 file changed, 8 insertions(+), 34 deletions(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index 2672f928d5..fce534d4e2 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -26,7 +26,7 @@ from .extern.ordered_set import OrderedSet from .extern.packaging.markers import InvalidMarker, Marker from .extern.packaging.specifiers import InvalidSpecifier, SpecifierSet -from .extern.packaging.version import InvalidVersion, Version +from .extern.packaging.version import Version from . import _entry_points from . import _normalization @@ -311,9 +311,7 @@ def __init__(self, attrs=None): self._set_metadata_defaults(attrs) - self.metadata.version = self._normalize_version( - self._validate_version(self.metadata.version) - ) + self.metadata.version = self._normalize_version(self.metadata.version) self._finalize_requires() def _validate_metadata(self): @@ -343,7 +341,10 @@ def _set_metadata_defaults(self, attrs): def _normalize_version(version): from . import sic - if isinstance(version, sic) or version is None: + if isinstance(version, numbers.Number): + # Some people apparently take "version number" too literally :) + version = str(version) + elif isinstance(version, sic) or version is None: return version normalized = str(Version(version)) @@ -352,33 +353,6 @@ def _normalize_version(version): return normalized return version - @staticmethod - def _validate_version(version): - if isinstance(version, numbers.Number): - # Some people apparently take "version number" too literally :) - version = str(version) - - if version is not None: - try: - Version(version) - except (InvalidVersion, TypeError): - from . import sic - - SetuptoolsDeprecationWarning.emit( - f"Invalid version: {version!r}.", - """ - The version specified is not a valid version according to PEP 440. - This may not work as expected with newer versions of - setuptools, pip, and PyPI. - """, - see_url="https://peps.python.org/pep-0440/", - due_date=(2023, 9, 26), - # Warning initially introduced in 26 Sept 2014 - # pypa/packaging already removed legacy versions. - ) - return sic(version) - return version - def _finalize_requires(self): """ Set `metadata.python_requires` and fix environment markers @@ -550,7 +524,7 @@ def warn_dash_deprecation(self, opt, section): versions. Please use the underscore name {underscore_opt!r} instead. """, see_docs="userguide/declarative_config.html", - due_date=(2023, 9, 26), + due_date=(2024, 9, 26), # Warning initially introduced in 3 Mar 2021 ) return underscore_opt @@ -574,7 +548,7 @@ def make_option_lowercase(self, opt, section): future versions. Please use lowercase {lowercase_opt!r} instead. """, see_docs="userguide/declarative_config.html", - due_date=(2023, 9, 26), + due_date=(2024, 9, 26), # Warning initially introduced in 6 Mar 2021 ) return lowercase_opt From 6cc69fe05659ebf0f2250397b173eed910065d0c Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 28 Sep 2023 18:07:42 +0100 Subject: [PATCH 09/20] Add news fragments --- newsfragments/4066.removal.1.rst | 2 ++ newsfragments/4066.removal.2.rst | 4 ++++ newsfragments/4066.removal.3.rst | 4 ++++ newsfragments/4066.removal.4.rst | 2 ++ 4 files changed, 12 insertions(+) create mode 100644 newsfragments/4066.removal.1.rst create mode 100644 newsfragments/4066.removal.2.rst create mode 100644 newsfragments/4066.removal.3.rst create mode 100644 newsfragments/4066.removal.4.rst diff --git a/newsfragments/4066.removal.1.rst b/newsfragments/4066.removal.1.rst new file mode 100644 index 0000000000..40cfd976ec --- /dev/null +++ b/newsfragments/4066.removal.1.rst @@ -0,0 +1,2 @@ +Configuring project ``version`` and ``egg_info.tag_*`` in such a way that +results in invalid version strings (according to :pep:`440`) is no longer permitted. diff --git a/newsfragments/4066.removal.2.rst b/newsfragments/4066.removal.2.rst new file mode 100644 index 0000000000..0209f5248d --- /dev/null +++ b/newsfragments/4066.removal.2.rst @@ -0,0 +1,4 @@ +Remove deprecated ``egg_base`` option from ``dist_info``. +Note that the ``dist_info`` command is considered internal to the way +``setuptools`` build backend works and not intended for +public usage. diff --git a/newsfragments/4066.removal.3.rst b/newsfragments/4066.removal.3.rst new file mode 100644 index 0000000000..7d4048b785 --- /dev/null +++ b/newsfragments/4066.removal.3.rst @@ -0,0 +1,4 @@ +The parsing of the deprecated ``metadata.license_file`` and +``metadata.requires`` fields in ``setup.cfg`` is no longer supported. +Users are expected to move to ``metadata.license_files`` and +``options.install_requires`` (respectively). diff --git a/newsfragments/4066.removal.4.rst b/newsfragments/4066.removal.4.rst new file mode 100644 index 0000000000..d599450ac2 --- /dev/null +++ b/newsfragments/4066.removal.4.rst @@ -0,0 +1,2 @@ +Passing ``config_settings`` to ``setuptools.build_meta`` with +deprecated values for ``--global-option`` is no longer allowed. From 4f60770c244be776e381dca5b3f624dc177cdc05 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 17 Nov 2023 12:38:26 +0000 Subject: [PATCH 10/20] Xfail on deprecated bdist_rpm tests --- setuptools/tests/test_bdist_deprecations.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setuptools/tests/test_bdist_deprecations.py b/setuptools/tests/test_bdist_deprecations.py index 1b69c41858..61f4e9a4cb 100644 --- a/setuptools/tests/test_bdist_deprecations.py +++ b/setuptools/tests/test_bdist_deprecations.py @@ -10,6 +10,7 @@ @pytest.mark.skipif(sys.platform == 'win32', reason='non-Windows only') +@pytest.mark.xfail(reason="bdist_rpm is long deprecated, should we remove it? #1988") @mock.patch('distutils.command.bdist_rpm.bdist_rpm') def test_bdist_rpm_warning(distutils_cmd, tmpdir_cwd): dist = Distribution( From e4b6fc964c887838b9cec546e053cf5ceee391c7 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 17 Nov 2023 12:39:05 +0000 Subject: [PATCH 11/20] Be strict on missing 'dynamic' in pyproject.toml --- setuptools/config/_apply_pyprojecttoml.py | 49 +++++++++++++------ setuptools/config/pyprojecttoml.py | 6 +-- .../tests/config/test_apply_pyprojecttoml.py | 11 ++--- setuptools/tests/config/test_pyprojecttoml.py | 11 +---- 4 files changed, 42 insertions(+), 35 deletions(-) diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py index 4489d22437..9d2e77c8f5 100644 --- a/setuptools/config/_apply_pyprojecttoml.py +++ b/setuptools/config/_apply_pyprojecttoml.py @@ -105,13 +105,13 @@ def _apply_tool_table(dist: "Distribution", config: dict, filename: _Path): def _handle_missing_dynamic(dist: "Distribution", project_table: dict): """Be temporarily forgiving with ``dynamic`` fields not listed in ``dynamic``""" - # TODO: Set fields back to `None` once the feature stabilizes dynamic = set(project_table.get("dynamic", [])) for field, getter in _PREVIOUSLY_DEFINED.items(): if not (field in project_table or field in dynamic): value = getter(dist) if value: - _WouldIgnoreField.emit(field=field, value=value) + _MissingDynamic.emit(field=field, value=value) + project_table[field] = _RESET_PREVIOUSLY_DEFINED.get(field) def json_compatible_key(key: str) -> str: @@ -226,14 +226,18 @@ def _unify_entry_points(project_table: dict): renaming = {"scripts": "console_scripts", "gui_scripts": "gui_scripts"} for key, value in list(project.items()): # eager to allow modifications norm_key = json_compatible_key(key) - if norm_key in renaming and value: + if norm_key in renaming: + # Don't skip even if value is empty (reason: reset missing `dynamic`) entry_points[renaming[norm_key]] = project.pop(key) if entry_points: project["entry-points"] = { name: [f"{k} = {v}" for k, v in group.items()] for name, group in entry_points.items() + if group # now we can skip empty groups } + # Sometimes this will set `project["entry-points"] = {}`, and that is + # intentional (for reseting configurations that are missing `dynamic`). def _copy_command_options(pyproject: dict, dist: "Distribution", filename: _Path): @@ -388,14 +392,27 @@ def _acessor(obj): } -class _WouldIgnoreField(SetuptoolsDeprecationWarning): - _SUMMARY = "`{field}` defined outside of `pyproject.toml` would be ignored." +_RESET_PREVIOUSLY_DEFINED: dict = { + # Fix improper setting: given in `setup.py`, but not listed in `dynamic` + # dict: pyproject name => value to which reset + "license": {}, + "authors": [], + "maintainers": [], + "keywords": [], + "classifiers": [], + "urls": {}, + "entry-points": {}, + "scripts": {}, + "gui-scripts": {}, + "dependencies": [], + "optional-dependencies": [], +} - _DETAILS = """ - ########################################################################## - # configuration would be ignored/result in error due to `pyproject.toml` # - ########################################################################## +class _MissingDynamic(SetuptoolsWarning): + _SUMMARY = "`{field}` defined outside of `pyproject.toml` is ignored." + + _DETAILS = """ The following seems to be defined outside of `pyproject.toml`: `{field} = {value!r}` @@ -405,12 +422,14 @@ class _WouldIgnoreField(SetuptoolsDeprecationWarning): https://packaging.python.org/en/latest/specifications/declaring-project-metadata/ - For the time being, `setuptools` will still consider the given value (as a - **transitional** measure), but please note that future releases of setuptools will - follow strictly the standard. - - To prevent this warning, you can list `{field}` under `dynamic` or alternatively + To prevent this problem, you can list `{field}` under `dynamic` or alternatively remove the `[project]` table from your file and rely entirely on other means of configuration. """ - _DUE_DATE = (2023, 10, 30) # Initially introduced in 27 May 2022 + # TODO: Consider removing this check in the future? + # There is a trade-off here between improving "debug-ability" and the cost + # of running/testing/maintaining these unnecessary checks... + + @classmethod + def details(cls, field: str, value: Any) -> str: + return cls._DETAILS.format(field=field, value=value) diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py index 93dbd9f559..5b7884fe4f 100644 --- a/setuptools/config/pyprojecttoml.py +++ b/setuptools/config/pyprojecttoml.py @@ -17,7 +17,7 @@ from ..errors import FileError, OptionError from ..warnings import SetuptoolsWarning from . import expand as _expand -from ._apply_pyprojecttoml import _PREVIOUSLY_DEFINED, _WouldIgnoreField +from ._apply_pyprojecttoml import _PREVIOUSLY_DEFINED, _MissingDynamic from ._apply_pyprojecttoml import apply as _apply if TYPE_CHECKING: @@ -330,9 +330,7 @@ def _set_scripts(field: str, group: str): if group in groups: value = groups.pop(group) if field not in self.dynamic: - _WouldIgnoreField.emit(field=field, value=value) - # TODO: Don't set field when support for pyproject.toml stabilizes - # instead raise an error as specified in PEP 621 + raise OptionError(_MissingDynamic.details(field, value)) expanded[field] = value _set_scripts("scripts", "console_scripts") diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py index 294947a00a..ba36416be1 100644 --- a/setuptools/tests/config/test_apply_pyprojecttoml.py +++ b/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -21,7 +21,7 @@ from setuptools.dist import Distribution from setuptools.config import setupcfg, pyprojecttoml from setuptools.config import expand -from setuptools.config._apply_pyprojecttoml import _WouldIgnoreField, _some_attrgetter +from setuptools.config._apply_pyprojecttoml import _MissingDynamic, _some_attrgetter from setuptools.command.egg_info import write_requirements from setuptools.warnings import SetuptoolsDeprecationWarning @@ -339,18 +339,15 @@ def pyproject(self, tmp_path, dynamic, extra_content=""): ], ) def test_not_listed_in_dynamic(self, tmp_path, attr, field, value): - """For the time being we just warn if the user pre-set values (e.g. via - ``setup.py``) but do not include them in ``dynamic``. - """ + """Setuptools cannot set a field if not listed in ``dynamic``""" pyproject = self.pyproject(tmp_path, []) dist = makedist(tmp_path, **{attr: value}) msg = re.compile(f"defined outside of `pyproject.toml`:.*{field}", re.S) - with pytest.warns(_WouldIgnoreField, match=msg): + with pytest.warns(_MissingDynamic, match=msg): dist = pyprojecttoml.apply_configuration(dist, pyproject) - # TODO: Once support for pyproject.toml config stabilizes attr should be None dist_value = _some_attrgetter(f"metadata.{attr}", attr)(dist) - assert dist_value == value + assert not dist_value @pytest.mark.parametrize( "attr, field, value", diff --git a/setuptools/tests/config/test_pyprojecttoml.py b/setuptools/tests/config/test_pyprojecttoml.py index 81ec949a42..318885a6bd 100644 --- a/setuptools/tests/config/test_pyprojecttoml.py +++ b/setuptools/tests/config/test_pyprojecttoml.py @@ -6,7 +6,6 @@ import tomli_w from path import Path as _Path -from setuptools.config._apply_pyprojecttoml import _WouldIgnoreField from setuptools.config.pyprojecttoml import ( read_configuration, expand_configuration, @@ -200,14 +199,8 @@ def test_scripts_not_listed_in_dynamic(self, tmp_path, missing_dynamic): dynamic = {"scripts", "gui-scripts", "entry-points"} - {missing_dynamic} msg = f"defined outside of `pyproject.toml`:.*{missing_dynamic}" - with pytest.warns(_WouldIgnoreField, match=re.compile(msg, re.S)): - expanded = expand_configuration(self.pyproject(dynamic), tmp_path) - - expanded_project = expanded["project"] - assert dynamic < set(expanded_project) - assert len(expanded_project["entry-points"]) == 1 - # TODO: Test the following when pyproject.toml support stabilizes: - # >>> assert missing_dynamic not in expanded_project + with pytest.raises(OptionError, match=re.compile(msg, re.S)): + expand_configuration(self.pyproject(dynamic), tmp_path) class TestClassifiers: From 9c35cd935aeae08596cf80f327da5b0b40a2cb7f Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 17 Nov 2023 13:17:45 +0000 Subject: [PATCH 12/20] Enforce namespace-packages are not used in pyproject.toml --- setuptools/config/_apply_pyprojecttoml.py | 25 +++++++++++-------- setuptools/errors.py | 4 +++ .../tests/config/test_apply_pyprojecttoml.py | 4 +-- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py index 9d2e77c8f5..ba0284e879 100644 --- a/setuptools/config/_apply_pyprojecttoml.py +++ b/setuptools/config/_apply_pyprojecttoml.py @@ -12,6 +12,7 @@ from collections.abc import Mapping from email.headerregistry import Address from functools import partial, reduce +from inspect import cleandoc from itertools import chain from types import MappingProxyType from typing import ( @@ -28,6 +29,7 @@ cast, ) +from ..errors import RemovedConfigError from ..warnings import SetuptoolsWarning, SetuptoolsDeprecationWarning if TYPE_CHECKING: @@ -90,12 +92,13 @@ def _apply_tool_table(dist: "Distribution", config: dict, filename: _Path): for field, value in tool_table.items(): norm_key = json_compatible_key(field) - if norm_key in TOOL_TABLE_DEPRECATIONS: - suggestion, kwargs = TOOL_TABLE_DEPRECATIONS[norm_key] - msg = f"The parameter `{norm_key}` is deprecated, {suggestion}" - SetuptoolsDeprecationWarning.emit( - "Deprecated config", msg, **kwargs # type: ignore - ) + if norm_key in TOOL_TABLE_REMOVALS: + suggestion = cleandoc(TOOL_TABLE_REMOVALS[norm_key]) + msg = f""" + The parameter `tool.setuptools.{field}` was long deprecated + and has been removed from `pyproject.toml`. + """ + raise RemovedConfigError("\n".join([cleandoc(msg), suggestion])) norm_key = TOOL_TABLE_RENAMES.get(norm_key, norm_key) _set_config(dist, norm_key, value) @@ -357,11 +360,11 @@ def _acessor(obj): } TOOL_TABLE_RENAMES = {"script_files": "scripts"} -TOOL_TABLE_DEPRECATIONS = { - "namespace_packages": ( - "consider using implicit namespaces instead (PEP 420).", - {"due_date": (2023, 10, 30)}, # warning introduced in May 2022 - ) +TOOL_TABLE_REMOVALS = { + "namespace_packages": """ + Please migrate to implicit native namespaces instead. + See https://packaging.python.org/en/latest/guides/packaging-namespace-packages/. + """, } SETUPTOOLS_PATCHES = { diff --git a/setuptools/errors.py b/setuptools/errors.py index ec7fb3b6c4..855875d0ed 100644 --- a/setuptools/errors.py +++ b/setuptools/errors.py @@ -29,6 +29,10 @@ BaseError = _distutils_errors.DistutilsError +class RemovedConfigError(OptionError): + """Error used for configurations that were deprecated and removed.""" + + class RemovedCommandError(BaseError, RuntimeError): """Error used for commands that have been removed in setuptools. diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py index ba36416be1..7905aa9ab6 100644 --- a/setuptools/tests/config/test_apply_pyprojecttoml.py +++ b/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -23,7 +23,7 @@ from setuptools.config import expand from setuptools.config._apply_pyprojecttoml import _MissingDynamic, _some_attrgetter from setuptools.command.egg_info import write_requirements -from setuptools.warnings import SetuptoolsDeprecationWarning +from setuptools.errors import RemovedConfigError from .downloads import retrieve_file, urls_from_file @@ -316,7 +316,7 @@ def test_namespace_packages(self, tmp_path): namespace-packages = ["myproj.pkg"] """ pyproject.write_text(cleandoc(config), encoding="utf-8") - with pytest.warns(SetuptoolsDeprecationWarning, match="namespace_packages"): + with pytest.raises(RemovedConfigError, match="namespace-packages"): pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) From 4a34a1ca341dc32a3a9603e3fecebdc4ae4e5d75 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 17 Nov 2023 13:18:17 +0000 Subject: [PATCH 13/20] Use custom class for InvalidConfigError This way we don't have to see `DistutilsOptionError` in the error message. --- setuptools/config/pyprojecttoml.py | 6 +++--- setuptools/errors.py | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py index 5b7884fe4f..379ef222f9 100644 --- a/setuptools/config/pyprojecttoml.py +++ b/setuptools/config/pyprojecttoml.py @@ -14,7 +14,7 @@ from functools import partial from typing import TYPE_CHECKING, Callable, Dict, Mapping, Optional, Set, Union -from ..errors import FileError, OptionError +from ..errors import FileError, InvalidConfigError from ..warnings import SetuptoolsWarning from . import expand as _expand from ._apply_pyprojecttoml import _PREVIOUSLY_DEFINED, _MissingDynamic @@ -265,7 +265,7 @@ def _ensure_previously_set(self, dist: "Distribution", field: str): "Some dynamic fields need to be specified via `tool.setuptools.dynamic`" "\nothers must be specified via the equivalent attribute in `setup.py`." ) - raise OptionError(msg) + raise InvalidConfigError(msg) def _expand_directive( self, specifier: str, directive, package_dir: Mapping[str, str] @@ -330,7 +330,7 @@ def _set_scripts(field: str, group: str): if group in groups: value = groups.pop(group) if field not in self.dynamic: - raise OptionError(_MissingDynamic.details(field, value)) + raise InvalidConfigError(_MissingDynamic.details(field, value)) expanded[field] = value _set_scripts("scripts", "console_scripts") diff --git a/setuptools/errors.py b/setuptools/errors.py index 855875d0ed..67a5a1df10 100644 --- a/setuptools/errors.py +++ b/setuptools/errors.py @@ -29,6 +29,10 @@ BaseError = _distutils_errors.DistutilsError +class InvalidConfigError(OptionError): + """Error used for invalid configurations.""" + + class RemovedConfigError(OptionError): """Error used for configurations that were deprecated and removed.""" From b44faba4a8a7598a15e2fb2f0c487c7224967a14 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 17 Nov 2023 13:33:59 +0000 Subject: [PATCH 14/20] Add newsfragments for latest removals --- newsfragments/4066.removal.5.rst | 4 ++++ newsfragments/4066.removal.6.rst | 4 ++++ 2 files changed, 8 insertions(+) create mode 100644 newsfragments/4066.removal.5.rst create mode 100644 newsfragments/4066.removal.6.rst diff --git a/newsfragments/4066.removal.5.rst b/newsfragments/4066.removal.5.rst new file mode 100644 index 0000000000..99f77a0965 --- /dev/null +++ b/newsfragments/4066.removal.5.rst @@ -0,0 +1,4 @@ +Removed deprecated ``namespace-packages`` from ``pyproject.toml``. +Users are asked to use +:doc:`implicit namespace packages ` +(as defined in :pep:`420`). diff --git a/newsfragments/4066.removal.6.rst b/newsfragments/4066.removal.6.rst new file mode 100644 index 0000000000..350ea60eb6 --- /dev/null +++ b/newsfragments/4066.removal.6.rst @@ -0,0 +1,4 @@ +Added strict enforcement for ``project.dynamic`` in ``pyproject.toml``. +This removes the transitional ability of users configuring certain parameters +via ``setup.py`` without making the necessary changes to ``pyproject.toml`` +(as mandated by :pep:`612`). From b802c1be4c83ab1bca762999bcbdf87dd2850372 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 17 Nov 2023 13:34:56 +0000 Subject: [PATCH 15/20] Update newsfragments --- newsfragments/4066.removal.2.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/newsfragments/4066.removal.2.rst b/newsfragments/4066.removal.2.rst index 0209f5248d..ff3c7c2885 100644 --- a/newsfragments/4066.removal.2.rst +++ b/newsfragments/4066.removal.2.rst @@ -1,4 +1,4 @@ -Remove deprecated ``egg_base`` option from ``dist_info``. +Removed deprecated ``egg_base`` option from ``dist_info``. Note that the ``dist_info`` command is considered internal to the way ``setuptools`` build backend works and not intended for public usage. From b2438188f124774dce67b3186c8ce8e433597849 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 17 Nov 2023 13:44:53 +0000 Subject: [PATCH 16/20] Fix lint errors --- setuptools/config/_apply_pyprojecttoml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py index ba0284e879..80318d5d0b 100644 --- a/setuptools/config/_apply_pyprojecttoml.py +++ b/setuptools/config/_apply_pyprojecttoml.py @@ -30,7 +30,7 @@ ) from ..errors import RemovedConfigError -from ..warnings import SetuptoolsWarning, SetuptoolsDeprecationWarning +from ..warnings import SetuptoolsWarning if TYPE_CHECKING: from setuptools._importlib import metadata # noqa From b97814ade36e48143202b6a56949faeda0c143ea Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 17 Nov 2023 14:28:31 +0000 Subject: [PATCH 17/20] Add workaround for unreleased PyNaCl --- .github/workflows/main.yml | 3 +++ tox.ini | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2e0e55cf43..f214cb148d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -81,6 +81,9 @@ jobs: echo "PRE_BUILT_SETUPTOOLS_SDIST=$(ls dist/*.tar.gz)" >> $GITHUB_ENV echo "PRE_BUILT_SETUPTOOLS_WHEEL=$(ls dist/*.whl)" >> $GITHUB_ENV rm -rf setuptools.egg-info # Avoid interfering with the other tests + - name: Workaround for unreleased PyNaCl (pyca/pynacl#805) + if: contains(matrix.python, 'pypy') + run: echo "SETUPTOOLS_ENFORCE_DEPRECATION=0" >> $GITHUB_ENV - name: Install tox run: | python -m pip install tox diff --git a/tox.ini b/tox.ini index ced3e00ddf..5381358e6d 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,7 @@ deps = # ^-- use dev version while we wait for the new release setenv = PYTHONWARNDEFAULTENCODING = 1 - SETUPTOOLS_ENFORCE_DEPRECATION = 1 + SETUPTOOLS_ENFORCE_DEPRECATION = {env:SETUPTOOLS_ENFORCE_DEPRECATION:1} commands = pytest {posargs} usedevelop = True @@ -14,6 +14,7 @@ extras = testing pass_env = SETUPTOOLS_USE_DISTUTILS + SETUPTOOLS_ENFORCE_DEPRECATION PRE_BUILT_SETUPTOOLS_WHEEL PRE_BUILT_SETUPTOOLS_SDIST TIMEOUT_BACKEND_TEST # timeout (in seconds) for test_build_meta From 7c111b812d5ee0916b1a15cc956f3099f3cd19f6 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 20 Nov 2023 13:04:08 +0000 Subject: [PATCH 18/20] Use InvalidConfigError instead of ValueError in build_meta --- setuptools/build_meta.py | 2 +- setuptools/tests/test_build_meta.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index 3696658f4d..b8bc80a4de 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -301,7 +301,7 @@ def _arbitrary_args(self, config_settings: _ConfigSettings) -> Iterator[str]: if bad_args: msg = f"Incompatible `config_settings`: {bad_args!r} ({config_settings!r})" - raise ValueError(msg) + raise errors.InvalidConfigError(msg) class _BuildMetaBackend(_ConfigSettingsTranslator): diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index 778aedf573..22e3b4303a 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -901,7 +901,8 @@ def test_build_with_empty_setuppy(self, build_backend, build_hook): files = {'setup.py': ''} path.build(files) - with pytest.raises(ValueError, match=re.escape('No distribution was found.')): + msg = re.escape('No distribution was found.') + with pytest.raises(ValueError, match=msg): getattr(build_backend, build_hook)("temp") From f43196216b38133848f9df2db9229ad864910900 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 20 Nov 2023 13:10:02 +0000 Subject: [PATCH 19/20] Remove deprecated handling of build-option passed as global-option --- setuptools/build_meta.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index b8bc80a4de..4a60397d69 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -284,25 +284,11 @@ def _arbitrary_args(self, config_settings: _ConfigSettings) -> Iterator[str]: ['foo'] >>> list(fn({'--build-option': 'foo bar'})) ['foo', 'bar'] - >>> list(fn({'--global-option': 'foo'})) # doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ValueError: Incompatible .config_settings. ...'foo'... + >>> list(fn({'--global-option': 'foo'})) + [] """ - args = self._get_config("--global-option", config_settings) - global_opts = self._valid_global_options() - bad_args = [] - - for arg in args: - if arg.strip("-") not in global_opts: - bad_args.append(arg) - yield arg - yield from self._get_config("--build-option", config_settings) - if bad_args: - msg = f"Incompatible `config_settings`: {bad_args!r} ({config_settings!r})" - raise errors.InvalidConfigError(msg) - class _BuildMetaBackend(_ConfigSettingsTranslator): def _get_build_requires(self, config_settings, requirements): From a5a75051eeef2e3b3dc938fb246bef3f6bbc6ca7 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 20 Nov 2023 13:12:38 +0000 Subject: [PATCH 20/20] Relax validation of --global-option in build_meta In `pip` docs, the following example is mentioned: > python -m pip wheel --global-option bdist_ext --global-option -DFOO wheel Since both `--global-option` and `--build-option` are supposed to be compatible with `pip` options, we should not prevent this abuse... (Although it is still error prone and the user is responsible for understanding in detail what is going on how this accidentally works). --- setuptools/build_meta.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index 4a60397d69..ff1d7eaee2 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -185,11 +185,6 @@ def _get_config(self, key: str, config_settings: _ConfigSettings) -> List[str]: opts = cfg.get(key) or [] return shlex.split(opts) if isinstance(opts, str) else opts - def _valid_global_options(self): - """Global options accepted by setuptools (e.g. quiet or verbose).""" - options = (opt[:2] for opt in setuptools.dist.Distribution.global_options) - return {flag for long_and_short in options for flag in long_and_short if flag} - def _global_args(self, config_settings: _ConfigSettings) -> Iterator[str]: """ Let the user specify ``verbose`` or ``quiet`` + escape hatch via @@ -220,9 +215,7 @@ def _global_args(self, config_settings: _ConfigSettings) -> Iterator[str]: level = str(cfg.get("quiet") or cfg.get("--quiet") or "1") yield ("-v" if level.lower() in falsey else "-q") - valid = self._valid_global_options() - args = self._get_config("--global-option", config_settings) - yield from (arg for arg in args if arg.strip("-") in valid) + yield from self._get_config("--global-option", config_settings) def __dist_info_args(self, config_settings: _ConfigSettings) -> Iterator[str]: """