Source code for looptime._internal.enabler

import asyncio
import functools
import inspect
import warnings
from typing import Any, Callable, ContextManager, ParamSpec, TypeVar

from . import loops

P = ParamSpec('P')
R = TypeVar('R')


[docs] class enabled(ContextManager[None]): """ Enable the looptime time compaction temporarily. If used as a context manager, enables the time compaction for the wrapped code block only:: import asyncio import looptime async def main() -> None: with looptime.enabled(strict=True): await asyncio.sleep(10) if __name__ == '__main__': asuncio.run(main()) If used as a function/fixture decorator, enables the time compaction for the duration of the function/fixture:: import asyncio import looptime @looptime.enabled(strict=True) async def main() -> None: await asyncio.sleep(10) if __name__ == '__main__': asuncio.run(main()) In both cases, the event loop must be pre-patched (usually at creation). In strict mode, if the event loop is not patched, the call will fail. In non-strict mode (the default), it will issue a warning and continue with the real time flow (i.e. with no time compaction). Use it, for example, for fixtures or finalizers of fixtures where the fast time flow is required despite fixtures are normally excluded from the time compaction magic (because it is impossible or difficult to infer which event loop is being used in the multi-scoped setup of pytest-asyncio), and because of the structure of pytest hooks for fixture finalizing (no finalizer hook, only the post-finalizer hook, when it is too late). Beware of a caveat: if used as a decorator on a yield-based fixture, it will enable the looptime magic for the whole duration of the test, including all its fixtures (even undecorated ones), until the decorated fixture reaches its finalizer. This might have unexpected side effects. """ strict: bool _loop: asyncio.AbstractEventLoop | None _mgr: ContextManager[None] | None def __init__(self, *, strict: bool = False, loop: asyncio.AbstractEventLoop | None = None) -> None: super().__init__() self.strict = strict self._loop = loop self._mgr = None def __enter__(self) -> None: msg = "The running loop is not a looptime-patched loop, cannot enable it." loop = self._loop if self._loop is not None else asyncio.get_running_loop() if isinstance(loop, loops.LoopTimeEventLoop): self._mgr = loop.looptime_enabled() self._mgr.__enter__() elif self.strict: raise RuntimeError(msg) else: warnings.warn(msg, UserWarning) def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: if self._mgr is not None: self._mgr.__exit__(exc_type, exc_val, exc_tb) self._mgr = None # Type checkers: too complicated. We get R=Coroutine[Y,S,RR] for async functions, # but return that last RR part, which turns to be Any. The runtime is unaffected. # I don't know how to properly annotate such a mixed sync-async decorator internally. # The external declaration of __call__() is sufficient and correct. # TODO: LATER: try annotating it properly. def __call__(self, fn: Callable[P, R]) -> Callable[P, R]: if inspect.iscoroutinefunction(fn): @functools.wraps(fn) async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: nonlocal self with self: return await fn(*args, **kwargs) # type: ignore else: @functools.wraps(fn) def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: nonlocal self with self: return fn(*args, **kwargs) return wrapper # type: ignore