Source code for gunz_utils.validation

# -*- coding: utf-8 -*-
"""
Validation utilities.

This module provides decorators and helpers to improve data validation
and error reporting, wrapping underlying libraries like Pydantic.
"""

import functools
import typing as t
from pydantic import validate_call, ValidationError

[docs] def type_checked(func: t.Optional[t.Callable] = None, **kwargs: t.Any) -> t.Callable: """ A wrapper around pydantic.validate_call that provides cleaner, user-friendly error messages. It catches `pydantic.ValidationError` and re-raises it as a `TypeError` with a formatted message indicating exactly which argument failed and why. Parameters ---------- func : Callable, optional The function to decorate. **kwargs : Additional arguments passed to `pydantic.validate_call` (e.g., `config`). Returns ------- Callable The decorated function with validation logic. Examples -------- >>> @type_checked ... def my_func(a: int): ... pass ... >>> my_func("string") TypeError: Validation error in 'my_func': Argument 'a': Input should be a valid integer, unable to parse string as an integer (got type 'str') """ def decorator(f: t.Callable) -> t.Callable: # Create the validated version of the function validated_func = validate_call(f, **kwargs) @functools.wraps(f) def wrapper(*args: t.Any, **kw: t.Any) -> t.Any: try: return validated_func(*args, **kw) except ValidationError as e: errors = [] for error in e.errors(): # Extract location: usually ('args', 0) or ('kwargs', 'arg_name') # We want to present a clean name to the user. loc = error.get("loc", ()) msg = error.get("msg", "Invalid input") input_val = error.get("input", "unknown") # Simplify location string # Remove 'args' or 'kwargs' if they are the first element clean_loc = [] for item in loc: if item not in ("args", "kwargs"): clean_loc.append(str(item)) loc_str = " -> ".join(clean_loc) if clean_loc else "input" # SECURITY: Do not leak the actual input value in the error message # as it might be sensitive (e.g., a password). # Instead, show the type of the input. input_type = type(input_val).__name__ errors.append(f"Argument '{loc_str}': {msg} (got type '{input_type}')") error_msg = f"Validation error in '{f.__name__}':\n" + "\n".join(errors) # Re-raise as TypeError to be more Pythonic for type issues, # effectively hiding the Pydantic trace from the end user unless they look closer. raise TypeError(error_msg) from None return wrapper if func: return decorator(func) return decorator