Skip to content

Commit

Permalink
Merge pull request #93 from botstory/feature/di
Browse files Browse the repository at this point in the history
Feature/di
  • Loading branch information
hyzhak committed Nov 28, 2016
2 parents 3c4e0dd + 4f12066 commit eb1d484
Show file tree
Hide file tree
Showing 29 changed files with 974 additions and 140 deletions.
2 changes: 2 additions & 0 deletions botstory/ast/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 10 additions & 18 deletions botstory/ast/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,28 @@
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 = []
self.library = library
self.middlewares = middlewares
self.parser_instance = parser_instance
self.storage = None
self.tracker = mocktracker.MockTracker()

def add_interface(self, interface):
if self.storage:
interface.add_storage(self.storage)
self.interfaces.append(interface)
interface.processor = self

def add_storage(self, storage):
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):
self.interfaces = []
self.storage = None

async def match_message(self, message):
"""
because match_message is recursive we split function to
Expand All @@ -49,6 +36,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'],
Expand Down Expand Up @@ -151,6 +140,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,
Expand Down
6 changes: 4 additions & 2 deletions botstory/ast/users.py
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -26,7 +28,7 @@ def on_new_user_comes(user):

def clear():
global _tracker
_tracker = tracker.MockTracker()
_tracker = tracker_module.MockTracker()


clear()
7 changes: 0 additions & 7 deletions botstory/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 14 additions & 0 deletions botstory/di/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from . import desciption as desc_module, inject as inject_module, injector_service

__all__ = []

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

__all__.extend([bind, child_scope, clear_instances, desc, inject])
25 changes: 25 additions & 0 deletions botstory/di/desciption.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import inspect
from .parser import camel_case_to_underscore
from .. import di


def desc(t=None, reg=True):
"""
Describe Class Dependency
:param reg: should we register this class as well
:param t: custom type as well
:return:
"""

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 reg:
di.injector.register(name, cls)
else:
di.injector.describe(name, cls)
return cls

return decorated_fn
21 changes: 21 additions & 0 deletions botstory/di/inject.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import inspect

from .parser import camel_case_to_underscore

from .. import di


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)
else:
# I'm not sure whether it possible case
raise NotImplementedError('try decorate {}'.format(fn))
return fn

return decorated_fn
138 changes: 138 additions & 0 deletions botstory/di/inject_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import pytest
from .. import di


def test_inject_decorator():
with di.child_scope():
@di.inject()
class OneClass:
def __init__(self):
pass

assert isinstance(di.injector.get('one_class'), OneClass)


def test_bind_singleton_instance_by_default():
with di.child_scope():
@di.inject()
class OneClass:
def __init__(self):
pass

assert di.injector.get('one_class') == di.injector.get('one_class')


def test_inject_into_method_of_class():
with di.child_scope():
@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)


def test_bind_should_inject_deps_in_decorated_methods_():
with di.child_scope():
@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)


def test_inject_default_value_if_we_dont_have_dep():
with di.child_scope():
@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!'


def test_no_autoupdate_deps_on_new_instance_comes():
with di.child_scope():
@di.inject()
class OuterClass:
@di.inject()
def inner(self, inner_class=None):
self.inner_class = inner_class

outer = di.bind(OuterClass(), auto=False)

@di.inject()
class InnerClass:
pass

assert isinstance(outer, OuterClass)
assert outer.inner_class is None


def test_autoupdate_deps_on_new_instance_comes():
with di.child_scope():
@di.inject()
class OuterClass:
@di.inject()
def inner(self, inner_class=None):
self.inner_class = inner_class

outer = di.bind(OuterClass(), auto=True)

@di.inject()
class InnerClass:
pass

assert isinstance(outer, OuterClass)
assert isinstance(outer.inner_class, InnerClass)


def test_fail_on_cyclic_deps():
with di.child_scope():
@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)


def test_custom_type():
with di.child_scope():
@di.inject('qwerty')
class OneClass:
pass

assert isinstance(di.injector.get('qwerty'), OneClass)


def test_fail_on_incorrect_using():
with pytest.raises(NotImplementedError):
di.inject()('qwerty')

0 comments on commit eb1d484

Please sign in to comment.