[PATCH 1 of 6 events v3] util: add event handling mechanism

Gregory Szorc gregory.szorc at gmail.com
Sun Sep 28 16:01:39 CDT 2014


# HG changeset patch
# User Gregory Szorc <gregory.szorc at gmail.com>
# Date 1411933407 25200
#      Sun Sep 28 12:43:27 2014 -0700
# Node ID d513c232aeba16137b291456536adee0d88a040f
# Parent  4109cc16279ef0e04dc70e7f4c9ab7415e6a22d3
util: add event handling mechanism

The patch adds a generic event handling mechanism to Mercurial. From a
high level, you create a class with methods corresponding to event
names. You can then register functions/callbacks against events that
get called when that event fires.

As will be demonstrated in subsequent patches, event handling can be
considered an internal hooks mechanism and will provide a better
alternative to wrapping or monkeypatching.

The intent of the events system is to give extensions a more
well-defined point for code insertion. Currently, extension authors
have a limited set of hooks and a giant pile of functions to choose
from. Hooks often don't satisfy your requirements and you need to dig
through a pile of code to find an appropriate function to intercept.
Then you need to replace/monkeypatch this function. This is an inexact
science and is difficult to do robustly. The result are extensions that
do live dangerously.

Events will provide a better mechanism for code insertion. Events solve
the discovery problem by providing a well-defined (like hooks) set of
places for supported code insertion. Events are also easier to code,
as extension authors don't need to worry about monkeypatching: just
write a function and register it.

Events have another advantage over monkeypatching in that they can be
instance specific. Monkeypatching often results in changing symbols
on modules or class types as opposed to individual methods on
individual object instances. Oftentimes you only want to apply
customization to a single instance of an object if that object meets
certain criteria. In the current world, you often have to globally
replace and filter out invocations at call time that aren't appropriate.
This is prone to failure due to monkeypatched functions not taking all
uses into consideration. Furthermore, monkeypatching can be difficult
for module-level symbols. If a module-level function is imported by
another module (from foo import bar), you'll need to monkeypatch that
imported symbol as well. It is time consuming for module authors to
keep up with Mercurial changes and to ensure that imported uses are
always monkeypatched. Again, events solve this challenge.

