efro.call

Call related functionality shared between all efro components.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Call related functionality shared between all efro components."""
  4
  5from __future__ import annotations
  6
  7from typing import TYPE_CHECKING, TypeVar, Generic, Callable, cast
  8import functools
  9
 10if TYPE_CHECKING:
 11    from typing import Any, overload
 12
 13CT = TypeVar('CT', bound=Callable)
 14
 15
 16class _CallbackCall(Generic[CT]):
 17    """Descriptor for exposing a call with a type defined by a TypeVar."""
 18
 19    def __get__(self, obj: Any, type_in: Any = None) -> CT:
 20        return cast(CT, None)
 21
 22
 23class CallbackSet(Generic[CT]):
 24    """Wrangles callbacks for a particular event in a type-safe manner."""
 25
 26    # In the type-checker's eyes, our 'run' attr is a CallbackCall which
 27    # returns a callable with the type we were created with. This lets us
 28    # type-check our run calls. (Is there another way to expose a function
 29    # with a signature defined by a generic?..)
 30    # At runtime, run() simply passes its args verbatim to its registered
 31    # callbacks; no types are checked.
 32    if TYPE_CHECKING:
 33        run: _CallbackCall[CT] = _CallbackCall()
 34    else:
 35
 36        def run(self, *args, **keywds):
 37            """Run all callbacks."""
 38            print('HELLO FROM RUN', *args, **keywds)
 39
 40    def __init__(self) -> None:
 41        print('CallbackSet()')
 42
 43    def __del__(self) -> None:
 44        print('~CallbackSet()')
 45
 46    def add(self, call: CT) -> None:
 47        """Add a callback to be run."""
 48        print('Would add call', call)
 49
 50
 51# Define Call() which can be used in type-checking call-wrappers that behave
 52# similarly to functools.partial (in that they take a callable and some
 53# positional arguments to be passed to it).
 54
 55# In type-checking land, We define several different _CallXArg classes
 56# corresponding to different argument counts and define Call() as an
 57# overloaded function which returns one of them based on how many args are
 58# passed.
 59
 60# To use this, simply assign your call type to this Call for type checking:
 61# Example:
 62#  class _MyCallWrapper:
 63#    <runtime class defined here>
 64#  if TYPE_CHECKING:
 65#    MyCallWrapper = efro.call.Call
 66#  else:
 67#    MyCallWrapper = _MyCallWrapper
 68
 69# Note that this setup currently only works with positional arguments; if you
 70# would like to pass args via keyword you can wrap a lambda or local function
 71# which takes keyword args and converts to a call containing keywords.
 72
 73if TYPE_CHECKING:
 74    In1T = TypeVar('In1T')
 75    In2T = TypeVar('In2T')
 76    In3T = TypeVar('In3T')
 77    In4T = TypeVar('In4T')
 78    In5T = TypeVar('In5T')
 79    In6T = TypeVar('In6T')
 80    In7T = TypeVar('In7T')
 81    OutT = TypeVar('OutT')
 82
 83    class _CallNoArgs(Generic[OutT]):
 84        """Single argument variant of call wrapper."""
 85
 86        def __init__(self, _call: Callable[[], OutT]):
 87            ...
 88
 89        def __call__(self) -> OutT:
 90            ...
 91
 92    class _Call1Arg(Generic[In1T, OutT]):
 93        """Single argument variant of call wrapper."""
 94
 95        def __init__(self, _call: Callable[[In1T], OutT]):
 96            ...
 97
 98        def __call__(self, _arg1: In1T) -> OutT:
 99            ...
100
101    class _Call2Args(Generic[In1T, In2T, OutT]):
102        """Two argument variant of call wrapper"""
103
104        def __init__(self, _call: Callable[[In1T, In2T], OutT]):
105            ...
106
107        def __call__(self, _arg1: In1T, _arg2: In2T) -> OutT:
108            ...
109
110    class _Call3Args(Generic[In1T, In2T, In3T, OutT]):
111        """Three argument variant of call wrapper"""
112
113        def __init__(self, _call: Callable[[In1T, In2T, In3T], OutT]):
114            ...
115
116        def __call__(self, _arg1: In1T, _arg2: In2T, _arg3: In3T) -> OutT:
117            ...
118
119    class _Call4Args(Generic[In1T, In2T, In3T, In4T, OutT]):
120        """Four argument variant of call wrapper"""
121
122        def __init__(self, _call: Callable[[In1T, In2T, In3T, In4T], OutT]):
123            ...
124
125        def __call__(
126            self, _arg1: In1T, _arg2: In2T, _arg3: In3T, _arg4: In4T
127        ) -> OutT:
128            ...
129
130    class _Call5Args(Generic[In1T, In2T, In3T, In4T, In5T, OutT]):
131        """Five argument variant of call wrapper"""
132
133        def __init__(
134            self, _call: Callable[[In1T, In2T, In3T, In4T, In5T], OutT]
135        ):
136            ...
137
138        def __call__(
139            self,
140            _arg1: In1T,
141            _arg2: In2T,
142            _arg3: In3T,
143            _arg4: In4T,
144            _arg5: In5T,
145        ) -> OutT:
146            ...
147
148    class _Call6Args(Generic[In1T, In2T, In3T, In4T, In5T, In6T, OutT]):
149        """Six argument variant of call wrapper"""
150
151        def __init__(
152            self, _call: Callable[[In1T, In2T, In3T, In4T, In5T, In6T], OutT]
153        ):
154            ...
155
156        def __call__(
157            self,
158            _arg1: In1T,
159            _arg2: In2T,
160            _arg3: In3T,
161            _arg4: In4T,
162            _arg5: In5T,
163            _arg6: In6T,
164        ) -> OutT:
165            ...
166
167    class _Call7Args(Generic[In1T, In2T, In3T, In4T, In5T, In6T, In7T, OutT]):
168        """Seven argument variant of call wrapper"""
169
170        def __init__(
171            self,
172            _call: Callable[[In1T, In2T, In3T, In4T, In5T, In6T, In7T], OutT],
173        ):
174            ...
175
176        def __call__(
177            self,
178            _arg1: In1T,
179            _arg2: In2T,
180            _arg3: In3T,
181            _arg4: In4T,
182            _arg5: In5T,
183            _arg6: In6T,
184            _arg7: In7T,
185        ) -> OutT:
186            ...
187
188    # No arg call; no args bundled.
189    # noinspection PyPep8Naming
190    @overload
191    def Call(call: Callable[[], OutT]) -> _CallNoArgs[OutT]:
192        ...
193
194    # 1 arg call; 1 arg bundled.
195    # noinspection PyPep8Naming
196    @overload
197    def Call(call: Callable[[In1T], OutT], arg1: In1T) -> _CallNoArgs[OutT]:
198        ...
199
200    # 1 arg call; no args bundled.
201    # noinspection PyPep8Naming
202    @overload
203    def Call(call: Callable[[In1T], OutT]) -> _Call1Arg[In1T, OutT]:
204        ...
205
206    # 2 arg call; 2 args bundled.
207    # noinspection PyPep8Naming
208    @overload
209    def Call(
210        call: Callable[[In1T, In2T], OutT], arg1: In1T, arg2: In2T
211    ) -> _CallNoArgs[OutT]:
212        ...
213
214    # 2 arg call; 1 arg bundled.
215    # noinspection PyPep8Naming
216    @overload
217    def Call(
218        call: Callable[[In1T, In2T], OutT], arg1: In1T
219    ) -> _Call1Arg[In2T, OutT]:
220        ...
221
222    # 2 arg call; no args bundled.
223    # noinspection PyPep8Naming
224    @overload
225    def Call(
226        call: Callable[[In1T, In2T], OutT]
227    ) -> _Call2Args[In1T, In2T, OutT]:
228        ...
229
230    # 3 arg call; 3 args bundled.
231    # noinspection PyPep8Naming
232    @overload
233    def Call(
234        call: Callable[[In1T, In2T, In3T], OutT],
235        arg1: In1T,
236        arg2: In2T,
237        arg3: In3T,
238    ) -> _CallNoArgs[OutT]:
239        ...
240
241    # 3 arg call; 2 args bundled.
242    # noinspection PyPep8Naming
243    @overload
244    def Call(
245        call: Callable[[In1T, In2T, In3T], OutT], arg1: In1T, arg2: In2T
246    ) -> _Call1Arg[In3T, OutT]:
247        ...
248
249    # 3 arg call; 1 arg bundled.
250    # noinspection PyPep8Naming
251    @overload
252    def Call(
253        call: Callable[[In1T, In2T, In3T], OutT], arg1: In1T
254    ) -> _Call2Args[In2T, In3T, OutT]:
255        ...
256
257    # 3 arg call; no args bundled.
258    # noinspection PyPep8Naming
259    @overload
260    def Call(
261        call: Callable[[In1T, In2T, In3T], OutT]
262    ) -> _Call3Args[In1T, In2T, In3T, OutT]:
263        ...
264
265    # 4 arg call; 4 args bundled.
266    # noinspection PyPep8Naming
267    @overload
268    def Call(
269        call: Callable[[In1T, In2T, In3T, In4T], OutT],
270        arg1: In1T,
271        arg2: In2T,
272        arg3: In3T,
273        arg4: In4T,
274    ) -> _CallNoArgs[OutT]:
275        ...
276
277    # 4 arg call; 3 args bundled.
278    # noinspection PyPep8Naming
279    @overload
280    def Call(
281        call: Callable[[In1T, In2T, In3T, In4T], OutT],
282        arg1: In1T,
283        arg2: In2T,
284        arg3: In3T,
285    ) -> _Call1Arg[In4T, OutT]:
286        ...
287
288    # 4 arg call; 2 args bundled.
289    # noinspection PyPep8Naming
290    @overload
291    def Call(
292        call: Callable[[In1T, In2T, In3T, In4T], OutT],
293        arg1: In1T,
294        arg2: In2T,
295    ) -> _Call2Args[In3T, In4T, OutT]:
296        ...
297
298    # 4 arg call; 1 arg bundled.
299    # noinspection PyPep8Naming
300    @overload
301    def Call(
302        call: Callable[[In1T, In2T, In3T, In4T], OutT],
303        arg1: In1T,
304    ) -> _Call3Args[In2T, In3T, In4T, OutT]:
305        ...
306
307    # 4 arg call; no args bundled.
308    # noinspection PyPep8Naming
309    @overload
310    def Call(
311        call: Callable[[In1T, In2T, In3T, In4T], OutT],
312    ) -> _Call4Args[In1T, In2T, In3T, In4T, OutT]:
313        ...
314
315    # 5 arg call; 5 args bundled.
316    # noinspection PyPep8Naming
317    @overload
318    def Call(
319        call: Callable[[In1T, In2T, In3T, In4T, In5T], OutT],
320        arg1: In1T,
321        arg2: In2T,
322        arg3: In3T,
323        arg4: In4T,
324        arg5: In5T,
325    ) -> _CallNoArgs[OutT]:
326        ...
327
328    # 6 arg call; 6 args bundled.
329    # noinspection PyPep8Naming
330    @overload
331    def Call(
332        call: Callable[[In1T, In2T, In3T, In4T, In5T, In6T], OutT],
333        arg1: In1T,
334        arg2: In2T,
335        arg3: In3T,
336        arg4: In4T,
337        arg5: In5T,
338        arg6: In6T,
339    ) -> _CallNoArgs[OutT]:
340        ...
341
342    # 7 arg call; 7 args bundled.
343    # noinspection PyPep8Naming
344    @overload
345    def Call(
346        call: Callable[[In1T, In2T, In3T, In4T, In5T, In6T, In7T], OutT],
347        arg1: In1T,
348        arg2: In2T,
349        arg3: In3T,
350        arg4: In4T,
351        arg5: In5T,
352        arg6: In6T,
353        arg7: In7T,
354    ) -> _CallNoArgs[OutT]:
355        ...
356
357    # noinspection PyPep8Naming
358    def Call(*_args: Any, **_keywds: Any) -> Any:
359        ...
360
361    # (Type-safe Partial)
362    # A convenient wrapper around functools.partial which adds type-safety
363    # (though it does not support keyword arguments).
364    tpartial = Call
365else:
366    tpartial = functools.partial
class CallbackSet(typing.Generic[~CT]):
24class CallbackSet(Generic[CT]):
25    """Wrangles callbacks for a particular event in a type-safe manner."""
26
27    # In the type-checker's eyes, our 'run' attr is a CallbackCall which
28    # returns a callable with the type we were created with. This lets us
29    # type-check our run calls. (Is there another way to expose a function
30    # with a signature defined by a generic?..)
31    # At runtime, run() simply passes its args verbatim to its registered
32    # callbacks; no types are checked.
33    if TYPE_CHECKING:
34        run: _CallbackCall[CT] = _CallbackCall()
35    else:
36
37        def run(self, *args, **keywds):
38            """Run all callbacks."""
39            print('HELLO FROM RUN', *args, **keywds)
40
41    def __init__(self) -> None:
42        print('CallbackSet()')
43
44    def __del__(self) -> None:
45        print('~CallbackSet()')
46
47    def add(self, call: CT) -> None:
48        """Add a callback to be run."""
49        print('Would add call', call)

Wrangles callbacks for a particular event in a type-safe manner.

def add(self, call: ~CT) -> None:
47    def add(self, call: CT) -> None:
48        """Add a callback to be run."""
49        print('Would add call', call)

Add a callback to be run.

def run(self, *args, **keywds):
37        def run(self, *args, **keywds):
38            """Run all callbacks."""
39            print('HELLO FROM RUN', *args, **keywds)

Run all callbacks.