13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18
17
"""Support for plugin hooking logic."""
19
from __future__ import absolute_import
19
25
from bzrlib.lazy_import import lazy_import
20
26
lazy_import(globals(), """
21
29
from bzrlib import (
30
_format_version_tuple,
34
from bzrlib.i18n import gettext
38
class KnownHooksRegistry(registry.Registry):
39
# known_hooks registry contains
40
# tuple of (module, member name) which is the hook point
41
# module where the specific hooks are defined
42
# callable to get the empty specific Hooks for that attribute
44
def register_lazy_hook(self, hook_module_name, hook_member_name,
45
hook_factory_member_name):
46
self.register_lazy((hook_module_name, hook_member_name),
47
hook_module_name, hook_factory_member_name)
49
def iter_parent_objects(self):
50
"""Yield (hook_key, (parent_object, attr)) tuples for every registered
51
hook, where 'parent_object' is the object that holds the hook
54
This is useful for resetting/restoring all the hooks to a known state,
55
as is done in bzrlib.tests.TestCase._clear_hooks.
57
for key in self.keys():
58
yield key, self.key_to_parent_and_attribute(key)
60
def key_to_parent_and_attribute(self, (module_name, member_name)):
61
"""Convert a known_hooks key to a (parent_obj, attr) pair.
63
:param key: A tuple (module_name, member_name) as found in the keys of
64
the known_hooks registry.
65
:return: The parent_object of the hook and the name of the attribute on
66
that parent object where the hook is kept.
68
parent_mod, parent_member, attr = pyutils.calc_parent_name(module_name,
70
return pyutils.get_named_object(parent_mod, parent_member), attr
73
_builtin_known_hooks = (
74
('bzrlib.branch', 'Branch.hooks', 'BranchHooks'),
75
('bzrlib.controldir', 'ControlDir.hooks', 'ControlDirHooks'),
76
('bzrlib.commands', 'Command.hooks', 'CommandHooks'),
77
('bzrlib.config', 'ConfigHooks', '_ConfigHooks'),
78
('bzrlib.info', 'hooks', 'InfoHooks'),
79
('bzrlib.lock', 'Lock.hooks', 'LockHooks'),
80
('bzrlib.merge', 'Merger.hooks', 'MergeHooks'),
81
('bzrlib.msgeditor', 'hooks', 'MessageEditorHooks'),
82
('bzrlib.mutabletree', 'MutableTree.hooks', 'MutableTreeHooks'),
83
('bzrlib.smart.client', '_SmartClient.hooks', 'SmartClientHooks'),
84
('bzrlib.smart.server', 'SmartTCPServer.hooks', 'SmartServerHooks'),
85
('bzrlib.status', 'hooks', 'StatusHooks'),
86
('bzrlib.transport', 'Transport.hooks', 'TransportHooks'),
87
('bzrlib.version_info_formats.format_rio', 'RioVersionInfoBuilder.hooks',
88
'RioVersionInfoBuilderHooks'),
89
('bzrlib.merge_directive', 'BaseMergeDirective.hooks',
90
'MergeDirectiveHooks'),
93
known_hooks = KnownHooksRegistry()
94
for (_hook_module, _hook_attribute, _hook_class) in _builtin_known_hooks:
95
known_hooks.register_lazy_hook(_hook_module, _hook_attribute, _hook_class)
96
del _builtin_known_hooks, _hook_module, _hook_attribute, _hook_class
99
def known_hooks_key_to_object((module_name, member_name)):
100
"""Convert a known_hooks key to a object.
102
:param key: A tuple (module_name, member_name) as found in the keys of
103
the known_hooks registry.
104
:return: The object this specifies.
106
return pyutils.get_named_object(module_name, member_name)
109
@symbol_versioning.deprecated_function(symbol_versioning.deprecated_in((2, 3)))
110
def known_hooks_key_to_parent_and_attribute(key):
111
"""See KnownHooksRegistry.key_to_parent_and_attribute."""
112
return known_hooks.key_to_parent_and_attribute(key)
27
115
class Hooks(dict):
28
116
"""A dictionary mapping hook name to a list of callables.
30
118
e.g. ['FOO'] Is the list of items to be called when the
31
119
FOO hook is triggered.
34
def install_hook(self, hook_name, a_callable):
35
"""Install a_callable in to the hook hook_name.
37
:param hook_name: A hook name. See the __init__ method of BranchHooks
38
for the complete list of hooks.
122
def __init__(self, module=None, member_name=None):
123
"""Create a new hooks dictionary.
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)
131
self._callable_names = {}
132
self._lazy_callable_names = {}
133
self._module = module
134
self._member_name = member_name
136
def add_hook(self, name, doc, introduced, deprecated=None):
137
"""Add a hook point to this dictionary.
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
146
raise errors.DuplicateKey(name)
148
callbacks = _lazy_hooks.setdefault(
149
(self._module, self._member_name, name), [])
152
hookpoint = HookPoint(name=name, doc=doc, introduced=introduced,
153
deprecated=deprecated, callbacks=callbacks)
154
self[name] = hookpoint
156
@symbol_versioning.deprecated_method(symbol_versioning.deprecated_in((2, 4)))
157
def create_hook(self, hook):
158
"""Create a hook which can have callbacks registered for it.
160
:param hook: The hook to create. An object meeting the protocol of
161
bzrlib.hooks.HookPoint. It's name is used as the key for future
164
if hook.name in self:
165
raise errors.DuplicateKey(hook.name)
166
self[hook.name] = hook
169
"""Generate the documentation for this Hooks instance.
171
This introspects all the individual hooks and returns their docs as well.
173
hook_names = sorted(self.keys())
175
name = self.__class__.__name__
176
hook_docs.append(name)
177
hook_docs.append("-"*len(name))
179
for hook_name in hook_names:
180
hook = self[hook_name]
182
hook_docs.append(hook.docs())
183
except AttributeError:
186
strings.append(hook_name)
187
strings.append("~" * len(hook_name))
189
strings.append("An old-style hook. For documentation see the __init__ "
190
"method of '%s'\n" % (name,))
191
hook_docs.extend(strings)
192
return "\n".join(hook_docs)
194
def get_hook_name(self, a_callable):
195
"""Get the name for a_callable for UI display.
197
If no name has been registered, the string 'No hook name' is returned.
198
We use a fixed string rather than repr or the callables module because
199
the code names are rarely meaningful for end users and this is not
200
intended for debugging.
202
name = self._callable_names.get(a_callable, None)
203
if name is None and a_callable is not None:
204
name = self._lazy_callable_names.get((a_callable.__module__,
205
a_callable.__name__),
208
return 'No hook name'
212
def install_named_hook_lazy(self, hook_name, callable_module,
213
callable_member, name):
214
"""Install a_callable in to the hook hook_name lazily, and label it.
216
:param hook_name: A hook name. See the __init__ method for the complete
218
:param callable_module: Name of the module in which the callable is
220
:param callable_member: Member name of the callable.
221
:param name: A name to associate the callable with, to show users what
225
hook = self[hook_name]
227
raise errors.UnknownHook(self.__class__.__name__, hook_name)
229
hook_lazy = getattr(hook, "hook_lazy")
230
except AttributeError:
231
raise errors.UnsupportedOperation(self.install_named_hook_lazy,
234
hook_lazy(callable_module, callable_member, name)
236
self.name_hook_lazy(callable_module, callable_member, name)
238
def install_named_hook(self, hook_name, a_callable, name):
239
"""Install a_callable in to the hook hook_name, and label it name.
241
:param hook_name: A hook name. See the __init__ method for the complete
39
243
:param a_callable: The callable to be invoked when the hook triggers.
40
The exact signature will depend on the hook - see the __init__
41
method of BranchHooks for details on each hook.
44
self[hook_name].append(a_callable)
46
raise errors.UnknownHook(self.__class__.__name__, hook_name)
244
The exact signature will depend on the hook - see the __init__
245
method for details on each hook.
246
:param name: A name to associate a_callable with, to show users what is
250
hook = self[hook_name]
252
raise errors.UnknownHook(self.__class__.__name__, hook_name)
254
# list hooks, old-style, not yet deprecated but less useful.
255
hook.append(a_callable)
256
except AttributeError:
257
hook.hook(a_callable, name)
259
self.name_hook(a_callable, name)
261
def uninstall_named_hook(self, hook_name, label):
262
"""Uninstall named hooks.
264
:param hook_name: Hook point name
265
:param label: Label of the callable to uninstall
268
hook = self[hook_name]
270
raise errors.UnknownHook(self.__class__.__name__, hook_name)
272
uninstall = getattr(hook, "uninstall")
273
except AttributeError:
274
raise errors.UnsupportedOperation(self.uninstall_named_hook, self)
278
def name_hook(self, a_callable, name):
279
"""Associate name with a_callable to show users what is running."""
280
self._callable_names[a_callable] = name
282
def name_hook_lazy(self, callable_module, callable_member, callable_name):
283
self._lazy_callable_names[(callable_module, callable_member)]= \
287
class HookPoint(object):
288
"""A single hook that clients can register to be called back when it fires.
290
:ivar name: The name of the hook.
291
:ivar doc: The docs for using the hook.
292
:ivar introduced: A version tuple specifying what version the hook was
293
introduced in. None indicates an unknown version.
294
:ivar deprecated: A version tuple specifying what version the hook was
295
deprecated or superseded in. None indicates that the hook is not
296
superseded or deprecated. If the hook is superseded then the doc
297
should describe the recommended replacement hook to register for.
300
def __init__(self, name, doc, introduced, deprecated=None, callbacks=None):
301
"""Create a HookPoint.
303
:param name: The name of the hook, for clients to use when registering.
304
:param doc: The docs for the hook.
305
:param introduced: When the hook was introduced (e.g. (0, 15)).
306
:param deprecated: When the hook was deprecated, None for
311
self.introduced = introduced
312
self.deprecated = deprecated
313
if callbacks is None:
316
self._callbacks = callbacks
319
"""Generate the documentation for this HookPoint.
321
:return: A string terminated in \n.
324
strings.append(self.name)
325
strings.append('~'*len(self.name))
328
introduced_string = _format_version_tuple(self.introduced)
330
introduced_string = 'unknown'
331
strings.append(gettext('Introduced in: %s') % introduced_string)
333
deprecated_string = _format_version_tuple(self.deprecated)
334
strings.append(gettext('Deprecated in: %s') % deprecated_string)
336
strings.extend(textwrap.wrap(self.__doc__,
337
break_long_words=False))
339
return '\n'.join(strings)
341
def __eq__(self, other):
342
return (type(other) == type(self) and other.__dict__ == self.__dict__)
344
def hook_lazy(self, callback_module, callback_member, callback_label):
345
"""Lazily register a callback to be called when this HookPoint fires.
347
:param callback_module: Module of the callable to use when this
349
:param callback_member: Member name of the callback.
350
:param callback_label: A label to show in the UI while this callback is
353
obj_getter = registry._LazyObjectGetter(callback_module,
355
self._callbacks.append((obj_getter, callback_label))
357
def hook(self, callback, callback_label):
358
"""Register a callback to be called when this HookPoint fires.
360
:param callback: The callable to use when this HookPoint fires.
361
:param callback_label: A label to show in the UI while this callback is
364
obj_getter = registry._ObjectGetter(callback)
365
self._callbacks.append((obj_getter, callback_label))
367
def uninstall(self, label):
368
"""Uninstall the callback with the specified label.
370
:param label: Label of the entry to uninstall
372
entries_to_remove = []
373
for entry in self._callbacks:
374
(entry_callback, entry_label) = entry
375
if entry_label == label:
376
entries_to_remove.append(entry)
377
if entries_to_remove == []:
378
raise KeyError("No entry with label %r" % label)
379
for entry in entries_to_remove:
380
self._callbacks.remove(entry)
383
return (callback.get_obj() for callback, name in self._callbacks)
386
return len(self._callbacks)
390
strings.append("<%s(" % type(self).__name__)
391
strings.append(self.name)
392
strings.append("), callbacks=[")
393
callbacks = self._callbacks
394
for (callback, callback_name) in callbacks:
395
strings.append(repr(callback.get_obj()))
397
strings.append(callback_name)
399
if len(callbacks) == 1:
402
return ''.join(strings)
413
A hook of type *xxx* of class *yyy* needs to be registered using::
415
yyy.hooks.install_named_hook("xxx", ...)
417
See :doc:`Using hooks<../user-guide/hooks>` in the User Guide for examples.
419
The class that contains each hook is given before the hooks it supplies. For
420
instance, BranchHooks as the class is the hooks class for
421
`bzrlib.branch.Branch.hooks`.
423
Each description also indicates whether the hook runs on the client (the
424
machine where bzr was invoked) or the server (the machine addressed by
425
the branch URL). These may be, but are not necessarily, the same machine.
427
Plugins (including hooks) are run on the server if all of these is true:
429
* The connection is via a smart server (accessed with a URL starting with
430
"bzr://", "bzr+ssh://" or "bzr+http://", or accessed via a "http://"
431
URL when a smart server is available via HTTP).
433
* The hook is either server specific or part of general infrastructure rather
434
than client specific code (such as commit).
438
def hooks_help_text(topic):
439
segments = [_help_prefix]
440
for hook_key in sorted(known_hooks.keys()):
441
hooks = known_hooks_key_to_object(hook_key)
442
segments.append(hooks.docs())
443
return '\n'.join(segments)
446
# Lazily registered hooks. Maps (module, name, hook_name) tuples
447
# to lists of tuples with objectgetters and names
451
def install_lazy_named_hook(hookpoints_module, hookpoints_name, hook_name,
453
"""Install a callable in to a hook lazily, and label it name.
455
:param hookpoints_module: Module name of the hook points.
456
:param hookpoints_name: Name of the hook points.
457
:param hook_name: A hook name.
458
:param callable: a callable to call for the hook.
459
:param name: A name to associate a_callable with, to show users what is
462
key = (hookpoints_module, hookpoints_name, hook_name)
463
obj_getter = registry._ObjectGetter(a_callable)
464
_lazy_hooks.setdefault(key, []).append((obj_getter, name))