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