deprecations.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. # util/deprecations.py
  2. # Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
  3. # <see AUTHORS file>
  4. #
  5. # This module is part of SQLAlchemy and is released under
  6. # the MIT License: https://www.opensource.org/licenses/mit-license.php
  7. # mypy: allow-untyped-defs, allow-untyped-calls
  8. """Helpers related to deprecation of functions, methods, classes, other
  9. functionality."""
  10. from __future__ import annotations
  11. import re
  12. from typing import Any
  13. from typing import Callable
  14. from typing import Dict
  15. from typing import Match
  16. from typing import Optional
  17. from typing import Sequence
  18. from typing import Set
  19. from typing import Tuple
  20. from typing import Type
  21. from typing import TypeVar
  22. from typing import Union
  23. from . import compat
  24. from .langhelpers import _hash_limit_string
  25. from .langhelpers import _warnings_warn
  26. from .langhelpers import decorator
  27. from .langhelpers import inject_docstring_text
  28. from .langhelpers import inject_param_text
  29. from .. import exc
  30. _T = TypeVar("_T", bound=Any)
  31. # https://mypy.readthedocs.io/en/stable/generics.html#declaring-decorators
  32. _F = TypeVar("_F", bound="Callable[..., Any]")
  33. def _warn_with_version(
  34. msg: str,
  35. version: str,
  36. type_: Type[exc.SADeprecationWarning],
  37. stacklevel: int,
  38. code: Optional[str] = None,
  39. ) -> None:
  40. warn = type_(msg, code=code)
  41. warn.deprecated_since = version
  42. _warnings_warn(warn, stacklevel=stacklevel + 1)
  43. def warn_deprecated(
  44. msg: str, version: str, stacklevel: int = 3, code: Optional[str] = None
  45. ) -> None:
  46. _warn_with_version(
  47. msg, version, exc.SADeprecationWarning, stacklevel, code=code
  48. )
  49. def warn_deprecated_limited(
  50. msg: str,
  51. args: Sequence[Any],
  52. version: str,
  53. stacklevel: int = 3,
  54. code: Optional[str] = None,
  55. ) -> None:
  56. """Issue a deprecation warning with a parameterized string,
  57. limiting the number of registrations.
  58. """
  59. if args:
  60. msg = _hash_limit_string(msg, 10, args)
  61. _warn_with_version(
  62. msg, version, exc.SADeprecationWarning, stacklevel, code=code
  63. )
  64. def deprecated_cls(
  65. version: str, message: str, constructor: Optional[str] = "__init__"
  66. ) -> Callable[[Type[_T]], Type[_T]]:
  67. header = ".. deprecated:: %s %s" % (version, (message or ""))
  68. def decorate(cls: Type[_T]) -> Type[_T]:
  69. return _decorate_cls_with_warning(
  70. cls,
  71. constructor,
  72. exc.SADeprecationWarning,
  73. message % dict(func=constructor),
  74. version,
  75. header,
  76. )
  77. return decorate
  78. def deprecated(
  79. version: str,
  80. message: Optional[str] = None,
  81. add_deprecation_to_docstring: bool = True,
  82. warning: Optional[Type[exc.SADeprecationWarning]] = None,
  83. enable_warnings: bool = True,
  84. ) -> Callable[[_F], _F]:
  85. """Decorates a function and issues a deprecation warning on use.
  86. :param version:
  87. Issue version in the warning.
  88. :param message:
  89. If provided, issue message in the warning. A sensible default
  90. is used if not provided.
  91. :param add_deprecation_to_docstring:
  92. Default True. If False, the wrapped function's __doc__ is left
  93. as-is. If True, the 'message' is prepended to the docs if
  94. provided, or sensible default if message is omitted.
  95. """
  96. if add_deprecation_to_docstring:
  97. header = ".. deprecated:: %s %s" % (
  98. version,
  99. (message or ""),
  100. )
  101. else:
  102. header = None
  103. if message is None:
  104. message = "Call to deprecated function %(func)s"
  105. if warning is None:
  106. warning = exc.SADeprecationWarning
  107. message += " (deprecated since: %s)" % version
  108. def decorate(fn: _F) -> _F:
  109. assert message is not None
  110. assert warning is not None
  111. return _decorate_with_warning(
  112. fn,
  113. warning,
  114. message % dict(func=fn.__name__),
  115. version,
  116. header,
  117. enable_warnings=enable_warnings,
  118. )
  119. return decorate
  120. def moved_20(
  121. message: str, **kw: Any
  122. ) -> Callable[[Callable[..., _T]], Callable[..., _T]]:
  123. return deprecated(
  124. "2.0", message=message, warning=exc.MovedIn20Warning, **kw
  125. )
  126. def became_legacy_20(
  127. api_name: str, alternative: Optional[str] = None, **kw: Any
  128. ) -> Callable[[_F], _F]:
  129. type_reg = re.match("^:(attr|func|meth):", api_name)
  130. if type_reg:
  131. type_ = {"attr": "attribute", "func": "function", "meth": "method"}[
  132. type_reg.group(1)
  133. ]
  134. else:
  135. type_ = "construct"
  136. message = (
  137. "The %s %s is considered legacy as of the "
  138. "1.x series of SQLAlchemy and %s in 2.0."
  139. % (
  140. api_name,
  141. type_,
  142. "becomes a legacy construct",
  143. )
  144. )
  145. if ":attr:" in api_name:
  146. attribute_ok = kw.pop("warn_on_attribute_access", False)
  147. if not attribute_ok:
  148. assert kw.get("enable_warnings") is False, (
  149. "attribute %s will emit a warning on read access. "
  150. "If you *really* want this, "
  151. "add warn_on_attribute_access=True. Otherwise please add "
  152. "enable_warnings=False." % api_name
  153. )
  154. if alternative:
  155. message += " " + alternative
  156. warning_cls = exc.LegacyAPIWarning
  157. return deprecated("2.0", message=message, warning=warning_cls, **kw)
  158. def deprecated_params(**specs: Tuple[str, str]) -> Callable[[_F], _F]:
  159. """Decorates a function to warn on use of certain parameters.
  160. e.g. ::
  161. @deprecated_params(
  162. weak_identity_map=(
  163. "0.7",
  164. "the :paramref:`.Session.weak_identity_map parameter "
  165. "is deprecated.",
  166. )
  167. )
  168. def some_function(**kwargs): ...
  169. """
  170. messages: Dict[str, str] = {}
  171. versions: Dict[str, str] = {}
  172. version_warnings: Dict[str, Type[exc.SADeprecationWarning]] = {}
  173. for param, (version, message) in specs.items():
  174. versions[param] = version
  175. messages[param] = _sanitize_restructured_text(message)
  176. version_warnings[param] = exc.SADeprecationWarning
  177. def decorate(fn: _F) -> _F:
  178. spec = compat.inspect_getfullargspec(fn)
  179. check_defaults: Union[Set[str], Tuple[()]]
  180. if spec.defaults is not None:
  181. defaults = dict(
  182. zip(
  183. spec.args[(len(spec.args) - len(spec.defaults)) :],
  184. spec.defaults,
  185. )
  186. )
  187. check_defaults = set(defaults).intersection(messages)
  188. check_kw = set(messages).difference(defaults)
  189. elif spec.kwonlydefaults is not None:
  190. defaults = spec.kwonlydefaults
  191. check_defaults = set(defaults).intersection(messages)
  192. check_kw = set(messages).difference(defaults)
  193. else:
  194. check_defaults = ()
  195. check_kw = set(messages)
  196. check_any_kw = spec.varkw
  197. # latest mypy has opinions here, not sure if they implemented
  198. # Concatenate or something
  199. @decorator
  200. def warned(fn: _F, *args: Any, **kwargs: Any) -> _F:
  201. for m in check_defaults:
  202. if (defaults[m] is None and kwargs[m] is not None) or (
  203. defaults[m] is not None and kwargs[m] != defaults[m]
  204. ):
  205. _warn_with_version(
  206. messages[m],
  207. versions[m],
  208. version_warnings[m],
  209. stacklevel=3,
  210. )
  211. if check_any_kw in messages and set(kwargs).difference(
  212. check_defaults
  213. ):
  214. assert check_any_kw is not None
  215. _warn_with_version(
  216. messages[check_any_kw],
  217. versions[check_any_kw],
  218. version_warnings[check_any_kw],
  219. stacklevel=3,
  220. )
  221. for m in check_kw:
  222. if m in kwargs:
  223. _warn_with_version(
  224. messages[m],
  225. versions[m],
  226. version_warnings[m],
  227. stacklevel=3,
  228. )
  229. return fn(*args, **kwargs) # type: ignore[no-any-return]
  230. doc = fn.__doc__ is not None and fn.__doc__ or ""
  231. if doc:
  232. doc = inject_param_text(
  233. doc,
  234. {
  235. param: ".. deprecated:: %s %s"
  236. % ("1.4" if version == "2.0" else version, (message or ""))
  237. for param, (version, message) in specs.items()
  238. },
  239. )
  240. decorated = warned(fn)
  241. decorated.__doc__ = doc
  242. return decorated
  243. return decorate
  244. def _sanitize_restructured_text(text: str) -> str:
  245. def repl(m: Match[str]) -> str:
  246. type_, name = m.group(1, 2)
  247. if type_ in ("func", "meth"):
  248. name += "()"
  249. return name
  250. text = re.sub(r":ref:`(.+) <.*>`", lambda m: '"%s"' % m.group(1), text)
  251. return re.sub(r"\:(\w+)\:`~?(?:_\w+)?\.?(.+?)`", repl, text)
  252. def _decorate_cls_with_warning(
  253. cls: Type[_T],
  254. constructor: Optional[str],
  255. wtype: Type[exc.SADeprecationWarning],
  256. message: str,
  257. version: str,
  258. docstring_header: Optional[str] = None,
  259. ) -> Type[_T]:
  260. doc = cls.__doc__ is not None and cls.__doc__ or ""
  261. if docstring_header is not None:
  262. if constructor is not None:
  263. docstring_header %= dict(func=constructor)
  264. if issubclass(wtype, exc.Base20DeprecationWarning):
  265. docstring_header += (
  266. " (Background on SQLAlchemy 2.0 at: "
  267. ":ref:`migration_20_toplevel`)"
  268. )
  269. doc = inject_docstring_text(doc, docstring_header, 1)
  270. constructor_fn = None
  271. if type(cls) is type:
  272. clsdict = dict(cls.__dict__)
  273. clsdict["__doc__"] = doc
  274. clsdict.pop("__dict__", None)
  275. clsdict.pop("__weakref__", None)
  276. cls = type(cls.__name__, cls.__bases__, clsdict)
  277. if constructor is not None:
  278. constructor_fn = clsdict[constructor]
  279. else:
  280. cls.__doc__ = doc
  281. if constructor is not None:
  282. constructor_fn = getattr(cls, constructor)
  283. if constructor is not None:
  284. assert constructor_fn is not None
  285. assert wtype is not None
  286. setattr(
  287. cls,
  288. constructor,
  289. _decorate_with_warning(
  290. constructor_fn, wtype, message, version, None
  291. ),
  292. )
  293. return cls
  294. def _decorate_with_warning(
  295. func: _F,
  296. wtype: Type[exc.SADeprecationWarning],
  297. message: str,
  298. version: str,
  299. docstring_header: Optional[str] = None,
  300. enable_warnings: bool = True,
  301. ) -> _F:
  302. """Wrap a function with a warnings.warn and augmented docstring."""
  303. message = _sanitize_restructured_text(message)
  304. if issubclass(wtype, exc.Base20DeprecationWarning):
  305. doc_only = (
  306. " (Background on SQLAlchemy 2.0 at: "
  307. ":ref:`migration_20_toplevel`)"
  308. )
  309. else:
  310. doc_only = ""
  311. @decorator
  312. def warned(fn: _F, *args: Any, **kwargs: Any) -> _F:
  313. skip_warning = not enable_warnings or kwargs.pop(
  314. "_sa_skip_warning", False
  315. )
  316. if not skip_warning:
  317. _warn_with_version(message, version, wtype, stacklevel=3)
  318. return fn(*args, **kwargs) # type: ignore[no-any-return]
  319. doc = func.__doc__ is not None and func.__doc__ or ""
  320. if docstring_header is not None:
  321. docstring_header %= dict(func=func.__name__)
  322. docstring_header += doc_only
  323. doc = inject_docstring_text(doc, docstring_header, 1)
  324. decorated = warned(func)
  325. decorated.__doc__ = doc
  326. decorated._sa_warn = lambda: _warn_with_version( # type: ignore
  327. message, version, wtype, stacklevel=3
  328. )
  329. return decorated