~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/hooks.py

  • Committer: John Arbash Meinel
  • Date: 2013-05-19 14:29:37 UTC
  • mfrom: (6437.63.9 2.5)
  • mto: (6437.63.10 2.5)
  • mto: This revision was merged to the branch mainline in revision 6575.
  • Revision ID: john@arbash-meinel.com-20130519142937-21ykz2n2y2f22za9
Merge in the actual 2.5 branch. It seems I failed before

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2007 Canonical Ltd
 
1
# Copyright (C) 2007-2011 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
12
12
#
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
16
 
 
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
16
 
18
17
"""Support for plugin hooking logic."""
 
18
 
 
19
from __future__ import absolute_import
 
20
 
 
21
from bzrlib import (
 
22
    registry,
 
23
    symbol_versioning,
 
24
    )
19
25
from bzrlib.lazy_import import lazy_import
20
26
lazy_import(globals(), """
 
27
import textwrap
 
28
 
21
29
from bzrlib import (
22
 
        errors,
23
 
        )
 
30
    _format_version_tuple,
 
31
    errors,
 
32
    pyutils,
 
33
    )
 
34
from bzrlib.i18n import gettext
24
35
""")
25
36
 
26
37
 
 
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
 
43
 
 
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)
 
48
 
 
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
 
52
        instance.
 
53
 
 
54
        This is useful for resetting/restoring all the hooks to a known state,
 
55
        as is done in bzrlib.tests.TestCase._clear_hooks.
 
56
        """
 
57
        for key in self.keys():
 
58
            yield key, self.key_to_parent_and_attribute(key)
 
59
 
 
60
    def key_to_parent_and_attribute(self, (module_name, member_name)):
 
61
        """Convert a known_hooks key to a (parent_obj, attr) pair.
 
62
 
 
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.
 
67
        """
 
68
        parent_mod, parent_member, attr = pyutils.calc_parent_name(module_name,
 
69
            member_name)
 
70
        return pyutils.get_named_object(parent_mod, parent_member), attr
 
71
 
 
72
 
 
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'),
 
91
    )
 
92
 
 
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
 
97
 
 
98
 
 
99
def known_hooks_key_to_object((module_name, member_name)):
 
100
    """Convert a known_hooks key to a object.
 
101
 
 
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.
 
105
    """
 
106
    return pyutils.get_named_object(module_name, member_name)
 
107
 
 
108
 
 
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)
 
113
 
 
114
 
27
115
class Hooks(dict):
28
116
    """A dictionary mapping hook name to a list of callables.
29
 
    
 
117
 
30
118
    e.g. ['FOO'] Is the list of items to be called when the
31
119
    FOO hook is triggered.
32
120
    """
33
121
 
34
 
    def install_hook(self, hook_name, a_callable):
35
 
        """Install a_callable in to the hook hook_name.
36
 
 
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.
 
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
    @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.
 
159
 
 
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
 
162
            lookups.
 
163
        """
 
164
        if hook.name in self:
 
165
            raise errors.DuplicateKey(hook.name)
 
166
        self[hook.name] = hook
 
167
 
 
168
    def docs(self):
 
169
        """Generate the documentation for this Hooks instance.
 
170
 
 
171
        This introspects all the individual hooks and returns their docs as well.
 
172
        """
 
173
        hook_names = sorted(self.keys())
 
174
        hook_docs = []
 
175
        name = self.__class__.__name__
 
176
        hook_docs.append(name)
 
177
        hook_docs.append("-"*len(name))
 
178
        hook_docs.append("")
 
179
        for hook_name in hook_names:
 
180
            hook = self[hook_name]
 
181
            try:
 
182
                hook_docs.append(hook.docs())
 
183
            except AttributeError:
 
184
                # legacy hook
 
185
                strings = []
 
186
                strings.append(hook_name)
 
187
                strings.append("~" * len(hook_name))
 
188
                strings.append("")
 
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)
 
193
 
 
194
    def get_hook_name(self, a_callable):
 
195
        """Get the name for a_callable for UI display.
 
