Merge pull request #142 from ets-labs/138_cython_injections

Add injections extension
This commit is contained in:
Roman 2016-11-03 01:02:52 +02:00 committed by GitHub
commit 55cde4729d
44 changed files with 325 additions and 277 deletions

View File

@ -1,6 +1,7 @@
[run]
include = dependency_injector/*
omit = tests/*
source = src/dependency_injector
omit = tests/unit
plugins = Cython.Coverage
[html]
directory=reports/unittests/

8
.gitignore vendored
View File

@ -63,7 +63,7 @@ venv/
.ropeproject/
# C extensions
dependency_injector/*.c
dependency_injector/*.so
dependency_injector/providers/*.c
dependency_injector/providers/*.so
src/dependency_injector/*.c
src/dependency_injector/*.so
src/dependency_injector/providers/*.c
src/dependency_injector/providers/*.so

View File

@ -2,7 +2,7 @@
# Add <file or directory> to the black list. It should be a base name, not a
# path. You may set this option multiple times.
ignore=utils,test
ignore=utils,tests
[MESSAGES CONTROL]

View File

@ -1,18 +1,22 @@
sudo: false
install:
- pip install tox
- pip install cython
- make cythonize
script:
- tox
language: python
install: pip install tox
script: tox
python:
- 3.5
- 3.5
env:
- TOXENV=coveralls
- TOXENV=pylint
- TOXENV=flake8
- TOXENV=pydocstyle
- TOXENV=py26
- TOXENV=py27
- TOXENV=py33
- TOXENV=py34
- TOXENV=py35
- TOXENV=pypy
- TOXENV=pypy3
- TOXENV=coveralls DEPENDENCY_INJECTOR_DEBUG_MODE=1
- TOXENV=pylint
- TOXENV=flake8
- TOXENV=pydocstyle
- TOXENV=py26
- TOXENV=py27
- TOXENV=py33
- TOXENV=py34
- TOXENV=py35
- TOXENV=pypy
- TOXENV=pypy3

View File

@ -1,4 +1,4 @@
include dependency_injector/*
recursive-include src/dependency_injector *.py *.pyx *.pxd *.c
include README.rst
include CONTRIBUTORS.rst
include LICENSE.rst

View File

@ -1,11 +1,22 @@
VERSION:=$(shell python setup.py --version)
VERSION := $(shell python setup.py --version)
CYTHON_SRC := $(shell find src/dependency_injector -name '*.pyx')
CYTHON_DIRECTIVES =
ifdef DEPENDENCY_INJECTOR_DEBUG_MODE
CYTHON_DIRECTIVES += -Xprofile=True
CYTHON_DIRECTIVES += -Xlinetrace=True
endif
clean:
# Clean sources
find dependency_injector -name '*.py[co]' -delete
find dependency_injector -name '__pycache__' -delete
find dependency_injector -name '*.c' -delete
find dependency_injector -name '*.so' -delete
find src -name '*.py[cod]' -delete
find src -name '__pycache__' -delete
find src -name '*.c' -delete
find src -name '*.so' -delete
find src -name '*.html' -delete
# Clean tests
find tests -name '*.py[co]' -delete
find tests -name '__pycache__' -delete
@ -13,21 +24,39 @@ clean:
find examples -name '*.py[co]' -delete
find examples -name '__pycache__' -delete
tests: clean
cythonize:
# Compile Cython to C
cython -a $(CYTHON_DIRECTIVES) $(CYTHON_SRC)
# Move all Cython html reports
mkdir -p reports/cython/
find src -name '*.html' -exec mv {} reports/cython/ \;
build: clean cythonize
# Compile C extensions
python setup.py build_ext --inplace
install: uninstall clean cythonize
pip install -ve .
uninstall:
- pip uninstall -y -q dependency-injector 2> /dev/null
test: build
# Unit tests with coverage report
coverage erase
coverage run --rcfile=./.coveragerc -m unittest2 discover tests
coverage run --rcfile=./.coveragerc -m unittest2 discover tests/unit
coverage report --rcfile=./.coveragerc
coverage html --rcfile=./.coveragerc
coverage erase
check:
# Static analysis
flake8 --max-complexity=10 dependency_injector/
flake8 --max-complexity=10 src/dependency_injector/
flake8 --max-complexity=10 examples/
# Code style analysis
pydocstyle dependency_injector/
pydocstyle src/dependency_injector/
pydocstyle examples/
publish: clean
publish: cythonize
# Create and upload build
python setup.py sdist upload
# Create and upload tag

View File

@ -56,7 +56,7 @@ author = u'ETS Labs'
#
# The short X.Y version.
# Getting version:
with open('../dependency_injector/__init__.py') as init_file:
with open('../src/dependency_injector/__init__.py') as init_file:
version = re.search('VERSION = \'(.*?)\'', init_file.read()).group(1)
# The full version, including alpha/beta/rc tags.
@ -281,7 +281,7 @@ man_pages = [
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'Dependency Injector', u'Dependency Injector Documentation',
author, 'Dependency Injector', 'Python dependency injection framework',
author, 'Dependency Injector', 'Dependency injection microframework for Python',
'Miscellaneous'),
]

View File

@ -9,8 +9,14 @@ follows `Semantic versioning`_
Development version
-------------------
- Add ``dependency_injector.injections`` module (C extension).
- Remove ``@inject`` decorator.
- Add makefile (``clean``, ``tests`` & ``publish`` commands).
- Add makefile (``clean``, ``test``, ``build``, ``install``, ``uninstall``
& ``publish`` commands).
- Update repository structure:
- Sources are moved under ``src``.
- Tests are moved under ``tests/unit``.
.. - No features.

View File

@ -1,33 +0,0 @@
"""`inject()` decorator and Flask view example."""
import sqlite3
import flask
import dependency_injector.providers as providers
import dependency_injector.injections as injections
database = providers.Singleton(sqlite3.connect,
':memory:',
timeout=30,
detect_types=True,
isolation_level='EXCLUSIVE')
app = flask.Flask(__name__)
@app.route('/')
@injections.inject(database)
@injections.inject(flask.request)
def hello(request, database):
"""Example Flask view."""
print request
one = database.execute('SELECT 1').fetchone()[0]
return 'Query returned {0}, db connection {1}'.format(one, database)
if __name__ == '__main__':
app.run()
# Example output of "GET / HTTP/1.1" is:
# Query returned 1, db connection <sqlite3.Connection object at 0x1057e4030>

View File

@ -1,43 +0,0 @@
"""`inject()` decorator with classes example."""
import sqlite3
import flask
import flask.views
import dependency_injector.providers as providers
import dependency_injector.injections as injections
database = providers.Singleton(sqlite3.Connection,
database=':memory:',
timeout=30,
detect_types=True,
isolation_level='EXCLUSIVE')
app = flask.Flask(__name__)
@injections.inject(database=database)
@injections.inject(some_setting=777)
class HelloView(flask.views.View):
"""Example flask class-based view."""
def __init__(self, database, some_setting):
"""Initializer."""
self.database = database
self.some_setting = some_setting
def dispatch_request(self):
"""Handle example request."""
one = self.database.execute('SELECT 1').fetchone()[0]
one *= self.some_setting
return 'Query returned {0}, db connection {1}'.format(one, database)
app.add_url_rule('/', view_func=HelloView.as_view('hello_view'))
if __name__ == '__main__':
app.run()
# Example output of "GET / HTTP/1.1" is:
# Query returned 777, db connection <sqlite3.Connection object at 0x1057e4030>

View File

@ -1,28 +0,0 @@
"""`inject()` decorator simple example."""
import dependency_injector.providers as providers
import dependency_injector.injections as injections
dependency_injector_factory = providers.Factory(object)
# Example of using `inject()` decorator keyword argument injections:
@injections.inject(new_object=dependency_injector_factory)
@injections.inject(some_setting=1334)
def example_callback1(new_object, some_setting):
"""Example callback that does some asserts for input args."""
assert isinstance(new_object, object)
assert some_setting == 1334
# Example of using `inject()` decorator with positional argument injections:
@injections.inject(dependency_injector_factory, 1334)
def example_callback2(new_object, some_setting):
"""Example callback that does some asserts for input args."""
assert isinstance(new_object, object)
assert some_setting == 1334
example_callback1()
example_callback2()

View File

@ -30,7 +30,7 @@ if __name__ == '__main__':
class _SequenceContainer1(SequencesContainer):
object_provider = providers.Factory(object)
except errors.Error as exception:
print exception
print(exception)
# <class '__main__._SequenceContainer1'> can contain only
# <class '__main__.SequenceProvider'> instances
@ -38,7 +38,7 @@ if __name__ == '__main__':
class _SequenceContainer2(SequencesContainer):
object_provider = SequenceProvider(object)
except errors.Error as exception:
print exception
print(exception)
# <class '__main__.SequenceProvider'> can provide only
# <class '_abcoll.Sequence'> instances

View File

@ -24,7 +24,7 @@ if __name__ == '__main__':
try:
sequences_container.object_provider = providers.Factory(object)
except errors.Error as exception:
print exception
print(exception)
# <dependency_injector.containers.DynamicContainer object at
# 0x107820ed0> can contain only <class '__main__.SequenceProvider'>
# instances
@ -32,7 +32,7 @@ if __name__ == '__main__':
try:
sequences_container.object_provider = SequenceProvider(object)
except errors.Error as exception:
print exception
print(exception)
# <class '__main__.SequenceProvider'> can provide only
# <class '_abcoll.Sequence'> instances

View File

@ -11,5 +11,5 @@ class ApiClient(object):
def call(self, operation, data):
"""Make some network operations."""
print 'API call [{0}:{1}], method - {2}, data - {3}'.format(
self.host, self.api_key, operation, repr(data))
print('API call [{0}:{1}], method - {2}, data - {3}'.format(
self.host, self.api_key, operation, repr(data)))

View File

@ -1,40 +0,0 @@
"""Pythonic way for Dependency Injection - Auth System."""
from dependency_injector import providers
from dependency_injector import injections
@providers.DelegatedCallable
def get_user_info(user_id):
"""Return user info."""
raise NotImplementedError()
@providers.Factory
@injections.inject(get_user_info=get_user_info)
class AuthComponent(object):
"""Some authentication component."""
def __init__(self, get_user_info):
"""Initializer."""
self.get_user_info = get_user_info
def authenticate_user(self, token):
"""Authenticate user by token."""
user_info = self.get_user_info(user_id=token + '1')
return user_info
print AuthComponent
print get_user_info
@providers.override(get_user_info)
@providers.DelegatedCallable
def get_user_info(user_id):
"""Return user info."""
return {'user_id': user_id}
print AuthComponent().authenticate_user(token='abc')
# {'user_id': 'abc1'}

View File

@ -1,66 +0,0 @@
"""Pythonic way for Dependency Injection - callback-based IoC container."""
import sqlite3
from dependency_injector import containers
from dependency_injector import providers
from dependency_injector import injections
class UsersService(object):
"""Users service, that has dependency on database."""
def __init__(self, db):
"""Initializer."""
self.db = db
class AuthService(object):
"""Auth service, that has dependencies on users service and database."""
def __init__(self, db, users_service):
"""Initializer."""
self.db = db
self.users_service = users_service
class Services(containers.DeclarativeContainer):
"""IoC container of service providers."""
@providers.Singleton
def database():
"""Provide database connection.
:rtype: sqlite3.Connection
"""
return sqlite3.connect(':memory:')
@providers.Factory
@injections.inject(db=database)
def users(**kwargs):
"""Provide users service.
:rtype: UsersService
"""
return UsersService(**kwargs)
@providers.Factory
@injections.inject(db=database)
@injections.inject(users_service=users)
def auth(**kwargs):
"""Provide users service.
:rtype: AuthService
"""
return AuthService(**kwargs)
# Retrieving services:
users_service = Services.users()
auth_service = Services.auth()
# Making some asserts:
assert users_service.db is auth_service.db is Services.database()
assert isinstance(auth_service.users_service, UsersService)
assert users_service is not Services.users()
assert auth_service is not Services.auth()

View File

@ -7,9 +7,9 @@ import dependency_injector.providers as providers
even_filter = providers.Callable(filter, lambda x: x % 2 == 0)
odd_filter = providers.Callable(filter, lambda x: x % 2 != 0)
# Creating even and odd ranges using xrange() and filter providers:
even_range = even_filter(xrange(1, 10))
odd_range = odd_filter(xrange(1, 10))
# Creating even and odd ranges using range() and filter providers:
even_range = even_filter(range(1, 10))
odd_range = odd_filter(range(1, 10))
# Making some asserts:
assert even_range == [2, 4, 6, 8]

View File

@ -25,6 +25,6 @@ some_service_provider = ServiceProvider(SomeService)
try:
some_service_provider = ServiceProvider(object)
except errors.Error as exception:
print exception
print(exception)
# <class '__main__.ServiceProvider'> can provide only
# <class '__main__.BaseService'> instances

View File

@ -30,6 +30,6 @@ photos_service_provider = ServiceProvider(PhotosService)
try:
some_service_provider = ServiceProvider(object)
except errors.Error as exception:
print exception
print(exception)
# <class '__main__.ServiceProvider'> can provide only
# <class '__main__.BaseService'> instances

View File

@ -28,7 +28,7 @@ thread_factory = providers.Factory(threading.Thread,
if __name__ == '__main__':
# Create 10 threads for concurrent execution of example():
threads = []
for thread_number in xrange(10):
for thread_number in range(10):
threads.append(thread_factory(name='Thread{0}'.format(thread_number)))
# Start execution of all created threads:

View File

@ -1,3 +1,4 @@
cython
tox
unittest2
sphinx

View File

@ -1,10 +1,14 @@
"""`Dependency injector` setup script."""
import os
import re
from setuptools import setup
from setuptools import setup, Extension
# Defining setup variables:
defined_macros = list()
# Getting description:
with open('README.rst') as readme_file:
description = readme_file.read()
@ -14,9 +18,14 @@ with open('requirements.txt') as version:
requirements = version.readlines()
# Getting version:
with open('dependency_injector/__init__.py') as init_file:
with open('src/dependency_injector/__init__.py') as init_file:
version = re.search('VERSION = \'(.*?)\'', init_file.read()).group(1)
# Adding debug options:
if os.environ.get('DEPENDENCY_INJECTOR_DEBUG_MODE') == '1':
defined_macros.append(('CYTHON_TRACE', 1))
defined_macros.append(('CYTHON_TRACE_NOGIL', 1))
setup(name='dependency-injector',
version=version,
@ -27,15 +36,27 @@ setup(name='dependency-injector',
maintainer='Roman Mogilatov',
maintainer_email='rmogilatov@gmail.com',
url='https://github.com/ets-labs/python-dependency-injector',
bugtrack_url='https://github.com/ets-labs/python-dependency-injector' +
'/issues',
download_url='https://pypi.python.org/pypi/dependency_injector',
license='BSD New',
packages=['dependency_injector',
'dependency_injector.providers'],
platforms=['any'],
zip_safe=True,
install_requires=requirements,
packages=[
'dependency_injector',
'dependency_injector.providers',
],
package_dir={
'': 'src',
},
ext_modules=[
Extension('dependency_injector.injections',
['src/dependency_injector/injections.c'],
define_macros=defined_macros,
extra_compile_args=['-O2']),
],
package_data={
'dependency_injector': ['*.pxd'],
},
zip_safe=True,
license='BSD New',
platforms=['any'],
keywords=[
'DI',
'Dependency injection',

View File

@ -0,0 +1,63 @@
"""Dependency injector injections.
Cython optimized code.
"""
cpdef tuple parse_positional_injections(tuple args)
cpdef tuple parse_named_injections(dict kwargs)
cdef class Injection:
pass
cdef class PositionalInjection(Injection):
cdef object __value
cdef int __is_provider
cdef int __is_delegated
cdef int __call
cdef inline object __get_value(self):
if self.__call == 0:
return self.__value
return self.__value()
cdef class NamedInjection(Injection):
cdef object __name
cdef object __value
cdef int __is_provider
cdef int __is_delegated
cdef int __call
cdef inline object __get_name(self):
return self.__name
cdef inline object __get_value(self):
if self.__call == 0:
return self.__value
return self.__value()
cdef inline tuple __provide_positional_args(tuple inj_args,
int inj_args_len,
tuple args):
cdef PositionalInjection injection
if inj_args_len > 0:
positional_args = list()
for index in range(inj_args_len):
injection = <PositionalInjection>inj_args[index]
positional_args.append(injection.get_value())
positional_args.extend(args)
args = positional_args
return args
cdef inline dict __provide_keyword_args(tuple inj_kwargs,
int inj_kwargs_len,
dict kwargs):
cdef NamedInjection kw_injection
if inj_kwargs_len > 0:
for index in range(inj_kwargs_len):
kw_injection = <NamedInjection>inj_kwargs[index]
kwargs[kw_injection.get_name()] = kw_injection.get_value()
return kwargs

View File

@ -0,0 +1,78 @@
"""Dependency injector injections.
Cython optimized code.
"""
# TODO: replace to cimport
from .utils import is_provider
cdef class Injection:
"""Abstract injection class."""
cdef class PositionalInjection(Injection):
"""Positional injection class."""
def __init__(self, value):
"""Initializer."""
self.__value = value
self.__is_provider = <int>is_provider(value)
self.__is_delegated = 0 # TODO: use utils.is_delegated()
self.__call = <int>self.__is_provider == 1 and self.__is_delegated == 0
def get_value(self):
"""Return injection value."""
return self.__get_value()
cdef class NamedInjection(Injection):
"""Keyword injection class."""
def __init__(self, name, value):
"""Initializer."""
self.__name = name
self.__value = value
self.__is_provider = <int>is_provider(value)
self.__is_delegated = 0 # TODO: use utils.is_delegated()
self.__call = <int>self.__is_provider == 1 and self.__is_delegated == 0
def get_name(self):
"""Return injection value."""
return self.__get_name()
def get_value(self):
"""Return injection value."""
return self.__get_value()
cpdef tuple parse_positional_injections(tuple args):
"""Parse positional injections."""
cdef list injections = list()
cdef int args_len = len(args)
cdef object arg
cdef int index
cdef PositionalInjection injection
for index in range(args_len):
arg = args[index]
injection = PositionalInjection(arg)
injections.append(injection)
return tuple(injections)
cpdef tuple parse_named_injections(dict kwargs):
"""Parse named injections."""
cdef list injections = list()
cdef object name
cdef object arg
cdef NamedInjection injection
for name, arg in kwargs.items():
injection = NamedInjection(name, arg)
injections.append(injection)
return tuple(injections)

2
tests/unit/.pydocstylerc Normal file
View File

@ -0,0 +1,2 @@
[pydocstyle]
ignore = D101,D102

View File

@ -0,0 +1,53 @@
"""Dependency injector injections unit tests."""
import unittest2 as unittest
from dependency_injector import injections
from dependency_injector import providers
class PositionalInjectionTests(unittest.TestCase):
def test_isinstance(self):
injection = injections.PositionalInjection(1)
self.assertIsInstance(injection, injections.Injection)
def test_get_value_with_not_provider(self):
injection = injections.PositionalInjection(123)
self.assertEquals(injection.get_value(), 123)
def test_get_value_with_factory(self):
injection = injections.PositionalInjection(providers.Factory(object))
obj1 = injection.get_value()
obj2 = injection.get_value()
self.assertIs(type(obj1), object)
self.assertIs(type(obj2), object)
self.assertIsNot(obj1, obj2)
class NamedInjectionTests(unittest.TestCase):
def test_isinstance(self):
injection = injections.NamedInjection('name', 1)
self.assertIsInstance(injection, injections.Injection)
def test_get_name(self):
injection = injections.NamedInjection('name', 123)
self.assertEquals(injection.get_name(), 'name')
def test_get_value_with_not_provider(self):
injection = injections.NamedInjection('name', 123)
self.assertEquals(injection.get_value(), 123)
def test_get_value_with_factory(self):
injection = injections.NamedInjection('name',
providers.Factory(object))
obj1 = injection.get_value()
obj2 = injection.get_value()
self.assertIs(type(obj1), object)
self.assertIs(type(obj2), object)
self.assertIsNot(obj1, obj2)

20
tox.ini
View File

@ -6,39 +6,39 @@ envlist=
deps=
unittest2
commands=
unit2 discover tests []
unit2 discover tests/unit
[testenv:coveralls]
basepython=python2.7
passenv=TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH
passenv=TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH DEPENDENCY_INJECTOR_DEBUG_MODE
basepython=python3.5
usedevelop=True
deps=
{[testenv]deps}
cython
coverage
coveralls
commands=
coverage erase
coverage run --rcfile=./.coveragerc -m unittest2 discover tests []
coverage run --rcfile=./.coveragerc -m unittest2 discover tests/unit
coverage report --rcfile=./.coveragerc
coveralls
[testenv:pylint]
basepython=python2.7
deps=
pylint
commands=
- pylint -f colorized --rcfile=./.pylintrc dependency_injector
- pylint -f colorized --rcfile=./.pylintrc src/dependency_injector
[testenv:flake8]
basepython=python2.7
deps=
flake8
commands=
flake8 --max-complexity=10 dependency_injector/
flake8 --max-complexity=10 src/dependency_injector/
flake8 --max-complexity=10 examples/
[testenv:pydocstyle]
basepython=python2.7
deps=
pydocstyle
commands=
pydocstyle dependency_injector/
pydocstyle src/dependency_injector/
pydocstyle examples/