Commit a5c66706 authored by xa's avatar xa

Merge branch 'py3.7' into 'master'

Py3.7

See merge request !1
parents 8026654e 2bd123d7
Pipeline #1040 failed with stage
in 118 minutes and 23 seconds
layout python3
......@@ -57,3 +57,6 @@ docs/_build/
# PyBuilder
target/
.mypy_cache/
.pytest_cache/
.vscode/
\ No newline at end of file
......@@ -4,17 +4,11 @@ before_script:
- python -m pip install -e .
- python -m pip install -r requirements-test.txt
python3.3 tests:
python3 tests:
script:
- py.test --cov knighted --cov-report term-missing tests/
tags:
- python3.3
python3.4 tests:
script:
- py.test --cov knighted --cov-report term-missing tests/
tags:
- python3.4
- python3
publish to pypi:
type: deploy
......@@ -23,7 +17,7 @@ publish to pypi:
- python setup.py sdist bdist_wheel
- twine upload -u $PYPI_USER -p $PYPI_PASSWORD dist/*
tags:
- python3.4
- python3
only:
- /^v[\d\.]+.*$/
allow_failure: true
......@@ -36,6 +36,62 @@ For example::
assert (yield from services.apply(fun)) == {'foo': 'I am foo',
'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
--------------
......
from __future__ import annotations
import asyncio
import concurrent.futures
import logging
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 weakref import WeakKeyDictionary
from functools import wraps
logger = logging.getLogger(__name__)
from cached_property import cached_property
logger = logging.getLogger("knighted")
class Factory:
......@@ -16,7 +22,7 @@ class Factory:
def __call__(self, note, func=None):
def decorate(func):
self.target.factories[note] = asyncio.coroutine(func)
self.target.factories[note] = func
return func
if func:
return decorate(func)
......@@ -59,7 +65,6 @@ class CloseHandler:
reaction = reaction or close_reaction
reactions = self.registry.setdefault(obj, set())
reactions.add(reaction)
print('reactions are', obj, reactions)
def unregister(self, obj, reaction=None):
"""Unregister callbacks that should not be thrown on close.
......@@ -93,55 +98,75 @@ class Injector(metaclass=ABCMeta):
self.reactions = defaultdict(WeakKeyDictionary)
self.close = CloseHandler(self)
@asyncio.coroutine
def get(self, note):
@cached_property
def executor(self):
return concurrent.futures.ThreadPoolExecutor(max_workers=10)
async def get(self, note):
if note in self.services:
return self.services[note]
for fact, args in note_loop(note):
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)
self.services[note] = instance
return instance
raise ValueError('%r is not defined' % note)
@asyncio.coroutine
def apply(self, *args, **kwargs):
async def apply(self, *args, **kwargs):
func, *args = args
response = yield from self.partial(func)(*args, **kwargs)
response = await self.partial(func)(*args, **kwargs)
return response
def partial(self, func):
"""Resolves lately dependancies.
"""Resolves lately dependencies.
Returns:
callable: the service partially resolved
"""
@wraps(func)
@asyncio.coroutine
def wrapper(*args, **kwargs):
async 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)
annotation = ANNOTATIONS[func]
given = annotation.given(*args, **kwargs)
to_load = {}
for key, note in annotation.marked.items():
if key not in given:
to_load[key] = asyncio.create_task(self.get(note))
for key, fut in to_load.items():
to_load[key] = await fut
kwargs.update(to_load)
result = func(*args, **kwargs)
if asyncio.iscoroutine(result):
result = await result
return result
logger.warning('%r is not annoted', func)
return func(*args, **kwargs)
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):
......@@ -151,7 +176,7 @@ def close_reaction(obj):
def annotate(*args, **kwargs):
def decorate(func):
ANNOTATIONS[func] = Annotation(args, kwargs)
ANNOTATIONS[func] = Annotation(args, kwargs, func)
return func
for arg in chain(args, kwargs.values()):
......
......@@ -10,3 +10,8 @@ description-file = README.rst
[flake8]
exclude = _version.py
max-complexity = 10
[doc8]
max-line-length=99
verbose=1
......@@ -9,9 +9,10 @@ setup(
author_email='clint.northwood@gmail.com',
description='inject dependencies',
packages=find_packages(),
install_requires=[],
install_requires=[
"cached_property"
],
extras_require={
':python_version=="3.3"': ['asyncio'],
},
classifiers=[
"Development Status :: 5 - Production/Stable",
......@@ -19,9 +20,7 @@ setup(
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.7",
"Topic :: Software Development :: Libraries",
"Topic :: Software Development :: Libraries :: Python Modules"
],
......
import pytest
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
def test_instance_factory():
async def test_instance_factory():
class MyInjector(Injector):
pass
......@@ -14,13 +32,13 @@ def test_instance_factory():
return 'I am foo'
@services.factory('bar')
def bar_factory():
async 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')
async def together_factory():
foo = await services.get('foo')
bar = await services.get('bar')
return [foo, bar]
@annotate('foo', 'bar')
......@@ -28,15 +46,98 @@ def test_instance_factory():
return {'foo': foo,
'bar': bar}
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.apply(fun)) == {'foo': 'I am foo',
'bar': 'I am bar'}
@annotate('foo', 'bar')
async def awaitable_fun(foo, bar):
return {'foo': foo,
'bar': 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
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):
pass
......@@ -58,12 +159,12 @@ def test_partial():
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 (await part()) == {'foo': 'I am foo', 'bar': 'I am bar'}
assert len(services.services) == 2
@pytest.mark.asyncio
def test_class_factory():
async def test_class_factory():
class MyInjector(Injector):
pass
......@@ -76,9 +177,9 @@ def test_class_factory():
return 'I am bar'
@MyInjector.factory('all')
def together_factory():
foo = yield from services.get('foo')
bar = yield from services.get('bar')
async def together_factory():
foo = await services.get('foo')
bar = await services.get('bar')
return [foo, bar]
@annotate('foo', 'bar')
......@@ -88,22 +189,22 @@ def test_class_factory():
services = MyInjector()
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.apply(fun)) == {'foo': 'I am foo',
'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']
assert (await services.apply(fun)) == {'foo': 'I am foo',
'bar': 'I am bar'}
@pytest.mark.asyncio
def test_undefined_service_error():
async def test_undefined_service_error():
class MyInjector(Injector):
pass
services = MyInjector()
with pytest.raises(ValueError):
yield from services.get('foo')
await services.get('foo')
def test_annotate_error():
......@@ -113,7 +214,7 @@ def test_annotate_error():
@pytest.mark.asyncio
def test_kw_apply():
async def test_kw_apply():
class MyInjector(Injector):
pass
......@@ -130,12 +231,12 @@ def test_kw_apply():
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'
assert (await services.apply(fun, bar='baz')) == 'I am bar, not baz'
assert (await services.apply(fun2, 'baz')) == 'nor baz'
@pytest.mark.asyncio
def test_late_register():
async def test_late_register():
class MyInjector(Injector):
pass
......@@ -145,11 +246,11 @@ def test_late_register():
services = MyInjector()
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
def test_sub_factory():
async def test_sub_factory():
class MyInjector(Injector):
pass
......@@ -163,12 +264,12 @@ def test_sub_factory():
def bar_factory():
return 'I am bar'
assert (yield from services.get('foo')) == 'I am foo'
assert (yield from services.get('foo:bar')) == 'I am bar'
assert (await services.get('foo')) == 'I am foo'
assert (await services.get('foo:bar')) == 'I am bar'
@pytest.mark.asyncio
def test_close():
async def test_close():
class MyInjector(Injector):
pass
......@@ -183,8 +284,8 @@ def test_close():
return 'I am bar'
assert len(services.services) == 0
yield from services.get('foo')
yield from services.get('foo:bar')
await services.get('foo')
await services.get('foo:bar')
assert len(services.services) == 2
services.close()
assert len(services.services) == 0
......@@ -196,7 +297,6 @@ def test_close_register():
services = MyInjector()
class Foo:
def __init__(self):
......@@ -206,6 +306,7 @@ def test_close_register():
self.value = value
foo = Foo()
def reaction(obj):
obj.set_value('baz')
services.close.register(foo, reaction=reaction)
......@@ -219,7 +320,6 @@ def test_close_unregister():
services = MyInjector()
class Foo:
def __init__(self):
......@@ -229,6 +329,7 @@ def test_close_unregister():
self.value = value
foo = Foo()
def reaction(obj):
obj.set_value('baz')
services.close.register(foo, reaction=reaction)
......
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