From 12d4a34ca4710b991805ae39a1f107594c0168ac Mon Sep 17 00:00:00 2001 From: Tihomir Trifonov Date: Tue, 17 Jun 2014 00:06:51 +0300 Subject: Added inheritance for hooks from parent classes also added hook inheritance from mixins, and adding hooks to child controllers, added as sub-controllers. Fixes bug 1330673 Change-Id: I709cece7bcce26943b254b15dc8ddac5613b1202 --- docs/source/hooks.rst | 101 +++++++++++++++++++++++++++++++++++++++++++++- pecan/hooks.py | 13 +++++- pecan/tests/test_hooks.py | 72 +++++++++++++++++++++++++++++++++ 3 files changed, 182 insertions(+), 4 deletions(-) diff --git a/docs/source/hooks.rst b/docs/source/hooks.rst index 7796189..18a530f 100644 --- a/docs/source/hooks.rst +++ b/docs/source/hooks.rst @@ -130,9 +130,106 @@ when we run the app and browse the application from our web browser. about to enter the controller... DO SOMETHING! - method: GET - response: 200 OK + method: GET + response: 200 OK +Hooks can be inherited from parent class or mixins. Just make sure to +subclass from :class:`~pecan.hooks.HookController`. + +:: + + from pecan import expose + from pecan.hooks import PecanHook, HookController + + class ParentHook(PecanHook): + + priority = 1 + + def before(self, state): + print "\nabout to enter the parent controller..." + + class CommonHook(PecanHook): + + priority = 2 + + def before(self, state): + print "\njust a common hook..." + + class SubHook(PecanHook): + + def before(self, state): + print "\nabout to enter the subcontroller..." + + class SubMixin(object): + __hooks__ = [SubHook()] + + # We'll use the same instance for both controllers, + # to avoid double calls + common = CommonHook() + + class SubController(HookController, SubMixin): + + __hooks__ = [common] + + @expose('json') + def index(self): + print "\nI AM THE SUB!" + return dict() + + class RootController(HookController): + + __hooks__ = [common, ParentHook()] + + @expose('json') + def index(self): + print "\nI AM THE ROOT!" + return dict() + + sub = SubController() + +Let's see what happens when we run the app. +First loading the root controller: + +:: + + pecan serve config.py + serving on 0.0.0.0:8080 view at http://127.0.0.1:8080 + + GET / HTTP/1.1" 200 + + about to enter the parent controller... + + just a common hook + + I AM THE ROOT! + +Then loading the sub controller: + +:: + + pecan serve config.py + serving on 0.0.0.0:8080 view at http://127.0.0.1:8080 + + GET /sub HTTP/1.1" 200 + + about to enter the parent controller... + + just a common hook + + about to enter the subcontroller... + + I AM THE SUB! + +.. note:: + + Make sure to set proper priority values for nested hooks in order + to get them executed in the desired order. + +.. warning:: + + Two hooks of the same type will be added/executed twice, if passed as + different instances to a parent and a child controller. + If passed as one instance variable - will be invoked once for both controllers. Hooks That Come with Pecan -------------------------- diff --git a/pecan/hooks.py b/pecan/hooks.py index 4ceeb42..27dc872 100644 --- a/pecan/hooks.py +++ b/pecan/hooks.py @@ -14,6 +14,10 @@ __all__ = [ def walk_controller(root_class, controller, hooks): if not isinstance(controller, (int, dict)): + for hook in getattr(controller, '__hooks__', []): + # Append hooks from controller class definition + hooks.add(hook) + for name, value in getmembers(controller): if name == 'controller': continue @@ -22,7 +26,7 @@ def walk_controller(root_class, controller, hooks): if iscontroller(value): for hook in hooks: - value._pecan.setdefault('hooks', []).append(hook) + value._pecan.setdefault('hooks', set()).add(hook) elif hasattr(value, '__class__'): if name.startswith('__') and name.endswith('__'): continue @@ -37,7 +41,12 @@ class HookControllerMeta(type): ''' def __init__(cls, name, bases, dict_): - walk_controller(cls, cls, dict_.get('__hooks__', [])) + hooks = set(dict_.get('__hooks__', [])) + for base in bases: + # Add hooks from parent class and mixins + for hook in getattr(base, '__hooks__', []): + hooks.add(hook) + walk_controller(cls, cls, hooks) HookController = HookControllerMeta( diff --git a/pecan/tests/test_hooks.py b/pecan/tests/test_hooks.py index 44963bf..8f1ca39 100644 --- a/pecan/tests/test_hooks.py +++ b/pecan/tests/test_hooks.py @@ -339,6 +339,78 @@ class TestHooks(PecanTestCase): assert run_hook[4] == 'after1' assert run_hook[5] == 'after2' + def test_mixin_hooks(self): + run_hook = [] + + class HelperHook(PecanHook): + priority = 2 + + def before(self, state): + run_hook.append('helper - before hook') + + # we'll use the same hook instance to avoid duplicate calls + helper_hook = HelperHook() + + class LastHook(PecanHook): + priority = 200 + + def before(self, state): + run_hook.append('last - before hook') + + class SimpleHook(PecanHook): + priority = 1 + + def before(self, state): + run_hook.append('simple - before hook') + + class HelperMixin(object): + __hooks__ = [helper_hook] + + class LastMixin(object): + __hooks__ = [LastHook()] + + class SubController(HookController, HelperMixin): + __hooks__ = [LastHook()] + + @expose() + def index(self): + return "This is sub controller!" + + class RootController(HookController, LastMixin): + __hooks__ = [SimpleHook(), helper_hook] + + @expose() + def index(self): + run_hook.append('inside') + return 'Hello, World!' + + sub = SubController() + + papp = make_app(RootController()) + app = TestApp(papp) + response = app.get('/') + assert response.status_int == 200 + assert response.body == b_('Hello, World!') + + assert len(run_hook) == 4 + assert run_hook[0] == 'simple - before hook', run_hook[0] + assert run_hook[1] == 'helper - before hook', run_hook[1] + assert run_hook[2] == 'last - before hook', run_hook[2] + assert run_hook[3] == 'inside', run_hook[3] + + run_hook = [] + response = app.get('/sub/') + assert response.status_int == 200 + assert response.body == b_('This is sub controller!') + + assert len(run_hook) == 4, run_hook + assert run_hook[0] == 'simple - before hook', run_hook[0] + assert run_hook[1] == 'helper - before hook', run_hook[1] + assert run_hook[2] == 'last - before hook', run_hook[2] + # LastHook is invoked once again - + # for each different instance of the Hook in the two Controllers + assert run_hook[3] == 'last - before hook', run_hook[3] + class TestTransactionHook(PecanTestCase): def test_transaction_hook(self): -- cgit v1.2.1