Motivation =========== .. py:module:: darts.lib.utils.events A Tale of Three Classes ----------------------- Let's assume, that you have some object `A`, which needs to know, whenever the value of an attribute in another object `B` changes. `A` might be some GUI component, and `B` is usually some kind of "model" object. So, the first approach might look like:: >>> class Observable(object): ... ... def __init__(self, callback): ... self._title = None ... self._callback = callback ... ... def title(): ... def getter(self): ... return self._title ... ... def setter(self, value): ... old_value = self._title ... if value != old_value: ... self._callback('title', 'before', old_value, value) ... self._title = value ... self._callback('title', 'before', old_value, value) ... ... return property(getter, setter) ... ... title = title() Now, our hypothetical GUI component could be set-up so, that the `callback` is actually some of its methods:: >>> gui_component = create_a_gui_component() >>> model_object = Observable(gui_component._property_change_callback) and everything is fine, right? The method `_property_change_callback` is called, whenever someone changes the value of the `title` property, so that's what we wanted. But, what if you have actually two or more views defined over the same model object, which all need to be notified? Well, simple:: >>> class Observable(object): ... ... def __init__(self): ... self._title = None ... self.callbacks = [] ... ... def title(): ... def getter(self): ... return self._title ... ... def setter(self, value): ... old_value = self._title ... if value != old_value: ... for cb in self.callbacks: ... cb('title', 'before', old_value, value) ... self._title = value ... for cb in self.callbacks: ... cb('title', 'before', old_value, value) ... ... return property(getter, setter) ... ... title = title() and >>> gui_component_1 = create_a_gui_component() >>> gui_component_2 = create_another_gui_component() >>> model_object = Observable() >>> model_object.callbacks.append(gui_component_1._callback) >>> model_object.callbacks.append(gui_component_2._callback) so, that one was easy, too. However, there is one flaw in the design above: the model will retain its callbacks indefinetly, i.e., even if the user of our hypothetical application closes one of the views, the callback will be kept in the `callbacks` list, keeping the entire view object alive (and worse, receiving event notifications, even though the view might no longer be fit to process them...) We could (and actually should), of course, remove the callback from the list when the view is closed. But that poses another problem:: >>> class Foo(object): ... def bar(self): ... pass ... >>> foo = Foo() >>> foo.bar is foo.bar False though they are equal, of course:: >>> foo.bar == foo.bar True So, we cannot remove the exact callback, unless we know it by identity. After all, it might be the case, that someone added the same function twice on purpose, which poses the question: which one is the one to remove [#Unusual]_? Also: having to remove the callback manually (and having to remember to do so) is error prone. So finally, before we introduce yet another version of the `Observable` class and try to fix the problem, let's see what the Darts Events library has to offer:: >>> from darts.lib.utils.event import Publisher, ReferenceRetention as RR >>> class Observable(object): ... ... def __init__(self): ... self._title = None ... self.on_property_change = Publisher() ... ... def title(): ... def getter(self): ... return self._title ... ... def setter(self, value): ... old_value = self._title ... if value != old_value: ... self.on_property_change.publish('title', 'before', old_value, value) ... self._title = value ... self.on_property_change.publish('title', 'after', old_value, value) ... ... return property(getter, setter) ... ... title = title() Ok. This looks quite like our second version. How do we add our GUI components' callbacks? >>> component_1 = create_a_gui_component() >>> component_2 = create_another_gui_component() >>> model_object = Observable() >>> c1 = model_object.on_property_change.subscribe(component_1, '_callback', RR.WEAK) >>> c2 = model_object.on_property_change.subscribe(component_2, '_callback', RR.WEAK) Hm. What did we gain? First of all, we can unambigously specify, which of the callbacks we want to remove. The result of the `subscribe` method is a special `Subscription` object, which provides us with a `cancel` method:: >>> c1.cancel() True No guessing involved here. Further more, since we registered the callbacks using the `WEAK` retention policy, only a weak reference to each GUI component is tracked, allowing the instance to be reclaimed by Python's garbage collector, if all other regular references to it are gone. And third: we can register an arbitrary number of listeners with our `Publisher` instance; no limits there. Additional benefits: the :class:`Publisher` class is thread-safe and fully reentrant. Callbacks (called listeners here) can be registered or and removed from within other listeners (i.e., as part of the action taken when actually called), so that's another thing you don't have to worry about. Yet another...? Really...? -------------------------- There are quite a few libraries floating around in the Python universe, whose goals are at least similar to what `darts.lib.utils.events` tries to accomplish. Off the top of my head * `Zope Event`_ * `Louie`_ * `PyDispatcher`_ and possibly more. So, if you are alredy using one of these (or the frameworks, they are associated with), then you might not need this library. However, if you are starting from scratch and are in need of an event dispatcher: consider giving this library a whirl... This library has been written, because non of the other libraries mentioned above was suitable for my actual use cases. The generic approach taken by `PyDispatcher`_ and derived implementations was too generic for my uses, and the `Zope Event`_ approach is overly simplistic. .. _Zope Event: http://pypi.python.org/pypi/zope.event .. _Louie: http://pypi.python.org/pypi/Louie .. _PyDispatcher: http://pydispatcher.sourceforge.net/ Reference ========== .. py:class:: Subscription This class declares the methods and attributes available on "subscription handles". A subscription handle essentially represents a subscription of a listener to some :class:`Publisher` instance. Note, that publishers will usually return some instance of a subclass of this class from their :py:meth:`~Publisher.subscribe` method, but client applications should never rely on the class of the actual result. .. py:method:: cancel() Undo an event subscription. This method cancels the subscription. After this method has been called, the listener will no longer receive event notifications for :py:meth:`~Publisher.publish` calls, which happen after the call to this method. Whether the listener receives notifications for :py:meth:`~Publisher.publish` calls active at the time of the call to :py:meth:`cancel` is undefined. .. py:attribute:: listener This property contains the subscription's underlying listener object. It may be `None`, if the subscription was created with a retention policy of `WEAK`, and if the listener has been reclaimed by the garbage collector. The property is read-only. This property is primarily provided for the sake of publication exception handlers, which want to provide more information about the culprit in logs, etc. Note, that the value of this property may actually be different from the listener value passed to :py:meth:`~Publisher.subscribe`. It should, however, be close enough an approximation to the original value, that it should be possible to identify the actual listener. .. py:attribute:: method This property contains the name of the method to be called on the subscription's underlying listener object during event dispatch. The property is read-only. This property is primarily provided for the sake of publication exception handlers, which want to provide more information about the culprit in logs, etc. Note, that the value of this property may actually be different from the value passed to :py:meth:`~Publisher.subscribe`. It should, however, be close enough an approximation to the original value, that it should be possible to identify the actual listener. .. py:class:: Publisher([exception_handler]) Instances of this class act as event publishers. Interested parties may register listener objects, which are notified, whenever an "event" occurs. .. py:method:: subscribe(listener [, method [, reference_retention]]) This method registers a new event listener with this publisher. Whenever an event occurs, the method named `method` of the listener instance is invoked. The default value for `method` is `"__call__"`. The value of `reference_retention` determines, whether the publisher should use a regular strong reference or a weak reference to remember the listener. The value should be one of the class constants defined in :class:`ReferenceRetention`. The default value is :py:attr:`~ReferenceRetention.STRONG`. The result of calling this method is a :class:`Subscription` instance, that can be used to cancel the subscription when notifications are no longer desired. .. py:method:: publish(*args, **keys) Publish an event, notifying the registered listeners. This method is simply a convenience interface for :py:meth:`publish_safely`, which uses the publisher's default exception handling policy. This method always returns `None`. .. py:method:: publish_safely(handler, *args, **keys) Publish an event, notifying the registered listeners. This method calls the notification method of each registered listener, passing the given positional and keyword arguments along. If a listener raises an exception, then the `handler` function is called to handle the exception. The `handler` should be a function with the signature:: def exception_handler(exception, value, traceback, subscription, args, keys): ... The values of `exception`, `value`, and `traceback` provide all information about the exception caught. The `subscription` argument will be the subscription handle of the listener, which raised the exception. The values of `args` and `keys` are passed along for the sake of the handler and represent the original arguments supplied to this method. The handler may decide to handle the exception (say, by logging it) and return normally, in which case the publication loop continues by notifying any remaining listeners. It may also decide to re-raise the exception (or raise another exception). In this case, the publication loop is aborted, and the exception will be propagated to the caller of this method. This method always returns `None`. .. py:class:: ReferenceRetention This class is intended as "pure" namespace for the documented values, which should be treated as opaque values. .. py:attribute:: WEAK Specifies, that the publisher should use a weak reference to keep track of the listener being registered. This allows the listener to be reclaimed by the garbage collector despite it being still subscribed to the publisher. This value is a class constant and should be treated as opaque by client applications. .. py:attribute:: STRONG Specifies, that the publisher should use a strong (regular) reference to keep track of the listener being registered. The listener will stay subscribed until it is explictely removed from the publisher by cancelling the subscription. This value is a class constant and should be treated as opaque by client application. .. py:data:: WEAK This is just an alternate name for the `WEAK` value defined as class constant on :class:`RetentionPolicy`. .. py:data:: STRONG This is just an alternate name for the `STRONG` value defined as class constant on :class:`RetentionPolicy` basics Indices and tables =================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` .. rubric:: Footnotes .. [#Unusual] I admit, that this might be somewhat unusual, but how should the model object know?