From 71ea1459abebada1faaef1198fec7a3792aa9a82 Mon Sep 17 00:00:00 2001 From: Jonathan Frederic Date: Mon, 28 Jul 2014 14:46:15 -0700 Subject: [PATCH] Add EventfulList and EventfulDict trait types. --- IPython/utils/eventful.py | 289 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 IPython/utils/eventful.py diff --git a/IPython/utils/eventful.py b/IPython/utils/eventful.py new file mode 100644 index 000000000..b82649881 --- /dev/null +++ b/IPython/utils/eventful.py @@ -0,0 +1,289 @@ +"""Contains eventful dict and list implementations.""" + +class EventfulDict(dict): + """Eventful dictionary. + + This class inherits from the Python intrinsic dictionary class, dict. It + adds events to the get, set, and del actions and optionally allows you to + intercept and cancel these actions. The eventfulness isn't recursive. In + other words, if you add a dict as a child, the events of that dict won't be + listened to. If you find you need something recursive, listen to the `add` + and `set` methods, and then cancel `dict` values from being set, and instead + set `EventfulDict`s that wrap those `dict`s. Then you can wire the events + to the same handlers if necessary. + + See the on_add, on_set, and on_del methods for registering an event + handler.""" + + def __init__(self, *args, **kwargs): + """Public constructor""" + self._add_callback = None + self._del_callback = None + self._set_callback = None + dict.__init__(self, *args, **kwargs) + + def on_add(self, callback): + """Register a callback for when an item is added to the dict. + + Allows the listener to detect when items are added to the dictionary and + optionally cancel the addition. + + callback: callable or None + If you want to ignore the addition event, pass None as the callback. + The callback should have a signature of callback(key, value). The + callback should return a boolean True if the additon should be + canceled, False or None otherwise.""" + self._add_callback = callback + + def on_del(self, callback): + """Register a callback for when an item is deleted from the dict. + + Allows the listener to detect when items are deleted from the dictionary + and optionally cancel the deletion. + + callback: callable or None + If you want to ignore the deletion event, pass None as the callback. + The callback should have a signature of callback(key). The + callback should return a boolean True if the deletion should be + canceled, False or None otherwise.""" + self._del_callback = callback + + def on_set(self, callback): + """Register a callback for when an item is changed in the dict. + + Allows the listener to detect when items are changed in the dictionary + and optionally cancel the change. + + callback: callable or None + If you want to ignore the change event, pass None as the callback. + The callback should have a signature of callback(key, value). The + callback should return a boolean True if the change should be + canceled, False or None otherwise.""" + self._set_callback = callback + + def _can_add(self, key, value): + """Check if the item can be added to the dict.""" + if callable(self._add_callback): + return not bool(self._add_callback(key, value)) + else: + return True + + def _can_del(self, key): + """Check if the item can be deleted from the dict.""" + if callable(self._del_callback): + return not bool(self._del_callback(key)) + else: + return True + + def _can_set(self, key, value): + """Check if the item can be changed in the dict.""" + if callable(self._set_callback): + return not bool(self._set_callback(key, value)) + else: + return True + + def pop(self, key): + """Returns the value of an item in the dictionary and then deletes the + item from the dictionary.""" + if self._can_del(key): + return dict.pop(self, key) + else: + raise Exception('Cannot `pop`, deletion of key "{}" failed.'.format(key)) + + def popitem(self): + """Pop the next key/value pair from the dictionary.""" + key = dict.keys(self)[0] + return key, self.pop(key) + + def update(self, other_dict): + """Copy the key/value pairs from another dictionary into this dictionary, + overwriting any conflicting keys in this dictionary.""" + for (key, value) in other_dict.items(): + self[key] = value + + def clear(self): + """Clear the dictionary.""" + for key in list(self.keys()): + del self[key] + + def __setitem__(self, key, value): + if (key in self and self._can_set(key, value)) or \ + (key not in self and self._can_add(key, value)): + return dict.__setitem__(self, key, value) + + def __delitem__(self, key): + if self._can_del(key): + return dict.__delitem__(self, key) + + +class EventfulList(list): + """Eventful list. + + This class inherits from the Python intrinsic `list` class. It adds events + that allow you to listen for actions that modify the list. You can + optionally cancel the actions. + + See the on_del, on_set, on_insert, on_sort, and on_reverse methods for + registering an event handler. + + Some of the method docstrings were taken from the Python documentation at + https://docs.python.org/2/tutorial/datastructures.html""" + + def __init__(self, *pargs, **kwargs): + """Public constructor""" + self._insert_callback = None + self._set_callback = None + self._del_callback = None + self._sort_callback = None + self._reverse_callback = None + list.__init__(self, *pargs, **kwargs) + + def on_insert(self, callback): + """Register a callback for when an item is inserted into the list. + + Allows the listener to detect when items are inserted into the list and + optionally cancel the insertion. + + callback: callable or None + If you want to ignore the insertion event, pass None as the callback. + The callback should have a signature of callback(index, value). The + callback should return a boolean True if the insertion should be + canceled, False or None otherwise.""" + self._insert_callback = callback + + def on_del(self, callback): + """Register a callback for item deletion. + + Allows the listener to detect when items are deleted from the list and + optionally cancel the deletion. + + callback: callable or None + If you want to ignore the deletion event, pass None as the callback. + The callback should have a signature of callback(index). The + callback should return a boolean True if the deletion should be + canceled, False or None otherwise.""" + self._del_callback = callback + + def on_set(self, callback): + """Register a callback for items are set. + + Allows the listener to detect when items are set and optionally cancel + the setting. Note, `set` is also called when one or more items are + added to the end of the list. + + callback: callable or None + If you want to ignore the set event, pass None as the callback. + The callback should have a signature of callback(index, value). The + callback should return a boolean True if the set should be + canceled, False or None otherwise.""" + self._set_callback = callback + + def on_reverse(self, callback): + """Register a callback for list reversal. + + callback: callable or None + If you want to ignore the reverse event, pass None as the callback. + The callback should have a signature of callback(). The + callback should return a boolean True if the reverse should be + canceled, False or None otherwise.""" + self._reverse_callback = callback + + def on_sort(self, callback): + """Register a callback for sortting of the list. + + callback: callable or None + If you want to ignore the sort event, pass None as the callback. + The callback signature should match that of Python list's `.sort` + method or `callback(*pargs, **kwargs)` as a catch all. The callback + should return a boolean True if the reverse should be canceled, + False or None otherwise.""" + self._sort_callback = callback + + def _can_insert(self, index, value): + """Check if the item can be inserted.""" + if callable(self._insert_callback): + return not bool(self._insert_callback(index, value)) + else: + return True + + def _can_del(self, index): + """Check if the item can be deleted.""" + if callable(self._del_callback): + return not bool(self._del_callback(index)) + else: + return True + + def _can_set(self, index, value): + """Check if the item can be set.""" + if callable(self._set_callback): + return not bool(self._set_callback(index, value)) + else: + return True + + def _can_reverse(self): + """Check if the list can be reversed.""" + if callable(self._reverse_callback): + return not bool(self._reverse_callback()) + else: + return True + + def _can_sort(self, *pargs, **kwargs): + """Check if the list can be sorted.""" + if callable(self._sort_callback): + return not bool(self._sort_callback(*pargs, **kwargs)) + else: + return True + + def append(self, x): + """Add an item to the end of the list.""" + self[len(self):] = [x] + + def extend(self, L): + """Extend the list by appending all the items in the given list.""" + self[len(self):] = L + + def remove(self, x): + """Remove the first item from the list whose value is x. It is an error + if there is no such item.""" + del self[self.index(x)] + + def pop(self, i=None): + """Remove the item at the given position in the list, and return it. If + no index is specified, a.pop() removes and returns the last item in the + list.""" + if i is None: + i = len(self) - 1 + val = self[i] + del self[i] + return val + + def reverse(self): + """Reverse the elements of the list, in place.""" + if self._can_reverse(): + list.reverse(self) + + def insert(self, index, value): + """Insert an item at a given position. The first argument is the index + of the element before which to insert, so a.insert(0, x) inserts at the + front of the list, and a.insert(len(a), x) is equivalent to + a.append(x).""" + if self._can_insert(index, value): + list.insert(self, index, value) + + def sort(self, *pargs, **kwargs): + """Sort the items of the list in place (the arguments can be used for + sort customization, see Python's sorted() for their explanation).""" + if self._can_sort(*pargs, **kwargs): + list.sort(self, *pargs, **kwargs) + + def __delitem__(self, index): + if self._can_del(index): + list.__delitem__(self, index) + + def __setitem__(self, index, value): + if self._can_set(index, value): + list.__setitem__(self, index, value) + + def __setslice__(self, start, end, value): + if self._can_set(slice(start, end), value): + list.__setslice__(self, start, end, value)