...
 
Commits (6)
layout python3
...@@ -57,3 +57,6 @@ docs/_build/ ...@@ -57,3 +57,6 @@ docs/_build/
# PyBuilder # PyBuilder
target/ target/
.mypy_cache/
.pytest_cache/
.vscode/
\ No newline at end of file
...@@ -4,17 +4,11 @@ before_script: ...@@ -4,17 +4,11 @@ before_script:
- python -m pip install -e . - python -m pip install -e .
- python -m pip install -r requirements-test.txt - python -m pip install -r requirements-test.txt
python3.3 tests: python3 tests:
script: script:
- py.test --cov knighted --cov-report term-missing tests/ - py.test --cov knighted --cov-report term-missing tests/
tags: tags:
- python3.3 - python3
python3.4 tests:
script:
- py.test --cov knighted --cov-report term-missing tests/
tags:
- python3.4
publish to pypi: publish to pypi:
type: deploy type: deploy
...@@ -23,7 +17,7 @@ publish to pypi: ...@@ -23,7 +17,7 @@ publish to pypi:
- python setup.py sdist bdist_wheel - python setup.py sdist bdist_wheel
- twine upload -u $PYPI_USER -p $PYPI_PASSWORD dist/* - twine upload -u $PYPI_USER -p $PYPI_PASSWORD dist/*
tags: tags:
- python3.4 - python3
only: only:
- /^v[\d\.]+.*$/ - /^v[\d\.]+.*$/
allow_failure: true allow_failure: true
...@@ -36,6 +36,62 @@ For example:: ...@@ -36,6 +36,62 @@ For example::
assert (yield from services.apply(fun)) == {'foo': 'I am foo', assert (yield from services.apply(fun)) == {'foo': 'I am foo',
'bar': 'I am bar'} 'bar': 'I am bar'}
The :func:`func` can be a function or an awaitable. These 2 examples works the same::
@annotate('foo', 'bar')
def sync_fun(foo, bar):
return {'foo': foo,
'bar': bar}
assert (yield from services.apply(sync_fun)) == {'foo': 'I am foo',
'bar': 'I am bar'}
@annotate('foo', 'bar')
async def awaitable_fun(foo, bar):
return {'foo': foo,
'bar': bar}
assert (yield from services.apply(awaitable_fun)) == {'foo': 'I am foo',
'bar': 'I am bar'}
When applied with some arguments, placeholders just fills the gaps::
@annotate('foo', 'bar')
def fun(foo, bar):
return {'foo': foo,
'bar': bar}
assert (yield from services.apply(fun, foo="yes")) == {'foo': 'yes',
'bar': 'I am bar'}
@annotate('foo', 'bar')
async def awaitable_fun(foo, bar):
return {'foo': foo,
'bar': bar}
assert (yield from services.apply(awaitable_fun)) == {'foo': 'I am foo',
'bar': 'I am bar'}
Factories also can be either sync or awaitable::
@services.factory('bar:sync')
def bar_factory():
return 'I am bar'
@services.factory('bar:awaitable')
async def bar_factory():
return 'I am bar'
Implementation Implementation
-------------- --------------
......
from __future__ import annotations
import asyncio import asyncio
import concurrent.futures
import logging import logging
from abc import ABCMeta from abc import ABCMeta
from collections import defaultdict, namedtuple, OrderedDict from collections import OrderedDict, defaultdict
from functools import wraps
from inspect import signature
from itertools import chain from itertools import chain
from weakref import WeakKeyDictionary from weakref import WeakKeyDictionary
from functools import wraps
logger = logging.getLogger(__name__) from cached_property import cached_property
logger = logging.getLogger("knighted")
class Factory: class Factory:
...@@ -16,7 +22,7 @@ class Factory: ...@@ -16,7 +22,7 @@ class Factory:
def __call__(self, note, func=None): def __call__(self, note, func=None):
def decorate(func): def decorate(func):
self.target.factories[note] = asyncio.coroutine(func) self.target.factories[note] = func
return func return func
if func: if func:
return decorate(func) return decorate(func)
...@@ -59,7 +65,6 @@ class CloseHandler: ...@@ -59,7 +65,6 @@ class CloseHandler:
reaction = reaction or close_reaction reaction = reaction or close_reaction
reactions = self.registry.setdefault(obj, set()) reactions = self.registry.setdefault(obj, set())
reactions.add(reaction) reactions.add(reaction)
print('reactions are', obj, reactions)
def unregister(self, obj, reaction=None): def unregister(self, obj, reaction=None):
"""Unregister callbacks that should not be thrown on close. """Unregister callbacks that should not be thrown on close.
...@@ -93,55 +98,75 @@ class Injector(metaclass=ABCMeta): ...@@ -93,55 +98,75 @@ class Injector(metaclass=ABCMeta):
self.reactions = defaultdict(WeakKeyDictionary) self.reactions = defaultdict(WeakKeyDictionary)
self.close = CloseHandler(self) self.close = CloseHandler(self)
@asyncio.coroutine @cached_property
def get(self, note): def executor(self):
return concurrent.futures.ThreadPoolExecutor(max_workers=10)
async def get(self, note):
if note in self.services: if note in self.services:
return self.services[note] return self.services[note]
for fact, args in note_loop(note): for fact, args in note_loop(note):
if fact in self.factories: if fact in self.factories:
instance = yield from self.factories[fact](*args) func = self.factories[fact]
if asyncio.iscoroutinefunction(func):
instance = await func(*args)
else:
loop = asyncio.get_running_loop()
instance = await loop.run_in_executor(self.executor, func, *args)
logger.info('loaded service %s' % note) logger.info('loaded service %s' % note)
self.services[note] = instance self.services[note] = instance
return instance return instance
raise ValueError('%r is not defined' % note) raise ValueError('%r is not defined' % note)
@asyncio.coroutine async def apply(self, *args, **kwargs):
def apply(self, *args, **kwargs):
func, *args = args func, *args = args
response = yield from self.partial(func)(*args, **kwargs) response = await self.partial(func)(*args, **kwargs)
return response return response
def partial(self, func): def partial(self, func):
"""Resolves lately dependancies. """Resolves lately dependencies.
Returns: Returns:
callable: the service partially resolved callable: the service partially resolved
""" """
@wraps(func) @wraps(func)
@asyncio.coroutine async def wrapper(*args, **kwargs):
def wrapper(*args, **kwargs):
if func in ANNOTATIONS: if func in ANNOTATIONS:
annotated = ANNOTATIONS[func] annotation = ANNOTATIONS[func]
service_args, service_kwargs = [], {} given = annotation.given(*args, **kwargs)
for note in annotated.pos_notes: to_load = {}
service = yield from self.get(note) for key, note in annotation.marked.items():
service_args.append(service) if key not in given:
for key, note in annotated.kw_notes.items(): to_load[key] = asyncio.create_task(self.get(note))
service = yield from self.get(note) for key, fut in to_load.items():
service_kwargs[key] = service to_load[key] = await fut
service_args.extend(args) kwargs.update(to_load)
service_kwargs.update(kwargs) result = func(*args, **kwargs)
return func(*service_args, **service_kwargs) if asyncio.iscoroutine(result):
logger.warn('%r is not annoted' % func) result = await result
return result
logger.warning('%r is not annoted', func)
return func(*args, **kwargs) return func(*args, **kwargs)
return wrapper return wrapper
ANNOTATIONS = WeakKeyDictionary() class Annotation:
def __init__(self, pos_notes, kw_notes, func):
self.pos_notes = pos_notes
self.kw_notes = kw_notes
self.bind_partial = signature(func).bind_partial
@cached_property
def marked(self):
return self.bind_partial(*self.pos_notes, **self.kw_notes).arguments
def given(self, *args, **kwargs):
return list(self.bind_partial(*args, **kwargs).arguments)
Annotation = namedtuple('Annotation', 'pos_notes kw_notes') ANNOTATIONS: WeakKeyDictionary[str, Annotation] = WeakKeyDictionary()
def close_reaction(obj): def close_reaction(obj):
...@@ -151,7 +176,7 @@ def close_reaction(obj): ...@@ -151,7 +176,7 @@ def close_reaction(obj):
def annotate(*args, **kwargs): def annotate(*args, **kwargs):
def decorate(func): def decorate(func):
ANNOTATIONS[func] = Annotation(args, kwargs) ANNOTATIONS[func] = Annotation(args, kwargs, func)
return func return func
for arg in chain(args, kwargs.values()): for arg in chain(args, kwargs.values()):
......
...@@ -10,3 +10,8 @@ description-file = README.rst ...@@ -10,3 +10,8 @@ description-file = README.rst
[flake8] [flake8]
exclude = _version.py exclude = _version.py
max-complexity = 10 max-complexity = 10
[doc8]
max-line-length=99
verbose=1
...@@ -9,9 +9,10 @@ setup( ...@@ -9,9 +9,10 @@ setup(
author_email='clint.northwood@gmail.com', author_email='clint.northwood@gmail.com',
description='inject dependencies', description='inject dependencies',
packages=find_packages(), packages=find_packages(),
install_requires=[], install_requires=[
"cached_property"
],
extras_require={ extras_require={
':python_version=="3.3"': ['asyncio'],
}, },
classifiers=[ classifiers=[
"Development Status :: 5 - Production/Stable", "Development Status :: 5 - Production/Stable",
...@@ -19,9 +20,7 @@ setup( ...@@ -19,9 +20,7 @@ setup(
"License :: OSI Approved :: MIT License", "License :: OSI Approved :: MIT License",
"Operating System :: OS Independent", "Operating System :: OS Independent",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries",
"Topic :: Software Development :: Libraries :: Python Modules" "Topic :: Software Development :: Libraries :: Python Modules"
], ],
......
import pytest import pytest
from knighted import Injector, annotate from knighted import Injector, annotate
import asyncio
from contextlib import contextmanager
from time import time_ns, sleep
class Timer:
started_at = None
stoped_at = None
duration = None
@contextmanager
def timed():
timer = Timer()
timer.started_at = time_ns()
yield timer
timer.stoped_at = time_ns()
timer.duration = timer.stoped_at - timer.started_at
@pytest.mark.asyncio @pytest.mark.asyncio
def test_instance_factory(): async def test_instance_factory():
class MyInjector(Injector): class MyInjector(Injector):
pass pass
...@@ -14,13 +32,13 @@ def test_instance_factory(): ...@@ -14,13 +32,13 @@ def test_instance_factory():
return 'I am foo' return 'I am foo'
@services.factory('bar') @services.factory('bar')
def bar_factory(): async def bar_factory():
return 'I am bar' return 'I am bar'
@services.factory('all') @services.factory('all')
def together_factory(): async def together_factory():
foo = yield from services.get('foo') foo = await services.get('foo')
bar = yield from services.get('bar') bar = await services.get('bar')
return [foo, bar] return [foo, bar]
@annotate('foo', 'bar') @annotate('foo', 'bar')
...@@ -28,15 +46,98 @@ def test_instance_factory(): ...@@ -28,15 +46,98 @@ def test_instance_factory():
return {'foo': foo, return {'foo': foo,
'bar': bar} 'bar': bar}
assert (yield from services.get('foo')) == 'I am foo' @annotate('foo', 'bar')
assert (yield from services.get('bar')) == 'I am bar' async def awaitable_fun(foo, bar):
assert (yield from services.get('all')) == ['I am foo', 'I am bar'] return {'foo': foo,
assert (yield from services.apply(fun)) == {'foo': 'I am foo', 'bar': bar}
'bar': 'I am bar'}
assert (await services.get('foo')) == 'I am foo'
assert (await services.get('bar')) == 'I am bar'
assert (await services.get('all')) == ['I am foo', 'I am bar']
# synchroneous
assert (await services.apply(fun)) == {'foo': 'I am foo',
'bar': 'I am bar'}
# asynchroneous
assert (await services.apply(awaitable_fun)) == {'foo': 'I am foo',
'bar': 'I am bar'}
@pytest.mark.asyncio @pytest.mark.asyncio
def test_partial(): async def test_fill_the_gaps():
class MyInjector(Injector):
pass
services = MyInjector()
@services.factory('foo:1')
def foo_factory():
return 'I am foo'
@services.factory('bar:2')
async def bar_factory():
return 'I am bar'
@services.factory('all')
async def together_factory():
foo = await services.get('foo:1')
bar = await services.get('bar:2')
return [foo, bar]
@annotate('foo:1', 'bar:2')
def fun(foo, bar):
return {'foo': foo,
'bar': bar}
assert (await services.get('foo:1')) == 'I am foo'
assert (await services.get('bar:2')) == 'I am bar'
assert (await services.apply(fun)) == {'foo': 'I am foo',
'bar': 'I am bar'}
assert (await services.apply(fun, "obviously")) == {'foo': 'obviously',
'bar': 'I am bar'}
assert (await services.apply(fun, "obviously", "not")) == {'foo': 'obviously',
'bar': 'not'}
assert (await services.apply(fun, foo="sometimes")) == {'foo': 'sometimes',
'bar': 'I am bar'}
assert (await services.apply(fun, bar="I feel")) == {'foo': 'I am foo',
'bar': 'I feel'}
@pytest.mark.slow
@pytest.mark.asyncio
async def test_load_in_parallel():
class MyInjector(Injector):
pass
services = MyInjector()
@services.factory('foo')
def foo_factory():
sleep(1)
return 'I am foo'
@services.factory('bar')
async def bar_factory():
await asyncio.sleep(1)
return 'I am bar'
@services.factory('baz')
async def baz_factory():
await asyncio.sleep(1)
return 'I am baz'
@annotate('foo', 'bar', 'baz')
def fun(foo, bar, baz):
return {'foo': foo,
'bar': bar,
'baz': baz}
with timed() as timer:
await services.apply(fun)
assert timer.duration == pytest.approx(1000000000, rel=1e-2)
@pytest.mark.asyncio
async def test_partial():
class MyInjector(Injector): class MyInjector(Injector):
pass pass
...@@ -58,12 +159,12 @@ def test_partial(): ...@@ -58,12 +159,12 @@ def test_partial():
assert len(services.services) == 0 assert len(services.services) == 0
part = services.partial(fun) part = services.partial(fun)
assert len(services.services) == 0 assert len(services.services) == 0
assert (yield from part()) == {'foo': 'I am foo', 'bar': 'I am bar'} assert (await part()) == {'foo': 'I am foo', 'bar': 'I am bar'}
assert len(services.services) == 2 assert len(services.services) == 2
@pytest.mark.asyncio @pytest.mark.asyncio
def test_class_factory(): async def test_class_factory():
class MyInjector(Injector): class MyInjector(Injector):
pass pass
...@@ -76,9 +177,9 @@ def test_class_factory(): ...@@ -76,9 +177,9 @@ def test_class_factory():
return 'I am bar' return 'I am bar'
@MyInjector.factory('all') @MyInjector.factory('all')
def together_factory(): async def together_factory():
foo = yield from services.get('foo') foo = await services.get('foo')
bar = yield from services.get('bar') bar = await services.get('bar')
return [foo, bar] return [foo, bar]
@annotate('foo', 'bar') @annotate('foo', 'bar')
...@@ -88,22 +189,22 @@ def test_class_factory(): ...@@ -88,22 +189,22 @@ def test_class_factory():
services = MyInjector() services = MyInjector()
assert (yield from services.get('foo')) == 'I am foo' assert (await services.get('foo')) == 'I am foo'
assert (yield from services.get('bar')) == 'I am bar' assert (await services.get('bar')) == 'I am bar'
assert (yield from services.get('all')) == ['I am foo', 'I am bar'] assert (await services.get('all')) == ['I am foo', 'I am bar']
assert (yield from services.apply(fun)) == {'foo': 'I am foo', assert (await services.apply(fun)) == {'foo': 'I am foo',
'bar': 'I am bar'} 'bar': 'I am bar'}
@pytest.mark.asyncio @pytest.mark.asyncio
def test_undefined_service_error(): async def test_undefined_service_error():
class MyInjector(Injector): class MyInjector(Injector):
pass pass
services = MyInjector() services = MyInjector()
with pytest.raises(ValueError): with pytest.raises(ValueError):
yield from services.get('foo') await services.get('foo')
def test_annotate_error(): def test_annotate_error():
...@@ -113,7 +214,7 @@ def test_annotate_error(): ...@@ -113,7 +214,7 @@ def test_annotate_error():
@pytest.mark.asyncio @pytest.mark.asyncio
def test_kw_apply(): async def test_kw_apply():
class MyInjector(Injector): class MyInjector(Injector):
pass pass
...@@ -130,12 +231,12 @@ def test_kw_apply(): ...@@ -130,12 +231,12 @@ def test_kw_apply():
def fun2(baz): def fun2(baz):
return 'nor %s' % baz return 'nor %s' % baz
assert (yield from services.apply(fun, bar='baz')) == 'I am bar, not baz' assert (await services.apply(fun, bar='baz')) == 'I am bar, not baz'
assert (yield from services.apply(fun2, 'baz')) == 'nor baz' assert (await services.apply(fun2, 'baz')) == 'nor baz'
@pytest.mark.asyncio @pytest.mark.asyncio
def test_late_register(): async def test_late_register():
class MyInjector(Injector): class MyInjector(Injector):
pass pass
...@@ -145,11 +246,11 @@ def test_late_register(): ...@@ -145,11 +246,11 @@ def test_late_register():
services = MyInjector() services = MyInjector()
services.factory('foo', factory_foo) services.factory('foo', factory_foo)
assert (yield from services.get('foo')) == 'I am foo' assert (await services.get('foo')) == 'I am foo'
@pytest.mark.asyncio @pytest.mark.asyncio
def test_sub_factory(): async def test_sub_factory():
class MyInjector(Injector): class MyInjector(Injector):
pass pass
...@@ -163,12 +264,12 @@ def test_sub_factory(): ...@@ -163,12 +264,12 @@ def test_sub_factory():
def bar_factory(): def bar_factory():
return 'I am bar' return 'I am bar'
assert (yield from services.get('foo')) == 'I am foo' assert (await services.get('foo')) == 'I am foo'
assert (yield from services.get('foo:bar')) == 'I am bar' assert (await services.get('foo:bar')) == 'I am bar'
@pytest.mark.asyncio @pytest.mark.asyncio
def test_close(): async def test_close():
class MyInjector(Injector): class MyInjector(Injector):
pass pass
...@@ -183,8 +284,8 @@ def test_close(): ...@@ -183,8 +284,8 @@ def test_close():
return 'I am bar' return 'I am bar'
assert len(services.services) == 0 assert len(services.services) == 0
yield from services.get('foo') await services.get('foo')
yield from services.get('foo:bar') await services.get('foo:bar')
assert len(services.services) == 2 assert len(services.services) == 2
services.close() services.close()
assert len(services.services) == 0 assert len(services.services) == 0
...@@ -196,7 +297,6 @@ def test_close_register(): ...@@ -196,7 +297,6 @@ def test_close_register():
services = MyInjector() services = MyInjector()
class Foo: class Foo:
def __init__(self): def __init__(self):
...@@ -206,6 +306,7 @@ def test_close_register(): ...@@ -206,6 +306,7 @@ def test_close_register():
self.value = value self.value = value
foo = Foo() foo = Foo()
def reaction(obj): def reaction(obj):
obj.set_value('baz') obj.set_value('baz')
services.close.register(foo, reaction=reaction) services.close.register(foo, reaction=reaction)
...@@ -219,7 +320,6 @@ def test_close_unregister(): ...@@ -219,7 +320,6 @@ def test_close_unregister():
services = MyInjector() services = MyInjector()
class Foo: class Foo:
def __init__(self): def __init__(self):
...@@ -229,6 +329,7 @@ def test_close_unregister(): ...@@ -229,6 +329,7 @@ def test_close_unregister():
self.value = value self.value = value
foo = Foo() foo = Foo()
def reaction(obj): def reaction(obj):
obj.set_value('baz') obj.set_value('baz')
services.close.register(foo, reaction=reaction) services.close.register(foo, reaction=reaction)
......