-
Notifications
You must be signed in to change notification settings - Fork 85
/
_trait_event_notifier.py
219 lines (189 loc) · 8.03 KB
/
_trait_event_notifier.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# (C) Copyright 2005-2024 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!
from functools import partial
import types
import weakref
from traits.observation.exception_handling import handle_exception
from traits.observation.exceptions import NotifierNotFound
class TraitEventNotifier:
""" Wrapper for invoking user's handler for a trait change event.
An instance of ``TraitEventNotifier`` is a callable to be contributed
to an instance of ``IObserverable``, e.g. ``CTrait``, ``TraitList`` etc.,
such that it will be called when an observerable emits notificaitons for
changes. The call signature is defined by the observable object
and may vary. It is the responsibility of the ``event_factory`` to adapt
the varying call signatures and create an event object to be given
to the user's handler.
A ``TraitEventNotifier`` keeps a reference count in order to address
situations where a same object is repeated inside a container
and one would not want to fire the same change handler multiple times
(see enthought/traits#237). For that purpose, a notifier keeps track of
the ``HasTraits`` instance (called ``target``) on which the user applies
the observers, keeps a reference count internally, and it also needs to
determine whether another notifier refers to the same change handler and
``HasTraits`` instance.
Since there is only one reference count associated with a notifier,
each notifier is expected to be added to only one observable.
Parameters
----------
handler : callable(event)
The user's handler to receive the change event.
The event object is returned by the ``event_factory``.
If the handler is an instance method, then a weak reference is
created for the method. If the instance is garbage collected,
the notifier will be muted.
target : object
An object for defining the context of the notifier.
A weak reference is created for the target.
If the target is garbage collected, the notifier will be muted.
This target is typically an instance of ``HasTraits`` and will be
seen by the user as the "owner" of the change handler.
This is also used for distinguishing one notifier from another
notifier wrapping the same handler.
event_factory : callable(*args, **kwargs) -> object
A factory function for creating the event object to be sent to
the handler. The call signature must be compatible with the
call signature expected by the observable this notifier is used
with. e.g. for CTrait, the call signature will be
``(object, name, old, new)``.
prevent_event : callable(event) -> boolean
A callable for controlling whether the user handler should be
invoked. It receives the event created by the event factory and
returns true if the event should be prevented, false if the event
should be fired.
dispatcher : callable(handler, event)
A callable for dispatching the handler, e.g. on a different
thread or on a GUI event loop. ``event`` is the object
created by the event factory.
Raises
------
ValueError
If the handler given is not a callable.
"""
def __init__(
self, *, handler, target,
event_factory, prevent_event, dispatcher):
if not callable(handler):
raise ValueError(
"handler must be a callable, got {!r}".format(handler))
# This is such that the notifier does not prevent
# the target from being garbage collected.
self.target = weakref.ref(target)
if isinstance(handler, types.MethodType):
self.handler = weakref.WeakMethod(handler)
else:
self.handler = partial(_return, value=handler)
self.dispatcher = dispatcher
self.event_factory = event_factory
self.prevent_event = prevent_event
# Reference count to avoid adding multiple equivalent notifiers
# to the same observable.
self._ref_count = 0
def __call__(self, *args, **kwargs):
""" Called by observables. The call signature will vary and will be
handled by the event factory.
"""
if self.target() is None:
# target is deleted. The notifier is disabled.
return
# Hold onto the reference while invoking the handler
handler = self.handler()
if handler is None:
# The instance method is deleted. The notifier is disabled.
return
event = self.event_factory(*args, **kwargs)
if self.prevent_event(event):
return
try:
self.dispatcher(handler, event)
except Exception:
handle_exception(event)
def add_to(self, observable):
""" Add this notifier to an observable object.
If an equivalent notifier exists, the existing notifier's reference
count is bumped. Hence this method is not idempotent.
N number of calls to this ``add_to`` must be matched by N calls to the
``remove_from`` method in order to completely remove a notifier from
an observable.
Parameters
----------
observable : IObservable
An object for adding this notifier to.
Raises
------
RuntimeError
If the internal reference count is not zero and an equivalent
notifier is not found in the observable.
"""
notifiers = observable._notifiers(True)
for other in notifiers:
if self.equals(other):
other._ref_count += 1
break
else:
# It is not a current use case to share a notifier with multiple
# observables. Using a single reference count will tie the lifetime
# of the notifier to multiple objects.
if self._ref_count != 0:
raise RuntimeError(
"Sharing notifiers across observables is unexpected."
)
notifiers.append(self)
self._ref_count += 1
def remove_from(self, observable):
""" Remove this notifier from an observable object.
If an equivalent notifier exists, the existing notifier's reference
count is decremented and the notifier is only removed if
the count is reduced to zero.
Parameters
----------
observable : IObservable
An object for removing this notifier from.
Raises
------
RuntimeError
If the reference count becomes negative unexpectedly.
NotifierNotFound
If the notifier is not found.
"""
notifiers = observable._notifiers(True)
for other in notifiers[:]:
if self.equals(other):
if other._ref_count == 1:
notifiers.remove(other)
other._ref_count -= 1
if other._ref_count < 0:
raise RuntimeError(
"Reference count is negative. "
"Race condition?"
)
break
else:
raise NotifierNotFound("Notifier not found.")
def equals(self, other):
""" Return true if the other notifier is equivalent to this one.
Parameters
----------
other : any
Returns
-------
boolean
"""
if other is self:
return True
if type(other) is not type(self):
return False
return (
self.handler() == other.handler()
and self.target() is other.target()
and self.dispatcher == other.dispatcher
)
def _return(value):
return value