Skip to content

Commit 30b8a88

Browse files
committed
Handle wave of overdue deprecations (#4066)
2 parents bbce801 + a5a7505 commit 30b8a88

22 files changed

+137
-207
lines changed

.github/workflows/main.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ jobs:
8181
echo "PRE_BUILT_SETUPTOOLS_SDIST=$(ls dist/*.tar.gz)" >> $GITHUB_ENV
8282
echo "PRE_BUILT_SETUPTOOLS_WHEEL=$(ls dist/*.whl)" >> $GITHUB_ENV
8383
rm -rf setuptools.egg-info # Avoid interfering with the other tests
84+
- name: Workaround for unreleased PyNaCl (pyca/pynacl#805)
85+
if: contains(matrix.python, 'pypy')
86+
run: echo "SETUPTOOLS_ENFORCE_DEPRECATION=0" >> $GITHUB_ENV
8487
- name: Install tox
8588
run: |
8689
python -m pip install tox

newsfragments/4066.removal.1.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Configuring project ``version`` and ``egg_info.tag_*`` in such a way that
2+
results in invalid version strings (according to :pep:`440`) is no longer permitted.

newsfragments/4066.removal.2.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Removed deprecated ``egg_base`` option from ``dist_info``.
2+
Note that the ``dist_info`` command is considered internal to the way
3+
``setuptools`` build backend works and not intended for
4+
public usage.

newsfragments/4066.removal.3.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
The parsing of the deprecated ``metadata.license_file`` and
2+
``metadata.requires`` fields in ``setup.cfg`` is no longer supported.
3+
Users are expected to move to ``metadata.license_files`` and
4+
``options.install_requires`` (respectively).

newsfragments/4066.removal.4.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Passing ``config_settings`` to ``setuptools.build_meta`` with
2+
deprecated values for ``--global-option`` is no longer allowed.

newsfragments/4066.removal.5.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Removed deprecated ``namespace-packages`` from ``pyproject.toml``.
2+
Users are asked to use
3+
:doc:`implicit namespace packages <PyPUG:guides/packaging-namespace-packages>`
4+
(as defined in :pep:`420`).

newsfragments/4066.removal.6.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Added strict enforcement for ``project.dynamic`` in ``pyproject.toml``.
2+
This removes the transitional ability of users configuring certain parameters
3+
via ``setup.py`` without making the necessary changes to ``pyproject.toml``
4+
(as mandated by :pep:`612`).

setuptools/_normalization.py

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77
from typing import Union
88

99
from .extern import packaging
10-
from .warnings import SetuptoolsDeprecationWarning
1110

1211
_Path = Union[str, Path]
1312

1413
# https://packaging.python.org/en/latest/specifications/core-metadata/#name
1514
_VALID_NAME = re.compile(r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", re.I)
1615
_UNSAFE_NAME_CHARS = re.compile(r"[^A-Z0-9.]+", re.I)
1716
_NON_ALPHANUMERIC = re.compile(r"[^A-Z0-9]+", re.I)
17+
_PEP440_FALLBACK = re.compile(r"^v?(?P<safe>(?:[0-9]+!)?[0-9]+(?:\.[0-9]+)*)", re.I)
1818

1919

2020
def safe_identifier(name: str) -> str:
@@ -42,6 +42,8 @@ def safe_name(component: str) -> str:
4242

4343
def safe_version(version: str) -> str:
4444
"""Convert an arbitrary string into a valid version string.
45+
Can still raise an ``InvalidVersion`` exception.
46+
To avoid exceptions use ``best_effort_version``.
4547
>>> safe_version("1988 12 25")
4648
'1988.12.25'
4749
>>> safe_version("v0.2.1")
@@ -65,32 +67,35 @@ def safe_version(version: str) -> str:
6567

6668
def best_effort_version(version: str) -> str:
6769
"""Convert an arbitrary string into a version-like string.
70+
Fallback when ``safe_version`` is not safe enough.
6871
>>> best_effort_version("v0.2 beta")
6972
'0.2b0'
70-
71-
>>> import warnings
72-
>>> warnings.simplefilter("ignore", category=SetuptoolsDeprecationWarning)
7373
>>> best_effort_version("ubuntu lts")
74-
'ubuntu.lts'
74+
'0.dev0+sanitized.ubuntu.lts'
75+
>>> best_effort_version("0.23ubuntu1")
76+
'0.23.dev0+sanitized.ubuntu1'
77+
>>> best_effort_version("0.23-")
78+
'0.23.dev0+sanitized'
79+
>>> best_effort_version("0.-_")
80+
'0.dev0+sanitized'
81+
>>> best_effort_version("42.+?1")
82+
'42.dev0+sanitized.1'
7583
"""
76-
# See pkg_resources.safe_version
84+
# See pkg_resources._forgiving_version
7785
try:
7886
return safe_version(version)
7987
except packaging.version.InvalidVersion:
80-
SetuptoolsDeprecationWarning.emit(
81-
f"Invalid version: {version!r}.",
82-
f"""
83-
Version {version!r} is not valid according to PEP 440.
84-
85-
Please make sure to specify a valid version for your package.
86-
Also note that future releases of setuptools may halt the build process
87-
if an invalid version is given.
88-
""",
89-
see_url="https://peps.python.org/pep-0440/",
90-
due_date=(2023, 9, 26), # See setuptools/dist _validate_version
91-
)
9288
v = version.replace(' ', '.')
93-
return safe_name(v)
89+
match = _PEP440_FALLBACK.search(v)
90+
if match:
91+
safe = match["safe"]
92+
rest = v[len(safe) :]
93+
else:
94+
safe = "0"
95+
rest = version
96+
safe_rest = _NON_ALPHANUMERIC.sub(".", rest).strip(".")
97+
local = f"sanitized.{safe_rest}".strip(".")
98+
return safe_version(f"{safe}.dev0+{local}")
9499

95100

96101
def safe_extra(extra: str) -> str:

setuptools/build_meta.py

Lines changed: 3 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -185,11 +185,6 @@ def _get_config(self, key: str, config_settings: _ConfigSettings) -> List[str]:
185185
opts = cfg.get(key) or []
186186
return shlex.split(opts) if isinstance(opts, str) else opts
187187

188-
def _valid_global_options(self):
189-
"""Global options accepted by setuptools (e.g. quiet or verbose)."""
190-
options = (opt[:2] for opt in setuptools.dist.Distribution.global_options)
191-
return {flag for long_and_short in options for flag in long_and_short if flag}
192-
193188
def _global_args(self, config_settings: _ConfigSettings) -> Iterator[str]:
194189
"""
195190
Let the user specify ``verbose`` or ``quiet`` + escape hatch via
@@ -220,9 +215,7 @@ def _global_args(self, config_settings: _ConfigSettings) -> Iterator[str]:
220215
level = str(cfg.get("quiet") or cfg.get("--quiet") or "1")
221216
yield ("-v" if level.lower() in falsey else "-q")
222217

223-
valid = self._valid_global_options()
224-
args = self._get_config("--global-option", config_settings)
225-
yield from (arg for arg in args if arg.strip("-") in valid)
218+
yield from self._get_config("--global-option", config_settings)
226219

227220
def __dist_info_args(self, config_settings: _ConfigSettings) -> Iterator[str]:
228221
"""
@@ -284,33 +277,11 @@ def _arbitrary_args(self, config_settings: _ConfigSettings) -> Iterator[str]:
284277
['foo']
285278
>>> list(fn({'--build-option': 'foo bar'}))
286279
['foo', 'bar']
287-
>>> warnings.simplefilter('error', SetuptoolsDeprecationWarning)
288-
>>> list(fn({'--global-option': 'foo'})) # doctest: +IGNORE_EXCEPTION_DETAIL
289-
Traceback (most recent call last):
290-
SetuptoolsDeprecationWarning: ...arguments given via `--global-option`...
280+
>>> list(fn({'--global-option': 'foo'}))
281+
[]
291282
"""
292-
args = self._get_config("--global-option", config_settings)
293-
global_opts = self._valid_global_options()
294-
bad_args = []
295-
296-
for arg in args:
297-
if arg.strip("-") not in global_opts:
298-
bad_args.append(arg)
299-
yield arg
300-
301283
yield from self._get_config("--build-option", config_settings)
302284

303-
if bad_args:
304-
SetuptoolsDeprecationWarning.emit(
305-
"Incompatible `config_settings` passed to build backend.",
306-
f"""
307-
The arguments {bad_args!r} were given via `--global-option`.
308-
Please use `--build-option` instead,
309-
`--global-option` is reserved for flags like `--verbose` or `--quiet`.
310-
""",
311-
due_date=(2023, 9, 26), # Warning introduced in v64.0.1, 11/Aug/2022.
312-
)
313-
314285

315286
class _BuildMetaBackend(_ConfigSettingsTranslator):
316287
def _get_build_requires(self, config_settings, requirements):

setuptools/command/dist_info.py

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
from pathlib import Path
1313

1414
from .. import _normalization
15-
from ..warnings import SetuptoolsDeprecationWarning
1615

1716

1817
class dist_info(Command):
@@ -24,13 +23,6 @@ class dist_info(Command):
2423
description = "DO NOT CALL DIRECTLY, INTERNAL ONLY: create .dist-info directory"
2524

2625
user_options = [
27-
(
28-
'egg-base=',
29-
'e',
30-
"directory containing .egg-info directories"
31-
" (default: top of the source tree)"
32-
" DEPRECATED: use --output-dir.",
33-
),
3426
(
3527
'output-dir=',
3628
'o',
@@ -47,7 +39,6 @@ class dist_info(Command):
4739
negative_opt = {'no-date': 'tag-date'}
4840

4941
def initialize_options(self):
50-
self.egg_base = None
5142
self.output_dir = None
5243
self.name = None
5344
self.dist_info_dir = None
@@ -56,13 +47,6 @@ def initialize_options(self):
5647
self.keep_egg_info = False
5748

5849
def finalize_options(self):
59-
if self.egg_base:
60-
msg = "--egg-base is deprecated for dist_info command. Use --output-dir."
61-
SetuptoolsDeprecationWarning.emit(msg, due_date=(2023, 9, 26))
62-
# This command is internal to setuptools, therefore it should be safe
63-
# to remove the deprecated support soon.
64-
self.output_dir = self.egg_base or self.output_dir
65-
6650
dist = self.distribution
6751
project_dir = dist.src_root or os.curdir
6852
self.output_dir = Path(self.output_dir or project_dir)

setuptools/command/egg_info.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def name(self):
127127

128128
def tagged_version(self):
129129
tagged = self._maybe_tag(self.distribution.get_version())
130-
return _normalization.best_effort_version(tagged)
130+
return _normalization.safe_version(tagged)
131131

132132
def _maybe_tag(self, version):
133133
"""
@@ -148,7 +148,10 @@ def _already_tagged(self, version: str) -> bool:
148148
def _safe_tags(self) -> str:
149149
# To implement this we can rely on `safe_version` pretending to be version 0
150150
# followed by tags. Then we simply discard the starting 0 (fake version number)
151-
return _normalization.best_effort_version(f"0{self.vtags}")[1:]
151+
try:
152+
return _normalization.safe_version(f"0{self.vtags}")[1:]
153+
except packaging.version.InvalidVersion:
154+
return _normalization.safe_name(self.vtags.replace(' ', '.'))
152155

153156
def tags(self) -> str:
154157
version = ''

setuptools/config/_apply_pyprojecttoml.py

Lines changed: 49 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from collections.abc import Mapping
1313
from email.headerregistry import Address
1414
from functools import partial, reduce
15+
from inspect import cleandoc
1516
from itertools import chain
1617
from types import MappingProxyType
1718
from typing import (
@@ -28,7 +29,8 @@
2829
cast,
2930
)
3031

31-
from ..warnings import SetuptoolsWarning, SetuptoolsDeprecationWarning
32+
from ..errors import RemovedConfigError
33+
from ..warnings import SetuptoolsWarning
3234

3335
if TYPE_CHECKING:
3436
from setuptools._importlib import metadata # noqa
@@ -90,12 +92,13 @@ def _apply_tool_table(dist: "Distribution", config: dict, filename: _Path):
9092
for field, value in tool_table.items():
9193
norm_key = json_compatible_key(field)
9294

93-
if norm_key in TOOL_TABLE_DEPRECATIONS:
94-
suggestion, kwargs = TOOL_TABLE_DEPRECATIONS[norm_key]
95-
msg = f"The parameter `{norm_key}` is deprecated, {suggestion}"
96-
SetuptoolsDeprecationWarning.emit(
97-
"Deprecated config", msg, **kwargs # type: ignore
98-
)
95+
if norm_key in TOOL_TABLE_REMOVALS:
96+
suggestion = cleandoc(TOOL_TABLE_REMOVALS[norm_key])
97+
msg = f"""
98+
The parameter `tool.setuptools.{field}` was long deprecated
99+
and has been removed from `pyproject.toml`.
100+
"""
101+
raise RemovedConfigError("\n".join([cleandoc(msg), suggestion]))
99102

100103
norm_key = TOOL_TABLE_RENAMES.get(norm_key, norm_key)
101104
_set_config(dist, norm_key, value)
@@ -105,13 +108,13 @@ def _apply_tool_table(dist: "Distribution", config: dict, filename: _Path):
105108

106109
def _handle_missing_dynamic(dist: "Distribution", project_table: dict):
107110
"""Be temporarily forgiving with ``dynamic`` fields not listed in ``dynamic``"""
108-
# TODO: Set fields back to `None` once the feature stabilizes
109111
dynamic = set(project_table.get("dynamic", []))
110112
for field, getter in _PREVIOUSLY_DEFINED.items():
111113
if not (field in project_table or field in dynamic):
112114
value = getter(dist)
113115
if value:
114-
_WouldIgnoreField.emit(field=field, value=value)
116+
_MissingDynamic.emit(field=field, value=value)
117+
project_table[field] = _RESET_PREVIOUSLY_DEFINED.get(field)
115118

116119

117120
def json_compatible_key(key: str) -> str:
@@ -226,14 +229,18 @@ def _unify_entry_points(project_table: dict):
226229
renaming = {"scripts": "console_scripts", "gui_scripts": "gui_scripts"}
227230
for key, value in list(project.items()): # eager to allow modifications
228231
norm_key = json_compatible_key(key)
229-
if norm_key in renaming and value:
232+
if norm_key in renaming:
233+
# Don't skip even if value is empty (reason: reset missing `dynamic`)
230234
entry_points[renaming[norm_key]] = project.pop(key)
231235

232236
if entry_points:
233237
project["entry-points"] = {
234238
name: [f"{k} = {v}" for k, v in group.items()]
235239
for name, group in entry_points.items()
240+
if group # now we can skip empty groups
236241
}
242+
# Sometimes this will set `project["entry-points"] = {}`, and that is
243+
# intentional (for reseting configurations that are missing `dynamic`).
237244

238245

239246
def _copy_command_options(pyproject: dict, dist: "Distribution", filename: _Path):
@@ -353,11 +360,11 @@ def _acessor(obj):
353360
}
354361

355362
TOOL_TABLE_RENAMES = {"script_files": "scripts"}
356-
TOOL_TABLE_DEPRECATIONS = {
357-
"namespace_packages": (
358-
"consider using implicit namespaces instead (PEP 420).",
359-
{"due_date": (2023, 10, 30)}, # warning introduced in May 2022
360-
)
363+
TOOL_TABLE_REMOVALS = {
364+
"namespace_packages": """
365+
Please migrate to implicit native namespaces instead.
366+
See https://packaging.python.org/en/latest/guides/packaging-namespace-packages/.
367+
""",
361368
}
362369

363370
SETUPTOOLS_PATCHES = {
@@ -388,14 +395,27 @@ def _acessor(obj):
388395
}
389396

390397

391-
class _WouldIgnoreField(SetuptoolsDeprecationWarning):
392-
_SUMMARY = "`{field}` defined outside of `pyproject.toml` would be ignored."
398+
_RESET_PREVIOUSLY_DEFINED: dict = {
399+
# Fix improper setting: given in `setup.py`, but not listed in `dynamic`
400+
# dict: pyproject name => value to which reset
401+
"license": {},
402+
"authors": [],
403+
"maintainers": [],
404+
"keywords": [],
405+
"classifiers": [],
406+
"urls": {},
407+
"entry-points": {},
408+
"scripts": {},
409+
"gui-scripts": {},
410+
"dependencies": [],
411+
"optional-dependencies": [],
412+
}
393413

394-
_DETAILS = """
395-
##########################################################################
396-
# configuration would be ignored/result in error due to `pyproject.toml` #
397-
##########################################################################
398414

415+
class _MissingDynamic(SetuptoolsWarning):
416+
_SUMMARY = "`{field}` defined outside of `pyproject.toml` is ignored."
417+
418+
_DETAILS = """
399419
The following seems to be defined outside of `pyproject.toml`:
400420
401421
`{field} = {value!r}`
@@ -405,12 +425,14 @@ class _WouldIgnoreField(SetuptoolsDeprecationWarning):
405425
406426
https://packaging.python.org/en/latest/specifications/declaring-project-metadata/
407427
408-
For the time being, `setuptools` will still consider the given value (as a
409-
**transitional** measure), but please note that future releases of setuptools will
410-
follow strictly the standard.
411-
412-
To prevent this warning, you can list `{field}` under `dynamic` or alternatively
428+
To prevent this problem, you can list `{field}` under `dynamic` or alternatively
413429
remove the `[project]` table from your file and rely entirely on other means of
414430
configuration.
415431
"""
416-
_DUE_DATE = (2023, 10, 30) # Initially introduced in 27 May 2022
432+
# TODO: Consider removing this check in the future?
433+
# There is a trade-off here between improving "debug-ability" and the cost
434+
# of running/testing/maintaining these unnecessary checks...
435+
436+
@classmethod
437+
def details(cls, field: str, value: Any) -> str:
438+
return cls._DETAILS.format(field=field, value=value)

0 commit comments

Comments
 (0)