validation.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. # -*- coding: utf-8 -*-
  2. """
  3. Validation-Decorators für Parameter- und Rückgabewert-Prüfung.
  4. Bietet deklarative Validierung von Funktionsargumenten und
  5. Rückgabewerten zur Laufzeit.
  6. """
  7. from __future__ import annotations
  8. import functools
  9. import inspect
  10. from dataclasses import dataclass
  11. from typing import Any, Callable, ParamSpec, Type, TypeVar, get_type_hints
  12. P = ParamSpec("P")
  13. R = TypeVar("R")
  14. class ValidationError(ValueError):
  15. """
  16. Ausnahme für Validierungsfehler.
  17. Erweitert ValueError mit zusätzlichen Informationen
  18. über den fehlgeschlagenen Validator.
  19. """
  20. def __init__(
  21. self,
  22. message: str,
  23. param_name: str | None = None,
  24. value: Any = None,
  25. validator: str | None = None,
  26. ) -> None:
  27. """
  28. Initialisiert den ValidationError.
  29. Args:
  30. message: Fehlermeldung.
  31. param_name: Name des fehlerhaften Parameters.
  32. value: Der fehlerhafte Wert.
  33. validator: Name des fehlgeschlagenen Validators.
  34. """
  35. super().__init__(message)
  36. self.param_name = param_name
  37. self.value = value
  38. self.validator = validator
  39. @dataclass
  40. class ValidatorSpec:
  41. """
  42. Spezifikation für einen Validator.
  43. Definiert die Validierungslogik und Fehlermeldung
  44. für einen Parameter.
  45. """
  46. name: str
  47. """Name des Validators."""
  48. check: Callable[[Any], bool]
  49. """Funktion, die True zurückgibt wenn gültig."""
  50. message: str = ""
  51. """Fehlermeldung bei ungültigem Wert."""
  52. def validate(self, value: Any, param_name: str) -> None:
  53. """
  54. Validiert einen Wert.
  55. Args:
  56. value: Zu validierender Wert.
  57. param_name: Parametername für Fehlermeldung.
  58. Raises:
  59. ValidationError: Wenn Validierung fehlschlägt.
  60. """
  61. if not self.check(value):
  62. msg = self.message or f"Validierung '{self.name}' fehlgeschlagen"
  63. raise ValidationError(
  64. f"Parameter '{param_name}': {msg} (Wert: {value!r})",
  65. param_name=param_name,
  66. value=value,
  67. validator=self.name,
  68. )
  69. # Vordefinierte Validatoren
  70. def not_none() -> ValidatorSpec:
  71. """Validator: Wert darf nicht None sein."""
  72. return ValidatorSpec(
  73. name="not_none",
  74. check=lambda v: v is not None,
  75. message="Wert darf nicht None sein",
  76. )
  77. def not_empty() -> ValidatorSpec:
  78. """Validator: Wert darf nicht leer sein."""
  79. return ValidatorSpec(
  80. name="not_empty",
  81. check=lambda v: bool(v),
  82. message="Wert darf nicht leer sein",
  83. )
  84. def min_length(length: int) -> ValidatorSpec:
  85. """Validator: Mindestlänge."""
  86. return ValidatorSpec(
  87. name=f"min_length({length})",
  88. check=lambda v: len(v) >= length if hasattr(v, "__len__") else True,
  89. message=f"Mindestlänge ist {length}",
  90. )
  91. def max_length(length: int) -> ValidatorSpec:
  92. """Validator: Maximallänge."""
  93. return ValidatorSpec(
  94. name=f"max_length({length})",
  95. check=lambda v: len(v) <= length if hasattr(v, "__len__") else True,
  96. message=f"Maximallänge ist {length}",
  97. )
  98. def in_range(min_val: float, max_val: float) -> ValidatorSpec:
  99. """Validator: Wert muss im Bereich liegen."""
  100. return ValidatorSpec(
  101. name=f"in_range({min_val}, {max_val})",
  102. check=lambda v: min_val <= v <= max_val,
  103. message=f"Wert muss zwischen {min_val} und {max_val} liegen",
  104. )
  105. def positive() -> ValidatorSpec:
  106. """Validator: Wert muss positiv sein."""
  107. return ValidatorSpec(
  108. name="positive",
  109. check=lambda v: v > 0,
  110. message="Wert muss positiv sein",
  111. )
  112. def non_negative() -> ValidatorSpec:
  113. """Validator: Wert darf nicht negativ sein."""
  114. return ValidatorSpec(
  115. name="non_negative",
  116. check=lambda v: v >= 0,
  117. message="Wert darf nicht negativ sein",
  118. )
  119. def matches_pattern(pattern: str) -> ValidatorSpec:
  120. """Validator: String muss Pattern matchen."""
  121. import re
  122. compiled = re.compile(pattern)
  123. return ValidatorSpec(
  124. name=f"matches_pattern({pattern!r})",
  125. check=lambda v: bool(compiled.match(str(v))),
  126. message=f"Wert muss Pattern '{pattern}' entsprechen",
  127. )
  128. def one_of(*values: Any) -> ValidatorSpec:
  129. """Validator: Wert muss einer der angegebenen Werte sein."""
  130. return ValidatorSpec(
  131. name=f"one_of({values})",
  132. check=lambda v: v in values,
  133. message=f"Wert muss einer von {values} sein",
  134. )
  135. def instance_of(*types: Type) -> ValidatorSpec:
  136. """Validator: Wert muss Instanz eines der Typen sein."""
  137. return ValidatorSpec(
  138. name=f"instance_of({types})",
  139. check=lambda v: isinstance(v, types),
  140. message=f"Wert muss Instanz von {types} sein",
  141. )
  142. def custom(
  143. check: Callable[[Any], bool],
  144. message: str = "Custom validation failed",
  145. name: str = "custom",
  146. ) -> ValidatorSpec:
  147. """Erstellt einen benutzerdefinierten Validator."""
  148. return ValidatorSpec(name=name, check=check, message=message)
  149. def validate(
  150. **validators: ValidatorSpec | list[ValidatorSpec],
  151. ) -> Callable[[Callable[P, R]], Callable[P, R]]:
  152. """
  153. Decorator für Parameter-Validierung.
  154. Args:
  155. **validators: Mapping von Parameternamen zu Validatoren.
  156. Returns:
  157. Decorator-Funktion.
  158. Example:
  159. @validate(
  160. name=not_empty(),
  161. age=in_range(0, 150),
  162. email=matches_pattern(r"^[^@]+@[^@]+$"),
  163. )
  164. def create_user(name: str, age: int, email: str):
  165. pass
  166. @validate(
  167. values=[not_none(), min_length(1)]
  168. )
  169. def process(values: list):
  170. pass
  171. """
  172. def decorator(func: Callable[P, R]) -> Callable[P, R]:
  173. sig = inspect.signature(func)
  174. @functools.wraps(func)
  175. def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
  176. # Argumente an Parameter binden
  177. bound = sig.bind(*args, **kwargs)
  178. bound.apply_defaults()
  179. # Validierung durchführen
  180. for param_name, specs in validators.items():
  181. if param_name not in bound.arguments:
  182. continue
  183. value = bound.arguments[param_name]
  184. # Einzelnen Validator oder Liste verarbeiten
  185. if isinstance(specs, ValidatorSpec):
  186. specs = [specs]
  187. for spec in specs:
  188. spec.validate(value, param_name)
  189. return func(*args, **kwargs)
  190. return wrapper
  191. return decorator
  192. def validate_args(
  193. *validators: ValidatorSpec,
  194. ) -> Callable[[Callable[P, R]], Callable[P, R]]:
  195. """
  196. Decorator für Positions-Argument-Validierung.
  197. Validiert alle Positionsargumente mit denselben Validatoren.
  198. Args:
  199. *validators: Validatoren für alle Positionsargumente.
  200. Returns:
  201. Decorator-Funktion.
  202. Example:
  203. @validate_args(not_none(), positive())
  204. def sum_positive(*numbers):
  205. return sum(numbers)
  206. """
  207. def decorator(func: Callable[P, R]) -> Callable[P, R]:
  208. @functools.wraps(func)
  209. def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
  210. for i, value in enumerate(args):
  211. param_name = f"arg[{i}]"
  212. for spec in validators:
  213. spec.validate(value, param_name)
  214. return func(*args, **kwargs)
  215. return wrapper
  216. return decorator
  217. def validate_return(
  218. *validators: ValidatorSpec,
  219. ) -> Callable[[Callable[P, R]], Callable[P, R]]:
  220. """
  221. Decorator für Rückgabewert-Validierung.
  222. Args:
  223. *validators: Validatoren für den Rückgabewert.
  224. Returns:
  225. Decorator-Funktion.
  226. Example:
  227. @validate_return(not_none(), min_length(1))
  228. def get_data() -> list:
  229. return fetch_from_database()
  230. """
  231. def decorator(func: Callable[P, R]) -> Callable[P, R]:
  232. @functools.wraps(func)
  233. def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
  234. result = func(*args, **kwargs)
  235. for spec in validators:
  236. spec.validate(result, "return")
  237. return result
  238. return wrapper
  239. return decorator
  240. def validate_types(
  241. strict: bool = False,
  242. ) -> Callable[[Callable[P, R]], Callable[P, R]]:
  243. """
  244. Decorator für Typ-Validierung basierend auf Type Hints.
  245. Prüft zur Laufzeit, ob die Argumente den deklarierten
  246. Typen entsprechen.
  247. Args:
  248. strict: Ob auch Subtypen abgelehnt werden sollen.
  249. Returns:
  250. Decorator-Funktion.
  251. Example:
  252. @validate_types()
  253. def process(name: str, count: int) -> str:
  254. return name * count
  255. """
  256. def decorator(func: Callable[P, R]) -> Callable[P, R]:
  257. sig = inspect.signature(func)
  258. try:
  259. hints = get_type_hints(func)
  260. except Exception:
  261. hints = {}
  262. @functools.wraps(func)
  263. def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
  264. bound = sig.bind(*args, **kwargs)
  265. bound.apply_defaults()
  266. # Argumente validieren
  267. for param_name, value in bound.arguments.items():
  268. if param_name not in hints:
  269. continue
  270. expected_type = hints[param_name]
  271. # None-Check überspringen wenn Optional
  272. if value is None:
  273. # Einfache Prüfung - in Produktion würde man
  274. # typing.get_origin/get_args verwenden
  275. continue
  276. # Typ prüfen
  277. if strict:
  278. if type(value) is not expected_type:
  279. raise ValidationError(
  280. f"Parameter '{param_name}' muss exakt Typ "
  281. f"{expected_type.__name__} haben, ist aber "
  282. f"{type(value).__name__}",
  283. param_name=param_name,
  284. value=value,
  285. validator="type_check",
  286. )
  287. else:
  288. if not isinstance(value, expected_type):
  289. raise ValidationError(
  290. f"Parameter '{param_name}' muss Typ "
  291. f"{expected_type.__name__} haben, ist aber "
  292. f"{type(value).__name__}",
  293. param_name=param_name,
  294. value=value,
  295. validator="type_check",
  296. )
  297. result = func(*args, **kwargs)
  298. # Rückgabewert validieren
  299. if "return" in hints and result is not None:
  300. expected_return = hints["return"]
  301. if not isinstance(result, expected_return):
  302. raise ValidationError(
  303. f"Rückgabewert muss Typ {expected_return.__name__} "
  304. f"haben, ist aber {type(result).__name__}",
  305. param_name="return",
  306. value=result,
  307. validator="type_check",
  308. )
  309. return result
  310. return wrapper
  311. return decorator