1# -*- coding: utf-8 -*-
2"""
3Utilities for TurboGears hooks management.
4
5Provides a consistent API to register and execute hooks.
6
7"""
8import atexit
9import warnings
10from .utils import TGConfigError
11from .milestones import config_ready, renderers_ready, environment_loaded
12from ..decorators import Decoration
13from .._compat import default_im_func
14from .app_config import config as tg_config
15
16
17from logging import getLogger
18log = getLogger(__name__)
19
20
21class HooksNamespace(object):
22    """Manages hooks registrations and notifications"""
23    def __init__(self):
24        self._hooks = dict()
25        atexit.register(self._atexit)
26
27    def _atexit(self):
28        for func in self._hooks.get('shutdown', tuple()):
29            func()
30
31    def _call_handler(self, hook_name, trap_exceptions, func, args, kwargs):
32        try:
33            return func(*args, **kwargs)
34        except:
35            if trap_exceptions is True:
36                log.exception('Trapped Exception while handling %s -> %s', hook_name, func)
37            else:
38                raise
39
40    def register(self, hook_name, func, controller=None):
41        """Registers a TurboGears hook.
42
43        Given an hook name and a function it registers the provided
44        function for that role. For a complete list of hooks
45        provided by default have a look at :ref:`hooks_and_events`.
46
47        It permits to register hooks both application wide
48        or for specific controllers::
49
50            tg.hooks.register('before_render', hook_func, controller=RootController.index)
51            tg.hooks.register('startup', startup_function)
52
53        """
54        if hook_name in ('startup', 'shutdown') and controller is not None:
55            raise TGConfigError('Startup and Shutdown hooks cannot be registered on controllers')
56
57        if hook_name == 'controller_wrapper':
58            raise TGConfigError('tg.hooks.wrap_controller must be used to register wrappers')
59
60        if controller is None:
61            config_ready.register(_ApplicationHookRegistration(self, hook_name, func))
62        else:
63            controller = default_im_func(controller)
64            renderers_ready.register(_ControllerHookRegistration(controller, hook_name, func))
65
66    def disconnect(self, hook_name, func, controller=None):
67        """Disconnect an hook.
68
69        The registered function is removed from the hook notification list.
70        """
71        if controller is None:
72            registrations = self._hooks.get(hook_name, [])
73        else:
74            deco = Decoration.get_decoration(controller)
75            registrations = deco.hooks.get(hook_name, [])
76
77        try:
78            registrations.remove(func)
79        except ValueError:
80            pass
81
82    def notify(self, hook_name, args=None, kwargs=None, controller=None,
83               context_config=None, trap_exceptions=False):
84        """Notifies a TurboGears hook.
85
86        Each function registered for the given hook will be executed,
87        ``args`` and ``kwargs`` will be passed to the registered functions
88        as arguments.
89
90        It permits to notify both application hooks::
91
92            tg.hooks.notify('custom_global_hook')
93
94        Or controller hooks::
95
96            tg.hooks.notify('before_render', args=(remainder, params, output),
97                            controller=RootController.index)
98
99        """
100        args = args or []
101        kwargs = kwargs or {}
102
103        try:
104            syswide_hooks = self._hooks[hook_name]
105        except KeyError:  # pragma: no cover
106            pass
107        else:
108            for func in syswide_hooks:
109                self._call_handler(hook_name, trap_exceptions, func, args, kwargs)
110
111        if controller is not None:
112            controller = default_im_func(controller)
113            deco = Decoration.get_decoration(controller)
114            for func in deco.hooks.get(hook_name, []):
115                self._call_handler(hook_name, trap_exceptions, func, args, kwargs)
116
117    def notify_with_value(self, hook_name, value, controller=None, context_config=None):
118        """Notifies a TurboGears hook which is expected to return a value.
119
120        hooks with values are expected to accept an input value an return
121        a replacement for it. Each registered function will receive as input
122        the value returned by the previous function in chain.
123
124        The resulting value will be returned by the ``notify_with_value``
125        call itself::
126
127            app = tg.hooks.notify_with_value('before_config', app)
128
129        """
130        try:
131            syswide_hooks = self._hooks[hook_name]
132        except KeyError:  # pragma: no cover
133            pass
134        else:
135            for func in syswide_hooks:
136                value = func(value)
137
138        if controller is not None:
139            controller = default_im_func(controller)
140            deco = Decoration.get_decoration(controller)
141            for func in deco.hooks[hook_name]:
142                value = func(value)
143
144        return value
145
146
147class _ApplicationHookRegistration(object):
148    def __init__(self, hooks_namespace, hook_name, func):
149        self.hook_name = hook_name
150        self.func = func
151        self.hooks_namespace = hooks_namespace
152
153    def __call__(self):
154        log.debug("Registering %s for application wide hook %s",
155                  self.func, self.hook_name)
156
157        if self.hook_name == 'controller_wrapper':
158            warnings.warn('controller wrappers should be registered on '
159                          'AppConfig using AppConfig.register_controller_wrapper',
160                          DeprecationWarning, stacklevel=3)
161
162            config = tg_config._current_obj()
163            config['controller_wrappers'].append(self.func)
164        else:
165            hooks = self.hooks_namespace._hooks
166            hooks.setdefault(self.hook_name, []).append(self.func)
167
168
169class _ControllerHookRegistration(object):
170    def __init__(self, controller, hook_name, func):
171        self.controller = controller
172        self.hook_name = hook_name
173        self.func = func
174
175    def __call__(self):
176        log.debug("Registering %s for hook %s on controller %s",
177                  self.func, self.hook_name, self.controller)
178
179        if self.hook_name == 'controller_wrapper':
180            warnings.warn('controller wrappers should be registered on '
181                          'AppConfig using AppConfig.register_controller_wrapper',
182                          DeprecationWarning, stacklevel=3)
183
184            deco = Decoration.get_decoration(self.controller)
185            deco._register_controller_wrapper(self.func)
186        else:
187            deco = Decoration.get_decoration(self.controller)
188            deco._register_hook(self.hook_name, self.func)
189
190
191class _TGGlobalHooksNamespace(HooksNamespace):
192    def wrap_controller(self, func, controller=None):
193        """Registers a TurboGears controller wrapper.
194
195        Controller Wrappers are much like a **decorator** applied to
196        every controller.
197        They receive :class:`tg.configuration.AppConfig` instance
198        as an argument and the next handler in chain and are expected
199        to return a new handler that performs whatever it requires
200        and then calls the next handler.
201
202        A simple example for a controller wrapper is a simple logging wrapper::
203
204            def controller_wrapper(app_config, caller):
205                def call(*args, **kw):
206                    try:
207                        print 'Before handler!'
208                        return caller(*args, **kw)
209                    finally:
210                        print 'After Handler!'
211                return call
212
213            tg.hooks.wrap_controller(controller_wrapper)
214
215        It is also possible to register wrappers for a specific controller::
216
217            tg.hooks.wrap_controller(controller_wrapper, controller=RootController.index)
218
219        """
220        if environment_loaded.reached:
221            raise TGConfigError('Controller wrappers can be registered only at '
222                                'configuration time.')
223
224        if controller is None:
225            environment_loaded.register(_ApplicationHookRegistration(self,
226                                                                     'controller_wrapper',
227                                                                     func))
228        else:
229            controller = default_im_func(controller)
230            registration = _ControllerHookRegistration(controller, 'controller_wrapper', func)
231            renderers_ready.register(registration)
232
233hooks = _TGGlobalHooksNamespace()
234