# -*- coding: utf-8 -*- """ Deprecation-Decorators für kontrollierte API-Evolution. Bietet Warnmechanismen für veraltete Funktionen und Klassen mit konfigurierbarem Verhalten. """ from __future__ import annotations import functools import warnings from dataclasses import dataclass, field from datetime import datetime from typing import Any, Callable, ParamSpec, Type, TypeVar P = ParamSpec("P") R = TypeVar("R") T = TypeVar("T") @dataclass class DeprecationInfo: """ Informationen über eine Deprecation. Speichert alle Details zur Deprecation für Reporting und Dokumentation. """ name: str """Name der deprecateten Funktion/Klasse.""" message: str """Deprecation-Nachricht.""" version: str | None = None """Version, in der deprecated wurde.""" removal_version: str | None = None """Version, in der entfernt wird.""" replacement: str | None = None """Empfohlene Alternative.""" since: datetime = field(default_factory=datetime.now) """Zeitpunkt der Deprecation.""" call_count: int = 0 """Anzahl der Aufrufe seit Deprecation.""" def format_warning(self) -> str: """ Formatiert die Deprecation-Warnung. Returns: Formatierte Warnmeldung. """ parts = [f"{self.name} ist deprecated"] if self.version: parts.append(f"seit Version {self.version}") if self.message: parts.append(f": {self.message}") if self.replacement: parts.append(f". Verwende stattdessen: {self.replacement}") if self.removal_version: parts.append(f". Wird entfernt in Version {self.removal_version}") return " ".join(parts) # Globales Registry für deprecatete APIs _deprecation_registry: dict[str, DeprecationInfo] = {} def get_deprecations() -> dict[str, DeprecationInfo]: """ Gibt alle registrierten Deprecations zurück. Returns: Dictionary mit DeprecationInfo-Objekten. """ return dict(_deprecation_registry) def deprecated( message: str = "", version: str | None = None, removal_version: str | None = None, replacement: str | None = None, warn_once: bool = True, category: Type[Warning] = DeprecationWarning, ) -> Callable[[Callable[P, R]], Callable[P, R]]: """ Decorator um Funktionen als deprecated zu markieren. Gibt bei jedem Aufruf eine Warnung aus (oder nur einmal, wenn warn_once=True). Args: message: Beschreibung warum deprecated. version: Version, in der deprecated wurde. removal_version: Version, in der entfernt wird. replacement: Empfohlene Alternative. warn_once: Ob nur einmal gewarnt werden soll. category: Warnungskategorie. Returns: Decorator-Funktion. Example: @deprecated( message="Diese Funktion ist veraltet", version="2.0.0", replacement="new_function()" ) def old_function(): pass @deprecated(removal_version="3.0.0") def soon_gone(): pass """ def decorator(func: Callable[P, R]) -> Callable[P, R]: name = func.__qualname__ # Info-Objekt erstellen info = DeprecationInfo( name=name, message=message, version=version, removal_version=removal_version, replacement=replacement, ) _deprecation_registry[name] = info warned = False @functools.wraps(func) def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: nonlocal warned info.call_count += 1 if not warn_once or not warned: warnings.warn( info.format_warning(), category=category, stacklevel=2, ) warned = True return func(*args, **kwargs) # Docstring aktualisieren if wrapper.__doc__: wrapper.__doc__ = f"**DEPRECATED**: {info.format_warning()}\n\n{wrapper.__doc__}" else: wrapper.__doc__ = f"**DEPRECATED**: {info.format_warning()}" # Info-Zugriff über die Funktion wrapper.deprecation_info = info # type: ignore return wrapper return decorator def pending_deprecation( message: str = "", version: str | None = None, replacement: str | None = None, warn_once: bool = True, ) -> Callable[[Callable[P, R]], Callable[P, R]]: """ Decorator für zukünftige Deprecation. Wie @deprecated, aber mit PendingDeprecationWarning. Gedacht für APIs, die noch nicht deprecated sind, aber bald sein werden. Args: message: Beschreibung der geplanten Änderung. version: Version, ab der deprecated sein wird. replacement: Empfohlene Alternative. warn_once: Ob nur einmal gewarnt werden soll. Returns: Decorator-Funktion. Example: @pending_deprecation( message="Wird in Version 3.0 deprecated", replacement="new_api()" ) def current_function(): pass """ return deprecated( message=f"Bald deprecated: {message}", version=version, replacement=replacement, warn_once=warn_once, category=PendingDeprecationWarning, ) def deprecated_argument( arg_name: str, message: str = "", replacement: str | None = None, warn_once: bool = True, ) -> Callable[[Callable[P, R]], Callable[P, R]]: """ Decorator um ein bestimmtes Argument als deprecated zu markieren. Warnt nur, wenn das deprecatete Argument tatsächlich verwendet wird. Args: arg_name: Name des deprecateten Arguments. message: Beschreibung warum deprecated. replacement: Empfohlenes Ersatzargument. warn_once: Ob nur einmal gewarnt werden soll. Returns: Decorator-Funktion. Example: @deprecated_argument("old_param", replacement="new_param") def my_function(new_param=None, old_param=None): if old_param is not None: new_param = old_param return new_param """ def decorator(func: Callable[P, R]) -> Callable[P, R]: warned = False @functools.wraps(func) def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: nonlocal warned if arg_name in kwargs: if not warn_once or not warned: warning_msg = ( f"Argument '{arg_name}' in {func.__qualname__} " f"ist deprecated" ) if message: warning_msg += f": {message}" if replacement: warning_msg += f". Verwende stattdessen: {replacement}" warnings.warn( warning_msg, DeprecationWarning, stacklevel=2, ) warned = True return func(*args, **kwargs) return wrapper return decorator def deprecated_class( message: str = "", version: str | None = None, removal_version: str | None = None, replacement: str | None = None, warn_once: bool = True, ) -> Callable[[Type[T]], Type[T]]: """ Decorator um eine Klasse als deprecated zu markieren. Warnt bei jeder Instanziierung. Args: message: Beschreibung warum deprecated. version: Version, in der deprecated wurde. removal_version: Version, in der entfernt wird. replacement: Empfohlene Ersatzklasse. warn_once: Ob nur einmal gewarnt werden soll. Returns: Decorator-Funktion. Example: @deprecated_class(replacement="NewClass") class OldClass: pass """ def decorator(cls: Type[T]) -> Type[T]: name = cls.__qualname__ info = DeprecationInfo( name=name, message=message, version=version, removal_version=removal_version, replacement=replacement, ) _deprecation_registry[name] = info original_init = cls.__init__ warned = False @functools.wraps(original_init) def new_init(self: T, *args: Any, **kwargs: Any) -> None: nonlocal warned info.call_count += 1 if not warn_once or not warned: warnings.warn( info.format_warning(), DeprecationWarning, stacklevel=2, ) warned = True original_init(self, *args, **kwargs) cls.__init__ = new_init # type: ignore # Docstring aktualisieren if cls.__doc__: cls.__doc__ = f"**DEPRECATED**: {info.format_warning()}\n\n{cls.__doc__}" else: cls.__doc__ = f"**DEPRECATED**: {info.format_warning()}" # Info-Zugriff über die Klasse cls.deprecation_info = info # type: ignore return cls return decorator def deprecated_alias( original: Callable[P, R], message: str = "", version: str | None = None, ) -> Callable[P, R]: """ Erstellt einen deprecateten Alias für eine Funktion. Nützlich für Umbenennungen, wo der alte Name noch eine Weile unterstützt werden soll. Args: original: Die originale (neue) Funktion. message: Optionale zusätzliche Nachricht. version: Version, in der der Alias deprecated wurde. Returns: Wrapper-Funktion. Example: def new_function(): return "result" # Alter Name als deprecateter Alias old_function = deprecated_alias( new_function, message="Wurde umbenannt zu new_function", version="2.0.0" ) """ @deprecated( message=message or f"Verwende {original.__qualname__} stattdessen", version=version, replacement=original.__qualname__, ) @functools.wraps(original) def alias(*args: P.args, **kwargs: P.kwargs) -> R: return original(*args, **kwargs) alias.__name__ = f"{original.__name__}_deprecated" return alias