Source code for countryflag.utils.validation

"""
Validation utility functions for the countryflag package.

This module contains utility functions for validating inputs and outputs.
"""

import functools
import logging
from typing import Any, Callable, TypeVar

import typeguard

# Configure logging
logger = logging.getLogger("countryflag.utils.validation")

# Type variable for generic functions
T = TypeVar("T")


[docs] def require(predicate: Callable[..., bool], message: str = "Precondition failed"): """ Decorator to enforce preconditions using design by contract. Args: predicate: A function that takes the same arguments as the decorated function and returns True if the precondition is satisfied. message: The error message to raise if the precondition fails. Returns: The decorated function. Raises: ValueError: If the precondition is not satisfied. Example: >>> @require(lambda x: x > 0, "x must be positive") ... def sqrt(x): ... return x ** 0.5 >>> sqrt(4) 2.0 >>> sqrt(-1) # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: x must be positive in sqrt """ def decorator(func: Callable[..., T]) -> Callable[..., T]: @functools.wraps(func) def wrapper(*args: Any, **kwargs: Any) -> T: if not predicate(*args, **kwargs): raise ValueError(f"{message} in {func.__name__}") return func(*args, **kwargs) return wrapper return decorator
[docs] def ensure(predicate: Callable[[T], bool], message: str = "Postcondition failed"): """ Decorator to enforce postconditions using design by contract. Args: predicate: A function that takes the return value of the decorated function and returns True if the postcondition is satisfied. message: The error message to raise if the postcondition fails. Returns: The decorated function with postcondition checking. Raises: ValueError: If the postcondition is not satisfied. Example: >>> @ensure(lambda result: result >= 0, "result must be non-negative") ... def abs(x): ... return -x # Bug: should be abs(x) >>> abs(1) # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: result must be non-negative in abs """ def decorator(func: Callable[..., T]) -> Callable[..., T]: @functools.wraps(func) def wrapper(*args: Any, **kwargs: Any) -> T: result = func(*args, **kwargs) if not predicate(result): raise ValueError(f"{message} in {func.__name__}") return result return wrapper return decorator
[docs] def runtime_typechecked(func: Callable[..., T]) -> Callable[..., T]: """ Decorator to add runtime type checking to a function. Args: func: The function to decorate. Returns: The decorated function with runtime type checking. Raises: TypeError: If the arguments or return value do not match the type annotations. Example: >>> @runtime_typechecked ... def add(x: int, y: int) -> int: ... return x + y >>> add(1, 2) 3 >>> add("1", 2) # doctest: +ELLIPSIS Traceback (most recent call last): ... TypeError: ... """ @functools.wraps(func) def wrapper(*args: Any, **kwargs: Any) -> T: try: return typeguard.typechecked(func)(*args, **kwargs) except typeguard.TypeCheckError as e: # Convert TypeCheckError to TypeError for consistency raise TypeError(str(e)) from e return wrapper