1# Copyright (C) 2007-2011 Canonical Ltd 2# 3# This program is free software; you can redistribute it and/or modify 4# it under the terms of the GNU General Public License as published by 5# the Free Software Foundation; either version 2 of the License, or 6# (at your option) any later version. 7# 8# This program is distributed in the hope that it will be useful, 9# but WITHOUT ANY WARRANTY; without even the implied warranty of 10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11# GNU General Public License for more details. 12# 13# You should have received a copy of the GNU General Public License 14# along with this program; if not, write to the Free Software 15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 16 17"""Support for plugin hooking logic.""" 18 19from . import ( 20 errors, 21 registry, 22 ) 23from .lazy_import import lazy_import 24lazy_import(globals(), """ 25import textwrap 26 27from breezy import ( 28 _format_version_tuple, 29 pyutils, 30 ) 31from breezy.i18n import gettext 32""") 33 34 35class UnknownHook(errors.BzrError): 36 37 _fmt = "The %(type)s hook '%(hook)s' is unknown in this version of breezy." 38 39 def __init__(self, hook_type, hook_name): 40 errors.BzrError.__init__(self) 41 self.type = hook_type 42 self.hook = hook_name 43 44 45class KnownHooksRegistry(registry.Registry): 46 # known_hooks registry contains 47 # tuple of (module, member name) which is the hook point 48 # module where the specific hooks are defined 49 # callable to get the empty specific Hooks for that attribute 50 51 def register_lazy_hook(self, hook_module_name, hook_member_name, 52 hook_factory_member_name): 53 self.register_lazy((hook_module_name, hook_member_name), 54 hook_module_name, hook_factory_member_name) 55 56 def iter_parent_objects(self): 57 """Yield (hook_key, (parent_object, attr)) tuples for every registered 58 hook, where 'parent_object' is the object that holds the hook 59 instance. 60 61 This is useful for resetting/restoring all the hooks to a known state, 62 as is done in breezy.tests.TestCase._clear_hooks. 63 """ 64 for key in self.keys(): 65 yield key, self.key_to_parent_and_attribute(key) 66 67 def key_to_parent_and_attribute(self, key): 68 """Convert a known_hooks key to a (parent_obj, attr) pair. 69 70 :param key: A tuple (module_name, member_name) as found in the keys of 71 the known_hooks registry. 72 :return: The parent_object of the hook and the name of the attribute on 73 that parent object where the hook is kept. 74 """ 75 parent_mod, parent_member, attr = pyutils.calc_parent_name(*key) 76 return pyutils.get_named_object(parent_mod, parent_member), attr 77 78 79_builtin_known_hooks = ( 80 ('breezy.branch', 'Branch.hooks', 'BranchHooks'), 81 ('breezy.controldir', 'ControlDir.hooks', 'ControlDirHooks'), 82 ('breezy.commands', 'Command.hooks', 'CommandHooks'), 83 ('breezy.config', 'ConfigHooks', '_ConfigHooks'), 84 ('breezy.info', 'hooks', 'InfoHooks'), 85 ('breezy.lock', 'Lock.hooks', 'LockHooks'), 86 ('breezy.merge', 'Merger.hooks', 'MergeHooks'), 87 ('breezy.msgeditor', 'hooks', 'MessageEditorHooks'), 88 ('breezy.mutabletree', 'MutableTree.hooks', 'MutableTreeHooks'), 89 ('breezy.bzr.smart.client', '_SmartClient.hooks', 'SmartClientHooks'), 90 ('breezy.bzr.smart.server', 'SmartTCPServer.hooks', 'SmartServerHooks'), 91 ('breezy.status', 'hooks', 'StatusHooks'), 92 ('breezy.transport', 'Transport.hooks', 'TransportHooks'), 93 ('breezy.version_info_formats.format_rio', 'RioVersionInfoBuilder.hooks', 94 'RioVersionInfoBuilderHooks'), 95 ('breezy.merge_directive', 'BaseMergeDirective.hooks', 96 'MergeDirectiveHooks'), 97 ) 98 99known_hooks = KnownHooksRegistry() 100for (_hook_module, _hook_attribute, _hook_class) in _builtin_known_hooks: 101 known_hooks.register_lazy_hook(_hook_module, _hook_attribute, _hook_class) 102del _builtin_known_hooks, _hook_module, _hook_attribute, _hook_class 103 104 105def known_hooks_key_to_object(key): 106 """Convert a known_hooks key to a object. 107 108 :param key: A tuple (module_name, member_name) as found in the keys of 109 the known_hooks registry. 110 :return: The object this specifies. 111 """ 112 return pyutils.get_named_object(*key) 113 114 115class Hooks(dict): 116 """A dictionary mapping hook name to a list of callables. 117 118 e.g. ['FOO'] Is the list of items to be called when the 119 FOO hook is triggered. 120 """ 121 122 def __init__(self, module=None, member_name=None): 123 """Create a new hooks dictionary. 124 125 :param module: The module from which this hooks dictionary should be loaded 126 (used for lazy hooks) 127 :param member_name: Name under which this hooks dictionary should be loaded. 128 (used for lazy hooks) 129 """ 130 dict.__init__(self) 131 self._callable_names = {} 132 self._lazy_callable_names = {} 133 self._module = module 134 self._member_name = member_name 135 136 def add_hook(self, name, doc, introduced, deprecated=None): 137 """Add a hook point to this dictionary. 138 139 :param name: The name of the hook, for clients to use when registering. 140 :param doc: The docs for the hook. 141 :param introduced: When the hook was introduced (e.g. (0, 15)). 142 :param deprecated: When the hook was deprecated, None for 143 not-deprecated. 144 """ 145 if name in self: 146 raise errors.DuplicateKey(name) 147 if self._module: 148 callbacks = _lazy_hooks.setdefault( 149 (self._module, self._member_name, name), []) 150 else: 151 callbacks = None 152 hookpoint = HookPoint(name=name, doc=doc, introduced=introduced, 153 deprecated=deprecated, callbacks=callbacks) 154 self[name] = hookpoint 155 156 def docs(self): 157 """Generate the documentation for this Hooks instance. 158 159 This introspects all the individual hooks and returns their docs as well. 160 """ 161 hook_names = sorted(self.keys()) 162 hook_docs = [] 163 name = self.__class__.__name__ 164 hook_docs.append(name) 165 hook_docs.append("-" * len(name)) 166 hook_docs.append("") 167 for hook_name in hook_names: 168 hook = self[hook_name] 169 try: 170 hook_docs.append(hook.docs()) 171 except AttributeError: 172 # legacy hook 173 strings = [] 174 strings.append(hook_name) 175 strings.append("~" * len(hook_name)) 176 strings.append("") 177 strings.append("An old-style hook. For documentation see the __init__ " 178 "method of '%s'\n" % (name,)) 179 hook_docs.extend(strings) 180 return "\n".join(hook_docs) 181 182 def get_hook_name(self, a_callable): 183 """Get the name for a_callable for UI display. 184 185 If no name has been registered, the string 'No hook name' is returned. 186 We use a fixed string rather than repr or the callables module because 187 the code names are rarely meaningful for end users and this is not 188 intended for debugging. 189 """ 190 name = self._callable_names.get(a_callable, None) 191 if name is None and a_callable is not None: 192 name = self._lazy_callable_names.get((a_callable.__module__, 193 a_callable.__name__), 194 None) 195 if name is None: 196 return 'No hook name' 197 return name 198 199 def install_named_hook_lazy(self, hook_name, callable_module, 200 callable_member, name): 201 """Install a_callable in to the hook hook_name lazily, and label it. 202 203 :param hook_name: A hook name. See the __init__ method for the complete 204 list of hooks. 205 :param callable_module: Name of the module in which the callable is 206 present. 207 :param callable_member: Member name of the callable. 208 :param name: A name to associate the callable with, to show users what 209 is running. 210 """ 211 try: 212 hook = self[hook_name] 213 except KeyError: 214 raise UnknownHook(self.__class__.__name__, hook_name) 215 try: 216 hook_lazy = getattr(hook, "hook_lazy") 217 except AttributeError: 218 raise errors.UnsupportedOperation(self.install_named_hook_lazy, 219 self) 220 else: 221 hook_lazy(callable_module, callable_member, name) 222 if name is not None: 223 self.name_hook_lazy(callable_module, callable_member, name) 224 225 def install_named_hook(self, hook_name, a_callable, name): 226 """Install a_callable in to the hook hook_name, and label it name. 227 228 :param hook_name: A hook name. See the __init__ method for the complete 229 list of hooks. 230 :param a_callable: The callable to be invoked when the hook triggers. 231 The exact signature will depend on the hook - see the __init__ 232 method for details on each hook. 233 :param name: A name to associate a_callable with, to show users what is 234 running. 235 """ 236 try: 237 hook = self[hook_name] 238 except KeyError: 239 raise UnknownHook(self.__class__.__name__, hook_name) 240 try: 241 # list hooks, old-style, not yet deprecated but less useful. 242 hook.append(a_callable) 243 except AttributeError: 244 hook.hook(a_callable, name) 245 if name is not None: 246 self.name_hook(a_callable, name) 247 248 def uninstall_named_hook(self, hook_name, label): 249 """Uninstall named hooks. 250 251 :param hook_name: Hook point name 252 :param label: Label of the callable to uninstall 253 """ 254 try: 255 hook = self[hook_name] 256 except KeyError: 257 raise UnknownHook(self.__class__.__name__, hook_name) 258 try: 259 uninstall = getattr(hook, "uninstall") 260 except AttributeError: 261 raise errors.UnsupportedOperation(self.uninstall_named_hook, self) 262 else: 263 uninstall(label) 264 265 def name_hook(self, a_callable, name): 266 """Associate name with a_callable to show users what is running.""" 267 self._callable_names[a_callable] = name 268 269 def name_hook_lazy(self, callable_module, callable_member, callable_name): 270 self._lazy_callable_names[(callable_module, callable_member)] = \ 271 callable_name 272 273 274class HookPoint(object): 275 """A single hook that clients can register to be called back when it fires. 276 277 :ivar name: The name of the hook. 278 :ivar doc: The docs for using the hook. 279 :ivar introduced: A version tuple specifying what version the hook was 280 introduced in. None indicates an unknown version. 281 :ivar deprecated: A version tuple specifying what version the hook was 282 deprecated or superseded in. None indicates that the hook is not 283 superseded or deprecated. If the hook is superseded then the doc 284 should describe the recommended replacement hook to register for. 285 """ 286 287 def __init__(self, name, doc, introduced, deprecated=None, callbacks=None): 288 """Create a HookPoint. 289 290 :param name: The name of the hook, for clients to use when registering. 291 :param doc: The docs for the hook. 292 :param introduced: When the hook was introduced (e.g. (0, 15)). 293 :param deprecated: When the hook was deprecated, None for 294 not-deprecated. 295 """ 296 self.name = name 297 self.__doc__ = doc 298 self.introduced = introduced 299 self.deprecated = deprecated 300 if callbacks is None: 301 self._callbacks = [] 302 else: 303 self._callbacks = callbacks 304 305 def docs(self): 306 """Generate the documentation for this HookPoint. 307 308 :return: A string terminated in \n. 309 """ 310 strings = [] 311 strings.append(self.name) 312 strings.append('~' * len(self.name)) 313 strings.append('') 314 if self.introduced: 315 introduced_string = _format_version_tuple(self.introduced) 316 else: 317 introduced_string = 'unknown' 318 strings.append(gettext('Introduced in: %s') % introduced_string) 319 if self.deprecated: 320 deprecated_string = _format_version_tuple(self.deprecated) 321 strings.append(gettext('Deprecated in: %s') % deprecated_string) 322 strings.append('') 323 strings.extend(textwrap.wrap(self.__doc__, 324 break_long_words=False)) 325 strings.append('') 326 return '\n'.join(strings) 327 328 def __eq__(self, other): 329 return (isinstance(other, type(self)) and other.__dict__ == self.__dict__) 330 331 def hook_lazy(self, callback_module, callback_member, callback_label): 332 """Lazily register a callback to be called when this HookPoint fires. 333 334 :param callback_module: Module of the callable to use when this 335 HookPoint fires. 336 :param callback_member: Member name of the callback. 337 :param callback_label: A label to show in the UI while this callback is 338 processing. 339 """ 340 obj_getter = registry._LazyObjectGetter(callback_module, 341 callback_member) 342 self._callbacks.append((obj_getter, callback_label)) 343 344 def hook(self, callback, callback_label): 345 """Register a callback to be called when this HookPoint fires. 346 347 :param callback: The callable to use when this HookPoint fires. 348 :param callback_label: A label to show in the UI while this callback is 349 processing. 350 """ 351 obj_getter = registry._ObjectGetter(callback) 352 self._callbacks.append((obj_getter, callback_label)) 353 354 def uninstall(self, label): 355 """Uninstall the callback with the specified label. 356 357 :param label: Label of the entry to uninstall 358 """ 359 entries_to_remove = [] 360 for entry in self._callbacks: 361 (entry_callback, entry_label) = entry 362 if entry_label == label: 363 entries_to_remove.append(entry) 364 if entries_to_remove == []: 365 raise KeyError("No entry with label %r" % label) 366 for entry in entries_to_remove: 367 self._callbacks.remove(entry) 368 369 def __iter__(self): 370 return (callback.get_obj() for callback, name in self._callbacks) 371 372 def __len__(self): 373 return len(self._callbacks) 374 375 def __repr__(self): 376 strings = [] 377 strings.append("<%s(" % type(self).__name__) 378 strings.append(self.name) 379 strings.append("), callbacks=[") 380 callbacks = self._callbacks 381 for (callback, callback_name) in callbacks: 382 strings.append(repr(callback.get_obj())) 383 strings.append("(") 384 strings.append(callback_name) 385 strings.append("),") 386 if len(callbacks) == 1: 387 strings[-1] = ")" 388 strings.append("]>") 389 return ''.join(strings) 390 391 392_help_prefix = \ 393 """ 394Hooks 395===== 396 397Introduction 398------------ 399 400A hook of type *xxx* of class *yyy* needs to be registered using:: 401 402 yyy.hooks.install_named_hook("xxx", ...) 403 404See :doc:`Using hooks<../user-guide/hooks>` in the User Guide for examples. 405 406The class that contains each hook is given before the hooks it supplies. For 407instance, BranchHooks as the class is the hooks class for 408`breezy.branch.Branch.hooks`. 409 410Each description also indicates whether the hook runs on the client (the 411machine where bzr was invoked) or the server (the machine addressed by 412the branch URL). These may be, but are not necessarily, the same machine. 413 414Plugins (including hooks) are run on the server if all of these is true: 415 416 * The connection is via a smart server (accessed with a URL starting with 417 "bzr://", "bzr+ssh://" or "bzr+http://", or accessed via a "http://" 418 URL when a smart server is available via HTTP). 419 420 * The hook is either server specific or part of general infrastructure rather 421 than client specific code (such as commit). 422 423""" 424 425 426def hooks_help_text(topic): 427 segments = [_help_prefix] 428 for hook_key in sorted(known_hooks.keys()): 429 hooks = known_hooks_key_to_object(hook_key) 430 segments.append(hooks.docs()) 431 return '\n'.join(segments) 432 433 434# Lazily registered hooks. Maps (module, name, hook_name) tuples 435# to lists of tuples with objectgetters and names 436_lazy_hooks = {} 437 438 439def install_lazy_named_hook(hookpoints_module, hookpoints_name, hook_name, 440 a_callable, name): 441 """Install a callable in to a hook lazily, and label it name. 442 443 :param hookpoints_module: Module name of the hook points. 444 :param hookpoints_name: Name of the hook points. 445 :param hook_name: A hook name. 446 :param callable: a callable to call for the hook. 447 :param name: A name to associate a_callable with, to show users what is 448 running. 449 """ 450 key = (hookpoints_module, hookpoints_name, hook_name) 451 obj_getter = registry._ObjectGetter(a_callable) 452 _lazy_hooks.setdefault(key, []).append((obj_getter, name)) 453