jpayne@69: """Provide basic warnings used by setuptools modules. jpayne@69: jpayne@69: Using custom classes (other than ``UserWarning``) allow users to set jpayne@69: ``PYTHONWARNINGS`` filters to run tests and prepare for upcoming changes in jpayne@69: setuptools. jpayne@69: """ jpayne@69: jpayne@69: from __future__ import annotations jpayne@69: jpayne@69: import os jpayne@69: import warnings jpayne@69: from datetime import date jpayne@69: from inspect import cleandoc jpayne@69: from textwrap import indent jpayne@69: from typing import TYPE_CHECKING, Tuple jpayne@69: jpayne@69: if TYPE_CHECKING: jpayne@69: from typing_extensions import TypeAlias jpayne@69: jpayne@69: _DueDate: TypeAlias = Tuple[int, int, int] # time tuple jpayne@69: _INDENT = 8 * " " jpayne@69: _TEMPLATE = f"""{80 * '*'}\n{{details}}\n{80 * '*'}""" jpayne@69: jpayne@69: jpayne@69: class SetuptoolsWarning(UserWarning): jpayne@69: """Base class in ``setuptools`` warning hierarchy.""" jpayne@69: jpayne@69: @classmethod jpayne@69: def emit( jpayne@69: cls, jpayne@69: summary: str | None = None, jpayne@69: details: str | None = None, jpayne@69: due_date: _DueDate | None = None, jpayne@69: see_docs: str | None = None, jpayne@69: see_url: str | None = None, jpayne@69: stacklevel: int = 2, jpayne@69: **kwargs, jpayne@69: ) -> None: jpayne@69: """Private: reserved for ``setuptools`` internal use only""" jpayne@69: # Default values: jpayne@69: summary_ = summary or getattr(cls, "_SUMMARY", None) or "" jpayne@69: details_ = details or getattr(cls, "_DETAILS", None) or "" jpayne@69: due_date = due_date or getattr(cls, "_DUE_DATE", None) jpayne@69: docs_ref = see_docs or getattr(cls, "_SEE_DOCS", None) jpayne@69: docs_url = docs_ref and f"https://setuptools.pypa.io/en/latest/{docs_ref}" jpayne@69: see_url = see_url or getattr(cls, "_SEE_URL", None) jpayne@69: due = date(*due_date) if due_date else None jpayne@69: jpayne@69: text = cls._format(summary_, details_, due, see_url or docs_url, kwargs) jpayne@69: if due and due < date.today() and _should_enforce(): jpayne@69: raise cls(text) jpayne@69: warnings.warn(text, cls, stacklevel=stacklevel + 1) jpayne@69: jpayne@69: @classmethod jpayne@69: def _format( jpayne@69: cls, jpayne@69: summary: str, jpayne@69: details: str, jpayne@69: due_date: date | None = None, jpayne@69: see_url: str | None = None, jpayne@69: format_args: dict | None = None, jpayne@69: ) -> str: jpayne@69: """Private: reserved for ``setuptools`` internal use only""" jpayne@69: today = date.today() jpayne@69: summary = cleandoc(summary).format_map(format_args or {}) jpayne@69: possible_parts = [ jpayne@69: cleandoc(details).format_map(format_args or {}), jpayne@69: ( jpayne@69: f"\nBy {due_date:%Y-%b-%d}, you need to update your project and remove " jpayne@69: "deprecated calls\nor your builds will no longer be supported." jpayne@69: if due_date and due_date > today jpayne@69: else None jpayne@69: ), jpayne@69: ( jpayne@69: "\nThis deprecation is overdue, please update your project and remove " jpayne@69: "deprecated\ncalls to avoid build errors in the future." jpayne@69: if due_date and due_date < today jpayne@69: else None jpayne@69: ), jpayne@69: (f"\nSee {see_url} for details." if see_url else None), jpayne@69: ] jpayne@69: parts = [x for x in possible_parts if x] jpayne@69: if parts: jpayne@69: body = indent(_TEMPLATE.format(details="\n".join(parts)), _INDENT) jpayne@69: return "\n".join([summary, "!!\n", body, "\n!!"]) jpayne@69: return summary jpayne@69: jpayne@69: jpayne@69: class InformationOnly(SetuptoolsWarning): jpayne@69: """Currently there is no clear way of displaying messages to the users jpayne@69: that use the setuptools backend directly via ``pip``. jpayne@69: The only thing that might work is a warning, although it is not the jpayne@69: most appropriate tool for the job... jpayne@69: jpayne@69: See pypa/packaging-problems#558. jpayne@69: """ jpayne@69: jpayne@69: jpayne@69: class SetuptoolsDeprecationWarning(SetuptoolsWarning): jpayne@69: """ jpayne@69: Base class for warning deprecations in ``setuptools`` jpayne@69: jpayne@69: This class is not derived from ``DeprecationWarning``, and as such is jpayne@69: visible by default. jpayne@69: """ jpayne@69: jpayne@69: jpayne@69: def _should_enforce(): jpayne@69: enforce = os.getenv("SETUPTOOLS_ENFORCE_DEPRECATION", "false").lower() jpayne@69: return enforce in ("true", "on", "ok", "1")