##// END OF EJS Templates
Add EventfulList and EventfulDict trait types.
Jonathan Frederic -
Show More
@@ -0,0 +1,289 b''
1 """Contains eventful dict and list implementations."""
2
3 class EventfulDict(dict):
4 """Eventful dictionary.
5
6 This class inherits from the Python intrinsic dictionary class, dict. It
7 adds events to the get, set, and del actions and optionally allows you to
8 intercept and cancel these actions. The eventfulness isn't recursive. In
9 other words, if you add a dict as a child, the events of that dict won't be
10 listened to. If you find you need something recursive, listen to the `add`
11 and `set` methods, and then cancel `dict` values from being set, and instead
12 set `EventfulDict`s that wrap those `dict`s. Then you can wire the events
13 to the same handlers if necessary.
14
15 See the on_add, on_set, and on_del methods for registering an event
16 handler."""
17
18 def __init__(self, *args, **kwargs):
19 """Public constructor"""
20 self._add_callback = None
21 self._del_callback = None
22 self._set_callback = None
23 dict.__init__(self, *args, **kwargs)
24
25 def on_add(self, callback):
26 """Register a callback for when an item is added to the dict.
27
28 Allows the listener to detect when items are added to the dictionary and
29 optionally cancel the addition.
30
31 callback: callable or None
32 If you want to ignore the addition event, pass None as the callback.
33 The callback should have a signature of callback(key, value). The
34 callback should return a boolean True if the additon should be
35 canceled, False or None otherwise."""
36 self._add_callback = callback
37
38 def on_del(self, callback):
39 """Register a callback for when an item is deleted from the dict.
40
41 Allows the listener to detect when items are deleted from the dictionary
42 and optionally cancel the deletion.
43
44 callback: callable or None
45 If you want to ignore the deletion event, pass None as the callback.
46 The callback should have a signature of callback(key). The
47 callback should return a boolean True if the deletion should be
48 canceled, False or None otherwise."""
49 self._del_callback = callback
50
51 def on_set(self, callback):
52 """Register a callback for when an item is changed in the dict.
53
54 Allows the listener to detect when items are changed in the dictionary
55 and optionally cancel the change.
56
57 callback: callable or None
58 If you want to ignore the change event, pass None as the callback.
59 The callback should have a signature of callback(key, value). The
60 callback should return a boolean True if the change should be
61 canceled, False or None otherwise."""
62 self._set_callback = callback
63
64 def _can_add(self, key, value):
65 """Check if the item can be added to the dict."""
66 if callable(self._add_callback):
67 return not bool(self._add_callback(key, value))
68 else:
69 return True
70
71 def _can_del(self, key):
72 """Check if the item can be deleted from the dict."""
73 if callable(self._del_callback):
74 return not bool(self._del_callback(key))
75 else:
76 return True
77
78 def _can_set(self, key, value):
79 """Check if the item can be changed in the dict."""
80 if callable(self._set_callback):
81 return not bool(self._set_callback(key, value))
82 else:
83 return True
84
85 def pop(self, key):
86 """Returns the value of an item in the dictionary and then deletes the
87 item from the dictionary."""
88 if self._can_del(key):
89 return dict.pop(self, key)
90 else:
91 raise Exception('Cannot `pop`, deletion of key "{}" failed.'.format(key))
92
93 def popitem(self):
94 """Pop the next key/value pair from the dictionary."""
95 key = dict.keys(self)[0]
96 return key, self.pop(key)
97
98 def update(self, other_dict):
99 """Copy the key/value pairs from another dictionary into this dictionary,
100 overwriting any conflicting keys in this dictionary."""
101 for (key, value) in other_dict.items():
102 self[key] = value
103
104 def clear(self):
105 """Clear the dictionary."""
106 for key in list(self.keys()):
107 del self[key]
108
109 def __setitem__(self, key, value):
110 if (key in self and self._can_set(key, value)) or \
111 (key not in self and self._can_add(key, value)):
112 return dict.__setitem__(self, key, value)
113
114 def __delitem__(self, key):
115 if self._can_del(key):
116 return dict.__delitem__(self, key)
117
118
119 class EventfulList(list):
120 """Eventful list.
121
122 This class inherits from the Python intrinsic `list` class. It adds events
123 that allow you to listen for actions that modify the list. You can
124 optionally cancel the actions.
125
126 See the on_del, on_set, on_insert, on_sort, and on_reverse methods for
127 registering an event handler.
128
129 Some of the method docstrings were taken from the Python documentation at
130 https://docs.python.org/2/tutorial/datastructures.html"""
131
132 def __init__(self, *pargs, **kwargs):
133 """Public constructor"""
134 self._insert_callback = None
135 self._set_callback = None
136 self._del_callback = None
137 self._sort_callback = None
138 self._reverse_callback = None
139 list.__init__(self, *pargs, **kwargs)
140
141 def on_insert(self, callback):
142 """Register a callback for when an item is inserted into the list.
143
144 Allows the listener to detect when items are inserted into the list and
145 optionally cancel the insertion.
146
147 callback: callable or None
148 If you want to ignore the insertion event, pass None as the callback.
149 The callback should have a signature of callback(index, value). The
150 callback should return a boolean True if the insertion should be
151 canceled, False or None otherwise."""
152 self._insert_callback = callback
153
154 def on_del(self, callback):
155 """Register a callback for item deletion.
156
157 Allows the listener to detect when items are deleted from the list and
158 optionally cancel the deletion.
159
160 callback: callable or None
161 If you want to ignore the deletion event, pass None as the callback.
162 The callback should have a signature of callback(index). The
163 callback should return a boolean True if the deletion should be
164 canceled, False or None otherwise."""
165 self._del_callback = callback
166
167 def on_set(self, callback):
168 """Register a callback for items are set.
169
170 Allows the listener to detect when items are set and optionally cancel
171 the setting. Note, `set` is also called when one or more items are
172 added to the end of the list.
173
174 callback: callable or None
175 If you want to ignore the set event, pass None as the callback.
176 The callback should have a signature of callback(index, value). The
177 callback should return a boolean True if the set should be
178 canceled, False or None otherwise."""
179 self._set_callback = callback
180
181 def on_reverse(self, callback):
182 """Register a callback for list reversal.
183
184 callback: callable or None
185 If you want to ignore the reverse event, pass None as the callback.
186 The callback should have a signature of callback(). The
187 callback should return a boolean True if the reverse should be
188 canceled, False or None otherwise."""
189 self._reverse_callback = callback
190
191 def on_sort(self, callback):
192 """Register a callback for sortting of the list.
193
194 callback: callable or None
195 If you want to ignore the sort event, pass None as the callback.
196 The callback signature should match that of Python list's `.sort`
197 method or `callback(*pargs, **kwargs)` as a catch all. The callback
198 should return a boolean True if the reverse should be canceled,
199 False or None otherwise."""
200 self._sort_callback = callback
201
202 def _can_insert(self, index, value):
203 """Check if the item can be inserted."""
204 if callable(self._insert_callback):
205 return not bool(self._insert_callback(index, value))
206 else:
207 return True
208
209 def _can_del(self, index):
210 """Check if the item can be deleted."""
211 if callable(self._del_callback):
212 return not bool(self._del_callback(index))
213 else:
214 return True
215
216 def _can_set(self, index, value):
217 """Check if the item can be set."""
218 if callable(self._set_callback):
219 return not bool(self._set_callback(index, value))
220 else:
221 return True
222
223 def _can_reverse(self):
224 """Check if the list can be reversed."""
225 if callable(self._reverse_callback):
226 return not bool(self._reverse_callback())
227 else:
228 return True
229
230 def _can_sort(self, *pargs, **kwargs):
231 """Check if the list can be sorted."""
232 if callable(self._sort_callback):
233 return not bool(self._sort_callback(*pargs, **kwargs))
234 else:
235 return True
236
237 def append(self, x):
238 """Add an item to the end of the list."""
239 self[len(self):] = [x]
240
241 def extend(self, L):
242 """Extend the list by appending all the items in the given list."""
243 self[len(self):] = L
244
245 def remove(self, x):
246 """Remove the first item from the list whose value is x. It is an error
247 if there is no such item."""
248 del self[self.index(x)]
249
250 def pop(self, i=None):
251 """Remove the item at the given position in the list, and return it. If
252 no index is specified, a.pop() removes and returns the last item in the
253 list."""
254 if i is None:
255 i = len(self) - 1
256 val = self[i]
257 del self[i]
258 return val
259
260 def reverse(self):
261 """Reverse the elements of the list, in place."""
262 if self._can_reverse():
263 list.reverse(self)
264
265 def insert(self, index, value):
266 """Insert an item at a given position. The first argument is the index
267 of the element before which to insert, so a.insert(0, x) inserts at the
268 front of the list, and a.insert(len(a), x) is equivalent to
269 a.append(x)."""
270 if self._can_insert(index, value):
271 list.insert(self, index, value)
272
273 def sort(self, *pargs, **kwargs):
274 """Sort the items of the list in place (the arguments can be used for
275 sort customization, see Python's sorted() for their explanation)."""
276 if self._can_sort(*pargs, **kwargs):
277 list.sort(self, *pargs, **kwargs)
278
279 def __delitem__(self, index):
280 if self._can_del(index):
281 list.__delitem__(self, index)
282
283 def __setitem__(self, index, value):
284 if self._can_set(index, value):
285 list.__setitem__(self, index, value)
286
287 def __setslice__(self, start, end, value):
288 if self._can_set(slice(start, end), value):
289 list.__setslice__(self, start, end, value)
@@ -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,83 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
1185 def handle_insert(index, value):
1186 event_cache.append('insert')
1187 def handle_del(index):
1188 event_cache.append('del')
1189 def handle_set(index, value):
1190 event_cache.append('set')
1191 def handle_reverse():
1192 event_cache.append('reverse')
1193 def handle_sort(*pargs, **kwargs):
1194 event_cache.append('sort')
1195 a.x.on_insert(handle_insert)
1196 a.x.on_del(handle_del)
1197 a.x.on_set(handle_set)
1198 a.x.on_reverse(handle_reverse)
1199 a.x.on_sort(handle_sort)
1200
1201 a.x.remove('c')
1202 # ab
1203 a.x.insert(0, 'z')
1204 # zab
1205 del a.x[1]
1206 # zb
1207 a.x.reverse()
1208 # bz
1209 a.x[1] = 'o'
1210 # bo
1211 a.x.append('a')
1212 # boa
1213 a.x.sort()
1214 # abo
1215
1216 # Were the correct events captured?
1217 self.assertEqual(event_cache, ['del', 'insert', 'del', 'reverse', 'set', 'set', 'sort'])
1218
1219 # Is the output correct?
1220 self.assertEqual(a.x, [c for c in 'abo'])
1221
1222 def test_dict(self):
1223 """Does the EventfulDict work?"""
1224 event_cache = []
1225
1226 class A(HasTraits):
1227 x = EventfulDict({c: c for c in 'abc'})
1228 a = A()
1229
1230 def handle_add(key, value):
1231 event_cache.append('add')
1232 def handle_del(key):
1233 event_cache.append('del')
1234 def handle_set(key, value):
1235 event_cache.append('set')
1236 a.x.on_add(handle_add)
1237 a.x.on_del(handle_del)
1238 a.x.on_set(handle_set)
1239
1240 del a.x['c']
1241 # ab
1242 a.x['z'] = 1
1243 # abz
1244 a.x['z'] = 'z'
1245 # abz
1246 a.x.pop('a')
1247 # bz
1248
1249 # Were the correct events captured?
1250 self.assertEqual(event_cache, ['del', 'add', 'set', 'del'])
1251
1252 # Is the output correct?
1253 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