From e42f0d6a0bfee98e4d1e938d5554a0251801ad88 Mon Sep 17 00:00:00 2001 From: Adrien Fallou Date: Mon, 21 Oct 2019 23:46:12 +0200 Subject: [PATCH] Isolate hooks between class and subclass (#67) **Why?** Component hooks are registered on the class that will be instantiated for the component, through a class attribute. This means that if component `a` is an instance of `class A`, and component `b` an instance of `class B(A)` then the hooks defined on those two components will get mixed. **What?** Ensure separation of hooks in cases like the one above by using the class name in the class attribute used to hold hooks. --- microcosm/hooks.py | 18 ++++++++++++------ microcosm/tests/test_hooks.py | 20 ++++++++++++++++++++ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/microcosm/hooks.py b/microcosm/hooks.py index eaac015..8a9a1a1 100644 --- a/microcosm/hooks.py +++ b/microcosm/hooks.py @@ -42,15 +42,20 @@ class Bar: ON_RESOLVE = "_microcosm_on_resolve_" -def _invoke_hook(hook_name, target): +def _get_hook_name(hook_prefix, target_cls): + return f"{hook_prefix}_{target_cls.__name__}" + + +def _invoke_hook(hook_prefix, target_component): """ Generic hook invocation. """ + hook_name = _get_hook_name(hook_prefix, target_component.__class__) try: - for value in getattr(target, hook_name): + for value in getattr(target_component, hook_name): func, args, kwargs = value - func(target, *args, **kwargs) + func(target_component, *args, **kwargs) except AttributeError: # no hook defined pass @@ -59,16 +64,17 @@ def _invoke_hook(hook_name, target): pass -def _register_hook(hook_name, target, func, *args, **kwargs): +def _register_hook(hook_prefix, target_cls, func, *args, **kwargs): """ Generic hook registration. """ + hook_name = _get_hook_name(hook_prefix, target_cls) call = (func, args, kwargs) try: - getattr(target, hook_name).append(call) + getattr(target_cls, hook_name).append(call) except AttributeError: - setattr(target, hook_name, [call]) + setattr(target_cls, hook_name, [call]) def invoke_resolve_hook(target): diff --git a/microcosm/tests/test_hooks.py b/microcosm/tests/test_hooks.py index f4f5cd6..495d0fd 100644 --- a/microcosm/tests/test_hooks.py +++ b/microcosm/tests/test_hooks.py @@ -19,12 +19,18 @@ def __init__(self, graph): self.callbacks = [] +@binding("subfoo") +class SubFoo(Foo): + pass + + @binding("bar") def new_foo(graph): return Foo(graph) on_resolve(Foo, foo_hook, "baz") +on_resolve(SubFoo, foo_hook, "qux") class TestHooks: @@ -54,6 +60,20 @@ def test_on_resolve_foo_again(self): assert_that(graph.foo.callbacks, contains("baz")) + def test_on_resolve_foo_subfoo(self): + """ + If we have two components, and one is a subclass of the other's class, we should + still have isolation of the hooks between them + + """ + graph = create_object_graph("test") + graph.use("foo") + graph.use("subfoo") + graph.lock() + + assert_that(graph.foo.callbacks, contains("baz")) + assert_that(graph.subfoo.callbacks, contains("qux")) + def test_on_resolve_bar_once(self): """ Resolving Foo through a separate factory calls the hook.