196
 
 
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.
 
201
        """
 
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__),
 
206
                                                 None)
 
207
        if name is None:
 
208
            return 'No hook name'
 
209
        return name
 
210
 
 
211
 
 
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.
 
215
 
 
216
        :param hook_name: A hook name. See the __init__ method for the complete
 
217
            list of hooks.
 
218
        :param callable_module: Name of the module in which the callable is
 
219
            present.
 
220
        :param callable_member: Member name of the callable.
 
221
        :param name: A name to associate the callable with, to show users what
 
222
            is running.
 
223
        """
 
224
        try:
 
225
            hook = self[hook_name]
 
226
        except KeyError:
 
227
            raise errors.UnknownHook(self.__class__.__name__, hook_name)
 
228
        try:
 
229
            hook_lazy = getattr(hook, "hook_lazy")
 
230
        except AttributeError:
 
231
            raise errors.UnsupportedOperation(self.install_named_hook_lazy,
 
232
                self)
 
233
        else:
 
234
            hook_lazy(callable_module, callable_member, name)
 
235
        if name is not None:
 
236
            self.name_hook_lazy(callable_module, callable_member, name)
 
237
 
 
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.
 
240
 
 
241
        :param hook_name: A hook name. See the __init__ method for the complete
 
242
            list of hooks.
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.
42
 
        """
43
 
        try:
44
 
            self[hook_name].append(a_callable)
45
 
        except KeyError:
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
 
247
            running.
 
248
        """
 
249
        try:
 
250
            hook = self[hook_name]
 
251
        except KeyError:
 
252
            raise errors.UnknownHook(self.__class__.__name__, hook_name)
 
253
        try:
 
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)
 
258
        if name is not None:
 
259
            self.name_hook(a_callable, name)
 
260
 
 
261
    def uninstall_named_hook(self, hook_name, label):
 
262
        """Uninstall named hooks.
 
263
 
 
264
        :param hook_name: Hook point name
 
265
        :param label: Label of the callable to uninstall
 
266
        """
 
267
        try:
 
268
            hook = self[hook_name]
 
269
        except KeyError:
 
270
            raise errors.UnknownHook(self.__class__.__name__, hook_name)
 
271
        try:
 
272
            uninstall = getattr(hook, "uninstall")
 
273
        except AttributeError:
 
274
            raise errors.UnsupportedOperation(self.uninstall_named_hook, self)
 
275
        else:
 
276
            uninstall(label)
 
277
 
 
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
 
281
 
 
282
    def name_hook_lazy(self, callable_module, callable_member, callable_name):
 
283
        self._lazy_callable_names[(callable_module, callable_member)]= \
 
284
            callable_name
 
285
 
 
286
 
 
287
class HookPoint(object):
 
288
    """A single hook that clients can register to be called back when it fires.
 
289
 
 
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.
 
298
    """
 
299
 
 
300
    def __init__(self, name, doc, introduced, deprecated=None, callbacks=None):
 
301
        """Create a HookPoint.
 
302
 
 
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
 
307
            not-deprecated.
 
308
        """
 
309
        self.name = name
 
310
        self.__doc__ = doc
 
311
        self.introduced = introduced
 
312
        self.deprecated = deprecated
 
313
        if callbacks is None:
 
314
            self._callbacks = []
 
315
        else:
 
316
            self._callbacks = callbacks
 
317
 
 
318
    def docs(self):
 
319
        """Generate the documentation for this HookPoint.
 
320
 
 
321
        :return: A string terminated in \n.
 
322
        """
 
323
        strings = []
 
324
        strings.append(self.name)
 
325
        strings.append('~'*len(self.name))
 
326
        strings.append('')
 
327
        if self.introduced:
 
328
            introduced_string = _format_version_tuple(self.introduced)
 
