summaryrefslogtreecommitdiff
path: root/bzrlib/hooks.py
diff options
context:
space:
mode:
authorLorry <lorry@roadtrain.codethink.co.uk>2012-08-22 15:47:16 +0100
committerLorry <lorry@roadtrain.codethink.co.uk>2012-08-22 15:47:16 +0100
commit25335618bf8755ce6b116ee14f47f5a1f2c821e9 (patch)
treed889d7ab3f9f985d0c54c534cb8052bd2e6d7163 /bzrlib/hooks.py
downloadbzr-tarball-25335618bf8755ce6b116ee14f47f5a1f2c821e9.tar.gz
Tarball conversion
Diffstat (limited to 'bzrlib/hooks.py')
-rw-r--r--bzrlib/hooks.py446
1 files changed, 446 insertions, 0 deletions
diff --git a/bzrlib/hooks.py b/bzrlib/hooks.py
new file mode 100644
index 0000000..3e9d7ea
--- /dev/null
+++ b/bzrlib/hooks.py
@@ -0,0 +1,446 @@
+# Copyright (C) 2007-2011 Canonical Ltd
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+"""Support for plugin hooking logic."""
+
+from __future__ import absolute_import
+
+from bzrlib import (
+ registry,
+ symbol_versioning,
+ )
+from bzrlib.lazy_import import lazy_import
+lazy_import(globals(), """
+import textwrap
+
+from bzrlib import (
+ _format_version_tuple,
+ errors,
+ pyutils,
+ )
+from bzrlib.i18n import gettext
+""")
+
+
+class KnownHooksRegistry(registry.Registry):
+ # known_hooks registry contains
+ # tuple of (module, member name) which is the hook point
+ # module where the specific hooks are defined
+ # callable to get the empty specific Hooks for that attribute
+
+ def register_lazy_hook(self, hook_module_name, hook_member_name,
+ hook_factory_member_name):
+ self.register_lazy((hook_module_name, hook_member_name),
+ hook_module_name, hook_factory_member_name)
+
+ def iter_parent_objects(self):
+ """Yield (hook_key, (parent_object, attr)) tuples for every registered
+ hook, where 'parent_object' is the object that holds the hook
+ instance.
+
+ This is useful for resetting/restoring all the hooks to a known state,
+ as is done in bzrlib.tests.TestCase._clear_hooks.
+ """
+ for key in self.keys():
+ yield key, self.key_to_parent_and_attribute(key)
+
+ def key_to_parent_and_attribute(self, (module_name, member_name)):
+ """Convert a known_hooks key to a (parent_obj, attr) pair.
+
+ :param key: A tuple (module_name, member_name) as found in the keys of
+ the known_hooks registry.
+ :return: The parent_object of the hook and the name of the attribute on
+ that parent object where the hook is kept.
+ """
+ parent_mod, parent_member, attr = pyutils.calc_parent_name(module_name,
+ member_name)
+ return pyutils.get_named_object(parent_mod, parent_member), attr
+
+
+_builtin_known_hooks = (
+ ('bzrlib.branch', 'Branch.hooks', 'BranchHooks'),
+ ('bzrlib.controldir', 'ControlDir.hooks', 'ControlDirHooks'),
+ ('bzrlib.commands', 'Command.hooks', 'CommandHooks'),
+ ('bzrlib.config', 'ConfigHooks', '_ConfigHooks'),
+ ('bzrlib.info', 'hooks', 'InfoHooks'),
+ ('bzrlib.lock', 'Lock.hooks', 'LockHooks'),
+ ('bzrlib.merge', 'Merger.hooks', 'MergeHooks'),
+ ('bzrlib.msgeditor', 'hooks', 'MessageEditorHooks'),
+ ('bzrlib.mutabletree', 'MutableTree.hooks', 'MutableTreeHooks'),
+ ('bzrlib.smart.client', '_SmartClient.hooks', 'SmartClientHooks'),
+ ('bzrlib.smart.server', 'SmartTCPServer.hooks', 'SmartServerHooks'),
+ ('bzrlib.status', 'hooks', 'StatusHooks'),
+ ('bzrlib.transport', 'Transport.hooks', 'TransportHooks'),
+ ('bzrlib.version_info_formats.format_rio', 'RioVersionInfoBuilder.hooks',
+ 'RioVersionInfoBuilderHooks'),
+ ('bzrlib.merge_directive', 'BaseMergeDirective.hooks',
+ 'MergeDirectiveHooks'),
+ )
+
+known_hooks = KnownHooksRegistry()
+for (_hook_module, _hook_attribute, _hook_class) in _builtin_known_hooks:
+ known_hooks.register_lazy_hook(_hook_module, _hook_attribute, _hook_class)
+del _builtin_known_hooks, _hook_module, _hook_attribute, _hook_class
+
+
+def known_hooks_key_to_object((module_name, member_name)):
+ """Convert a known_hooks key to a object.
+
+ :param key: A tuple (module_name, member_name) as found in the keys of
+ the known_hooks registry.
+ :return: The object this specifies.
+ """
+ return pyutils.get_named_object(module_name, member_name)
+
+
+class Hooks(dict):
+ """A dictionary mapping hook name to a list of callables.
+
+ e.g. ['FOO'] Is the list of items to be called when the
+ FOO hook is triggered.
+ """
+
+ def __init__(self, module=None, member_name=None):
+ """Create a new hooks dictionary.
+
+ :param module: The module from which this hooks dictionary should be loaded
+ (used for lazy hooks)
+ :param member_name: Name under which this hooks dictionary should be loaded.
+ (used for lazy hooks)
+ """
+ dict.__init__(self)
+ self._callable_names = {}
+ self._lazy_callable_names = {}
+ self._module = module
+ self._member_name = member_name
+
+ def add_hook(self, name, doc, introduced, deprecated=None):
+ """Add a hook point to this dictionary.
+
+ :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.
+ """
+ if name in self:
+ raise errors.DuplicateKey(name)
+ if self._module:
+ callbacks = _lazy_hooks.setdefault(
+ (self._module, self._member_name, name), [])
+ else:
+ callbacks = None
+ hookpoint = HookPoint(name=name, doc=doc, introduced=introduced,
+ deprecated=deprecated, callbacks=callbacks)
+ self[name] = hookpoint
+
+ 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 = []
+ name = self.__class__.__name__
+ hook_docs.append(name)
+ hook_docs.append("-"*len(name))
+ hook_docs.append("")
+ 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" % (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.
+
+ If no name has been registered, the string 'No hook name' is returned.
+ We use a fixed string rather than repr or the callables module because
+ the code names are rarely meaningful for end users and this is not
+ intended for debugging.
+ """
+ name = self._callable_names.get(a_callable, None)
+ if name is None and a_callable is not None:
+ name = self._lazy_callable_names.get((a_callable.__module__,
+ a_callable.__name__),
+ None)
+ if name is None:
+ return 'No hook name'
+ return name
+
+
+ def install_named_hook_lazy(self, hook_name, callable_module,
+ callable_member, name):
+ """Install a_callable in to the hook hook_name lazily, and label it.
+
+ :param hook_name: A hook name. See the __init__ method for the complete
+ list of hooks.
+ :param callable_module: Name of the module in which the callable is
+ present.
+ :param callable_member: Member name of the callable.
+ :param name: A name to associate the callable with, to show users what
+ is running.
+ """
+ try:
+ hook = self[hook_name]
+ except KeyError:
+ raise errors.UnknownHook(self.__class__.__name__, hook_name)
+ try:
+ hook_lazy = getattr(hook, "hook_lazy")
+ except AttributeError:
+ raise errors.UnsupportedOperation(self.install_named_hook_lazy,
+ self)
+ else:
+ hook_lazy(callable_module, callable_member, name)
+ if name is not None:
+ self.name_hook_lazy(callable_module, callable_member, name)
+
+ def install_named_hook(self, hook_name, a_callable, name):
+ """Install a_callable in to the hook hook_name, and label it name.
+
+ :param hook_name: A hook name. See the __init__ method for the complete
+ list of hooks.
+ :param a_callable: The callable to be invoked when the hook triggers.
+ The exact signature will depend on the hook - see the __init__
+ method for details on each hook.
+ :param name: A name to associate a_callable with, to show users what is
+ running.
+ """
+ try:
+ 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 uninstall_named_hook(self, hook_name, label):
+ """Uninstall named hooks.
+
+ :param hook_name: Hook point name
+ :param label: Label of the callable to uninstall
+ """
+ try:
+ hook = self[hook_name]
+ except KeyError:
+ raise errors.UnknownHook(self.__class__.__name__, hook_name)
+ try:
+ uninstall = getattr(hook, "uninstall")
+ except AttributeError:
+ raise errors.UnsupportedOperation(self.uninstall_named_hook, self)
+ else:
+ uninstall(label)
+
+ def name_hook(self, a_callable, name):
+ """Associate name with a_callable to show users what is running."""
+ self._callable_names[a_callable] = name
+
+ def name_hook_lazy(self, callable_module, callable_member, callable_name):
+ self._lazy_callable_names[(callable_module, callable_member)]= \
+ 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 doc: The docs for using 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 superseded in. None indicates that the hook is not
+ superseded or deprecated. If the hook is superseded then the doc
+ should describe the recommended replacement hook to register for.
+ """
+
+ def __init__(self, name, doc, introduced, deprecated=None, callbacks=None):
+ """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
+ if callbacks is None:
+ self._callbacks = []
+ else:
+ self._callbacks = callbacks
+
+ 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(gettext('Introduced in: %s') % introduced_string)
+ if self.deprecated:
+ deprecated_string = _format_version_tuple(self.deprecated)
+ strings.append(gettext('Deprecated in: %s') % deprecated_string)
+ strings.append('')
+ strings.extend(textwrap.wrap(self.__doc__,
+ break_long_words=False))
+ strings.append('')
+ return '\n'.join(strings)
+
+ def __eq__(self, other):
+ return (type(other) == type(self) and other.__dict__ == self.__dict__)
+
+ def hook_lazy(self, callback_module, callback_member, callback_label):
+ """Lazily register a callback to be called when this HookPoint fires.
+
+ :param callback_module: Module of the callable to use when this
+ HookPoint fires.
+ :param callback_member: Member name of the callback.
+ :param callback_label: A label to show in the UI while this callback is
+ processing.
+ """
+ obj_getter = registry._LazyObjectGetter(callback_module,
+ callback_member)
+ self._callbacks.append((obj_getter, callback_label))
+
+ 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.
+ """
+ obj_getter = registry._ObjectGetter(callback)
+ self._callbacks.append((obj_getter, callback_label))
+
+ def uninstall(self, label):
+ """Uninstall the callback with the specified label.
+
+ :param label: Label of the entry to uninstall
+ """
+ entries_to_remove = []
+ for entry in self._callbacks:
+ (entry_callback, entry_label) = entry
+ if entry_label == label:
+ entries_to_remove.append(entry)
+ if entries_to_remove == []:
+ raise KeyError("No entry with label %r" % label)
+ for entry in entries_to_remove:
+ self._callbacks.remove(entry)
+
+ def __iter__(self):
+ return (callback.get_obj() for callback, name in self._callbacks)
+
+ def __len__(self):
+ return len(self._callbacks)
+
+ def __repr__(self):
+ strings = []
+ strings.append("<%s(" % type(self).__name__)
+ strings.append(self.name)
+ strings.append("), callbacks=[")
+ callbacks = self._callbacks
+ for (callback, callback_name) in callbacks:
+ strings.append(repr(callback.get_obj()))
+ strings.append("(")
+ strings.append(callback_name)
+ strings.append("),")
+ if len(callbacks) == 1:
+ strings[-1] = ")"
+ strings.append("]>")
+ return ''.join(strings)
+
+
+_help_prefix = \
+"""
+Hooks
+=====
+
+Introduction
+------------
+
+A hook of type *xxx* of class *yyy* needs to be registered using::
+
+ yyy.hooks.install_named_hook("xxx", ...)
+
+See :doc:`Using hooks<../user-guide/hooks>` in the User Guide for examples.
+
+The class that contains each hook is given before the hooks it supplies. For
+instance, BranchHooks as the class is the hooks class for
+`bzrlib.branch.Branch.hooks`.
+
+Each description also indicates whether the hook runs on the client (the
+machine where bzr was invoked) or the server (the machine addressed by
+the branch URL). These may be, but are not necessarily, the same machine.
+
+Plugins (including hooks) are run on the server if all of these is true:
+
+ * The connection is via a smart server (accessed with a URL starting with
+ "bzr://", "bzr+ssh://" or "bzr+http://", or accessed via a "http://"
+ URL when a smart server is available via HTTP).
+
+ * The hook is either server specific or part of general infrastructure rather
+ than client specific code (such as commit).
+
+"""
+
+def hooks_help_text(topic):
+ segments = [_help_prefix]
+ for hook_key in sorted(known_hooks.keys()):
+ hooks = known_hooks_key_to_object(hook_key)
+ segments.append(hooks.docs())
+ return '\n'.join(segments)
+
+
+# Lazily registered hooks. Maps (module, name, hook_name) tuples
+# to lists of tuples with objectgetters and names
+_lazy_hooks = {}
+
+
+def install_lazy_named_hook(hookpoints_module, hookpoints_name, hook_name,
+ a_callable, name):
+ """Install a callable in to a hook lazily, and label it name.
+
+ :param hookpoints_module: Module name of the hook points.
+ :param hookpoints_name: Name of the hook points.
+ :param hook_name: A hook name.
+ :param callable: a callable to call for the hook.
+ :param name: A name to associate a_callable with, to show users what is
+ running.
+ """
+ key = (hookpoints_module, hookpoints_name, hook_name)
+ obj_getter = registry._ObjectGetter(a_callable)
+ _lazy_hooks.setdefault(key, []).append((obj_getter, name))