userver: en/testsuite/utils/callinfo.py Source File
Loading...
Searching...
No Matches
callinfo.py
1import asyncio
2import inspect
3import typing
4
5from testsuite.utils import cached_property, traceback
6
7
8class BaseError(Exception):
9 """Base exception class for this module."""
10
11
13 pass
14
15
17 """Call queue is empty error."""
18
19
21 """Timed out while waiting for call."""
22
23
24CheckerType = typing.Callable[[str], None]
25
26__tracebackhide__ = traceback.hide(BaseError)
27
28
30 """Function wrapper that puts information about function call into async
31 queue.
32
33 This class provides methods to wait/check function underlying function
34 calls.
35 """
36
37 def __init__(
38 self,
39 func: typing.Callable,
40 *,
41 name=None,
42 checker: CheckerType | None = None,
43 ):
44 self._func = func
45 self._name = name or func.__name__
46 self._checker = checker
47
48 @property
49 def func(self):
50 """Returns underlying function."""
51 return self._func
52
53 @cached_property
54 def _is_coro(self):
55 return inspect.iscoroutinefunction(self._func)
56
57 @cached_property
58 def _get_callinfo(self):
59 return callinfo(self._func)
60
61 @cached_property
62 def _queue(self) -> asyncio.Queue:
63 return asyncio.Queue()
64
65 def __repr__(self):
66 return f'<AsyncCallQueue: for {self._func!r}>'
67
68 async def __call__(self, *args, **kwargs):
69 """Call underlying function."""
70 try:
71 if self._is_coro:
72 return await self._func(*args, **kwargs)
73 return self._func(*args, **kwargs)
74 finally:
75 await self._queue.put((args, kwargs))
76
77 def flush(self) -> None:
78 """Clear call queue."""
79 self._queue = asyncio.Queue()
80
81 @property
82 def has_calls(self) -> bool:
83 """Returns ``True`` if call queue is not empty."""
84 self._check_callqueue('has_calls')
85 return self.times_called > 0
86
87 @property
88 def times_called(self) -> int:
89 """Returns call queue length."""
90 self._check_callqueue('times_called')
91 return self._queue.qsize()
92
93 def next_call(self) -> dict:
94 """Pops call from queue and return its arguments dict.
95
96 Raises ``CallQueueError`` if queue is empty
97 """
98 self._check_callqueue('next_call')
99 try:
100 return self._get_callinfo(*self._queue.get_nowait())
101 except asyncio.queues.QueueEmpty:
103 f'No calls for {self._name}() left in the queue',
104 ) from None
105
106 async def wait_call(self, timeout=10.0) -> dict:
107 """Wait for fucntion to be called. Pops call from queue. Blocks if
108 it's empty.
109
110 :param timeout: timeout in seconds
111
112 Raises ``CallQueueTimeoutError`` if queue is empty for ``timeout``
113 seconds.
114 """
115 self._check_callqueue('wait_call')
116 try:
117 item = await asyncio.wait_for(self._queue.get(), timeout=timeout)
118 return self._get_callinfo(*item)
119 except asyncio.TimeoutError:
121 f'Timeout while waiting for {self._name}() to be called',
122 ) from None
123
124 def _check_callqueue(self, caller):
125 if self._checker is not None:
126 self._checker(caller)
127
128
129def getfullargspec(func):
130 if isinstance(func, staticmethod):
131 func = func.__func__
132 func = getattr(func, '__wrapped__', func)
133 return inspect.getfullargspec(func)
134
135
136def callinfo(func):
137 func_spec = getfullargspec(func)
138 func_varkw = func_spec.varkw
139 func_kwonlyargs = func_spec.kwonlyargs
140 func_kwonlydefaults = func_spec.kwonlydefaults
141
142 func_args = func_spec.args
143 func_varargs = func_spec.varargs
144 defaults = func_spec.defaults or ()
145 func_defaults = dict(zip(func_args[-len(defaults) :], defaults))
146
147 def callinfo_getter(args, kwargs):
148 dct = dict(zip(func_args, args))
149 for argname in func_args[len(args) :]:
150 if argname in kwargs:
151 dct[argname] = kwargs[argname]
152 else:
153 dct[argname] = func_defaults.get(argname)
154 if func_varargs is not None:
155 dct[func_varargs] = args[len(dct) :]
156 for argname in func_kwonlyargs:
157 if argname in kwargs:
158 dct[argname] = kwargs[argname]
159 else:
160 dct[argname] = func_kwonlydefaults[argname]
161 if func_varkw is not None:
162 dct[func_varkw] = {k: v for k, v in kwargs.items() if k not in dct}
163 return dct
164
165 return callinfo_getter
166
167
168def acallqueue(
169 func: typing.Callable,
170 *,
171 checker: CheckerType | None = None,
172) -> AsyncCallQueue:
173 """Turn function into async call queue.
174
175 :param func: async or sync callable, can be decorated with @staticmethod
176 :param checker: optional function to check whether or not operation on
177 callqueue is possible
178 """
179 if isinstance(func, AsyncCallQueue):
180 return func
181 if isinstance(func, staticmethod):
182 func = func.__func__
183 name = None
184 if hasattr(func, '__name__'):
185 name = func.__name__
186 elif hasattr(func, '__call__'):
187 name = func.__class__.__name__
188 func = func.__call__
189 else:
190 raise RuntimeError(f'Unsupported func {func!r} given')
191 return AsyncCallQueue(func, name=name, checker=checker)