Skip to content

Commit

Permalink
Handle wave of overdue deprecations (#4066)
Browse files Browse the repository at this point in the history
  • Loading branch information
abravalheri committed Nov 20, 2023
2 parents bbce801 + a5a7505 commit 30b8a88
Show file tree
Hide file tree
Showing 22 changed files with 137 additions and 207 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions newsfragments/4066.removal.1.rst
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 4 additions & 0 deletions newsfragments/4066.removal.2.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
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.
4 changes: 4 additions & 0 deletions newsfragments/4066.removal.3.rst
Original file line number Diff line number Diff line change
@@ -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).
2 changes: 2 additions & 0 deletions newsfragments/4066.removal.4.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Passing ``config_settings`` to ``setuptools.build_meta`` with
deprecated values for ``--global-option`` is no longer allowed.
4 changes: 4 additions & 0 deletions newsfragments/4066.removal.5.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Removed deprecated ``namespace-packages`` from ``pyproject.toml``.
Users are asked to use
:doc:`implicit namespace packages <PyPUG:guides/packaging-namespace-packages>`
(as defined in :pep:`420`).
4 changes: 4 additions & 0 deletions newsfragments/4066.removal.6.rst
Original file line number Diff line number Diff line change
@@ -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`).
43 changes: 24 additions & 19 deletions setuptools/_normalization.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
from typing import Union

from .extern import packaging
from .warnings import SetuptoolsDeprecationWarning

_Path = Union[str, Path]

# https://packaging.python.org/en/latest/specifications/core-metadata/#name
_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<safe>(?:[0-9]+!)?[0-9]+(?:\.[0-9]+)*)", re.I)


def safe_identifier(name: str) -> str:
Expand Down Expand Up @@ -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")
Expand All @@ -65,32 +67,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:
Expand Down
35 changes: 3 additions & 32 deletions setuptools/build_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]:
"""
Expand Down Expand Up @@ -284,33 +277,11 @@ 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`...
>>> 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:
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.
)


class _BuildMetaBackend(_ConfigSettingsTranslator):
def _get_build_requires(self, config_settings, requirements):
Expand Down
16 changes: 0 additions & 16 deletions setuptools/command/dist_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from pathlib import Path

from .. import _normalization
from ..warnings import SetuptoolsDeprecationWarning


class dist_info(Command):
Expand All @@ -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',
Expand All @@ -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
Expand All @@ -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)
Expand Down
7 changes: 5 additions & 2 deletions setuptools/command/egg_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand All @@ -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 = ''
Expand Down
76 changes: 49 additions & 27 deletions setuptools/config/_apply_pyprojecttoml.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -28,7 +29,8 @@
cast,
)

from ..warnings import SetuptoolsWarning, SetuptoolsDeprecationWarning
from ..errors import RemovedConfigError
from ..warnings import SetuptoolsWarning

if TYPE_CHECKING:
from setuptools._importlib import metadata # noqa
Expand Down Expand Up @@ -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)
Expand All @@ -105,13 +108,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:
Expand Down Expand Up @@ -226,14 +229,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):
Expand Down Expand Up @@ -353,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 = {
Expand Down Expand Up @@ -388,14 +395,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}`
Expand All @@ -405,12 +425,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)
Loading

0 comments on commit 30b8a88

Please sign in to comment.