Rev 4104: (robertc) Allow Hooks to be self documenting. (Robert Collins) in file:///home/pqm/archives/thelove/bzr/%2Btrunk/
Canonical.com Patch Queue Manager
pqm at pqm.ubuntu.com
Tue Mar 10 06:07:41 GMT 2009
At file:///home/pqm/archives/thelove/bzr/%2Btrunk/
------------------------------------------------------------
revno: 4104
revision-id: pqm at pqm.ubuntu.com-20090310060738-6js2ofvx7q1gfg63
parent: pqm at pqm.ubuntu.com-20090310052823-5h4znt0j8j5ak38o
parent: robertc at robertcollins.net-20090310051431-ak48mkkj5xzf2m0p
committer: Canonical.com Patch Queue Manager <pqm at pqm.ubuntu.com>
branch nick: +trunk
timestamp: Tue 2009-03-10 06:07:38 +0000
message:
(robertc) Allow Hooks to be self documenting. (Robert Collins)
modified:
NEWS NEWS-20050323055033-4e00b5db738777ff
bzrlib/__init__.py __init__.py-20050309040759-33e65acf91bbcd5d
bzrlib/hooks.py hooks.py-20070325015548-ix4np2q0kd8452au-1
bzrlib/tests/test_hooks.py test_hooks.py-20070628030849-89rtsbe5dmer5npz-1
------------------------------------------------------------
revno: 4098.2.3
revision-id: robertc at robertcollins.net-20090310051431-ak48mkkj5xzf2m0p
parent: robertc at robertcollins.net-20090310030521-1ew21nsm0ktgcv26
committer: Robert Collins <robertc at robertcollins.net>
branch nick: integration
timestamp: Tue 2009-03-10 16:14:31 +1100
message:
White space difference-of-opinion.
modified:
bzrlib/hooks.py hooks.py-20070325015548-ix4np2q0kd8452au-1
------------------------------------------------------------
revno: 4098.2.2
revision-id: robertc at robertcollins.net-20090310030521-1ew21nsm0ktgcv26
parent: robertc at robertcollins.net-20090310011651-73eyp8l970t21ah8
committer: Robert Collins <robertc at robertcollins.net>
branch nick: Hooks.docs
timestamp: Tue 2009-03-10 14:05:21 +1100
message:
Review feedback.
modified:
bzrlib/hooks.py hooks.py-20070325015548-ix4np2q0kd8452au-1
bzrlib/tests/test_hooks.py test_hooks.py-20070628030849-89rtsbe5dmer5npz-1
------------------------------------------------------------
revno: 4098.2.1
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:
NEWS NEWS-20050323055033-4e00b5db738777ff
bzrlib/__init__.py __init__.py-20050309040759-33e65acf91bbcd5d
bzrlib/hooks.py hooks.py-20070325015548-ix4np2q0kd8452au-1
bzrlib/tests/test_hooks.py test_hooks.py-20070628030849-89rtsbe5dmer5npz-1
=== modified file 'NEWS'
--- a/NEWS 2009-03-10 02:44:32 +0000
+++ b/NEWS 2009-03-10 06:07:38 +0000
@@ -144,6 +144,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 05:14:31 +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.HookPoint. 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 HookPoint(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 HookPoint.
+
+ :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 HookPoint.
+
+ :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):
+ """Register a callback to be called when this HookPoint fires.
+
+ :param callback: The callable to use when this HookPoint 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("<%s(" % type(self).__name__)
+ 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 03:05:21 +0000
@@ -16,7 +16,9 @@
"""Tests for the core Hooks logic."""
+from bzrlib import errors
from bzrlib.hooks import (
+ HookPoint,
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 = HookPoint("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 = HookPoint("post_tip_change", doc, (0, 15), None)
+ hook2 = HookPoint("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 = HookPoint('post_tip_change',
+ "Invoked after the tip of a branch changes. Called with "
+ "a ChangeBranchTipParams object.", (1, 4), None)
+ hook2 = HookPoint('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 = HookPoint("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 = HookPoint("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 = HookPoint("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 = HookPoint("foo", "no docs", None, None)
+ def callback():
+ pass
+ hook.hook(callback, "my callback")
+ callback_repr = repr(callback)
+ self.assertEqual(
+ '<HookPoint(foo), callbacks=[%s(my callback)]>' %
+ callback_repr, repr(hook))
More information about the bazaar-commits
mailing list