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

A simple way to manage a set of callbacks.

Any number of calls can be added to a callback set. Each add results in an entry that can be used to remove the call from the set later. Callbacks are also implicitly removed 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 add(self, call: ~T) -> CallbackRegistration[~T]:
36    def add(self, call: T) -> CallbackRegistration[T]:
37        """Add a 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

Add a callback.

def getcalls(self) -> list[~T]:
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 cleared.
58        entries = [e() for e in self._entries]
59        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]):
81class CallbackRegistration(Generic[T]):
82    """An entry for a callback set."""
83
84    def __init__(self, call: T, callbackset: CallbackSet[T]) -> None:
85        self.call: T | None = call
86        self.callbackset: CallbackSet[T] | None = callbackset
87
88    def clear(self) -> None:
89        """Explicitly remove a callback from a CallbackSet."""
90        assert (
91            self.callbackset is None
92            or threading.current_thread() == self.callbackset.thread
93        )
94        # Simply clear the call to mark us as dead.
95        self.call = None

An entry for a callback set.

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

Explicitly remove a callback from a CallbackSet.