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
 7import weakref
 8import threading
 9from typing import TYPE_CHECKING, TypeVar, Generic
10
11T = TypeVar('T')
12
13if TYPE_CHECKING:
14    from typing import Callable
15
16
17class CallbackSet(Generic[T]):
18    """A simple way to manage a set of callbacks.
19
20    Any number of calls can be registered with a callback set. Each
21    registration results in a Registration object that can be used to
22    deregister the call from the set later. Callbacks are also
23    implicitly deregistered when an entry is deallocated, so make sure
24    to hold on to the return value when adding.
25
26    CallbackSet instances should be used from a single thread only
27    (this will be checked in debug mode).
28    """
29
30    def __init__(self) -> None:
31        self._entries: list[weakref.ref[CallbackRegistration[T]]] = []
32        self.thread: threading.Thread
33        if __debug__:
34            self.thread = threading.current_thread()
35
36    def register(self, call: T) -> CallbackRegistration[T]:
37        """Register a new callback."""
38        assert threading.current_thread() == self.thread
39
40        self._prune()
41
42        entry = CallbackRegistration(call, self)
43        self._entries.append(weakref.ref(entry))
44        return entry
45
46    def getcalls(self) -> list[T]:
47        """Return the current set of registered calls.
48
49        Note that this returns a flattened list of calls; generally this
50        should protect against calls which themselves add or remove
51        callbacks.
52        """
53        assert threading.current_thread() == self.thread
54
55        self._prune()
56
57        # Ignore calls that have been deallocated or explicitly
58        # deregistered.
59        entries = [e() for e in self._entries]
60        return [e.call for e in entries if e is not None and e.call is not None]
61
62    def _prune(self) -> None:
63
64        # Quick-out if all our entries are intact.
65        needs_prune = False
66        for entry in self._entries:
67            entrytarget = entry()
68            if entrytarget is None or entrytarget.call is None:
69                needs_prune = True
70                break
71        if not needs_prune:
72            return
73
74        # Ok; something needs pruning. Rebuild the entries list.
75        newentries: list[weakref.ref[CallbackRegistration[T]]] = []
76        for entry in self._entries:
77            entrytarget = entry()
78            if entrytarget is not None and entrytarget.call is not None:
79                newentries.append(entry)
80        self._entries = newentries
81
82
83class CallbackRegistration(Generic[T]):
84    """An entry for a callback set."""
85
86    def __init__(self, call: T, callbackset: CallbackSet[T]) -> None:
87        self.call: T | None = call
88        self.callbackset: CallbackSet[T] | None = callbackset
89
90    def deregister(self) -> None:
91        """Explicitly remove a callback from a CallbackSet."""
92        assert (
93            self.callbackset is None
94            or threading.current_thread() == self.callbackset.thread
95        )
96        # Simply clear the call to mark us as dead.
97        self.call = None
class CallbackSet(typing.Generic[~T]):
18class CallbackSet(Generic[T]):
19    """A simple way to manage a set of callbacks.
20
21    Any number of calls can be registered with a callback set. Each
22    registration results in a Registration object that can be used to
23    deregister the call from the set later. Callbacks are also
24    implicitly deregistered when an entry is deallocated, so make sure
25    to hold on to the return value when adding.
26
27    CallbackSet instances should be used from a single thread only
28    (this will be checked in debug mode).
29    """
30
31    def __init__(self) -> None:
32        self._entries: list[weakref.ref[CallbackRegistration[T]]] = []
33        self.thread: threading.Thread
34        if __debug__:
35            self.thread = threading.current_thread()
36
37    def register(self, call: T) -> CallbackRegistration[T]:
38        """Register a new callback."""
39        assert threading.current_thread() == self.thread
40
41        self._prune()
42
43        entry = CallbackRegistration(call, self)
44        self._entries.append(weakref.ref(entry))
45        return entry
46
47    def getcalls(self) -> list[T]:
48        """Return the current set of registered calls.
49
50        Note that this returns a flattened list of calls; generally this
51        should protect against calls which themselves add or remove
52        callbacks.
53        """
54        assert threading.current_thread() == self.thread
55
56        self._prune()
57
58        # Ignore calls that have been deallocated or explicitly
59        # deregistered.
60        entries = [e() for e in self._entries]
61        return [e.call for e in entries if e is not None and e.call is not None]
62
63    def _prune(self) -> None:
64
65        # Quick-out if all our entries are intact.
66        needs_prune = False
67        for entry in self._entries:
68            entrytarget = entry()
69            if entrytarget is None or entrytarget.call is None:
70                needs_prune = True
71                break
72        if not needs_prune:
73            return
74
75        # Ok; something needs pruning. Rebuild the entries list.
76        newentries: list[weakref.ref[CallbackRegistration[T]]] = []
77        for entry in self._entries:
78            entrytarget = entry()
79            if entrytarget is not None and entrytarget.call is not None:
80                newentries.append(entry)
81        self._entries = newentries

A simple way to manage a set of callbacks.

Any number of calls can be registered with a callback set. Each registration results in a Registration object that can be used to deregister the call from the set later. Callbacks are also implicitly deregistered when an entry is deallocated, so make sure to hold on to the return value when adding.

CallbackSet instances should be used from a single thread only (this will be checked in debug mode).

thread: threading.Thread
def register(self, call: ~T) -> CallbackRegistration[~T]:
37    def register(self, call: T) -> CallbackRegistration[T]:
38        """Register a new callback."""
39        assert threading.current_thread() == self.thread
40
41        self._prune()
42
43        entry = CallbackRegistration(call, self)
44        self._entries.append(weakref.ref(entry))
45        return entry

Register a new callback.

def getcalls(self) -> list[~T]:
47    def getcalls(self) -> list[T]:
48        """Return the current set of registered calls.
49
50        Note that this returns a flattened list of calls; generally this
51        should protect against calls which themselves add or remove
52        callbacks.
53        """
54        assert threading.current_thread() == self.thread
55
56        self._prune()
57
58        # Ignore calls that have been deallocated or explicitly
59        # deregistered.
60        entries = [e() for e in self._entries]
61        return [e.call for e in entries if e is not None and e.call is not None]

Return the current set of registered calls.

Note that this returns a flattened list of calls; generally this should protect against calls which themselves add or remove callbacks.

class CallbackRegistration(typing.Generic[~T]):
84class CallbackRegistration(Generic[T]):
85    """An entry for a callback set."""
86
87    def __init__(self, call: T, callbackset: CallbackSet[T]) -> None:
88        self.call: T | None = call
89        self.callbackset: CallbackSet[T] | None = callbackset
90
91    def deregister(self) -> None:
92        """Explicitly remove a callback from a CallbackSet."""
93        assert (
94            self.callbackset is None
95            or threading.current_thread() == self.callbackset.thread
96        )
97        # Simply clear the call to mark us as dead.
98        self.call = None

An entry for a callback set.

CallbackRegistration(call: ~T, callbackset: CallbackSet[~T])
87    def __init__(self, call: T, callbackset: CallbackSet[T]) -> None:
88        self.call: T | None = call
89        self.callbackset: CallbackSet[T] | None = callbackset
call: Optional[~T]
callbackset: Optional[CallbackSet[~T]]
def deregister(self) -> None:
91    def deregister(self) -> None:
92        """Explicitly remove a callback from a CallbackSet."""
93        assert (
94            self.callbackset is None
95            or threading.current_thread() == self.callbackset.thread
96        )
97        # Simply clear the call to mark us as dead.
98        self.call = None

Explicitly remove a callback from a CallbackSet.