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        def __call__(self) -> OutT: ...
 89
 90    class _Call1Arg(Generic[In1T, OutT]):
 91        """Single argument variant of call wrapper."""
 92
 93        def __init__(self, _call: Callable[[In1T], OutT]): ...
 94
 95        def __call__(self, _arg1: In1T) -> OutT: ...
 96
 97    class _Call2Args(Generic[In1T, In2T, OutT]):
 98        """Two argument variant of call wrapper"""
 99
100        def __init__(self, _call: Callable[[In1T, In2T], OutT]): ...
101
102        def __call__(self, _arg1: In1T, _arg2: In2T) -> OutT: ...
103
104    class _Call3Args(Generic[In1T, In2T, In3T, OutT]):
105        """Three argument variant of call wrapper"""
106
107        def __init__(self, _call: Callable[[In1T, In2T, In3T], OutT]): ...
108
109        def __call__(self, _arg1: In1T, _arg2: In2T, _arg3: In3T) -> OutT: ...
110
111    class _Call4Args(Generic[In1T, In2T, In3T, In4T, OutT]):
112        """Four argument variant of call wrapper"""
113
114        def __init__(self, _call: Callable[[In1T, In2T, In3T, In4T], OutT]): ...
115
116        def __call__(
117            self, _arg1: In1T, _arg2: In2T, _arg3: In3T, _arg4: In4T
118        ) -> OutT: ...
119
120    class _Call5Args(Generic[In1T, In2T, In3T, In4T, In5T, OutT]):
121        """Five argument variant of call wrapper"""
122
123        def __init__(
124            self, _call: Callable[[In1T, In2T, In3T, In4T, In5T], OutT]
125        ): ...
126
127        def __call__(
128            self,
129            _arg1: In1T,
130            _arg2: In2T,
131            _arg3: In3T,
132            _arg4: In4T,
133            _arg5: In5T,
134        ) -> OutT: ...
135
136    class _Call6Args(Generic[In1T, In2T, In3T, In4T, In5T, In6T, OutT]):
137        """Six argument variant of call wrapper"""
138
139        def __init__(
140            self, _call: Callable[[In1T, In2T, In3T, In4T, In5T, In6T], OutT]
141        ): ...
142
143        def __call__(
144            self,
145            _arg1: In1T,
146            _arg2: In2T,
147            _arg3: In3T,
148            _arg4: In4T,
149            _arg5: In5T,
150            _arg6: In6T,
151        ) -> OutT: ...
152
153    class _Call7Args(Generic[In1T, In2T, In3T, In4T, In5T, In6T, In7T, OutT]):
154        """Seven argument variant of call wrapper"""
155
156        def __init__(
157            self,
158            _call: Callable[[In1T, In2T, In3T, In4T, In5T, In6T, In7T], OutT],
159        ): ...
160
161        def __call__(
162            self,
163            _arg1: In1T,
164            _arg2: In2T,
165            _arg3: In3T,
166            _arg4: In4T,
167            _arg5: In5T,
168            _arg6: In6T,
169            _arg7: In7T,
170        ) -> OutT: ...
171
172    # No arg call; no args bundled.
173    # noinspection PyPep8Naming
174    @overload
175    def Call(call: Callable[[], OutT]) -> _CallNoArgs[OutT]: ...
176
177    # 1 arg call; 1 arg bundled.
178    # noinspection PyPep8Naming
179    @overload
180    def Call(call: Callable[[In1T], OutT], arg1: In1T) -> _CallNoArgs[OutT]: ...
181
182    # 1 arg call; no args bundled.
183    # noinspection PyPep8Naming
184    @overload
185    def Call(call: Callable[[In1T], OutT]) -> _Call1Arg[In1T, OutT]: ...
186
187    # 2 arg call; 2 args bundled.
188    # noinspection PyPep8Naming
189    @overload
190    def Call(
191        call: Callable[[In1T, In2T], OutT], arg1: In1T, arg2: In2T
192    ) -> _CallNoArgs[OutT]: ...
193
194    # 2 arg call; 1 arg bundled.
195    # noinspection PyPep8Naming
196    @overload
197    def Call(
198        call: Callable[[In1T, In2T], OutT], arg1: In1T
199    ) -> _Call1Arg[In2T, OutT]: ...
200
201    # 2 arg call; no args bundled.
202    # noinspection PyPep8Naming
203    @overload
204    def Call(
205        call: Callable[[In1T, In2T], OutT]
206    ) -> _Call2Args[In1T, In2T, OutT]: ...
207
208    # 3 arg call; 3 args bundled.
209    # noinspection PyPep8Naming
210    @overload
211    def Call(
212        call: Callable[[In1T, In2T, In3T], OutT],
213        arg1: In1T,
214        arg2: In2T,
215        arg3: In3T,
216    ) -> _CallNoArgs[OutT]: ...
217
218    # 3 arg call; 2 args bundled.
219    # noinspection PyPep8Naming
220    @overload
221    def Call(
222        call: Callable[[In1T, In2T, In3T], OutT], arg1: In1T, arg2: In2T
223    ) -> _Call1Arg[In3T, OutT]: ...
224
225    # 3 arg call; 1 arg bundled.
226    # noinspection PyPep8Naming
227    @overload
228    def Call(
229        call: Callable[[In1T, In2T, In3T], OutT], arg1: In1T
230    ) -> _Call2Args[In2T, In3T, OutT]: ...
231
232    # 3 arg call; no args bundled.
233    # noinspection PyPep8Naming
234    @overload
235    def Call(
236        call: Callable[[In1T, In2T, In3T], OutT]
237    ) -> _Call3Args[In1T, In2T, In3T, OutT]: ...
238
239    # 4 arg call; 4 args bundled.
240    # noinspection PyPep8Naming
241    @overload
242    def Call(
243        call: Callable[[In1T, In2T, In3T, In4T], OutT],
244        arg1: In1T,
245        arg2: In2T,
246        arg3: In3T,
247        arg4: In4T,
248    ) -> _CallNoArgs[OutT]: ...
249
250    # 4 arg call; 3 args bundled.
251    # noinspection PyPep8Naming
252    @overload
253    def Call(
254        call: Callable[[In1T, In2T, In3T, In4T], OutT],
255        arg1: In1T,
256        arg2: In2T,
257        arg3: In3T,
258    ) -> _Call1Arg[In4T, OutT]: ...
259
260    # 4 arg call; 2 args bundled.
261    # noinspection PyPep8Naming
262    @overload
263    def Call(
264        call: Callable[[In1T, In2T, In3T, In4T], OutT],
265        arg1: In1T,
266        arg2: In2T,
267    ) -> _Call2Args[In3T, In4T, OutT]: ...
268
269    # 4 arg call; 1 arg bundled.
270    # noinspection PyPep8Naming
271    @overload
272    def Call(
273        call: Callable[[In1T, In2T, In3T, In4T], OutT],
274        arg1: In1T,
275    ) -> _Call3Args[In2T, In3T, In4T, OutT]: ...
276
277    # 4 arg call; no args bundled.
278    # noinspection PyPep8Naming
279    @overload
280    def Call(
281        call: Callable[[In1T, In2T, In3T, In4T], OutT],
282    ) -> _Call4Args[In1T, In2T, In3T, In4T, OutT]: ...
283
284    # 5 arg call; 5 args bundled.
285    # noinspection PyPep8Naming
286    @overload
287    def Call(
288        call: Callable[[In1T, In2T, In3T, In4T, In5T], OutT],
289        arg1: In1T,
290        arg2: In2T,
291        arg3: In3T,
292        arg4: In4T,
293        arg5: In5T,
294    ) -> _CallNoArgs[OutT]: ...
295
296    # 6 arg call; 6 args bundled.
297    # noinspection PyPep8Naming
298    @overload
299    def Call(
300        call: Callable[[In1T, In2T, In3T, In4T, In5T, In6T], OutT],
301        arg1: In1T,
302        arg2: In2T,
303        arg3: In3T,
304        arg4: In4T,
305        arg5: In5T,
306        arg6: In6T,
307    ) -> _CallNoArgs[OutT]: ...
308
309    # 7 arg call; 7 args bundled.
310    # noinspection PyPep8Naming
311    @overload
312    def Call(
313        call: Callable[[In1T, In2T, In3T, In4T, In5T, In6T, In7T], OutT],
314        arg1: In1T,
315        arg2: In2T,
316        arg3: In3T,
317        arg4: In4T,
318        arg5: In5T,
319        arg6: In6T,
320        arg7: In7T,
321    ) -> _CallNoArgs[OutT]: ...
322
323    # noinspection PyPep8Naming
324    def Call(*_args: Any, **_keywds: Any) -> Any: ...
325
326    # (Type-safe Partial)
327    # A convenient wrapper around functools.partial which adds type-safety
328    # (though it does not support keyword arguments).
329    tpartial = Call
330else:
331    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.

def tpartial(*_args: Any, **_keywds: Any) -> Any:
def Call(*_args: Any, **_keywds: Any) -> Any: