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 | 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() |
@@ -1171,4 +1171,83 b' def test_pickle_hastraits():' | |||
|
1171 | 1171 | c2 = pickle.loads(p) |
|
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 | ||
|
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 | 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