Universal decorator (template)¶
pedros.decorators.universal_decorator is not part of the public API: it
isn't exported from pedros or from pedros.decorators. It exists as a
template for writing new decorators that need the same shape as
@timed, @safe, and @trace: usable bare or configured, transparent on
both sync and async functions, built on
wrapt so the wrapped function's
signature is preserved.
def universal_decorator(func: Callable[P, Any] | None = None) -> Any:
def decorator(wrapped_func: Callable[P, Any]) -> Callable[P, Any]:
@wrapt.decorator
def wrapper(
wrapped: Callable[P, Any],
instance: Any,
args: tuple[Any, ...],
kwargs: dict[str, Any],
) -> Any:
@contextlib.contextmanager
def _execute() -> Generator[None, None, None]:
try:
# 1) pre
yield
# 3) post (success)
except Exception:
# 4) error
raise
finally:
# 5) finally
pass
if inspect.iscoroutinefunction(wrapped):
async def _async_call() -> Any:
with _execute():
return await wrapped(*args, **kwargs)
return _async_call()
with _execute():
return wrapped(*args, **kwargs)
return cast(Callable[P, Any], wrapper(wrapped_func))
return decorator(func) if func is not None else decorator
The full file also carries @overload signatures for bare vs. configured,
sync vs. async usage, see src/pedros/decorators/universal_decorator.py
for those; they don't affect runtime behavior.
The hook points¶
Inside _execute, the numbered comments mark where your own decorator's
behavior goes:
- pre: runs before the wrapped call
- post (success): runs immediately after a successful call
- error: runs if the wrapped call raises; re-raises by default
- finally: always runs, success or failure
@timed fills in pre (start a timer) and finally (log the elapsed
time). @safe fills in error (log, invoke on_error, decide whether to
re-raise) and finally (invoke on_finally). @trace fills in pre (log
the call), post (log the return value), and error (log the exception,
always re-raising).
Building your own decorator from it¶
- Copy
universal_decorator.pyinto your own module and rename it. - Add whatever configuration parameters your decorator needs as keyword
arguments on the outer function (see
safe'scatch,log_level,on_error, ... for the pattern). - Fill in the hook(s) you need inside
_execute. - Leave the
inspect.iscoroutinefunctionbranch and thedecorator(func) if func is not None else decoratorreturn alone, that's what makes bare (@my_decorator) and configured (@my_decorator(...)) usage both work on sync and async functions.