329
        else:
 
330
            introduced_string = 'unknown'
 
331
        strings.append(gettext('Introduced in: %s') % introduced_string)
 
332
        if self.deprecated:
 
333
            deprecated_string = _format_version_tuple(self.deprecated)
 
334
            strings.append(gettext('Deprecated in: %s') % deprecated_string)
 
335
        strings.append('')
 
336
        strings.extend(textwrap.wrap(self.__doc__,
 
337
            break_long_words=False))
 
338
        strings.append('')
 
339
        return '\n'.join(strings)
 
340
 
 
341
    def __eq__(self, other):
 
342
        return (type(other) == type(self) and other.__dict__ == self.__dict__)
 
343
 
 
344
    def hook_lazy(self, callback_module, callback_member, callback_label):
 
345
        """Lazily register a callback to be called when this HookPoint fires.
 
346
 
 
347
        :param callback_module: Module of the callable to use when this
 
348
            HookPoint fires.
 
349
        :param callback_member: Member name of the callback.
 
350
        :param callback_label: A label to show in the UI while this callback is
 
351
            processing.
 
352
        """
 
353
        obj_getter = registry._LazyObjectGetter(callback_module,
 
354
            callback_member)
 
355
        self._callbacks.append((obj_getter, callback_label))
 
356
 
 
357
    def hook(self, callback, callback_label):
 
358
        """Register a callback to be called when this HookPoint fires.
 
359
 
 
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
 
362
            processing.
 
363
        """
 
364
        obj_getter = registry._ObjectGetter(callback)
 
365
        self._callbacks.append((obj_getter, callback_label))
 
366
 
 
367
    def uninstall(self, label):
 
368
        """Uninstall the callback with the specified label.
 
369
 
 
370
        :param label: Label of the entry to uninstall
 
371
        """
 
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)
 
381
 
 
382
    def __iter__(self):
 
383
        return (callback.get_obj() for callback, name in self._callbacks)
 
384
 
 
385
    def __len__(self):
 
386
        return len(self._callbacks)
 
387
 
 
388
    def __repr__(self):
 
389
        strings = []
 
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()))
 
396
            strings.append("(")
 
397
            strings.append(callback_name)
 
398
            strings.append("),")
 
399
        if len(callbacks) == 1:
 
400
            strings[-1] = ")"
 
401
        strings.append("]>")
 
402
        return ''.join(strings)
 
403
 
 
404
 
 
405
_help_prefix = \
 
406
"""
 
407
Hooks
 
408
=====
 
409
 
 
410
Introduction
 
411
------------
 
412
 
 
413
A hook of type *xxx* of class *yyy* needs to be registered using::
 
414
 
 
415
  yyy.hooks.install_named_hook("xxx", ...)
 
416
 
 
417
See :doc:`Using hooks<../user-guide/hooks>` in the User Guide for examples.
 
418
 
 
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`.
 
422
 
 
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.
 
426
 
 
427
Plugins (including hooks) are run on the server if all of these is true:
 
428
 
 
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).
 
432
 
 
433
  * The hook is either server specific or part of general infrastructure rather
 
434
    than client specific code (such as commit).
 
435
 
 
436
"""
 
437
 
 
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)
 
444
 
 
445
 
 
446
# Lazily registered hooks. Maps (module, name, hook_name) tuples
 
447
# to lists of tuples with objectgetters and names
 
448
_lazy_hooks = {}
 
449
 
 
450
 
 
451
def install_lazy_named_hook(hookpoints_module, hookpoints_name, hook_name,
 
452
    a_callable, name):
 
453
    """Install a callable in to a hook lazily, and label it name.
 
454
 
 
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
 
460
        running.
 
461
    """
 
462
    key = (hookpoints_module, hookpoints_name, hook_name)
 
463
    obj_getter = registry._ObjectGetter(a_callable)
 
464
    _lazy_hooks.setdefault(key, []).append((obj_getter, name))