summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnderson Bravalheri <andersonbravalheri@gmail.com>2023-04-20 14:24:08 +0100
committerAnderson Bravalheri <andersonbravalheri@gmail.com>2023-04-20 14:24:08 +0100
commit73b5c62939cd2ef039da9d5f784083fa956ef4a1 (patch)
treec24848a7a4c705362a9c076f2c7b16aec8d752c7
parent000efbfae1e79d0a9fa9b16b55c4f5a2e90a64dd (diff)
parent54da8b6d69b7333424eff305218a10d9605a7e36 (diff)
downloadpython-setuptools-git-73b5c62939cd2ef039da9d5f784083fa956ef4a1.tar.gz
Overhaul for better visibility of warnings (#3849)
-rw-r--r--setuptools/__init__.py20
-rw-r--r--setuptools/_deprecation_warning.py7
-rw-r--r--setuptools/_importlib.py19
-rw-r--r--setuptools/_normalization.py31
-rw-r--r--setuptools/build_meta.py17
-rw-r--r--setuptools/command/bdist_rpm.py15
-rw-r--r--setuptools/command/build.py19
-rw-r--r--setuptools/command/build_py.py72
-rw-r--r--setuptools/command/dist_info.py7
-rw-r--r--setuptools/command/easy_install.py33
-rw-r--r--setuptools/command/editable_wheel.py131
-rw-r--r--setuptools/command/egg_info.py52
-rw-r--r--setuptools/command/install.py22
-rw-r--r--setuptools/command/upload_docs.py15
-rw-r--r--setuptools/config/__init__.py31
-rw-r--r--setuptools/config/_apply_pyprojecttoml.py38
-rw-r--r--setuptools/config/expand.py4
-rw-r--r--setuptools/config/pyprojecttoml.py42
-rw-r--r--setuptools/config/setupcfg.py93
-rw-r--r--setuptools/dist.py82
-rw-r--r--setuptools/installer.py22
-rw-r--r--setuptools/package_index.py8
-rw-r--r--setuptools/tests/config/test_apply_pyprojecttoml.py2
-rw-r--r--setuptools/tests/config/test_setupcfg.py24
-rw-r--r--setuptools/tests/test_warnings.py107
-rw-r--r--setuptools/warnings.py104
-rw-r--r--tox.ini1
27 files changed, 660 insertions, 358 deletions
diff --git a/setuptools/__init__.py b/setuptools/__init__.py
index 89f6f06e..35d7bd1c 100644
--- a/setuptools/__init__.py
+++ b/setuptools/__init__.py
@@ -3,7 +3,6 @@
import functools
import os
import re
-import warnings
import _distutils_hack.override # noqa: F401
@@ -11,7 +10,7 @@ import distutils.core
from distutils.errors import DistutilsOptionError
from distutils.util import convert_path as _convert_path
-from ._deprecation_warning import SetuptoolsDeprecationWarning
+from .warnings import SetuptoolsDeprecationWarning
import setuptools.version
from setuptools.extension import Extension
@@ -249,14 +248,17 @@ def findall(dir=os.curdir):
@functools.wraps(_convert_path)
def convert_path(pathname):
- from inspect import cleandoc
+ SetuptoolsDeprecationWarning.emit(
+ "Access to implementation detail",
+ """
+ The function `convert_path` is not provided by setuptools itself,
+ and therefore not part of the public API.
- msg = """
- The function `convert_path` is considered internal and not part of the public API.
- Its direct usage by 3rd-party packages is considered deprecated and the function
- may be removed in the future.
- """
- warnings.warn(cleandoc(msg), SetuptoolsDeprecationWarning)
+ Its direct usage by 3rd-party packages is considered improper and the function
+ may be removed in the future.
+ """,
+ due_date=(2023, 12, 13) # initial deprecation 2022-03-25, see #3201
+ )
return _convert_path(pathname)
diff --git a/setuptools/_deprecation_warning.py b/setuptools/_deprecation_warning.py
deleted file mode 100644
index 086b64dd..00000000
--- a/setuptools/_deprecation_warning.py
+++ /dev/null
@@ -1,7 +0,0 @@
-class SetuptoolsDeprecationWarning(Warning):
- """
- Base class for warning deprecations in ``setuptools``
-
- This class is not derived from ``DeprecationWarning``, and as such is
- visible by default.
- """
diff --git a/setuptools/_importlib.py b/setuptools/_importlib.py
index 819bf5d3..5ae94b47 100644
--- a/setuptools/_importlib.py
+++ b/setuptools/_importlib.py
@@ -13,14 +13,17 @@ def disable_importlib_metadata_finder(metadata):
except ImportError:
return
except AttributeError:
- import warnings
-
- msg = (
- "`importlib-metadata` version is incompatible with `setuptools`.\n"
- "This problem is likely to be solved by installing an updated version of "
- "`importlib-metadata`."
- )
- warnings.warn(msg) # Ensure a descriptive message is shown.
+ from .warnings import SetuptoolsWarning
+
+ SetuptoolsWarning.emit(
+ "Incompatibility problem.",
+ """
+ `importlib-metadata` version is incompatible with `setuptools`.
+ This problem is likely to be solved by installing an updated version of
+ `importlib-metadata`.
+ """,
+ see_url="https://github.com/python/importlib_metadata/issues/396"
+ ) # Ensure a descriptive message is shown.
raise # This exception can be suppressed by _distutils_hack
if importlib_metadata is metadata:
diff --git a/setuptools/_normalization.py b/setuptools/_normalization.py
index 8ba7c802..31899f7a 100644
--- a/setuptools/_normalization.py
+++ b/setuptools/_normalization.py
@@ -3,14 +3,11 @@ Helpers for normalization as expected in wheel/sdist/module file names
and core metadata
"""
import re
-import warnings
-from inspect import cleandoc
from pathlib import Path
from typing import Union
-from setuptools.extern import packaging
-
-from ._deprecation_warning import SetuptoolsDeprecationWarning
+from .extern import packaging
+from .warnings import SetuptoolsDeprecationWarning
_Path = Union[str, Path]
@@ -79,18 +76,18 @@ def best_effort_version(version: str) -> str:
try:
return safe_version(version)
except packaging.version.InvalidVersion:
- msg = f"""Invalid version: {version!r}.
- !!\n\n
- ###################
- # Invalid version #
- ###################
- {version!r} is not valid according to PEP 440.\n
- Please make sure 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.
- \n\n!!
- """
- warnings.warn(cleandoc(msg), SetuptoolsDeprecationWarning)
+ 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)
diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py
index 618a5e8f..ee8ef13f 100644
--- a/setuptools/build_meta.py
+++ b/setuptools/build_meta.py
@@ -43,7 +43,7 @@ import distutils
from . import errors
from ._path import same_path
from ._reqs import parse_strings
-from ._deprecation_warning import SetuptoolsDeprecationWarning
+from .warnings import SetuptoolsDeprecationWarning
from distutils.util import strtobool
@@ -299,12 +299,15 @@ class _ConfigSettingsTranslator:
yield from self._get_config("--build-option", config_settings)
if bad_args:
- msg = f"""
- The arguments {bad_args!r} were given via `--global-option`.
- Please use `--build-option` instead,
- `--global-option` is reserved to flags like `--verbose` or `--quiet`.
- """
- warnings.warn(msg, SetuptoolsDeprecationWarning)
+ 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):
diff --git a/setuptools/command/bdist_rpm.py b/setuptools/command/bdist_rpm.py
index 98bf5dea..047a6d08 100644
--- a/setuptools/command/bdist_rpm.py
+++ b/setuptools/command/bdist_rpm.py
@@ -1,7 +1,6 @@
import distutils.command.bdist_rpm as orig
-import warnings
-from setuptools import SetuptoolsDeprecationWarning
+from ..warnings import SetuptoolsDeprecationWarning
class bdist_rpm(orig.bdist_rpm):
@@ -14,10 +13,14 @@ class bdist_rpm(orig.bdist_rpm):
"""
def run(self):
- warnings.warn(
- "bdist_rpm is deprecated and will be removed in a future "
- "version. Use bdist_wheel (wheel packages) instead.",
- SetuptoolsDeprecationWarning,
+ SetuptoolsDeprecationWarning.emit(
+ "Deprecated command",
+ """
+ bdist_rpm is deprecated and will be removed in a future version.
+ Use bdist_wheel (wheel packages) instead.
+ """,
+ see_url="https://github.com/pypa/setuptools/issues/1988",
+ due_date=(2023, 10, 30) # Deprecation introduced in 22 Oct 2021.
)
# ensure distro name is up-to-date
diff --git a/setuptools/command/build.py b/setuptools/command/build.py
index fa3c99ef..0f1d688e 100644
--- a/setuptools/command/build.py
+++ b/setuptools/command/build.py
@@ -1,9 +1,8 @@
import sys
-import warnings
from typing import TYPE_CHECKING, List, Dict
from distutils.command.build import build as _build
-from setuptools import SetuptoolsDeprecationWarning
+from ..warnings import SetuptoolsDeprecationWarning
if sys.version_info >= (3, 8):
from typing import Protocol
@@ -23,12 +22,16 @@ class build(_build):
def get_sub_commands(self):
subcommands = {cmd[0] for cmd in _build.sub_commands}
if subcommands - _ORIGINAL_SUBCOMMANDS:
- msg = """
- It seems that you are using `distutils.command.build` to add
- new subcommands. Using `distutils` directly is considered deprecated,
- please use `setuptools.command.build`.
- """
- warnings.warn(msg, SetuptoolsDeprecationWarning)
+ SetuptoolsDeprecationWarning.emit(
+ "Direct usage of `distutils` commands",
+ """
+ It seems that you are using `distutils.command.build` to add
+ new subcommands. Using `distutils` directly is considered deprecated,
+ please use `setuptools.command.build`.
+ """,
+ due_date=(2023, 12, 13), # Warning introduced in 13 Jun 2022.
+ see_url="https://peps.python.org/pep-0632/",
+ )
self.sub_commands = _build.sub_commands
return super().get_sub_commands()
diff --git a/setuptools/command/build_py.py b/setuptools/command/build_py.py
index ec062742..f094496e 100644
--- a/setuptools/command/build_py.py
+++ b/setuptools/command/build_py.py
@@ -9,12 +9,11 @@ import io
import distutils.errors
import itertools
import stat
-import warnings
from pathlib import Path
from typing import Dict, Iterable, Iterator, List, Optional, Tuple
-from setuptools._deprecation_warning import SetuptoolsDeprecationWarning
-from setuptools.extern.more_itertools import unique_everseen
+from ..extern.more_itertools import unique_everseen
+from ..warnings import SetuptoolsDeprecationWarning
def make_writable(target):
@@ -325,28 +324,48 @@ def assert_relative(path):
class _IncludePackageDataAbuse:
"""Inform users that package or module is included as 'data file'"""
- MESSAGE = """\
- Installing {importable!r} as data is deprecated, please list it in `packages`.
- !!\n\n
- ############################
- # Package would be ignored #
- ############################
- Python recognizes {importable!r} as an importable package,
- but it is not listed in the `packages` configuration of setuptools.
-
- {importable!r} has been automatically added to the distribution only
- because it may contain data files, but this behavior is likely to change
- in future versions of setuptools (and therefore is considered deprecated).
-
- Please make sure that {importable!r} is included as a package by using
- the `packages` configuration field or the proper discovery methods
- (for example by using `find_namespace_packages(...)`/`find_namespace:`
- instead of `find_packages(...)`/`find:`).
-
- You can read more about "package discovery" and "data files" on setuptools
- documentation page.
- \n\n!!
- """
+ class _Warning(SetuptoolsDeprecationWarning):
+ _SUMMARY = """
+ Package {importable!r} is absent from the `packages` configuration.
+ """
+
+ _DETAILS = """
+ ############################
+ # Package would be ignored #
+ ############################
+ Python recognizes {importable!r} as an importable package[^1],
+ but it is absent from setuptools' `packages` configuration.
+
+ This leads to an ambiguous overall configuration. If you want to distribute this
+ package, please make sure that {importable!r} is explicitly added
+ to the `packages` configuration field.
+
+ Alternatively, you can also rely on setuptools' discovery methods
+ (for example by using `find_namespace_packages(...)`/`find_namespace:`
+ instead of `find_packages(...)`/`find:`).
+
+ You can read more about "package discovery" on setuptools documentation page:
+
+ - https://setuptools.pypa.io/en/latest/userguide/package_discovery.html
+
+ If you don't want {importable!r} to be distributed and are
+ already explicitly excluding {importable!r} via
+ `find_namespace_packages(...)/find_namespace` or `find_packages(...)/find`,
+ you can try to use `exclude_package_data`, or `include-package-data=False` in
+ combination with a more fine grained `package-data` configuration.
+
+ You can read more about "package data files" on setuptools documentation page:
+
+ - https://setuptools.pypa.io/en/latest/userguide/datafiles.html
+
+
+ [^1]: For Python, any directory (with suitable naming) can be imported,
+ even if it does not contain any `.py` files.
+ On the other hand, currently there is no concept of package data
+ directory, all directories are treated like packages.
+ """
+ # _DUE_DATE: still not defined as this is particularly controversial.
+ # Warning initially introduced in May 2022. See issue #3340 for discussion.
def __init__(self):
self._already_warned = set()
@@ -363,6 +382,5 @@ class _IncludePackageDataAbuse:
def warn(self, importable):
if importable not in self._already_warned:
- msg = textwrap.dedent(self.MESSAGE).format(importable=importable)
- warnings.warn(msg, SetuptoolsDeprecationWarning, stacklevel=2)
+ self._Warning.emit(importable=importable)
self._already_warned.add(importable)
diff --git a/setuptools/command/dist_info.py b/setuptools/command/dist_info.py
index 40fdfd0a..99d3976d 100644
--- a/setuptools/command/dist_info.py
+++ b/setuptools/command/dist_info.py
@@ -6,14 +6,13 @@ As defined in the wheel specification
import os
import shutil
import sys
-import warnings
from contextlib import contextmanager
from distutils import log
from distutils.core import Command
from pathlib import Path
from .. import _normalization
-from .._deprecation_warning import SetuptoolsDeprecationWarning
+from ..warnings import SetuptoolsDeprecationWarning
class dist_info(Command):
@@ -51,7 +50,9 @@ class dist_info(Command):
def finalize_options(self):
if self.egg_base:
msg = "--egg-base is deprecated for dist_info command. Use --output-dir."
- warnings.warn(msg, SetuptoolsDeprecationWarning)
+ 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
diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py
index 3185ee1d..29e424a8 100644
--- a/setuptools/command/easy_install.py
+++ b/setuptools/command/easy_install.py
@@ -44,8 +44,6 @@ import sysconfig
from sysconfig import get_path
-from setuptools import SetuptoolsDeprecationWarning
-
from setuptools import Command
from setuptools.sandbox import run_setup
from setuptools.command import setopt
@@ -54,6 +52,7 @@ from setuptools.package_index import (
PackageIndex, parse_requirement_arg, URL_SCHEME,
)
from setuptools.command import bdist_egg, egg_info
+from setuptools.warnings import SetuptoolsDeprecationWarning, SetuptoolsWarning
from setuptools.wheel import Wheel
from pkg_resources import (
normalize_path, resource_string,
@@ -142,11 +141,7 @@ class easy_install(Command):
create_index = PackageIndex
def initialize_options(self):
- warnings.warn(
- "easy_install command is deprecated. "
- "Use build and pip and other standards-based tools.",
- EasyInstallDeprecationWarning,
- )
+ EasyInstallDeprecationWarning.emit()
# the --user option seems to be an opt-in one,
# so the default should be False.
@@ -2095,7 +2090,8 @@ class ScriptWriter:
@classmethod
def get_script_args(cls, dist, executable=None, wininst=False):
# for backward compatibility
- warnings.warn("Use get_args", EasyInstallDeprecationWarning)
+ EasyInstallDeprecationWarning.emit("Use get_args", due_date=(2023, 6, 1))
+ # This is a direct API call, it should be safe to remove soon.
writer = (WindowsScriptWriter if wininst else ScriptWriter).best()
header = cls.get_script_header("", executable, wininst)
return writer.get_args(dist, header)
@@ -2103,8 +2099,8 @@ class ScriptWriter:
@classmethod
def get_script_header(cls, script_text, executable=None, wininst=False):
# for backward compatibility
- warnings.warn(
- "Use get_header", EasyInstallDeprecationWarning, stacklevel=2)
+ EasyInstallDeprecationWarning.emit("Use get_header", due_date=(2023, 6, 1))
+ # This is a direct API call, it should be safe to remove soon.
if wininst:
executable = "python.exe"
return cls.get_header(script_text, executable)
@@ -2139,7 +2135,8 @@ class ScriptWriter:
@classmethod
def get_writer(cls, force_windows):
# for backward compatibility
- warnings.warn("Use best", EasyInstallDeprecationWarning)
+ EasyInstallDeprecationWarning.emit("Use best", due_date=(2023, 6, 1))
+ # This is a direct API call, it should be safe to remove soon.
return WindowsScriptWriter.best() if force_windows else cls.best()
@classmethod
@@ -2171,7 +2168,8 @@ class WindowsScriptWriter(ScriptWriter):
@classmethod
def get_writer(cls):
# for backward compatibility
- warnings.warn("Use best", EasyInstallDeprecationWarning)
+ EasyInstallDeprecationWarning.emit("Use best", due_date=(2023, 6, 1))
+ # This is a direct API call, it should be safe to remove soon.
return cls.best()
@classmethod
@@ -2196,7 +2194,7 @@ class WindowsScriptWriter(ScriptWriter):
"{ext} not listed in PATHEXT; scripts will not be "
"recognized as executables."
).format(**locals())
- warnings.warn(msg, UserWarning)
+ SetuptoolsWarning.emit(msg)
old = ['.pya', '.py', '-script.py', '.pyc', '.pyo', '.pyw', '.exe']
old.remove(ext)
header = cls._adjust_header(type_, header)
@@ -2308,6 +2306,11 @@ def only_strs(values):
class EasyInstallDeprecationWarning(SetuptoolsDeprecationWarning):
+ _SUMMARY = "easy_install command is deprecated."
+ _DETAILS = """
+ Please avoid running ``setup.py`` and ``easy_install``.
+ Instead, use pypa/build, pypa/installer, pypa/build or
+ other standards-based tools.
"""
- Warning for EasyInstall deprecations, bypassing suppression.
- """
+ _SEE_URL = "https://github.com/pypa/setuptools/issues/917"
+ # _DUE_DATE not defined yet
diff --git a/setuptools/command/editable_wheel.py b/setuptools/command/editable_wheel.py
index 6fddf03d..ffcc2cc0 100644
--- a/setuptools/command/editable_wheel.py
+++ b/setuptools/command/editable_wheel.py
@@ -15,7 +15,6 @@ import os
import shutil
import sys
import traceback
-import warnings
from contextlib import suppress
from enum import Enum
from inspect import cleandoc
@@ -37,7 +36,6 @@ from typing import (
from .. import (
Command,
- SetuptoolsDeprecationWarning,
_normalization,
_path,
errors,
@@ -45,6 +43,11 @@ from .. import (
)
from ..discovery import find_package_path
from ..dist import Distribution
+from ..warnings import (
+ InformationOnly,
+ SetuptoolsDeprecationWarning,
+ SetuptoolsWarning,
+)
from .build_py import build_py as build_py_cls
if TYPE_CHECKING:
@@ -84,16 +87,21 @@ class _EditableMode(Enum):
raise errors.OptionError(f"Invalid editable mode: {mode!r}. Try: 'strict'.")
if _mode == "COMPAT":
- msg = """
- The 'compat' editable mode is transitional and will be removed
- in future versions of `setuptools`.
- Please adapt your code accordingly to use either the 'strict' or the
- 'lenient' modes.
-
- For more information, please check:
- https://setuptools.pypa.io/en/latest/userguide/development_mode.html
- """
- warnings.warn(msg, SetuptoolsDeprecationWarning)
+ SetuptoolsDeprecationWarning.emit(
+ "Compat editable installs",
+ """
+ The 'compat' editable mode is transitional and will be removed
+ in future versions of `setuptools`.
+ Please adapt your code accordingly to use either the 'strict' or the
+ 'lenient' modes.
+ """,
+ see_docs="userguide/development_mode.html",
+ # TODO: define due_date
+ # There is a series of shortcomings with the available editable install
+ # methods, and they are very controversial. This is something that still
+ # needs work.
+ # Moreover, `pip` is still hiding this warning, so users are not aware.
+ )
return _EditableMode[_mode]
@@ -148,7 +156,7 @@ class editable_wheel(Command):
except Exception:
traceback.print_exc()
project = self.distribution.name or self.distribution.get_name()
- _DebuggingTips.warn(project)
+ _DebuggingTips.emit(project=project)
raise
def _ensure_dist_info(self):
@@ -289,21 +297,29 @@ class editable_wheel(Command):
try:
return self.run_command(cmd_name)
except Exception:
- msg = f"""{traceback.format_exc()}\n
- If you are seeing this warning it is very likely that a setuptools
- plugin or customization overrides the `{cmd_name}` command, without
- taking into consideration how editable installs run build steps
- starting from v64.0.0.
-
- Plugin authors and developers relying on custom build steps are encouraged
- to update their `{cmd_name}` implementation considering the information in
- https://setuptools.pypa.io/en/latest/userguide/extension.html
- about editable installs.
-
- For the time being `setuptools` will silence this error and ignore
- the faulty command, but this behaviour will change in future versions.\n
- """
- warnings.warn(msg, SetuptoolsDeprecationWarning, stacklevel=2)
+ SetuptoolsDeprecationWarning.emit(
+ "Customization incompatible with editable install",
+ f"""
+ {traceback.format_exc()}
+
+ If you are seeing this warning it is very likely that a setuptools
+ plugin or customization overrides the `{cmd_name}` command, without
+ taking into consideration how editable installs run build steps
+ starting from setuptools v64.0.0.
+
+ Plugin authors and developers relying on custom build steps are
+ encouraged to update their `{cmd_name}` implementation considering the
+ information about editable installs in
+ https://setuptools.pypa.io/en/latest/userguide/extension.html.
+
+ For the time being `setuptools` will silence this error and ignore
+ the faulty command, but this behaviour will change in future versions.
+ """,
+ # TODO: define due_date
+ # There is a series of shortcomings with the available editable install
+ # methods, and they are very controversial. This is something that still
+ # needs work.
+ )
def _create_wheel_file(self, bdist_wheel):
from wheel.wheelfile import WheelFile
@@ -468,7 +484,7 @@ class _LinkTree(_StaticPth):
Please be careful to not remove this directory, otherwise you might not be able
to import/use your package.
"""
- warnings.warn(msg, InformationOnly)
+ InformationOnly.emit("Editable installation.", msg)
class _TopLevelFinder:
@@ -505,7 +521,7 @@ class _TopLevelFinder:
Please be careful with folders in your working directory with the same
name as your package as they may take precedence during imports.
"""
- warnings.warn(msg, InformationOnly)
+ InformationOnly.emit("Editable installation.", msg)
def _can_symlink_files(base_dir: Path) -> bool:
@@ -811,46 +827,31 @@ def _finder_template(
return _FINDER_TEMPLATE.format(name=name, mapping=mapping, namespaces=namespaces)
-class InformationOnly(UserWarning):
- """Currently there is no clear way of displaying messages to the users
- that use the setuptools backend directly via ``pip``.
- The only thing that might work is a warning, although it is not the
- most appropriate tool for the job...
- """
-
-
class LinksNotSupported(errors.FileError):
"""File system does not seem to support either symlinks or hard links."""
-class _DebuggingTips(InformationOnly):
- @classmethod
- def warn(cls, project: str):
- msg = f"""An error happened while installing {project!r} in editable mode.
-
- ************************************************************************
- The following steps are recommended to help debugging this problem:
+class _DebuggingTips(SetuptoolsWarning):
+ _SUMMARY = "Problem in editable installation."
+ _DETAILS = """
+ An error happened while installing `{project}` in editable mode.
- - Try to install the project normally, without using the editable mode.
- Does the error still persists?
- (If it does, try fixing the problem before attempting the editable mode).
- - If you are using binary extensions, make sure you have all OS-level
- dependencies installed (e.g. compilers, toolchains, binary libraries, ...).
- - Try the latest version of setuptools (maybe the error was already fixed).
- - If you (or your project dependencies) are using any setuptools extension
- or customization, make sure they support the editable mode.
+ The following steps are recommended to help debug this problem:
- After following the steps above, if the problem still persist and
- you think this is related to how setuptools handles editable installations,
- please submit a reproducible example
- (see https://stackoverflow.com/help/minimal-reproducible-example) to:
+ - Try to install the project normally, without using the editable mode.
+ Does the error still persist?
+ (If it does, try fixing the problem before attempting the editable mode).
+ - If you are using binary extensions, make sure you have all OS-level
+ dependencies installed (e.g. compilers, toolchains, binary libraries, ...).
+ - Try the latest version of setuptools (maybe the error was already fixed).
+ - If you (or your project dependencies) are using any setuptools extension
+ or customization, make sure they support the editable mode.
- https://github.com/pypa/setuptools/issues
+ After following the steps above, if the problem still persists and
+ you think this is related to how setuptools handles editable installations,
+ please submit a reproducible example
+ (see https://stackoverflow.com/help/minimal-reproducible-example) to:
- More information about editable installs can be found in the docs:
-
- https://setuptools.pypa.io/en/latest/userguide/development_mode.html
- ************************************************************************
- """
- # We cannot use `add_notes` since pip hides PEP 678 notes
- warnings.warn(msg, cls, stacklevel=2)
+ https://github.com/pypa/setuptools/issues
+ """
+ _SEE_DOCS = "userguide/development_mode.html"
diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py
index e40df9bb..f5163ae7 100644
--- a/setuptools/command/egg_info.py
+++ b/setuptools/command/egg_info.py
@@ -13,7 +13,6 @@ import os
import re
import sys
import io
-import warnings
import time
import collections
@@ -30,7 +29,7 @@ from setuptools.glob import glob
from setuptools.extern import packaging
from setuptools.extern.jaraco.text import yield_lines
-from setuptools import SetuptoolsDeprecationWarning
+from ..warnings import SetuptoolsDeprecationWarning
PY_MAJOR = '{}.{}'.format(*sys.version_info)
@@ -331,12 +330,16 @@ class egg_info(InfoCommon, Command):
if self.egg_base != os.curdir:
bei = os.path.join(self.egg_base, bei)
if os.path.exists(bei):
- log.warn(
- "-" * 78 + '\n'
- "Note: Your current .egg-info directory has a '-' in its name;"
- '\nthis will not work correctly with "setup.py develop".\n\n'
- 'Please rename %s to %s to correct this problem.\n' + '-' * 78,
- bei, self.egg_info
+ EggInfoDeprecationWarning.emit(
+ "Invalid egg-info directory name.",
+ f"""
+ Your current .egg-info directory has a '-' in its name;
+ this will not work correctly with setuptools commands.
+
+ Please rename {bei!r} to {self.egg_info!r} to correct this problem.
+ """,
+ due_date=(2023, 6, 1),
+ # Old warning, introduced in 2005, might be safe to remove soon
)
self.broken_egg_info = self.egg_info
self.egg_info = bei # make it work for now
@@ -658,11 +661,14 @@ class manifest_maker(sdist):
if hasattr(build_py, 'get_data_files_without_manifest'):
return build_py.get_data_files_without_manifest()
- warnings.warn(
- "Custom 'build_py' does not implement "
- "'get_data_files_without_manifest'.\nPlease extend command classes"
- " from setuptools instead of distutils.",
- SetuptoolsDeprecationWarning
+ SetuptoolsDeprecationWarning.emit(
+ "`build_py` command does not inherit from setuptools' `build_py`.",
+ """
+ Custom 'build_py' does not implement 'get_data_files_without_manifest'.
+ Please extend command classes from setuptools instead of distutils.
+ """,
+ see_url="https://peps.python.org/pep-0632/",
+ # due_date not defined yet, old projects might still do it?
)
return build_py.get_data_files()
@@ -701,9 +707,15 @@ def write_pkg_info(cmd, basename, filename):
def warn_depends_obsolete(cmd, basename, filename):
if os.path.exists(filename):
- log.warn(
- "WARNING: 'depends.txt' is not used by setuptools 0.6!\n"
- "Use the install_requires/extras_require setup() args instead."
+ EggInfoDeprecationWarning.emit(
+ "Deprecated config.",
+ """
+ 'depends.txt' is not used by setuptools >= 0.6!
+ Configure your dependencies via `setup.cfg` or `pyproject.toml` instead.
+ """,
+ see_docs="userguide/declarative_config.html",
+ due_date=(2023, 6, 1),
+ # Old warning, introduced in 2005, it might be safe to remove soon.
)
@@ -766,8 +778,12 @@ def get_pkg_info_revision():
Get a -r### off of PKG-INFO Version in case this is an sdist of
a subversion revision.
"""
- warnings.warn(
- "get_pkg_info_revision is deprecated.", EggInfoDeprecationWarning)
+ EggInfoDeprecationWarning.emit(
+ "Deprecated API call",
+ "get_pkg_info_revision is deprecated.",
+ due_date=(2023, 6, 1),
+ # Warning introduced in 11 Dec 2015, should be safe to remove
+ )
if os.path.exists('PKG-INFO'):
with io.open('PKG-INFO') as f:
for line in f:
diff --git a/setuptools/command/install.py b/setuptools/command/install.py
index 55fdb124..dec4e320 100644
--- a/setuptools/command/install.py
+++ b/setuptools/command/install.py
@@ -1,11 +1,11 @@
from distutils.errors import DistutilsArgError
import inspect
import glob
-import warnings
import platform
import distutils.command.install as orig
import setuptools
+from ..warnings import SetuptoolsDeprecationWarning, SetuptoolsWarning
# Prior to numpy 1.9, NumPy relies on the '_install' name, so provide it for
# now. See https://github.com/pypa/setuptools/issues/199/
@@ -30,11 +30,17 @@ class install(orig.install):
_nc = dict(new_commands)
def initialize_options(self):
-
- warnings.warn(
- "setup.py install is deprecated. "
- "Use build and pip and other standards-based tools.",
- setuptools.SetuptoolsDeprecationWarning,
+ SetuptoolsDeprecationWarning.emit(
+ "setup.py install is deprecated.",
+ """
+ Please avoid running ``setup.py`` directly.
+ Instead, use pypa/build, pypa/installer, pypa/build or
+ other standards-based tools.
+ """,
+ see_url="https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html",
+ # TODO: Document how to bootstrap setuptools without install
+ # (e.g. by unziping the wheel file)
+ # and then add a due_date to this warning.
)
orig.install.initialize_options(self)
@@ -86,10 +92,10 @@ class install(orig.install):
"""
if run_frame is None:
msg = "Call stack not available. bdist_* commands may fail."
- warnings.warn(msg)
+ SetuptoolsWarning.emit(msg)
if platform.python_implementation() == 'IronPython':
msg = "For best results, pass -X:Frames to enable call stack."
- warnings.warn(msg)
+ SetuptoolsWarning.emit(msg)
return True
frames = inspect.getouterframes(run_frame)
diff --git a/setuptools/command/upload_docs.py b/setuptools/command/upload_docs.py
index 63eb28c7..077c9d2f 100644
--- a/setuptools/command/upload_docs.py
+++ b/setuptools/command/upload_docs.py
@@ -16,10 +16,9 @@ import itertools
import functools
import http.client
import urllib.parse
-import warnings
from .._importlib import metadata
-from .. import SetuptoolsDeprecationWarning
+from ..warnings import SetuptoolsDeprecationWarning
from .upload import upload
@@ -91,10 +90,14 @@ class upload_docs(upload):
zip_file.close()
def run(self):
- warnings.warn(
- "upload_docs is deprecated and will be removed in a future "
- "version. Use tools like httpie or curl instead.",
- SetuptoolsDeprecationWarning,
+ SetuptoolsDeprecationWarning.emit(
+ "Deprecated command",
+ """
+ upload_docs is deprecated and will be removed in a future version.
+ Instead, use tools like devpi and Read the Docs; or lower level tools like
+ httpie and curl to interact directly with your hosting service API.
+ """,
+ due_date=(2023, 9, 26), # warning introduced in 27 Jul 2022
)
# Run sub commands
diff --git a/setuptools/config/__init__.py b/setuptools/config/__init__.py
index 1a5153ad..ffea3944 100644
--- a/setuptools/config/__init__.py
+++ b/setuptools/config/__init__.py
@@ -1,12 +1,10 @@
"""For backward compatibility, expose main functions from
``setuptools.config.setupcfg``
"""
-import warnings
from functools import wraps
-from textwrap import dedent
from typing import Callable, TypeVar, cast
-from .._deprecation_warning import SetuptoolsDeprecationWarning
+from ..warnings import SetuptoolsDeprecationWarning
from . import setupcfg
Fn = TypeVar("Fn", bound=Callable)
@@ -17,15 +15,24 @@ __all__ = ('parse_configuration', 'read_configuration')
def _deprecation_notice(fn: Fn) -> Fn:
@wraps(fn)
def _wrapper(*args, **kwargs):
- msg = f"""\
- As setuptools moves its configuration towards `pyproject.toml`,
- `{__name__}.{fn.__name__}` became deprecated.
-
- For the time being, you can use the `{setupcfg.__name__}` module
- to access a backward compatible API, but this module is provisional
- and might be removed in the future.
- """
- warnings.warn(dedent(msg), SetuptoolsDeprecationWarning, stacklevel=2)
+ SetuptoolsDeprecationWarning.emit(
+ "Deprecated API usage.",
+ f"""
+ As setuptools moves its configuration towards `pyproject.toml`,
+ `{__name__}.{fn.__name__}` became deprecated.
+
+ For the time being, you can use the `{setupcfg.__name__}` module
+ to access a backward compatible API, but this module is provisional
+ and might be removed in the future.
+
+ To read project metadata, consider using
+ ``build.util.project_wheel_metadata`` (https://pypi.org/project/build/).
+ For simple scenarios, you can also try parsing the file directly
+ with the help of ``configparser``.
+ """,
+ # due_date not defined yet, because the community still heavily relies on it
+ # Warning introduced in 24 Mar 2022
+ )
return fn(*args, **kwargs)
return cast(Fn, _wrapper)
diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py
index a2b44365..3091e3b5 100644
--- a/setuptools/config/_apply_pyprojecttoml.py
+++ b/setuptools/config/_apply_pyprojecttoml.py
@@ -9,7 +9,6 @@ need to be processed before being applied.
"""
import logging
import os
-import warnings
from collections.abc import Mapping
from email.headerregistry import Address
from functools import partial, reduce
@@ -18,7 +17,7 @@ from types import MappingProxyType
from typing import (TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Tuple,
Type, Union, cast)
-from setuptools._deprecation_warning import SetuptoolsDeprecationWarning
+from ..warnings import SetuptoolsWarning, SetuptoolsDeprecationWarning
if TYPE_CHECKING:
from setuptools._importlib import metadata # noqa
@@ -81,9 +80,11 @@ def _apply_tool_table(dist: "Distribution", config: dict, filename: _Path):
norm_key = json_compatible_key(field)
if norm_key in TOOL_TABLE_DEPRECATIONS:
- suggestion = TOOL_TABLE_DEPRECATIONS[norm_key]
+ suggestion, kwargs = TOOL_TABLE_DEPRECATIONS[norm_key]
msg = f"The parameter `{norm_key}` is deprecated, {suggestion}"
- warnings.warn(msg, SetuptoolsDeprecationWarning)
+ SetuptoolsDeprecationWarning.emit(
+ "Deprecated config", msg, **kwargs # type: ignore
+ )
norm_key = TOOL_TABLE_RENAMES.get(norm_key, norm_key)
_set_config(dist, norm_key, value)
@@ -99,8 +100,7 @@ def _handle_missing_dynamic(dist: "Distribution", project_table: dict):
if not (field in project_table or field in dynamic):
value = getter(dist)
if value:
- msg = _WouldIgnoreField.message(field, value)
- warnings.warn(msg, _WouldIgnoreField)
+ _WouldIgnoreField.emit(field=field, value=value)
def json_compatible_key(key: str) -> str:
@@ -200,7 +200,7 @@ def _python_requires(dist: "Distribution", val: dict, _root_dir):
def _dependencies(dist: "Distribution", val: list, _root_dir):
if getattr(dist, "install_requires", []):
msg = "`install_requires` overwritten in `pyproject.toml` (dependencies)"
- warnings.warn(msg)
+ SetuptoolsWarning.emit(msg)
_set_config(dist, "install_requires", val)
@@ -331,7 +331,10 @@ PYPROJECT_CORRESPONDENCE: Dict[str, _Correspondence] = {
TOOL_TABLE_RENAMES = {"script_files": "scripts"}
TOOL_TABLE_DEPRECATIONS = {
- "namespace_packages": "consider using implicit namespaces instead (PEP 420)."
+ "namespace_packages": (
+ "consider using implicit namespaces instead (PEP 420).",
+ {"due_date": (2023, 10, 30)}, # warning introduced in May 2022
+ )
}
SETUPTOOLS_PATCHES = {"long_description_content_type", "project_urls",
@@ -355,12 +358,10 @@ _PREVIOUSLY_DEFINED = {
}
-class _WouldIgnoreField(UserWarning):
- """Inform users that ``pyproject.toml`` would overwrite previous metadata."""
+class _WouldIgnoreField(SetuptoolsDeprecationWarning):
+ _SUMMARY = "`{field}` defined outside of `pyproject.toml` would be ignored."
- MESSAGE = """\
- {field!r} defined outside of `pyproject.toml` would be ignored.
- !!\n\n
+ _DETAILS = """
##########################################################################
# configuration would be ignored/result in error due to `pyproject.toml` #
##########################################################################
@@ -370,7 +371,7 @@ class _WouldIgnoreField(UserWarning):
`{field} = {value!r}`
According to the spec (see the link below), however, setuptools CANNOT
- consider this value unless {field!r} is listed as `dynamic`.
+ consider this value unless `{field}` is listed as `dynamic`.
https://packaging.python.org/en/latest/specifications/declaring-project-metadata/
@@ -378,13 +379,8 @@ class _WouldIgnoreField(UserWarning):
**transitional** measure), but please note that future releases of setuptools will
follow strictly the standard.
- To prevent this warning, you can list {field!r} under `dynamic` or alternatively
+ To prevent this warning, you can list `{field}` under `dynamic` or alternatively
remove the `[project]` table from your file and rely entirely on other means of
configuration.
- \n\n!!
"""
-
- @classmethod
- def message(cls, field, value):
- from inspect import cleandoc
- return cleandoc(cls.MESSAGE.format(field=field, value=value))
+ _DUE_DATE = (2023, 10, 30) # Initially introduced in 27 May 2022
diff --git a/setuptools/config/expand.py b/setuptools/config/expand.py
index c8db2c4b..30988843 100644
--- a/setuptools/config/expand.py
+++ b/setuptools/config/expand.py
@@ -23,7 +23,6 @@ import io
import os
import pathlib
import sys
-import warnings
from glob import iglob
from configparser import ConfigParser
from importlib.machinery import ModuleSpec
@@ -48,6 +47,7 @@ from types import ModuleType
from distutils.errors import DistutilsOptionError
from .._path import same_path as _same_path
+from ..warnings import SetuptoolsWarning
if TYPE_CHECKING:
from setuptools.dist import Distribution # noqa
@@ -141,7 +141,7 @@ def _filter_existing_files(filepaths: Iterable[_Path]) -> Iterator[_Path]:
if os.path.isfile(path):
yield path
else:
- warnings.warn(f"File {path!r} cannot be found")
+ SetuptoolsWarning.emit(f"File {path!r} cannot be found")
def _read_file(filepath: Union[bytes, _Path]) -> str:
diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index 9ce55022..8d1dcaed 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -2,19 +2,23 @@
Load setuptools configuration from ``pyproject.toml`` files.
**PRIVATE MODULE**: API reserved for setuptools internal usage only.
+
+To read project metadata, consider using
+``build.util.project_wheel_metadata`` (https://pypi.org/project/build/).
+For simple scenarios, you can also try parsing the file directly
+with the help of ``tomllib`` or ``tomli``.
"""
import logging
import os
-import warnings
from contextlib import contextmanager
from functools import partial
-from typing import TYPE_CHECKING, Callable, Dict, Optional, Mapping, Set, Union
-
-from setuptools.errors import FileError, OptionError
+from typing import TYPE_CHECKING, Callable, Dict, Mapping, Optional, Set, Union
+from ..errors import FileError, OptionError
+from ..warnings import SetuptoolsWarning
from . import expand as _expand
-from ._apply_pyprojecttoml import apply as _apply
from ._apply_pyprojecttoml import _PREVIOUSLY_DEFINED, _WouldIgnoreField
+from ._apply_pyprojecttoml import apply as _apply
if TYPE_CHECKING:
from setuptools.dist import Distribution # noqa
@@ -104,8 +108,7 @@ def read_configuration(
if setuptools_table:
# TODO: Remove the following once the feature stabilizes:
- msg = "Support for `[tool.setuptools]` in `pyproject.toml` is still *beta*."
- warnings.warn(msg, _BetaConfiguration)
+ _BetaConfiguration.emit()
# There is an overall sense in the community that making include_package_data=True
# the default would be an improvement.
@@ -166,7 +169,7 @@ def _skip_bad_config(
# It seems that the docs in cibuildtool has been inadvertently encouraging users
# to create `pyproject.toml` files that are not compliant with the standards.
# Let's be forgiving for the time being.
- warnings.warn(_InvalidFile.message(), _InvalidFile, stacklevel=2)
+ _InvalidFile.emit()
return True
return False
@@ -369,8 +372,7 @@ class _ConfigExpander:
if group in groups:
value = groups.pop(group)
if field not in self.dynamic:
- msg = _WouldIgnoreField.message(field, value)
- warnings.warn(msg, _WouldIgnoreField)
+ _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
expanded[field] = value
@@ -472,13 +474,13 @@ class _EnsurePackagesDiscovered(_expand.EnsurePackagesDiscovered):
return super().__exit__(exc_type, exc_value, traceback)
-class _BetaConfiguration(UserWarning):
- """Explicitly inform users that some `pyproject.toml` configuration is *beta*"""
+class _BetaConfiguration(SetuptoolsWarning):
+ _SUMMARY = "Support for `[tool.setuptools]` in `pyproject.toml` is still *beta*."
-class _InvalidFile(UserWarning):
- """The given `pyproject.toml` file is invalid and would be ignored.
- !!\n\n
+class _InvalidFile(SetuptoolsWarning):
+ _SUMMARY = "The given `pyproject.toml` file is invalid and would be ignored."
+ _DETAILS = """
############################
# Invalid `pyproject.toml` #
############################
@@ -488,11 +490,7 @@ class _InvalidFile(UserWarning):
if an invalid file is given.
To prevent setuptools from considering `pyproject.toml` please
- DO NOT include the `[project]` or `[tool.setuptools]` tables in your file.
- \n\n!!
+ DO NOT include both `[project]` or `[tool.setuptools]` tables in your file.
"""
-
- @classmethod
- def message(cls):
- from inspect import cleandoc
- return cleandoc(cls.__doc__)
+ _DUE_DATE = (2023, 6, 1) # warning introduced in 2022-03-26
+ _SEE_DOCS = "userguide/pyproject_config.html"
diff --git a/setuptools/config/setupcfg.py b/setuptools/config/setupcfg.py
index 7b7d57e6..050e5385 100644
--- a/setuptools/config/setupcfg.py
+++ b/setuptools/config/setupcfg.py
@@ -2,12 +2,15 @@
Load setuptools configuration from ``setup.cfg`` files.
**API will be made private in the future**
-"""
-import os
+To read project metadata, consider using
+``build.util.project_wheel_metadata`` (https://pypi.org/project/build/).
+For simple scenarios, you can also try parsing the file directly
+with the help of ``configparser``.
+"""
import contextlib
import functools
-import warnings
+import os
from collections import defaultdict
from functools import partial
from functools import wraps
@@ -26,19 +29,19 @@ from typing import (
Union,
)
-from distutils.errors import DistutilsOptionError, DistutilsFileError
-from setuptools.extern.packaging.requirements import Requirement, InvalidRequirement
-from setuptools.extern.packaging.markers import default_environment as marker_env
-from setuptools.extern.packaging.version import Version, InvalidVersion
-from setuptools.extern.packaging.specifiers import SpecifierSet
-from setuptools._deprecation_warning import SetuptoolsDeprecationWarning
-
+from ..errors import FileError, OptionError
+from ..extern.packaging.markers import default_environment as marker_env
+from ..extern.packaging.requirements import InvalidRequirement, Requirement
+from ..extern.packaging.specifiers import SpecifierSet
+from ..extern.packaging.version import InvalidVersion, Version
+from ..warnings import SetuptoolsDeprecationWarning
from . import expand
if TYPE_CHECKING:
- from setuptools.dist import Distribution # noqa
from distutils.dist import DistributionMetadata # noqa
+ from setuptools.dist import Distribution # noqa
+
_Path = Union[str, os.PathLike]
SingleCommandOptions = Dict["str", Tuple["str", Any]]
"""Dict that associate the name of the options of a particular command to a
@@ -97,7 +100,7 @@ def _apply(
filepath = os.path.abspath(filepath)
if not os.path.isfile(filepath):
- raise DistutilsFileError('Configuration file %s does not exist.' % filepath)
+ raise FileError(f'Configuration file {filepath} does not exist.')
current_directory = os.getcwd()
os.chdir(os.path.dirname(filepath))
@@ -121,7 +124,7 @@ def _get_option(target_obj: Target, key: str):
the target object, either through a get_{key} method or
from an attribute directly.
"""
- getter_name = 'get_{key}'.format(**locals())
+ getter_name = f'get_{key}'
by_attribute = functools.partial(getattr, target_obj, key)
getter = getattr(target_obj, getter_name, by_attribute)
return getter()
@@ -212,19 +215,14 @@ def _warn_accidental_env_marker_misconfig(label: str, orig_value: str, parsed: l
return
markers = marker_env().keys()
- msg = (
- f"One of the parsed requirements in `{label}` "
- f"looks like a valid environment marker: '{parsed[1]}'\n"
- "Make sure that the config is correct and check "
- "https://setuptools.pypa.io/en/latest/userguide/declarative_config.html#opt-2" # noqa: E501
- )
try:
req = Requirement(parsed[1])
if req.name in markers:
- warnings.warn(msg)
+ _AmbiguousMarker.emit(field=label, req=parsed[1])
except InvalidRequirement as ex:
if any(parsed[1].startswith(marker) for marker in markers):
+ msg = _AmbiguousMarker.message(field=label, req=parsed[1])
raise InvalidRequirement(msg) from ex
@@ -334,9 +332,7 @@ class ConfigHandler(Generic[Target]):
for line in cls._parse_list(value):
key, sep, val = line.partition(separator)
if sep != separator:
- raise DistutilsOptionError(
- 'Unable to parse option value to dict: %s' % value
- )
+ raise OptionError(f"Unable to parse option value to dict: {value}")
result[key.strip()] = val.strip()
return result
@@ -496,24 +492,24 @@ class ConfigHandler(Generic[Target]):
)
if section_parser_method is None:
- raise DistutilsOptionError(
- 'Unsupported distribution option section: [%s.%s]'
- % (self.section_prefix, section_name)
+ raise OptionError(
+ "Unsupported distribution option section: "
+ f"[{self.section_prefix}.{section_name}]"
)
section_parser_method(section_options)
- def _deprecated_config_handler(self, func, msg, warning_class):
+ def _deprecated_config_handler(self, func, msg, **kw):
"""this function will wrap around parameters that are deprecated
:param msg: deprecation message
- :param warning_class: class of warning exception to be raised
:param func: function to be wrapped around
"""
@wraps(func)
def config_handler(*args, **kwargs):
- warnings.warn(msg, warning_class, stacklevel=2)
+ kw.setdefault("stacklevel", 2)
+ _DeprecatedConfig.emit("Deprecated config in `setup.cfg`", msg, **kw)
return func(*args, **kwargs)
return config_handler
@@ -564,7 +560,8 @@ class ConfigMetadataHandler(ConfigHandler["DistributionMetadata"]):
parse_list,
"The requires parameter is deprecated, please use "
"install_requires for runtime dependencies.",
- SetuptoolsDeprecationWarning,
+ due_date=(2023, 10, 30),
+ # Warning introduced in 27 Oct 2018
),
'obsoletes': parse_list,
'classifiers': self._get_parser_compound(parse_file, parse_list),
@@ -573,7 +570,8 @@ class ConfigMetadataHandler(ConfigHandler["DistributionMetadata"]):
exclude_files_parser('license_file'),
"The license_file parameter is deprecated, "
"use license_files instead.",
- SetuptoolsDeprecationWarning,
+ due_date=(2023, 10, 30),
+ # Warning introduced in 23 May 2021
),
'license_files': parse_list,
'description': parse_file,
@@ -598,11 +596,10 @@ class ConfigMetadataHandler(ConfigHandler["DistributionMetadata"]):
try:
Version(version)
except InvalidVersion:
- tmpl = (
- 'Version loaded from {value} does not '
- 'comply with PEP 440: {version}'
+ raise OptionError(
+ f'Version loaded from {value} does not '
+ f'comply with PEP 440: {version}'
)
- raise DistutilsOptionError(tmpl.format(**locals()))
return version
@@ -657,7 +654,7 @@ class ConfigOptionsHandler(ConfigHandler["Distribution"]):
parse_list,
"The namespace_packages parameter is deprecated, "
"consider using implicit namespaces instead (PEP 420).",
- SetuptoolsDeprecationWarning,
+ # TODO: define due date, see setuptools.dist:check_nsp.
),
'install_requires': partial(
self._parse_requirements_list, "install_requires"
@@ -766,3 +763,27 @@ class ConfigOptionsHandler(ConfigHandler["Distribution"]):
"""
parsed = self._parse_section_to_dict(section_options, self._parse_list)
self['data_files'] = expand.canonic_data_files(parsed, self.root_dir)
+
+
+class _AmbiguousMarker(SetuptoolsDeprecationWarning):
+ _SUMMARY = "Ambiguous requirement marker."
+ _DETAILS = """
+ One of the parsed requirements in `{field}` looks like a valid environment marker:
+
+ {req!r}
+
+ Please make sure that the configuration file is correct.
+ You can use dangling lines to avoid this problem.
+ """
+ _SEE_DOCS = "userguide/declarative_config.html#opt-2"
+ # TODO: should we include due_date here? Initially introduced in 6 Aug 2022.
+ # Does this make sense with latest version of packaging?
+
+ @classmethod
+ def message(cls, **kw):
+ docs = f"https://setuptools.pypa.io/en/latest/{cls._SEE_DOCS}"
+ return cls._format(cls._SUMMARY, cls._DETAILS, see_url=docs, format_args=kw)
+
+
+class _DeprecatedConfig(SetuptoolsDeprecationWarning):
+ _SEE_DOCS = "https://setuptools.pypa.io/en/latest/userguide/declarative_config.html"
diff --git a/setuptools/dist.py b/setuptools/dist.py
index eb59f3a0..c75aaabe 100644
--- a/setuptools/dist.py
+++ b/setuptools/dist.py
@@ -4,7 +4,6 @@ import io
import sys
import re
import os
-import warnings
import numbers
import distutils.log
import distutils.core
@@ -31,10 +30,6 @@ from setuptools.extern import packaging
from setuptools.extern import ordered_set
from setuptools.extern.more_itertools import unique_everseen, partition
-from ._importlib import metadata
-
-from . import SetuptoolsDeprecationWarning, _normalization
-
import setuptools
import setuptools.command
from setuptools import windows_support
@@ -45,6 +40,9 @@ from setuptools.discovery import ConfigDiscovery
from setuptools.extern.packaging import version
from . import _reqs
from . import _entry_points
+from . import _normalization
+from ._importlib import metadata
+from .warnings import InformationOnly, SetuptoolsDeprecationWarning
if TYPE_CHECKING:
from email.message import Message
@@ -54,7 +52,12 @@ __import__('setuptools.extern.packaging.version')
def _get_unpatched(cls):
- warnings.warn("Do not call this function", DistDeprecationWarning)
+ DistDeprecationWarning.emit(
+ "Private function",
+ "Do not call this function",
+ due_date=(2023, 6, 1),
+ # Warning initially introduced in 2016
+ )
return get_unpatched(cls)
@@ -156,7 +159,9 @@ def single_line(val):
if '\n' in val:
# TODO: Replace with `raise ValueError("newlines not allowed")`
# after reviewing #2893.
- warnings.warn("newlines not allowed and will break in the future")
+ msg = "newlines are not allowed in `summary` and will break in the future"
+ SetuptoolsDeprecationWarning.emit("Invalid config.", msg)
+ # due_date is undefined. Controversial change, there was a lot of push back.
val = val.strip().split('\n')[0]
return val
@@ -278,13 +283,15 @@ def check_nsp(dist, attr, value):
nsp,
parent,
)
- msg = (
- "The namespace_packages parameter is deprecated, "
- "consider using implicit namespaces instead (PEP 420). "
- "See https://setuptools.pypa.io/en/latest/references/"
- "keywords.html#keyword-namespace-packages"
+ SetuptoolsDeprecationWarning.emit(
+ "The namespace_packages parameter is deprecated.",
+ "Please replace its usage with implicit namespaces (PEP 420).",
+ see_docs="references/keywords.html#keyword-namespace-packages"
+ # TODO: define due_date, it may break old packages that are no longer
+ # maintained (e.g. sphinxcontrib extensions) when installed from source.
+ # Warning officially introduced in May 2022, however the deprecation
+ # was mentioned much earlier in the docs (May 2020, see #2149).
)
- warnings.warn(msg, SetuptoolsDeprecationWarning)
def check_extras(dist, attr, value):
@@ -325,8 +332,8 @@ def assert_bool(dist, attr, value):
def invalid_unless_false(dist, attr, value):
if not value:
- warnings.warn(f"{attr} is ignored.", DistDeprecationWarning)
- return
+ DistDeprecationWarning.emit(f"{attr} is ignored.")
+ # TODO: should there be a `due_date` here?
raise DistutilsSetupError(f"{attr} is invalid.")
@@ -543,8 +550,7 @@ class Distribution(_Distribution):
normalized = str(packaging.version.Version(version))
if version != normalized:
- tmpl = "Normalizing '{version}' to '{normalized}'"
- warnings.warn(tmpl.format(**locals()))
+ InformationOnly.emit(f"Normalizing '{version}' to '{normalized}'")
return normalized
return version
@@ -558,11 +564,17 @@ class Distribution(_Distribution):
try:
packaging.version.Version(version)
except (packaging.version.InvalidVersion, TypeError):
- warnings.warn(
- "The version specified (%r) is an invalid version, this "
- "may not work as expected with newer versions of "
- "setuptools, pip, and PyPI. Please see PEP 440 for more "
- "details." % version
+ 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 setuptools.sic(version)
return version
@@ -785,10 +797,15 @@ class Distribution(_Distribution):
return underscore_opt
if '-' in opt:
- warnings.warn(
- "Usage of dash-separated '%s' will not be supported in future "
- "versions. Please use the underscore name '%s' instead"
- % (opt, underscore_opt)
+ SetuptoolsDeprecationWarning.emit(
+ "Invalid dash-separated options",
+ f"""
+ Usage of dash-separated {opt!r} will not be supported in future
+ versions. Please use the underscore name {underscore_opt!r} instead.
+ """,
+ see_docs="userguide/declarative_config.html",
+ due_date=(2023, 9, 26),
+ # Warning initially introduced in 3 Mar 2021
)
return underscore_opt
@@ -804,10 +821,15 @@ class Distribution(_Distribution):
return opt
lowercase_opt = opt.lower()
- warnings.warn(
- "Usage of uppercase key '%s' in '%s' will be deprecated in future "
- "versions. Please use lowercase '%s' instead"
- % (opt, section, lowercase_opt)
+ SetuptoolsDeprecationWarning.emit(
+ "Invalid uppercase configuration",
+ f"""
+ Usage of uppercase key {opt!r} in {section!r} will not be supported in
+ future versions. Please use lowercase {lowercase_opt!r} instead.
+ """,
+ see_docs="userguide/declarative_config.html",
+ due_date=(2023, 9, 26),
+ # Warning initially introduced in 6 Mar 2021
)
return lowercase_opt
diff --git a/setuptools/installer.py b/setuptools/installer.py
index e9a7567a..44ed0da2 100644
--- a/setuptools/installer.py
+++ b/setuptools/installer.py
@@ -3,14 +3,13 @@ import os
import subprocess
import sys
import tempfile
-import warnings
from distutils import log
from distutils.errors import DistutilsError
from functools import partial
from . import _reqs
from .wheel import Wheel
-from ._deprecation_warning import SetuptoolsDeprecationWarning
+from .warnings import SetuptoolsDeprecationWarning
def _fixup_find_links(find_links):
@@ -25,7 +24,7 @@ def fetch_build_egg(dist, req):
"""Fetch an egg needed for building.
Use pip/wheel to fetch/build a wheel."""
- _DeprecatedInstaller.warn(stacklevel=2)
+ _DeprecatedInstaller.emit()
_warn_wheel_not_available(dist)
return _fetch_build_egg_no_warn(dist, req)
@@ -33,7 +32,7 @@ def fetch_build_egg(dist, req):
def _fetch_build_eggs(dist, requires):
import pkg_resources # Delay import to avoid unnecessary side-effects
- _DeprecatedInstaller.warn(stacklevel=3)
+ _DeprecatedInstaller.emit(stacklevel=3)
_warn_wheel_not_available(dist)
resolved_dists = pkg_resources.working_set.resolve(
@@ -131,12 +130,9 @@ def _warn_wheel_not_available(dist):
class _DeprecatedInstaller(SetuptoolsDeprecationWarning):
- @classmethod
- def warn(cls, stacklevel=1):
- warnings.warn(
- "setuptools.installer and fetch_build_eggs are deprecated. "
- "Requirements should be satisfied by a PEP 517 installer. "
- "If you are using pip, you can try `pip install --use-pep517`.",
- cls,
- stacklevel=stacklevel+1
- )
+ _SUMMARY = "setuptools.installer and fetch_build_eggs are deprecated."
+ _DETAILS = """
+ Requirements should be satisfied by a PEP 517 installer.
+ If you are using pip, you can try `pip install --use-pep517`.
+ """
+ # _DUE_DATE not decided yet
diff --git a/setuptools/package_index.py b/setuptools/package_index.py
index bec41835..06b78ac6 100644
--- a/setuptools/package_index.py
+++ b/setuptools/package_index.py
@@ -9,7 +9,6 @@ import socket
import base64
import hashlib
import itertools
-import warnings
import configparser
import html
import http.client
@@ -40,6 +39,7 @@ from distutils.errors import DistutilsError
from fnmatch import translate
from setuptools.wheel import Wheel
from setuptools.extern.more_itertools import unique_everseen
+from setuptools.warnings import SetuptoolsDeprecationWarning
EGG_FRAGMENT = re.compile(r'^egg=([-A-Za-z0-9_.+!]+)$')
@@ -868,7 +868,11 @@ class PackageIndex(Environment):
raise DistutilsError("Unexpected HTML page found at " + url)
def _download_svn(self, url, filename):
- warnings.warn("SVN download support is deprecated", UserWarning)
+ SetuptoolsDeprecationWarning.emit(
+ "Invalid config",
+ f"SVN download support is deprecated: {url}",
+ due_date=(2023, 6, 1), # Initially introduced in 23 Sept 2018
+ )
url = url.split('#', 1)[0] # remove any fragment for svn's sake
creds = ''
if url.lower().startswith('svn:') and '@' in url:
diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py
index 0a814a34..c9c521be 100644
--- a/setuptools/tests/config/test_apply_pyprojecttoml.py
+++ b/setuptools/tests/config/test_apply_pyprojecttoml.py
@@ -15,12 +15,12 @@ import pytest
from ini2toml.api import Translator
import setuptools # noqa ensure monkey patch to metadata
-from setuptools._deprecation_warning import SetuptoolsDeprecationWarning
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.command.egg_info import write_requirements
+from setuptools.warnings import SetuptoolsDeprecationWarning
from .downloads import retrieve_file, urls_from_file
diff --git a/setuptools/tests/config/test_setupcfg.py b/setuptools/tests/config/test_setupcfg.py
index a47138c8..69b75a89 100644
--- a/setuptools/tests/config/test_setupcfg.py
+++ b/setuptools/tests/config/test_setupcfg.py
@@ -7,10 +7,10 @@ from unittest.mock import Mock, patch
import pytest
from distutils.errors import DistutilsOptionError, DistutilsFileError
-from setuptools._deprecation_warning import SetuptoolsDeprecationWarning
from setuptools.dist import Distribution, _Distribution
from setuptools.config.setupcfg import ConfigHandler, read_configuration
from setuptools.extern.packaging.requirements import InvalidRequirement
+from setuptools.warnings import SetuptoolsDeprecationWarning
from ..textwrap import DALS
@@ -468,12 +468,8 @@ class TestMetadata:
'author-email = test@test.com\n'
'maintainer_email = foo@foo.com\n',
)
- msg = (
- "Usage of dash-separated 'author-email' will not be supported "
- "in future versions. "
- "Please use the underscore name 'author_email' instead"
- )
- with pytest.warns(UserWarning, match=msg):
+ msg = "Usage of dash-separated 'author-email' will not be supported"
+ with pytest.warns(SetuptoolsDeprecationWarning, match=msg):
with get_dist(tmpdir) as dist:
metadata = dist.metadata
@@ -486,12 +482,8 @@ class TestMetadata:
fake_env(
tmpdir, '[metadata]\n' 'Name = foo\n' 'description = Some description\n'
)
- msg = (
- "Usage of uppercase key 'Name' in 'metadata' will be deprecated in "
- "future versions. "
- "Please use lowercase 'name' instead"
- )
- with pytest.warns(UserWarning, match=msg):
+ msg = "Usage of uppercase key 'Name' in 'metadata' will not be supported"
+ with pytest.warns(SetuptoolsDeprecationWarning, match=msg):
with get_dist(tmpdir) as dist:
metadata = dist.metadata
@@ -755,7 +747,7 @@ class TestOptions:
r"One of the parsed requirements in `(install_requires|extras_require.+)` "
"looks like a valid environment marker.*"
)
- with pytest.warns(UserWarning, match=match):
+ with pytest.warns(SetuptoolsDeprecationWarning, match=match):
with get_dist(tmpdir) as _:
pass
@@ -774,12 +766,14 @@ class TestOptions:
"[options]\ninstall_requires =\n bar\n python_version<3\n",
],
)
+ @pytest.mark.filterwarnings("error::setuptools.SetuptoolsDeprecationWarning")
def test_nowarn_accidental_env_marker_misconfig(self, config, tmpdir, recwarn):
fake_env(tmpdir, config)
+ num_warnings = len(recwarn)
with get_dist(tmpdir) as _:
pass
# The examples are valid, no warnings shown
- assert not any(w.category == UserWarning for w in recwarn)
+ assert len(recwarn) == num_warnings
def test_dash_preserved_extras_require(self, tmpdir):
fake_env(tmpdir, '[options.extras_require]\n' 'foo-a = foo\n' 'foo_b = test\n')
diff --git a/setuptools/tests/test_warnings.py b/setuptools/tests/test_warnings.py
new file mode 100644
index 00000000..013e2526
--- /dev/null
+++ b/setuptools/tests/test_warnings.py
@@ -0,0 +1,107 @@
+from inspect import cleandoc
+
+import pytest
+
+from setuptools.warnings import SetuptoolsDeprecationWarning, SetuptoolsWarning
+
+
+_EXAMPLES = {
+ "default": dict(
+ args=("Hello {x}", "\n\t{target} {v:.1f}"),
+ kwargs={"x": 5, "v": 3, "target": "World"},
+ expected = """
+ Hello 5
+ !!
+
+ ********************************************************************************
+ World 3.0
+ ********************************************************************************
+
+ !!
+ """ # noqa,
+ ),
+ "futue_due_date": dict(
+ args=("Summary", "Lorem ipsum"),
+ kwargs={"due_date": (9999, 11, 22)},
+ expected = """
+ Summary
+ !!
+
+ ********************************************************************************
+ Lorem ipsum
+
+ By 9999-Nov-22, you need to update your project and remove deprecated calls
+ or your builds will no longer be supported.
+ ********************************************************************************
+
+ !!
+ """ # noqa
+ ),
+ "past_due_date_with_docs": dict(
+ args=("Summary", "Lorem ipsum"),
+ kwargs={"due_date": (2000, 11, 22), "see_docs": "some_page.html"},
+ expected="""
+ Summary
+ !!
+
+ ********************************************************************************
+ Lorem ipsum
+
+ This deprecation is overdue, please update your project and remove deprecated
+ calls to avoid build errors in the future.
+
+ See https://setuptools.pypa.io/en/latest/some_page.html for details.
+ ********************************************************************************
+
+ !!
+ """ # noqa
+ ),
+}
+
+
+@pytest.mark.parametrize("example_name", _EXAMPLES.keys())
+def test_formatting(monkeypatch, example_name):
+ """
+ It should automatically handle indentation, interpolation and things like due date.
+ """
+ args = _EXAMPLES[example_name]["args"]
+ kwargs = _EXAMPLES[example_name]["kwargs"]
+ expected = _EXAMPLES[example_name]["expected"]
+
+ monkeypatch.setenv("SETUPTOOLS_ENFORCE_DEPRECATION", "false")
+ with pytest.warns(SetuptoolsWarning) as warn_info:
+ SetuptoolsWarning.emit(*args, **kwargs)
+ assert _get_message(warn_info) == cleandoc(expected)
+
+
+def test_due_date_enforcement(monkeypatch):
+ class _MyDeprecation(SetuptoolsDeprecationWarning):
+ _SUMMARY = "Summary"
+ _DETAILS = "Lorem ipsum"
+ _DUE_DATE = (2000, 11, 22)
+ _SEE_DOCS = "some_page.html"
+
+ monkeypatch.setenv("SETUPTOOLS_ENFORCE_DEPRECATION", "true")
+ with pytest.raises(SetuptoolsDeprecationWarning) as exc_info:
+ _MyDeprecation.emit()
+
+ expected="""
+ Summary
+ !!
+
+ ********************************************************************************
+ Lorem ipsum
+
+ This deprecation is overdue, please update your project and remove deprecated
+ calls to avoid build errors in the future.
+
+ See https://setuptools.pypa.io/en/latest/some_page.html for details.
+ ********************************************************************************
+
+ !!
+ """ # noqa
+ assert str(exc_info.value) == cleandoc(expected)
+
+
+def _get_message(warn_info):
+ return next(warn.message.args[0] for warn in warn_info)
diff --git a/setuptools/warnings.py b/setuptools/warnings.py
new file mode 100644
index 00000000..4ea782e5
--- /dev/null
+++ b/setuptools/warnings.py
@@ -0,0 +1,104 @@
+"""Provide basic warnings used by setuptools modules.
+
+Using custom classes (other than ``UserWarning``) allow users to set
+``PYTHONWARNINGS`` filters to run tests and prepare for upcoming changes in
+setuptools.
+"""
+
+import os
+import warnings
+from datetime import date
+from inspect import cleandoc
+from textwrap import indent
+from typing import Optional, Tuple
+
+_DueDate = Tuple[int, int, int] # time tuple
+_INDENT = 8 * " "
+_TEMPLATE = f"""{80 * '*'}\n{{details}}\n{80 * '*'}"""
+
+
+class SetuptoolsWarning(UserWarning):
+ """Base class in ``setuptools`` warning hierarchy."""
+
+ @classmethod
+ def emit(
+ cls,
+ summary: Optional[str] = None,
+ details: Optional[str] = None,
+ due_date: Optional[_DueDate] = None,
+ see_docs: Optional[str] = None,
+ see_url: Optional[str] = None,
+ stacklevel: int = 2,
+ **kwargs
+ ):
+ """Private: reserved for ``setuptools`` internal use only"""
+ # Default values:
+ summary_ = summary or getattr(cls, "_SUMMARY", None) or ""
+ details_ = details or getattr(cls, "_DETAILS", None) or ""
+ due_date = due_date or getattr(cls, "_DUE_DATE", None)
+ docs_ref = see_docs or getattr(cls, "_SEE_DOCS", None)
+ docs_url = docs_ref and f"https://setuptools.pypa.io/en/latest/{docs_ref}"
+ see_url = see_url or getattr(cls, "_SEE_URL", None)
+ due = date(*due_date) if due_date else None
+
+ text = cls._format(summary_, details_, due, see_url or docs_url, kwargs)
+ if due and due < date.today() and _should_enforce():
+ raise cls(text)
+ warnings.warn(text, cls, stacklevel=stacklevel + 1)
+
+ @classmethod
+ def _format(
+ cls,
+ summary: str,
+ details: str,
+ due_date: Optional[date] = None,
+ see_url: Optional[str] = None,
+ format_args: Optional[dict] = None,
+ ):
+ """Private: reserved for ``setuptools`` internal use only"""
+ today = date.today()
+ summary = cleandoc(summary).format_map(format_args or {})
+ possible_parts = [
+ cleandoc(details).format_map(format_args or {}),
+ (
+ f"\nBy {due_date:%Y-%b-%d}, you need to update your project and remove "
+ "deprecated calls\nor your builds will no longer be supported."
+ if due_date and due_date > today else None
+ ),
+ (
+ "\nThis deprecation is overdue, please update your project and remove "
+ "deprecated\ncalls to avoid build errors in the future."
+ if due_date and due_date < today else None
+ ),
+ (f"\nSee {see_url} for details." if see_url else None)
+
+ ]
+ parts = [x for x in possible_parts if x]
+ if parts:
+ body = indent(_TEMPLATE.format(details="\n".join(parts)), _INDENT)
+ return "\n".join([summary, "!!\n", body, "\n!!"])
+ return summary
+
+
+class InformationOnly(SetuptoolsWarning):
+ """Currently there is no clear way of displaying messages to the users
+ that use the setuptools backend directly via ``pip``.
+ The only thing that might work is a warning, although it is not the
+ most appropriate tool for the job...
+
+ See pypa/packaging-problems#558.
+ """
+
+
+class SetuptoolsDeprecationWarning(SetuptoolsWarning):
+ """
+ Base class for warning deprecations in ``setuptools``
+
+ This class is not derived from ``DeprecationWarning``, and as such is
+ visible by default.
+ """
+
+
+def _should_enforce():
+ enforce = os.getenv("SETUPTOOLS_ENFORCE_DEPRECATION", "false").lower()
+ return enforce in ("true", "on", "ok", "1")
diff --git a/tox.ini b/tox.ini
index fec1bd56..2a521733 100644
--- a/tox.ini
+++ b/tox.ini
@@ -10,6 +10,7 @@ deps =
# Ideally all the dependencies should be set as "extras"
setenv =
PYTHONWARNDEFAULTENCODING = 1
+ SETUPTOOLS_ENFORCE_DEPRECATION = 1
commands =
pytest {posargs}
usedevelop = True