diff --git a/mercurial/util.py b/mercurial/util.py
--- a/mercurial/util.py
+++ b/mercurial/util.py
@@ -13,9 +13,9 @@ This contains helper routines that are i
 hide platform-specific details from the core.
 """
 
 from i18n import _
-import error, osutil, encoding
+import error, osutil, encoding, inspect
 import errno, shutil, sys, tempfile, traceback
 import re as remod
 import os, time, datetime, calendar, textwrap, signal, collections
 import imp, socket, urllib
@@ -2059,4 +2059,142 @@ def debugstacktrace(msg='stacktrace', sk
     f.flush()
 
 # convenient shortcut
 dst = debugstacktrace
+
+class event(object):
+    '''An event with its handlers.
+
+    An ``event`` is essentially a collection of functions that will be invoked
+    when the event fires. ``event`` instances are typically created by defining
+    methods on ``eventmanager`` instances.
+
+    Handler functions can be registered against an instance via the ``+=``
+    operator. They can be unregistered via the ``-=`` operator.
+
+    Handler functions can be invoked by calling an ``event`` instance like
+    it is a function.
+
+    Handlers are executed in the order they are registered.
+
+    The return value of handler functions is ignored.
+
+    When events are created, they are "bound" to 0 or more values which will
+    be passed to every handler function in addition to the values passed to
+    that event. To reduce potential for confusion and to increase awareness
+    of changes by consumers, all arguments are passed as named (not
+    positional) arguments.
+
+    e.g.
+
+    >>> def handler(foo, bar, baz):
+    ...     print '%s %s %s' % (foo, bar, baz)
+    >>> e = event(foo='foo', bar='bar')
+    >>> e += handler
+    >>> e(baz='baz')
+    foo bar baz
+    '''
+
+    def __init__(self, **kwargs):
+        # Convert to list to facilitate + operator later.
+        self._args = kwargs
+        self._handlers = []
+
+    def __iadd__(self, fn):
+        if fn not in self._handlers:
+            self._handlers.append(fn)
+        return self
+
+    def __isub__(self, fn):
+        self._handlers.remove(fn)
+        return self
+
+    def __len__(self):
+        return len(self._handlers)
+
+    def __call__(self, **kwargs):
+        args = dict(self._args)
+        args.update(kwargs)
+        for fn in self._handlers:
+            fn(**args)
+
+class eventmanager(object):
+    '''A collection of events.
+
+    This class powers the internal events system. Instances of this class are
+    typically attached to an object, but they can be standalone.
+
+    This class is an abstract base class. Actual event containers should
+    subclass this class. Events are registered by defining methods on these
+    subclasses. The methods should define the named arguments the event
+    accepts and a docstring describing the purpose of the event. The custom
+    __new__ implementation of this class will automagically convert each
+    method to an ``event`` instance.
+
+    Methods lacking docstrings will result in an exception during class
+    creation. This requirement serves to reinforce the well-defined intent
+    of event APIs.
+
+    Arguments defined in the event/method definition are parsed for special
+    meaning. If an argument shares a name with a named argument passed to the
+    ``eventmanager`` constructor, the value passed to the ``eventmanager``
+    constructor will be used as the default value for that argument in the
+    event. In addition, if a default value is defined on the method/event,
+    that default value will be used when calling events. Method-specific
+    defaults override the "global" defaults from the ``eventmanager``
+    constructor.
+
+    For example, say numerous events pass a repository instance into events.
+    You can pass the repo as a named argument to the class constructor to
+    save some typing at event call time. e.g.
+
+    >>> class repoevents(eventmanager):
+    ...     def myevent(repo):
+    ...         """Some repo event."""
+    >>> def handler(repo):
+    ...     print(repo['name'])
+    >>> repo = {'name': 'my repo'}
+    >>> e = repoevents(repo=repo)
+    >>> e.myevent += handler
+    >>> e.myevent()
+    my repo
+    '''
+    def __new__(cls, **kwargs):
+        # We start with a new, empty object.
+        o = super(eventmanager, cls).__new__(cls)
+
+        for attr in dir(cls):
+            if attr.startswith('_'):
+                continue
+
+            method = getattr(cls, attr)
+
+            if not getattr(method, '__doc__'):
+                raise ValueError('methods of eventmanager classes must '
+                    'contain a docstring: %s' % attr)
+
+            args, varargs, keywords, defaults = inspect.getargspec(method)
+            # defaults is None or an iterable. defaults corresponds to the last
+            # N arguments from args. This one-liner aligns things properly.
+            methoddefaults = dict(zip(reversed(args), reversed(defaults or ())))
+            eventdefaults = {}
+            for arg in args:
+                # Every argument in the method definition gets turned into
+                # a default argument to the created event.
+                #
+                # If there is a default value in the method signature, it has
+                # the highest priority.
+                #
+                # If the argument exists in the constructor arguments, use it
+                # as a fallback.
+                #
+                # If it is a positional argument (no default value could be
+                # located), we set the default argument value to None. This
+                # ensures that all arguments defined as part of the event
+                # definition actually get passed to the event. This prevents
+                # a disconnect between event definitions and their use.
+                eventdefaults[arg] = methoddefaults.get(arg,
+                    kwargs.get(arg, None))
+
+            setattr(o, attr, event(**eventdefaults))
+
+        return o
diff --git a/tests/test-events.py b/tests/test-events.py
new file mode 100644
--- /dev/null
+++ b/tests/test-events.py
@@ -0,0 +1,168 @@
+from mercurial.util import event, eventmanager, safehasattr
+
+import unittest
+import silenttestrunner
+
+class testevents(unittest.TestCase):
+    def testeventsimple(self):
+        e = event()
+        self.assertEqual(len(e), 0)
+
+        calls = {'h1': 0, 'h2': 0}
+
+        def h1():
+            calls['h1'] += 1
+
+        def h2():
+            calls['h2'] += 1
+
+        e += h1
+        self.assertEqual(len(e), 1)
+        e += h2
+        self.assertEqual(len(e), 2)
+        e += h2
+        self.assertEqual(len(e), 2)
+
+        e()
+        self.assertEqual(calls, {'h1': 1, 'h2': 1})
+        e()
+        self.assertEqual(calls, {'h1': 2, 'h2': 2})
+
+        e -= h1
+        e()
+        self.assertEqual(calls, {'h1': 2, 'h2': 3})
+
+    def testeventarguments(self):
+        e = event()
+
+        calls = {'h1': [], 'h2': []}
+
+        def h1(foo, bar, baz=False):
+            calls['h1'].append((foo, bar, baz))
+
+        def h2(foo, bar, baz=True):
+            calls['h2'].append((foo, bar, baz))
+
+        e += h1
+        e += h2
+
+        e(foo=1, bar=2, baz=3)
+        self.assertEqual(calls, {'h1': [(1, 2, 3)], 'h2': [(1, 2, 3)]})
+        e(foo=3, bar=4, baz=None)
+        self.assertEqual(calls, {'h1': [(1, 2, 3), (3, 4, None)],
+                                 'h2': [(1, 2, 3), (3, 4, None)]})
+        e(foo=5, bar=6)
+        self.assertEqual(calls, {'h1': [(1, 2, 3), (3, 4, None), (5, 6, False)],
+                                 'h2': [(1, 2, 3), (3, 4, None), (5, 6, True)]})
+
+    def testeventdefaultargs(self):
+        expected = [1, True]
+        def h(obj, foo, bar=True):
+            self.assertEqual(o, obj)
+            self.assertEqual(foo, expected[0])
+            self.assertEqual(bar, expected[1])
+
+        o = object()
+        e = event(obj=o, foo=1)
+        e += h
+
+        e()
+        expected = [2, False]
+        e(foo=2, bar=False)
+
+    def testeventmanagerbasic(self):
+        # Base class does not do anything special.
+        o = eventmanager()
+        self.assertFalse(safehasattr(o, 'e1'))
+        try:
+            o.e1()
+        except AttributeError:
+            pass
+
+        # Subclass with methods results in event creation.
+        class testevents1(eventmanager):
+            def e1():
+                '''Event e1'''
+
+            def e2():
+                '''Event e2'''
+
+        o = testevents1()
+        self.assertTrue(safehasattr(o, 'e1'))
+        self.assertTrue(safehasattr(o, 'e2'))
+        self.assertEqual(type(getattr(o, 'e1')), event)
+        self.assertEqual(type(getattr(o, 'e2')), event)
+
+        # Verify that registered functions get called properly.
+        calls = {'e1': 0, 'e2': 0}
+
+        def e1h():
+            calls['e1'] += 1
+        def e2h():
+            calls['e2'] += 1
+
+        o.e1 += e1h
+        o.e2 += e2h
+        o.e1()
+        self.assertEqual(calls, {'e1': 1, 'e2': 0})
+        o.e2()
+        self.assertEqual(calls, {'e1': 1, 'e2': 1})
+        o.e1()
+        self.assertEqual(calls, {'e1': 2, 'e2': 1})
+
+    def testeventmanagerbadmethods(self):
+        class nodocstring(eventmanager):
+            def badevent():
+                pass
+
+        self.assertRaisesRegexp(ValueError, 'must contain a docstring',
+            nodocstring)
+
+    def testeventmanagerdefaultargument(self):
+        class defaults(eventmanager):
+            def event1(foo=True, bar=1):
+                '''Event 1'''
+
+            def event2(foo, bar):
+                '''Event 2'''
+
+        expected1 = {'foo': True, 'bar': 1}
+        def h1(**kwargs):
+            self.assertEqual(kwargs, expected1)
+
+        expected2 = {'foo': None, 'bar': None}
+        def h2(**kwargs):
+            self.assertEqual(kwargs, expected2)
+
+        o = defaults()
+        o.event1 += h1
+        o.event2 += h2
+        o.event1()
+        o.event2()
+
+        expected1 = {'foo': False, 'bar': 1}
+        o.event1(foo=False)
+        expected1 = {'foo': True, 'bar': 2}
+        o.event1(foo=True, bar=2)
+
+        expected2 = {'foo': True, 'bar': None}
+        o.event2(foo=True)
+
+    def testeventmanagerkwargsdefaults(self):
+        class defaults(eventmanager):
+            def event1(foo, bar=1):
+                '''Event 1'''
+
+        expected = {'foo': True, 'bar': 1}
+        def h1(**kwargs):
+            self.assertEqual(kwargs, expected)
+
+        o = defaults(foo=True)
+        o.event1 += h1
+        o.event1()
+
+        expected = {'foo': False, 'bar': 1}
+        o.event1(foo=False)
+
+if __name__ == '__main__':
+    silenttestrunner.main(__name__)


More information about the Mercurial-devel mailing list