Rev 4099: Allow self documenting hooks. in http://people.ubuntu.com/~robertc/baz2.0/pending/Hooks.docs

Robert Collins robertc at robertcollins.net
Tue Mar 10 01:16:54 GMT 2009


At http://people.ubuntu.com/~robertc/baz2.0/pending/Hooks.docs

------------------------------------------------------------
revno: 4099
revision-id: robertc at robertcollins.net-20090310011651-73eyp8l970t21ah8
parent: pqm at pqm.ubuntu.com-20090309084556-9i2m12qlud2qcrtw
committer: Robert Collins <robertc at robertcollins.net>
branch nick: Hooks.docs
timestamp: Tue 2009-03-10 12:16:51 +1100
message:
  Allow self documenting hooks.
=== modified file 'NEWS'
--- a/NEWS	2009-03-09 08:45:56 +0000
+++ b/NEWS	2009-03-10 01:16:51 +0000
@@ -135,6 +135,9 @@
       repositories and the processes/workflows implied/enabled by each.
       (Ian Clatworthy)
 
+    * Hooks can now be self documenting. ``bzrlib.hooks.Hooks.create_hook``
+      is the entry point for this feature. (Robert Collins)
+
     * The documentation for ``shelve`` and ``unshelve`` has been clarified.
       (Daniel Watkins, #327421, #327425)
 

=== modified file 'bzrlib/__init__.py'
--- a/bzrlib/__init__.py	2009-02-25 22:17:25 +0000
+++ b/bzrlib/__init__.py	2009-03-10 01:16:51 +0000
@@ -58,7 +58,7 @@
 
 
 def _format_version_tuple(version_info):
-    """Turn a version number 3-tuple or 5-tuple into a short string.
+    """Turn a version number 2, 3 or 5-tuple into a short string.
 
     This format matches <http://docs.python.org/dist/meta-data.html>
     and the typical presentation used in Python output.
@@ -74,12 +74,14 @@
     1.1.1rc2
     >>> print _format_version_tuple((1, 4, 0))
     1.4
+    >>> print _format_version_tuple((1, 4))
+    1.4
     >>> print _format_version_tuple((1, 4, 0, 'wibble', 0))
     Traceback (most recent call last):
     ...
     ValueError: version_info (1, 4, 0, 'wibble', 0) not valid
     """
-    if version_info[2] == 0:
+    if len(version_info) == 2 or version_info[2] == 0:
         main_version = '%d.%d' % version_info[:2]
     else:
         main_version = '%d.%d.%d' % version_info[:3]

=== modified file 'bzrlib/hooks.py'
--- a/bzrlib/hooks.py	2009-01-17 01:30:58 +0000
+++ b/bzrlib/hooks.py	2009-03-10 01:16:51 +0000
@@ -19,7 +19,10 @@
 from bzrlib.lazy_import import lazy_import
 from bzrlib.symbol_versioning import deprecated_method, one_five
 lazy_import(globals(), """
+import textwrap
+
 from bzrlib import (
+        _format_version_tuple,
         errors,
         )
 """)
@@ -36,6 +39,39 @@
         dict.__init__(self)
         self._callable_names = {}
 
+    def create_hook(self, hook):
+        """Create a hook which can have callbacks registered for it.
+
+        :param hook: The hook to create. An object meeting the protocol of
+            bzrlib.hooks.Hook. It's name is used as the key for future
+            lookups.
+        """
+        if hook.name in self:
+            raise errors.DuplicateKey(hook.name)
+        self[hook.name] = hook
+
+    def docs(self):
+        """Generate the documentation for this Hooks instance.
+
+        This introspects all the individual hooks and returns their docs as well.
+        """
+        hook_names = sorted(self.keys())
+        hook_docs = []
+        for hook_name in hook_names:
+            hook = self[hook_name]
+            try:
+                hook_docs.append(hook.docs())
+            except AttributeError:
+                # legacy hook
+                strings = []
+                strings.append(hook_name)
+                strings.append("-" * len(hook_name))
+                strings.append("")
+                strings.append("An old-style hook. For documentation see the __init__ "
+                    "method of '%s'\n" % (self.__class__.__name__,))
+                hook_docs.extend(strings)
+        return "\n".join(hook_docs)
+
     def get_hook_name(self, a_callable):
         """Get the name for a_callable for UI display.
 
@@ -70,12 +106,99 @@
             running.
         """
         try:
-            self[hook_name].append(a_callable)
+            hook = self[hook_name]
         except KeyError:
             raise errors.UnknownHook(self.__class__.__name__, hook_name)
+        try:
+            # list hooks, old-style, not yet deprecated but less useful.
+            hook.append(a_callable)
+        except AttributeError:
+            hook.hook(a_callable, name)
         if name is not None:
             self.name_hook(a_callable, name)
 
     def name_hook(self, a_callable, name):
         """Associate name with a_callable to show users what is running."""
         self._callable_names[a_callable] = name
+
+
+class Hook(object):
+    """A single hook that clients can register to be called back when it fires.
+
+    :ivar name: The name of the hook.
+    :ivar introduced: A version tuple specifying what version the hook was
+        introduced in. None indicates an unknown version.
+    :ivar deprecated: A version tuple specifying what version the hook was
+        deprecated or superceded in. None indicates that the hook is not
+        superceded or deprecated. If the hook is superceded then the doc
+        should describe the recommended replacement hook to register for.
+    :ivar doc: The docs for using the hook.
+    """
+
+    def __init__(self, name, doc, introduced, deprecated):
+        """Create a Hook.
+        
+        :param name: The name of the hook, for clients to use when registering.
+        :param doc: The docs for the hook.
+        :param introduced: When the hook was introduced (e.g. (0, 15)).
+        :param deprecated: When the hook was deprecated, None for
+            not-deprecated.
+        """
+        self.name = name
+        self.__doc__ = doc
+        self.introduced = introduced
+        self.deprecated = deprecated
+        self._callbacks = []
+        self._callback_names = {}
+
+    def docs(self):
+        """Generate the documentation for this Hook.
+        
+        :return: A string terminated in \n.
+        """
+        strings = []
+        strings.append(self.name)
+        strings.append('-'*len(self.name))
+        strings.append('')
+        if self.introduced:
+            introduced_string = _format_version_tuple(self.introduced)
+        else:
+            introduced_string = 'unknown'
+        strings.append('Introduced in: %s' % introduced_string)
+        if self.deprecated:
+            deprecated_string = _format_version_tuple(self.deprecated)
+        else:
+            deprecated_string = 'Not deprecated'
+        strings.append('Deprecated in: %s' % deprecated_string)
+        strings.append('')
+        strings.extend(textwrap.wrap(self.__doc__))
+        strings.append('')
+        return '\n'.join(strings)
+
+    def hook(self, callback, callback_label):
+        """Call this hook with callback, using callback_label to describe it.
+
+        :param callback: The callable to use when this Hook fires.
+        :param callback_label: A label to show in the UI while this callback is
+            processing.
+        """
+        self._callbacks.append(callback)
+        self._callback_names[callback] = callback_label
+
+    def __iter__(self):
+        return iter(self._callbacks)
+
+    def __repr__(self):
+        strings = []
+        strings.append("<bzrlib.hooks.Hook(")
+        strings.append(self.name)
+        strings.append("), callbacks=[")
+        for callback in self._callbacks:
+            strings.append(repr(callback))
+            strings.append("(")
+            strings.append(self._callback_names[callback])
+            strings.append("),")
+        if len(self._callbacks) == 1:
+            strings[-1] = ")"
+        strings.append("]>")
+        return ''.join(strings)

=== modified file 'bzrlib/tests/test_hooks.py'
--- a/bzrlib/tests/test_hooks.py	2008-09-23 05:37:53 +0000
+++ b/bzrlib/tests/test_hooks.py	2009-03-10 01:16:51 +0000
@@ -16,7 +16,9 @@
 
 """Tests for the core Hooks logic."""
 
+from bzrlib import errors
 from bzrlib.hooks import (
+    Hook,
     Hooks,
     )
 from bzrlib.errors import (
@@ -29,6 +31,63 @@
 
 class TestHooks(TestCase):
 
+    def test_create_hook_first(self):
+        hooks = Hooks()
+        doc = ("Invoked after changing the tip of a branch object. Called with"
+            "a bzrlib.branch.PostChangeBranchTipParams object")
+        hook = Hook("post_tip_change", doc, (0, 15), None)
+        hooks.create_hook(hook)
+        self.assertEqual(hook, hooks['post_tip_change'])
+
+    def test_create_hook_name_collision_errors(self):
+        hooks = Hooks()
+        doc = ("Invoked after changing the tip of a branch object. Called with"
+            "a bzrlib.branch.PostChangeBranchTipParams object")
+        hook = Hook("post_tip_change", doc, (0, 15), None)
+        hook2 = Hook("post_tip_change", None, None, None)
+        hooks.create_hook(hook)
+        self.assertRaises(errors.DuplicateKey, hooks.create_hook, hook2)
+        self.assertEqual(hook, hooks['post_tip_change'])
+
+    def test_docs(self):
+        """docs() should return something reasonable about the Hooks."""
+        hooks = Hooks()
+        hooks['legacy'] = []
+        hook1 = Hook('post_tip_change',
+            "Invoked after the tip of a branch changes. Called with "
+            "a ChangeBranchTipParams object.", (1, 4), None)
+        hook2 = Hook('pre_tip_change',
+            "Invoked before the tip of a branch changes. Called with "
+            "a ChangeBranchTipParams object. Hooks should raise "
+            "TipChangeRejected to signal that a tip change is not permitted.",
+            (1, 6), None)
+        hooks.create_hook(hook1)
+        hooks.create_hook(hook2)
+        self.assertEqual(
+            "legacy\n"
+            "------\n"
+            "\n"
+            "An old-style hook. For documentation see the __init__ method of 'Hooks'\n"
+            "\n"
+            "post_tip_change\n"
+            "---------------\n"
+            "\n"
+            "Introduced in: 1.4\n"
+            "Deprecated in: Not deprecated\n"
+            "\n"
+            "Invoked after the tip of a branch changes. Called with a\n"
+            "ChangeBranchTipParams object.\n"
+            "\n"
+            "pre_tip_change\n"
+            "--------------\n"
+            "\n"
+            "Introduced in: 1.6\n"
+            "Deprecated in: Not deprecated\n"
+            "\n"
+            "Invoked before the tip of a branch changes. Called with a\n"
+            "ChangeBranchTipParams object. Hooks should raise TipChangeRejected to\n"
+            "signal that a tip change is not permitted.\n", hooks.docs())
+
     def test_install_hook_raises_unknown_hook(self):
         """install_hook should raise UnknownHook if a hook is unknown."""
         hooks = Hooks()
@@ -72,3 +131,47 @@
         hooks['set_rh'] = []
         self.applyDeprecated(one_five, hooks.install_hook, 'set_rh', None)
         self.assertEqual("No hook name", hooks.get_hook_name(None))
+
+
+class TestHook(TestCase):
+
+    def test___init__(self):
+        doc = ("Invoked after changing the tip of a branch object. Called with"
+            "a bzrlib.branch.PostChangeBranchTipParams object")
+        hook = Hook("post_tip_change", doc, (0, 15), None)
+        self.assertEqual(doc, hook.__doc__)
+        self.assertEqual("post_tip_change", hook.name)
+        self.assertEqual((0, 15), hook.introduced)
+        self.assertEqual(None, hook.deprecated)
+        self.assertEqual([], list(hook))
+
+    def test_docs(self):
+        doc = ("Invoked after changing the tip of a branch object. Called with"
+            " a bzrlib.branch.PostChangeBranchTipParams object")
+        hook = Hook("post_tip_change", doc, (0, 15), None)
+        self.assertEqual("post_tip_change\n"
+            "---------------\n"
+            "\n"
+            "Introduced in: 0.15\n"
+            "Deprecated in: Not deprecated\n"
+            "\n"
+            "Invoked after changing the tip of a branch object. Called with a\n"
+            "bzrlib.branch.PostChangeBranchTipParams object\n", hook.docs())
+
+    def test_hook(self):
+        hook = Hook("foo", "no docs", None, None)
+        def callback():
+            pass
+        hook.hook(callback, "my callback")
+        self.assertEqual([callback], list(hook))
+
+    def test___repr(self):
+        # The repr should list all the callbacks, with names.
+        hook = Hook("foo", "no docs", None, None)
+        def callback():
+            pass
+        hook.hook(callback, "my callback")
+        callback_repr = repr(callback)
+        self.assertEqual(
+            '<bzrlib.hooks.Hook(foo), callbacks=[%s(my callback)]>' %
+            callback_repr, repr(hook))




More information about the bazaar-commits mailing list