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 | 19 | HasTraits, MetaHasTraits, TraitType, Any, CBytes, Dict, |
|
20 | 20 | Int, Long, Integer, Float, Complex, Bytes, Unicode, TraitError, |
|
21 | 21 | Undefined, Type, This, Instance, TCPAddress, List, Tuple, |
|
22 | ObjectName, DottedObjectName, CRegExp, link | |
|
22 | ObjectName, DottedObjectName, CRegExp, link, EventfulList, EventfulDict | |
|
23 | 23 | ) |
|
24 | 24 | from IPython.utils import py3compat |
|
25 | 25 | from IPython.testing.decorators import skipif |
@@ -1034,7 +1034,7 b' class TestCRegExp(TraitTestBase):' | |||
|
1034 | 1034 | |
|
1035 | 1035 | _default_value = re.compile(r'') |
|
1036 | 1036 | _good_values = [r'\d+', re.compile(r'\d+')] |
|
1037 |
_bad_values = [ |
|
|
1037 | _bad_values = ['(', None, ()] | |
|
1038 | 1038 | |
|
1039 | 1039 | class DictTrait(HasTraits): |
|
1040 | 1040 | value = Dict() |
@@ -1172,3 +1172,64 b' def test_pickle_hastraits():' | |||
|
1172 | 1172 | nt.assert_equal(c2.i, c.i) |
|
1173 | 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 | 55 | from .importstring import import_item |
|
56 | 56 | from IPython.utils import py3compat |
|
57 | from IPython.utils import eventful | |
|
57 | 58 | from IPython.utils.py3compat import iteritems |
|
58 | 59 | from IPython.testing.skipdoctest import skip_doctest |
|
59 | 60 | |
@@ -1490,6 +1491,49 b' class Dict(Instance):' | |||
|
1490 | 1491 | super(Dict,self).__init__(klass=dict, args=args, |
|
1491 | 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 | 1537 | class TCPAddress(TraitType): |
|
1494 | 1538 | """A trait for an (ip, port) tuple. |
|
1495 | 1539 |
General Comments 0
You need to be logged in to leave comments.
Login now