From c4df16dc3bc7ac52d68fa8e1e0923e3e735a353c Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Sun, 20 Nov 2016 12:51:30 +0100 Subject: [PATCH 01/40] basic injector --- botstory/di/__init__.py | 3 +++ botstory/di/injector_service.py | 20 ++++++++++++++++++++ botstory/di/injector_service_test.py | 6 ++++++ 3 files changed, 29 insertions(+) create mode 100644 botstory/di/__init__.py create mode 100644 botstory/di/injector_service.py create mode 100644 botstory/di/injector_service_test.py diff --git a/botstory/di/__init__.py b/botstory/di/__init__.py new file mode 100644 index 0000000..75762db --- /dev/null +++ b/botstory/di/__init__.py @@ -0,0 +1,3 @@ +from .injector_service import Injector + +injector = Injector() diff --git a/botstory/di/injector_service.py b/botstory/di/injector_service.py new file mode 100644 index 0000000..556342a --- /dev/null +++ b/botstory/di/injector_service.py @@ -0,0 +1,20 @@ +class Scope: + def __init__(self): + self.storage = {} + + def get(self, type_name): + return self.storage[type_name] + + def register(self, type_name, value): + self.storage[type_name] = value + + +class Injector: + def __init__(self): + self.root = Scope() + + def register(self, type_name, instance): + self.root.register(type_name, instance) + + def get(self, type_name): + return self.root.get(type_name) diff --git a/botstory/di/injector_service_test.py b/botstory/di/injector_service_test.py new file mode 100644 index 0000000..5bbd5fe --- /dev/null +++ b/botstory/di/injector_service_test.py @@ -0,0 +1,6 @@ +from .. import di + + +def test_injector_get(): + di.injector.register('once_instance', 'Hello World!') + assert di.injector.get('once_instance') == 'Hello World!' From a894f7fe44b0613ab81a5260be02c203f2eccaa7 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Sun, 20 Nov 2016 15:22:19 +0100 Subject: [PATCH 02/40] inject class --- botstory/di/__init__.py | 5 +++++ botstory/di/inject.py | 14 ++++++++++++++ botstory/di/inject_test.py | 10 ++++++++++ botstory/di/injector_service.py | 9 ++++++++- botstory/di/parser.py | 28 ++++++++++++++++++++++++++++ botstory/di/parser_test.py | 5 +++++ 6 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 botstory/di/inject.py create mode 100644 botstory/di/inject_test.py create mode 100644 botstory/di/parser.py create mode 100644 botstory/di/parser_test.py diff --git a/botstory/di/__init__.py b/botstory/di/__init__.py index 75762db..1ed97ba 100644 --- a/botstory/di/__init__.py +++ b/botstory/di/__init__.py @@ -1,3 +1,8 @@ from .injector_service import Injector +from .inject import inject + +__all__ = [] injector = Injector() + +__all__.extend([inject]) diff --git a/botstory/di/inject.py b/botstory/di/inject.py new file mode 100644 index 0000000..e3bc054 --- /dev/null +++ b/botstory/di/inject.py @@ -0,0 +1,14 @@ +from inspect import signature + +from .parser import camel_case_to_underscore + +from .. import di + + +def inject(): + def decorated_fn(fn): + fn_sig = signature(fn) + di.injector.register(camel_case_to_underscore(fn.__name__)[0], fn) + return fn + + return decorated_fn diff --git a/botstory/di/inject_test.py b/botstory/di/inject_test.py new file mode 100644 index 0000000..1dfe689 --- /dev/null +++ b/botstory/di/inject_test.py @@ -0,0 +1,10 @@ +from .. import di + + +def test_decorator(): + @di.inject() + class OneClass: + def __init__(self): + pass + + assert isinstance(di.injector.get('one_class'), OneClass) diff --git a/botstory/di/injector_service.py b/botstory/di/injector_service.py index 556342a..d67705e 100644 --- a/botstory/di/injector_service.py +++ b/botstory/di/injector_service.py @@ -1,9 +1,16 @@ +import inspect + + class Scope: def __init__(self): self.storage = {} def get(self, type_name): - return self.storage[type_name] + item = self.storage[type_name] + if inspect.isclass(item): + return item() + else: + return item def register(self, type_name, value): self.storage[type_name] = value diff --git a/botstory/di/parser.py b/botstory/di/parser.py new file mode 100644 index 0000000..699ced0 --- /dev/null +++ b/botstory/di/parser.py @@ -0,0 +1,28 @@ +import re + + +def camel_case_to_underscore(class_name): + """Converts normal class names into normal arg names. + Normal class names are assumed to be CamelCase with an optional leading + underscore. Normal arg names are assumed to be lower_with_underscores. + Args: + class_name: a class name, e.g., "FooBar" or "_FooBar" + Returns: + all likely corresponding arg names, e.g., ["foo_bar"] + + based on: + + """ + parts = [] + rest = class_name + if rest.startswith('_'): + rest = rest[1:] + while True: + m = re.match(r'([A-Z][a-z]+)(.*)', rest) + if m is None: + break + parts.append(m.group(1)) + rest = m.group(2) + if not parts: + return [] + return ['_'.join(part.lower() for part in parts)] diff --git a/botstory/di/parser_test.py b/botstory/di/parser_test.py new file mode 100644 index 0000000..a07cc97 --- /dev/null +++ b/botstory/di/parser_test.py @@ -0,0 +1,5 @@ +from .parser import camel_case_to_underscore + + +def test_(): + assert camel_case_to_underscore('ClassName')[0] == 'class_name' From e7b149440ac2acc9c8fa01fa4099968ee1b64d32 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Sun, 20 Nov 2016 15:26:25 +0100 Subject: [PATCH 03/40] bind singleton instance by default --- botstory/di/inject_test.py | 11 ++++++++++- botstory/di/injector_service.py | 8 +++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/botstory/di/inject_test.py b/botstory/di/inject_test.py index 1dfe689..e4ef499 100644 --- a/botstory/di/inject_test.py +++ b/botstory/di/inject_test.py @@ -1,10 +1,19 @@ from .. import di -def test_decorator(): +def test_inject_decorator(): @di.inject() class OneClass: def __init__(self): pass assert isinstance(di.injector.get('one_class'), OneClass) + + +def test_bind_singleton_instance_by_default(): + @di.inject() + class OneClass: + def __init__(self): + pass + + assert di.injector.get('one_class') == di.injector.get('one_class') diff --git a/botstory/di/injector_service.py b/botstory/di/injector_service.py index d67705e..67ce366 100644 --- a/botstory/di/injector_service.py +++ b/botstory/di/injector_service.py @@ -19,9 +19,15 @@ def register(self, type_name, value): class Injector: def __init__(self): self.root = Scope() + self.singleton_cache = {} def register(self, type_name, instance): self.root.register(type_name, instance) def get(self, type_name): - return self.root.get(type_name) + try: + return self.singleton_cache[type_name] + except KeyError: + instance = self.root.get(type_name) + self.singleton_cache[type_name] = instance + return instance From 2cf4f83f3dcf35c7ab6efeb5bb0409010ccedd38 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Mon, 21 Nov 2016 01:19:02 +0100 Subject: [PATCH 04/40] inject to method of class --- botstory/di/inject.py | 14 +++++++++++--- botstory/di/inject_test.py | 16 ++++++++++++++++ botstory/di/injector_service.py | 28 +++++++++++++++++++++++++--- 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/botstory/di/inject.py b/botstory/di/inject.py index e3bc054..57caa92 100644 --- a/botstory/di/inject.py +++ b/botstory/di/inject.py @@ -1,4 +1,4 @@ -from inspect import signature +import inspect from .parser import camel_case_to_underscore @@ -7,8 +7,16 @@ def inject(): def decorated_fn(fn): - fn_sig = signature(fn) - di.injector.register(camel_case_to_underscore(fn.__name__)[0], fn) + if inspect.isclass(fn): + name = camel_case_to_underscore(fn.__name__) + di.injector.register(name[0], fn) + elif inspect.isfunction(fn): + fn_sig = inspect.signature(fn) + args = [key for key in fn_sig.parameters.keys() if key != 'self'] + di.injector.requires(fn, args) + else: + # I'm not sure whether it possible case + raise NotImplementedError('try decorate {}'.format(fn)) return fn return decorated_fn diff --git a/botstory/di/inject_test.py b/botstory/di/inject_test.py index e4ef499..f5263ff 100644 --- a/botstory/di/inject_test.py +++ b/botstory/di/inject_test.py @@ -17,3 +17,19 @@ def __init__(self): pass assert di.injector.get('one_class') == di.injector.get('one_class') + + +def test_inject_into_method_of_class(): + @di.inject() + class OuterClass: + @di.inject() + def inner(self, inner_class): + self.inner_class = inner_class + + @di.inject() + class InnerClass: + pass + + outer = di.injector.get('outer_class') + assert isinstance(outer, OuterClass) + assert isinstance(outer.inner_class, InnerClass) diff --git a/botstory/di/injector_service.py b/botstory/di/injector_service.py index 67ce366..cdda093 100644 --- a/botstory/di/injector_service.py +++ b/botstory/di/injector_service.py @@ -20,14 +20,36 @@ class Injector: def __init__(self): self.root = Scope() self.singleton_cache = {} + self.requires_fns = {} def register(self, type_name, instance): self.root.register(type_name, instance) + def requires(self, fn, requires): + self.requires_fns[fn] = requires + def get(self, type_name): try: return self.singleton_cache[type_name] except KeyError: - instance = self.root.get(type_name) - self.singleton_cache[type_name] = instance - return instance + try: + instance = self.root.get(type_name) + except KeyError: + # TODO: sometimes we should fail loudly in this case + return None + + methods = [ + (m, cls.__dict__[m]) + for cls in inspect.getmro(type(instance)) + for m in cls.__dict__ if inspect.isfunction(cls.__dict__[m]) + ] + + requires_of_methods = [(method_ptr, {dep: self.get(dep) for dep in self.requires_fns.get(method_ptr, [])}) + for (method_name, method_ptr) in methods] + + for (method_ptr, method_deps) in requires_of_methods: + if len(method_deps) > 0: + method_ptr(instance, **method_deps) + + self.singleton_cache[type_name] = instance + return instance From 7f509a3e9d7a5c873ea76b5718ad2c566ff3b87c Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Mon, 21 Nov 2016 01:32:14 +0100 Subject: [PATCH 05/40] inject deps into methods of class on bind --- botstory/di/__init__.py | 4 +++- botstory/di/inject_test.py | 20 ++++++++++++++++ botstory/di/injector_service.py | 36 ++++++++++++++++++---------- botstory/di/injector_service_test.py | 4 ++++ 4 files changed, 50 insertions(+), 14 deletions(-) diff --git a/botstory/di/__init__.py b/botstory/di/__init__.py index 1ed97ba..c3de7d9 100644 --- a/botstory/di/__init__.py +++ b/botstory/di/__init__.py @@ -4,5 +4,7 @@ __all__ = [] injector = Injector() +bind = injector.bind +clear = injector.clear -__all__.extend([inject]) +__all__.extend([bind, clear, inject]) diff --git a/botstory/di/inject_test.py b/botstory/di/inject_test.py index f5263ff..9e8b118 100644 --- a/botstory/di/inject_test.py +++ b/botstory/di/inject_test.py @@ -1,6 +1,10 @@ from .. import di +def teardown_function(function): + di.clear() + + def test_inject_decorator(): @di.inject() class OneClass: @@ -33,3 +37,19 @@ class InnerClass: outer = di.injector.get('outer_class') assert isinstance(outer, OuterClass) assert isinstance(outer.inner_class, InnerClass) + + +def test_bind_should_inject_deps_in_decorated_methods_(): + @di.inject() + class OuterClass: + @di.inject() + def inner(self, inner_class): + self.inner_class = inner_class + + @di.inject() + class InnerClass: + pass + + outer = di.bind(OuterClass()) + assert isinstance(outer, OuterClass) + assert isinstance(outer.inner_class, InnerClass) diff --git a/botstory/di/injector_service.py b/botstory/di/injector_service.py index cdda093..48a0105 100644 --- a/botstory/di/injector_service.py +++ b/botstory/di/injector_service.py @@ -28,6 +28,27 @@ def register(self, type_name, instance): def requires(self, fn, requires): self.requires_fns[fn] = requires + def bind(self, instance): + methods = [ + (m, cls.__dict__[m]) + for cls in inspect.getmro(type(instance)) + for m in cls.__dict__ if inspect.isfunction(cls.__dict__[m]) + ] + + requires_of_methods = [(method_ptr, {dep: self.get(dep) for dep in self.requires_fns.get(method_ptr, [])}) + for (method_name, method_ptr) in methods] + + for (method_ptr, method_deps) in requires_of_methods: + if len(method_deps) > 0: + method_ptr(instance, **method_deps) + + return instance + + def clear(self): + self.root = Scope() + self.singleton_cache = {} + self.requires_fns = {} + def get(self, type_name): try: return self.singleton_cache[type_name] @@ -38,18 +59,7 @@ def get(self, type_name): # TODO: sometimes we should fail loudly in this case return None - methods = [ - (m, cls.__dict__[m]) - for cls in inspect.getmro(type(instance)) - for m in cls.__dict__ if inspect.isfunction(cls.__dict__[m]) - ] - - requires_of_methods = [(method_ptr, {dep: self.get(dep) for dep in self.requires_fns.get(method_ptr, [])}) - for (method_name, method_ptr) in methods] - - for (method_ptr, method_deps) in requires_of_methods: - if len(method_deps) > 0: - method_ptr(instance, **method_deps) + instance = self.bind(instance) + self.singleton_cache[type_name] = instance - self.singleton_cache[type_name] = instance return instance diff --git a/botstory/di/injector_service_test.py b/botstory/di/injector_service_test.py index 5bbd5fe..3e8c528 100644 --- a/botstory/di/injector_service_test.py +++ b/botstory/di/injector_service_test.py @@ -1,6 +1,10 @@ from .. import di +def teardown_function(function): + di.clear() + + def test_injector_get(): di.injector.register('once_instance', 'Hello World!') assert di.injector.get('once_instance') == 'Hello World!' From b5d284add2098127a24a0171fb41ba8a67a8a20b Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Mon, 21 Nov 2016 01:43:20 +0100 Subject: [PATCH 06/40] fallback to default value if we don't have dep instance yet --- botstory/di/inject.py | 4 +--- botstory/di/inject_test.py | 12 ++++++++++++ botstory/di/injector_service.py | 11 ++++++++--- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/botstory/di/inject.py b/botstory/di/inject.py index 57caa92..b1fed3c 100644 --- a/botstory/di/inject.py +++ b/botstory/di/inject.py @@ -11,9 +11,7 @@ def decorated_fn(fn): name = camel_case_to_underscore(fn.__name__) di.injector.register(name[0], fn) elif inspect.isfunction(fn): - fn_sig = inspect.signature(fn) - args = [key for key in fn_sig.parameters.keys() if key != 'self'] - di.injector.requires(fn, args) + di.injector.requires(fn) else: # I'm not sure whether it possible case raise NotImplementedError('try decorate {}'.format(fn)) diff --git a/botstory/di/inject_test.py b/botstory/di/inject_test.py index 9e8b118..4c36da0 100644 --- a/botstory/di/inject_test.py +++ b/botstory/di/inject_test.py @@ -53,3 +53,15 @@ class InnerClass: outer = di.bind(OuterClass()) assert isinstance(outer, OuterClass) assert isinstance(outer.inner_class, InnerClass) + + +def test_inject_default_value_if_we_dont_have_dep(): + @di.inject() + class OuterClass: + @di.inject() + def inner(self, inner_class='Hello World!'): + self.inner_class = inner_class + + outer = di.bind(OuterClass()) + assert isinstance(outer, OuterClass) + assert outer.inner_class == 'Hello World!' diff --git a/botstory/di/injector_service.py b/botstory/di/injector_service.py index 48a0105..a2bf4ac 100644 --- a/botstory/di/injector_service.py +++ b/botstory/di/injector_service.py @@ -25,8 +25,11 @@ def __init__(self): def register(self, type_name, instance): self.root.register(type_name, instance) - def requires(self, fn, requires): - self.requires_fns[fn] = requires + def requires(self, fn): + fn_sig = inspect.signature(fn) + self.requires_fns[fn] = { + key: {'default': fn_sig.parameters[key].default} + for key in fn_sig.parameters.keys() if key != 'self'} def bind(self, instance): methods = [ @@ -35,7 +38,9 @@ def bind(self, instance): for m in cls.__dict__ if inspect.isfunction(cls.__dict__[m]) ] - requires_of_methods = [(method_ptr, {dep: self.get(dep) for dep in self.requires_fns.get(method_ptr, [])}) + requires_of_methods = [(method_ptr, {dep: self.get(dep) or dep_spec['default'] + for dep, dep_spec in + self.requires_fns.get(method_ptr, {}).items()}) for (method_name, method_ptr) in methods] for (method_ptr, method_deps) in requires_of_methods: From 990e85d8ad62d965168d69c891fa9c371c4c5ae0 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Mon, 21 Nov 2016 01:51:30 +0100 Subject: [PATCH 07/40] can auto-bind deps --- botstory/di/inject_test.py | 34 +++++++++++++++++++++++++++++++++ botstory/di/injector_service.py | 8 +++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/botstory/di/inject_test.py b/botstory/di/inject_test.py index 4c36da0..24c5983 100644 --- a/botstory/di/inject_test.py +++ b/botstory/di/inject_test.py @@ -65,3 +65,37 @@ def inner(self, inner_class='Hello World!'): outer = di.bind(OuterClass()) assert isinstance(outer, OuterClass) assert outer.inner_class == 'Hello World!' + + +def test_no_autoupdate_deps_on_new_instance_comes(): + @di.inject() + class OuterClass: + @di.inject() + def inner(self, inner_class=None): + self.inner_class = inner_class + + outer = di.bind(OuterClass(), autoupdate=False) + + @di.inject() + class InnerClass: + pass + + assert isinstance(outer, OuterClass) + assert outer.inner_class is None + + +def test_autoupdate_deps_on_new_instance_comes(): + @di.inject() + class OuterClass: + @di.inject() + def inner(self, inner_class=None): + self.inner_class = inner_class + + outer = di.bind(OuterClass(), autoupdate=True) + + @di.inject() + class InnerClass: + pass + + assert isinstance(outer, OuterClass) + assert isinstance(outer.inner_class, InnerClass) diff --git a/botstory/di/injector_service.py b/botstory/di/injector_service.py index a2bf4ac..561cf7c 100644 --- a/botstory/di/injector_service.py +++ b/botstory/di/injector_service.py @@ -21,9 +21,12 @@ def __init__(self): self.root = Scope() self.singleton_cache = {} self.requires_fns = {} + self.waits_for_deps_list = [] def register(self, type_name, instance): self.root.register(type_name, instance) + for wait_instance in self.waits_for_deps_list: + self.bind(wait_instance) def requires(self, fn): fn_sig = inspect.signature(fn) @@ -31,7 +34,7 @@ def requires(self, fn): key: {'default': fn_sig.parameters[key].default} for key in fn_sig.parameters.keys() if key != 'self'} - def bind(self, instance): + def bind(self, instance, autoupdate=False): methods = [ (m, cls.__dict__[m]) for cls in inspect.getmro(type(instance)) @@ -47,6 +50,9 @@ def bind(self, instance): if len(method_deps) > 0: method_ptr(instance, **method_deps) + if autoupdate: + self.waits_for_deps_list.append(instance) + return instance def clear(self): From f8445538e5eeecac0e031e5f96a70b0b0d72dd18 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Mon, 21 Nov 2016 02:14:30 +0100 Subject: [PATCH 08/40] resolve cyclic deps --- botstory/di/inject_test.py | 18 ++++++++++++++++++ botstory/di/injector_service.py | 11 +++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/botstory/di/inject_test.py b/botstory/di/inject_test.py index 24c5983..1d0db60 100644 --- a/botstory/di/inject_test.py +++ b/botstory/di/inject_test.py @@ -99,3 +99,21 @@ class InnerClass: assert isinstance(outer, OuterClass) assert isinstance(outer.inner_class, InnerClass) + + +def test_fail_on_cyclic_deps(): + @di.inject() + class FirstClass: + @di.inject() + def deps(self, second_class=None): + self.second_class = second_class + + @di.inject() + class SecondClass: + @di.inject() + def deps(self, first_class=None): + self.first_class = first_class + + first_class = di.injector.get('first_class') + assert isinstance(first_class.second_class, SecondClass) + assert isinstance(first_class.second_class.first_class, FirstClass) diff --git a/botstory/di/injector_service.py b/botstory/di/injector_service.py index 561cf7c..3a8b640 100644 --- a/botstory/di/injector_service.py +++ b/botstory/di/injector_service.py @@ -19,13 +19,16 @@ def register(self, type_name, value): class Injector: def __init__(self): self.root = Scope() + # all instances that are singletones self.singleton_cache = {} + # functions that waits for deps self.requires_fns = {} - self.waits_for_deps_list = [] + # instances that will autoupdate on each new instance come + self.auto_update_list = [] def register(self, type_name, instance): self.root.register(type_name, instance) - for wait_instance in self.waits_for_deps_list: + for wait_instance in self.auto_update_list: self.bind(wait_instance) def requires(self, fn): @@ -51,7 +54,7 @@ def bind(self, instance, autoupdate=False): method_ptr(instance, **method_deps) if autoupdate: - self.waits_for_deps_list.append(instance) + self.auto_update_list.append(instance) return instance @@ -70,7 +73,7 @@ def get(self, type_name): # TODO: sometimes we should fail loudly in this case return None - instance = self.bind(instance) self.singleton_cache[type_name] = instance + instance = self.bind(instance) return instance From ae54ee42949df8b1d32481b0559dadaf8c73ed7e Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Tue, 22 Nov 2016 09:39:25 +0100 Subject: [PATCH 09/40] custom type --- botstory/di/inject.py | 6 +++--- botstory/di/inject_test.py | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/botstory/di/inject.py b/botstory/di/inject.py index b1fed3c..abc48a2 100644 --- a/botstory/di/inject.py +++ b/botstory/di/inject.py @@ -5,11 +5,11 @@ from .. import di -def inject(): +def inject(t=None): def decorated_fn(fn): if inspect.isclass(fn): - name = camel_case_to_underscore(fn.__name__) - di.injector.register(name[0], fn) + name = t or camel_case_to_underscore(fn.__name__)[0] + di.injector.register(name, fn) elif inspect.isfunction(fn): di.injector.requires(fn) else: diff --git a/botstory/di/inject_test.py b/botstory/di/inject_test.py index 1d0db60..17a61d8 100644 --- a/botstory/di/inject_test.py +++ b/botstory/di/inject_test.py @@ -117,3 +117,11 @@ def deps(self, first_class=None): first_class = di.injector.get('first_class') assert isinstance(first_class.second_class, SecondClass) assert isinstance(first_class.second_class.first_class, FirstClass) + + +def test_custom_type(): + @di.inject('qwerty') + class OneClass: + pass + + assert isinstance(di.injector.get('qwerty'), OneClass) From 14017465062ff8525d6937db1d569ca928d0e875 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Tue, 22 Nov 2016 10:13:10 +0100 Subject: [PATCH 10/40] test remove leading underscore --- botstory/di/parser_test.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/botstory/di/parser_test.py b/botstory/di/parser_test.py index a07cc97..888dd7e 100644 --- a/botstory/di/parser_test.py +++ b/botstory/di/parser_test.py @@ -1,5 +1,9 @@ from .parser import camel_case_to_underscore -def test_(): +def test_camelcase_to_underscore(): assert camel_case_to_underscore('ClassName')[0] == 'class_name' + + +def test_remove_leading_underscore(): + assert camel_case_to_underscore('_ClassName')[0] == 'class_name' From 565aab9d9cf4fb1fae101c1bb2a93ae730e687f2 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Tue, 22 Nov 2016 10:14:51 +0100 Subject: [PATCH 11/40] empty array on non class name value --- botstory/di/parser_test.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/botstory/di/parser_test.py b/botstory/di/parser_test.py index 888dd7e..2ef0c15 100644 --- a/botstory/di/parser_test.py +++ b/botstory/di/parser_test.py @@ -7,3 +7,7 @@ def test_camelcase_to_underscore(): def test_remove_leading_underscore(): assert camel_case_to_underscore('_ClassName')[0] == 'class_name' + + +def test_should_return_empty_array_if_no_any_class_name_here(): + assert camel_case_to_underscore('_qwerty') == [] From e72bcf34c60e30508eee64703069240adec93f0b Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Tue, 22 Nov 2016 10:19:20 +0100 Subject: [PATCH 12/40] cover incorrect usage --- botstory/di/inject_test.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/botstory/di/inject_test.py b/botstory/di/inject_test.py index 17a61d8..8e5378b 100644 --- a/botstory/di/inject_test.py +++ b/botstory/di/inject_test.py @@ -1,3 +1,4 @@ +import pytest from .. import di @@ -125,3 +126,8 @@ class OneClass: pass assert isinstance(di.injector.get('qwerty'), OneClass) + + +def test_fail_on_incorrect_using(): + with pytest.raises(NotImplementedError): + di.inject()('qwerty') From 0302ff8c822196a1d9c301b16050a6b53fd39848 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Tue, 22 Nov 2016 10:47:48 +0100 Subject: [PATCH 13/40] request aiohttp interface as dep --- botstory/di/inject.py | 1 + botstory/di/injector_service_test.py | 1 + botstory/integrations/aiohttp/aiohttp.py | 22 ++++++++++++------- botstory/integrations/aiohttp/aiohttp_test.py | 12 ++++++++++ 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/botstory/di/inject.py b/botstory/di/inject.py index abc48a2..1c583cf 100644 --- a/botstory/di/inject.py +++ b/botstory/di/inject.py @@ -9,6 +9,7 @@ def inject(t=None): def decorated_fn(fn): if inspect.isclass(fn): name = t or camel_case_to_underscore(fn.__name__)[0] + print('register {} on name {}'.format(fn, name)) di.injector.register(name, fn) elif inspect.isfunction(fn): di.injector.requires(fn) diff --git a/botstory/di/injector_service_test.py b/botstory/di/injector_service_test.py index 3e8c528..da4ec87 100644 --- a/botstory/di/injector_service_test.py +++ b/botstory/di/injector_service_test.py @@ -1,6 +1,7 @@ from .. import di +# TODO: should make scoped di def teardown_function(function): di.clear() diff --git a/botstory/integrations/aiohttp/aiohttp.py b/botstory/integrations/aiohttp/aiohttp.py index ac2e156..1d59255 100644 --- a/botstory/integrations/aiohttp/aiohttp.py +++ b/botstory/integrations/aiohttp/aiohttp.py @@ -7,6 +7,7 @@ from yarl import URL from ..commonhttp import errors as common_errors, statuses +from ... import di logger = logging.getLogger(__name__) @@ -24,6 +25,10 @@ def is_ok(status): return 200 <= status < 400 +print('before @di.inject') + + +@di.inject('http.interface') class AioHttpInterface: type = 'interface.aiohttp' @@ -154,15 +159,16 @@ async def method(self, method_type, session, url, **kwargs): message=await resp.text(), ) except Exception as err: - logger.warn('Exception: status: {status}, message: {message}, type: {type}, method: {method}, url: {url}, {kwargs}' - .format(status=getattr(err, 'code', None), - message=getattr(err, 'message', None), - type=type(err), - method=method_name, - url=url, - kwargs=kwargs, - ) + logger.warn( + 'Exception: status: {status}, message: {message}, type: {type}, method: {method}, url: {url}, {kwargs}' + .format(status=getattr(err, 'code', None), + message=getattr(err, 'message', None), + type=type(err), + method=method_name, + url=url, + kwargs=kwargs, ) + ) raise err return resp diff --git a/botstory/integrations/aiohttp/aiohttp_test.py b/botstory/integrations/aiohttp/aiohttp_test.py index 82d336d..904b0fd 100644 --- a/botstory/integrations/aiohttp/aiohttp_test.py +++ b/botstory/integrations/aiohttp/aiohttp_test.py @@ -1,9 +1,13 @@ from aiohttp import test_utils import json +import importlib import pytest from . import AioHttpInterface +from .. import aiohttp from ..commonhttp import errors from ..tests import fake_server +from ... import di + @pytest.fixture def webhook_handler(): @@ -13,6 +17,7 @@ def webhook_handler(): 'text': json.dumps({'message': 'Ok!'}), }) + @pytest.mark.asyncio async def test_post(event_loop): async with fake_server.FakeFacebook(event_loop) as server: @@ -179,3 +184,10 @@ async def mock_middleware_handler(request): assert handler_stub.called finally: await http.stop() + + +def test_get_as_deps(): + # TODO: require reload aiohttp module because somewhere is used global di.clear() + importlib.reload(aiohttp.aiohttp) + importlib.reload(aiohttp) + assert isinstance(di.injector.get('http.interface'), aiohttp.AioHttpInterface) From a51ea36d415e12a47636fa0c4b3e2c8f60e8c82f Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Tue, 22 Nov 2016 23:35:15 +0100 Subject: [PATCH 14/40] request fb interface as dep --- botstory/integrations/fb/messenger.py | 2 ++ botstory/integrations/fb/messenger_test.py | 12 ++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/botstory/integrations/fb/messenger.py b/botstory/integrations/fb/messenger.py index 08d61fd..2cda8ca 100644 --- a/botstory/integrations/fb/messenger.py +++ b/botstory/integrations/fb/messenger.py @@ -2,12 +2,14 @@ import logging from . import validate from .. import commonhttp +from ... import di from ...middlewares import option from ...ast import users logger = logging.getLogger(__name__) +@di.inject('fb.interface') class FBInterface: type = 'interface.facebook' diff --git a/botstory/integrations/fb/messenger_test.py b/botstory/integrations/fb/messenger_test.py index e984e94..0d717ef 100644 --- a/botstory/integrations/fb/messenger_test.py +++ b/botstory/integrations/fb/messenger_test.py @@ -1,11 +1,12 @@ import asyncio import logging +import importlib from unittest import mock import pytest from . import messenger -from .. import commonhttp, mockdb, mockhttp -from ... import chat, story, utils +from .. import commonhttp, fb, mockdb, mockhttp +from ... import chat, di, story, utils from ...middlewares import any, option logger = logging.getLogger(__name__) @@ -835,3 +836,10 @@ async def test_remove_persistent_menu(): 'thread_state': 'existing_thread' } ) + + +def test_get_as_deps(): + # TODO: require reload aiohttp module because somewhere is used global di.clear() + importlib.reload(fb.messenger) + importlib.reload(fb) + assert isinstance(di.injector.get('fb.interface'), messenger.FBInterface) From be182b92b4af42cfec66ebd679f959fc06bc03c6 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Tue, 22 Nov 2016 23:58:50 +0100 Subject: [PATCH 15/40] request ga tracker interface as dep --- botstory/integrations/ga/tracker.py | 4 +++- botstory/integrations/ga/tracker_test.py | 12 ++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/botstory/integrations/ga/tracker.py b/botstory/integrations/ga/tracker.py index b7f11d9..f043f16 100644 --- a/botstory/integrations/ga/tracker.py +++ b/botstory/integrations/ga/tracker.py @@ -2,9 +2,11 @@ import json from .universal_analytics.tracker import Tracker +from ... import di from ...utils import queue +@di.inject('tracker') class GAStatistics: type = 'interface.tracker' """ @@ -15,7 +17,7 @@ class GAStatistics: """ def __init__(self, - tracking_id, + tracking_id=None, story_tracking_template='{story}/{part}', new_message_tracking_template='receive: {data}', ): diff --git a/botstory/integrations/ga/tracker_test.py b/botstory/integrations/ga/tracker_test.py index 6f60446..1cd0f46 100644 --- a/botstory/integrations/ga/tracker_test.py +++ b/botstory/integrations/ga/tracker_test.py @@ -1,12 +1,13 @@ import aiohttp import asyncio +import importlib import json import pytest from unittest import mock from . import GAStatistics, tracker -from .. import fb, mockdb, mockhttp -from ... import story, utils +from .. import fb, ga, mockdb, mockhttp +from ... import di, story, utils def setup_function(): @@ -133,3 +134,10 @@ def greeting(message): 'one_story/greeting', ), ]) + + +def test_get_as_deps(): + # TODO: require reload aiohttp module because somewhere is used global di.clear() + importlib.reload(ga.tracker) + importlib.reload(ga) + assert isinstance(di.injector.get('tracker'), ga.GAStatistics) From e618ddae7bb58762a04d8ad16db257fa5ed9d723 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Wed, 23 Nov 2016 00:01:18 +0100 Subject: [PATCH 16/40] request mock db interface as dep --- botstory/integrations/mockdb/db.py | 3 ++- botstory/integrations/mockdb/db_test.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 botstory/integrations/mockdb/db_test.py diff --git a/botstory/integrations/mockdb/db.py b/botstory/integrations/mockdb/db.py index e745628..70564da 100644 --- a/botstory/integrations/mockdb/db.py +++ b/botstory/integrations/mockdb/db.py @@ -1,7 +1,8 @@ import aiohttp -from ... import utils +from ... import di, utils +@di.inject('storage') class MockDB: type = 'interface.session_storage' diff --git a/botstory/integrations/mockdb/db_test.py b/botstory/integrations/mockdb/db_test.py new file mode 100644 index 0000000..f2d74a8 --- /dev/null +++ b/botstory/integrations/mockdb/db_test.py @@ -0,0 +1,10 @@ +import importlib +from .. import mockdb +from ... import di + + +def test_get_as_deps(): + # TODO: require reload aiohttp module because somewhere is used global di.clear() + importlib.reload(mockdb.db) + importlib.reload(mockdb) + assert isinstance(di.injector.get('storage'), mockdb.MockDB) From 5353d5958e436477ec02039bfdd44453b46d6652 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Wed, 23 Nov 2016 00:02:53 +0100 Subject: [PATCH 17/40] request mock http interface as dep --- botstory/integrations/mockhttp/mockhttp.py | 3 +++ botstory/integrations/mockhttp/mockhttp_test.py | 10 ++++++++++ 2 files changed, 13 insertions(+) create mode 100644 botstory/integrations/mockhttp/mockhttp_test.py diff --git a/botstory/integrations/mockhttp/mockhttp.py b/botstory/integrations/mockhttp/mockhttp.py index c6a1202..ac90886 100644 --- a/botstory/integrations/mockhttp/mockhttp.py +++ b/botstory/integrations/mockhttp/mockhttp.py @@ -3,6 +3,8 @@ import json from unittest import mock +from ... import di + def stub(name=None): """ @@ -15,6 +17,7 @@ def stub(name=None): return mock.MagicMock(spec=lambda *args, **kwargs: None, name=name) +@di.inject('http') class MockHttpInterface: type = 'interface.http' diff --git a/botstory/integrations/mockhttp/mockhttp_test.py b/botstory/integrations/mockhttp/mockhttp_test.py new file mode 100644 index 0000000..927cc10 --- /dev/null +++ b/botstory/integrations/mockhttp/mockhttp_test.py @@ -0,0 +1,10 @@ +import importlib +from .. import mockhttp +from ... import di + + +def test_get_as_deps(): + # TODO: require reload aiohttp module because somewhere is used global di.clear() + importlib.reload(mockhttp.mockhttp) + importlib.reload(mockhttp) + assert isinstance(di.injector.get('http'), mockhttp.MockHttpInterface) From 6099d02e19a2339d652f45f47069e67fdd6fd6d3 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Wed, 23 Nov 2016 00:13:43 +0100 Subject: [PATCH 18/40] bind mock tracker as dep --- botstory/integrations/mocktracker/tracker.py | 2 ++ .../integrations/mocktracker/tracker_test.py | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/botstory/integrations/mocktracker/tracker.py b/botstory/integrations/mocktracker/tracker.py index 0bd3c35..b8b20d2 100644 --- a/botstory/integrations/mocktracker/tracker.py +++ b/botstory/integrations/mocktracker/tracker.py @@ -1,8 +1,10 @@ import logging +from ... import di logger = logging.getLogger(__name__) +@di.inject('tracker') class MockTracker: type = 'interface.tracker' diff --git a/botstory/integrations/mocktracker/tracker_test.py b/botstory/integrations/mocktracker/tracker_test.py index a57ecfc..9a3b3b8 100644 --- a/botstory/integrations/mocktracker/tracker_test.py +++ b/botstory/integrations/mocktracker/tracker_test.py @@ -1,4 +1,7 @@ +import importlib from . import tracker +from .. import mocktracker +from ... import di, story def test_event(): @@ -19,3 +22,19 @@ def test_new_user(): def test_story(): t = tracker.MockTracker() t.story() + + +def test_get_as_deps(): + # TODO: require reload aiohttp module because somewhere is used global di.clear() + # importlib.reload(mocktracker.tracker) + # importlib.reload(mocktracker) + + story.use(mocktracker.MockTracker()) + + @di.inject() + class OneClass: + @di.inject() + def deps(self, tracker): + self.tracker = tracker + + assert isinstance(di.injector.get('one-class').tracker, mocktracker.MockTracker) From 539b59bc38439cc68ff0e47e4369d08e880836ab Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Wed, 23 Nov 2016 01:43:59 +0100 Subject: [PATCH 19/40] skip fail test --- botstory/integrations/mocktracker/tracker_test.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/botstory/integrations/mocktracker/tracker_test.py b/botstory/integrations/mocktracker/tracker_test.py index 9a3b3b8..235577a 100644 --- a/botstory/integrations/mocktracker/tracker_test.py +++ b/botstory/integrations/mocktracker/tracker_test.py @@ -1,4 +1,5 @@ -import importlib +import pytest + from . import tracker from .. import mocktracker from ... import di, story @@ -24,7 +25,8 @@ def test_story(): t.story() -def test_get_as_deps(): +@pytest.mark.skip('DI does not work this way') +def test_get_mock_tracker_as_dep(): # TODO: require reload aiohttp module because somewhere is used global di.clear() # importlib.reload(mocktracker.tracker) # importlib.reload(mocktracker) From e03dc50eec5980f28db04974cc791629ee433308 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Wed, 23 Nov 2016 02:12:39 +0100 Subject: [PATCH 20/40] describe lazy class, that can be later easily registrated --- botstory/di/__init__.py | 10 ++++--- botstory/di/desciption.py | 24 +++++++++++++++++ botstory/di/injector_service.py | 23 +++++++++++++++- botstory/di/injector_service_test.py | 26 +++++++++++++++++++ .../integrations/mocktracker/tracker_test.py | 2 +- 5 files changed, 79 insertions(+), 6 deletions(-) create mode 100644 botstory/di/desciption.py diff --git a/botstory/di/__init__.py b/botstory/di/__init__.py index c3de7d9..e08753c 100644 --- a/botstory/di/__init__.py +++ b/botstory/di/__init__.py @@ -1,10 +1,12 @@ -from .injector_service import Injector -from .inject import inject +from . import desciption, inject as inject_module, injector_service __all__ = [] -injector = Injector() +injector = injector_service.Injector() + bind = injector.bind clear = injector.clear +desc = desciption.desc +inject = inject_module.inject -__all__.extend([bind, clear, inject]) +__all__.extend([bind, clear, desc, inject]) diff --git a/botstory/di/desciption.py b/botstory/di/desciption.py new file mode 100644 index 0000000..3763640 --- /dev/null +++ b/botstory/di/desciption.py @@ -0,0 +1,24 @@ +import inspect +from .parser import camel_case_to_underscore +from .. import di + + +def desc(lazy=False, t=None): + """ + TODO: + :param lazy: + :param t: + :return: + """ + + def decorated_fn(fn): + if not inspect.isclass(fn): + return NotImplemented('For now we can only describe classes') + name = t or camel_case_to_underscore(fn.__name__)[0] + if lazy: + di.injector.describe(name, fn) + else: + di.injector.register(name, fn) + return fn + + return decorated_fn diff --git a/botstory/di/injector_service.py b/botstory/di/injector_service.py index 3a8b640..81ae982 100644 --- a/botstory/di/injector_service.py +++ b/botstory/di/injector_service.py @@ -25,8 +25,29 @@ def __init__(self): self.requires_fns = {} # instances that will autoupdate on each new instance come self.auto_update_list = [] + # description of classes + self.described = {} - def register(self, type_name, instance): + def describe(self, type_name, cls): + """ + add description of class + + :param type_name: + :param cls: + :return: + """ + + self.described[cls] = { + 'type': type_name, + } + + def register(self, type_name=None, instance=None): + if not type_name: + try: + desc = self.described.get(instance, self.described.get(type(instance))) + except KeyError: + return None + type_name = desc['type'] self.root.register(type_name, instance) for wait_instance in self.auto_update_list: self.bind(wait_instance) diff --git a/botstory/di/injector_service_test.py b/botstory/di/injector_service_test.py index da4ec87..7b04f6b 100644 --- a/botstory/di/injector_service_test.py +++ b/botstory/di/injector_service_test.py @@ -9,3 +9,29 @@ def teardown_function(function): def test_injector_get(): di.injector.register('once_instance', 'Hello World!') assert di.injector.get('once_instance') == 'Hello World!' + + +def test_lazy_description_should_not_register_class(): + @di.desc(lazy=True) + class OneClass: + pass + + assert di.injector.get('one_class') is None + + +def test_lazy_description_should_simplify_registration(): + @di.desc(lazy=True) + class OneClass: + pass + + di.injector.register(instance=OneClass()) + + assert isinstance(di.injector.get('one_class'), OneClass) + + +def test_not_lazy_description_should_simplify_registration(): + @di.desc(lazy=False) + class OneClass: + pass + + assert isinstance(di.injector.get('one_class'), OneClass) diff --git a/botstory/integrations/mocktracker/tracker_test.py b/botstory/integrations/mocktracker/tracker_test.py index 235577a..e8de42c 100644 --- a/botstory/integrations/mocktracker/tracker_test.py +++ b/botstory/integrations/mocktracker/tracker_test.py @@ -25,7 +25,7 @@ def test_story(): t.story() -@pytest.mark.skip('DI does not work this way') +@pytest.mark.skip('DI gdoes not work this way') def test_get_mock_tracker_as_dep(): # TODO: require reload aiohttp module because somewhere is used global di.clear() # importlib.reload(mocktracker.tracker) From f30971e01d096b4e1a3fc22f3e8227b54cd48756 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Wed, 23 Nov 2016 10:11:18 +0100 Subject: [PATCH 21/40] lazy registration of mock tracker --- botstory/di/__init__.py | 4 +-- botstory/di/desciption.py | 14 +++++----- botstory/di/injector_service.py | 24 ++++++++++------- botstory/integrations/mocktracker/tracker.py | 2 +- .../integrations/mocktracker/tracker_test.py | 26 ++++++++++++++----- botstory/story.py | 4 ++- 6 files changed, 48 insertions(+), 26 deletions(-) diff --git a/botstory/di/__init__.py b/botstory/di/__init__.py index e08753c..ae10127 100644 --- a/botstory/di/__init__.py +++ b/botstory/di/__init__.py @@ -1,4 +1,4 @@ -from . import desciption, inject as inject_module, injector_service +from . import desciption as desc_module, inject as inject_module, injector_service __all__ = [] @@ -6,7 +6,7 @@ bind = injector.bind clear = injector.clear -desc = desciption.desc +desc = desc_module.desc inject = inject_module.inject __all__.extend([bind, clear, desc, inject]) diff --git a/botstory/di/desciption.py b/botstory/di/desciption.py index 3763640..64dea03 100644 --- a/botstory/di/desciption.py +++ b/botstory/di/desciption.py @@ -3,7 +3,7 @@ from .. import di -def desc(lazy=False, t=None): +def desc(t=None, lazy=False): """ TODO: :param lazy: @@ -11,14 +11,14 @@ def desc(lazy=False, t=None): :return: """ - def decorated_fn(fn): - if not inspect.isclass(fn): + def decorated_fn(cls): + if not inspect.isclass(cls): return NotImplemented('For now we can only describe classes') - name = t or camel_case_to_underscore(fn.__name__)[0] + name = t or camel_case_to_underscore(cls.__name__)[0] if lazy: - di.injector.describe(name, fn) + di.injector.describe(name, cls) else: - di.injector.register(name, fn) - return fn + di.injector.register(name, cls) + return cls return decorated_fn diff --git a/botstory/di/injector_service.py b/botstory/di/injector_service.py index 81ae982..09cc51c 100644 --- a/botstory/di/injector_service.py +++ b/botstory/di/injector_service.py @@ -16,17 +16,21 @@ def register(self, type_name, value): self.storage[type_name] = value +def null_if_empty(value): + return value if value is not inspect.Parameter.empty else None + + class Injector: def __init__(self): - self.root = Scope() - # all instances that are singletones - self.singleton_cache = {} - # functions that waits for deps - self.requires_fns = {} # instances that will autoupdate on each new instance come self.auto_update_list = [] # description of classes self.described = {} + # functions that waits for deps + self.requires_fns = {} + self.root = Scope() + # all instances that are singletones + self.singleton_cache = {} def describe(self, type_name, cls): """ @@ -42,9 +46,9 @@ def describe(self, type_name, cls): } def register(self, type_name=None, instance=None): - if not type_name: + if type_name is None: try: - desc = self.described.get(instance, self.described.get(type(instance))) + desc = self.described.get(instance, self.described[type(instance)]) except KeyError: return None type_name = desc['type'] @@ -55,7 +59,7 @@ def register(self, type_name=None, instance=None): def requires(self, fn): fn_sig = inspect.signature(fn) self.requires_fns[fn] = { - key: {'default': fn_sig.parameters[key].default} + key: {'default': null_if_empty(fn_sig.parameters[key].default)} for key in fn_sig.parameters.keys() if key != 'self'} def bind(self, instance, autoupdate=False): @@ -80,9 +84,11 @@ def bind(self, instance, autoupdate=False): return instance def clear(self): + self.auto_update_list = [] + self.described = {} + self.requires_fns = {} self.root = Scope() self.singleton_cache = {} - self.requires_fns = {} def get(self, type_name): try: diff --git a/botstory/integrations/mocktracker/tracker.py b/botstory/integrations/mocktracker/tracker.py index b8b20d2..43893b1 100644 --- a/botstory/integrations/mocktracker/tracker.py +++ b/botstory/integrations/mocktracker/tracker.py @@ -4,7 +4,7 @@ logger = logging.getLogger(__name__) -@di.inject('tracker') +@di.desc('tracker', lazy=True) class MockTracker: type = 'interface.tracker' diff --git a/botstory/integrations/mocktracker/tracker_test.py b/botstory/integrations/mocktracker/tracker_test.py index e8de42c..33e3045 100644 --- a/botstory/integrations/mocktracker/tracker_test.py +++ b/botstory/integrations/mocktracker/tracker_test.py @@ -1,10 +1,22 @@ import pytest +import importlib from . import tracker from .. import mocktracker from ... import di, story +# TODO: should make scoped di +def teardown_function(function): + di.clear() + + +def reload_mocktracker(): + # TODO: require reload aiohttp module because somewhere is used global di.clear() + importlib.reload(mocktracker.tracker) + importlib.reload(mocktracker) + + def test_event(): t = tracker.MockTracker() t.event() @@ -25,18 +37,20 @@ def test_story(): t.story() -@pytest.mark.skip('DI gdoes not work this way') +# @pytest.mark.skip('DI gdoes not work this way') def test_get_mock_tracker_as_dep(): - # TODO: require reload aiohttp module because somewhere is used global di.clear() - # importlib.reload(mocktracker.tracker) - # importlib.reload(mocktracker) + reload_mocktracker() story.use(mocktracker.MockTracker()) - @di.inject() + @di.desc() class OneClass: @di.inject() def deps(self, tracker): self.tracker = tracker - assert isinstance(di.injector.get('one-class').tracker, mocktracker.MockTracker) + print('di.injector.root.storage') + print(di.injector.root.storage) + print('di.injector.described') + print(di.injector.described) + assert isinstance(di.injector.get('one_class').tracker, mocktracker.MockTracker) diff --git a/botstory/story.py b/botstory/story.py index 98b492e..1f3c10f 100644 --- a/botstory/story.py +++ b/botstory/story.py @@ -1,7 +1,7 @@ import asyncio import logging -from . import chat +from . import chat, di from .ast import callable as callable_module, common, \ forking, library, parser, processor, users @@ -75,6 +75,8 @@ def use(middleware): middlewares.append(middleware) + di.injector.register(instance=middleware) + # TODO: maybe it is good time to start using DI (dependency injection) if check_spec(['send_text_message'], middleware): From 792012c7263067975dea020f26ef3fb3794ea776 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Wed, 23 Nov 2016 10:17:57 +0100 Subject: [PATCH 22/40] type of registered instance should be string --- botstory/di/injector_service.py | 2 ++ botstory/di/injector_service_test.py | 9 +++++++++ botstory/integrations/mocktracker/tracker_test.py | 5 ----- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/botstory/di/injector_service.py b/botstory/di/injector_service.py index 09cc51c..704e76f 100644 --- a/botstory/di/injector_service.py +++ b/botstory/di/injector_service.py @@ -46,6 +46,8 @@ def describe(self, type_name, cls): } def register(self, type_name=None, instance=None): + if not isinstance(type_name, str) and type_name is not None: + raise ValueError('type_name parameter should be string or None') if type_name is None: try: desc = self.described.get(instance, self.described[type(instance)]) diff --git a/botstory/di/injector_service_test.py b/botstory/di/injector_service_test.py index 7b04f6b..2a05fd0 100644 --- a/botstory/di/injector_service_test.py +++ b/botstory/di/injector_service_test.py @@ -1,3 +1,4 @@ +import pytest from .. import di @@ -35,3 +36,11 @@ class OneClass: pass assert isinstance(di.injector.get('one_class'), OneClass) + + +def test_fail_if_type_is_not_string(): + class OneClass: + pass + + with pytest.raises(ValueError): + di.injector.register(OneClass) diff --git a/botstory/integrations/mocktracker/tracker_test.py b/botstory/integrations/mocktracker/tracker_test.py index 33e3045..5082642 100644 --- a/botstory/integrations/mocktracker/tracker_test.py +++ b/botstory/integrations/mocktracker/tracker_test.py @@ -37,7 +37,6 @@ def test_story(): t.story() -# @pytest.mark.skip('DI gdoes not work this way') def test_get_mock_tracker_as_dep(): reload_mocktracker() @@ -49,8 +48,4 @@ class OneClass: def deps(self, tracker): self.tracker = tracker - print('di.injector.root.storage') - print(di.injector.root.storage) - print('di.injector.described') - print(di.injector.described) assert isinstance(di.injector.get('one_class').tracker, mocktracker.MockTracker) From 2cc510296ccb655ab8b87a43b718371e1766c2e3 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Wed, 23 Nov 2016 10:32:20 +0100 Subject: [PATCH 23/40] kekab-string-style is synonym to underscore_string_style --- botstory/di/injector_service.py | 3 +++ botstory/di/injector_service_test.py | 8 ++++++++ botstory/di/parser.py | 9 +++++++++ botstory/di/parser_test.py | 6 +++++- 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/botstory/di/injector_service.py b/botstory/di/injector_service.py index 704e76f..205e9e9 100644 --- a/botstory/di/injector_service.py +++ b/botstory/di/injector_service.py @@ -1,5 +1,7 @@ import inspect +from . import parser + class Scope: def __init__(self): @@ -93,6 +95,7 @@ def clear(self): self.singleton_cache = {} def get(self, type_name): + type_name = parser.kebab_to_underscore(type_name) try: return self.singleton_cache[type_name] except KeyError: diff --git a/botstory/di/injector_service_test.py b/botstory/di/injector_service_test.py index 2a05fd0..f0b7ee5 100644 --- a/botstory/di/injector_service_test.py +++ b/botstory/di/injector_service_test.py @@ -44,3 +44,11 @@ class OneClass: with pytest.raises(ValueError): di.injector.register(OneClass) + + +def test_kebab_string_style_is_synonym_to_underscore(): + @di.desc() + class OneClass: + pass + + assert isinstance(di.injector.get('one-class'), OneClass) diff --git a/botstory/di/parser.py b/botstory/di/parser.py index 699ced0..4aabe7c 100644 --- a/botstory/di/parser.py +++ b/botstory/di/parser.py @@ -26,3 +26,12 @@ def camel_case_to_underscore(class_name): if not parts: return [] return ['_'.join(part.lower() for part in parts)] + + +def kebab_to_underscore(s): + """ + Convert kebab-styled-string to underscore_styled_string + :param s: + :return: + """ + return s.replace('-', '_') diff --git a/botstory/di/parser_test.py b/botstory/di/parser_test.py index 2ef0c15..1d549ab 100644 --- a/botstory/di/parser_test.py +++ b/botstory/di/parser_test.py @@ -1,4 +1,4 @@ -from .parser import camel_case_to_underscore +from .parser import camel_case_to_underscore, kebab_to_underscore def test_camelcase_to_underscore(): @@ -11,3 +11,7 @@ def test_remove_leading_underscore(): def test_should_return_empty_array_if_no_any_class_name_here(): assert camel_case_to_underscore('_qwerty') == [] + + +def test_kebab_to_underscore(): + assert kebab_to_underscore('hello-world') == 'hello_world' From a62f0ea12f26b8df01dab62137419faca5ffdb2b Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Wed, 23 Nov 2016 10:36:34 +0100 Subject: [PATCH 24/40] rename lazy to reg(istrate) --- botstory/di/desciption.py | 15 ++++++++------- botstory/di/injector_service_test.py | 6 +++--- botstory/integrations/mocktracker/tracker.py | 2 +- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/botstory/di/desciption.py b/botstory/di/desciption.py index 64dea03..c03dc09 100644 --- a/botstory/di/desciption.py +++ b/botstory/di/desciption.py @@ -3,11 +3,12 @@ from .. import di -def desc(t=None, lazy=False): +def desc(t=None, reg=True): """ - TODO: - :param lazy: - :param t: + Describe Class Dependency + + :param reg: should we register this class as well + :param t: custom type as well :return: """ @@ -15,10 +16,10 @@ def decorated_fn(cls): if not inspect.isclass(cls): return NotImplemented('For now we can only describe classes') name = t or camel_case_to_underscore(cls.__name__)[0] - if lazy: - di.injector.describe(name, cls) - else: + if reg: di.injector.register(name, cls) + else: + di.injector.describe(name, cls) return cls return decorated_fn diff --git a/botstory/di/injector_service_test.py b/botstory/di/injector_service_test.py index f0b7ee5..c321a68 100644 --- a/botstory/di/injector_service_test.py +++ b/botstory/di/injector_service_test.py @@ -13,7 +13,7 @@ def test_injector_get(): def test_lazy_description_should_not_register_class(): - @di.desc(lazy=True) + @di.desc(reg=False) class OneClass: pass @@ -21,7 +21,7 @@ class OneClass: def test_lazy_description_should_simplify_registration(): - @di.desc(lazy=True) + @di.desc(reg=False) class OneClass: pass @@ -31,7 +31,7 @@ class OneClass: def test_not_lazy_description_should_simplify_registration(): - @di.desc(lazy=False) + @di.desc(reg=True) class OneClass: pass diff --git a/botstory/integrations/mocktracker/tracker.py b/botstory/integrations/mocktracker/tracker.py index 43893b1..887702b 100644 --- a/botstory/integrations/mocktracker/tracker.py +++ b/botstory/integrations/mocktracker/tracker.py @@ -4,7 +4,7 @@ logger = logging.getLogger(__name__) -@di.desc('tracker', lazy=True) +@di.desc('tracker', reg=False) class MockTracker: type = 'interface.tracker' From 4197ae798063e6a9f2e1abd5b703dc3d81772066 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Wed, 23 Nov 2016 23:25:10 +0100 Subject: [PATCH 25/40] describe mongo interface without registration --- .../integrations/mocktracker/tracker_test.py | 1 - botstory/integrations/mongodb/db.py | 2 ++ botstory/integrations/mongodb/db_test.py | 24 ++++++++++++++++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/botstory/integrations/mocktracker/tracker_test.py b/botstory/integrations/mocktracker/tracker_test.py index 5082642..91e446e 100644 --- a/botstory/integrations/mocktracker/tracker_test.py +++ b/botstory/integrations/mocktracker/tracker_test.py @@ -1,4 +1,3 @@ -import pytest import importlib from . import tracker diff --git a/botstory/integrations/mongodb/db.py b/botstory/integrations/mongodb/db.py index e85a282..58f7992 100644 --- a/botstory/integrations/mongodb/db.py +++ b/botstory/integrations/mongodb/db.py @@ -1,10 +1,12 @@ import asyncio import logging from motor import motor_asyncio +from ... import di logger = logging.getLogger(__name__) +@di.desc('storage', reg=False) class MongodbInterface: type = 'interface.session_storage' diff --git a/botstory/integrations/mongodb/db_test.py b/botstory/integrations/mongodb/db_test.py index 6db00b5..b27f45c 100644 --- a/botstory/integrations/mongodb/db_test.py +++ b/botstory/integrations/mongodb/db_test.py @@ -1,10 +1,12 @@ import logging +import importlib import os import pytest from . import db +from .. import mongodb from ..fb import messenger -from ... import story, utils +from ... import di, story, utils logger = logging.getLogger(__name__) @@ -14,6 +16,12 @@ def teardown_function(function): story.stories_library.clear() +def reload_module(): + # TODO: require reload aiohttp module because somewhere is used global di.clear() + importlib.reload(mongodb.db) + importlib.reload(mongodb) + + @pytest.fixture def build_context(): async def builder(mongodb, no_session=False, no_user=False): @@ -119,3 +127,17 @@ async def test_start_should_open_connection_and_close_on_stop(): assert not db_interface.session_collection assert not db_interface.user_collection assert not db_interface.db + + +def test_get_mongodb_as_dep(): + reload_module() + + story.use(mongodb.MongodbInterface()) + + @di.desc() + class OneClass: + @di.inject() + def deps(self, storage): + self.storage = storage + + assert isinstance(di.injector.get('one_class').storage, mongodb.MongodbInterface) From 9097257b4e72a54beb50e6d4cd92113d1d166539 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Wed, 23 Nov 2016 23:32:33 +0100 Subject: [PATCH 26/40] describe mock http interface without registation --- .../integrations/mockhttp/mockhttp_test.py | 19 ++++++++++++++++--- botstory/integrations/mongodb/db_test.py | 2 +- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/botstory/integrations/mockhttp/mockhttp_test.py b/botstory/integrations/mockhttp/mockhttp_test.py index 927cc10..5f7802d 100644 --- a/botstory/integrations/mockhttp/mockhttp_test.py +++ b/botstory/integrations/mockhttp/mockhttp_test.py @@ -1,10 +1,23 @@ import importlib from .. import mockhttp -from ... import di +from ... import di, story -def test_get_as_deps(): +def reload_module(): # TODO: require reload aiohttp module because somewhere is used global di.clear() importlib.reload(mockhttp.mockhttp) importlib.reload(mockhttp) - assert isinstance(di.injector.get('http'), mockhttp.MockHttpInterface) + + +def test_get_as_deps(): + reload_module() + + story.use(mockhttp.MockHttpInterface()) + + @di.desc() + class OneClass: + @di.inject() + def deps(self, http): + self.http = http + + assert isinstance(di.injector.get('one_class').http, mockhttp.MockHttpInterface) diff --git a/botstory/integrations/mongodb/db_test.py b/botstory/integrations/mongodb/db_test.py index b27f45c..6b4728c 100644 --- a/botstory/integrations/mongodb/db_test.py +++ b/botstory/integrations/mongodb/db_test.py @@ -18,7 +18,7 @@ def teardown_function(function): def reload_module(): # TODO: require reload aiohttp module because somewhere is used global di.clear() - importlib.reload(mongodb.db) + importlib.reload(mongodb.dbddd) importlib.reload(mongodb) From 7b9bd3de843d9947e3e5148b7273c721d22cb552 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Wed, 23 Nov 2016 23:56:22 +0100 Subject: [PATCH 27/40] fix tests --- botstory/integrations/mockdb/db_test.py | 23 ++++++++++++++++--- botstory/integrations/mockhttp/mockhttp.py | 2 +- .../integrations/mockhttp/mockhttp_test.py | 6 ++++- botstory/integrations/mongodb/db_test.py | 2 +- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/botstory/integrations/mockdb/db_test.py b/botstory/integrations/mockdb/db_test.py index f2d74a8..4655f75 100644 --- a/botstory/integrations/mockdb/db_test.py +++ b/botstory/integrations/mockdb/db_test.py @@ -1,10 +1,27 @@ import importlib from .. import mockdb -from ... import di +from ... import di, story -def test_get_as_deps(): +def teardown_function(function): + di.clear() + + +def reload_module(): # TODO: require reload aiohttp module because somewhere is used global di.clear() importlib.reload(mockdb.db) importlib.reload(mockdb) - assert isinstance(di.injector.get('storage'), mockdb.MockDB) + + +def test_get_mockdb_as_dep(): + reload_module() + + story.use(mockdb.MockDB()) + + @di.desc() + class OneClass: + @di.inject() + def deps(self, storage): + self.storage = storage + + assert isinstance(di.injector.get('one_class').storage, mockdb.MockDB) diff --git a/botstory/integrations/mockhttp/mockhttp.py b/botstory/integrations/mockhttp/mockhttp.py index ac90886..23456b9 100644 --- a/botstory/integrations/mockhttp/mockhttp.py +++ b/botstory/integrations/mockhttp/mockhttp.py @@ -17,7 +17,7 @@ def stub(name=None): return mock.MagicMock(spec=lambda *args, **kwargs: None, name=name) -@di.inject('http') +@di.desc('http', reg=False) class MockHttpInterface: type = 'interface.http' diff --git a/botstory/integrations/mockhttp/mockhttp_test.py b/botstory/integrations/mockhttp/mockhttp_test.py index 5f7802d..b1342d8 100644 --- a/botstory/integrations/mockhttp/mockhttp_test.py +++ b/botstory/integrations/mockhttp/mockhttp_test.py @@ -3,13 +3,17 @@ from ... import di, story +def teardown_function(function): + di.clear() + + def reload_module(): # TODO: require reload aiohttp module because somewhere is used global di.clear() importlib.reload(mockhttp.mockhttp) importlib.reload(mockhttp) -def test_get_as_deps(): +def test_get_mockhttp_as_dep(): reload_module() story.use(mockhttp.MockHttpInterface()) diff --git a/botstory/integrations/mongodb/db_test.py b/botstory/integrations/mongodb/db_test.py index 6b4728c..b27f45c 100644 --- a/botstory/integrations/mongodb/db_test.py +++ b/botstory/integrations/mongodb/db_test.py @@ -18,7 +18,7 @@ def teardown_function(function): def reload_module(): # TODO: require reload aiohttp module because somewhere is used global di.clear() - importlib.reload(mongodb.dbddd) + importlib.reload(mongodb.db) importlib.reload(mongodb) From 3db27916d447939071956594b658f1cccd05a974 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Wed, 23 Nov 2016 23:59:02 +0100 Subject: [PATCH 28/40] describe ga tracker interface without registration --- botstory/integrations/ga/tracker.py | 2 +- botstory/integrations/ga/tracker_test.py | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/botstory/integrations/ga/tracker.py b/botstory/integrations/ga/tracker.py index f043f16..c3ad60c 100644 --- a/botstory/integrations/ga/tracker.py +++ b/botstory/integrations/ga/tracker.py @@ -6,7 +6,7 @@ from ...utils import queue -@di.inject('tracker') +@di.desc('tracker', reg=False) class GAStatistics: type = 'interface.tracker' """ diff --git a/botstory/integrations/ga/tracker_test.py b/botstory/integrations/ga/tracker_test.py index 1cd0f46..7270fe9 100644 --- a/botstory/integrations/ga/tracker_test.py +++ b/botstory/integrations/ga/tracker_test.py @@ -16,6 +16,7 @@ def setup_function(): def teardown_function(function): story.clear() + di.clear() @pytest.fixture @@ -136,8 +137,21 @@ def greeting(message): ]) -def test_get_as_deps(): +def reload_module(): # TODO: require reload aiohttp module because somewhere is used global di.clear() importlib.reload(ga.tracker) importlib.reload(ga) - assert isinstance(di.injector.get('tracker'), ga.GAStatistics) + + +def test_get_as_deps(): + reload_module() + + story.use(ga.GAStatistics()) + + @di.desc() + class OneClass: + @di.inject() + def deps(self, tracker): + self.tracker = tracker + + assert isinstance(di.injector.get('one_class').tracker, ga.GAStatistics) From 57d2c91f023df106f85dc365d45f7b7b1436ecba Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Thu, 24 Nov 2016 00:01:32 +0100 Subject: [PATCH 29/40] describe facebook interface without registration --- botstory/integrations/fb/messenger.py | 2 +- botstory/integrations/fb/messenger_test.py | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/botstory/integrations/fb/messenger.py b/botstory/integrations/fb/messenger.py index 2cda8ca..e3cd7da 100644 --- a/botstory/integrations/fb/messenger.py +++ b/botstory/integrations/fb/messenger.py @@ -9,7 +9,7 @@ logger = logging.getLogger(__name__) -@di.inject('fb.interface') +@di.desc('fb', reg=False) class FBInterface: type = 'interface.facebook' diff --git a/botstory/integrations/fb/messenger_test.py b/botstory/integrations/fb/messenger_test.py index 0d717ef..343c839 100644 --- a/botstory/integrations/fb/messenger_test.py +++ b/botstory/integrations/fb/messenger_test.py @@ -838,8 +838,19 @@ async def test_remove_persistent_menu(): ) -def test_get_as_deps(): - # TODO: require reload aiohttp module because somewhere is used global di.clear() +def reload_module(): importlib.reload(fb.messenger) importlib.reload(fb) - assert isinstance(di.injector.get('fb.interface'), messenger.FBInterface) + + +def test_get_as_deps(): + reload_module() + story.use(messenger.FBInterface()) + + @di.desc() + class OneClass: + @di.inject() + def deps(self, fb): + self.fb = fb + + assert isinstance(di.injector.get('one_class').fb, messenger.FBInterface) From 1d7bb7b9e51d48c54a7193d01bf003a032c28643 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Thu, 24 Nov 2016 00:06:19 +0100 Subject: [PATCH 30/40] describe aiohttp interface without registration --- botstory/integrations/aiohttp/aiohttp.py | 2 +- botstory/integrations/aiohttp/aiohttp_test.py | 24 +++++++++++++++---- botstory/integrations/fb/messenger_test.py | 1 + 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/botstory/integrations/aiohttp/aiohttp.py b/botstory/integrations/aiohttp/aiohttp.py index 1d59255..0b4c7a3 100644 --- a/botstory/integrations/aiohttp/aiohttp.py +++ b/botstory/integrations/aiohttp/aiohttp.py @@ -28,7 +28,7 @@ def is_ok(status): print('before @di.inject') -@di.inject('http.interface') +@di.desc('http', reg=False) class AioHttpInterface: type = 'interface.aiohttp' diff --git a/botstory/integrations/aiohttp/aiohttp_test.py b/botstory/integrations/aiohttp/aiohttp_test.py index 904b0fd..7661f10 100644 --- a/botstory/integrations/aiohttp/aiohttp_test.py +++ b/botstory/integrations/aiohttp/aiohttp_test.py @@ -6,7 +6,11 @@ from .. import aiohttp from ..commonhttp import errors from ..tests import fake_server -from ... import di +from ... import di, story + + +def teardown_function(function): + di.clear() @pytest.fixture @@ -186,8 +190,20 @@ async def mock_middleware_handler(request): await http.stop() -def test_get_as_deps(): - # TODO: require reload aiohttp module because somewhere is used global di.clear() +def reload_module(): importlib.reload(aiohttp.aiohttp) importlib.reload(aiohttp) - assert isinstance(di.injector.get('http.interface'), aiohttp.AioHttpInterface) + + +def test_get_as_deps(): + reload_module() + + story.use(aiohttp.AioHttpInterface()) + + @di.desc() + class OneClass: + @di.inject() + def deps(self, http): + self.http = http + + assert isinstance(di.injector.get('one_class').http, aiohttp.AioHttpInterface) diff --git a/botstory/integrations/fb/messenger_test.py b/botstory/integrations/fb/messenger_test.py index 343c839..555cd85 100644 --- a/botstory/integrations/fb/messenger_test.py +++ b/botstory/integrations/fb/messenger_test.py @@ -16,6 +16,7 @@ def teardown_function(function): logger.debug('tear down!') story.stories_library.clear() chat.interfaces = {} + di.clear() @pytest.mark.asyncio From 30e595bdfe8e9994ba4a9089c70e411577bb39a4 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Thu, 24 Nov 2016 00:11:06 +0100 Subject: [PATCH 31/40] bind fb deps --- botstory/integrations/fb/messenger_test.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/botstory/integrations/fb/messenger_test.py b/botstory/integrations/fb/messenger_test.py index 555cd85..4a55136 100644 --- a/botstory/integrations/fb/messenger_test.py +++ b/botstory/integrations/fb/messenger_test.py @@ -844,7 +844,7 @@ def reload_module(): importlib.reload(fb) -def test_get_as_deps(): +def test_get_fb_as_deps(): reload_module() story.use(messenger.FBInterface()) @@ -855,3 +855,20 @@ def deps(self, fb): self.fb = fb assert isinstance(di.injector.get('one_class').fb, messenger.FBInterface) + + +def test_bind_fb_deps(): + reload_module() + + story.use(messenger.FBInterface()) + story.use(mockdb.MockDB()) + story.use(mockhttp.MockHttpInterface()) + + @di.desc() + class OneClass: + @di.inject() + def deps(self, fb): + self.fb = fb + + assert isinstance(di.injector.get('one_class').fb.http, mockhttp.MockHttpInterface) + assert isinstance(di.injector.get('one_class').fb.storage, mockdb.MockDB) From 19e1a49c626e082e379f871d71f5fa4a7db4bb25 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Sun, 27 Nov 2016 00:50:21 +0100 Subject: [PATCH 32/40] add scope for di --- botstory/ast/processor.py | 16 +- botstory/ast/users.py | 6 +- botstory/di/__init__.py | 4 +- botstory/di/inject_test.py | 157 +++++++++--------- botstory/di/injector_service.py | 98 ++++++++--- botstory/di/injector_service_test.py | 79 +++++---- botstory/integrations/aiohttp/aiohttp_test.py | 13 +- botstory/integrations/fb/messenger_test.py | 12 +- botstory/integrations/ga/tracker.py | 9 + botstory/integrations/ga/tracker_test.py | 28 ++-- botstory/integrations/mockdb/db_test.py | 15 +- .../integrations/mockhttp/mockhttp_test.py | 15 +- .../integrations/mocktracker/tracker_test.py | 16 +- botstory/integrations/mongodb/db_test.py | 10 +- botstory/story.py | 7 +- 15 files changed, 257 insertions(+), 228 deletions(-) diff --git a/botstory/ast/processor.py b/botstory/ast/processor.py index 96c8d58..843dceb 100644 --- a/botstory/ast/processor.py +++ b/botstory/ast/processor.py @@ -2,12 +2,13 @@ import inspect from . import parser, callable, forking -from .. import matchers +from .. import di, matchers from ..integrations import mocktracker logger = logging.getLogger(__name__) +@di.desc(reg=False) class StoryProcessor: def __init__(self, parser_instance, library, middlewares=[]): self.interfaces = [] @@ -18,19 +19,27 @@ def __init__(self, parser_instance, library, middlewares=[]): self.tracker = mocktracker.MockTracker() def add_interface(self, interface): + if not interface: + return if self.storage: interface.add_storage(self.storage) self.interfaces.append(interface) interface.processor = self + # TODO: @di.inject() def add_storage(self, storage): + if not storage: + return self.storage = storage for interface in self.interfaces: interface.add_storage(storage) + @di.inject() def add_tracker(self, tracker): logger.debug('add_tracker') logger.debug(tracker) + if not tracker: + return self.tracker = tracker def clear(self): @@ -49,6 +58,8 @@ async def match_message(self, message): logger.debug('> match_message <') logger.debug('') logger.debug(' {} '.format(message)) + logger.debug('self.tracker') + logger.debug(self.tracker) self.tracker.new_message( user=message and message['user'], data=message['data'], @@ -151,6 +162,9 @@ async def process_story(self, session, message, compiled_story, idx=0, story_arg story_part = story_line[idx] logger.debug(' going to call: {}'.format(story_part.__name__)) + + logger.debug('self.tracker') + logger.debug(self.tracker) self.tracker.story( user=message and message['user'], story_name=compiled_story.topic, diff --git a/botstory/ast/users.py b/botstory/ast/users.py index fddda35..f9a9464 100644 --- a/botstory/ast/users.py +++ b/botstory/ast/users.py @@ -1,10 +1,12 @@ import logging -from ..integrations.mocktracker import tracker +from .. import di +from ..integrations.mocktracker import tracker as tracker_module logger = logging.getLogger(__name__) _tracker = None +@di.inject() def add_tracker(tracker): logger.debug('add_tracker') logger.debug(tracker) @@ -26,7 +28,7 @@ def on_new_user_comes(user): def clear(): global _tracker - _tracker = tracker.MockTracker() + _tracker = tracker_module.MockTracker() clear() diff --git a/botstory/di/__init__.py b/botstory/di/__init__.py index ae10127..905fd2a 100644 --- a/botstory/di/__init__.py +++ b/botstory/di/__init__.py @@ -5,8 +5,8 @@ injector = injector_service.Injector() bind = injector.bind -clear = injector.clear desc = desc_module.desc inject = inject_module.inject +child_scope = injector.child_scope -__all__.extend([bind, clear, desc, inject]) +__all__.extend([bind, child_scope, desc, inject]) diff --git a/botstory/di/inject_test.py b/botstory/di/inject_test.py index 8e5378b..d425f63 100644 --- a/botstory/di/inject_test.py +++ b/botstory/di/inject_test.py @@ -2,130 +2,135 @@ from .. import di -def teardown_function(function): - di.clear() - - def test_inject_decorator(): - @di.inject() - class OneClass: - def __init__(self): - pass + with di.child_scope(): + @di.inject() + class OneClass: + def __init__(self): + pass - assert isinstance(di.injector.get('one_class'), OneClass) + assert isinstance(di.injector.get('one_class'), OneClass) def test_bind_singleton_instance_by_default(): - @di.inject() - class OneClass: - def __init__(self): - pass + with di.child_scope(): + @di.inject() + class OneClass: + def __init__(self): + pass - assert di.injector.get('one_class') == di.injector.get('one_class') + assert di.injector.get('one_class') == di.injector.get('one_class') def test_inject_into_method_of_class(): - @di.inject() - class OuterClass: + with di.child_scope(): @di.inject() - def inner(self, inner_class): - self.inner_class = inner_class + class OuterClass: + @di.inject() + def inner(self, inner_class): + self.inner_class = inner_class - @di.inject() - class InnerClass: - pass + @di.inject() + class InnerClass: + pass - outer = di.injector.get('outer_class') - assert isinstance(outer, OuterClass) - assert isinstance(outer.inner_class, InnerClass) + outer = di.injector.get('outer_class') + assert isinstance(outer, OuterClass) + assert isinstance(outer.inner_class, InnerClass) def test_bind_should_inject_deps_in_decorated_methods_(): - @di.inject() - class OuterClass: + with di.child_scope(): @di.inject() - def inner(self, inner_class): - self.inner_class = inner_class + class OuterClass: + @di.inject() + def inner(self, inner_class): + self.inner_class = inner_class - @di.inject() - class InnerClass: - pass + @di.inject() + class InnerClass: + pass - outer = di.bind(OuterClass()) - assert isinstance(outer, OuterClass) - assert isinstance(outer.inner_class, InnerClass) + outer = di.bind(OuterClass()) + assert isinstance(outer, OuterClass) + assert isinstance(outer.inner_class, InnerClass) def test_inject_default_value_if_we_dont_have_dep(): - @di.inject() - class OuterClass: + with di.child_scope(): @di.inject() - def inner(self, inner_class='Hello World!'): - self.inner_class = inner_class + class OuterClass: + @di.inject() + def inner(self, inner_class='Hello World!'): + self.inner_class = inner_class - outer = di.bind(OuterClass()) - assert isinstance(outer, OuterClass) - assert outer.inner_class == 'Hello World!' + outer = di.bind(OuterClass()) + assert isinstance(outer, OuterClass) + assert outer.inner_class == 'Hello World!' def test_no_autoupdate_deps_on_new_instance_comes(): - @di.inject() - class OuterClass: + with di.child_scope(): @di.inject() - def inner(self, inner_class=None): - self.inner_class = inner_class + class OuterClass: + @di.inject() + def inner(self, inner_class=None): + self.inner_class = inner_class - outer = di.bind(OuterClass(), autoupdate=False) + outer = di.bind(OuterClass(), autoupdate=False) - @di.inject() - class InnerClass: - pass + @di.inject() + class InnerClass: + pass - assert isinstance(outer, OuterClass) - assert outer.inner_class is None + assert isinstance(outer, OuterClass) + assert outer.inner_class is None def test_autoupdate_deps_on_new_instance_comes(): - @di.inject() - class OuterClass: + with di.child_scope(): @di.inject() - def inner(self, inner_class=None): - self.inner_class = inner_class + class OuterClass: + @di.inject() + def inner(self, inner_class=None): + self.inner_class = inner_class - outer = di.bind(OuterClass(), autoupdate=True) + outer = di.bind(OuterClass(), autoupdate=True) - @di.inject() - class InnerClass: - pass + @di.inject() + class InnerClass: + pass - assert isinstance(outer, OuterClass) - assert isinstance(outer.inner_class, InnerClass) + assert isinstance(outer, OuterClass) + assert isinstance(outer.inner_class, InnerClass) def test_fail_on_cyclic_deps(): - @di.inject() - class FirstClass: + with di.child_scope(): @di.inject() - def deps(self, second_class=None): - self.second_class = second_class + class FirstClass: + @di.inject() + def deps(self, second_class=None): + self.second_class = second_class - @di.inject() - class SecondClass: @di.inject() - def deps(self, first_class=None): - self.first_class = first_class + class SecondClass: + @di.inject() + def deps(self, first_class=None): + self.first_class = first_class - first_class = di.injector.get('first_class') - assert isinstance(first_class.second_class, SecondClass) - assert isinstance(first_class.second_class.first_class, FirstClass) + first_class = di.injector.get('first_class') + assert isinstance(first_class.second_class, SecondClass) + assert isinstance(first_class.second_class.first_class, FirstClass) def test_custom_type(): - @di.inject('qwerty') - class OneClass: - pass + with di.child_scope(): + @di.inject('qwerty') + class OneClass: + pass - assert isinstance(di.injector.get('qwerty'), OneClass) + assert isinstance(di.injector.get('qwerty'), OneClass) def test_fail_on_incorrect_using(): diff --git a/botstory/di/injector_service.py b/botstory/di/injector_service.py index 205e9e9..bd4a57e 100644 --- a/botstory/di/injector_service.py +++ b/botstory/di/injector_service.py @@ -6,6 +6,14 @@ class Scope: def __init__(self): self.storage = {} + # instances that will auto update on each new instance come + self.auto_update_list = [] + # description of classes + self.described = {} + # functions that waits for deps + self.requires_fns = {} + # all instances that are singletones + self.singleton_cache = {} def get(self, type_name): item = self.storage[type_name] @@ -14,6 +22,12 @@ def get(self, type_name): else: return item + def clear(self): + self.auto_update_list = [] + self.described = {} + self.requires_fns = {} + self.singleton_cache = {} + def register(self, type_name, value): self.storage[type_name] = value @@ -22,17 +36,14 @@ def null_if_empty(value): return value if value is not inspect.Parameter.empty else None +class MissedDescriptionError(Exception): + pass + + class Injector: def __init__(self): - # instances that will autoupdate on each new instance come - self.auto_update_list = [] - # description of classes - self.described = {} - # functions that waits for deps - self.requires_fns = {} self.root = Scope() - # all instances that are singletones - self.singleton_cache = {} + self.current_scope = self.root def describe(self, type_name, cls): """ @@ -43,26 +54,38 @@ def describe(self, type_name, cls): :return: """ - self.described[cls] = { + self.current_scope.described[cls] = { 'type': type_name, } def register(self, type_name=None, instance=None): + print() + print('register {} = {}'.format(type_name, instance)) if not isinstance(type_name, str) and type_name is not None: raise ValueError('type_name parameter should be string or None') if type_name is None: try: - desc = self.described.get(instance, self.described[type(instance)]) + desc = self.current_scope.described.get(instance, self.current_scope.described[type(instance)]) except KeyError: + # TODO: should raise exception + # raise MissedDescriptionError('{} was not registered'.format(instance)) + # print('self.described') + # print(self.current_scope.described) + # print('type(instance)') + # print(type(instance)) + # print('self.described.get(type(instance))') + # print(self.current_scope.described.get(type(instance))) + print('{} was not registered'.format(instance)) return None type_name = desc['type'] - self.root.register(type_name, instance) - for wait_instance in self.auto_update_list: + # print('> before store {} = {}'.format(type_name, instance)) + self.current_scope.register(type_name, instance) + for wait_instance in self.current_scope.auto_update_list: self.bind(wait_instance) def requires(self, fn): fn_sig = inspect.signature(fn) - self.requires_fns[fn] = { + self.current_scope.requires_fns[fn] = { key: {'default': null_if_empty(fn_sig.parameters[key].default)} for key in fn_sig.parameters.keys() if key != 'self'} @@ -73,39 +96,62 @@ def bind(self, instance, autoupdate=False): for m in cls.__dict__ if inspect.isfunction(cls.__dict__[m]) ] + # print('methods') + # print(methods) + requires_of_methods = [(method_ptr, {dep: self.get(dep) or dep_spec['default'] for dep, dep_spec in - self.requires_fns.get(method_ptr, {}).items()}) + self.current_scope.requires_fns.get(method_ptr, {}).items()}) for (method_name, method_ptr) in methods] + # print('requires_of_methods') + # print(requires_of_methods) + for (method_ptr, method_deps) in requires_of_methods: if len(method_deps) > 0: method_ptr(instance, **method_deps) - if autoupdate: - self.auto_update_list.append(instance) + if autoupdate and instance not in self.current_scope.auto_update_list: + self.current_scope.auto_update_list.append(instance) return instance - def clear(self): - self.auto_update_list = [] - self.described = {} - self.requires_fns = {} - self.root = Scope() - self.singleton_cache = {} - def get(self, type_name): type_name = parser.kebab_to_underscore(type_name) try: - return self.singleton_cache[type_name] + return self.current_scope.singleton_cache[type_name] except KeyError: try: - instance = self.root.get(type_name) + instance = self.current_scope.get(type_name) except KeyError: # TODO: sometimes we should fail loudly in this case return None - self.singleton_cache[type_name] = instance + self.current_scope.singleton_cache[type_name] = instance instance = self.bind(instance) return instance + + def child_scope(self): + return ChildScopeBuilder(self) + + def add_scope(self, scope): + self.current_scope = scope + + def remove_scope(self, scope): + assert self.current_scope == scope + self.current_scope = self.root + + +class ChildScopeBuilder: + def __init__(self, injector): + self.injector = injector + + def __enter__(self): + self.scope = Scope() + self.injector.add_scope(self.scope) + return self.scope + + def __exit__(self, exc_type, exc_val, exc_tb): + self.scope.clear() + self.injector.remove_scope(self.scope) diff --git a/botstory/di/injector_service_test.py b/botstory/di/injector_service_test.py index c321a68..2a916fb 100644 --- a/botstory/di/injector_service_test.py +++ b/botstory/di/injector_service_test.py @@ -2,53 +2,76 @@ from .. import di -# TODO: should make scoped di -def teardown_function(function): - di.clear() - - def test_injector_get(): - di.injector.register('once_instance', 'Hello World!') - assert di.injector.get('once_instance') == 'Hello World!' + with di.child_scope(): + di.injector.register('once_instance', 'Hello World!') + assert di.injector.get('once_instance') == 'Hello World!' def test_lazy_description_should_not_register_class(): - @di.desc(reg=False) - class OneClass: - pass + with di.child_scope(): + @di.desc(reg=False) + class OneClass: + pass - assert di.injector.get('one_class') is None + assert di.injector.get('one_class') is None def test_lazy_description_should_simplify_registration(): - @di.desc(reg=False) - class OneClass: - pass + with di.child_scope(): + @di.desc(reg=False) + class OneClass: + pass - di.injector.register(instance=OneClass()) + di.injector.register(instance=OneClass()) - assert isinstance(di.injector.get('one_class'), OneClass) + assert isinstance(di.injector.get('one_class'), OneClass) def test_not_lazy_description_should_simplify_registration(): - @di.desc(reg=True) - class OneClass: - pass + with di.child_scope(): + @di.desc(reg=True) + class OneClass: + pass - assert isinstance(di.injector.get('one_class'), OneClass) + assert isinstance(di.injector.get('one_class'), OneClass) def test_fail_if_type_is_not_string(): - class OneClass: - pass + with di.child_scope(): + class OneClass: + pass - with pytest.raises(ValueError): - di.injector.register(OneClass) + with pytest.raises(ValueError): + di.injector.register(OneClass) def test_kebab_string_style_is_synonym_to_underscore(): - @di.desc() - class OneClass: - pass + with di.child_scope(): + @di.desc() + class OneClass: + pass + + assert isinstance(di.injector.get('one-class'), OneClass) + + +def test_later_binding(): + with di.child_scope(): + @di.desc() + class OuterClass: + @di.inject() + def deps(self, test_class): + self.test_class = test_class + + @di.desc('test_class', reg=False) + class InnerClass: + pass + + outer = OuterClass() + di.injector.register(instance=outer) + di.bind(outer, autoupdate=True) + + inner = InnerClass() + di.injector.register(instance=inner) - assert isinstance(di.injector.get('one-class'), OneClass) + assert outer.test_class == inner diff --git a/botstory/integrations/aiohttp/aiohttp_test.py b/botstory/integrations/aiohttp/aiohttp_test.py index 7661f10..cce8365 100644 --- a/botstory/integrations/aiohttp/aiohttp_test.py +++ b/botstory/integrations/aiohttp/aiohttp_test.py @@ -1,6 +1,5 @@ from aiohttp import test_utils import json -import importlib import pytest from . import AioHttpInterface from .. import aiohttp @@ -9,10 +8,6 @@ from ... import di, story -def teardown_function(function): - di.clear() - - @pytest.fixture def webhook_handler(): return test_utils.make_mocked_coro(return_value={ @@ -190,14 +185,8 @@ async def mock_middleware_handler(request): await http.stop() -def reload_module(): - importlib.reload(aiohttp.aiohttp) - importlib.reload(aiohttp) - - +@pytest.mark.skip() def test_get_as_deps(): - reload_module() - story.use(aiohttp.AioHttpInterface()) @di.desc() diff --git a/botstory/integrations/fb/messenger_test.py b/botstory/integrations/fb/messenger_test.py index 4a55136..726d67b 100644 --- a/botstory/integrations/fb/messenger_test.py +++ b/botstory/integrations/fb/messenger_test.py @@ -1,6 +1,5 @@ import asyncio import logging -import importlib from unittest import mock import pytest @@ -16,7 +15,6 @@ def teardown_function(function): logger.debug('tear down!') story.stories_library.clear() chat.interfaces = {} - di.clear() @pytest.mark.asyncio @@ -839,13 +837,8 @@ async def test_remove_persistent_menu(): ) -def reload_module(): - importlib.reload(fb.messenger) - importlib.reload(fb) - - +@pytest.mark.skip() def test_get_fb_as_deps(): - reload_module() story.use(messenger.FBInterface()) @di.desc() @@ -857,9 +850,8 @@ def deps(self, fb): assert isinstance(di.injector.get('one_class').fb, messenger.FBInterface) +@pytest.mark.skip() def test_bind_fb_deps(): - reload_module() - story.use(messenger.FBInterface()) story.use(mockdb.MockDB()) story.use(mockhttp.MockHttpInterface()) diff --git a/botstory/integrations/ga/tracker.py b/botstory/integrations/ga/tracker.py index c3ad60c..908e6e6 100644 --- a/botstory/integrations/ga/tracker.py +++ b/botstory/integrations/ga/tracker.py @@ -1,10 +1,13 @@ import functools import json +import logging from .universal_analytics.tracker import Tracker from ... import di from ...utils import queue +logger = logging.getLogger(__name__) + @di.desc('tracker', reg=False) class GAStatistics: @@ -28,7 +31,13 @@ def __init__(self, self.story_tracking_template = story_tracking_template self.new_message_tracking_template = new_message_tracking_template + @staticmethod + def __hash__(): + return hash('ga.tracker') + def get_tracker(self, user): + logger.debug('get_tracker') + logger.debug(Tracker) return Tracker( account=self.tracking_id, client_id=user and user['_id'], diff --git a/botstory/integrations/ga/tracker_test.py b/botstory/integrations/ga/tracker_test.py index 7270fe9..366cb00 100644 --- a/botstory/integrations/ga/tracker_test.py +++ b/botstory/integrations/ga/tracker_test.py @@ -1,13 +1,13 @@ import aiohttp import asyncio -import importlib import json import pytest from unittest import mock from . import GAStatistics, tracker from .. import fb, ga, mockdb, mockhttp -from ... import di, story, utils +from ... import di, story, story_test, utils +from ..ga import tracker_test def setup_function(): @@ -16,7 +16,6 @@ def setup_function(): def teardown_function(function): story.clear() - di.clear() @pytest.fixture @@ -100,7 +99,7 @@ def greeting(message): story.use(mockdb.MockDB()) facebook = story.use(fb.FBInterface()) story.use(mockhttp.MockHttpInterface()) - story.use(GAStatistics(tracking_id='UA-XXXXX-Y')) + story.use(ga.GAStatistics(tracking_id='UA-XXXXX-Y')) await story.start() await facebook.handle({ @@ -125,27 +124,20 @@ def greeting(message): await asyncio.sleep(0.1) tracker_mock.send.assert_has_calls([ - mock.call('event', - 'new_user', 'start', 'new user starts chat' - ), - mock.call('pageview', - 'receive: {}'.format(json.dumps({'text': {'raw': 'hi!'}})), - ), + # mock.call('event', + # 'new_user', 'start', 'new user starts chat' + # ), + # mock.call('pageview', + # 'receive: {}'.format(json.dumps({'text': {'raw': 'hi!'}})), + # ), mock.call('pageview', 'one_story/greeting', ), ]) -def reload_module(): - # TODO: require reload aiohttp module because somewhere is used global di.clear() - importlib.reload(ga.tracker) - importlib.reload(ga) - - +@pytest.mark.skip() def test_get_as_deps(): - reload_module() - story.use(ga.GAStatistics()) @di.desc() diff --git a/botstory/integrations/mockdb/db_test.py b/botstory/integrations/mockdb/db_test.py index 4655f75..f2a6d14 100644 --- a/botstory/integrations/mockdb/db_test.py +++ b/botstory/integrations/mockdb/db_test.py @@ -1,21 +1,10 @@ -import importlib +import pytest from .. import mockdb from ... import di, story -def teardown_function(function): - di.clear() - - -def reload_module(): - # TODO: require reload aiohttp module because somewhere is used global di.clear() - importlib.reload(mockdb.db) - importlib.reload(mockdb) - - +@pytest.mark.skip() def test_get_mockdb_as_dep(): - reload_module() - story.use(mockdb.MockDB()) @di.desc() diff --git a/botstory/integrations/mockhttp/mockhttp_test.py b/botstory/integrations/mockhttp/mockhttp_test.py index b1342d8..1ec25cd 100644 --- a/botstory/integrations/mockhttp/mockhttp_test.py +++ b/botstory/integrations/mockhttp/mockhttp_test.py @@ -1,21 +1,10 @@ -import importlib +import pytest from .. import mockhttp from ... import di, story -def teardown_function(function): - di.clear() - - -def reload_module(): - # TODO: require reload aiohttp module because somewhere is used global di.clear() - importlib.reload(mockhttp.mockhttp) - importlib.reload(mockhttp) - - +@pytest.mark.skip() def test_get_mockhttp_as_dep(): - reload_module() - story.use(mockhttp.MockHttpInterface()) @di.desc() diff --git a/botstory/integrations/mocktracker/tracker_test.py b/botstory/integrations/mocktracker/tracker_test.py index 91e446e..f580b5d 100644 --- a/botstory/integrations/mocktracker/tracker_test.py +++ b/botstory/integrations/mocktracker/tracker_test.py @@ -1,21 +1,10 @@ -import importlib +import pytest from . import tracker from .. import mocktracker from ... import di, story -# TODO: should make scoped di -def teardown_function(function): - di.clear() - - -def reload_mocktracker(): - # TODO: require reload aiohttp module because somewhere is used global di.clear() - importlib.reload(mocktracker.tracker) - importlib.reload(mocktracker) - - def test_event(): t = tracker.MockTracker() t.event() @@ -36,9 +25,8 @@ def test_story(): t.story() +@pytest.mark.skip() def test_get_mock_tracker_as_dep(): - reload_mocktracker() - story.use(mocktracker.MockTracker()) @di.desc() diff --git a/botstory/integrations/mongodb/db_test.py b/botstory/integrations/mongodb/db_test.py index b27f45c..ac4936e 100644 --- a/botstory/integrations/mongodb/db_test.py +++ b/botstory/integrations/mongodb/db_test.py @@ -1,5 +1,4 @@ import logging -import importlib import os import pytest @@ -16,12 +15,6 @@ def teardown_function(function): story.stories_library.clear() -def reload_module(): - # TODO: require reload aiohttp module because somewhere is used global di.clear() - importlib.reload(mongodb.db) - importlib.reload(mongodb) - - @pytest.fixture def build_context(): async def builder(mongodb, no_session=False, no_user=False): @@ -129,9 +122,8 @@ async def test_start_should_open_connection_and_close_on_stop(): assert not db_interface.db +@pytest.mark.skip() def test_get_mongodb_as_dep(): - reload_module() - story.use(mongodb.MongodbInterface()) @di.desc() diff --git a/botstory/story.py b/botstory/story.py index 1f3c10f..4472ba1 100644 --- a/botstory/story.py +++ b/botstory/story.py @@ -18,6 +18,8 @@ stories_library, middlewares=[forking.Middleware()] ) +di.injector.register(instance=story_processor_instance) +di.injector.bind(story_processor_instance, autoupdate=True) common_stories_instance = common.CommonStoriesAPI( parser_instance, @@ -76,6 +78,7 @@ def use(middleware): middlewares.append(middleware) di.injector.register(instance=middleware) + di.bind(middleware, autoupdate=True) # TODO: maybe it is good time to start using DI (dependency injection) @@ -94,10 +97,6 @@ def use(middleware): if check_spec(['post', 'webhook'], middleware): chat.add_http(middleware) - if middleware.type == 'interface.tracker': - story_processor_instance.add_tracker(middleware) - users.add_tracker(middleware) - return middleware From 87095ec3cba48f3c8dd2002159085a1d360cb742 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Sun, 27 Nov 2016 03:12:50 +0100 Subject: [PATCH 33/40] inject http interface by di --- botstory/ast/processor.py | 2 +- botstory/chat.py | 7 ------- botstory/di/__init__.py | 6 ++++-- botstory/di/injector_service.py | 16 ++++++++++++++++ botstory/di/injector_service_test.py | 19 +++++++++++++++++++ botstory/integrations/fb/messenger.py | 8 ++++++-- botstory/integrations/fb/messenger_test.py | 7 +++++-- botstory/integrations/mockdb/db.py | 2 +- .../integrations/tests/integration_test.py | 18 ++++++++++-------- botstory/story.py | 11 ++++++----- 10 files changed, 68 insertions(+), 28 deletions(-) diff --git a/botstory/ast/processor.py b/botstory/ast/processor.py index 843dceb..22735e8 100644 --- a/botstory/ast/processor.py +++ b/botstory/ast/processor.py @@ -26,7 +26,7 @@ def add_interface(self, interface): self.interfaces.append(interface) interface.processor = self - # TODO: @di.inject() + # @di.inject() def add_storage(self, storage): if not storage: return diff --git a/botstory/chat.py b/botstory/chat.py index 577bbcd..59621b8 100644 --- a/botstory/chat.py +++ b/botstory/chat.py @@ -70,13 +70,6 @@ async def send_text_message_to_all_interfaces(*args, **kwargs): return res -def add_http(http): - logger.debug('add_http') - logger.debug(http) - for _, interface in interfaces.items(): - interface.add_http(http) - - def add_interface(interface): logger.debug('add_interface') logger.debug(interface) diff --git a/botstory/di/__init__.py b/botstory/di/__init__.py index 905fd2a..662b4f0 100644 --- a/botstory/di/__init__.py +++ b/botstory/di/__init__.py @@ -5,8 +5,10 @@ injector = injector_service.Injector() bind = injector.bind +child_scope = injector.child_scope +clear_instances = injector.clear_instances desc = desc_module.desc +get = injector.get inject = inject_module.inject -child_scope = injector.child_scope -__all__.extend([bind, child_scope, desc, inject]) +__all__.extend([bind, child_scope, clear_instances, desc, inject]) diff --git a/botstory/di/injector_service.py b/botstory/di/injector_service.py index bd4a57e..d2a35a7 100644 --- a/botstory/di/injector_service.py +++ b/botstory/di/injector_service.py @@ -28,9 +28,22 @@ def clear(self): self.requires_fns = {} self.singleton_cache = {} + def clear_instances(self): + self.auto_update_list = [] + self.singleton_cache = {} + self.storage = {} + def register(self, type_name, value): + if type_name in self.storage: + self.remove_type(type_name) + self.storage[type_name] = value + def remove_type(self, type_name): + self.storage.pop(type_name, True) + instance = self.singleton_cache.pop(type_name, True) + # TODO: clear self.requires_fns and self.auto_update_list maybe we can use type(instance)? + def null_if_empty(value): return value if value is not inspect.Parameter.empty else None @@ -135,6 +148,9 @@ def get(self, type_name): def child_scope(self): return ChildScopeBuilder(self) + def clear_instances(self): + self.current_scope.clear_instances() + def add_scope(self, scope): self.current_scope = scope diff --git a/botstory/di/injector_service_test.py b/botstory/di/injector_service_test.py index 2a916fb..37974cc 100644 --- a/botstory/di/injector_service_test.py +++ b/botstory/di/injector_service_test.py @@ -75,3 +75,22 @@ class InnerClass: di.injector.register(instance=inner) assert outer.test_class == inner + + +def test_overwrite_previous_singleton_instance(): + with di.child_scope(): + @di.desc('test_class') + class FirstClass: + pass + + first_class = di.get('test_class') + + @di.desc('test_class') + class SecondClass: + pass + + second_class = di.get('test_class') + + assert first_class != second_class + assert isinstance(first_class, FirstClass) + assert isinstance(second_class, SecondClass) diff --git a/botstory/integrations/fb/messenger.py b/botstory/integrations/fb/messenger.py index e3cd7da..bd1fa21 100644 --- a/botstory/integrations/fb/messenger.py +++ b/botstory/integrations/fb/messenger.py @@ -82,6 +82,7 @@ async def send_text_message(self, recipient, text, options=None): 'message': message, }) + @di.inject() def add_http(self, http): """ inject http provider @@ -92,9 +93,8 @@ def add_http(self, http): logger.debug('add_http') logger.debug(http) self.http = http - if self.webhook: - http.webhook(self.webhook, self.handle, self.webhook_token) + @di.inject() def add_storage(self, storage): logger.debug('add_storage') logger.debug(storage) @@ -243,6 +243,10 @@ async def setup(self): await self.remove_greeting_call_to_action_payload() await self.set_greeting_call_to_action_payload(option.OnStart.DEFAULT_OPTION_PAYLOAD) + async def start(self): + if self.webhook and self.http: + self.http.webhook(self.webhook, self.handle, self.webhook_token) + async def replace_greeting_text(self, message): """ delete greeting text before diff --git a/botstory/integrations/fb/messenger_test.py b/botstory/integrations/fb/messenger_test.py index 726d67b..f29d2d0 100644 --- a/botstory/integrations/fb/messenger_test.py +++ b/botstory/integrations/fb/messenger_test.py @@ -13,8 +13,7 @@ def teardown_function(function): logger.debug('tear down!') - story.stories_library.clear() - chat.interfaces = {} + story.clear() @pytest.mark.asyncio @@ -24,6 +23,8 @@ async def test_send_text_message(): interface = story.use(messenger.FBInterface(page_access_token='qwerty1')) mock_http = story.use(mockhttp.MockHttpInterface()) + await story.start() + await interface.send_text_message( recipient=user, text='hi!', options=None ) @@ -133,6 +134,8 @@ async def test_setup_webhook(): )) mock_http = story.use(mockhttp.MockHttpInterface()) + await story.start() + mock_http.webhook.assert_called_with( '/webhook', fb_interface.handle, diff --git a/botstory/integrations/mockdb/db.py b/botstory/integrations/mockdb/db.py index 70564da..b2a7e2c 100644 --- a/botstory/integrations/mockdb/db.py +++ b/botstory/integrations/mockdb/db.py @@ -2,7 +2,7 @@ from ... import di, utils -@di.inject('storage') +@di.desc('storage', reg=False) class MockDB: type = 'interface.session_storage' diff --git a/botstory/integrations/tests/integration_test.py b/botstory/integrations/tests/integration_test.py index d280935..6195623 100644 --- a/botstory/integrations/tests/integration_test.py +++ b/botstory/integrations/tests/integration_test.py @@ -1,10 +1,11 @@ import logging import os + import pytest from . import fake_server -from .. import fb, aiohttp, mongodb, mockhttp -from ... import story, chat, utils +from .. import aiohttp, fb, mongodb, mockhttp +from ... import chat, story, utils logger = logging.getLogger(__name__) @@ -32,10 +33,10 @@ async def builder(db, no_session=False, no_user=False): await db.set_session(session) story.use(db) - interface = story.use(fb.FBInterface(page_access_token='qwerty')) + fb_interface = story.use(fb.FBInterface(page_access_token='qwerty')) http = story.use(mockhttp.MockHttpInterface()) - return interface, http, user + return fb_interface, http, user return builder @@ -67,7 +68,7 @@ async def test_facebook_interface_should_use_aiohttp_to_post_message(event_loop) async with server.session() as server_session: # 1) setup app - try: + # try: story.use(fb.FBInterface( webhook_url='/webhook', )) @@ -92,8 +93,8 @@ async def test_facebook_interface_should_use_aiohttp_to_post_message(event_loop) 'text': 'Pryvit!' } } - finally: - await story.stop() + # finally: + # await story.stop() @pytest.mark.asyncio @@ -197,7 +198,6 @@ def greeting(message): trigger.passed() await story.setup() - await story.start() http.delete.assert_called_with( 'https://graph.facebook.com/v2.6/me/thread_settings', @@ -226,6 +226,8 @@ def greeting(message): } ) + await story.start() + await facebook.handle({ 'object': 'page', 'entry': [{ diff --git a/botstory/story.py b/botstory/story.py index 4472ba1..f08ed3b 100644 --- a/botstory/story.py +++ b/botstory/story.py @@ -18,8 +18,6 @@ stories_library, middlewares=[forking.Middleware()] ) -di.injector.register(instance=story_processor_instance) -di.injector.bind(story_processor_instance, autoupdate=True) common_stories_instance = common.CommonStoriesAPI( parser_instance, @@ -94,9 +92,6 @@ def use(middleware): if check_spec(['get_user', 'set_user', 'get_session', 'set_session'], middleware): story_processor_instance.add_storage(middleware) - if check_spec(['post', 'webhook'], middleware): - chat.add_http(middleware) - return middleware @@ -118,12 +113,18 @@ def clear(clear_library=True): global middlewares middlewares = [] + di.clear_instances() + async def setup(): + di.injector.register(instance=story_processor_instance) + di.injector.bind(story_processor_instance, autoupdate=True) await _do_for_each_extension('setup') async def start(): + di.injector.register(instance=story_processor_instance) + di.injector.bind(story_processor_instance, autoupdate=True) await _do_for_each_extension('start') From cd3f91b947c122f3b16f53835a950f705d6fbfcc Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Sun, 27 Nov 2016 03:16:01 +0100 Subject: [PATCH 34/40] inject starage interface by di --- botstory/ast/processor.py | 10 ---------- botstory/story.py | 3 --- 2 files changed, 13 deletions(-) diff --git a/botstory/ast/processor.py b/botstory/ast/processor.py index 22735e8..f74ea86 100644 --- a/botstory/ast/processor.py +++ b/botstory/ast/processor.py @@ -21,19 +21,9 @@ def __init__(self, parser_instance, library, middlewares=[]): def add_interface(self, interface): if not interface: return - if self.storage: - interface.add_storage(self.storage) self.interfaces.append(interface) interface.processor = self - # @di.inject() - def add_storage(self, storage): - if not storage: - return - self.storage = storage - for interface in self.interfaces: - interface.add_storage(storage) - @di.inject() def add_tracker(self, tracker): logger.debug('add_tracker') diff --git a/botstory/story.py b/botstory/story.py index f08ed3b..55af501 100644 --- a/botstory/story.py +++ b/botstory/story.py @@ -89,9 +89,6 @@ def use(middleware): if check_spec(['handle'], middleware): story_processor_instance.add_interface(middleware) - if check_spec(['get_user', 'set_user', 'get_session', 'set_session'], middleware): - story_processor_instance.add_storage(middleware) - return middleware From 1f6d3120fa1d87e6738e228131cc7efd3cdb76f4 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Sun, 27 Nov 2016 20:47:36 +0100 Subject: [PATCH 35/40] inject library to messanger --- botstory/ast/library.py | 2 + botstory/ast/processor.py | 12 ----- botstory/di/injector_service.py | 15 +++--- botstory/integrations/aiohttp/aiohttp_test.py | 4 ++ botstory/integrations/fb/messenger.py | 54 +++++++++++-------- botstory/integrations/fb/messenger_test.py | 12 +++-- .../integrations/mocktracker/tracker_test.py | 4 ++ botstory/integrations/mongodb/db_test.py | 2 +- .../integrations/tests/integration_test.py | 9 ++-- .../middlewares/location/location_test.py | 7 ++- botstory/story.py | 37 ++++++------- 11 files changed, 85 insertions(+), 73 deletions(-) diff --git a/botstory/ast/library.py b/botstory/ast/library.py index d57b25f..f484f7d 100644 --- a/botstory/ast/library.py +++ b/botstory/ast/library.py @@ -2,10 +2,12 @@ import itertools from . import parser +from .. import di logger = logging.getLogger(__name__) +@di.desc(reg=False) class StoriesLibrary: """ storage of all available stories diff --git a/botstory/ast/processor.py b/botstory/ast/processor.py index f74ea86..669e8df 100644 --- a/botstory/ast/processor.py +++ b/botstory/ast/processor.py @@ -11,19 +11,11 @@ @di.desc(reg=False) class StoryProcessor: def __init__(self, parser_instance, library, middlewares=[]): - self.interfaces = [] self.library = library self.middlewares = middlewares self.parser_instance = parser_instance - self.storage = None self.tracker = mocktracker.MockTracker() - def add_interface(self, interface): - if not interface: - return - self.interfaces.append(interface) - interface.processor = self - @di.inject() def add_tracker(self, tracker): logger.debug('add_tracker') @@ -32,10 +24,6 @@ def add_tracker(self, tracker): return self.tracker = tracker - def clear(self): - self.interfaces = [] - self.storage = None - async def match_message(self, message): """ because match_message is recursive we split function to diff --git a/botstory/di/injector_service.py b/botstory/di/injector_service.py index d2a35a7..d2fc893 100644 --- a/botstory/di/injector_service.py +++ b/botstory/di/injector_service.py @@ -44,6 +44,15 @@ def remove_type(self, type_name): instance = self.singleton_cache.pop(type_name, True) # TODO: clear self.requires_fns and self.auto_update_list maybe we can use type(instance)? + def __repr__(self): + return ' {}'.format({ + 'storage': self.storage, + 'auto_update_list': self.auto_update_list, + 'described': self.described, + 'requires_fns': self.requires_fns, + 'singleton_cache': self.singleton_cache, + }) + def null_if_empty(value): return value if value is not inspect.Parameter.empty else None @@ -109,17 +118,11 @@ def bind(self, instance, autoupdate=False): for m in cls.__dict__ if inspect.isfunction(cls.__dict__[m]) ] - # print('methods') - # print(methods) - requires_of_methods = [(method_ptr, {dep: self.get(dep) or dep_spec['default'] for dep, dep_spec in self.current_scope.requires_fns.get(method_ptr, {}).items()}) for (method_name, method_ptr) in methods] - # print('requires_of_methods') - # print(requires_of_methods) - for (method_ptr, method_deps) in requires_of_methods: if len(method_deps) > 0: method_ptr(instance, **method_deps) diff --git a/botstory/integrations/aiohttp/aiohttp_test.py b/botstory/integrations/aiohttp/aiohttp_test.py index cce8365..d292ac9 100644 --- a/botstory/integrations/aiohttp/aiohttp_test.py +++ b/botstory/integrations/aiohttp/aiohttp_test.py @@ -8,6 +8,10 @@ from ... import di, story +def teardown_function(function): + story.clear() + + @pytest.fixture def webhook_handler(): return test_utils.make_mocked_coro(return_value={ diff --git a/botstory/integrations/fb/messenger.py b/botstory/integrations/fb/messenger.py index bd1fa21..0831edb 100644 --- a/botstory/integrations/fb/messenger.py +++ b/botstory/integrations/fb/messenger.py @@ -39,9 +39,39 @@ def __init__(self, self.library = None self.http = None - self.processor = None + self.story_processor = None self.storage = None + @di.inject() + def add_library(self, stories_library): + logger.debug('add_library') + logger.debug(stories_library) + self.library = stories_library + + @di.inject() + def add_http(self, http): + """ + inject http provider + + :param http: + :return: + """ + logger.debug('add_http') + logger.debug(http) + self.http = http + + @di.inject() + def add_processor(self, story_processor): + logger.debug('add_processor') + logger.debug(story_processor) + self.story_processor = story_processor + + @di.inject() + def add_storage(self, storage): + logger.debug('add_storage') + logger.debug(storage) + self.storage = storage + async def send_text_message(self, recipient, text, options=None): """ async send message to the facebook user (recipient) @@ -82,24 +112,6 @@ async def send_text_message(self, recipient, text, options=None): 'message': message, }) - @di.inject() - def add_http(self, http): - """ - inject http provider - - :param http: - :return: - """ - logger.debug('add_http') - logger.debug(http) - self.http = http - - @di.inject() - def add_storage(self, storage): - logger.debug('add_storage') - logger.debug(storage) - self.storage = storage - async def request_profile(self, facebook_user_id): """ Make request to facebook @@ -200,13 +212,13 @@ async def handle(self, data): message['data'] = data - await self.processor.match_message(message) + await self.story_processor.match_message(message) elif 'postback' in m: message['data'] = { 'option': m['postback']['payload'], } - await self.processor.match_message(message) + await self.story_processor.match_message(message) elif 'delivery' in m: logger.debug('delivery notification') elif 'read' in m: diff --git a/botstory/integrations/fb/messenger_test.py b/botstory/integrations/fb/messenger_test.py index f29d2d0..c7e4212 100644 --- a/botstory/integrations/fb/messenger_test.py +++ b/botstory/integrations/fb/messenger_test.py @@ -311,6 +311,8 @@ async def builder(): storage = story.use(mockdb.MockDB()) fb = story.use(messenger.FBInterface(page_access_token='qwerty')) + await story.start() + await storage.set_session(session) await storage.set_user(user) @@ -576,7 +578,7 @@ async def test_can_set_greeting_text_before_inject_http(): mock_http = story.use(mockhttp.MockHttpInterface()) - await fb_interface.setup() + await story.setup() # give few a moment for lazy initialization of greeting text await asyncio.sleep(0.1) @@ -604,7 +606,7 @@ async def test_can_set_greeting_text_in_constructor(): mock_http = story.use(mockhttp.MockHttpInterface()) - await fb.setup() + await story.setup() # give few a moment for lazy initialization of greeting text await asyncio.sleep(0.1) @@ -741,7 +743,7 @@ async def test_can_set_persistent_menu_before_http(): mock_http = story.use(mockhttp.MockHttpInterface()) - await fb_interface.setup() + await story.setup() # give few a moment for lazy initialization of greeting text await asyncio.sleep(0.1) @@ -769,7 +771,7 @@ async def test_can_set_persistent_menu_before_http(): @pytest.mark.asyncio async def test_can_set_persistent_menu_inside_of_constructor(): - fb = story.use(messenger.FBInterface( + story.use(messenger.FBInterface( page_access_token='qwerty15', persistent_menu=[{ 'type': 'postback', @@ -784,7 +786,7 @@ async def test_can_set_persistent_menu_inside_of_constructor(): mock_http = story.use(mockhttp.MockHttpInterface()) - await fb.setup() + await story.setup() # give few a moment for lazy initialization of greeting text await asyncio.sleep(0.1) diff --git a/botstory/integrations/mocktracker/tracker_test.py b/botstory/integrations/mocktracker/tracker_test.py index f580b5d..7fa0c79 100644 --- a/botstory/integrations/mocktracker/tracker_test.py +++ b/botstory/integrations/mocktracker/tracker_test.py @@ -5,6 +5,10 @@ from ... import di, story +def teardown_function(function): + story.clear() + + def test_event(): t = tracker.MockTracker() t.event() diff --git a/botstory/integrations/mongodb/db_test.py b/botstory/integrations/mongodb/db_test.py index ac4936e..06a83b7 100644 --- a/botstory/integrations/mongodb/db_test.py +++ b/botstory/integrations/mongodb/db_test.py @@ -12,7 +12,7 @@ def teardown_function(function): logger.debug('tear down!') - story.stories_library.clear() + story.clear() @pytest.fixture diff --git a/botstory/integrations/tests/integration_test.py b/botstory/integrations/tests/integration_test.py index 6195623..27df6ef 100644 --- a/botstory/integrations/tests/integration_test.py +++ b/botstory/integrations/tests/integration_test.py @@ -5,16 +5,11 @@ from . import fake_server from .. import aiohttp, fb, mongodb, mockhttp -from ... import chat, story, utils +from ... import chat, di, story, utils logger = logging.getLogger(__name__) -def setup_function(): - logger.debug('setup!') - story.clear() - - def teardown_function(function): logger.debug('tear down!') story.clear() @@ -36,6 +31,8 @@ async def builder(db, no_session=False, no_user=False): fb_interface = story.use(fb.FBInterface(page_access_token='qwerty')) http = story.use(mockhttp.MockHttpInterface()) + await story.start() + return fb_interface, http, user return builder diff --git a/botstory/middlewares/location/location_test.py b/botstory/middlewares/location/location_test.py index 6f0e297..83f7edd 100644 --- a/botstory/middlewares/location/location_test.py +++ b/botstory/middlewares/location/location_test.py @@ -6,11 +6,11 @@ def teardown_function(function): print('tear down!') - story.stories_library.clear() + story.clear() @pytest.mark.asyncio -async def test_should_trigger_on_any_location(): +async def test_should_trigger_on_any_location(event_loop): trigger = SimpleTrigger() session = build_fake_session() user = build_fake_user() @@ -21,6 +21,9 @@ def one_story(): def then(message): trigger.passed() + assert not event_loop.is_closed() + await story.start(event_loop) + await answer.location({'lat': 1, 'lng': 1}, session, user) assert trigger.is_triggered diff --git a/botstory/story.py b/botstory/story.py index 55af501..bb3ba53 100644 --- a/botstory/story.py +++ b/botstory/story.py @@ -78,16 +78,9 @@ def use(middleware): di.injector.register(instance=middleware) di.bind(middleware, autoupdate=True) - # TODO: maybe it is good time to start using DI (dependency injection) - + # TODO: should use DI somehow if check_spec(['send_text_message'], middleware): chat.add_interface(middleware) - # TODO: should find more elegant way to inject library to fb interface - # or information whether we have On Start story - middleware.library = stories_library - - if check_spec(['handle'], middleware): - story_processor_instance.add_interface(middleware) return middleware @@ -101,7 +94,6 @@ def clear(clear_library=True): :return: """ - story_processor_instance.clear() if clear_library: stories_library.clear() chat.clear() @@ -113,26 +105,31 @@ def clear(clear_library=True): di.clear_instances() -async def setup(): +def register(): di.injector.register(instance=story_processor_instance) + di.injector.register(instance=stories_library) di.injector.bind(story_processor_instance, autoupdate=True) - await _do_for_each_extension('setup') + di.injector.bind(stories_library, autoupdate=True) -async def start(): - di.injector.register(instance=story_processor_instance) - di.injector.bind(story_processor_instance, autoupdate=True) - await _do_for_each_extension('start') +async def setup(event_loop=None): + register() + await _do_for_each_extension('setup', event_loop) + + +async def start(event_loop=None): + register() + await _do_for_each_extension('start', event_loop) -async def stop(): - await _do_for_each_extension('stop') +async def stop(event_loop=None): + await _do_for_each_extension('stop', event_loop) -async def _do_for_each_extension(command): +async def _do_for_each_extension(command, even_loop): await asyncio.gather( - *[getattr(m, command)() for m in middlewares if hasattr(m, command)] - ) + *[getattr(m, command)() for m in middlewares if hasattr(m, command)], + loop=even_loop) def forever(loop): From ac82a3ba263f8e5af772d654b03a6103422a6275 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Sun, 27 Nov 2016 23:15:23 +0100 Subject: [PATCH 36/40] inherit di scope --- botstory/di/inject_test.py | 4 +- botstory/di/injector_service.py | 122 +++++++++++++++++++-------- botstory/di/injector_service_test.py | 20 ++++- botstory/story.py | 6 +- 4 files changed, 112 insertions(+), 40 deletions(-) diff --git a/botstory/di/inject_test.py b/botstory/di/inject_test.py index d425f63..18b92c2 100644 --- a/botstory/di/inject_test.py +++ b/botstory/di/inject_test.py @@ -77,7 +77,7 @@ class OuterClass: def inner(self, inner_class=None): self.inner_class = inner_class - outer = di.bind(OuterClass(), autoupdate=False) + outer = di.bind(OuterClass(), auto=False) @di.inject() class InnerClass: @@ -95,7 +95,7 @@ class OuterClass: def inner(self, inner_class=None): self.inner_class = inner_class - outer = di.bind(OuterClass(), autoupdate=True) + outer = di.bind(OuterClass(), auto=True) @di.inject() class InnerClass: diff --git a/botstory/di/injector_service.py b/botstory/di/injector_service.py index d2fc893..719e4e0 100644 --- a/botstory/di/injector_service.py +++ b/botstory/di/injector_service.py @@ -4,32 +4,80 @@ class Scope: - def __init__(self): + def __init__(self, name, parent=None): self.storage = {} # instances that will auto update on each new instance come - self.auto_update_list = [] + self.auto_bind_list = [] # description of classes self.described = {} # functions that waits for deps self.requires_fns = {} # all instances that are singletones self.singleton_cache = {} + self.name = name + # parent scope + self.parent = parent def get(self, type_name): - item = self.storage[type_name] - if inspect.isclass(item): - return item() - else: - return item + try: + item = self.storage[type_name] + if inspect.isclass(item): + return item() + else: + return item + except KeyError as err: + if self.parent: + return self.parent.get(type_name) + raise err + + def get_instance(self, type_name): + try: + return self.singleton_cache[type_name] + except KeyError as err: + if self.parent: + return self.parent.get_instance(type_name) + raise err + + def describe(self, type_name, cls): + self.described[cls] = { + 'type': type_name, + } + + def get_auto_bind_list(self): + yield from self.auto_bind_list + if self.parent: + yield from self.parent.get_auto_bind_list() + + def auto_bind(self, instance): + self.auto_bind_list.append(instance) + + def get_description(self, value): + try: + return self.described.get(value, self.described[type(value)]) + except KeyError as err: + if self.parent: + return self.parent.get_description(value) + raise err + + def store_instance(self, type_name, instance): + self.singleton_cache[type_name] = instance + + def get_endpoint_deps(self, method_ptr): + return self.requires_fns.get(method_ptr, {}).items() or \ + self.parent and self.parent.get_endpoint_deps(method_ptr) or \ + [] + + def store_deps_endpoint(self, fn, deps): + self.requires_fns[fn] = deps def clear(self): - self.auto_update_list = [] + self.auto_bind_list = [] self.described = {} self.requires_fns = {} self.singleton_cache = {} def clear_instances(self): - self.auto_update_list = [] + self.auto_bind_list = [] self.singleton_cache = {} self.storage = {} @@ -45,9 +93,9 @@ def remove_type(self, type_name): # TODO: clear self.requires_fns and self.auto_update_list maybe we can use type(instance)? def __repr__(self): - return ' {}'.format({ + return ' {}'.format(self.name, { 'storage': self.storage, - 'auto_update_list': self.auto_update_list, + 'auto_update_list': self.auto_bind_list, 'described': self.described, 'requires_fns': self.requires_fns, 'singleton_cache': self.singleton_cache, @@ -64,7 +112,7 @@ class MissedDescriptionError(Exception): class Injector: def __init__(self): - self.root = Scope() + self.root = Scope('root') self.current_scope = self.root def describe(self, type_name, cls): @@ -76,9 +124,7 @@ def describe(self, type_name, cls): :return: """ - self.current_scope.described[cls] = { - 'type': type_name, - } + self.current_scope.describe(type_name, cls) def register(self, type_name=None, instance=None): print() @@ -87,7 +133,7 @@ def register(self, type_name=None, instance=None): raise ValueError('type_name parameter should be string or None') if type_name is None: try: - desc = self.current_scope.described.get(instance, self.current_scope.described[type(instance)]) + desc = self.current_scope.get_description(instance) except KeyError: # TODO: should raise exception # raise MissedDescriptionError('{} was not registered'.format(instance)) @@ -102,40 +148,46 @@ def register(self, type_name=None, instance=None): type_name = desc['type'] # print('> before store {} = {}'.format(type_name, instance)) self.current_scope.register(type_name, instance) - for wait_instance in self.current_scope.auto_update_list: + for wait_instance in self.current_scope.get_auto_bind_list(): self.bind(wait_instance) def requires(self, fn): fn_sig = inspect.signature(fn) - self.current_scope.requires_fns[fn] = { + self.current_scope.store_deps_endpoint(fn, { key: {'default': null_if_empty(fn_sig.parameters[key].default)} - for key in fn_sig.parameters.keys() if key != 'self'} + for key in fn_sig.parameters.keys() if key != 'self'}) - def bind(self, instance, autoupdate=False): + def bind(self, instance, auto=False): + """ + Bind deps to instance + + :param instance: + :param auto: follow update of DI and refresh binds once we will get something new + :return: + """ methods = [ (m, cls.__dict__[m]) for cls in inspect.getmro(type(instance)) for m in cls.__dict__ if inspect.isfunction(cls.__dict__[m]) ] - requires_of_methods = [(method_ptr, {dep: self.get(dep) or dep_spec['default'] - for dep, dep_spec in - self.current_scope.requires_fns.get(method_ptr, {}).items()}) - for (method_name, method_ptr) in methods] + deps_of_endpoints = [(method_ptr, {dep: self.get(dep) or dep_spec['default'] + for dep, dep_spec in self.current_scope.get_endpoint_deps(method_ptr)}) + for (method_name, method_ptr) in methods] - for (method_ptr, method_deps) in requires_of_methods: + for (method_ptr, method_deps) in deps_of_endpoints: if len(method_deps) > 0: method_ptr(instance, **method_deps) - if autoupdate and instance not in self.current_scope.auto_update_list: - self.current_scope.auto_update_list.append(instance) + if auto and instance not in self.current_scope.get_auto_bind_list(): + self.current_scope.auto_bind(instance) return instance def get(self, type_name): type_name = parser.kebab_to_underscore(type_name) try: - return self.current_scope.singleton_cache[type_name] + return self.current_scope.get_instance(type_name) except KeyError: try: instance = self.current_scope.get(type_name) @@ -143,13 +195,13 @@ def get(self, type_name): # TODO: sometimes we should fail loudly in this case return None - self.current_scope.singleton_cache[type_name] = instance + self.current_scope.store_instance(type_name, instance) instance = self.bind(instance) return instance - def child_scope(self): - return ChildScopeBuilder(self) + def child_scope(self, name='undefined'): + return ChildScopeBuilder(self, self.current_scope, name) def clear_instances(self): self.current_scope.clear_instances() @@ -159,15 +211,17 @@ def add_scope(self, scope): def remove_scope(self, scope): assert self.current_scope == scope - self.current_scope = self.root + self.current_scope = scope.parent class ChildScopeBuilder: - def __init__(self, injector): + def __init__(self, injector, parent, name): self.injector = injector + self.name = name + self.parent = parent def __enter__(self): - self.scope = Scope() + self.scope = Scope(self.name, self.parent) self.injector.add_scope(self.scope) return self.scope diff --git a/botstory/di/injector_service_test.py b/botstory/di/injector_service_test.py index 37974cc..4349618 100644 --- a/botstory/di/injector_service_test.py +++ b/botstory/di/injector_service_test.py @@ -69,7 +69,7 @@ class InnerClass: outer = OuterClass() di.injector.register(instance=outer) - di.bind(outer, autoupdate=True) + di.bind(outer, auto=True) inner = InnerClass() di.injector.register(instance=inner) @@ -94,3 +94,21 @@ class SecondClass: assert first_class != second_class assert isinstance(first_class, FirstClass) assert isinstance(second_class, SecondClass) + + +def test_inherit_scope(): + with di.child_scope('first'): + @di.desc() + class First: + pass + + with di.child_scope('second'): + @di.desc() + class Second: + @di.inject() + def deps(self, first): + self.first = first + + second = di.get('second') + assert isinstance(second, Second) + assert isinstance(second.first, First) diff --git a/botstory/story.py b/botstory/story.py index bb3ba53..b6012ae 100644 --- a/botstory/story.py +++ b/botstory/story.py @@ -76,7 +76,7 @@ def use(middleware): middlewares.append(middleware) di.injector.register(instance=middleware) - di.bind(middleware, autoupdate=True) + di.bind(middleware, auto=True) # TODO: should use DI somehow if check_spec(['send_text_message'], middleware): @@ -108,8 +108,8 @@ def clear(clear_library=True): def register(): di.injector.register(instance=story_processor_instance) di.injector.register(instance=stories_library) - di.injector.bind(story_processor_instance, autoupdate=True) - di.injector.bind(stories_library, autoupdate=True) + di.injector.bind(story_processor_instance, auto=True) + di.injector.bind(stories_library, auto=True) async def setup(event_loop=None): From 552075c9e009158c1cc4b107953dcf1792968847 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Sun, 27 Nov 2016 23:19:22 +0100 Subject: [PATCH 37/40] test injection of middlewares --- botstory/integrations/aiohttp/aiohttp_test.py | 14 ++++----- botstory/integrations/fb/messenger_test.py | 30 +++++++++---------- botstory/integrations/ga/tracker_test.py | 14 ++++----- botstory/integrations/mockdb/db_test.py | 14 ++++----- .../integrations/mockhttp/mockhttp_test.py | 14 ++++----- .../integrations/mocktracker/tracker_test.py | 14 ++++----- botstory/integrations/mongodb/db_test.py | 14 ++++----- 7 files changed, 57 insertions(+), 57 deletions(-) diff --git a/botstory/integrations/aiohttp/aiohttp_test.py b/botstory/integrations/aiohttp/aiohttp_test.py index d292ac9..efe527e 100644 --- a/botstory/integrations/aiohttp/aiohttp_test.py +++ b/botstory/integrations/aiohttp/aiohttp_test.py @@ -189,14 +189,14 @@ async def mock_middleware_handler(request): await http.stop() -@pytest.mark.skip() def test_get_as_deps(): story.use(aiohttp.AioHttpInterface()) - @di.desc() - class OneClass: - @di.inject() - def deps(self, http): - self.http = http + with di.child_scope('http'): + @di.desc() + class OneClass: + @di.inject() + def deps(self, http): + self.http = http - assert isinstance(di.injector.get('one_class').http, aiohttp.AioHttpInterface) + assert isinstance(di.injector.get('one_class').http, aiohttp.AioHttpInterface) diff --git a/botstory/integrations/fb/messenger_test.py b/botstory/integrations/fb/messenger_test.py index c7e4212..e3ccbef 100644 --- a/botstory/integrations/fb/messenger_test.py +++ b/botstory/integrations/fb/messenger_test.py @@ -842,30 +842,30 @@ async def test_remove_persistent_menu(): ) -@pytest.mark.skip() def test_get_fb_as_deps(): story.use(messenger.FBInterface()) - @di.desc() - class OneClass: - @di.inject() - def deps(self, fb): - self.fb = fb + with di.child_scope(): + @di.desc() + class OneClass: + @di.inject() + def deps(self, fb): + self.fb = fb - assert isinstance(di.injector.get('one_class').fb, messenger.FBInterface) + assert isinstance(di.injector.get('one_class').fb, messenger.FBInterface) -@pytest.mark.skip() def test_bind_fb_deps(): story.use(messenger.FBInterface()) story.use(mockdb.MockDB()) story.use(mockhttp.MockHttpInterface()) - @di.desc() - class OneClass: - @di.inject() - def deps(self, fb): - self.fb = fb + with di.child_scope(): + @di.desc() + class OneClass: + @di.inject() + def deps(self, fb): + self.fb = fb - assert isinstance(di.injector.get('one_class').fb.http, mockhttp.MockHttpInterface) - assert isinstance(di.injector.get('one_class').fb.storage, mockdb.MockDB) + assert isinstance(di.injector.get('one_class').fb.http, mockhttp.MockHttpInterface) + assert isinstance(di.injector.get('one_class').fb.storage, mockdb.MockDB) diff --git a/botstory/integrations/ga/tracker_test.py b/botstory/integrations/ga/tracker_test.py index 366cb00..a272604 100644 --- a/botstory/integrations/ga/tracker_test.py +++ b/botstory/integrations/ga/tracker_test.py @@ -136,14 +136,14 @@ def greeting(message): ]) -@pytest.mark.skip() def test_get_as_deps(): story.use(ga.GAStatistics()) - @di.desc() - class OneClass: - @di.inject() - def deps(self, tracker): - self.tracker = tracker + with di.child_scope(): + @di.desc() + class OneClass: + @di.inject() + def deps(self, tracker): + self.tracker = tracker - assert isinstance(di.injector.get('one_class').tracker, ga.GAStatistics) + assert isinstance(di.injector.get('one_class').tracker, ga.GAStatistics) diff --git a/botstory/integrations/mockdb/db_test.py b/botstory/integrations/mockdb/db_test.py index f2a6d14..a5b8b93 100644 --- a/botstory/integrations/mockdb/db_test.py +++ b/botstory/integrations/mockdb/db_test.py @@ -3,14 +3,14 @@ from ... import di, story -@pytest.mark.skip() def test_get_mockdb_as_dep(): story.use(mockdb.MockDB()) - @di.desc() - class OneClass: - @di.inject() - def deps(self, storage): - self.storage = storage + with di.child_scope(): + @di.desc() + class OneClass: + @di.inject() + def deps(self, storage): + self.storage = storage - assert isinstance(di.injector.get('one_class').storage, mockdb.MockDB) + assert isinstance(di.injector.get('one_class').storage, mockdb.MockDB) diff --git a/botstory/integrations/mockhttp/mockhttp_test.py b/botstory/integrations/mockhttp/mockhttp_test.py index 1ec25cd..24e692e 100644 --- a/botstory/integrations/mockhttp/mockhttp_test.py +++ b/botstory/integrations/mockhttp/mockhttp_test.py @@ -3,14 +3,14 @@ from ... import di, story -@pytest.mark.skip() def test_get_mockhttp_as_dep(): story.use(mockhttp.MockHttpInterface()) - @di.desc() - class OneClass: - @di.inject() - def deps(self, http): - self.http = http + with di.child_scope(): + @di.desc() + class OneClass: + @di.inject() + def deps(self, http): + self.http = http - assert isinstance(di.injector.get('one_class').http, mockhttp.MockHttpInterface) + assert isinstance(di.injector.get('one_class').http, mockhttp.MockHttpInterface) diff --git a/botstory/integrations/mocktracker/tracker_test.py b/botstory/integrations/mocktracker/tracker_test.py index 7fa0c79..9c14c75 100644 --- a/botstory/integrations/mocktracker/tracker_test.py +++ b/botstory/integrations/mocktracker/tracker_test.py @@ -29,14 +29,14 @@ def test_story(): t.story() -@pytest.mark.skip() def test_get_mock_tracker_as_dep(): story.use(mocktracker.MockTracker()) - @di.desc() - class OneClass: - @di.inject() - def deps(self, tracker): - self.tracker = tracker + with di.child_scope(): + @di.desc() + class OneClass: + @di.inject() + def deps(self, tracker): + self.tracker = tracker - assert isinstance(di.injector.get('one_class').tracker, mocktracker.MockTracker) + assert isinstance(di.injector.get('one_class').tracker, mocktracker.MockTracker) diff --git a/botstory/integrations/mongodb/db_test.py b/botstory/integrations/mongodb/db_test.py index 06a83b7..7cf4abf 100644 --- a/botstory/integrations/mongodb/db_test.py +++ b/botstory/integrations/mongodb/db_test.py @@ -122,14 +122,14 @@ async def test_start_should_open_connection_and_close_on_stop(): assert not db_interface.db -@pytest.mark.skip() def test_get_mongodb_as_dep(): story.use(mongodb.MongodbInterface()) - @di.desc() - class OneClass: - @di.inject() - def deps(self, storage): - self.storage = storage + with di.child_scope(): + @di.desc() + class OneClass: + @di.inject() + def deps(self, storage): + self.storage = storage - assert isinstance(di.injector.get('one_class').storage, mongodb.MongodbInterface) + assert isinstance(di.injector.get('one_class').storage, mongodb.MongodbInterface) From b7956c9dfc532adfbfc7728c08829b166c467526 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Sun, 27 Nov 2016 23:22:41 +0100 Subject: [PATCH 38/40] remove useless field type --- botstory/integrations/aiohttp/aiohttp.py | 2 -- botstory/integrations/fb/messenger.py | 2 +- botstory/integrations/ga/tracker.py | 14 +++++++------- botstory/integrations/mockdb/db.py | 2 -- botstory/integrations/mockhttp/mockhttp.py | 2 -- botstory/integrations/mocktracker/tracker.py | 2 -- botstory/integrations/mongodb/db.py | 2 -- 7 files changed, 8 insertions(+), 18 deletions(-) diff --git a/botstory/integrations/aiohttp/aiohttp.py b/botstory/integrations/aiohttp/aiohttp.py index 0b4c7a3..7821e3f 100644 --- a/botstory/integrations/aiohttp/aiohttp.py +++ b/botstory/integrations/aiohttp/aiohttp.py @@ -30,8 +30,6 @@ def is_ok(status): @di.desc('http', reg=False) class AioHttpInterface: - type = 'interface.aiohttp' - def __init__(self, host='0.0.0.0', port=None, shutdown_timeout=60.0, ssl_context=None, backlog=128, auto_start=True, diff --git a/botstory/integrations/fb/messenger.py b/botstory/integrations/fb/messenger.py index 0831edb..8e683e3 100644 --- a/botstory/integrations/fb/messenger.py +++ b/botstory/integrations/fb/messenger.py @@ -11,7 +11,7 @@ @di.desc('fb', reg=False) class FBInterface: - type = 'interface.facebook' + type = 'facebook' def __init__(self, api_uri='https://graph.facebook.com/v2.6', diff --git a/botstory/integrations/ga/tracker.py b/botstory/integrations/ga/tracker.py index 908e6e6..4b3c2af 100644 --- a/botstory/integrations/ga/tracker.py +++ b/botstory/integrations/ga/tracker.py @@ -1,3 +1,10 @@ +""" +pageview: [ page path ] +event: category, action, [ label [, value ] ] +social: network, action [, target ] +timing: category, variable, time [, label ] +""" + import functools import json import logging @@ -11,13 +18,6 @@ @di.desc('tracker', reg=False) class GAStatistics: - type = 'interface.tracker' - """ - pageview: [ page path ] - event: category, action, [ label [, value ] ] - social: network, action [, target ] - timing: category, variable, time [, label ] - """ def __init__(self, tracking_id=None, diff --git a/botstory/integrations/mockdb/db.py b/botstory/integrations/mockdb/db.py index b2a7e2c..3cec89f 100644 --- a/botstory/integrations/mockdb/db.py +++ b/botstory/integrations/mockdb/db.py @@ -4,8 +4,6 @@ @di.desc('storage', reg=False) class MockDB: - type = 'interface.session_storage' - def __init__(self): self.session = None self.user = None diff --git a/botstory/integrations/mockhttp/mockhttp.py b/botstory/integrations/mockhttp/mockhttp.py index 23456b9..7ed3e60 100644 --- a/botstory/integrations/mockhttp/mockhttp.py +++ b/botstory/integrations/mockhttp/mockhttp.py @@ -19,8 +19,6 @@ def stub(name=None): @di.desc('http', reg=False) class MockHttpInterface: - type = 'interface.http' - def __init__(self, get={}, get_raise=sentinel, post=True, post_raise=sentinel, diff --git a/botstory/integrations/mocktracker/tracker.py b/botstory/integrations/mocktracker/tracker.py index 887702b..c1fff75 100644 --- a/botstory/integrations/mocktracker/tracker.py +++ b/botstory/integrations/mocktracker/tracker.py @@ -6,8 +6,6 @@ @di.desc('tracker', reg=False) class MockTracker: - type = 'interface.tracker' - def event(self, *args, **kwargs): logging.debug('event') logging.debug(kwargs) diff --git a/botstory/integrations/mongodb/db.py b/botstory/integrations/mongodb/db.py index 58f7992..c79ce7e 100644 --- a/botstory/integrations/mongodb/db.py +++ b/botstory/integrations/mongodb/db.py @@ -8,8 +8,6 @@ @di.desc('storage', reg=False) class MongodbInterface: - type = 'interface.session_storage' - """ https://github.com/mongodb/motor """ From d562c896f441189998be0d77b017669584fc687b Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Mon, 28 Nov 2016 00:51:59 +0100 Subject: [PATCH 39/40] inject only complete deps --- botstory/di/injector_service.py | 40 ++++++++++---- botstory/di/injector_service_test.py | 68 ++++++++++++++++++++++++ botstory/integrations/aiohttp/aiohttp.py | 19 +++---- 3 files changed, 107 insertions(+), 20 deletions(-) diff --git a/botstory/di/injector_service.py b/botstory/di/injector_service.py index 719e4e0..ee76922 100644 --- a/botstory/di/injector_service.py +++ b/botstory/di/injector_service.py @@ -1,7 +1,10 @@ +import logging import inspect from . import parser +logger = logging.getLogger(__name__) + class Scope: def __init__(self, name, parent=None): @@ -63,6 +66,8 @@ def store_instance(self, type_name, instance): self.singleton_cache[type_name] = instance def get_endpoint_deps(self, method_ptr): + logger.debug('get_endpoint_deps {}'.format(method_ptr)) + logger.debug(self.requires_fns.get(method_ptr, {})) return self.requires_fns.get(method_ptr, {}).items() or \ self.parent and self.parent.get_endpoint_deps(method_ptr) or \ [] @@ -102,8 +107,8 @@ def __repr__(self): }) -def null_if_empty(value): - return value if value is not inspect.Parameter.empty else None +def empty_array_if_empty(default): + return [default] if default is not inspect.Parameter.empty else [] class MissedDescriptionError(Exception): @@ -154,9 +159,21 @@ def register(self, type_name=None, instance=None): def requires(self, fn): fn_sig = inspect.signature(fn) self.current_scope.store_deps_endpoint(fn, { - key: {'default': null_if_empty(fn_sig.parameters[key].default)} + key: {'default': default for default in empty_array_if_empty(fn_sig.parameters[key].default)} for key in fn_sig.parameters.keys() if key != 'self'}) + def entrypoint_deps(self, method_ptr): + # we should have something to inject + # registered instance, class or default value + if not any(self.get(dep) or 'default' in dep_spec + for dep, dep_spec + in self.current_scope.get_endpoint_deps(method_ptr)): + # otherwise we should inject anything + return {} + + return {dep: self.get(dep) or dep_spec['default'] + for dep, dep_spec in self.current_scope.get_endpoint_deps(method_ptr)} + def bind(self, instance, auto=False): """ Bind deps to instance @@ -171,13 +188,18 @@ def bind(self, instance, auto=False): for m in cls.__dict__ if inspect.isfunction(cls.__dict__[m]) ] - deps_of_endpoints = [(method_ptr, {dep: self.get(dep) or dep_spec['default'] - for dep, dep_spec in self.current_scope.get_endpoint_deps(method_ptr)}) - for (method_name, method_ptr) in methods] + logger.debug('bind {}'.format(instance)) + logger.debug('methods {}'.format(methods)) - for (method_ptr, method_deps) in deps_of_endpoints: - if len(method_deps) > 0: - method_ptr(instance, **method_deps) + try: + deps_of_endpoints = [(method_ptr, self.entrypoint_deps(method_ptr)) + for (method_name, method_ptr) in methods] + + for (method_ptr, method_deps) in deps_of_endpoints: + if len(method_deps) > 0: + method_ptr(instance, **method_deps) + except KeyError: + pass if auto and instance not in self.current_scope.get_auto_bind_list(): self.current_scope.auto_bind(instance) diff --git a/botstory/di/injector_service_test.py b/botstory/di/injector_service_test.py index 4349618..4ae7c70 100644 --- a/botstory/di/injector_service_test.py +++ b/botstory/di/injector_service_test.py @@ -112,3 +112,71 @@ def deps(self, first): second = di.get('second') assert isinstance(second, Second) assert isinstance(second.first, First) + + +def test_do_not_call_deps_endpoint_before_we_have_all_needed_deps(): + with di.child_scope(): + @di.desc() + class Container: + def __init__(self): + self.one = 'undefined' + self.two = 'undefined' + + @di.inject() + def deps(self, one, two): + self.one = one + self.two = two + + container = di.get('container') + assert container.one == 'undefined' + assert container.two == 'undefined' + + @di.desc() + class One: + pass + + di.bind(container) + assert container.one == 'undefined' + assert container.two == 'undefined' + + @di.desc() + class Two: + pass + + di.bind(container) + assert isinstance(container.one, One) + assert isinstance(container.two, Two) + + +def test_do_not_call_deps_endpoint_before_we_have_all_needed_deps_or_default_value(): + with di.child_scope(): + @di.desc() + class Container: + def __init__(self): + self.one = 'undefined' + self.two = 'undefined' + + @di.inject() + def deps(self, one, two='default'): + self.one = one + self.two = two + + container = di.get('container') + assert container.one == 'undefined' + assert container.two == 'undefined' + + @di.desc() + class One: + pass + + di.bind(container) + assert isinstance(container.one, One) + assert container.two == 'default' + + @di.desc() + class Two: + pass + + di.bind(container) + assert isinstance(container.one, One) + assert isinstance(container.two, Two) diff --git a/botstory/integrations/aiohttp/aiohttp.py b/botstory/integrations/aiohttp/aiohttp.py index 7821e3f..f8cec34 100644 --- a/botstory/integrations/aiohttp/aiohttp.py +++ b/botstory/integrations/aiohttp/aiohttp.py @@ -25,9 +25,6 @@ def is_ok(status): return 200 <= status < 400 -print('before @di.inject') - - @di.desc('http', reg=False) class AioHttpInterface: def __init__(self, host='0.0.0.0', port=None, @@ -159,14 +156,14 @@ async def method(self, method_type, session, url, **kwargs): except Exception as err: logger.warn( 'Exception: status: {status}, message: {message}, type: {type}, method: {method}, url: {url}, {kwargs}' - .format(status=getattr(err, 'code', None), - message=getattr(err, 'message', None), - type=type(err), - method=method_name, - url=url, - kwargs=kwargs, - ) - ) + .format(status=getattr(err, 'code', None), + message=getattr(err, 'message', None), + type=type(err), + method=method_name, + url=url, + kwargs=kwargs, + ) + ) raise err return resp From 4f12066c90dea28a246d98604d7a77d82fe01ae0 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Mon, 28 Nov 2016 00:59:06 +0100 Subject: [PATCH 40/40] remove extra logging --- botstory/di/injector_service.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/botstory/di/injector_service.py b/botstory/di/injector_service.py index ee76922..c0bf145 100644 --- a/botstory/di/injector_service.py +++ b/botstory/di/injector_service.py @@ -66,8 +66,6 @@ def store_instance(self, type_name, instance): self.singleton_cache[type_name] = instance def get_endpoint_deps(self, method_ptr): - logger.debug('get_endpoint_deps {}'.format(method_ptr)) - logger.debug(self.requires_fns.get(method_ptr, {})) return self.requires_fns.get(method_ptr, {}).items() or \ self.parent and self.parent.get_endpoint_deps(method_ptr) or \ [] @@ -188,9 +186,6 @@ def bind(self, instance, auto=False): for m in cls.__dict__ if inspect.isfunction(cls.__dict__[m]) ] - logger.debug('bind {}'.format(instance)) - logger.debug('methods {}'.format(methods)) - try: deps_of_endpoints = [(method_ptr, self.entrypoint_deps(method_ptr)) for (method_name, method_ptr) in methods]