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