##// END OF EJS Templates
Merge pull request #6228 from jdfreder/eventful-list-dict...
Thomas Kluyver -
r17618:3c464028 merge
parent child Browse files
Show More
@@ -0,0 +1,299 b''
1 """Contains eventful dict and list implementations."""
2
3 # void function used as a callback placeholder.
4 def _void(*p, **k): return None
5
6 class EventfulDict(dict):
7 """Eventful dictionary.
8
9 This class inherits from the Python intrinsic dictionary class, dict. It
10 adds events to the get, set, and del actions and optionally allows you to
11 intercept and cancel these actions. The eventfulness isn't recursive. In
12 other words, if you add a dict as a child, the events of that dict won't be
13 listened to. If you find you need something recursive, listen to the `add`
14 and `set` methods, and then cancel `dict` values from being set, and instead
15 set `EventfulDict`s that wrap those `dict`s. Then you can wire the events
16 to the same handlers if necessary.
17
18 See the on_events, on_add, on_set, and on_del methods for registering
19 event handlers."""
20
21 def __init__(self, *args, **kwargs):
22 """Public constructor"""
23 self._add_callback = _void
24 self._del_callback = _void
25 self._set_callback = _void
26 dict.__init__(self, *args, **kwargs)
27
28 def on_events(self, add_callback=None, set_callback=None, del_callback=None):
29 """Register callbacks for add, set, and del actions.
30
31 See the doctstrings for on_(add/set/del) for details about each
32 callback.
33
34 add_callback: [callback = None]
35 set_callback: [callback = None]
36 del_callback: [callback = None]"""
37 self.on_add(add_callback)
38 self.on_set(set_callback)
39 self.on_del(del_callback)
40
41 def on_add(self, callback):
42 """Register a callback for when an item is added to the dict.
43
44 Allows the listener to detect when items are added to the dictionary and
45 optionally cancel the addition.
46
47 callback: callable or None
48 If you want to ignore the addition event, pass None as the callback.
49 The callback should have a signature of callback(key, value). The
50 callback should return a boolean True if the additon should be
51 canceled, False or None otherwise."""
52 self._add_callback = callback if callable(callback) else _void
53
54 def on_del(self, callback):
55 """Register a callback for when an item is deleted from the dict.
56
57 Allows the listener to detect when items are deleted from the dictionary
58 and optionally cancel the deletion.
59
60 callback: callable or None
61 If you want to ignore the deletion event, pass None as the callback.
62 The callback should have a signature of callback(key). The
63 callback should return a boolean True if the deletion should be
64 canceled, False or None otherwise."""
65 self._del_callback = callback if callable(callback) else _void
66
67 def on_set(self, callback):
68 """Register a callback for when an item is changed in the dict.
69
70 Allows the listener to detect when items are changed in the dictionary
71 and optionally cancel the change.
72
73 callback: callable or None
74 If you want to ignore the change event, pass None as the callback.
75 The callback should have a signature of callback(key, value). The
76 callback should return a boolean True if the change should be
77 canceled, False or None otherwise."""
78 self._set_callback = callback if callable(callback) else _void
79
80 def pop(self, key):
81 """Returns the value of an item in the dictionary and then deletes the
82 item from the dictionary."""
83 if self._can_del(key):
84 return dict.pop(self, key)
85 else:
86 raise Exception('Cannot `pop`, deletion of key "{}" failed.'.format(key))
87
88 def popitem(self):
89 """Pop the next key/value pair from the dictionary."""
90 key = next(iter(self))
91 return key, self.pop(key)
92
93 def update(self, other_dict):
94 """Copy the key/value pairs from another dictionary into this dictionary,
95 overwriting any conflicting keys in this dictionary."""
96 for (key, value) in other_dict.items():
97 self[key] = value
98
99 def clear(self):
100 """Clear the dictionary."""
101 for key in list(self.keys()):
102 del self[key]
103
104 def __setitem__(self, key, value):
105 if (key in self and self._can_set(key, value)) or \
106 (key not in self and self._can_add(key, value)):
107 return dict.__setitem__(self, key, value)
108
109 def __delitem__(self, key):
110 if self._can_del(key):
111 return dict.__delitem__(self, key)
112
113 def _can_add(self, key, value):
114 """Check if the item can be added to the dict."""
115 return not bool(self._add_callback(key, value))
116
117 def _can_del(self, key):
118 """Check if the item can be deleted from the dict."""
119 return not bool(self._del_callback(key))
120
121 def _can_set(self, key, value):
122 """Check if the item can be changed in the dict."""
123 return not bool(self._set_callback(key, value))
124
125
126 class EventfulList(list):
127 """Eventful list.
128
129 This class inherits from the Python intrinsic `list` class. It adds events
130 that allow you to listen for actions that modify the list. You can
131 optionally cancel the actions.
132
133 See the on_del, on_set, on_insert, on_sort, and on_reverse methods for
134 registering an event handler.
135
136 Some of the method docstrings were taken from the Python documentation at
137 https://docs.python.org/2/tutorial/datastructures.html"""
138
139 def __init__(self, *pargs, **kwargs):
140 """Public constructor"""
141 self._insert_callback = _void
142 self._set_callback = _void
143 self._del_callback = _void
144 self._sort_callback = _void
145 self._reverse_callback = _void
146 list.__init__(self, *pargs, **kwargs)
147
148 def on_events(self, insert_callback=None, set_callback=None,
149 del_callback=None, reverse_callback=None, sort_callback=None):
150 """Register callbacks for add, set, and del actions.
151
152 See the doctstrings for on_(insert/set/del/reverse/sort) for details
153 about each callback.
154
155 insert_callback: [callback = None]
156 set_callback: [callback = None]
157 del_callback: [callback = None]
158 reverse_callback: [callback = None]
159 sort_callback: [callback = None]"""
160 self.on_insert(insert_callback)
161 self.on_set(set_callback)
162 self.on_del(del_callback)
163 self.on_reverse(reverse_callback)
164 self.on_sort(sort_callback)
165
166 def on_insert(self, callback):
167 """Register a callback for when an item is inserted into the list.
168
169 Allows the listener to detect when items are inserted into the list and
170 optionally cancel the insertion.
171
172 callback: callable or None
173 If you want to ignore the insertion event, pass None as the callback.
174 The callback should have a signature of callback(index, value). The
175 callback should return a boolean True if the insertion should be
176 canceled, False or None otherwise."""
177 self._insert_callback = callback if callable(callback) else _void
178
179 def on_del(self, callback):
180 """Register a callback for item deletion.
181
182 Allows the listener to detect when items are deleted from the list and
183 optionally cancel the deletion.
184
185 callback: callable or None
186 If you want to ignore the deletion event, pass None as the callback.
187 The callback should have a signature of callback(index). The
188 callback should return a boolean True if the deletion should be
189 canceled, False or None otherwise."""
190 self._del_callback = callback if callable(callback) else _void
191
192 def on_set(self, callback):
193 """Register a callback for items are set.
194
195 Allows the listener to detect when items are set and optionally cancel
196 the setting. Note, `set` is also called when one or more items are
197 added to the end of the list.
198
199 callback: callable or None
200 If you want to ignore the set event, pass None as the callback.
201 The callback should have a signature of callback(index, value). The
202 callback should return a boolean True if the set should be
203 canceled, False or None otherwise."""
204 self._set_callback = callback if callable(callback) else _void
205
206 def on_reverse(self, callback):
207 """Register a callback for list reversal.
208
209 callback: callable or None
210 If you want to ignore the reverse event, pass None as the callback.
211 The callback should have a signature of callback(). The
212 callback should return a boolean True if the reverse should be
213 canceled, False or None otherwise."""
214 self._reverse_callback = callback if callable(callback) else _void
215
216 def on_sort(self, callback):
217 """Register a callback for sortting of the list.
218
219 callback: callable or None
220 If you want to ignore the sort event, pass None as the callback.
221 The callback signature should match that of Python list's `.sort`
222 method or `callback(*pargs, **kwargs)` as a catch all. The callback
223 should return a boolean True if the reverse should be canceled,
224 False or None otherwise."""
225 self._sort_callback = callback if callable(callback) else _void
226
227 def append(self, x):
228 """Add an item to the end of the list."""
229 self[len(self):] = [x]
230
231 def extend(self, L):
232 """Extend the list by appending all the items in the given list."""
233 self[len(self):] = L
234
235 def remove(self, x):
236 """Remove the first item from the list whose value is x. It is an error
237 if there is no such item."""
238 del self[self.index(x)]
239
240 def pop(self, i=None):
241 """Remove the item at the given position in the list, and return it. If
242 no index is specified, a.pop() removes and returns the last item in the
243 list."""
244 if i is None:
245 i = len(self) - 1
246 val = self[i]
247 del self[i]
248 return val
249
250 def reverse(self):
251 """Reverse the elements of the list, in place."""
252 if self._can_reverse():
253 list.reverse(self)
254
255 def insert(self, index, value):
256 """Insert an item at a given position. The first argument is the index
257 of the element before which to insert, so a.insert(0, x) inserts at the
258 front of the list, and a.insert(len(a), x) is equivalent to
259 a.append(x)."""
260 if self._can_insert(index, value):
261 list.insert(self, index, value)
262
263 def sort(self, *pargs, **kwargs):
264 """Sort the items of the list in place (the arguments can be used for
265 sort customization, see Python's sorted() for their explanation)."""
266 if self._can_sort(*pargs, **kwargs):
267 list.sort(self, *pargs, **kwargs)
268
269 def __delitem__(self, index):
270 if self._can_del(index):
271 list.__delitem__(self, index)
272
273 def __setitem__(self, index, value):
274 if self._can_set(index, value):
275 list.__setitem__(self, index, value)
276
277 def __setslice__(self, start, end, value):
278 if self._can_set(slice(start, end), value):
279 list.__setslice__(self, start, end, value)
280
281 def _can_insert(self, index, value):
282 """Check if the item can be inserted."""
283 return not bool(self._insert_callback(index, value))
284
285 def _can_del(self, index):
286 """Check if the item can be deleted."""
287 return not bool(self._del_callback(index))
288
289 def _can_set(self, index, value):
290 """Check if the item can be set."""
291 return not bool(self._set_callback(index, value))
292
293 def _can_reverse(self):
294 """Check if the list can be reversed."""
295 return not bool(self._reverse_callback())
296
297 def _can_sort(self, *pargs, **kwargs):
298 """Check if the list can be sorted."""
299 return not bool(self._sort_callback(*pargs, **kwargs))
@@ -19,7 +19,7 b' from IPython.utils.traitlets import ('
19 HasTraits, MetaHasTraits, TraitType, Any, CBytes, Dict,
19 HasTraits, MetaHasTraits, TraitType, Any, CBytes, Dict,
20 Int, Long, Integer, Float, Complex, Bytes, Unicode, TraitError,
20 Int, Long, Integer, Float, Complex, Bytes, Unicode, TraitError,
21 Undefined, Type, This, Instance, TCPAddress, List, Tuple,
21 Undefined, Type, This, Instance, TCPAddress, List, Tuple,
22 ObjectName, DottedObjectName, CRegExp, link
22 ObjectName, DottedObjectName, CRegExp, link, EventfulList, EventfulDict
23 )
23 )
24 from IPython.utils import py3compat
24 from IPython.utils import py3compat
25 from IPython.testing.decorators import skipif
25 from IPython.testing.decorators import skipif
@@ -1034,7 +1034,7 b' class TestCRegExp(TraitTestBase):'
1034
1034
1035 _default_value = re.compile(r'')
1035 _default_value = re.compile(r'')
1036 _good_values = [r'\d+', re.compile(r'\d+')]
1036 _good_values = [r'\d+', re.compile(r'\d+')]
1037 _bad_values = [r'(', None, ()]
1037 _bad_values = ['(', None, ()]
1038
1038
1039 class DictTrait(HasTraits):
1039 class DictTrait(HasTraits):
1040 value = Dict()
1040 value = Dict()
@@ -1171,4 +1171,65 b' def test_pickle_hastraits():'
1171 c2 = pickle.loads(p)
1171 c2 = pickle.loads(p)
1172 nt.assert_equal(c2.i, c.i)
1172 nt.assert_equal(c2.i, c.i)
1173 nt.assert_equal(c2.j, c.j)
1173 nt.assert_equal(c2.j, c.j)
1174
1174
1175 class TestEventful(TestCase):
1176
1177 def test_list(self):
1178 """Does the EventfulList work?"""
1179 event_cache = []
1180
1181 class A(HasTraits):
1182 x = EventfulList([c for c in 'abc'])
1183 a = A()
1184 a.x.on_events(lambda i, x: event_cache.append('insert'), \
1185 lambda i, x: event_cache.append('set'), \
1186 lambda i: event_cache.append('del'), \
1187 lambda: event_cache.append('reverse'), \
1188 lambda *p, **k: event_cache.append('sort'))
1189
1190 a.x.remove('c')
1191 # ab
1192 a.x.insert(0, 'z')
1193 # zab
1194 del a.x[1]
1195 # zb
1196 a.x.reverse()
1197 # bz
1198 a.x[1] = 'o'
1199 # bo
1200 a.x.append('a')
1201 # boa
1202 a.x.sort()
1203 # abo
1204
1205 # Were the correct events captured?
1206 self.assertEqual(event_cache, ['del', 'insert', 'del', 'reverse', 'set', 'set', 'sort'])
1207
1208 # Is the output correct?
1209 self.assertEqual(a.x, [c for c in 'abo'])
1210
1211 def test_dict(self):
1212 """Does the EventfulDict work?"""
1213 event_cache = []
1214
1215 class A(HasTraits):
1216 x = EventfulDict({c: c for c in 'abc'})
1217 a = A()
1218 a.x.on_events(lambda k, v: event_cache.append('add'), \
1219 lambda k, v: event_cache.append('set'), \
1220 lambda k: event_cache.append('del'))
1221
1222 del a.x['c']
1223 # ab
1224 a.x['z'] = 1
1225 # abz
1226 a.x['z'] = 'z'
1227 # abz
1228 a.x.pop('a')
1229 # bz
1230
1231 # Were the correct events captured?
1232 self.assertEqual(event_cache, ['del', 'add', 'set', 'del'])
1233
1234 # Is the output correct?
1235 self.assertEqual(a.x, {c: c for c in 'bz'})
@@ -54,6 +54,7 b' except:'
54
54
55 from .importstring import import_item
55 from .importstring import import_item
56 from IPython.utils import py3compat
56 from IPython.utils import py3compat
57 from IPython.utils import eventful
57 from IPython.utils.py3compat import iteritems
58 from IPython.utils.py3compat import iteritems
58 from IPython.testing.skipdoctest import skip_doctest
59 from IPython.testing.skipdoctest import skip_doctest
59
60
@@ -1490,6 +1491,49 b' class Dict(Instance):'
1490 super(Dict,self).__init__(klass=dict, args=args,
1491 super(Dict,self).__init__(klass=dict, args=args,
1491 allow_none=allow_none, **metadata)
1492 allow_none=allow_none, **metadata)
1492
1493
1494
1495 class EventfulDict(Instance):
1496 """An instance of an EventfulDict."""
1497
1498 def __init__(self, default_value=None, allow_none=True, **metadata):
1499 """Create a EventfulDict trait type from a dict.
1500
1501 The default value is created by doing
1502 ``eventful.EvenfulDict(default_value)``, which creates a copy of the
1503 ``default_value``.
1504 """
1505 if default_value is None:
1506 args = ((),)
1507 elif isinstance(default_value, dict):
1508 args = (default_value,)
1509 elif isinstance(default_value, SequenceTypes):
1510 args = (default_value,)
1511 else:
1512 raise TypeError('default value of EventfulDict was %s' % default_value)
1513
1514 super(EventfulDict, self).__init__(klass=eventful.EventfulDict, args=args,
1515 allow_none=allow_none, **metadata)
1516
1517
1518 class EventfulList(Instance):
1519 """An instance of an EventfulList."""
1520
1521 def __init__(self, default_value=None, allow_none=True, **metadata):
1522 """Create a EventfulList trait type from a dict.
1523
1524 The default value is created by doing
1525 ``eventful.EvenfulList(default_value)``, which creates a copy of the
1526 ``default_value``.
1527 """
1528 if default_value is None:
1529 args = ((),)
1530 else:
1531 args = (default_value,)
1532
1533 super(EventfulList, self).__init__(klass=eventful.EventfulList, args=args,
1534 allow_none=allow_none, **metadata)
1535
1536
1493 class TCPAddress(TraitType):
1537 class TCPAddress(TraitType):
1494 """A trait for an (ip, port) tuple.
1538 """A trait for an (ip, port) tuple.
1495
1539
General Comments 0
You need to be logged in to leave comments. Login now