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 = [ |
|
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