# -*- coding: utf-8 -*- """ Validation-Decorators für Parameter- und Rückgabewert-Prüfung. Bietet deklarative Validierung von Funktionsargumenten und Rückgabewerten zur Laufzeit. """ from __future__ import annotations import functools import inspect from dataclasses import dataclass from typing import Any, Callable, ParamSpec, Type, TypeVar, get_type_hints P = ParamSpec("P") R = TypeVar("R") class ValidationError(ValueError): """ Ausnahme für Validierungsfehler. Erweitert ValueError mit zusätzlichen Informationen über den fehlgeschlagenen Validator. """ def __init__( self, message: str, param_name: str | None = None, value: Any = None, validator: str | None = None, ) -> None: """ Initialisiert den ValidationError. Args: message: Fehlermeldung. param_name: Name des fehlerhaften Parameters. value: Der fehlerhafte Wert. validator: Name des fehlgeschlagenen Validators. """ super().__init__(message) self.param_name = param_name self.value = value self.validator = validator @dataclass class ValidatorSpec: """ Spezifikation für einen Validator. Definiert die Validierungslogik und Fehlermeldung für einen Parameter. """ name: str """Name des Validators.""" check: Callable[[Any], bool] """Funktion, die True zurückgibt wenn gültig.""" message: str = "" """Fehlermeldung bei ungültigem Wert.""" def validate(self, value: Any, param_name: str) -> None: """ Validiert einen Wert. Args: value: Zu validierender Wert. param_name: Parametername für Fehlermeldung. Raises: ValidationError: Wenn Validierung fehlschlägt. """ if not self.check(value): msg = self.message or f"Validierung '{self.name}' fehlgeschlagen" raise ValidationError( f"Parameter '{param_name}': {msg} (Wert: {value!r})", param_name=param_name, value=value, validator=self.name, ) # Vordefinierte Validatoren def not_none() -> ValidatorSpec: """Validator: Wert darf nicht None sein.""" return ValidatorSpec( name="not_none", check=lambda v: v is not None, message="Wert darf nicht None sein", ) def not_empty() -> ValidatorSpec: """Validator: Wert darf nicht leer sein.""" return ValidatorSpec( name="not_empty", check=lambda v: bool(v), message="Wert darf nicht leer sein", ) def min_length(length: int) -> ValidatorSpec: """Validator: Mindestlänge.""" return ValidatorSpec( name=f"min_length({length})", check=lambda v: len(v) >= length if hasattr(v, "__len__") else True, message=f"Mindestlänge ist {length}", ) def max_length(length: int) -> ValidatorSpec: """Validator: Maximallänge.""" return ValidatorSpec( name=f"max_length({length})", check=lambda v: len(v) <= length if hasattr(v, "__len__") else True, message=f"Maximallänge ist {length}", ) def in_range(min_val: float, max_val: float) -> ValidatorSpec: """Validator: Wert muss im Bereich liegen.""" return ValidatorSpec( name=f"in_range({min_val}, {max_val})", check=lambda v: min_val <= v <= max_val, message=f"Wert muss zwischen {min_val} und {max_val} liegen", ) def positive() -> ValidatorSpec: """Validator: Wert muss positiv sein.""" return ValidatorSpec( name="positive", check=lambda v: v > 0, message="Wert muss positiv sein", ) def non_negative() -> ValidatorSpec: """Validator: Wert darf nicht negativ sein.""" return ValidatorSpec( name="non_negative", check=lambda v: v >= 0, message="Wert darf nicht negativ sein", ) def matches_pattern(pattern: str) -> ValidatorSpec: """Validator: String muss Pattern matchen.""" import re compiled = re.compile(pattern) return ValidatorSpec( name=f"matches_pattern({pattern!r})", check=lambda v: bool(compiled.match(str(v))), message=f"Wert muss Pattern '{pattern}' entsprechen", ) def one_of(*values: Any) -> ValidatorSpec: """Validator: Wert muss einer der angegebenen Werte sein.""" return ValidatorSpec( name=f"one_of({values})", check=lambda v: v in values, message=f"Wert muss einer von {values} sein", ) def instance_of(*types: Type) -> ValidatorSpec: """Validator: Wert muss Instanz eines der Typen sein.""" return ValidatorSpec( name=f"instance_of({types})", check=lambda v: isinstance(v, types), message=f"Wert muss Instanz von {types} sein", ) def custom( check: Callable[[Any], bool], message: str = "Custom validation failed", name: str = "custom", ) -> ValidatorSpec: """Erstellt einen benutzerdefinierten Validator.""" return ValidatorSpec(name=name, check=check, message=message) def validate( **validators: ValidatorSpec | list[ValidatorSpec], ) -> Callable[[Callable[P, R]], Callable[P, R]]: """ Decorator für Parameter-Validierung. Args: **validators: Mapping von Parameternamen zu Validatoren. Returns: Decorator-Funktion. Example: @validate( name=not_empty(), age=in_range(0, 150), email=matches_pattern(r"^[^@]+@[^@]+$"), ) def create_user(name: str, age: int, email: str): pass @validate( values=[not_none(), min_length(1)] ) def process(values: list): pass """ def decorator(func: Callable[P, R]) -> Callable[P, R]: sig = inspect.signature(func) @functools.wraps(func) def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: # Argumente an Parameter binden bound = sig.bind(*args, **kwargs) bound.apply_defaults() # Validierung durchführen for param_name, specs in validators.items(): if param_name not in bound.arguments: continue value = bound.arguments[param_name] # Einzelnen Validator oder Liste verarbeiten if isinstance(specs, ValidatorSpec): specs = [specs] for spec in specs: spec.validate(value, param_name) return func(*args, **kwargs) return wrapper return decorator def validate_args( *validators: ValidatorSpec, ) -> Callable[[Callable[P, R]], Callable[P, R]]: """ Decorator für Positions-Argument-Validierung. Validiert alle Positionsargumente mit denselben Validatoren. Args: *validators: Validatoren für alle Positionsargumente. Returns: Decorator-Funktion. Example: @validate_args(not_none(), positive()) def sum_positive(*numbers): return sum(numbers) """ def decorator(func: Callable[P, R]) -> Callable[P, R]: @functools.wraps(func) def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: for i, value in enumerate(args): param_name = f"arg[{i}]" for spec in validators: spec.validate(value, param_name) return func(*args, **kwargs) return wrapper return decorator def validate_return( *validators: ValidatorSpec, ) -> Callable[[Callable[P, R]], Callable[P, R]]: """ Decorator für Rückgabewert-Validierung. Args: *validators: Validatoren für den Rückgabewert. Returns: Decorator-Funktion. Example: @validate_return(not_none(), min_length(1)) def get_data() -> list: return fetch_from_database() """ def decorator(func: Callable[P, R]) -> Callable[P, R]: @functools.wraps(func) def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: result = func(*args, **kwargs) for spec in validators: spec.validate(result, "return") return result return wrapper return decorator def validate_types( strict: bool = False, ) -> Callable[[Callable[P, R]], Callable[P, R]]: """ Decorator für Typ-Validierung basierend auf Type Hints. Prüft zur Laufzeit, ob die Argumente den deklarierten Typen entsprechen. Args: strict: Ob auch Subtypen abgelehnt werden sollen. Returns: Decorator-Funktion. Example: @validate_types() def process(name: str, count: int) -> str: return name * count """ def decorator(func: Callable[P, R]) -> Callable[P, R]: sig = inspect.signature(func) try: hints = get_type_hints(func) except Exception: hints = {} @functools.wraps(func) def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: bound = sig.bind(*args, **kwargs) bound.apply_defaults() # Argumente validieren for param_name, value in bound.arguments.items(): if param_name not in hints: continue expected_type = hints[param_name] # None-Check überspringen wenn Optional if value is None: # Einfache Prüfung - in Produktion würde man # typing.get_origin/get_args verwenden continue # Typ prüfen if strict: if type(value) is not expected_type: raise ValidationError( f"Parameter '{param_name}' muss exakt Typ " f"{expected_type.__name__} haben, ist aber " f"{type(value).__name__}", param_name=param_name, value=value, validator="type_check", ) else: if not isinstance(value, expected_type): raise ValidationError( f"Parameter '{param_name}' muss Typ " f"{expected_type.__name__} haben, ist aber " f"{type(value).__name__}", param_name=param_name, value=value, validator="type_check", ) result = func(*args, **kwargs) # Rückgabewert validieren if "return" in hints and result is not None: expected_return = hints["return"] if not isinstance(result, expected_return): raise ValidationError( f"Rückgabewert muss Typ {expected_return.__name__} " f"haben, ist aber {type(result).__name__}", param_name="return", value=result, validator="type_check", ) return result return wrapper return decorator