1"""Provide basic warnings used by setuptools modules.
2
3Using custom classes (other than ``UserWarning``) allow users to set
4``PYTHONWARNINGS`` filters to run tests and prepare for upcoming changes in
5setuptools.
6"""
7
8from __future__ import annotations
9
10import os
11import warnings
12from datetime import date
13from inspect import cleandoc
14from textwrap import indent
15from typing import Tuple
16
17_DueDate = Tuple[int, int, int] # time tuple
18_INDENT = 8 * " "
19_TEMPLATE = f"""{80 * '*'}\n{{details}}\n{80 * '*'}"""
20
21
22class SetuptoolsWarning(UserWarning):
23 """Base class in ``setuptools`` warning hierarchy."""
24
25 @classmethod
26 def emit(
27 cls,
28 summary: str | None = None,
29 details: str | None = None,
30 due_date: _DueDate | None = None,
31 see_docs: str | None = None,
32 see_url: str | None = None,
33 stacklevel: int = 2,
34 **kwargs,
35 ) -> None:
36 """Private: reserved for ``setuptools`` internal use only"""
37 # Default values:
38 summary_ = summary or getattr(cls, "_SUMMARY", None) or ""
39 details_ = details or getattr(cls, "_DETAILS", None) or ""
40 due_date = due_date or getattr(cls, "_DUE_DATE", None)
41 docs_ref = see_docs or getattr(cls, "_SEE_DOCS", None)
42 docs_url = docs_ref and f"https://setuptools.pypa.io/en/latest/{docs_ref}"
43 see_url = see_url or getattr(cls, "_SEE_URL", None)
44 due = date(*due_date) if due_date else None
45
46 text = cls._format(summary_, details_, due, see_url or docs_url, kwargs)
47 if due and due < date.today() and _should_enforce():
48 raise cls(text)
49 warnings.warn(text, cls, stacklevel=stacklevel + 1)
50
51 @classmethod
52 def _format(
53 cls,
54 summary: str,
55 details: str,
56 due_date: date | None = None,
57 see_url: str | None = None,
58 format_args: dict | None = None,
59 ) -> str:
60 """Private: reserved for ``setuptools`` internal use only"""
61 today = date.today()
62 summary = cleandoc(summary).format_map(format_args or {})
63 possible_parts = [
64 cleandoc(details).format_map(format_args or {}),
65 (
66 f"\nBy {due_date:%Y-%b-%d}, you need to update your project and remove "
67 "deprecated calls\nor your builds will no longer be supported."
68 if due_date and due_date > today
69 else None
70 ),
71 (
72 "\nThis deprecation is overdue, please update your project and remove "
73 "deprecated\ncalls to avoid build errors in the future."
74 if due_date and due_date < today
75 else None
76 ),
77 (f"\nSee {see_url} for details." if see_url else None),
78 ]
79 parts = [x for x in possible_parts if x]
80 if parts:
81 body = indent(_TEMPLATE.format(details="\n".join(parts)), _INDENT)
82 return "\n".join([summary, "!!\n", body, "\n!!"])
83 return summary
84
85
86class InformationOnly(SetuptoolsWarning):
87 """Currently there is no clear way of displaying messages to the users
88 that use the setuptools backend directly via ``pip``.
89 The only thing that might work is a warning, although it is not the
90 most appropriate tool for the job...
91
92 See pypa/packaging-problems#558.
93 """
94
95
96class SetuptoolsDeprecationWarning(SetuptoolsWarning):
97 """
98 Base class for warning deprecations in ``setuptools``
99
100 This class is not derived from ``DeprecationWarning``, and as such is
101 visible by default.
102 """
103
104
105def _should_enforce():
106 enforce = os.getenv("SETUPTOOLS_ENFORCE_DEPRECATION", "false").lower()
107 return enforce in ("true", "on", "ok", "1")