Commit cefaba60 authored by Xavier Barbosa's avatar Xavier Barbosa

finish the core implementation.

parent 8d74ab58
Pipeline #186 passed with stage
[run]
omit = aiocompose/_version.py
omit = knighted/_version.py
......@@ -6,13 +6,13 @@ before_script:
python3.3 tests:
script:
- py.test --cov aiocompose --cov-report term-missing tests/
- py.test --cov knighted --cov-report term-missing tests/
tags:
- python3.3
python3.4 tests:
script:
- py.test --cov aiocompose --cov-report term-missing tests/
- py.test --cov knighted --cov-report term-missing tests/
tags:
- python3.4
......
AIOCompose
==========
Knighted
========
Knighted, is heavily inspired by jeni_ and works only with asyncio_.
It allows to described dependencies, and inject them later.
For example::
from knighted import annotation, Injector
class MyInjector(Injector):
pass
services = MyInjector()
@services.factory('foo')
def foo_factory():
return 'I am foo'
@services.factory('bar')
def bar_factory():
return 'I am bar'
@services.factory('all')
def together_factory():
foo = yield from services.get('foo')
bar = yield from services.get('bar')
return [foo, bar]
@annotate('foo', 'bar')
def fun(foo, bar):
return {'foo': foo,
'bar': bar}
assert (yield from services.apply(fun)) == {'foo': 'I am foo',
'bar': 'I am bar'}
.. _asyncio: https://pypi.python.org/pypi/asyncio
.. _jeni: https://pypi.python.org/pypi/jeni
......@@ -38,7 +38,7 @@ def get_config():
cfg.style = "pep440"
cfg.tag_prefix = "v"
cfg.parentdir_prefix = "None"
cfg.versionfile_source = "aiocompose/_version.py"
cfg.versionfile_source = "knighted/_version.py"
cfg.verbose = False
return cfg
......
import asyncio
import logging
from abc import ABCMeta
from collections import namedtuple, OrderedDict
from collections import defaultdict, namedtuple, OrderedDict
from itertools import chain
from weakref import WeakKeyDictionary, WeakSet
from functools import wraps
logger = logging.getLogger(__name__)
......@@ -30,7 +32,7 @@ class FactoryMethod:
return Factory(target)
class DataStore:
class DataProxy:
def __init__(self, name, type):
self.name = name
......@@ -43,17 +45,56 @@ class DataStore:
return getattr(target, self.name)
class CloseHandler:
"""Closes mounted services
"""
def __init__(self, injector):
self.injector = injector
self.registry = WeakKeyDictionary()
def register(self, obj, reaction=None):
"""Register callbacks that should be thrown on close.
"""
reaction = reaction or close_reaction
reactions = self.registry.setdefault(obj, WeakSet())
reactions.add(reaction)
def unregister(self, obj, reaction=None):
"""Unregister callbacks that should not be thrown on close.
"""
if reaction:
reactions = self.registry.setdefault(obj, WeakSet())
reactions.remove(reaction)
if not reactions:
self.registry.pop(obj, None)
else:
self.registry.pop(obj, None)
@asyncio.coroutine
def __call__(self):
for obj, reactions in self.registry.items():
for reaction in reactions:
if asyncio.iscoroutinefunction(reaction):
yield from reaction(obj)
else:
reaction(obj)
self.injector.services.clear()
class Injector(metaclass=ABCMeta):
"""Collects dependencies and reads annotations to inject them.
"""
factory = FactoryMethod()
services = DataStore('_services', OrderedDict)
factories = DataStore('_factories', OrderedDict)
services = DataProxy('_services', OrderedDict)
factories = DataProxy('_factories', OrderedDict)
def __init__(self):
self.services = self.__class__.services.copy()
self.factories = self.__class__.factories.copy()
self.reactions = defaultdict(WeakKeyDictionary)
self.close = CloseHandler(self)
@asyncio.coroutine
def get(self, note):
......@@ -69,47 +110,47 @@ class Injector(metaclass=ABCMeta):
raise ValueError('%r is not defined' % note)
@asyncio.coroutine
def inject(self, *args, **kwargs):
def apply(self, *args, **kwargs):
func, *args = args
if func in ANNOTATIONS:
annotated = ANNOTATIONS[func]
service_args, service_kwargs = [], {}
for note in annotated.pos_notes:
service = yield from self.get(note)
service_args.append(service)
for key, note in annotated.kw_notes.items():
service = yield from self.get(note)
service_kwargs[key] = service
service_args.extend(args)
service_kwargs.update(kwargs)
return func(*service_args, **service_kwargs)
logger.warn('%r is not annoted' % callable)
return callable(*args, **kwargs)
@staticmethod
def close_on_exit(obj):
"""Mark object should be closed on exit
"""
EXIT_OBJECTS.add(obj)
response = yield from self.partial(func)(*args, **kwargs)
return response
def partial(self, func):
"""Resolves lately dependancies.
def close(self):
"""Closes mounted services
Returns:
callable: the service partially resolved
"""
flush = []
for name, service in self.services.items():
if service in EXIT_OBJECTS:
service.close()
logger.info('closed service %s', name)
flush.append(name)
for name in flush:
self.services.pop(name, None)
logger.info('flushed service %s', name)
ANNOTATIONS = {}
@wraps(func)
@asyncio.coroutine
def wrapper(*args, **kwargs):
if func in ANNOTATIONS:
annotated = ANNOTATIONS[func]
service_args, service_kwargs = [], {}
for note in annotated.pos_notes:
service = yield from self.get(note)
service_args.append(service)
for key, note in annotated.kw_notes.items():
service = yield from self.get(note)
service_kwargs[key] = service
service_args.extend(args)
service_kwargs.update(kwargs)
return func(*service_args, **service_kwargs)
logger.warn('%r is not annoted' % func)
return func(*args, **kwargs)
return wrapper
ANNOTATIONS = WeakKeyDictionary()
Annotation = namedtuple('Annotation', 'pos_notes kw_notes')
def close_reaction(obj):
obj.close()
def annotate(*args, **kwargs):
def decorate(func):
......@@ -123,15 +164,6 @@ def annotate(*args, **kwargs):
return decorate
EXIT_OBJECTS = set()
def close_all():
for obj in EXIT_OBJECTS:
logger.info('close %s', obj)
obj.close()
def note_loop(note):
args = note.split(':')
results = []
......
[versioneer]
VCS = git
style = pep440
versionfile_source = aiocompose/_version.py
versionfile_source = knighted/_version.py
tag_prefix = v
[metadata]
......
......@@ -3,7 +3,7 @@ from setuptools import setup, find_packages
import versioneer
setup(
name='aiocompose',
name='knighted',
version=versioneer.get_version(),
author='Xavier Barbosa',
author_email='clint.northwood@gmail.com',
......@@ -26,7 +26,7 @@ setup(
"Topic :: Software Development :: Libraries :: Python Modules"
],
keywords=['dependency injection', 'composing'],
url='http://lab.errorist.xyz/abc/aiocompose',
url='http://lab.errorist.xyz/abc/knighted',
license='MIT',
cmdclass=versioneer.get_cmdclass()
)
import pytest
from aiocompose import Injector, annotate
from knighted import Injector, annotate
@pytest.mark.asyncio
......@@ -31,10 +31,37 @@ def test_instance_factory():
assert (yield from services.get('foo')) == 'I am foo'
assert (yield from services.get('bar')) == 'I am bar'
assert (yield from services.get('all')) == ['I am foo', 'I am bar']
assert (yield from services.inject(fun)) == {'foo': 'I am foo',
assert (yield from services.apply(fun)) == {'foo': 'I am foo',
'bar': 'I am bar'}
@pytest.mark.asyncio
def test_partial():
class MyInjector(Injector):
pass
services = MyInjector()
@services.factory('foo')
def foo_factory():
return 'I am foo'
@services.factory('bar')
def bar_factory():
return 'I am bar'
@annotate('foo', 'bar')
def fun(foo, bar):
return {'foo': foo,
'bar': bar}
assert len(services.services) == 0
part = services.partial(fun)
assert len(services.services) == 0
assert (yield from part()) == {'foo': 'I am foo', 'bar': 'I am bar'}
assert len(services.services) == 2
@pytest.mark.asyncio
def test_class_factory():
class MyInjector(Injector):
......@@ -64,7 +91,7 @@ def test_class_factory():
assert (yield from services.get('foo')) == 'I am foo'
assert (yield from services.get('bar')) == 'I am bar'
assert (yield from services.get('all')) == ['I am foo', 'I am bar']
assert (yield from services.inject(fun)) == {'foo': 'I am foo',
assert (yield from services.apply(fun)) == {'foo': 'I am foo',
'bar': 'I am bar'}
......@@ -86,7 +113,7 @@ def test_annotate_error():
@pytest.mark.asyncio
def test_kw_inject():
def test_kw_apply():
class MyInjector(Injector):
pass
......@@ -100,7 +127,11 @@ def test_kw_inject():
def fun(foo, *, bar):
return '%s, not %s' % (foo, bar)
assert (yield from services.inject(fun, bar='baz')) == 'I am bar, not baz'
def fun2(baz):
return 'nor %s' % baz
assert (yield from services.apply(fun, bar='baz')) == 'I am bar, not baz'
assert (yield from services.apply(fun2, 'baz')) == 'nor baz'
@pytest.mark.asyncio
......@@ -134,3 +165,75 @@ def test_sub_factory():
assert (yield from services.get('foo')) == 'I am foo'
assert (yield from services.get('foo:bar')) == 'I am bar'
@pytest.mark.asyncio
def test_close():
class MyInjector(Injector):
pass
services = MyInjector()
@services.factory('foo')
def foo_factory():
return 'I am foo'
@services.factory('foo:bar')
def bar_factory():
return 'I am bar'
assert len(services.services) == 0
yield from services.get('foo')
yield from services.get('foo:bar')
assert len(services.services) == 2
yield from services.close()
assert len(services.services) == 0
@pytest.mark.asyncio
def test_close_register():
class MyInjector(Injector):
pass
services = MyInjector()
class Foo:
def __init__(self):
self.value = 'bar'
def set_value(self, value):
self.value = value
foo = Foo()
def reaction(obj):
obj.set_value('baz')
services.close.register(foo, reaction=reaction)
yield from services.close()
assert foo.value == 'baz'
@pytest.mark.asyncio
def test_close_unregister():
class MyInjector(Injector):
pass
services = MyInjector()
class Foo:
def __init__(self):
self.value = 'bar'
def set_value(self, value):
self.value = value
foo = Foo()
def reaction(obj):
obj.set_value('baz')
services.close.register(foo, reaction=reaction)
services.close.unregister(foo, reaction=reaction)
yield from services.close()
assert foo.value == 'bar'
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment