deprecated.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. # -*- coding: utf-8 -*-
  2. """
  3. Deprecation-Decorators für kontrollierte API-Evolution.
  4. Bietet Warnmechanismen für veraltete Funktionen und Klassen
  5. mit konfigurierbarem Verhalten.
  6. """
  7. from __future__ import annotations
  8. import functools
  9. import warnings
  10. from dataclasses import dataclass, field
  11. from datetime import datetime
  12. from typing import Any, Callable, ParamSpec, Type, TypeVar
  13. P = ParamSpec("P")
  14. R = TypeVar("R")
  15. T = TypeVar("T")
  16. @dataclass
  17. class DeprecationInfo:
  18. """
  19. Informationen über eine Deprecation.
  20. Speichert alle Details zur Deprecation für Reporting
  21. und Dokumentation.
  22. """
  23. name: str
  24. """Name der deprecateten Funktion/Klasse."""
  25. message: str
  26. """Deprecation-Nachricht."""
  27. version: str | None = None
  28. """Version, in der deprecated wurde."""
  29. removal_version: str | None = None
  30. """Version, in der entfernt wird."""
  31. replacement: str | None = None
  32. """Empfohlene Alternative."""
  33. since: datetime = field(default_factory=datetime.now)
  34. """Zeitpunkt der Deprecation."""
  35. call_count: int = 0
  36. """Anzahl der Aufrufe seit Deprecation."""
  37. def format_warning(self) -> str:
  38. """
  39. Formatiert die Deprecation-Warnung.
  40. Returns:
  41. Formatierte Warnmeldung.
  42. """
  43. parts = [f"{self.name} ist deprecated"]
  44. if self.version:
  45. parts.append(f"seit Version {self.version}")
  46. if self.message:
  47. parts.append(f": {self.message}")
  48. if self.replacement:
  49. parts.append(f". Verwende stattdessen: {self.replacement}")
  50. if self.removal_version:
  51. parts.append(f". Wird entfernt in Version {self.removal_version}")
  52. return " ".join(parts)
  53. # Globales Registry für deprecatete APIs
  54. _deprecation_registry: dict[str, DeprecationInfo] = {}
  55. def get_deprecations() -> dict[str, DeprecationInfo]:
  56. """
  57. Gibt alle registrierten Deprecations zurück.
  58. Returns:
  59. Dictionary mit DeprecationInfo-Objekten.
  60. """
  61. return dict(_deprecation_registry)
  62. def deprecated(
  63. message: str = "",
  64. version: str | None = None,
  65. removal_version: str | None = None,
  66. replacement: str | None = None,
  67. warn_once: bool = True,
  68. category: Type[Warning] = DeprecationWarning,
  69. ) -> Callable[[Callable[P, R]], Callable[P, R]]:
  70. """
  71. Decorator um Funktionen als deprecated zu markieren.
  72. Gibt bei jedem Aufruf eine Warnung aus (oder nur einmal,
  73. wenn warn_once=True).
  74. Args:
  75. message: Beschreibung warum deprecated.
  76. version: Version, in der deprecated wurde.
  77. removal_version: Version, in der entfernt wird.
  78. replacement: Empfohlene Alternative.
  79. warn_once: Ob nur einmal gewarnt werden soll.
  80. category: Warnungskategorie.
  81. Returns:
  82. Decorator-Funktion.
  83. Example:
  84. @deprecated(
  85. message="Diese Funktion ist veraltet",
  86. version="2.0.0",
  87. replacement="new_function()"
  88. )
  89. def old_function():
  90. pass
  91. @deprecated(removal_version="3.0.0")
  92. def soon_gone():
  93. pass
  94. """
  95. def decorator(func: Callable[P, R]) -> Callable[P, R]:
  96. name = func.__qualname__
  97. # Info-Objekt erstellen
  98. info = DeprecationInfo(
  99. name=name,
  100. message=message,
  101. version=version,
  102. removal_version=removal_version,
  103. replacement=replacement,
  104. )
  105. _deprecation_registry[name] = info
  106. warned = False
  107. @functools.wraps(func)
  108. def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
  109. nonlocal warned
  110. info.call_count += 1
  111. if not warn_once or not warned:
  112. warnings.warn(
  113. info.format_warning(),
  114. category=category,
  115. stacklevel=2,
  116. )
  117. warned = True
  118. return func(*args, **kwargs)
  119. # Docstring aktualisieren
  120. if wrapper.__doc__:
  121. wrapper.__doc__ = f"**DEPRECATED**: {info.format_warning()}\n\n{wrapper.__doc__}"
  122. else:
  123. wrapper.__doc__ = f"**DEPRECATED**: {info.format_warning()}"
  124. # Info-Zugriff über die Funktion
  125. wrapper.deprecation_info = info # type: ignore
  126. return wrapper
  127. return decorator
  128. def pending_deprecation(
  129. message: str = "",
  130. version: str | None = None,
  131. replacement: str | None = None,
  132. warn_once: bool = True,
  133. ) -> Callable[[Callable[P, R]], Callable[P, R]]:
  134. """
  135. Decorator für zukünftige Deprecation.
  136. Wie @deprecated, aber mit PendingDeprecationWarning.
  137. Gedacht für APIs, die noch nicht deprecated sind, aber
  138. bald sein werden.
  139. Args:
  140. message: Beschreibung der geplanten Änderung.
  141. version: Version, ab der deprecated sein wird.
  142. replacement: Empfohlene Alternative.
  143. warn_once: Ob nur einmal gewarnt werden soll.
  144. Returns:
  145. Decorator-Funktion.
  146. Example:
  147. @pending_deprecation(
  148. message="Wird in Version 3.0 deprecated",
  149. replacement="new_api()"
  150. )
  151. def current_function():
  152. pass
  153. """
  154. return deprecated(
  155. message=f"Bald deprecated: {message}",
  156. version=version,
  157. replacement=replacement,
  158. warn_once=warn_once,
  159. category=PendingDeprecationWarning,
  160. )
  161. def deprecated_argument(
  162. arg_name: str,
  163. message: str = "",
  164. replacement: str | None = None,
  165. warn_once: bool = True,
  166. ) -> Callable[[Callable[P, R]], Callable[P, R]]:
  167. """
  168. Decorator um ein bestimmtes Argument als deprecated zu markieren.
  169. Warnt nur, wenn das deprecatete Argument tatsächlich verwendet wird.
  170. Args:
  171. arg_name: Name des deprecateten Arguments.
  172. message: Beschreibung warum deprecated.
  173. replacement: Empfohlenes Ersatzargument.
  174. warn_once: Ob nur einmal gewarnt werden soll.
  175. Returns:
  176. Decorator-Funktion.
  177. Example:
  178. @deprecated_argument("old_param", replacement="new_param")
  179. def my_function(new_param=None, old_param=None):
  180. if old_param is not None:
  181. new_param = old_param
  182. return new_param
  183. """
  184. def decorator(func: Callable[P, R]) -> Callable[P, R]:
  185. warned = False
  186. @functools.wraps(func)
  187. def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
  188. nonlocal warned
  189. if arg_name in kwargs:
  190. if not warn_once or not warned:
  191. warning_msg = (
  192. f"Argument '{arg_name}' in {func.__qualname__} "
  193. f"ist deprecated"
  194. )
  195. if message:
  196. warning_msg += f": {message}"
  197. if replacement:
  198. warning_msg += f". Verwende stattdessen: {replacement}"
  199. warnings.warn(
  200. warning_msg,
  201. DeprecationWarning,
  202. stacklevel=2,
  203. )
  204. warned = True
  205. return func(*args, **kwargs)
  206. return wrapper
  207. return decorator
  208. def deprecated_class(
  209. message: str = "",
  210. version: str | None = None,
  211. removal_version: str | None = None,
  212. replacement: str | None = None,
  213. warn_once: bool = True,
  214. ) -> Callable[[Type[T]], Type[T]]:
  215. """
  216. Decorator um eine Klasse als deprecated zu markieren.
  217. Warnt bei jeder Instanziierung.
  218. Args:
  219. message: Beschreibung warum deprecated.
  220. version: Version, in der deprecated wurde.
  221. removal_version: Version, in der entfernt wird.
  222. replacement: Empfohlene Ersatzklasse.
  223. warn_once: Ob nur einmal gewarnt werden soll.
  224. Returns:
  225. Decorator-Funktion.
  226. Example:
  227. @deprecated_class(replacement="NewClass")
  228. class OldClass:
  229. pass
  230. """
  231. def decorator(cls: Type[T]) -> Type[T]:
  232. name = cls.__qualname__
  233. info = DeprecationInfo(
  234. name=name,
  235. message=message,
  236. version=version,
  237. removal_version=removal_version,
  238. replacement=replacement,
  239. )
  240. _deprecation_registry[name] = info
  241. original_init = cls.__init__
  242. warned = False
  243. @functools.wraps(original_init)
  244. def new_init(self: T, *args: Any, **kwargs: Any) -> None:
  245. nonlocal warned
  246. info.call_count += 1
  247. if not warn_once or not warned:
  248. warnings.warn(
  249. info.format_warning(),
  250. DeprecationWarning,
  251. stacklevel=2,
  252. )
  253. warned = True
  254. original_init(self, *args, **kwargs)
  255. cls.__init__ = new_init # type: ignore
  256. # Docstring aktualisieren
  257. if cls.__doc__:
  258. cls.__doc__ = f"**DEPRECATED**: {info.format_warning()}\n\n{cls.__doc__}"
  259. else:
  260. cls.__doc__ = f"**DEPRECATED**: {info.format_warning()}"
  261. # Info-Zugriff über die Klasse
  262. cls.deprecation_info = info # type: ignore
  263. return cls
  264. return decorator
  265. def deprecated_alias(
  266. original: Callable[P, R],
  267. message: str = "",
  268. version: str | None = None,
  269. ) -> Callable[P, R]:
  270. """
  271. Erstellt einen deprecateten Alias für eine Funktion.
  272. Nützlich für Umbenennungen, wo der alte Name noch
  273. eine Weile unterstützt werden soll.
  274. Args:
  275. original: Die originale (neue) Funktion.
  276. message: Optionale zusätzliche Nachricht.
  277. version: Version, in der der Alias deprecated wurde.
  278. Returns:
  279. Wrapper-Funktion.
  280. Example:
  281. def new_function():
  282. return "result"
  283. # Alter Name als deprecateter Alias
  284. old_function = deprecated_alias(
  285. new_function,
  286. message="Wurde umbenannt zu new_function",
  287. version="2.0.0"
  288. )
  289. """
  290. @deprecated(
  291. message=message or f"Verwende {original.__qualname__} stattdessen",
  292. version=version,
  293. replacement=original.__qualname__,
  294. )
  295. @functools.wraps(original)
  296. def alias(*args: P.args, **kwargs: P.kwargs) -> R:
  297. return original(*args, **kwargs)
  298. alias.__name__ = f"{original.__name__}_deprecated"
  299. return alias