-
Notifications
You must be signed in to change notification settings - Fork 3
/
obsub.py
222 lines (168 loc) · 6.92 KB
/
obsub.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
220
221
222
'''
This is an implementation of the observer pattern. It uses function
decorators to achieve the desired event registration mechanism.
For further reference, see
http://en.wikipedia.org/wiki/Observer_pattern
The idea is based on this thread:
http://stackoverflow.com/questions/1904351/python-observer-pattern-examples-tips
'''
import functools
import inspect
try:
# use python3 signatures if available
# this takes care of enforcing the correct signature at call time and
# provides the correct default arguments
from inspect import signature
except ImportError: # pragma: no cover
# python2 has no support for signatures
def signature(fn):
return None
__all__ = ['event']
__version__ = '0.2'
class event(object):
'''
This class serves as a utility to decorate a function as an event.
The following example demonstrates its functionality in an abstract way.
A class method can be decorated as follows:
>>> class A(object):
... def __init__(self, name):
... self.name = name
...
... @event
... def progress(self, first, second):
... print("Doing something...")
A.progress is the event. It is triggered after executing the code in the
decorated progress routine.
Now that we have a class with some event, let's create an event handler.
>>> def handler(self, first, second):
... print("%s %s and %s!" % (first, self.name, second))
Note that the handler (and signal calls) must have the signature defined
by the decorated event method.
This handler only greets the object that triggered the event by using its
name attribute. Let's create some instances of A and register our new
event handler to their progress event.
>>> a = A("Foo")
>>> b = A("Bar")
>>> a.progress += handler
>>> b.progress += handler
Now everything has been setup. When we call the method, the event will be
triggered:
>>> a.progress("Hello", "World")
Doing something...
Hello Foo and World!
>>> b.progress(second="Others", first="Hi")
Doing something...
Hi Bar and Others!
What happens if we disobey the call signature?
>>> c = A("World")
>>> c.progress(second="World") # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
TypeError: progress() missing 1 required positional argument: 'first'
Class based access is possible as well:
>>> A.progress(a, "Hello", "Y")
Doing something...
Hello Foo and Y!
Bound methods keep the instance alive:
>>> f = a.progress
>>> import weakref, gc
>>> wr = weakref.ref(a)
>>> del a
>>> c=gc.collect()
>>> assert wr() is not None
>>> f("Hi", "Z")
Doing something...
Hi Foo and Z!
If we delete the hard reference to the bound method and run the garbage
collector (to make sure it is run at all), the object will be gone:
>>> del f
>>> c=gc.collect()
>>> assert wr() is None
'''
def __init__(self, function):
'''
Constructor.
* function -- The function to be wrapped by the decorator.
'''
# Copy docstring and other attributes from function
functools.update_wrapper(self, function)
self.__signature__ = signature(function)
# Used to enforce call signature even when no slot is connected.
# Can also execute code (called before handlers)
self.__function = function
def __set__(self, instance, value):
'''
This is a NOP preventing that a boundevent instance is stored.
This prevents operations like `a.progress += handler` to have
side effects that result in a cyclic reference.
http://stackoverflow.com/questions/18287336/memory-leak-when-invoking-iadd-via-get-without-using-temporary
'''
pass
def __get__(self, instance, owner):
'''
Overloaded __get__ method. Defines the object resulting from
a method/function decorated with @event.
See http://docs.python.org/reference/datamodel.html?highlight=__get__#object.__get__
for a detailed explanation of what this special method usually does.
* instance -- The instance of event invoked.
* owner -- The owner class.
'''
# this case corresponds to access via the owner class:
if instance is None:
@functools.wraps(self.__function)
def wrapper(instance, *args, **kwargs):
return self.__get__(instance, owner)(*args, **kwargs)
else:
wrapper = functools.wraps(self.__function)(boundevent(instance, self.__function))
wrapper.__signature__ = self.__signature__
return wrapper
class boundevent(object):
'''Private helper class for event system.'''
def __init__(self, instance, function):
'''
Constructor.
* instance -- the instance whose member the event is
'''
self.instance = instance
self.__function = function
self.__key = ' ' + function.__name__
@property
def __event_handlers(self):
if self.__key not in self.instance.__dict__:
self.instance.__dict__[self.__key] = []
return self.instance.__dict__[self.__key]
def __iadd__(self, function):
'''
Overloaded += operator. It registers event handlers to the event.
* function -- The right-hand-side argument of the operator; this is the
event handling function that registers to the event.
'''
# Add the function as a new event handler
self.__event_handlers.append(function)
# Return the boundevent instance itself for coherent syntax behaviour
return self
def __isub__(self, function):
'''
Overloaded -= operator. It removes registered event handlers from
the event.
* function -- The right-hand-side argument of the operator; this is the
function that needs to be removed from the list of event handlers.
'''
# Remove the function from the list of registered event handlers
self.__event_handlers.remove(function)
# Return the boundevent instance itself for coherent syntax behaviour
return self
def __call__(self, *args, **kwargs):
'''
Overloaded call method; it defines the behaviour of boundevent().
When the event is called, all registered event handlers are called.
* *args -- Arguments given to the event handlers.
* **kwargs -- Keyword arguments given to the event handlers.
'''
# Enforce signature and possibly execute entry code. This makes sure
# any inconsistent call will be caught immediately, independent of
# connected handlers.
result = self.__function(self.instance, *args, **kwargs)
# Call all registered event handlers
for f in self.__event_handlers[:]:
f(self.instance, *args, **kwargs)
return result