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