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

Gregory Szorc gregory.szorc at gmail.com
Tue Aug 19 00:30:13 CDT 2014


# HG changeset patch
# User Gregory Szorc <gregory.szorc at gmail.com>
# Date 1408422497 25200
#      Mon Aug 18 21:28:17 2014 -0700
# Node ID 110ff56e99e2ccff03841cc8baeed7f14c35f69b
# Parent  8dda6f6ff564d8fe6ac7b8ce4c74eb9bfb5de14a
util: add event handling mechanism

The patch adds a generic event handling mechanism to Mercurial. From a
high level, you create a named event, register some functions with that
event, and when you fire that event the registered functions get called.

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 more well-defined
points for code insertion. Currently, extension authors have a limited
set of hooks and a giant pile of functions to choose from. When hooks
won't satisfy your requirements, you need to dig through a pile of code
to find an appropriate function to intercept. Then you need to replace
it and hope you remembered to call the original function properly.
It's a dangerous, inexact science.

Events, by contrast, will provide a well-defined set of points for
extensions to inject code. These require a simple code grep to locate
(".events.register()") and are less prone to common mistakes, such as
calling the original function incorrectly.

Events have another advantage 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 that aren't appropriate. This is very dangerous. Events
support modifying behavior at a per-instance level and are thus much
safer.

diff --git a/mercurial/util.py b/mercurial/util.py
--- a/mercurial/util.py
+++ b/mercurial/util.py
@@ -2039,8 +2039,94 @@ class hooks(object):
         for source, hook in self._hooks:
             results.append(hook(*args))
         return results
 
+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 through
+    ``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.
+
+    e.g.
+
+    >>> def handler(x, y, z):
+    ...     print '%s %s %s' % (x, y, z)
+    >>> e = event('foo', 'bar')
+    >>> e += handler
+    >>> e('baz')
+    foo bar baz
+    '''
+
+    def __init__(self, *args):
+        # Convert to list to facilitate + operator later.
+        self._args = list(args)
+        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, *args, **kwargs):
+        args = self._args + list(args)
+        for fn in self._handlers:
+            fn(*args, **kwargs)
+
+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.
+
+    Events are registered by calling the ``register`` function. Afterwards,
+    the created ``event`` instance will be available under a property of the
+    registered name.
+
+    e.g.
+
+    >>> m = eventmanager()
+    >>> m.register('oncreate')
+    >>> def handler():
+    ...     print 'hello'
+    >>> m.oncreate += handler
+    >>> m.oncreate()
+    hello
+    '''
+    def register(self, name, *args):
+        """Register an event.
+
+        Events have names and default positional arguments. If positional
+        arguments are defined, these argument will be passed as the first
+        arguments to every invocation of the event.
+
+        A common pattern is to pass the ``self`` handle on whatever object
+        is holding this ``eventmanager`` instance.
+        """
+        if not safehasattr(self, name):
+            setattr(self, name, event(*args))
+
 def debugstacktrace(msg='stacktrace', skip=0, f=sys.stderr, otherf=sys.stdout):
     '''Writes a message to f (stderr) with a nicely formatted stacktrace.
     Skips the 'skip' last entries. By default it will flush stdout first.
     It can be used everywhere and do intentionally not require an ui object.
diff --git a/tests/test-eventmanager.py b/tests/test-eventmanager.py
new file mode 100644
--- /dev/null
+++ b/tests/test-eventmanager.py
@@ -0,0 +1,109 @@
+from mercurial.util import event, eventmanager, safehasattr
+
+import unittest
+import silenttestrunner
+
+class testeventmanager(unittest.TestCase):
+    def test_eventsimple(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 test_eventarguments(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(1, 2, 3)
+        self.assertEqual(calls, {'h1': [(1, 2, 3)], 'h2': [(1, 2, 3)]})
+        e(3, 4, baz=None)
+        self.assertEqual(calls, {'h1': [(1, 2, 3), (3, 4, None)],
+                                 'h2': [(1, 2, 3), (3, 4, None)]})
+        e(5, 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 test_defaultargs(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(o)
+        e += h
+
+        e(1)
+        expected = [2, False]
+        e(2, bar=False)
+
+    def test_eventmanager(self):
+        class eventobject(object):
+            def __init__(self):
+                self.events = eventmanager()
+
+        o = eventobject()
+        self.assertFalse(safehasattr(o.events, 'e1'))
+        try:
+            o.events.e1()
+        except AttributeError:
+            pass
+        o.events.register('e1')
+        self.assertTrue(safehasattr(o.events, 'e1'))
+
+        obj = object()
+        o.events.register('e2', obj)
+        self.assertTrue(safehasattr(o.events, 'e2'))
+
+        calls = {'e1': 0, 'e2': []}
+
+        def e1h():
+            calls['e1'] += 1
+
+        o.events.e1 += e1h
+        o.events.e1()
+        self.assertEqual(calls, {'e1': 1, 'e2': []})
+
+        def e2h(obj2, foo, bar):
+            self.assertEqual(obj2, obj)
+            calls['e2'].append((foo, bar))
+
+        o.events.e2 += e2h
+        o.events.e2('1', '2')
+        self.assertEqual(calls, {'e1': 1, 'e2': [('1', '2')]})
+
+if __name__ == '__main__':
+    silenttestrunner.main(__name__)


More information about the Mercurial-devel mailing list