Source code for stouputils.decorators.timeout


# Imports
from collections.abc import Callable
from functools import wraps
from typing import Any, overload

from ..typing import JsonList
from .common import get_function_name, get_wrapper_name, set_wrapper_name


# Decorator that raises an exception if the function runs too long
@overload
def timeout[T](
	func: Callable[..., T],
	*,
	seconds: float = 60.0,
	message: str = ""
) -> Callable[..., T]: ...

@overload
def timeout[T](
	func: None = None,
	*,
	seconds: float = 60.0,
	message: str = ""
) -> Callable[[Callable[..., T]], Callable[..., T]]: ...

[docs] def timeout[T]( func: Callable[..., T] | None = None, *, seconds: float = 60.0, message: str = "" ) -> Callable[..., T] | Callable[[Callable[..., T]], Callable[..., T]]: """ Decorator that raises a TimeoutError if the function runs longer than the specified timeout. Note: This decorator uses SIGALRM on Unix systems, which only works in the main thread. On Windows or in non-main threads, it will fall back to a polling-based approach. Args: func (Callable[..., T] | None): Function to apply timeout to seconds (float): Timeout duration in seconds (default: 60.0) message (str): Custom timeout message (default: "Function '{func_name}' timed out after {seconds} seconds") Raises: :py:exc:`TimeoutError`: If the function execution exceeds the timeout duration Examples: >>> import time >>> @timeout(seconds=2.0) ... def slow_function(): ... time.sleep(5) >>> slow_function() # Raises TimeoutError after 2 seconds Traceback (most recent call last): ... TimeoutError: Function 'slow_function()' timed out after 2.0 seconds >>> @timeout(seconds=1.0, message="Custom timeout message") ... def another_slow_function(): ... time.sleep(3) >>> another_slow_function() # Raises TimeoutError after 1 second Traceback (most recent call last): ... TimeoutError: Custom timeout message """ def decorator(func: Callable[..., T]) -> Callable[..., T]: # Check if we can use signal-based timeout (Unix only) import os use_signal: bool = os.name != "nt" # Not Windows if use_signal: try: import signal # Verify SIGALRM is available use_signal = hasattr(signal, 'SIGALRM') except ImportError: use_signal = False @wraps(func) def wrapper(*args: tuple[Any, ...], **kwargs: dict[str, Any]) -> Any: # Build timeout message msg: str = message if message else f"Function '{get_function_name(func)}()' timed out after {seconds} seconds" # Use signal-based timeout on Unix (main thread only) if use_signal: import signal import threading # Signal only works in main thread if threading.current_thread() is threading.main_thread(): def timeout_handler(signum: int, frame: Any) -> None: raise TimeoutError(msg) # Set the signal handler and alarm old_handler = signal.signal(signal.SIGALRM, timeout_handler) # type: ignore signal.setitimer(signal.ITIMER_REAL, seconds) # type: ignore try: result = func(*args, **kwargs) finally: # Cancel the alarm and restore the old handler signal.setitimer(signal.ITIMER_REAL, 0) # type: ignore signal.signal(signal.SIGALRM, old_handler) # type: ignore return result # Fall back to polling-based timeout (Windows or non-main thread) import threading result_container: JsonList = [] exception_container: list[BaseException] = [] def target() -> None: try: result_container.append(func(*args, **kwargs)) except BaseException as e_2: exception_container.append(e_2) thread = threading.Thread(target=target, daemon=True) thread.start() thread.join(timeout=seconds) if thread.is_alive(): # Thread is still running, timeout occurred raise TimeoutError(msg) # Check if an exception was raised in the thread if exception_container: raise exception_container[0] # Return the result if available if result_container: return result_container[0] set_wrapper_name(wrapper, get_wrapper_name("stouputils.decorators.timeout", func)) return wrapper # Handle both @timeout and @timeout(seconds=..., message=...) if func is None: return decorator return decorator(func)