├── tests ├── __init__.py ├── py3 │ ├── __init__.py │ ├── dependency_tests.py │ └── interface_tests.py ├── scanning │ ├── __init__.py │ ├── testmodule │ │ ├── __init__.py │ │ ├── testsubmodule │ │ │ ├── __init__.py │ │ │ └── submodule_registers.py │ │ ├── ignoredsubmodule │ │ │ ├── __init__.py │ │ │ └── ignored_registers.py │ │ ├── register_function.py │ │ ├── register_factory.py │ │ ├── register_instance.py │ │ └── plain_register.py │ └── scanning_tests.py └── all │ ├── __init__.py │ ├── categories_tests.py │ ├── providers_tests.py │ ├── scopes_tests.py │ ├── configuration_tests.py │ ├── dependency_tests.py │ └── graph_tests.py ├── example ├── guestbook │ ├── __init__.py │ ├── views.py │ ├── templates │ │ ├── home.html │ │ └── base.html │ ├── static │ │ └── base.css │ ├── response.py │ ├── wsgi.py │ ├── module.py │ └── application.py ├── .gitignore ├── setup.cfg ├── README.rst └── setup.py ├── MANIFEST.in ├── wiring ├── scanning │ ├── __init__.py │ ├── scan.py │ └── register.py ├── __init__.py ├── categories.py ├── scopes.py ├── providers.py ├── configuration.py ├── dependency.py ├── interface.py └── graph.py ├── docs ├── dependency_injection │ └── example1.png ├── api │ ├── wiring.rst │ ├── categories.rst │ ├── scanning │ │ ├── scan.rst │ │ └── register.rst │ ├── scopes.rst │ ├── dependency.rst │ ├── configuration.rst │ ├── providers.rst │ ├── graph.rst │ └── interface.rst ├── api.rst ├── installation.rst ├── _templates │ └── page.html ├── license.rst ├── glossary.rst ├── index.rst ├── interfaces.rst ├── Makefile ├── make.bat ├── conf.py └── rationale.rst ├── .gitignore ├── setup.cfg ├── .travis.yml ├── tox.ini ├── setup.py ├── README.rst ├── sphinx_wiring.py └── LICENSE /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/py3/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/scanning/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/guestbook/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/scanning/testmodule/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst LICENSE 2 | -------------------------------------------------------------------------------- /tests/scanning/testmodule/testsubmodule/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/scanning/testmodule/ignoredsubmodule/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wiring/scanning/__init__.py: -------------------------------------------------------------------------------- 1 | from wiring.scanning.scan import * # noqa 2 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | /*.egg 2 | /*.egg-info 3 | /environment 4 | /dist 5 | /build 6 | -------------------------------------------------------------------------------- /docs/dependency_injection/example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msiedlarek/wiring/HEAD/docs/dependency_injection/example1.png -------------------------------------------------------------------------------- /docs/api/wiring.rst: -------------------------------------------------------------------------------- 1 | wiring 2 | ====== 3 | 4 | .. automodule:: wiring 5 | 6 | .. autodata:: __title__ 7 | .. autodata:: __version__ 8 | -------------------------------------------------------------------------------- /example/guestbook/views.py: -------------------------------------------------------------------------------- 1 | from guestbook.response import TemplateResponse 2 | 3 | 4 | def home(request): 5 | return TemplateResponse('home.html') 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | __pycache__ 3 | /*.egg 4 | /*.egg-info 5 | /.tox 6 | /.coverage 7 | /docs/_build 8 | /coverage 9 | /environment 10 | /dist 11 | /build 12 | -------------------------------------------------------------------------------- /example/guestbook/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |

Guestbook

5 |

Welcome to our guestbook!

6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /docs/api/categories.rst: -------------------------------------------------------------------------------- 1 | wiring.categories 2 | ==================== 3 | 4 | .. automodule:: wiring.categories 5 | 6 | Category 7 | -------- 8 | 9 | .. autoclass:: Category 10 | -------------------------------------------------------------------------------- /example/guestbook/static/base.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | color: #333; 4 | font-size: 14px; 5 | } 6 | 7 | #content { 8 | width: 600px; 9 | margin: 0 auto 0 auto; 10 | } 11 | -------------------------------------------------------------------------------- /tests/scanning/testmodule/ignoredsubmodule/ignored_registers.py: -------------------------------------------------------------------------------- 1 | from wiring.scanning import register 2 | 3 | 4 | @register.function() 5 | def ignored_function(): 6 | pass 7 | 8 | 9 | @register.factory() 10 | class IgnoredFactory(): 11 | pass 12 | -------------------------------------------------------------------------------- /tests/scanning/testmodule/testsubmodule/submodule_registers.py: -------------------------------------------------------------------------------- 1 | from wiring.scanning import register 2 | 3 | 4 | @register.function() 5 | def submodule_function(): 6 | pass 7 | 8 | 9 | @register.factory() 10 | class SubmoduleFactory(): 11 | pass 12 | -------------------------------------------------------------------------------- /example/setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E127,E128 3 | 4 | [isort] 5 | multi_line_output = 3 6 | indent = 4 7 | lines_after_imports = 2 8 | known_standard_library = pkg_resources 9 | known_third_party = wiring,werkzeug,jinja2 10 | known_first_party = guestbook 11 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API Documentation 2 | ================= 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | api/wiring 8 | api/categories 9 | api/configuration 10 | api/dependency 11 | api/graph 12 | api/interface 13 | api/providers 14 | api/scopes 15 | api/scanning/register 16 | api/scanning/scan 17 | -------------------------------------------------------------------------------- /docs/api/scanning/scan.rst: -------------------------------------------------------------------------------- 1 | wiring.scanning.scan 2 | ==================== 3 | 4 | .. automodule:: wiring.scanning.scan 5 | 6 | scan_to_module 7 | -------------- 8 | 9 | .. autofunction:: scan_to_module 10 | 11 | scan_to_graph 12 | ------------- 13 | 14 | .. autofunction:: scan_to_graph 15 | 16 | scan 17 | ---- 18 | 19 | .. autofunction:: scan 20 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | 4 | [nosetests] 5 | nocapture=1 6 | cover-package=wiring,wiring.scanning 7 | cover-html=1 8 | cover-html-dir=coverage 9 | tests=tests/all 10 | 11 | [flake8] 12 | ignore = E127,E128 13 | exclude = tests/py3/* 14 | 15 | [isort] 16 | multi_line_output = 3 17 | indent = 4 18 | lines_after_imports = 2 19 | known_third_party = six,nose,venusian 20 | known_first_party = wiring 21 | -------------------------------------------------------------------------------- /tests/scanning/testmodule/register_function.py: -------------------------------------------------------------------------------- 1 | from wiring.scanning import register 2 | 3 | 4 | @register.function() 5 | def register_function(): 6 | pass 7 | 8 | 9 | @register.function('register_function_named_function') 10 | class register_function_name(object): 11 | pass 12 | 13 | 14 | @register.function('register_function', 'tuple_function') 15 | class register_function_tuple(object): 16 | pass 17 | -------------------------------------------------------------------------------- /tests/scanning/testmodule/register_factory.py: -------------------------------------------------------------------------------- 1 | from wiring.scanning import register 2 | 3 | 4 | @register.factory() 5 | class RegisterFactoryFactory(object): 6 | pass 7 | 8 | 9 | @register.factory('register_factory_named_factory') 10 | class RegisterFactoryNamedFactory(object): 11 | pass 12 | 13 | 14 | @register.factory('register_factory', 'tuple_factory') 15 | class RegisterFactoryTupleFactory(object): 16 | pass 17 | -------------------------------------------------------------------------------- /example/guestbook/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Guestbook 7 | 8 | 9 |
10 | {% block content %} 11 | {% endblock %} 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/scanning/testmodule/register_instance.py: -------------------------------------------------------------------------------- 1 | from wiring.scanning import register 2 | 3 | 4 | class MyInstance(object): 5 | pass 6 | 7 | 8 | instance = MyInstance() 9 | register.instance()(instance) 10 | 11 | 12 | named_instance = MyInstance() 13 | register.instance('register_instance_named_instance')(named_instance) 14 | 15 | 16 | tuple_instance = MyInstance() 17 | register.instance('register_instance', 'tuple_instance')(tuple_instance) 18 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Latest stable version of Wiring can be easily installed from `PyPi`_ with 5 | either `pip` or `easy_install`:: 6 | 7 | pip install wiring 8 | 9 | You can also install current development version straight from the `Git 10 | repository`_:: 11 | 12 | pip install git+https://github.com/msiedlarek/wiring.git 13 | 14 | .. _PyPi: https://pypi.python.org 15 | .. _Git repository: https://github.com/msiedlarek/wiring 16 | -------------------------------------------------------------------------------- /example/README.rst: -------------------------------------------------------------------------------- 1 | Guestbook - Wiring Web Application Example 2 | ****************************************** 3 | 4 | This is an example showing how to use Wiring to create a simple web 5 | application. 6 | 7 | For more information about Wiring see `it's README <../README.rst>`_. 8 | 9 | Development 10 | =========== 11 | 12 | You can install package for development and testing with:: 13 | 14 | virtualenv environment 15 | . environment/bin/activate 16 | pip install -e . 17 | guestbook-serve 18 | -------------------------------------------------------------------------------- /docs/api/scopes.rst: -------------------------------------------------------------------------------- 1 | wiring.scopes 2 | ============= 3 | 4 | .. automodule:: wiring.scopes 5 | 6 | SingletonScope 7 | -------------- 8 | 9 | .. autoclass:: SingletonScope 10 | :show-interfaces: 11 | 12 | ProcessScope 13 | ------------ 14 | 15 | .. autoclass:: ProcessScope 16 | :show-interfaces: 17 | 18 | ThreadScope 19 | ----------- 20 | 21 | .. autoclass:: ThreadScope 22 | :show-interfaces: 23 | 24 | IScope 25 | ------ 26 | 27 | .. autointerface:: IScope 28 | :members: 29 | :member-order: groupwise 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3.5 3 | install: 4 | - pip install tox coveralls 5 | script: 6 | - tox -- --with-coverage 7 | after_success: 8 | - coveralls 9 | env: 10 | - TOXENV=py27-scanning 11 | - TOXENV=py27-noscanning 12 | - TOXENV=py34-scanning 13 | - TOXENV=py34-noscanning 14 | - TOXENV=py35-scanning 15 | - TOXENV=py35-noscanning 16 | - TOXENV=pypy-scanning 17 | - TOXENV=pypy-noscanning 18 | - TOXENV=pypy3-scanning 19 | - TOXENV=pypy3-noscanning 20 | - TOXENV=flake8 21 | - TOXENV=docs 22 | -------------------------------------------------------------------------------- /docs/api/scanning/register.rst: -------------------------------------------------------------------------------- 1 | wiring.scanning.register 2 | ======================== 3 | 4 | .. automodule:: wiring.scanning.register 5 | 6 | @factory 7 | -------- 8 | 9 | .. autofunction:: factory 10 | 11 | @function 12 | --------- 13 | 14 | .. autofunction:: function 15 | 16 | @instance 17 | --------- 18 | 19 | .. autofunction:: instance 20 | 21 | @register 22 | --------- 23 | 24 | .. autofunction:: register 25 | 26 | WIRING_VENUSIAN_CATEGORY 27 | ------------------------ 28 | 29 | .. autodata:: WIRING_VENUSIAN_CATEGORY 30 | -------------------------------------------------------------------------------- /docs/api/dependency.rst: -------------------------------------------------------------------------------- 1 | wiring.dependency 2 | ================= 3 | 4 | .. automodule:: wiring.dependency 5 | 6 | Factory 7 | ------- 8 | 9 | .. autoclass:: Factory 10 | 11 | .. automethod:: __new__ 12 | .. autoattribute:: specification 13 | :annotation: 14 | 15 | @inject 16 | ------- 17 | 18 | .. autofunction:: inject 19 | 20 | injected 21 | -------- 22 | 23 | .. autodata:: injected 24 | :annotation: 25 | 26 | get_dependencies 27 | ---------------- 28 | 29 | .. autofunction:: get_dependencies 30 | 31 | UnrealizedInjection 32 | ------------------- 33 | 34 | .. autoclass:: UnrealizedInjection 35 | 36 | .. automethod:: __new__ 37 | .. autoattribute:: specification 38 | :annotation: 39 | -------------------------------------------------------------------------------- /docs/_templates/page.html: -------------------------------------------------------------------------------- 1 | {% extends "!page.html" %} 2 | 3 | {% block body %} 4 | 11 | Fork me on GitHub 12 | {{ super() }} 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /wiring/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module imports everything from all other wiring modules, so you can easily 3 | import anything without actually remembering where it was declared:: 4 | 5 | from wiring import injected 6 | """ 7 | 8 | from wiring.categories import * # noqa 9 | from wiring.configuration import * # noqa 10 | from wiring.dependency import * # noqa 11 | from wiring.graph import * # noqa 12 | from wiring.interface import * # noqa 13 | from wiring.providers import * # noqa 14 | from wiring.scopes import * # noqa 15 | 16 | 17 | __title__ = 'wiring' 18 | """Name of this package.""" 19 | 20 | __version__ = '0.4.0' 21 | """ 22 | Version of this package, compatible with 23 | `Semantic Versioning 2.0 `_. 24 | """ 25 | -------------------------------------------------------------------------------- /docs/api/configuration.rst: -------------------------------------------------------------------------------- 1 | wiring.configuration 2 | ==================== 3 | 4 | .. automodule:: wiring.configuration 5 | 6 | Module 7 | ------ 8 | 9 | .. autoclass:: Module 10 | 11 | .. autoattribute:: providers 12 | :annotation: 13 | .. autoattribute:: scan 14 | :annotation: 15 | .. autoattribute:: scan_ignore 16 | :annotation: 17 | .. automethod:: add_to 18 | 19 | @provides 20 | --------- 21 | 22 | .. autofunction:: provides 23 | 24 | @scope 25 | ------ 26 | 27 | .. autofunction:: scope 28 | 29 | InvalidConfigurationError 30 | ------------------------- 31 | 32 | .. autoexception:: InvalidConfigurationError 33 | 34 | .. autoinstanceattribute:: module 35 | :annotation: 36 | .. autoinstanceattribute:: message 37 | :annotation: 38 | -------------------------------------------------------------------------------- /docs/api/providers.rst: -------------------------------------------------------------------------------- 1 | wiring.providers 2 | ================ 3 | 4 | .. automodule:: wiring.providers 5 | 6 | FactoryProvider 7 | --------------- 8 | 9 | .. autoclass:: FactoryProvider 10 | :show-interfaces: 11 | 12 | .. autoinstanceattribute:: factory 13 | :annotation: 14 | 15 | FunctionProvider 16 | ---------------- 17 | 18 | .. autoclass:: FunctionProvider 19 | :show-interfaces: 20 | 21 | .. autoinstanceattribute:: function 22 | :annotation: 23 | 24 | InstanceProvider 25 | ---------------- 26 | 27 | .. autoclass:: InstanceProvider 28 | :show-interfaces: 29 | 30 | .. autoinstanceattribute:: instance 31 | :annotation: 32 | 33 | IProvider 34 | --------- 35 | 36 | .. autointerface:: IProvider 37 | :members: 38 | :member-order: groupwise 39 | -------------------------------------------------------------------------------- /tests/scanning/testmodule/plain_register.py: -------------------------------------------------------------------------------- 1 | from wiring import FactoryProvider, InstanceProvider 2 | from wiring.scanning import register 3 | 4 | 5 | @register.register(FactoryProvider) 6 | class PlainRegisterFactory(object): 7 | pass 8 | 9 | 10 | @register.register(FactoryProvider, 'plain_register_named_factory') 11 | class PlainRegisterNamedFactory(object): 12 | pass 13 | 14 | 15 | @register.register(FactoryProvider, 'plain_register', 'tuple_factory') 16 | class PlainRegisterTupleFactory(object): 17 | pass 18 | 19 | 20 | @register.register(InstanceProvider) 21 | class PlainRegisterInstance(object): 22 | pass 23 | 24 | 25 | @register.register(InstanceProvider, 'plain_register_named_instance') 26 | class PlainRegisterNamedInstance(object): 27 | pass 28 | 29 | 30 | @register.register(InstanceProvider, 'plain_register', 'tuple_instance') 31 | class PlainRegisterTupleInstance(object): 32 | pass 33 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | License 2 | ======= 3 | 4 | .. topic:: Wiring |release| 5 | 6 | .. raw:: html 7 | 8 | Copyright © 2014-2015 Mikołaj Siedlarek 9 | <mikolaj@siedlarek.pl> 10 | 11 | Licensed under the Apache License, Version 2.0 (the "License"); you may not 12 | use this software except in compliance with the License. You may obtain 13 | a copy of the License at: 14 | 15 | http://www.apache.org/licenses/LICENSE-2.0 16 | 17 | Unless required by applicable law or agreed to in writing, software 18 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 19 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 20 | License for the specific language governing permissions and limitations 21 | under the License. 22 | 23 | .. literalinclude:: ../LICENSE 24 | -------------------------------------------------------------------------------- /example/guestbook/response.py: -------------------------------------------------------------------------------- 1 | import jinja2 2 | from werkzeug.wrappers import Response 3 | from wiring import inject 4 | 5 | 6 | class TemplateRenderer(object): 7 | 8 | @inject(jinja2.Environment) 9 | def __init__(self, environment): 10 | self.environment = environment 11 | 12 | def __call__(self, response, context, template=None): 13 | response.set_data( 14 | self.environment.get_template(template).render(context) 15 | ) 16 | return response 17 | 18 | 19 | class LazyResponse(Response): 20 | 21 | def __init__(self, renderer, configuration, context, **kwargs): 22 | self.renderer = renderer 23 | self.renderer_configuration = configuration 24 | self.renderer_context = context 25 | super(LazyResponse, self).__init__(**kwargs) 26 | 27 | 28 | class TemplateResponse(LazyResponse): 29 | 30 | def __init__(self, template, context={}, **kwargs): 31 | kwargs.setdefault('mimetype', 'text/html') 32 | super(TemplateResponse, self).__init__( 33 | TemplateRenderer, 34 | { 35 | 'template': template, 36 | }, 37 | context, 38 | **kwargs 39 | ) 40 | -------------------------------------------------------------------------------- /example/guestbook/wsgi.py: -------------------------------------------------------------------------------- 1 | import logging.config 2 | 3 | from werkzeug.serving import run_simple 4 | from wiring import Graph 5 | 6 | from guestbook.application import RequestScope 7 | from guestbook.module import GuestbookModule 8 | 9 | 10 | def get_application(): 11 | graph = Graph() 12 | graph.register_scope(RequestScope, RequestScope()) 13 | graph.register_instance(Graph, graph) 14 | GuestbookModule().add_to(graph) 15 | graph.validate() 16 | return graph.get('wsgi.application') 17 | 18 | 19 | def serve(): 20 | logging.config.dictConfig({ 21 | 'version': 1, 22 | 'formatters': { 23 | 'simple': { 24 | 'format': '%(asctime)s %(levelname)s %(name)s: %(message)s', 25 | }, 26 | }, 27 | 'handlers': { 28 | 'console': { 29 | 'class': 'logging.StreamHandler', 30 | 'formatter': 'simple', 31 | 'stream': 'ext://sys.stdout', 32 | }, 33 | }, 34 | 'loggers': { 35 | 'guestbook': { 36 | 'level': 'DEBUG', 37 | }, 38 | }, 39 | 'root': { 40 | 'level': 'INFO', 41 | 'handlers': ['console'], 42 | }, 43 | }) 44 | run_simple( 45 | '127.0.0.1', 46 | 8000, 47 | get_application(), 48 | use_debugger=True, 49 | use_reloader=True 50 | ) 51 | -------------------------------------------------------------------------------- /example/setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from codecs import open 4 | from os import path 5 | 6 | from setuptools import find_packages, setup 7 | 8 | 9 | here = path.abspath(path.dirname(__file__)) 10 | 11 | with open(path.join(here, 'README.rst'), encoding='utf-8') as readme: 12 | long_description = readme.read() 13 | 14 | setup( 15 | name='guestbook', 16 | version='1.0.0', 17 | description='Simple example of Wiring used in a web application.', 18 | long_description=long_description, 19 | url='https://github.com/msiedlarek/wiring/tree/master/example', 20 | author=u'Mikołaj Siedlarek', 21 | author_email='mikolaj@siedlarek.pl', 22 | license='Apache License, Version 2.0', 23 | classifiers=[ 24 | 'Intended Audience :: Developers', 25 | 'License :: OSI Approved :: Apache Software License', 26 | 'Operating System :: OS Independent', 27 | 'Programming Language :: Python :: 2', 28 | 'Programming Language :: Python :: 2.7', 29 | 'Topic :: Internet :: WWW/HTTP', 30 | 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', 31 | ], 32 | packages=find_packages(), 33 | include_package_data=True, 34 | install_requires=[ 35 | 'wiring', 36 | 'werkzeug==0.10.1', 37 | 'jinja2==2.7.3', 38 | ], 39 | entry_points={ 40 | 'console_scripts': [ 41 | 'guestbook-serve = guestbook.wsgi:serve', 42 | ], 43 | } 44 | ) 45 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = 8 | {py27,py34,py35,pypy,pypy3}-{scanning,noscanning}, 9 | flake8, 10 | isort, 11 | docs 12 | 13 | [testenv] 14 | basepython = 15 | py27: python2.7 16 | py34: python3.4 17 | py35: python3.5 18 | pypy: pypy 19 | pypy3: pypy3 20 | deps = 21 | nose 22 | coverage 23 | commands = 24 | scanning: pip install wiring[scanning] 25 | py27: {envpython} setup.py nosetests --tests=tests/all [] 26 | py34: {envpython} setup.py nosetests --tests=tests/all,tests/py3 [] 27 | py35: {envpython} setup.py nosetests --tests=tests/all,tests/py3 [] 28 | pypy: {envpython} setup.py nosetests --tests=tests/all [] 29 | pypy3: {envpython} setup.py nosetests --tests=tests/all,tests/py3 [] 30 | scanning: {envpython} setup.py nosetests --tests=tests/scanning [] 31 | 32 | [testenv:flake8] 33 | basepython = python 34 | deps = flake8 35 | commands = flake8 wiring tests 36 | 37 | [testenv:isort] 38 | basepython = python 39 | deps = isort 40 | commands = isort --check-only --recursive wiring tests 41 | 42 | [testenv:docs] 43 | basepython = python 44 | deps = 45 | sphinx 46 | sphinx_rtd_theme 47 | changedir = docs 48 | commands = 49 | pip install wiring[scanning] 50 | sphinx-build -b html -d {envtmpdir}/doctrees . {envtmpdir}/html 51 | -------------------------------------------------------------------------------- /docs/api/graph.rst: -------------------------------------------------------------------------------- 1 | wiring.graph 2 | ============ 3 | 4 | .. automodule:: wiring.graph 5 | 6 | Graph 7 | ----- 8 | 9 | .. autoclass:: Graph 10 | 11 | .. autoinstanceattribute:: providers 12 | :annotation: 13 | .. autoinstanceattribute:: scopes 14 | :annotation: 15 | .. automethod:: acquire 16 | .. automethod:: get 17 | .. automethod:: register_provider 18 | .. automethod:: unregister_provider 19 | .. automethod:: register_factory 20 | .. automethod:: register_instance 21 | .. automethod:: register_scope 22 | .. automethod:: unregister_scope 23 | .. automethod:: validate 24 | 25 | GraphValidationError 26 | -------------------- 27 | 28 | .. autoexception:: GraphValidationError 29 | 30 | SelfDependencyError 31 | ------------------- 32 | 33 | .. autoexception:: SelfDependencyError 34 | :show-inheritance: 35 | 36 | .. autoinstanceattribute:: specification 37 | :annotation: 38 | 39 | MissingDependencyError 40 | ---------------------- 41 | 42 | .. autoexception:: MissingDependencyError 43 | :show-inheritance: 44 | 45 | .. autoinstanceattribute:: dependency 46 | :annotation: 47 | .. autoinstanceattribute:: dependant 48 | :annotation: 49 | 50 | DependencyCycleError 51 | -------------------- 52 | 53 | .. autoexception:: DependencyCycleError 54 | :show-inheritance: 55 | 56 | .. autoinstanceattribute:: cycle 57 | :annotation: 58 | 59 | UnknownScopeError 60 | ----------------- 61 | 62 | .. autoexception:: UnknownScopeError 63 | 64 | .. autoinstanceattribute:: scope_type 65 | :annotation: 66 | -------------------------------------------------------------------------------- /example/guestbook/module.py: -------------------------------------------------------------------------------- 1 | import pkg_resources 2 | 3 | import jinja2 4 | from werkzeug.routing import Map, Rule 5 | from werkzeug.wsgi import SharedDataMiddleware 6 | from wiring import Module, SingletonScope, inject, provides, scope 7 | 8 | from guestbook import views 9 | from guestbook.application import Application, ApplicationModule 10 | from guestbook.response import TemplateRenderer 11 | 12 | 13 | class GuestbookModule(Module): 14 | 15 | factories = { 16 | TemplateRenderer: TemplateRenderer, 17 | } 18 | 19 | functions = { 20 | 'views.home': views.home, 21 | } 22 | 23 | routes = [ 24 | Rule('/', endpoint='views.home'), 25 | ] 26 | 27 | def add_to(self, graph): 28 | ApplicationModule().add_to(graph) 29 | super(GuestbookModule, self).add_to(graph) 30 | 31 | @provides('wsgi.application') 32 | @scope(SingletonScope) 33 | @inject(application=Application) 34 | def provide_wsgi_application(self, application=None): 35 | application = SharedDataMiddleware(application, { 36 | '/static': pkg_resources.resource_filename(__name__, 'static') 37 | }) 38 | return application 39 | 40 | @provides(Map) 41 | @scope(SingletonScope) 42 | def provide_url_map(self): 43 | return Map(self.routes) 44 | 45 | @provides(jinja2.Environment) 46 | @scope(SingletonScope) 47 | def provide_jinja_environment(self, url=None, static=None): 48 | return jinja2.Environment( 49 | loader=jinja2.PackageLoader(__name__), 50 | auto_reload=True 51 | ) 52 | -------------------------------------------------------------------------------- /wiring/categories.py: -------------------------------------------------------------------------------- 1 | __all__ = ( 2 | 'Category', 3 | ) 4 | 5 | 6 | class Category(tuple): 7 | """ 8 | This type acts as tuple with one key difference - two instances of it are 9 | equal only when they have the same type. This allows you to easily mitigate 10 | collisions when using common types (like string) in a dict or as a Wiring 11 | :term:`specification`. 12 | 13 | Example:: 14 | 15 | from wiring import Graph, Category 16 | 17 | class Public(Category): 18 | pass 19 | 20 | class Secret(Category): 21 | pass 22 | 23 | graph = Graph() 24 | graph.register_instance(Public('database', 1), 'db://public/1') 25 | graph.register_instance(Secret('database', 1), 'db://private/1') 26 | 27 | assert Public('database', 1) != Private('database', 1) 28 | assert ( 29 | graph.get(Public('database', 1)) 30 | != graph.get(Private('database', 1)) 31 | ) 32 | """ 33 | 34 | def __new__(cls, *args): 35 | return super(Category, cls).__new__(cls, args) 36 | 37 | def __repr__(self): 38 | return type(self).__name__ + super(Category, self).__repr__() 39 | 40 | def __str__(self): 41 | return repr(self) 42 | 43 | def __eq__(self, other): 44 | return ( 45 | type(self) == type(other) and super(Category, self).__eq__(other) 46 | ) 47 | 48 | def __ne__(self, other): 49 | return not self.__eq__(other) 50 | 51 | def __hash__(self): 52 | return hash(( 53 | type(self), 54 | super(Category, self).__hash__() 55 | )) 56 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from codecs import open 4 | from os import path 5 | 6 | from setuptools import setup 7 | 8 | 9 | here = path.abspath(path.dirname(__file__)) 10 | 11 | with open(path.join(here, 'README.rst'), encoding='utf-8') as readme: 12 | long_description = readme.read() 13 | 14 | setup( 15 | name='wiring', 16 | version='0.4.0', 17 | description='Architectural foundation for Python applications.', 18 | long_description=long_description, 19 | url='https://github.com/msiedlarek/wiring', 20 | author=u'Mikołaj Siedlarek', 21 | author_email='mikolaj@siedlarek.pl', 22 | license='Apache License, Version 2.0', 23 | classifiers=[ 24 | 'Development Status :: 5 - Production/Stable', 25 | 'Intended Audience :: Developers', 26 | 'License :: OSI Approved :: Apache Software License', 27 | 'Operating System :: OS Independent', 28 | 'Programming Language :: Python :: 2', 29 | 'Programming Language :: Python :: 2.7', 30 | 'Programming Language :: Python :: 3', 31 | 'Programming Language :: Python :: 3.4', 32 | 'Programming Language :: Python :: 3.5', 33 | 'Topic :: Software Development :: Libraries :: Application Frameworks', 34 | ], 35 | keywords='wiring dependency injection', 36 | install_requires=[ 37 | 'six', 38 | ], 39 | extras_require={ 40 | 'scanning': ['venusian'], 41 | }, 42 | tests_require=[ 43 | 'nose', 44 | ], 45 | test_suite='nose.collector', 46 | packages=[ 47 | 'wiring', 48 | 'wiring.scanning', 49 | ], 50 | package_data={ 51 | '': ['LICENSE'], 52 | }, 53 | include_package_data=True 54 | ) 55 | -------------------------------------------------------------------------------- /docs/api/interface.rst: -------------------------------------------------------------------------------- 1 | wiring.interface 2 | ================ 3 | 4 | .. automodule:: wiring.interface 5 | 6 | Interface 7 | --------- 8 | 9 | .. autoclass:: Interface 10 | 11 | .. autoattribute:: implied 12 | :annotation: 13 | .. autoattribute:: attributes 14 | :annotation: 15 | .. automethod:: check_compliance 16 | 17 | @implements 18 | ----------- 19 | 20 | .. autofunction:: implements 21 | 22 | @implements_only 23 | ---------------- 24 | 25 | .. autofunction:: implements_only 26 | 27 | isimplementation 28 | ---------------- 29 | 30 | .. autofunction:: isimplementation 31 | 32 | get_implemented_interfaces 33 | -------------------------- 34 | 35 | .. autofunction:: get_implemented_interfaces 36 | 37 | set_implemented_interfaces 38 | -------------------------- 39 | 40 | .. autofunction:: set_implemented_interfaces 41 | 42 | add_implemented_interfaces 43 | -------------------------- 44 | 45 | .. autofunction:: add_implemented_interfaces 46 | 47 | Attribute 48 | --------- 49 | 50 | .. autoclass:: Attribute 51 | 52 | .. autoinstanceattribute:: docstring 53 | :annotation: 54 | 55 | Method 56 | ------ 57 | 58 | .. autoclass:: Method 59 | :show-inheritance: 60 | 61 | .. autoinstanceattribute:: argument_specification 62 | :annotation: 63 | .. automethod:: check_compliance 64 | 65 | MissingAttributeError 66 | --------------------- 67 | 68 | .. autoexception:: MissingAttributeError 69 | :show-inheritance: 70 | 71 | .. autoinstanceattribute:: attribute_name 72 | :annotation: 73 | 74 | MethodValidationError 75 | --------------------- 76 | 77 | .. autoexception:: MethodValidationError 78 | :show-inheritance: 79 | 80 | .. autoinstanceattribute:: function 81 | :annotation: 82 | .. autoinstanceattribute:: expected_argspec 83 | :annotation: 84 | .. autoinstanceattribute:: observed_argspec 85 | :annotation: 86 | 87 | InterfaceComplianceError 88 | ------------------------ 89 | 90 | .. autoexception:: InterfaceComplianceError 91 | -------------------------------------------------------------------------------- /wiring/scanning/scan.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | 3 | import six 4 | import venusian 5 | 6 | from wiring.scanning.register import WIRING_VENUSIAN_CATEGORY 7 | 8 | 9 | __all__ = ( 10 | 'scan_to_module', 11 | 'scan_to_graph', 12 | 'scan', 13 | ) 14 | 15 | 16 | def scan_to_module(python_modules, module, ignore=tuple()): 17 | """ 18 | Scans `python_modules` with :py:func:`scan` and adds found providers 19 | to `module`'s :py:attr:`wiring.configuration.Module.providers`. 20 | 21 | `ignore` argument is passed through to :py:func:`scan`. 22 | """ 23 | def callback(specification, provider): 24 | module.providers[specification] = provider 25 | scan(python_modules, callback, ignore=ignore) 26 | 27 | 28 | def scan_to_graph(python_modules, graph, ignore=tuple()): 29 | """ 30 | Scans `python_modules` with :py:func:`scan` and registers found providers 31 | in `graph`. 32 | 33 | `ignore` argument is passed through to :py:func:`scan`. 34 | """ 35 | def callback(specification, provider): 36 | graph.register_provider(specification, provider) 37 | scan(python_modules, callback, ignore=ignore) 38 | 39 | 40 | def scan(python_modules, callback, ignore=tuple()): 41 | """ 42 | Recursively scans `python_modules` for providers registered with 43 | :py:mod:`wiring.scanning.register` module and for each one calls `callback` 44 | with :term:`specification` as the first argument, and the provider object 45 | as the second. 46 | 47 | Each element in `python_modules` may be a module reference or a string 48 | representing a path to a module. 49 | 50 | Module paths given in `ignore` are excluded from scanning. 51 | """ 52 | scanner = venusian.Scanner(callback=callback) 53 | for python_module in python_modules: 54 | if isinstance(python_module, six.string_types): 55 | python_module = importlib.import_module(python_module) 56 | scanner.scan( 57 | python_module, 58 | categories=[WIRING_VENUSIAN_CATEGORY], 59 | ignore=ignore 60 | ) 61 | -------------------------------------------------------------------------------- /tests/all/__init__.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import importlib 3 | 4 | import six 5 | 6 | 7 | class ModuleTest(unittest.TestCase): 8 | 9 | module = 'wiring' 10 | 11 | def test_import_all(self): 12 | package = importlib.import_module(self.module) 13 | if not hasattr(package, '__all__'): 14 | return 15 | for name in package.__all__: 16 | self.assertTrue( 17 | hasattr(package, name), 18 | msg=( 19 | "Module `{module}` is missing `{name}` which was declared" 20 | " in `__all__`." 21 | ).format( 22 | module=self.module, 23 | name=name 24 | ) 25 | ) 26 | 27 | 28 | class InitTest(unittest.TestCase): 29 | 30 | imported_modules = ( 31 | 'wiring.configuration', 32 | 'wiring.dependency', 33 | 'wiring.graph', 34 | 'wiring.interface', 35 | 'wiring.providers', 36 | 'wiring.scopes', 37 | ) 38 | 39 | def test_imports(self): 40 | import wiring 41 | for module in self.imported_modules: 42 | package = importlib.import_module(module) 43 | if not hasattr(package, '__all__'): 44 | continue 45 | for name in package.__all__: 46 | self.assertTrue( 47 | hasattr(wiring, name), 48 | msg=( 49 | "Module `wiring` is missing `{name}` which should be" 50 | " wildcard-imported from `{module}`." 51 | ).format( 52 | name=name, 53 | module=module 54 | ) 55 | ) 56 | 57 | def test_metadata(self): 58 | import wiring 59 | 60 | self.assertIsInstance(wiring.__title__, six.string_types) 61 | self.assertRegexpMatches(wiring.__title__, r'^\w+$') 62 | 63 | self.assertIsInstance(wiring.__version__, six.string_types) 64 | self.assertRegexpMatches(wiring.__version__, r'^\d+\.\d+\.\d+$') 65 | -------------------------------------------------------------------------------- /tests/all/categories_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import six 4 | 5 | from wiring import Category 6 | 7 | 8 | class A(Category): 9 | pass 10 | 11 | 12 | class B(Category): 13 | pass 14 | 15 | 16 | class CategoriesTest(unittest.TestCase): 17 | 18 | def test_tuplicity(self): 19 | c = A(1, 2, "foo", "bar") 20 | self.assertIsInstance(c, tuple) 21 | self.assertSequenceEqual(c, (1, 2, "foo", "bar")) 22 | 23 | def test_equality_int(self): 24 | self.assertEqual(A(1), A(1)) 25 | self.assertNotEqual(A(1), A(2)) 26 | self.assertNotEqual(A(1), B(1)) 27 | self.assertNotEqual(A(1), B(2)) 28 | 29 | def test_hash_int(self): 30 | self.assertEqual(hash(A(1)), hash(A(1))) 31 | self.assertNotEqual(hash(A(1)), hash(A(2))) 32 | self.assertNotEqual(hash(A(1)), hash(B(1))) 33 | self.assertNotEqual(hash(A(1)), hash(B(2))) 34 | 35 | def test_equality_string(self): 36 | self.assertEqual(A('foo'), A('foo')) 37 | self.assertNotEqual(A('foo'), A('bar')) 38 | self.assertNotEqual(A('foo'), B('foo')) 39 | self.assertNotEqual(A('foo'), B('bar')) 40 | 41 | def test_hash_string(self): 42 | self.assertEqual(hash(A('foo')), hash(A('foo'))) 43 | self.assertNotEqual(hash(A('foo')), hash(A('bar'))) 44 | self.assertNotEqual(hash(A('foo')), hash(B('foo'))) 45 | self.assertNotEqual(hash(A('foo')), hash(B('bar'))) 46 | 47 | def test_equality_tuple(self): 48 | self.assertEqual(A(1, 'foo'), A(1, 'foo')) 49 | self.assertNotEqual(A(1, 'foo'), A(1, 'bar')) 50 | self.assertNotEqual(A(1, 'foo'), B(1, 'foo')) 51 | self.assertNotEqual(A(1, 'foo'), B(1, 'bar')) 52 | 53 | def test_hash_tuple(self): 54 | self.assertEqual(hash(A(1, 'foo')), hash(A(1, 'foo'))) 55 | self.assertNotEqual(hash(A(1, 'foo')), hash(A(1, 'bar'))) 56 | self.assertNotEqual(hash(A(1, 'foo')), hash(B(1, 'foo'))) 57 | self.assertNotEqual(hash(A(1, 'foo')), hash(B(1, 'bar'))) 58 | 59 | def test_repr(self): 60 | c = A(1, 2, 'foo', 'bar') 61 | self.assertEqual(repr(c), "A(1, 2, 'foo', 'bar')") 62 | self.assertEqual(six.text_type(c), six.u("A(1, 2, 'foo', 'bar')")) 63 | -------------------------------------------------------------------------------- /tests/py3/dependency_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from wiring.dependency import UnrealizedInjection, get_dependencies 4 | 5 | 6 | class GetDependenciesTest(unittest.TestCase): 7 | 8 | def test_keyword_only_arguments(self): 9 | def function(foo, bar, somearg=UnrealizedInjection(11), *, 10 | foobar=UnrealizedInjection('foo', 12), test=12, 11 | other=UnrealizedInjection(42)): 12 | pass 13 | self.assertDictEqual( 14 | get_dependencies(function), 15 | { 16 | 'somearg': 11, 17 | 'foobar': ('foo', 12), 18 | 'other': 42, 19 | } 20 | ) 21 | 22 | def test_annotations_only(self): 23 | def function(foo, bar: 33, other_arg=5, somearg: 15 = None, *, 24 | other: 'foo' = 7, test=2): 25 | pass 26 | self.assertDictEqual( 27 | get_dependencies(function), 28 | { 29 | 'bar': 33, 30 | 'somearg': 15, 31 | 'other': 'foo', 32 | } 33 | ) 34 | 35 | def test_annotations_mixed(self): 36 | def function(foo, bar: 33, other_arg=UnrealizedInjection(5), 37 | somearg: 15 = None, *, other: 'foo' = 7, 38 | test=UnrealizedInjection(2)): 39 | pass 40 | self.assertDictEqual( 41 | get_dependencies(function), 42 | { 43 | 'bar': 33, 44 | 'somearg': 15, 45 | 'other_arg': 5, 46 | 'other': 'foo', 47 | 'test': 2, 48 | } 49 | ) 50 | 51 | def test_annotations_positional(self): 52 | def function(foo, bar: 33): 53 | pass 54 | self.assertDictEqual( 55 | get_dependencies(function), 56 | { 57 | 'bar': 33, 58 | } 59 | ) 60 | 61 | def test_annotations_positional_class(self): 62 | class SomeClass: 63 | def __init__(self, foo, bar: 33): 64 | pass 65 | self.assertDictEqual( 66 | get_dependencies(SomeClass), 67 | { 68 | 'bar': 33, 69 | } 70 | ) 71 | -------------------------------------------------------------------------------- /tests/py3/interface_tests.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import unittest 3 | 4 | import six 5 | 6 | from wiring.interface import Method, MethodValidationError 7 | 8 | 9 | class MethodTest(unittest.TestCase): 10 | 11 | def _example_method_definition(foo, bar=12, *, foobar): 12 | """Some example method.""" 13 | 14 | def _example_method_implementation(self, foo, bar=12, *, foobar): 15 | pass 16 | 17 | @classmethod 18 | def _example_method_classmethod(cls, foo, bar=12, *, foobar): 19 | pass 20 | 21 | @staticmethod 22 | def _example_method_static(foo, bar=12, *, foobar): 23 | pass 24 | 25 | def _get_argspec(self): 26 | return inspect.getfullargspec(self._example_method_definition) 27 | 28 | def test_construction(self): 29 | argspec = self._get_argspec() 30 | 31 | method = Method(argspec) 32 | self.assertEqual(method.argument_specification, argspec) 33 | self.assertIsNone(method.docstring) 34 | 35 | method = Method(argspec, docstring="Some docstring.") 36 | self.assertEqual(method.argument_specification, argspec) 37 | self.assertEqual(method.docstring, "Some docstring.") 38 | 39 | def test_repr(self): 40 | argspec = self._get_argspec() 41 | 42 | method = Method(argspec) 43 | self.assertEqual(repr(method), '') 44 | 45 | method = Method(argspec, docstring="Some docstring.") 46 | self.assertEqual(repr(method), '') 47 | 48 | def test_check_compliance(self): 49 | argspec = self._get_argspec() 50 | 51 | method = Method(argspec) 52 | method.check_compliance(self._example_method_implementation) 53 | method.check_compliance(self._example_method_classmethod) 54 | method.check_compliance(self._example_method_static) 55 | 56 | def other_implementation(foo, bar=12, *, foobar): 57 | pass 58 | method.check_compliance(other_implementation) 59 | 60 | def invalid_implementation1(foo, bar=12): 61 | pass 62 | with self.assertRaises(MethodValidationError): 63 | method.check_compliance(invalid_implementation1) 64 | 65 | def invalid_implementation2(foo, bar=13, *, foobar): 66 | pass 67 | with self.assertRaises(MethodValidationError): 68 | method.check_compliance(invalid_implementation2) 69 | -------------------------------------------------------------------------------- /docs/glossary.rst: -------------------------------------------------------------------------------- 1 | Glossary 2 | ======== 3 | 4 | .. glossary:: 5 | 6 | dependency 7 | is a relation between two :term:`providers ` when product of 8 | one provider is needed by the second provider. For example database 9 | connection provider may need the result from a configuration provider to 10 | retrieve the connection parameters. 11 | 12 | Dependencies are declared using :term:`specifications `. 13 | 14 | dependency cycle 15 | is a situation when dependencies for a given :term:`provider` cannot be 16 | realized, because one of them is dependent on a said provider, or 17 | the provider is dependent on itself. 18 | 19 | interface 20 | is a definition of an object, describing attributes and methods an object 21 | must have to conform to the interface. It is useful for testing whether 22 | some interchangable module can be used in a given context. 23 | 24 | Interfaces are defined as subclasses of 25 | :py:class:`wiring.interface.Interface`. 26 | 27 | module 28 | is a unit of configuration, a :py:class:`wiring.configuration.Module` 29 | subclass that defines some set of :term:`specifications ` 30 | and their :term:`providers `. This allows for easy and modular 31 | registration of entire application parts into the object graph. 32 | 33 | object graph 34 | is an object that manages :term:`providers ` and their 35 | :term:`dependencies `, and can be used to retrieve objects 36 | with all their dependencies satisfied. 37 | 38 | See :py:class:`wiring.graph.Graph`. 39 | 40 | provider 41 | is a callable implementing :py:interface:`wiring.providers.IProvider` 42 | interface, that the :term:`object graph` uses to retrieve objects for 43 | particular :term:`specification`. 44 | 45 | It declares dependencies that will be injected from the object graph when 46 | the provider is called, and a type of the scope that the graph will use 47 | to cache the result. 48 | 49 | scope 50 | is an object implementing :py:interface:`wiring.scopes.IScope` interface, 51 | that manages object cached for an :term:`object graph`. 52 | 53 | For example when an object is costly to create (like a database 54 | connection) but can be reused, :py:class:`wiring.scopes.ThreadScope` 55 | can be used. Then, its provider will be called only once for each thread 56 | and its result will be reused whenever database connection is retrieved 57 | from the object graph in this particular thread. 58 | 59 | specification 60 | is a hashable object used as an unique identifier of some kind of object 61 | in the :term:`object graph`. This identifier can be used to register 62 | provider for that kind of object, declare the provider's dependencies on 63 | other kinds of objects and retrieve the object from the graph. 64 | 65 | Example specifications could be:: 66 | 67 | DBConnection 68 | (DBConnection, 'test_db') 69 | 'database_url' 70 | -------------------------------------------------------------------------------- /example/guestbook/application.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from werkzeug.exceptions import HTTPException 4 | from werkzeug.routing import Map, MapAdapter 5 | from werkzeug.wrappers import Request 6 | from wiring import ( 7 | Factory, 8 | Graph, 9 | IScope, 10 | Module, 11 | implements, 12 | inject, 13 | provides, 14 | scope 15 | ) 16 | 17 | from guestbook.response import LazyResponse 18 | 19 | 20 | class Application(object): 21 | 22 | _threadlocal = threading.local() 23 | 24 | @inject( 25 | graph=Graph, 26 | get_url_map_adapter=Factory(MapAdapter) 27 | ) 28 | def __init__(self, graph=None, get_url_map_adapter=None): 29 | self.graph = graph 30 | self.get_url_map_adapter = get_url_map_adapter 31 | 32 | def __call__(self, environment, start_response): 33 | self._threadlocal.request = Request(environment) 34 | try: 35 | return self.dispatch(self._threadlocal.request)( 36 | environment, 37 | start_response 38 | ) 39 | finally: 40 | self._threadlocal.request = None 41 | 42 | def dispatch(self, request): 43 | adapter = self.get_url_map_adapter() 44 | try: 45 | endpoint, values = adapter.match() 46 | response = self.graph.get(endpoint)(request, **values) 47 | if isinstance(response, LazyResponse): 48 | renderer = self.graph.get(response.renderer) 49 | response = renderer.__call__( 50 | response, 51 | response.renderer_context, 52 | **response.renderer_configuration 53 | ) 54 | return response 55 | except HTTPException, error: 56 | return error 57 | 58 | @classmethod 59 | def get_current_request(cls): 60 | try: 61 | return cls._threadlocal.request 62 | except AttributeError: 63 | return None 64 | 65 | 66 | @implements(IScope) 67 | class RequestScope(object): 68 | 69 | def _get_storage(self): 70 | request = Application.get_current_request() 71 | if not hasattr(request, 'scope_storage'): 72 | request.scope_storage = {} 73 | return request.scope_storage 74 | 75 | def __getitem__(self, specification): 76 | self._get_storage().__getitem__(specification) 77 | 78 | def __setitem__(self, specification, instance): 79 | self._get_storage().__setitem__(specification, instance) 80 | 81 | def __contains__(self, specification): 82 | self._get_storage().__contains__(specification) 83 | 84 | 85 | class ApplicationModule(Module): 86 | 87 | factories = { 88 | Application: Application, 89 | } 90 | 91 | @provides(Request) 92 | def provide_request(self): 93 | return Application.get_current_request() 94 | 95 | @provides(MapAdapter) 96 | @scope(RequestScope) 97 | @inject(Map, Request) 98 | def provide_map_adapter(self, url_map, request): 99 | return url_map.bind_to_environ(request.environ) 100 | -------------------------------------------------------------------------------- /wiring/scopes.py: -------------------------------------------------------------------------------- 1 | import os 2 | import threading 3 | 4 | from wiring import interface 5 | 6 | 7 | __all__ = ( 8 | 'IScope', 9 | 'SingletonScope', 10 | 'ProcessScope', 11 | 'ThreadScope', 12 | ) 13 | 14 | 15 | class IScope(interface.Interface): 16 | """ 17 | Interface defining a :term:`scope` object. 18 | """ 19 | 20 | def __getitem__(specification): 21 | """ 22 | Returns a cached instance for given :term:`specification`. Raises 23 | `KeyError` if there is none. 24 | 25 | :raises: 26 | KeyError 27 | """ 28 | 29 | def __setitem__(specification, instance): 30 | """ 31 | Saves a cached `instance` for given :term:`specification`. If there was 32 | already a cached instance for this specification, it is overriden. 33 | """ 34 | 35 | def __contains__(specification): 36 | """ 37 | Returns `True` if there is cached instance for given 38 | :term:`specification` and `False` otherwise. 39 | """ 40 | 41 | 42 | @interface.implements(IScope) 43 | class SingletonScope(object): 44 | """ 45 | :term:`Scope` where only one provided instance is created and reused. 46 | """ 47 | 48 | def __init__(self): 49 | self._cache = {} 50 | 51 | def __getitem__(self, specification): 52 | return self._cache[specification] 53 | 54 | def __setitem__(self, specification, instance): 55 | self._cache[specification] = instance 56 | 57 | def __contains__(self, specification): 58 | return (specification in self._cache) 59 | 60 | 61 | @interface.implements(IScope) 62 | class ProcessScope(object): 63 | """ 64 | :term:`Scope` where provided instances are cached per-process. The 65 | instances cached in this scope will not be available for a forked process. 66 | """ 67 | 68 | def __init__(self): 69 | self._pid = os.getpid() 70 | self._cache = {} 71 | 72 | def __getitem__(self, specification): 73 | self._validate() 74 | return self._cache[specification] 75 | 76 | def __setitem__(self, specification, instance): 77 | self._validate() 78 | self._cache[specification] = instance 79 | 80 | def __contains__(self, specification): 81 | self._validate() 82 | return (specification in self._cache) 83 | 84 | def _validate(self): 85 | current_pid = os.getpid() 86 | if self._pid != current_pid: # pragma: no cover 87 | self._pid = current_pid 88 | self._cache = {} 89 | 90 | 91 | @interface.implements(IScope) 92 | class ThreadScope(object): 93 | """ 94 | :term:`Scope` where provided instances are cached per-thread. 95 | """ 96 | 97 | def __init__(self): 98 | self._local = threading.local() 99 | 100 | def __getitem__(self, specification): 101 | self._validate() 102 | return self._local.cache[specification] 103 | 104 | def __setitem__(self, specification, instance): 105 | self._validate() 106 | self._local.cache[specification] = instance 107 | 108 | def __contains__(self, specification): 109 | self._validate() 110 | return (specification in self._local.cache) 111 | 112 | def _validate(self): 113 | if not hasattr(self._local, 'cache'): 114 | self._local.cache = {} 115 | -------------------------------------------------------------------------------- /wiring/scanning/register.py: -------------------------------------------------------------------------------- 1 | import venusian 2 | 3 | from wiring import FactoryProvider, FunctionProvider, InstanceProvider 4 | 5 | 6 | WIRING_VENUSIAN_CATEGORY = 'wiring' 7 | """ 8 | A `Venusian`_ category under which all Wiring callbacks are registered. 9 | 10 | .. _Venusian: https://pypi.python.org/pypi/venusian 11 | """ 12 | 13 | 14 | def register(provider_factory, *args, **kwargs): 15 | """ 16 | Returns a decorator that registers its arguments for scanning, so it can be 17 | picked up by :py:func:`wiring.scanning.scan.scan`. 18 | 19 | First argument - `provider_factory` - is a callable that is invoked during 20 | scanning with decorated argument and `kwargs` as arguments, and it should 21 | return a :term:`provider` to be registered. 22 | 23 | Rest of the positional arguments (`args`) are used to build the 24 | :term:`specification` for registration. If there is only one - it is used 25 | directly as a specification. If there are more - a tuple of them is used as 26 | a specification. If there are none - the decorated object itself is used as 27 | a specification. 28 | 29 | Example:: 30 | 31 | @register(FactoryProvider) 32 | class MyClass: 33 | pass 34 | 35 | graph = Graph() 36 | scan_to_graph([__package__], graph) 37 | assert isinstance(graph.get(MyClass), MyClass) 38 | 39 | Another example:: 40 | 41 | @register(FactoryProvider, 'my_factory') 42 | class MyClass: 43 | pass 44 | 45 | graph = Graph() 46 | scan_to_graph([__package__], graph) 47 | assert isinstance(graph.get('my_factory'), MyClass) 48 | """ 49 | def decorator(target): 50 | def callback(scanner, name, target): 51 | if not args: 52 | specification = target 53 | elif len(args) == 1: 54 | specification = args[0] 55 | else: 56 | specification = tuple(args) 57 | scanner.callback( 58 | specification, 59 | provider_factory(target, **kwargs) 60 | ) 61 | venusian.attach(target, callback, category=WIRING_VENUSIAN_CATEGORY) 62 | return target 63 | return decorator 64 | 65 | 66 | def factory(*args, **kwargs): 67 | """ 68 | A shortcut for using :py:func:`register` with 69 | :py:class:`wiring.providers.FactoryProvider`. 70 | 71 | Example:: 72 | 73 | from wiring.scanning import register 74 | 75 | @register.factory() 76 | class MyClass: 77 | pass 78 | """ 79 | return register(FactoryProvider, *args, **kwargs) 80 | 81 | 82 | def function(*args, **kwargs): 83 | """ 84 | A shortcut for using :py:func:`register` with 85 | :py:class:`wiring.providers.FunctionProvider`. 86 | 87 | Example:: 88 | 89 | from wiring.scanning import register 90 | 91 | @register.function() 92 | def my_function(): 93 | pass 94 | """ 95 | return register(FunctionProvider, *args, **kwargs) 96 | 97 | 98 | def instance(*args, **kwargs): 99 | """ 100 | A shortcut for using :py:func:`register` with 101 | :py:class:`wiring.providers.FunctionProvider`. 102 | 103 | Example:: 104 | 105 | from wiring.scanning import register 106 | 107 | class MyGlobal: 108 | pass 109 | 110 | my_global = MyGlobal() 111 | register.instance('my_global')(my_global) 112 | """ 113 | return register(InstanceProvider, *args, **kwargs) 114 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Wiring 2 | ====== 3 | 4 | .. image:: http://img.shields.io/pypi/v/wiring.svg?style=flat 5 | :target: https://pypi.python.org/pypi/wiring/ 6 | :alt: Latest Version 7 | 8 | .. image:: http://img.shields.io/pypi/l/wiring.svg?style=flat 9 | :target: https://pypi.python.org/pypi/wiring/ 10 | :alt: License 11 | 12 | .. image:: http://img.shields.io/travis/msiedlarek/wiring.svg?style=flat 13 | :target: https://travis-ci.org/msiedlarek/wiring 14 | :alt: Last Build 15 | 16 | .. image:: http://img.shields.io/coveralls/msiedlarek/wiring.svg?style=flat 17 | :target: https://coveralls.io/r/msiedlarek/wiring 18 | :alt: Test Coverage 19 | 20 | .. image:: https://readthedocs.org/projects/wiring/badge/?style=flat 21 | :target: http://wiring.readthedocs.org 22 | :alt: Documentation 23 | 24 | .. A line break to separate badges from description. 25 | 26 | | 27 | 28 | **Wiring provides architectural foundation for Python applications**, 29 | featuring: 30 | 31 | * dependency injection 32 | * interface definition and validation 33 | * modular component configuration 34 | * small, extremely pedantic codebase 35 | 36 | Wiring is supported and tested on Python 2.7, Python 3.4, Python 3.5, PyPy and 37 | PyPy 3. 38 | 39 | Source code and issue tracker are `available at GitHub`_. 40 | 41 | Support is provided on a best-effort basis, through `Stack Overflow 42 | `_. Please tag your question `wiring`. 43 | 44 | This documentation is for Wiring version |release|. Wiring's versioning scheme 45 | fully complies with `Semantic Versioning 2.0`_ specification. 46 | 47 | .. _Semantic Versioning 2.0: http://semver.org/spec/v2.0.0.html 48 | .. _available at GitHub: https://github.com/msiedlarek/wiring 49 | 50 | Quick Peek 51 | ---------- 52 | 53 | .. code-block:: python 54 | 55 | import wiring 56 | from wiring import provides, scope, inject, injected, implements 57 | 58 | class DatabaseModule(wiring.Module): 59 | @provides('db_connection') 60 | @scope(wiring.ThreadScope) 61 | def provide_db_connection(self, database_url=injected('database_url')): 62 | return db_engine.connect(database_url) 63 | 64 | class IUserManager(wiring.Interface): 65 | def get(id): 66 | """Get user by ID.""" 67 | 68 | @implements(IUserManager) 69 | class DefaultUserManager(object): 70 | 71 | @inject('db_connection') 72 | def __init__(self, db_connection): 73 | self.db = db_connection 74 | 75 | def get(self, id): 76 | return self.db.sql('SELECT * FROM users WHERE id = :id', id=id) 77 | 78 | class UserModule(wiring.Module): 79 | factories = { 80 | IUserManager: DefaultUserManager, 81 | } 82 | 83 | graph = wiring.Graph() 84 | DatabaseModule().add_to(graph) 85 | UserModule().add_to(graph) 86 | graph.register_instance('database_url', 'sqlite://some.db') 87 | graph.validate() 88 | 89 | user_manager = graph.get(IUserManager) 90 | user = user_manager.get(12) 91 | 92 | Support 93 | ------- 94 | 95 | If you have a question about Wiring please `post it on Stack Overflow 96 | `_ tagging it `wiring`. I'll try my 97 | best to help you whenever I find time. 98 | 99 | Contents 100 | -------- 101 | 102 | .. toctree:: 103 | :maxdepth: 2 104 | 105 | installation 106 | rationale 107 | dependency_injection 108 | interfaces 109 | license 110 | glossary 111 | api 112 | 113 | Indices and tables 114 | ------------------ 115 | 116 | * :ref:`genindex` 117 | * :ref:`modindex` 118 | * :ref:`search` 119 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Wiring 2 | ****** 3 | 4 | .. image:: http://img.shields.io/pypi/v/wiring.svg?style=flat 5 | :target: https://pypi.python.org/pypi/wiring/ 6 | .. image:: http://img.shields.io/pypi/l/wiring.svg?style=flat 7 | :target: https://pypi.python.org/pypi/wiring/ 8 | .. image:: http://img.shields.io/travis/msiedlarek/wiring.svg?style=flat 9 | :target: https://travis-ci.org/msiedlarek/wiring 10 | .. image:: http://img.shields.io/coveralls/msiedlarek/wiring.svg?style=flat 11 | :target: https://coveralls.io/r/msiedlarek/wiring 12 | .. image:: https://readthedocs.org/projects/wiring/badge/?style=flat 13 | :target: http://wiring.readthedocs.org 14 | 15 | **Wiring provides architectural foundation for Python applications**, 16 | featuring: 17 | 18 | * dependency injection 19 | * interface definition and validation 20 | * modular component configuration 21 | * small, extremely pedantic codebase 22 | 23 | Wiring is supported and tested on Python 2.7, Python 3.4, Python 3.5, PyPy and 24 | PyPy 3. 25 | 26 | Quick Peek 27 | ========== 28 | 29 | .. code-block:: python 30 | 31 | import wiring 32 | from wiring import provides, scope, inject, injected, implements 33 | 34 | class DatabaseModule(wiring.Module): 35 | @provides('db_connection') 36 | @scope(wiring.ThreadScope) 37 | def provide_db_connection(self, database_url=injected('database_url')): 38 | return db_engine.connect(database_url) 39 | 40 | class IUserManager(wiring.Interface): 41 | def get(id): 42 | """Get user by ID.""" 43 | 44 | @implements(IUserManager) 45 | class DefaultUserManager(object): 46 | 47 | @inject('db_connection') 48 | def __init__(self, db_connection): 49 | self.db = db_connection 50 | 51 | def get(self, id): 52 | return self.db.sql('SELECT * FROM users WHERE id = :id', id=id) 53 | 54 | class UserModule(wiring.Module): 55 | factories = { 56 | IUserManager: DefaultUserManager, 57 | } 58 | 59 | graph = wiring.Graph() 60 | DatabaseModule().add_to(graph) 61 | UserModule().add_to(graph) 62 | graph.register_instance('database_url', 'sqlite://some.db') 63 | graph.validate() 64 | 65 | user_manager = graph.get(IUserManager) 66 | user = user_manager.get(12) 67 | 68 | Documentation 69 | ============= 70 | 71 | Full documentation is available at `wiring.readthedocs.org 72 | `_. 73 | 74 | Support 75 | ======= 76 | 77 | Support is provided on a best-effort basis, through `Stack Overflow 78 | `_. Please tag your question *wiring*. 79 | 80 | For commercial support, please contact me directly at mikolaj@siedlarek.pl. 81 | 82 | Development 83 | =========== 84 | 85 | You can install package for development and testing with:: 86 | 87 | virtualenv environment 88 | . environment/bin/activate 89 | pip install sphinx tox flake8 wheel sphinx_rtd_theme 90 | pip install -e . 91 | 92 | To run the test suite on supported Python versions use:: 93 | 94 | tox 95 | 96 | Or on a single version:: 97 | 98 | tox -e py27 99 | 100 | To validate PEP8 compliance and run code static checking:: 101 | 102 | tox -e flake8 103 | 104 | To generate test coverage report:: 105 | 106 | rm -rf .coverage coverage 107 | tox -- --with-coverage 108 | open coverage/index.html 109 | 110 | To generate html documentation:: 111 | 112 | cd docs 113 | make html 114 | open _build/html/index.html 115 | 116 | To release:: 117 | 118 | git tag -s -u gpgkey@example.com v0.4.0 119 | python setup.py register 120 | python setup.py sdist upload -s -i gpgkey@example.com 121 | python setup.py bdist_wheel upload -s -i gpgkey@example.com 122 | git push origin v0.4.0 123 | 124 | License 125 | ======= 126 | 127 | Copyright 2014-2015 Mikołaj Siedlarek 128 | 129 | Licensed under the Apache License, Version 2.0 (the "License"); 130 | you may not use this software except in compliance with the License. 131 | You may obtain a copy of the License at 132 | 133 | http://www.apache.org/licenses/LICENSE-2.0 134 | 135 | Unless required by applicable law or agreed to in writing, software 136 | distributed under the License is distributed on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 138 | See the License for the specific language governing permissions and 139 | limitations under the License. 140 | -------------------------------------------------------------------------------- /wiring/providers.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import functools 3 | 4 | from wiring import interface 5 | from wiring.dependency import get_dependencies 6 | 7 | 8 | __all__ = ( 9 | 'IProvider', 10 | 'FactoryProvider', 11 | 'FunctionProvider', 12 | 'InstanceProvider', 13 | ) 14 | 15 | 16 | class IProvider(interface.Interface): 17 | """ 18 | Interface defining a :term:`provider` object. 19 | """ 20 | 21 | dependencies = """ 22 | A dictionary of provider dependencies, required to provide an object. 23 | A mapping of:: 24 | 25 | [argument index/name] -> [specification] 26 | """ 27 | 28 | scope = """ 29 | The type of :term:`scope` that should manage how provided instances will be 30 | cached, or None when not applicable. 31 | """ 32 | 33 | def __call__(*args, **kwargs): 34 | """ 35 | Called with dependencies (specified in :py:attr:`dependencies` 36 | attribute) as arguments (and possibly other, additional arguments) 37 | returns a provided object. Provider is not responsible for caching 38 | objects in scopes and should never return a cached object. 39 | """ 40 | 41 | 42 | class ProviderBase(object): 43 | 44 | def __init__(self): 45 | self.dependencies = {} 46 | self.scope = None 47 | 48 | 49 | @interface.implements(IProvider) 50 | class FactoryProvider(ProviderBase): 51 | """ 52 | A :term:`provider` that wraps a :py:attr:`factory` callable and when called 53 | passes required dependencies to it and returns its result. 54 | 55 | For example:: 56 | 57 | class DBConnection(object): 58 | def __init__(self, url=injected('db_url')): 59 | self._connection = engine.open(url) 60 | 61 | graph = Graph() 62 | graph.register_instance('db_url', 'somedb://localhost') 63 | graph.register_provider('db_connection', FactoryProvider(DBConnection)) 64 | 65 | # `url` is automatically injected from the graph 66 | db_connection = graph.get('db_connection') 67 | """ 68 | 69 | def __init__(self, factory, scope=None): 70 | super(FactoryProvider, self).__init__() 71 | self.dependencies = get_dependencies(factory) 72 | self.factory = factory 73 | """A callable that returns an object to be provided.""" 74 | self.scope = scope 75 | 76 | def __call__(self, *args, **kwargs): 77 | return self.factory(*args, **kwargs) 78 | 79 | 80 | @interface.implements(IProvider) 81 | class FunctionProvider(ProviderBase): 82 | """ 83 | A :term:`provider` that wraps a :py:attr:`function` to provide a version of 84 | it with automatically injected dependencies, as defined in 85 | :py:attr:`dependencies` attribute. 86 | 87 | For example:: 88 | 89 | def get_user(id, db_connection=injected('db_connection')): 90 | return db_connection.sql( 91 | 'SELECT * FROM users WHERE id = :id', 92 | id=id 93 | ) 94 | 95 | graph = Graph() 96 | graph.register_instance('db_connection', connection) 97 | graph.register_provider('get_user', FunctionProvider(get_user)) 98 | 99 | # Database connection is automatically injected from the object graph, 100 | # but we provide `id` manually. 101 | user = graph.get('get_user')(12) 102 | """ 103 | 104 | def __init__(self, function, scope=None): 105 | super(FunctionProvider, self).__init__() 106 | self.dependencies = get_dependencies(function) 107 | self.function = function 108 | """Wrapped function object.""" 109 | self.scope = scope 110 | 111 | def __call__(self, *args, **kwargs): 112 | @functools.wraps(self.function) 113 | def wrapper(*call_args, **call_kwargs): 114 | # Make sure they're not a tuple and copy them at the same time. 115 | target_args = list(args) 116 | target_args[:len(call_args)] = call_args 117 | target_kwargs = copy.copy(kwargs) 118 | target_kwargs.update(call_kwargs) 119 | return self.function(*target_args, **target_kwargs) 120 | return wrapper 121 | 122 | 123 | @interface.implements(IProvider) 124 | class InstanceProvider(ProviderBase): 125 | """ 126 | A :term:`provider` wrapping already created singleton object. Always 127 | returns the object given to the constructor. 128 | """ 129 | 130 | def __init__(self, instance): 131 | super(InstanceProvider, self).__init__() 132 | self.instance = instance 133 | """A single object that will be provided on every call.""" 134 | 135 | def __call__(self, *args, **kwargs): 136 | return self.instance 137 | -------------------------------------------------------------------------------- /tests/all/providers_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from wiring.dependency import inject, injected 4 | from wiring.providers import ( 5 | FactoryProvider, 6 | FunctionProvider, 7 | InstanceProvider, 8 | IProvider 9 | ) 10 | 11 | from . import ModuleTest 12 | 13 | 14 | class ProvidersModuleTest(ModuleTest): 15 | module = 'wiring.providers' 16 | 17 | 18 | class FactoryProviderTest(unittest.TestCase): 19 | 20 | def test_basic(self): 21 | def factory(): 22 | return 42 23 | provider = FactoryProvider(factory) 24 | IProvider.check_compliance(provider) 25 | self.assertDictEqual(provider.dependencies, {}) 26 | self.assertEqual(provider(), 42) 27 | 28 | def test_dependencies(self): 29 | @inject(injected(12), None, ('foo', 14), foobar=4) 30 | def function(first, second, third, foo=injected('test'), 31 | foobar=None): 32 | return (first, second, third, foo, foobar) 33 | provider = FactoryProvider(function) 34 | self.assertDictEqual( 35 | provider.dependencies, 36 | { 37 | 0: 12, 38 | 2: ('foo', 14), 39 | 'foo': 'test', 40 | 'foobar': 4, 41 | } 42 | ) 43 | self.assertTupleEqual( 44 | provider(1, 2, 3, 4, 5), 45 | (1, 2, 3, 4, 5) 46 | ) 47 | 48 | def test_class(self): 49 | class TestClass(object): 50 | 51 | @inject(injected(12), None, ('foo', 14), foobar=4) 52 | def __init__(self, second, third, foo=injected('test'), 53 | foobar=None): 54 | self.arguments = (second, third, foo) 55 | 56 | provider = FactoryProvider(TestClass) 57 | self.assertDictEqual( 58 | provider.dependencies, 59 | { 60 | 0: 12, 61 | 2: ('foo', 14), 62 | 'foo': 'test', 63 | 'foobar': 4, 64 | } 65 | ) 66 | self.assertIsInstance( 67 | provider(1, 2, 3,), 68 | TestClass 69 | ) 70 | self.assertTupleEqual( 71 | provider(1, 2, 3,).arguments, 72 | (1, 2, 3) 73 | ) 74 | 75 | 76 | class FunctionProviderTest(unittest.TestCase): 77 | 78 | def test(self): 79 | def foo(bar, foobar=injected('test')): 80 | return bar + foobar 81 | provider = FunctionProvider(foo) 82 | IProvider.check_compliance(provider) 83 | self.assertEqual(provider.function, foo) 84 | self.assertDictEqual( 85 | provider.dependencies, 86 | { 87 | 'foobar': 'test', 88 | } 89 | ) 90 | wrapped_function = provider(foobar=12) 91 | self.assertEqual(wrapped_function(5), 17) 92 | self.assertEqual(wrapped_function(6), 18) 93 | self.assertEqual(wrapped_function(-2), 10) 94 | wrapped_function = provider(foobar=1) 95 | self.assertEqual(wrapped_function(6), 7) 96 | self.assertEqual(wrapped_function(-2), -1) 97 | 98 | def test_variable_arguments(self): 99 | """ 100 | This test is related to a problem with variable number of arguments 101 | and FunctionProvider, see issue #4. 102 | """ 103 | def foo(*args): 104 | return tuple(args) 105 | provider = FunctionProvider(foo) 106 | wrapped_function = provider() 107 | self.assertSequenceEqual(wrapped_function(1, 2), (1, 2)) 108 | self.assertSequenceEqual(wrapped_function(1), (1,)) 109 | 110 | def test_kwargs_copy(self): 111 | def foo(*args, **kwargs): 112 | return tuple(args), dict(kwargs) 113 | 114 | provider = FunctionProvider(foo) 115 | wrapped_function = provider() 116 | self.assertSequenceEqual( 117 | wrapped_function(1, 2, test=1), 118 | ((1, 2), {'test': 1}) 119 | ) 120 | self.assertSequenceEqual( 121 | wrapped_function(1, 2, test=2), 122 | ((1, 2), {'test': 2}) 123 | ) 124 | self.assertSequenceEqual( 125 | wrapped_function(1, 2), 126 | ((1, 2), {}) 127 | ) 128 | 129 | provider = FunctionProvider(foo) 130 | wrapped_function = provider() 131 | self.assertSequenceEqual( 132 | wrapped_function(1, 2, test=1), 133 | ((1, 2), {'test': 1}) 134 | ) 135 | self.assertSequenceEqual( 136 | wrapped_function(test=2), 137 | ((), {'test': 2}) 138 | ) 139 | 140 | 141 | class InstanceProviderTest(unittest.TestCase): 142 | 143 | def test(self): 144 | instance = object() 145 | provider = InstanceProvider(instance) 146 | IProvider.check_compliance(provider) 147 | self.assertDictEqual(provider.dependencies, {}) 148 | self.assertEqual(provider(), instance) 149 | -------------------------------------------------------------------------------- /tests/all/scopes_tests.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | import threading 3 | import unittest 4 | 5 | from wiring.scopes import IScope, ProcessScope, SingletonScope, ThreadScope 6 | 7 | from . import ModuleTest 8 | 9 | 10 | class ScopesModuleTest(ModuleTest): 11 | module = 'wiring.scopes' 12 | 13 | 14 | class SingletonScopeTest(unittest.TestCase): 15 | 16 | def test_interface(self): 17 | IScope.check_compliance(SingletonScope()) 18 | 19 | def test(self): 20 | scope1 = SingletonScope() 21 | scope2 = SingletonScope() 22 | 23 | scope1['foo'] = 12 24 | scope2['bar'] = 15 25 | 26 | self.assertNotIn('foo', scope2) 27 | self.assertIn('foo', scope1) 28 | self.assertEqual(scope1['foo'], 12) 29 | scope1['foo'] = 13 30 | self.assertEqual(scope1['foo'], 13) 31 | 32 | self.assertNotIn('bar', scope1) 33 | self.assertIn('bar', scope2) 34 | self.assertEqual(scope2['bar'], 15) 35 | scope2['bar'] = 16 36 | self.assertEqual(scope2['bar'], 16) 37 | 38 | def thread_function(): 39 | self.assertNotIn('foo', scope2) 40 | self.assertIn('foo', scope1) 41 | self.assertEqual(scope1['foo'], 13) 42 | scope1['foo'] = 14 43 | self.assertEqual(scope1['foo'], 14) 44 | 45 | self.assertNotIn('bar', scope1) 46 | self.assertIn('bar', scope2) 47 | self.assertEqual(scope2['bar'], 16) 48 | scope2['bar'] = 17 49 | self.assertEqual(scope2['bar'], 17) 50 | 51 | thread = threading.Thread(target=thread_function) 52 | thread.start() 53 | thread.join(10) 54 | 55 | self.assertEqual(scope1['foo'], 14) 56 | self.assertEqual(scope2['bar'], 17) 57 | 58 | 59 | class ProcessScopeTest(unittest.TestCase): 60 | 61 | def test_interface(self): 62 | IScope.check_compliance(ProcessScope()) 63 | 64 | def test(self): 65 | scope1 = ProcessScope() 66 | scope2 = ProcessScope() 67 | 68 | scope1['foo'] = 12 69 | scope2['bar'] = 15 70 | 71 | self.assertNotIn('foo', scope2) 72 | self.assertIn('foo', scope1) 73 | self.assertEqual(scope1['foo'], 12) 74 | scope1['foo'] = 13 75 | self.assertEqual(scope1['foo'], 13) 76 | 77 | self.assertNotIn('bar', scope1) 78 | self.assertIn('bar', scope2) 79 | self.assertEqual(scope2['bar'], 15) 80 | scope2['bar'] = 16 81 | self.assertEqual(scope2['bar'], 16) 82 | 83 | def process_function(): 84 | self.assertNotIn('foo', scope1) 85 | self.assertNotIn('foo', scope2) 86 | self.assertNotIn('bar', scope1) 87 | self.assertNotIn('bar', scope2) 88 | 89 | scope1['foo'] = 80 90 | self.assertEqual(scope1['foo'], 80) 91 | scope2['bar'] = 90 92 | self.assertEqual(scope2['bar'], 90) 93 | 94 | process = multiprocessing.Process(target=process_function) 95 | process.start() 96 | process.join(10) 97 | 98 | self.assertEqual(scope1['foo'], 13) 99 | self.assertEqual(scope2['bar'], 16) 100 | 101 | def thread_function(): 102 | self.assertIn('foo', scope1) 103 | self.assertEqual(scope1['foo'], 13) 104 | self.assertIn('bar', scope2) 105 | self.assertEqual(scope2['bar'], 16) 106 | 107 | scope1['from_thread'] = 97 108 | self.assertEqual(scope1['from_thread'], 97) 109 | 110 | thread = threading.Thread(target=thread_function) 111 | thread.start() 112 | thread.join(10) 113 | 114 | self.assertEqual(scope1['from_thread'], 97) 115 | 116 | 117 | class ThreadScopeTest(unittest.TestCase): 118 | 119 | def test_interface(self): 120 | IScope.check_compliance(ThreadScope()) 121 | 122 | def test(self): 123 | scope1 = ThreadScope() 124 | scope2 = ThreadScope() 125 | 126 | scope1['foo'] = 12 127 | scope2['bar'] = 15 128 | 129 | self.assertNotIn('foo', scope2) 130 | self.assertIn('foo', scope1) 131 | self.assertEqual(scope1['foo'], 12) 132 | scope1['foo'] = 13 133 | self.assertEqual(scope1['foo'], 13) 134 | 135 | self.assertNotIn('bar', scope1) 136 | self.assertIn('bar', scope2) 137 | self.assertEqual(scope2['bar'], 15) 138 | scope2['bar'] = 16 139 | self.assertEqual(scope2['bar'], 16) 140 | 141 | def thread_function(): 142 | self.assertNotIn('foo', scope1) 143 | self.assertNotIn('foo', scope2) 144 | self.assertNotIn('bar', scope1) 145 | self.assertNotIn('bar', scope2) 146 | 147 | scope1['foo'] = 80 148 | self.assertEqual(scope1['foo'], 80) 149 | scope2['bar'] = 90 150 | self.assertEqual(scope2['bar'], 90) 151 | 152 | thread = threading.Thread(target=thread_function) 153 | thread.start() 154 | thread.join(10) 155 | 156 | self.assertEqual(scope1['foo'], 13) 157 | self.assertEqual(scope2['bar'], 16) 158 | -------------------------------------------------------------------------------- /docs/interfaces.rst: -------------------------------------------------------------------------------- 1 | Interfaces Guide 2 | ================ 3 | 4 | .. note:: 5 | 6 | To understand what interfaces are and why are they useful, head to the 7 | :doc:`rationale`. 8 | 9 | When building a large, modular Python application it's easy to lost track of 10 | what exactly this function argument can be or which properties of that class 11 | are used where. For a big application that does not fit entirely into 12 | a single programmer's mind, lack of static typing can be a problem. 13 | 14 | Dependency injection, with all its merits, also makes this problem worse. When 15 | you're only marking some argument as a ``'db_connection'``, you're not really 16 | helping other programmers reason about it's properties. What attributes does 17 | this object have? What methods? 18 | 19 | And what if you want to switch to a new database engine, and need to write 20 | a new ``'db_connection'``? How do you know what properties should it have? Even 21 | if you'll find the original implementation, it may not be obvious which 22 | methods, and particularly, which attributes are part of its API. 23 | 24 | That's why Wiring provides a hint of static typing for Python - interface 25 | system - as a natural counterweight for its dependency injection feature. 26 | 27 | Defining Interfaces 28 | ------------------- 29 | 30 | :term:`Interfaces ` are classes that describe what attributes and 31 | methods some object has. Let's jump right in with an example interface: 32 | 33 | .. code-block:: python 34 | 35 | from wiring import Interface 36 | 37 | class IUser(Interface): 38 | 39 | email = """User's contact e-mail.""" 40 | 41 | def change_password(old_password, new_password): 42 | """ 43 | Changes user's password to `new_password`, providing that 44 | `old_password` is his valid, current password. 45 | """ 46 | 47 | Interfaces are defined by declaring classes inheriting from 48 | :py:class:`Interface `. Their names should start 49 | with a capital ``I`` to easily distinguish between them and their 50 | implementations. 51 | 52 | This interface declares two requirements that an object must meet to be 53 | considered its implementation: 54 | 55 | #. It needs to have an `email` property. 56 | #. It needs to have a `change_password` method with two arguments named exactly 57 | `old_password` and `new_password`. 58 | 59 | Both argument and method declaration can be easily documented with docstrings. 60 | 61 | Implementing Interfaces 62 | ----------------------- 63 | 64 | Implementing class of objects conforming to the interface is rather 65 | straightforward: 66 | 67 | .. code-block:: python 68 | 69 | from wiring import Interface, implements 70 | 71 | class IUser(Interface): 72 | 73 | email = """User's contact e-mail.""" 74 | 75 | def change_password(old_password, new_password): 76 | """ 77 | Changes user's password to `new_password`, providing that 78 | `old_password` is he's valid, current password. 79 | """ 80 | 81 | @implements(IUser) 82 | class User(object): 83 | 84 | def __init__(self): 85 | self.email = None 86 | 87 | def change_password(self, old_password, new_password): 88 | # ... 89 | 90 | There are three important things to notice here: 91 | 92 | #. Interface describes **properties of an object, not of a class**. Notice that 93 | `email` attribute belongs to a `User` instance, not the class. 94 | #. Interface describes API of an object, not its implementation. Notice that 95 | there is no `self` argument in interface definition of `change_password` 96 | method. That's because a user of the API doesn't have to actually provide 97 | it. 98 | #. To implement interface you just need to create an object conforming to it. 99 | There is an :py:func:`@implements ` decorator, 100 | but it is purely optional. An object doesn't have to be an instance of 101 | a class annotated with this decorator to be considered an implementation of 102 | the interface. Using the decorator is considered a good practice, as it aids 103 | other programmers in reasoning about the class and can actually serve as its 104 | documentation. 105 | 106 | Validating Interfaces 107 | --------------------- 108 | 109 | Unlike in statically-typed languages, interface implementations are not 110 | implicitly validated. That's because just by looking at a class in Python we 111 | cannot determine what properties will its instance have. That's why it is 112 | recommended to construct an object of the class and run it through interface 113 | validation as part of your unit tests. 114 | 115 | .. code-block:: python 116 | 117 | user = User() 118 | IUser.check_compliance(user) 119 | 120 | The :py:meth:`Interface.check_compliance() 121 | ` method will raise 122 | :py:exc:`InterfaceComplianceError ` 123 | if it detects any errors in the implementation. 124 | 125 | Using Interfaces with Dependency Injection 126 | ------------------------------------------ 127 | 128 | Interfaces make perfect companions to the dependency injection pattern - they 129 | serve as fantastic :term:`specifications `. To lear why, please 130 | read the :ref:`Powers Combined section of Rationale 131 | `. 132 | -------------------------------------------------------------------------------- /sphinx_wiring.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | from sphinx.domains.python import PyClasslike, PyXRefRole 4 | from sphinx.ext import autodoc 5 | from sphinx.locale import _ 6 | from sphinx.util import force_decode 7 | from sphinx.util.docstrings import prepare_docstring 8 | 9 | from wiring.interface import Interface, Method, get_implemented_interfaces 10 | 11 | 12 | class InterfaceDesc(PyClasslike): 13 | 14 | def get_index_text(self, modname, name_cls): 15 | return '{name} (interface in {module})'.format( 16 | name=name_cls[0], 17 | module=modname 18 | ) 19 | 20 | 21 | class InterfaceDocumenter(autodoc.ClassDocumenter): 22 | 23 | objtype = 'interface' 24 | member_order = 20 25 | 26 | def __init__(self, *args, **kwargs): 27 | super(InterfaceDocumenter, self).__init__(*args, **kwargs) 28 | self.options.show_inheritance = True 29 | 30 | def add_directive_header(self, sig): 31 | if self.doc_as_attr: 32 | self.directivetype = 'attribute' 33 | autodoc.Documenter.add_directive_header(self, sig) 34 | bases = [ 35 | base for base in self.object.__bases__ if base is not Interface 36 | ] 37 | if not self.doc_as_attr and self.options.show_inheritance and bases: 38 | self.add_line(u'', '') 39 | bases = [ 40 | u':class:`{module}.{name}`'.format( 41 | module=base.__module__, 42 | name=base.__name__ 43 | ) 44 | for base in bases 45 | ] 46 | self.add_line( 47 | u' Extends: {}'.format(', '.join(bases)), 48 | '' 49 | ) 50 | 51 | def format_args(self): 52 | return '' 53 | 54 | def document_members(self, all_members=True): 55 | oldindent = self.indent 56 | 57 | members = list(self.object.attributes.items()) 58 | if self.options.members is not autodoc.ALL: 59 | specified = [] 60 | for line in (self.options.members or []): 61 | specified.extend(line.split()) 62 | members = { 63 | name: value for name, value in members if name in specified 64 | } 65 | 66 | member_order = ( 67 | self.options.member_order or self.env.config.autodoc_member_order 68 | ) 69 | if member_order == 'alphabetical': 70 | members.sort() 71 | if member_order == 'groupwise': 72 | members.sort(key=lambda e: isinstance(e[1], Method)) 73 | elif member_order == 'bysource' and self.analyzer: 74 | name = self.object.__name__ 75 | def keyfunc(entry): 76 | return self.analyzer.tagorder.get( 77 | '.'.join((name, entry[0])), 78 | len(self.analyzer.tagorder) 79 | ) 80 | members.sort(key=keyfunc) 81 | 82 | for name, specification in members: 83 | self.add_line(u'', '') 84 | if isinstance(specification, Method): 85 | self.add_line( 86 | u'.. method:: {name}{arguments}'.format( 87 | name=name, 88 | arguments=inspect.formatargspec( 89 | *specification.argument_specification 90 | ) 91 | ), 92 | '' 93 | ) 94 | else: 95 | self.add_line( 96 | u'.. attribute:: {name}'.format(name=name), 97 | '' 98 | ) 99 | 100 | doc = specification.docstring 101 | if doc: 102 | self.add_line(u'', '') 103 | self.indent += self.content_indent 104 | sourcename = u'docstring of %s.%s' % (self.fullname, name) 105 | docstrings = [prepare_docstring(force_decode(doc, None))] 106 | for i, line in enumerate(self.process_doc(docstrings)): 107 | self.add_line(line, sourcename, i) 108 | self.add_line(u'', '') 109 | self.indent = oldindent 110 | 111 | 112 | def class_interface_documenter(app, what, name, obj, options, lines): 113 | if what != 'class': 114 | return 115 | if options.show_interfaces: 116 | interfaces = [ 117 | u':py:interface:`{name} <{module}.{name}>`'.format( 118 | module=interface.__module__, 119 | name=interface.__name__ 120 | ) 121 | for interface in get_implemented_interfaces(obj) 122 | ] 123 | lines.insert( 124 | 0, 125 | _(u'Implements: {}').format(', '.join(interfaces)) 126 | ) 127 | lines.insert(1, u'') 128 | 129 | 130 | def method_interface_documenter(app, what, name, obj, options, lines): 131 | if what != 'method': 132 | return 133 | if lines: 134 | return 135 | if not getattr(obj, 'im_class'): 136 | return 137 | interfaces = get_implemented_interfaces(obj.im_class) 138 | for interface in interfaces: 139 | if obj.__name__ in interface.attributes: 140 | docstring = interface.attributes[obj.__name__].docstring 141 | lines.extend( 142 | prepare_docstring(force_decode(docstring, None)) 143 | ) 144 | 145 | 146 | def setup(app): 147 | app.add_directive_to_domain('py', 'interface', InterfaceDesc) 148 | app.add_role_to_domain('py', 'interface', PyXRefRole()) 149 | app.add_autodocumenter(InterfaceDocumenter) 150 | 151 | autodoc.ClassDocumenter.option_spec['show-interfaces'] = lambda arg: True 152 | 153 | app.connect('autodoc-process-docstring', class_interface_documenter) 154 | app.connect('autodoc-process-docstring', method_interface_documenter) 155 | -------------------------------------------------------------------------------- /tests/all/configuration_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from wiring.configuration import ( 4 | InvalidConfigurationError, 5 | Module, 6 | provides, 7 | scope 8 | ) 9 | from wiring.dependency import inject 10 | from wiring.graph import Graph 11 | from wiring.providers import ( 12 | FactoryProvider, 13 | FunctionProvider, 14 | InstanceProvider 15 | ) 16 | from wiring.scopes import ProcessScope 17 | 18 | from . import ModuleTest 19 | 20 | 21 | class DependencyModuleTest(ModuleTest): 22 | module = 'wiring.configuration' 23 | 24 | 25 | class ModuleTest(unittest.TestCase): 26 | 27 | def test(self): 28 | class SomeClass(object): 29 | pass 30 | 31 | def foobar(): 32 | return 7 33 | 34 | class SomeModule(Module): 35 | providers = { 36 | 'test': InstanceProvider('test'), 37 | } 38 | instances = { 39 | 'foo': 12, 40 | } 41 | factories = { 42 | 'bar': SomeClass, 43 | 'singleton': (SomeClass, ProcessScope), 44 | } 45 | functions = { 46 | 'foobar': foobar, 47 | } 48 | 49 | @provides('fizz') 50 | def provide_fizz(self): 51 | return 'fizz!' 52 | 53 | @provides('buzz', 12) 54 | @scope(ProcessScope) 55 | def provide_buzz_12(self): 56 | return 'buzz12!' 57 | 58 | self.assertSetEqual( 59 | set(SomeModule.providers.keys()), 60 | {'test', 'foo', 'bar', 'singleton', 'foobar'} 61 | ) 62 | self.assertIsInstance(SomeModule.providers['test'], InstanceProvider) 63 | self.assertIsInstance(SomeModule.providers['foo'], InstanceProvider) 64 | self.assertIsInstance(SomeModule.providers['bar'], FactoryProvider) 65 | self.assertIsInstance( 66 | SomeModule.providers['singleton'], 67 | FactoryProvider 68 | ) 69 | self.assertIsInstance(SomeModule.providers['foobar'], FunctionProvider) 70 | 71 | graph = Graph() 72 | module = SomeModule() 73 | 74 | module.add_to(graph) 75 | 76 | self.assertSetEqual( 77 | set(graph.providers.keys()), 78 | {'test', 'foo', 'bar', 'singleton', 'foobar', 'fizz', ('buzz', 12)} 79 | ) 80 | self.assertIsInstance(graph.providers['test'], InstanceProvider) 81 | self.assertIsInstance(graph.providers['foo'], InstanceProvider) 82 | self.assertIsInstance(graph.providers['bar'], FactoryProvider) 83 | self.assertIsInstance( 84 | graph.providers['singleton'], 85 | FactoryProvider 86 | ) 87 | self.assertIs(graph.get('singleton'), graph.get('singleton')) 88 | self.assertIsInstance(graph.providers['foobar'], FunctionProvider) 89 | self.assertIsInstance(graph.providers[('buzz', 12)], FactoryProvider) 90 | self.assertIs( 91 | graph.providers[('buzz', 12)].scope, 92 | ProcessScope 93 | ) 94 | self.assertIsInstance(graph.providers['fizz'], FactoryProvider) 95 | self.assertIsNone(graph.providers['fizz'].scope) 96 | 97 | self.assertEqual(graph.get('fizz'), 'fizz!') 98 | self.assertEqual(graph.get(('buzz', 12)), 'buzz12!') 99 | 100 | def test_duplicate_validation(self): 101 | with self.assertRaises(InvalidConfigurationError) as cm: 102 | class SomeModule(Module): 103 | providers = { 104 | 'foo': InstanceProvider(11), 105 | } 106 | instances = { 107 | 'foo': 12, 108 | } 109 | 110 | self.assertEqual(cm.exception.module.__name__, 'SomeModule') 111 | self.assertIn("Multiple sources", cm.exception.message) 112 | self.assertIn("foo", cm.exception.message) 113 | self.assertIn(cm.exception.message, str(cm.exception)) 114 | 115 | with self.assertRaises(InvalidConfigurationError) as cm: 116 | class OtherModule(Module): 117 | instances = { 118 | 'fizzbuzz': 12, 119 | } 120 | 121 | @provides('fizzbuzz') 122 | def provide_fizzbuzz(self): 123 | return 13 124 | 125 | self.assertEqual(cm.exception.module.__name__, 'OtherModule') 126 | self.assertIn("Multiple sources", cm.exception.message) 127 | self.assertIn("fizzbuzz", cm.exception.message) 128 | self.assertIn(cm.exception.message, str(cm.exception)) 129 | 130 | def test_factory_args_count_validation(self): 131 | with self.assertRaises(InvalidConfigurationError) as cm: 132 | class WrongNumberModule1(Module): 133 | factories = { 134 | 'foo': (lambda: 12, ProcessScope, 'invalid'), 135 | } 136 | 137 | self.assertEqual(cm.exception.module.__name__, 'WrongNumberModule1') 138 | self.assertIn("Wrong number", cm.exception.message) 139 | self.assertIn("foo", cm.exception.message) 140 | self.assertIn(cm.exception.message, str(cm.exception)) 141 | 142 | with self.assertRaises(InvalidConfigurationError) as cm: 143 | class WrongNumberModule2(Module): 144 | factories = { 145 | 'foo': tuple(), 146 | } 147 | 148 | self.assertEqual(cm.exception.module.__name__, 'WrongNumberModule2') 149 | self.assertIn("Wrong number", cm.exception.message) 150 | self.assertIn("foo", cm.exception.message) 151 | self.assertIn(cm.exception.message, str(cm.exception)) 152 | 153 | def test_provides_positional_arguments(self): 154 | class SomeModule(Module): 155 | instances = { 156 | 'buzz': 12, 157 | } 158 | 159 | @provides('fizzbuzz') 160 | @inject('buzz') 161 | def provide_fizzbuzz(self, buzz): 162 | return 13, buzz 163 | 164 | graph = Graph() 165 | SomeModule().add_to(graph) 166 | 167 | fizz, buzz = graph.get('fizzbuzz') 168 | self.assertEqual(fizz, 13) 169 | self.assertEqual(buzz, 12) 170 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/wiring.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/wiring.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/wiring" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/wiring" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\wiring.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\wiring.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /tests/scanning/scanning_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from wiring import ( 4 | FactoryProvider, 5 | FunctionProvider, 6 | Graph, 7 | InstanceProvider, 8 | Module 9 | ) 10 | from wiring.scanning import scan, scan_to_graph, scan_to_module 11 | 12 | 13 | class ScanningTest(unittest.TestCase): 14 | 15 | def test_module_autoscan(self): 16 | class FirstModule(Module): 17 | scan = ['tests.scanning.testmodule'] 18 | scan_ignore = ['tests.scanning.testmodule.ignoredsubmodule'] 19 | module = FirstModule() 20 | self._validate_providers(module.providers) 21 | 22 | from . import testmodule 23 | 24 | class SecondModule(Module): 25 | scan = [testmodule] 26 | scan_ignore = ['tests.scanning.testmodule.ignoredsubmodule'] 27 | module = FirstModule() 28 | self._validate_providers(module.providers) 29 | 30 | def test_scan(self): 31 | providers = {} 32 | 33 | def callback(specification, provider): 34 | providers[specification] = provider 35 | 36 | scan( 37 | ['tests.scanning.testmodule'], 38 | callback, 39 | ignore=['tests.scanning.testmodule.ignoredsubmodule'] 40 | ) 41 | self._validate_providers(providers) 42 | 43 | from . import testmodule 44 | 45 | providers = {} 46 | 47 | def callback(specification, provider): 48 | providers[specification] = provider 49 | 50 | scan( 51 | [testmodule], 52 | callback, 53 | ignore=['tests.scanning.testmodule.ignoredsubmodule'] 54 | ) 55 | self._validate_providers(providers) 56 | 57 | def test_scan_to_graph(self): 58 | graph = Graph() 59 | scan_to_graph( 60 | ['tests.scanning.testmodule'], 61 | graph, 62 | ignore=['tests.scanning.testmodule.ignoredsubmodule'] 63 | ) 64 | self._validate_providers(graph.providers) 65 | 66 | from . import testmodule 67 | 68 | graph = Graph() 69 | scan_to_graph( 70 | [testmodule], 71 | graph, 72 | ignore=['tests.scanning.testmodule.ignoredsubmodule'] 73 | ) 74 | self._validate_providers(graph.providers) 75 | 76 | def test_scan_to_module(self): 77 | module = Module() 78 | scan_to_module( 79 | ['tests.scanning.testmodule'], 80 | module, 81 | ignore=['tests.scanning.testmodule.ignoredsubmodule'] 82 | ) 83 | self._validate_providers(module.providers) 84 | 85 | from . import testmodule 86 | 87 | module = Module() 88 | scan_to_module( 89 | [testmodule], 90 | module, 91 | ignore=['tests.scanning.testmodule.ignoredsubmodule'] 92 | ) 93 | self._validate_providers(module.providers) 94 | 95 | def _validate_providers(self, providers): 96 | # register.register() 97 | 98 | from .testmodule.plain_register import PlainRegisterFactory 99 | provider = providers[PlainRegisterFactory] 100 | self.assertIsInstance(provider, FactoryProvider) 101 | self.assertIs(provider.factory, PlainRegisterFactory) 102 | 103 | from .testmodule.plain_register import PlainRegisterNamedFactory 104 | provider = providers['plain_register_named_factory'] 105 | self.assertIsInstance(provider, FactoryProvider) 106 | self.assertIs(provider.factory, PlainRegisterNamedFactory) 107 | 108 | from .testmodule.plain_register import PlainRegisterTupleFactory 109 | provider = providers[('plain_register', 'tuple_factory')] 110 | self.assertIsInstance(provider, FactoryProvider) 111 | self.assertIs(provider.factory, PlainRegisterTupleFactory) 112 | 113 | from .testmodule.plain_register import PlainRegisterInstance 114 | provider = providers[PlainRegisterInstance] 115 | self.assertIsInstance(provider, InstanceProvider) 116 | self.assertIs(provider.instance, PlainRegisterInstance) 117 | 118 | from .testmodule.plain_register import PlainRegisterNamedInstance 119 | provider = providers['plain_register_named_instance'] 120 | self.assertIsInstance(provider, InstanceProvider) 121 | self.assertIs(provider.instance, PlainRegisterNamedInstance) 122 | 123 | from .testmodule.plain_register import PlainRegisterTupleInstance 124 | provider = providers[('plain_register', 'tuple_instance')] 125 | self.assertIsInstance(provider, InstanceProvider) 126 | self.assertIs(provider.instance, PlainRegisterTupleInstance) 127 | 128 | # register.factory() 129 | 130 | from .testmodule.register_factory import RegisterFactoryFactory 131 | provider = providers[RegisterFactoryFactory] 132 | self.assertIsInstance(provider, FactoryProvider) 133 | self.assertIs(provider.factory, RegisterFactoryFactory) 134 | 135 | from .testmodule.register_factory import RegisterFactoryNamedFactory 136 | provider = providers['register_factory_named_factory'] 137 | self.assertIsInstance(provider, FactoryProvider) 138 | self.assertIs(provider.factory, RegisterFactoryNamedFactory) 139 | 140 | from .testmodule.register_factory import RegisterFactoryTupleFactory 141 | provider = providers[('register_factory', 'tuple_factory')] 142 | self.assertIsInstance(provider, FactoryProvider) 143 | self.assertIs(provider.factory, RegisterFactoryTupleFactory) 144 | 145 | # register.function() 146 | 147 | from .testmodule.register_function import register_function 148 | provider = providers[register_function] 149 | self.assertIsInstance(provider, FunctionProvider) 150 | self.assertIs(provider.function, register_function) 151 | 152 | from .testmodule.register_function import register_function_name 153 | provider = providers['register_function_named_function'] 154 | self.assertIsInstance(provider, FunctionProvider) 155 | self.assertIs(provider.function, register_function_name) 156 | 157 | from .testmodule.register_function import register_function_tuple 158 | provider = providers[('register_function', 'tuple_function')] 159 | self.assertIsInstance(provider, FunctionProvider) 160 | self.assertIs(provider.function, register_function_tuple) 161 | 162 | # register.instance() 163 | 164 | from .testmodule.register_instance import instance 165 | provider = providers[instance] 166 | self.assertIsInstance(provider, InstanceProvider) 167 | self.assertIs(provider.instance, instance) 168 | 169 | from .testmodule.register_instance import named_instance 170 | provider = providers['register_instance_named_instance'] 171 | self.assertIsInstance(provider, InstanceProvider) 172 | self.assertIs(provider.instance, named_instance) 173 | 174 | from .testmodule.register_instance import tuple_instance 175 | provider = providers[('register_instance', 'tuple_instance')] 176 | self.assertIsInstance(provider, InstanceProvider) 177 | self.assertIs(provider.instance, tuple_instance) 178 | 179 | # submodule 180 | 181 | from .testmodule.testsubmodule.submodule_registers import ( 182 | submodule_function, 183 | SubmoduleFactory, 184 | ) 185 | self.assertIn(submodule_function, providers) 186 | self.assertIn(SubmoduleFactory, providers) 187 | 188 | # ignored 189 | 190 | from .testmodule.ignoredsubmodule.ignored_registers import ( 191 | ignored_function, 192 | IgnoredFactory, 193 | ) 194 | self.assertNotIn(ignored_function, providers) 195 | self.assertNotIn(IgnoredFactory, providers) 196 | -------------------------------------------------------------------------------- /tests/all/dependency_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import six 4 | 5 | from wiring.dependency import ( 6 | Factory, 7 | UnrealizedInjection, 8 | get_dependencies, 9 | inject, 10 | injected 11 | ) 12 | 13 | from . import ModuleTest 14 | 15 | 16 | class DependencyModuleTest(ModuleTest): 17 | module = 'wiring.dependency' 18 | 19 | 20 | class FactoryTest(unittest.TestCase): 21 | 22 | def test(self): 23 | factory = Factory('foo', 2, 3) 24 | self.assertTupleEqual(factory.specification, ('foo', 2, 3)) 25 | self.assertEqual( 26 | repr(factory), 27 | "" 28 | ) 29 | 30 | factory = Factory(42) 31 | self.assertEqual(factory.specification, 42) 32 | self.assertEqual( 33 | repr(factory), 34 | "" 35 | ) 36 | 37 | def test_missing_specification(self): 38 | with self.assertRaises(ValueError): 39 | Factory() 40 | 41 | def test_hash(self): 42 | factory = Factory('test') 43 | self.assertIsInstance(hash(factory), six.integer_types) 44 | factory = Factory([]) 45 | with self.assertRaises(TypeError): 46 | # List is an unhashable type. 47 | hash(factory) 48 | 49 | def test_immutability(self): 50 | factory = Factory('test') 51 | self.assertFalse(hasattr(factory, '__dict__')) 52 | with self.assertRaises(AttributeError): 53 | factory.specification = 'test2' 54 | with self.assertRaises(AttributeError): 55 | factory.foobar = 12 56 | 57 | 58 | class UnrealizedInjectionTest(unittest.TestCase): 59 | 60 | def test(self): 61 | injection = UnrealizedInjection('foo', 2, 3) 62 | self.assertTupleEqual(injection.specification, ('foo', 2, 3)) 63 | self.assertFalse(injection) 64 | self.assertEqual( 65 | repr(injection), 66 | "" 67 | ) 68 | 69 | injection = UnrealizedInjection(42) 70 | self.assertEqual(injection.specification, 42) 71 | self.assertFalse(injection) 72 | self.assertEqual( 73 | repr(injection), 74 | "" 75 | ) 76 | 77 | def test_missing_specification(self): 78 | with self.assertRaises(ValueError): 79 | UnrealizedInjection() 80 | 81 | def test_shortcut(self): 82 | self.assertEqual(injected, UnrealizedInjection) 83 | 84 | def test_hash(self): 85 | injection = UnrealizedInjection('test') 86 | self.assertIsInstance(hash(injection), six.integer_types) 87 | injection = UnrealizedInjection([]) 88 | with self.assertRaises(TypeError): 89 | # List is an unhashable type. 90 | hash(injection) 91 | 92 | def test_immutability(self): 93 | injection = UnrealizedInjection('test') 94 | self.assertFalse(hasattr(injection, '__dict__')) 95 | with self.assertRaises(AttributeError): 96 | injection.specification = 'test2' 97 | with self.assertRaises(AttributeError): 98 | injection.foobar = 12 99 | 100 | 101 | class GetDependenciesTest(unittest.TestCase): 102 | 103 | def test_none(self): 104 | def function(): 105 | pass 106 | self.assertDictEqual( 107 | get_dependencies(function), 108 | {} 109 | ) 110 | 111 | def test_empty(self): 112 | def function(foo): 113 | pass 114 | self.assertDictEqual( 115 | get_dependencies(function), 116 | {} 117 | ) 118 | 119 | def test_preprocessed(self): 120 | def function(foo): 121 | pass 122 | function.__injection__ = {'foo': 42} 123 | self.assertDictEqual( 124 | get_dependencies(function), 125 | {'foo': 42} 126 | ) 127 | 128 | def test_exception(self): 129 | with self.assertRaises(TypeError): 130 | get_dependencies(42) 131 | 132 | def test_keyword_arguments(self): 133 | def function(foo, bar, foobar=UnrealizedInjection('foo', 12), test=12, 134 | other=UnrealizedInjection(42)): 135 | pass 136 | self.assertDictEqual( 137 | get_dependencies(function), 138 | { 139 | 'foobar': ('foo', 12), 140 | 'other': 42, 141 | } 142 | ) 143 | 144 | def test_class(self): 145 | class Foo(object): 146 | def __init__(self, bar=UnrealizedInjection('bar')): 147 | pass 148 | 149 | self.assertDictEqual( 150 | get_dependencies(Foo), 151 | { 152 | 'bar': 'bar', 153 | } 154 | ) 155 | 156 | def test_class_with_new(self): 157 | class Foo(tuple): 158 | __slots__ = [] 159 | 160 | def __new__(self, bar=UnrealizedInjection('bar')): 161 | pass 162 | 163 | self.assertDictEqual( 164 | get_dependencies(Foo), 165 | { 166 | 'bar': 'bar', 167 | } 168 | ) 169 | 170 | def test_class_with_both_init_and_new(self): 171 | class Foo(object): 172 | 173 | def __init__(self, foo=UnrealizedInjection('foo'), **kwargs): 174 | pass 175 | 176 | def __new__(self, bar=UnrealizedInjection('bar'), **kwargs): 177 | pass 178 | 179 | self.assertDictEqual( 180 | get_dependencies(Foo), 181 | { 182 | 'foo': 'foo', 183 | 'bar': 'bar', 184 | } 185 | ) 186 | 187 | 188 | class InjectTest(unittest.TestCase): 189 | 190 | def test_basic(self): 191 | @inject(injected(12), None, ('foo', 14), foobar=4) 192 | def function(first, second, third, foo=injected('test'), 193 | foobar=None): 194 | return (first, second, third, foo, foobar) 195 | 196 | self.assertDictEqual( 197 | function.__injection__, 198 | { 199 | 0: 12, 200 | 2: ('foo', 14), 201 | 'foo': 'test', 202 | 'foobar': 4, 203 | } 204 | ) 205 | self.assertTupleEqual( 206 | function(1, 2, 3, 4, 5), 207 | (1, 2, 3, 4, 5) 208 | ) 209 | 210 | def test_class(self): 211 | class TestClass(object): 212 | 213 | @inject(injected(12), None, ('foo', 14), foobar=4) 214 | def __init__(self, first, second, third, foo=injected('test'), 215 | foobar=None): 216 | pass 217 | 218 | self.assertDictEqual( 219 | TestClass.__init__.__injection__, 220 | { 221 | 0: 12, 222 | 2: ('foo', 14), 223 | 'foo': 'test', 224 | 'foobar': 4, 225 | } 226 | ) 227 | 228 | def test_overriding(self): 229 | @inject(12, None, ('foo', 14), foobar=4) 230 | def function(first, second, third, foobar=injected(7)): 231 | pass 232 | self.assertDictEqual( 233 | function.__injection__, 234 | { 235 | 0: 12, 236 | 2: ('foo', 14), 237 | 'foobar': 4, 238 | } 239 | ) 240 | 241 | def test_removing(self): 242 | @inject(foobar=None) 243 | def function(foobar=injected(7)): 244 | pass 245 | self.assertDictEqual( 246 | function.__injection__, 247 | {} 248 | ) 249 | 250 | def test_recursion(self): 251 | @inject(11) 252 | @inject(foobar=injected(7)) 253 | def function(test, foobar=None): 254 | pass 255 | self.assertDictEqual( 256 | function.__injection__, 257 | { 258 | 0: 11, 259 | 'foobar': 7, 260 | } 261 | ) 262 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # wiring documentation build configuration file, created by 4 | # sphinx-quickstart on Sun Jul 20 11:43:04 2014. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import os 16 | import sys 17 | 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | here = os.path.dirname(__file__) 23 | sys.path.insert(0, os.path.abspath(os.path.join(here, '..'))) 24 | 25 | # on_rtd is whether the docs are being built on readthedocs.org 26 | on_rtd = (os.environ.get('READTHEDOCS', None) == 'True') 27 | 28 | # -- General configuration ------------------------------------------------ 29 | 30 | # If your documentation needs a minimal Sphinx version, state it here. 31 | #needs_sphinx = '1.0' 32 | 33 | # Add any Sphinx extension module names here, as strings. They can be 34 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 35 | # ones. 36 | extensions = [ 37 | 'sphinx.ext.autodoc', 38 | 'sphinx.ext.viewcode', 39 | 'sphinx_wiring', 40 | ] 41 | 42 | # Add any paths that contain templates here, relative to this directory. 43 | templates_path = ['_templates'] 44 | 45 | # The suffix of source filenames. 46 | source_suffix = '.rst' 47 | 48 | # The encoding of source files. 49 | #source_encoding = 'utf-8-sig' 50 | 51 | # The master toctree document. 52 | master_doc = 'index' 53 | 54 | # General information about the project. 55 | project = 'wiring' 56 | copyright = u'2014-2015 Mikołaj Siedlarek' 57 | 58 | # The version info for the project you're documenting, acts as replacement for 59 | # |version| and |release|, also used in various other places throughout the 60 | # built documents. 61 | # 62 | # The short X.Y version. 63 | version = '0.4' 64 | # The full version, including alpha/beta/rc tags. 65 | release = '0.4.0' 66 | 67 | # The language for content autogenerated by Sphinx. Refer to documentation 68 | # for a list of supported languages. 69 | #language = None 70 | 71 | # There are two options for replacing |today|: either, you set today to some 72 | # non-false value, then it is used: 73 | #today = '' 74 | # Else, today_fmt is used as the format for a strftime call. 75 | #today_fmt = '%B %d, %Y' 76 | 77 | # List of patterns, relative to source directory, that match files and 78 | # directories to ignore when looking for source files. 79 | exclude_patterns = ['_build'] 80 | 81 | # The reST default role (used for this markup: `text`) to use for all 82 | # documents. 83 | #default_role = None 84 | 85 | # If true, '()' will be appended to :func: etc. cross-reference text. 86 | #add_function_parentheses = True 87 | 88 | # If true, the current module name will be prepended to all description 89 | # unit titles (such as .. function::). 90 | #add_module_names = True 91 | 92 | # If true, sectionauthor and moduleauthor directives will be shown in the 93 | # output. They are ignored by default. 94 | #show_authors = False 95 | 96 | # The name of the Pygments (syntax highlighting) style to use. 97 | pygments_style = 'sphinx' 98 | 99 | # A list of ignored prefixes for module index sorting. 100 | #modindex_common_prefix = [] 101 | 102 | # If true, keep warnings as "system message" paragraphs in the built documents. 103 | #keep_warnings = False 104 | 105 | 106 | # -- Options for HTML output ---------------------------------------------- 107 | 108 | # The theme to use for HTML and HTML Help pages. See the documentation for 109 | # a list of builtin themes. 110 | if not on_rtd: 111 | import sphinx_rtd_theme 112 | html_theme = 'sphinx_rtd_theme' 113 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 114 | 115 | # Theme options are theme-specific and customize the look and feel of a theme 116 | # further. For a list of options available for each theme, see the 117 | # documentation. 118 | #html_theme_options = {} 119 | 120 | # Add any paths that contain custom themes here, relative to this directory. 121 | #html_theme_path = [] 122 | 123 | # The name for this set of Sphinx documents. If None, it defaults to 124 | # " v documentation". 125 | #html_title = None 126 | 127 | # A shorter title for the navigation bar. Default is the same as html_title. 128 | #html_short_title = None 129 | 130 | # The name of an image file (relative to this directory) to place at the top 131 | # of the sidebar. 132 | #html_logo = None 133 | 134 | # The name of an image file (within the static path) to use as favicon of the 135 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 136 | # pixels large. 137 | #html_favicon = None 138 | 139 | # Add any paths that contain custom static files (such as style sheets) here, 140 | # relative to this directory. They are copied after the builtin static files, 141 | # so a file named "default.css" will overwrite the builtin "default.css". 142 | html_static_path = ['_static'] 143 | 144 | # Add any extra paths that contain custom files (such as robots.txt or 145 | # .htaccess) here, relative to this directory. These files are copied 146 | # directly to the root of the documentation. 147 | #html_extra_path = [] 148 | 149 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 150 | # using the given strftime format. 151 | #html_last_updated_fmt = '%b %d, %Y' 152 | 153 | # If true, SmartyPants will be used to convert quotes and dashes to 154 | # typographically correct entities. 155 | #html_use_smartypants = True 156 | 157 | # Custom sidebar templates, maps document names to template names. 158 | #html_sidebars = {} 159 | 160 | # Additional templates that should be rendered to pages, maps page names to 161 | # template names. 162 | #html_additional_pages = {} 163 | 164 | # If false, no module index is generated. 165 | #html_domain_indices = True 166 | 167 | # If false, no index is generated. 168 | #html_use_index = True 169 | 170 | # If true, the index is split into individual pages for each letter. 171 | #html_split_index = False 172 | 173 | # If true, links to the reST sources are added to the pages. 174 | #html_show_sourcelink = True 175 | 176 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 177 | #html_show_sphinx = True 178 | 179 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 180 | #html_show_copyright = True 181 | 182 | # If true, an OpenSearch description file will be output, and all pages will 183 | # contain a tag referring to it. The value of this option must be the 184 | # base URL from which the finished HTML is served. 185 | #html_use_opensearch = '' 186 | 187 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 188 | #html_file_suffix = None 189 | 190 | # Output file base name for HTML help builder. 191 | htmlhelp_basename = 'wiringdoc' 192 | 193 | 194 | # -- Options for LaTeX output --------------------------------------------- 195 | 196 | latex_elements = { 197 | # The paper size ('letterpaper' or 'a4paper'). 198 | #'papersize': 'letterpaper', 199 | 200 | # The font size ('10pt', '11pt' or '12pt'). 201 | #'pointsize': '10pt', 202 | 203 | # Additional stuff for the LaTeX preamble. 204 | #'preamble': '', 205 | } 206 | 207 | # Grouping the document tree into LaTeX files. List of tuples 208 | # (source start file, target name, title, 209 | # author, documentclass [howto, manual, or own class]). 210 | latex_documents = [ 211 | ('index', 'wiring.tex', u'wiring Documentation', 212 | u'Mikołaj Siedlarek', 'manual'), 213 | ] 214 | 215 | # The name of an image file (relative to this directory) to place at the top of 216 | # the title page. 217 | #latex_logo = None 218 | 219 | # For "manual" documents, if this is true, then toplevel headings are parts, 220 | # not chapters. 221 | #latex_use_parts = False 222 | 223 | # If true, show page references after internal links. 224 | #latex_show_pagerefs = False 225 | 226 | # If true, show URL addresses after external links. 227 | #latex_show_urls = False 228 | 229 | # Documents to append as an appendix to all manuals. 230 | #latex_appendices = [] 231 | 232 | # If false, no module index is generated. 233 | #latex_domain_indices = True 234 | 235 | 236 | # -- Options for manual page output --------------------------------------- 237 | 238 | # One entry per manual page. List of tuples 239 | # (source start file, name, description, authors, manual section). 240 | man_pages = [ 241 | ('index', 'wiring', u'wiring Documentation', 242 | [u'Mikołaj Siedlarek'], 1) 243 | ] 244 | 245 | # If true, show URL addresses after external links. 246 | #man_show_urls = False 247 | 248 | 249 | # -- Options for Texinfo output ------------------------------------------- 250 | 251 | # Grouping the document tree into Texinfo files. List of tuples 252 | # (source start file, target name, title, author, 253 | # dir menu entry, description, category) 254 | texinfo_documents = [ 255 | ('index', 'wiring', u'wiring Documentation', 256 | u'Mikołaj Siedlarek', 'wiring', 'One line description of project.', 257 | 'Miscellaneous'), 258 | ] 259 | 260 | # Documents to append as an appendix to all manuals. 261 | #texinfo_appendices = [] 262 | 263 | # If false, no module index is generated. 264 | #texinfo_domain_indices = True 265 | 266 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 267 | #texinfo_show_urls = 'footnote' 268 | 269 | # If true, do not generate a @detailmenu in the "Top" node's menu. 270 | #texinfo_no_detailmenu = False 271 | -------------------------------------------------------------------------------- /docs/rationale.rst: -------------------------------------------------------------------------------- 1 | Rationale 2 | ========= 3 | 4 | Writing medium-to-large applications in Python is something that happens more 5 | and more often as the language gains popularity, particularly in the web 6 | environment. However, Python, traditionally a scripting and prototyping 7 | language, has a shortage of good tooling to tackle architectural challenges of 8 | big, object-oriented applications. 9 | 10 | Programming environments like Java, designed and used for years in enterprise 11 | settings, have already developed some ideas how to meet those challenges. Java 12 | language constructs like interfaces and frameworks like `Spring`_ or `Guice`_, 13 | while commonly ridiculed by Python community, can be in fact very useful in 14 | this context. 15 | 16 | Wiring was created from the real need of a real application I've been working 17 | on and aims to solve its problems by adapting some of those proven ideas to the 18 | Python environment, while staying away from blindly copying APIs from other 19 | languages and libraries. 20 | 21 | .. _Spring: http://spring.io 22 | .. _Guice: https://github.com/google/guice 23 | 24 | Dependency Injection 25 | -------------------- 26 | 27 | `Inversion of Control`_ - commonly realized through dependency injection - is 28 | a popular pattern in object-oriented applications, allowing loose coupling 29 | while promoting object composition. Instead of getting its dependencies from 30 | some predefined location or creating them on the spot, an object can only 31 | declare what it needs and rely on external mechanism to provide it. 32 | 33 | For example, this is how an object **without** dependency injection could look 34 | like:: 35 | 36 | from myapp.database import database_connection 37 | from myapp.security import Encryptor 38 | 39 | class UserManager(object): 40 | def __init__(self): 41 | self.db = database_connection 42 | self.encryptor = Encryptor(algo='bcrypt') 43 | 44 | class Application(object): 45 | def __init__(self): 46 | self.user_manager = UserManager() 47 | 48 | def run(self): 49 | pass 50 | 51 | application = Application() 52 | application.run() 53 | 54 | While many applications are built like that, this approach has some major 55 | drawbacks, which are becoming really visible while the application grows: 56 | 57 | #. Tight coupling. User management module is strongly connected with database 58 | and security modules. If you'd like to replace the security module with some 59 | other implementation, you'd need to also modify the user management module. 60 | 61 | #. It uses a global variable `database_connection`. This implies that whatever 62 | context this class might be used in, the database connection is always the 63 | same. If in the future you'd want to use `UserManager` on some kind of 64 | archival database, while the rest of your application simultaneously works 65 | on the default database, you'd be in trouble. Also you need to change 66 | a global variable for unit testing. 67 | 68 | #. The `UserManager` creates its own `Encryptor` object. This means that if 69 | you'd need to change password hashing algorithm for one deployment (for 70 | example due to local government security restrictions), you'd have to modify 71 | the `UserManager` to use some kind of global setting or provide the 72 | algorithm as an argument with each use of the class. 73 | 74 | Those problems can be easily solved with dependency injection:: 75 | 76 | from wiring.dependency import injected 77 | from wiring.graph import Graph 78 | from myapp.database import database_connection 79 | from myapp.security import Encryptor 80 | 81 | class UserManager(object): 82 | def __init__(self, db=injected('db_connection'), 83 | encryptor=injected('password_encryptor')): 84 | self.db = db 85 | self.encryptor = encryptor 86 | 87 | class Application(object): 88 | def __init__(self, user_manager=injected('user_manager')): 89 | self.user_manager = user_manager 90 | 91 | def run(self): 92 | pass 93 | 94 | graph = Graph() 95 | # Wiring has much more convenient methods of registering components. This is 96 | # just for the clarity of the example. 97 | graph.register_factory('user_manager', UserManager) 98 | graph.register_factory('application', Application) 99 | graph.register_instance('db_connection', database_connection) 100 | graph.register_instance('password_encryptor', Encryptor(algo='bcrypt')) 101 | 102 | application = graph.get('application') 103 | application.run() 104 | 105 | This may look like a silly overhead when presented in one file, but if this was 106 | split in three separate modules we'd have all of our previously mentioned 107 | problems solved: 108 | 109 | #. It's now loosely coupled. If you want to replace the security module, you 110 | just need to reconfigure your :term:`object graph`. `UserManager` doesn't 111 | even know that `myapp.security` exists. 112 | #. There are no global variables that are used between the modules - if you 113 | want to replace database connection for unit testing you just need to 114 | configure your object graph differently. 115 | #. You want to use different password hashing algorithm for one deployment? Not 116 | a problem - you just configure your object graph differently on that 117 | deployment. No need to search the code for every single use of `Encryptor` 118 | class and really no need to modify security or user management modules at 119 | all. 120 | 121 | Those benefits, while not that obvious in a small example, become pretty 122 | obvious in big applications - most importantly those with multiple, differing 123 | deployments. 124 | 125 | There's a little amount of solid tools to tackle big Python applications 126 | architecture problem: 127 | 128 | * `zope.component`_, while having some truly brilliant ideas, does not provide 129 | dependency injection and above all its codebase and API are really old and 130 | messy. 131 | * `pinject`_ is not very flexible and relies on class and argument names to do 132 | the injection, which is very limiting. Also its latest commit while I'm 133 | writing this is over a year old, while there are several issues open. 134 | * `injector`_ while quite good, also lacks flexibility and leaves out many 135 | possibilities. 136 | 137 | .. _Inversion of Control: http://www.martinfowler.com/articles/injection.html 138 | .. _zope.component: https://pypi.python.org/pypi/zope.component 139 | .. _pinject: https://pypi.python.org/pypi/pinject 140 | .. _injector: https://pypi.python.org/pypi/injector 141 | 142 | Interfaces 143 | ---------- 144 | 145 | Many would argue that interfaces are useful only in languages like Java, where 146 | typing is static and multiple inheritance seriously limited. Those people view 147 | interfaces only as a tool to enable polymorphism, failing to recognise other 148 | use - definition and validation of objects. 149 | 150 | Python uses idea of duck typing, as the saying goes - *if it looks like a duck, 151 | swims like a duck, and quacks like a duck, then it probably is a duck*. The 152 | problem with this approach is when you want to replace some component - said 153 | duck - you must know exactly how to create your own duck, that is *what it 154 | means to be a duck*. 155 | 156 | Most popular approach to this is documenting required methods and attributes of 157 | a duck in project's documentation. While basically valid, this has two 158 | problems: 159 | 160 | * It moves away the duck description from the code to the external 161 | documentation. This may easily create a divergence between the documentation 162 | and the code and requires programmer to know where to look for the duck 163 | description. 164 | * You have no way of automatically testing whether the duck you created is 165 | a valid duck. What if a duck definition changes in a future? You must 166 | remember to update your implementation. 167 | 168 | Interfaces as implemented in :py:mod:`wiring.interface` solve exactly those two 169 | problems: 170 | 171 | * They are defined in code, and implementing classes can declare them in code. 172 | They're also presented in a simplest possible form for the programmer to 173 | read -- in the form of Python code. 174 | * Any object can be tested against them and proved to have valid attributes and 175 | methods. This can be checked for example in unit tests. 176 | 177 | While there is `zope.interface`_ available it shares the problem of all Zope 178 | libraries - its codebase and API are both pretty old and messy. 179 | 180 | .. _zope.interface: https://pypi.python.org/pypi/zope.interface 181 | 182 | .. _rationale-powerscombined: 183 | 184 | Powers Combined 185 | --------------- 186 | 187 | There is an important reason those two tools - dependency injection and 188 | interfaces - are coupled together into this project. Let's bring back 189 | a fragment of the dependency injection example:: 190 | 191 | class UserManager(object): 192 | def __init__(self, db=injected('db_connection')): 193 | self.db = db 194 | 195 | If a programmer is asked to change some behavior of `UserManager` and 196 | encounters this code, he has no way of knowing what exactly can he do 197 | with the `db` variable. What are its methods and attributes? He has to 198 | trace component configuration looking for specific implementation that 199 | is registered under ``db_connection``. Fortunately, there's a better 200 | way:: 201 | 202 | class IDatabaseConnection(object): 203 | 204 | version = """Version of the used database engine.""" 205 | 206 | def sql(query): 207 | """Runs a given `query` and returns its result as a list of tuples.""" 208 | 209 | class UserManager(object): 210 | def __init__(self, db=injected(IDatabaseConnection)): 211 | self.db = db 212 | 213 | Interfaces make perfect :term:`specifications ` for dependency 214 | injection. Now anyone visiting `UserManager`'s code can easily trace what 215 | properties `db` variable will always have. Also, when replacing the database 216 | component its also obvious what properties new component should have to fit in 217 | place of the old one. It just have to conform to the `IDatabaseConnection` 218 | interface. 219 | -------------------------------------------------------------------------------- /wiring/configuration.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import inspect 3 | 4 | import six 5 | 6 | from wiring.providers import ( 7 | FactoryProvider, 8 | FunctionProvider, 9 | InstanceProvider 10 | ) 11 | 12 | 13 | __all__ = ( 14 | 'InvalidConfigurationError', 15 | 'Module', 16 | 'provides', 17 | 'scope', 18 | ) 19 | 20 | 21 | class InvalidConfigurationError(Exception): 22 | """ 23 | Raised when there is some problem with a :term:`module` class, for example 24 | when module defines more than one :term:`provider` for a single 25 | :term:`specification`. 26 | """ 27 | 28 | def __init__(self, module, message): 29 | self.module = module 30 | """A :term:`module` class where the problem was found.""" 31 | self.message = message 32 | """A message describing the problem.""" 33 | 34 | def __str__(self): 35 | return "Configuration error in module {module}: {message}".format( 36 | module='.'.join((self.module.__module__, self.module.__name__)), 37 | message=self.message 38 | ) 39 | 40 | 41 | class ModuleMetaclass(type): 42 | """ 43 | This metaclass analyzes special attributes of a new :term:`module` classes 44 | and generates `providers` attribute, which is a mapping of 45 | :term:`specifications ` and related :term:`providers 46 | `. 47 | 48 | Supported attributes are: 49 | 50 | `providers` 51 | A dictionary mapping specifications to provider objects, 52 | implementing :py:interface:`wiring.providers.IProvider` interface. 53 | 54 | `instances` 55 | A dictionary mapping specifications to objects that will be wrapped 56 | in :py:class:`wiring.providers.InstanceProvider`. 57 | 58 | `factories` 59 | A dictionary mapping specifications to callable that will be 60 | wrapped in :py:class:`wiring.providers.FactoryProvider`. If 61 | dictionary value is a tuple, first element is treated as callable 62 | and the second as scope type for this provider. 63 | 64 | `functions` 65 | A dictionary mapping specifications to callable that will be 66 | wrapped in :py:class:`wiring.providers.FunctionProvider`. 67 | 68 | Three last attributes are provided for convinience and will be merged into 69 | the first one by this metaclass. For example this module:: 70 | 71 | class SomeModule(Module): 72 | providers = { 73 | 'foo': CustomProvider('foo'), 74 | } 75 | instances = { 76 | 'db_url': 'sqlite://somedb', 77 | } 78 | factories = { 79 | 'db_connection': (DatabaseConnection, ThreadScope), 80 | 'bar': create_bar, 81 | } 82 | functions = { 83 | 'foobarize': foobarize, 84 | } 85 | 86 | @provides('fizz') 87 | def provide_fizz(self, db_connection=injected('db_connection')): 88 | return db_connection.sql('SELECT fizz FROM buzz;') 89 | 90 | is an equivalent of:: 91 | 92 | class SomeModule(Module): 93 | providers = { 94 | 'foo': CustomProvider('foo'), 95 | 'db_url': InstanceProvider('sqlite://somedb'), 96 | 'db_connection': FactoryProvider( 97 | DatabaseConnection, 98 | scope=ThreadScope 99 | ), 100 | 'bar': FactoryProvider(create_bar), 101 | 'foobarize': FunctionProvider(foobarize), 102 | } 103 | 104 | @provides('fizz') 105 | def provide_fizz(self, db_connection=injected('db_connection')): 106 | return db_connection.sql('SELECT fizz FROM buzz;') 107 | 108 | Defined modules can later register their providers into an 109 | :term:`object graph` using :py:meth:`Module.add_to`. 110 | 111 | When there is more than one provider declared for a single specification, 112 | :py:exc:`InvalidConfigurationError` is raised. 113 | 114 | :raises: 115 | :py:exc:`InvalidConfigurationError` 116 | """ 117 | 118 | def __new__(cls, module_name, bases, attributes): 119 | special_attributes = ( 120 | 'providers', 121 | 'instances', 122 | 'factories', 123 | 'functions', 124 | ) 125 | module = super(ModuleMetaclass, cls).__new__( 126 | cls, 127 | module_name, 128 | bases, 129 | { 130 | key: value for key, value in six.iteritems(attributes) 131 | if key not in special_attributes 132 | } 133 | ) 134 | 135 | providers = {} 136 | 137 | for ancestor in reversed(inspect.getmro(module)): 138 | if cls._is_module_class(ancestor): 139 | providers.update(ancestor.providers) 140 | 141 | already_provided = set() 142 | 143 | providers_attribute = attributes.get('providers', {}) 144 | providers.update(providers_attribute) 145 | already_provided.update(six.iterkeys(providers_attribute)) 146 | 147 | def check_specification(key): 148 | if key in already_provided: 149 | raise InvalidConfigurationError( 150 | module, 151 | "Multiple sources defined for specification {spec}".format( 152 | spec=repr(key) 153 | ) 154 | ) 155 | already_provided.add(key) 156 | 157 | for key, value in six.iteritems(attributes.get('instances', {})): 158 | check_specification(key) 159 | providers[key] = InstanceProvider(value) 160 | for key, value in six.iteritems(attributes.get('factories', {})): 161 | check_specification(key) 162 | if not isinstance(value, collections.Iterable): 163 | value = [value] 164 | if len(value) < 1 or len(value) > 2: 165 | raise InvalidConfigurationError( 166 | module, 167 | ( 168 | "Wrong number of arguments for {spec} in" 169 | " `factories`." 170 | ).format( 171 | spec=repr(key) 172 | ) 173 | ) 174 | providers[key] = FactoryProvider( 175 | value[0], 176 | scope=(value[1] if len(value) > 1 else None) 177 | ) 178 | for key, value in six.iteritems(attributes.get('functions', {})): 179 | check_specification(key) 180 | providers[key] = FunctionProvider(value) 181 | 182 | for key, value in six.iteritems(attributes): 183 | if hasattr(value, '__provides__'): 184 | check_specification(value.__provides__) 185 | 186 | module.providers = providers 187 | 188 | return module 189 | 190 | @classmethod 191 | def _is_module_class(cls, other_class): 192 | if not isinstance(other_class, cls): 193 | # Other class didn't came from this metaclass. 194 | return False 195 | if all(map(lambda b: not isinstance(b, cls), other_class.__bases__)): 196 | # Other class is Module class from this module. 197 | return False 198 | return True 199 | 200 | 201 | @six.add_metaclass(ModuleMetaclass) 202 | class Module(object): 203 | __doc__ = """ 204 | A base class for :term:`module` classes, using the 205 | :py:class:`ModuleMetaclass`. 206 | """ + ModuleMetaclass.__doc__ 207 | 208 | providers = {} 209 | """ 210 | A dictionary mapping specifications to provider objects, implementing 211 | :py:interface:`wiring.providers.IProvider` interface. 212 | """ 213 | 214 | scan = [] 215 | """ 216 | A sequence of module references to recursively scan for providers 217 | registered with :py:mod:`wiring.scanning.register` module. 218 | If a string is given instead of a module reference, it will be used to 219 | import the module. 220 | """ 221 | 222 | scan_ignore = [] 223 | """ 224 | A sequence of module paths to ignore when scanning modules in 225 | :py:attr:`scan`. 226 | """ 227 | 228 | def __init__(self): 229 | if self.scan: 230 | from wiring.scanning import scan_to_module 231 | scan_to_module(self.scan, self, ignore=self.scan_ignore) 232 | 233 | def add_to(self, graph): 234 | """ 235 | Register all of declared providers into a given :term:`object graph`. 236 | """ 237 | for specification, provider in six.iteritems(self.providers): 238 | graph.register_provider(specification, provider) 239 | for name in dir(self): 240 | value = getattr(self, name) 241 | if hasattr(value, '__provides__'): 242 | graph.register_factory( 243 | value.__provides__, 244 | value, 245 | scope=getattr(value, '__scope__', None) 246 | ) 247 | 248 | 249 | def provides(*specification): 250 | """ 251 | Decorator marking wrapped :py:class:`Module` method as :term:`provider` for 252 | given :term:`specification`. 253 | 254 | For example:: 255 | 256 | class ApplicationModule(Module): 257 | 258 | @provides('db_connection') 259 | def provide_db_connection(self): 260 | return DBConnection(host='localhost') 261 | """ 262 | if len(specification) == 1: 263 | specification = specification[0] 264 | else: 265 | specification = tuple(specification) 266 | 267 | def decorator(function): 268 | function.__provides__ = specification 269 | return function 270 | 271 | return decorator 272 | 273 | 274 | def scope(scope): 275 | """ 276 | Decorator specifying a :term:`scope` for wrapped :py:class:`Module` 277 | :term:`provider` method. `scope` should be a scope type that will later be 278 | registered in an :term:`object graph`. 279 | 280 | For example:: 281 | 282 | class ApplicationModule(Module): 283 | 284 | @provides('db_connection') 285 | @scope(ThreadScope) 286 | def provide_db_connection(self): 287 | return DBConnection(host='localhost') 288 | """ 289 | def decorator(function): 290 | function.__scope__ = scope 291 | return function 292 | return decorator 293 | -------------------------------------------------------------------------------- /wiring/dependency.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | import six 4 | 5 | 6 | __all__ = ( 7 | 'Factory', 8 | 'UnrealizedInjection', 9 | 'get_dependencies', 10 | 'inject', 11 | 'injected', 12 | ) 13 | 14 | 15 | class Factory(tuple): 16 | """ 17 | This class is a wrapper for a specification, declaring that instead of 18 | a created object for the specification, a callable returning the object 19 | should be injected. This callable accepts additional arguments that will be 20 | merged with the injected ones, just like in 21 | :py:meth:`wiring.graph.Graph.get` method. 22 | 23 | For example:: 24 | 25 | class DBConnection(object): 26 | @injected('db.url') 27 | def __init__(self, url, read_only=False): 28 | # .... 29 | 30 | @inject(db_factory=Factory('db.connection')): 31 | def get_user(id, db_factory=None): 32 | db = db_factory(read_only=True) 33 | return db.get_model('user', id=id) 34 | 35 | Unless an instance for `db.connection` specification is cached in a scope, 36 | each execution of `get_user()` will create a new database connection 37 | object. 38 | 39 | This feature is particularly useful when you need an object from a narrower 40 | scope, like a thread-scoped database connection in an application 41 | singleton. You cannot just get a connection object in the application 42 | constructor and save it, because when one of it methods is called from 43 | a different thread it will use the same connection object. That effectively 44 | defeats the purpose of thread scope. 45 | 46 | To prevent that you can inject and save in a constructor a factory of 47 | database connections and call it in every method to obtain a connection 48 | object for current thread. 49 | """ 50 | 51 | __slots__ = [] 52 | 53 | def __new__(cls, *specification): 54 | """ 55 | You construct this class by giving it :term:`specification` elements. 56 | For example, if your specification is:: 57 | 58 | (IDBConnection, 'archive') 59 | 60 | then you can declare the :term:`dependency` like this:: 61 | 62 | @inject(db=Factory(IDBConnection, 'archive')) 63 | def foo(db=None): 64 | pass 65 | 66 | When no specification is given to the class constructor, 67 | a `ValueError` is raised. 68 | 69 | :raises: 70 | ValueError 71 | """ 72 | if not specification: 73 | raise ValueError("No dependency specification given.") 74 | if len(specification) == 1: 75 | specification = specification[0] 76 | else: 77 | specification = tuple(specification) 78 | return super(Factory, cls).__new__( 79 | cls, 80 | (specification,) 81 | ) 82 | 83 | @property 84 | def specification(self): 85 | """ 86 | A :term:`specification` of an object of which a factory will be 87 | injected. 88 | """ 89 | return self[0] 90 | 91 | def __repr__(self): 92 | specification = self.specification 93 | if not isinstance(specification, tuple): 94 | specification = '({})'.format(specification) 95 | return ''.format( 96 | specification=specification 97 | ) 98 | 99 | 100 | class UnrealizedInjection(tuple): 101 | """ 102 | Instances of this class are placeholders that can be used as default values 103 | for arguments to mark that they should be provided with injected 104 | :term:`dependency`, without using the :py:func:`inject()` decorator. For 105 | example:: 106 | 107 | def __init__(self, db_connection=UnrealizedInjection(IDBConnection)): 108 | if not db_connection: 109 | raise ValueError() 110 | 111 | Note that instances of this class always evaluate to `False` when converted 112 | to boolean, to allow easy checking for dependencies that hasn't been 113 | injected. 114 | 115 | Instances of this class are immutable, `as any default argument value in 116 | Python should be 117 | `_. 118 | 119 | There's also an :py:data:`injected` shortcut for this class in this 120 | package. 121 | """ 122 | 123 | __slots__ = [] 124 | 125 | def __new__(cls, *specification): 126 | """ 127 | You construct this class by giving it :term:`specification` elements. 128 | For example, if your specification is:: 129 | 130 | (IDBConnection, 'archive') 131 | 132 | then you can declare the :term:`dependency` like this:: 133 | 134 | def foo(db=UnrealizedInjection(IDBConnection, 'archive')): 135 | pass 136 | 137 | When no specification is given to the class constructor, 138 | a `ValueError` is raised. 139 | 140 | :raises: 141 | ValueError 142 | """ 143 | if not specification: 144 | raise ValueError("No dependency specification given.") 145 | if len(specification) == 1: 146 | specification = specification[0] 147 | else: 148 | specification = tuple(specification) 149 | return super(UnrealizedInjection, cls).__new__( 150 | cls, 151 | (specification,) 152 | ) 153 | 154 | @property 155 | def specification(self): 156 | """ 157 | A :term:`specification` of an object that should be injected in place 158 | of this placholder. 159 | """ 160 | return self[0] 161 | 162 | def __repr__(self): 163 | specification = self.specification 164 | if not isinstance(specification, tuple): 165 | specification = '({})'.format(specification) 166 | return ''.format( 167 | specification=specification 168 | ) 169 | 170 | def __bool__(self): 171 | return False 172 | 173 | def __nonzero__(self): 174 | return False 175 | 176 | 177 | def get_dependencies(factory): 178 | """ 179 | This function inspects a function to find its arguments marked for 180 | injection, either with :py:func:`inject()` decorator, 181 | :py:class:`UnrealizedInjection` class of through Python 3 function 182 | annotations. If `factory` is a class, then its constructor is inspected. 183 | 184 | Returned dictionary is a mapping of:: 185 | 186 | [argument index/name] -> [specification] 187 | 188 | For example, dependencies for function:: 189 | 190 | @inject(ILogger, db=(IDBConnection, 'archive')) 191 | def foo(log, db=None): 192 | pass 193 | 194 | would be:: 195 | 196 | { 197 | 0: ILogger, 198 | 'db': (IDBConnection, 'archive'), 199 | } 200 | 201 | `Old-style classes`_ (from before Python 2.2) are not supported. 202 | 203 | .. _Old-style classes: 204 | https://docs.python.org/2/reference/datamodel.html#new-style-and-classic-classes 205 | """ 206 | if inspect.isclass(factory): 207 | # If factory is a class we want to check constructor depdendencies. 208 | if six.PY3: 209 | init_check = inspect.isfunction 210 | else: 211 | init_check = inspect.ismethod 212 | dependencies = {} 213 | if hasattr(factory, '__init__') and init_check(factory.__init__): 214 | dependencies.update(get_dependencies(factory.__init__)) 215 | if hasattr(factory, '__new__') and inspect.isfunction(factory.__new__): 216 | dependencies.update(get_dependencies(factory.__new__)) 217 | return dependencies 218 | elif inspect.isfunction(factory) or inspect.ismethod(factory): 219 | function = factory 220 | else: 221 | raise TypeError("`factory` must be a class or a function.") 222 | 223 | if hasattr(function, '__injection__'): 224 | # Function has precollected dependencies (happens when using `inject()` 225 | # decorator. Nothing to do here. 226 | return function.__injection__ 227 | 228 | dependencies = {} 229 | 230 | def process_dependency_tuples(tuples): 231 | for key, value in tuples: 232 | if isinstance(value, UnrealizedInjection): 233 | dependencies[key] = value.specification 234 | if six.PY3: 235 | argument_specification = inspect.getfullargspec(function) 236 | if argument_specification.kwonlydefaults: 237 | process_dependency_tuples( 238 | six.iteritems(argument_specification.kwonlydefaults) 239 | ) 240 | if argument_specification.annotations: 241 | dependencies.update(argument_specification.annotations) 242 | else: 243 | argument_specification = inspect.getargspec(function) 244 | if argument_specification.defaults: 245 | process_dependency_tuples(zip( 246 | reversed(argument_specification.args), 247 | reversed(argument_specification.defaults) 248 | )) 249 | return dependencies 250 | 251 | 252 | def inject(*positional_dependencies, **keyword_dependencies): 253 | """ 254 | This decorator can be used to specify injection rules for decorated 255 | function arguments. Each argument to this decorator should be 256 | a :term:`specification` for injecting into related argument of decorated 257 | function. `None` can be given instead of a specification to prevent 258 | argument from being injected. This is handy for positional arguments. 259 | 260 | Example:: 261 | 262 | @inject(None, IDBConnection, logger=(ILogger, 'system')) 263 | def foo(noninjectable_argument, db_connection, logger=None): 264 | pass 265 | 266 | This decorator can be used multiple times and also with 267 | :py:class:`UnrealizedInjection` class. Specified dependencies are collected 268 | and when conflicting the outermost :term:`specification` is used:: 269 | 270 | @inject(db=(IDBConnection, 'archive2')) 271 | @inject(db=IDBConnection) 272 | def foo(db=injected(IDBConnection, 'archive')): 273 | # In this example 'archive2' database connection will be injected. 274 | pass 275 | """ 276 | def decorator(function): 277 | dependencies = get_dependencies(function) 278 | 279 | def process_dependency_tuples(tuples): 280 | for key, dependency_description in tuples: 281 | if dependency_description is None: 282 | specification = None 283 | elif isinstance(dependency_description, UnrealizedInjection): 284 | specification = dependency_description.specification 285 | else: 286 | specification = dependency_description 287 | if specification is None: 288 | try: 289 | del dependencies[key] 290 | except KeyError: 291 | pass 292 | else: 293 | dependencies[key] = specification 294 | 295 | process_dependency_tuples(enumerate(positional_dependencies)) 296 | process_dependency_tuples(six.iteritems(keyword_dependencies)) 297 | function.__injection__ = dependencies 298 | return function 299 | return decorator 300 | 301 | 302 | injected = UnrealizedInjection 303 | """ 304 | Shortcut for :py:class:`UnrealizedInjection` to be used in method definition 305 | arguments. 306 | """ 307 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /tests/all/graph_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from wiring.dependency import Factory, inject, injected 4 | from wiring.graph import ( 5 | DependencyCycleError, 6 | Graph, 7 | MissingDependencyError, 8 | SelfDependencyError, 9 | UnknownScopeError 10 | ) 11 | from wiring.scopes import ProcessScope 12 | 13 | from . import ModuleTest 14 | 15 | 16 | class GraphModuleTest(ModuleTest): 17 | module = 'wiring.graph' 18 | 19 | 20 | class GraphTest(unittest.TestCase): 21 | 22 | def test_valid(self): 23 | db_hostname = 'example.com' 24 | 25 | def get_database_connection(db_hostname=injected('db.hostname')): 26 | if db_hostname == 'example.com': 27 | return { 28 | 'connected': True, 29 | } 30 | else: 31 | raise Exception("Injection went wrong.") 32 | 33 | class TestClass(object): 34 | @inject('db_connection') 35 | def __init__(self, db): 36 | self.is_ok = db['connected'] 37 | 38 | graph = Graph() 39 | graph.register_instance('db.hostname', db_hostname) 40 | graph.register_factory('db_connection', get_database_connection) 41 | graph.register_function( 42 | 'db_connection_function', 43 | get_database_connection 44 | ) 45 | graph.register_factory(TestClass, TestClass) 46 | graph.validate() 47 | 48 | self.assertEqual(graph.get('db.hostname'), 'example.com') 49 | self.assertDictEqual( 50 | graph.get('db_connection_function')(), 51 | {'connected': True} 52 | ) 53 | test_instance = graph.get(TestClass) 54 | self.assertIsInstance(test_instance, TestClass) 55 | self.assertTrue(test_instance.is_ok) 56 | 57 | def test_some_positional_arguments(self): 58 | @inject(None, 'foo') 59 | def function(first, second): 60 | return first, second 61 | graph = Graph() 62 | graph.register_instance('foo', 'bar') 63 | graph.register_function('function', function) 64 | graph.validate() 65 | 66 | function_instance = graph.get('function') 67 | first, second = function_instance(123) 68 | self.assertEqual(first, 123) 69 | self.assertEqual(second, 'bar') 70 | 71 | def test_some_positional_arguments_class(self): 72 | class TestClass(object): 73 | @inject(None, 'foo') 74 | def __init__(self, first, second): 75 | self.first = first 76 | self.second = second 77 | 78 | def test(self): 79 | return self.first, self.second 80 | graph = Graph() 81 | graph.register_instance('foo', 'bar') 82 | graph.register_factory(TestClass, TestClass) 83 | graph.validate() 84 | 85 | test_class = graph.get(TestClass, 123) 86 | first, second = test_class.test() 87 | self.assertEqual(first, 123) 88 | self.assertEqual(second, 'bar') 89 | 90 | def test_factory(self): 91 | class DBConnection(object): 92 | counter = 0 93 | 94 | def __init__(self): 95 | DBConnection.counter += 1 96 | self.id = DBConnection.counter 97 | 98 | @inject(Factory('db')) 99 | def foo(db_factory): 100 | self.assertNotIsInstance(db_factory, DBConnection) 101 | db = db_factory() 102 | self.assertIsInstance(db, DBConnection) 103 | return db.id 104 | 105 | graph = Graph() 106 | graph.register_factory('db', DBConnection) 107 | graph.register_function('foo', foo) 108 | graph.validate() 109 | 110 | foo_instance = graph.get('foo') 111 | self.assertEqual(foo_instance(), 1) 112 | self.assertEqual(foo_instance(), 2) 113 | self.assertEqual(foo_instance(), 3) 114 | 115 | def test_factory_arguments(self): 116 | class DBConnection(object): 117 | counter = 0 118 | 119 | def __init__(self): 120 | DBConnection.counter += 1 121 | self.id = DBConnection.counter 122 | 123 | @inject(db_factory=Factory('db')) 124 | def foo(multiplier, db_factory=None): 125 | self.assertNotIsInstance(db_factory, DBConnection) 126 | db = db_factory() 127 | self.assertIsInstance(db, DBConnection) 128 | return multiplier * db.id 129 | 130 | graph = Graph() 131 | graph.register_factory('db', DBConnection) 132 | graph.register_function('foo', foo) 133 | graph.validate() 134 | 135 | foo_instance = graph.get('foo') 136 | self.assertEqual(foo_instance(100), 100) 137 | self.assertEqual(foo_instance(1), 2) 138 | self.assertEqual(foo_instance(100), 300) 139 | 140 | def test_factory_scope(self): 141 | class DBConnection(object): 142 | counter = 0 143 | 144 | def __init__(self): 145 | DBConnection.counter += 1 146 | self.id = DBConnection.counter 147 | 148 | @inject(Factory('db')) 149 | def foo(db_factory): 150 | self.assertNotIsInstance(db_factory, DBConnection) 151 | db = db_factory() 152 | self.assertIsInstance(db, DBConnection) 153 | return db.id 154 | 155 | graph = Graph() 156 | graph.register_factory('db', DBConnection, scope=ProcessScope) 157 | graph.register_function('foo', foo) 158 | graph.validate() 159 | 160 | foo_instance = graph.get('foo') 161 | self.assertEqual(foo_instance(), 1) 162 | self.assertEqual(foo_instance(), 1) 163 | self.assertEqual(foo_instance(), 1) 164 | 165 | def test_self_dependency(self): 166 | @inject('foobar') 167 | def function(foo): 168 | pass 169 | graph = Graph() 170 | graph.register_factory('foobar', function) 171 | with self.assertRaises(SelfDependencyError) as cm: 172 | graph.validate() 173 | self.assertEqual(cm.exception.specification, 'foobar') 174 | self.assertEqual( 175 | str(cm.exception), 176 | "Provider for 'foobar' is dependent on itself." 177 | ) 178 | 179 | def test_missing_dependency(self): 180 | @inject('foobar') 181 | def function(foo): 182 | return foo + 1 183 | 184 | graph = Graph() 185 | graph.register_factory('function', function) 186 | with self.assertRaises(MissingDependencyError) as cm: 187 | graph.validate() 188 | self.assertEqual(cm.exception.dependency, 'foobar') 189 | self.assertEqual(cm.exception.dependant, 'function') 190 | self.assertEqual( 191 | str(cm.exception), 192 | "Cannot find dependency 'foobar' for 'function' provider." 193 | ) 194 | 195 | graph.register_instance('foobar', 42) 196 | graph.validate() 197 | self.assertEqual(graph.get('function'), 43) 198 | 199 | def test_dependency_cycle(self): 200 | @inject('c') 201 | def a(c): 202 | pass 203 | 204 | @inject('a') 205 | def b(a): 206 | pass 207 | 208 | @inject('b') 209 | def c(b): 210 | pass 211 | 212 | graph = Graph() 213 | graph.register_factory('a', a) 214 | graph.register_factory('b', b) 215 | graph.register_factory('c', c) 216 | with self.assertRaises(DependencyCycleError) as cm: 217 | graph.validate() 218 | self.assertSetEqual( 219 | frozenset(cm.exception.cycle), 220 | frozenset(('a', 'b', 'c')) 221 | ) 222 | message = str(cm.exception) 223 | self.assertIn("Dependency cycle: ", message) 224 | self.assertIn("'a'", message) 225 | self.assertIn("'b'", message) 226 | self.assertIn("'c'", message) 227 | 228 | def test_acquire_arguments(self): 229 | @inject(1, None, 3, foo=4) 230 | def function(a, b, c, foo=None, bar=None): 231 | return (a, b, c, foo, bar) 232 | 233 | graph = Graph() 234 | graph.register_instance(1, 11) 235 | graph.register_instance(3, 33) 236 | graph.register_instance(4, 44) 237 | graph.register_factory('function', function) 238 | graph.validate() 239 | 240 | self.assertTupleEqual( 241 | graph.acquire('function', arguments={1: 22, 'bar': 55}), 242 | (11, 22, 33, 44, 55) 243 | ) 244 | self.assertTupleEqual( 245 | graph.acquire( 246 | 'function', 247 | arguments={ 248 | 1: 22, 249 | 'bar': 55, 250 | 'foo': 100, 251 | } 252 | ), 253 | (11, 22, 33, 100, 55) 254 | ) 255 | with self.assertRaises(TypeError): 256 | graph.acquire( 257 | 'function', 258 | arguments={ 259 | 1: 22, 260 | ('invalid', 'argument', 'key'): 55, 261 | } 262 | ) 263 | 264 | def test_get_arguments(self): 265 | def function(a, b=None, c=injected(1)): 266 | return (a, b, c) 267 | 268 | graph = Graph() 269 | graph.register_instance(1, 11) 270 | graph.register_factory('function', function) 271 | graph.validate() 272 | 273 | self.assertTupleEqual( 274 | graph.get('function', 33, b=22), 275 | (33, 22, 11) 276 | ) 277 | self.assertTupleEqual( 278 | graph.get('function', 33, b=22, c=44), 279 | (33, 22, 44) 280 | ) 281 | 282 | def test_scope_factory(self): 283 | notlocal = [0] 284 | 285 | def factory(): 286 | notlocal[0] += 1 287 | return notlocal[0] 288 | 289 | graph = Graph() 290 | graph.register_factory('scoped', factory, scope=ProcessScope) 291 | graph.register_factory('unscoped', factory) 292 | 293 | self.assertEqual(graph.get('scoped'), 1) 294 | self.assertEqual(graph.get('scoped'), 1) 295 | self.assertEqual(graph.get('scoped'), 1) 296 | self.assertEqual(graph.get('scoped'), 1) 297 | self.assertEqual(graph.get('scoped'), 1) 298 | 299 | self.assertEqual(graph.get('unscoped'), 2) 300 | self.assertEqual(graph.get('unscoped'), 3) 301 | 302 | self.assertEqual(graph.get('scoped'), 1) 303 | self.assertEqual(graph.get('scoped'), 1) 304 | 305 | self.assertEqual(graph.get('unscoped'), 4) 306 | 307 | def test_scope_function(self): 308 | notlocal = [0] 309 | 310 | def factory(): 311 | notlocal[0] += 1 312 | return notlocal[0] 313 | 314 | @inject(i='i') 315 | def function(i): 316 | return i 317 | 318 | graph = Graph() 319 | graph.register_factory('i', factory) 320 | graph.register_function('function', function, scope=ProcessScope) 321 | 322 | self.assertEqual(graph.get('function')(), 1) 323 | self.assertEqual(graph.get('function')(), 1) 324 | self.assertEqual(graph.get('function')(), 1) 325 | 326 | def test_unknown_scope(self): 327 | class FooBarScope(object): 328 | pass 329 | 330 | graph = Graph() 331 | 332 | with self.assertRaises(UnknownScopeError) as cm: 333 | graph.register_factory('foo', lambda: None, scope=FooBarScope) 334 | self.assertEqual(cm.exception.scope_type, FooBarScope) 335 | self.assertIn('FooBarScope', str(cm.exception)) 336 | 337 | with self.assertRaises(UnknownScopeError) as cm: 338 | graph.register_function('foo', lambda: None, scope=FooBarScope) 339 | self.assertEqual(cm.exception.scope_type, FooBarScope) 340 | self.assertIn('FooBarScope', str(cm.exception)) 341 | 342 | def test_late_unknown_scope(self): 343 | class FooBarScope(object): 344 | pass 345 | 346 | graph = Graph() 347 | graph.register_factory('foo', lambda: None) 348 | graph.providers['foo'].scope = FooBarScope 349 | with self.assertRaises(UnknownScopeError) as cm: 350 | graph.get('foo') 351 | self.assertEqual(cm.exception.scope_type, FooBarScope) 352 | self.assertIn('FooBarScope', str(cm.exception)) 353 | 354 | def test_unregister_provider(self): 355 | graph = Graph() 356 | graph.register_instance('foo', 'bar') 357 | self.assertEqual(graph.get('foo'), 'bar') 358 | graph.unregister_provider('foo') 359 | with self.assertRaises(KeyError): 360 | graph.get('foo') 361 | 362 | def test_unregister_scope(self): 363 | graph = Graph() 364 | graph.register_factory('foo', lambda: None, scope=ProcessScope) 365 | graph.unregister_scope(ProcessScope) 366 | with self.assertRaises(UnknownScopeError): 367 | graph.register_factory('bar', lambda: None, scope=ProcessScope) 368 | -------------------------------------------------------------------------------- /wiring/interface.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import inspect 3 | import operator 4 | 5 | import six 6 | 7 | 8 | __all__ = ( 9 | 'InterfaceComplianceError', 10 | 'MissingAttributeError', 11 | 'MethodValidationError', 12 | 'Attribute', 13 | 'Method', 14 | 'Interface', 15 | 'get_implemented_interfaces', 16 | 'set_implemented_interfaces', 17 | 'add_implemented_interfaces', 18 | 'implements', 19 | 'implements_only', 20 | 'isimplementation', 21 | ) 22 | 23 | 24 | class InterfaceComplianceError(Exception): 25 | """ 26 | Common base for all interface compliance validation errors. 27 | """ 28 | 29 | 30 | class MissingAttributeError(InterfaceComplianceError): 31 | """ 32 | Exception raised when an object is validated against :py:class:`Interface` 33 | (by :py:meth:`Interface.check_compliance`) and is found to be missing 34 | a required attribute. 35 | """ 36 | 37 | def __init__(self, attribute_name): 38 | self.attribute_name = attribute_name 39 | """Name of the missing attribute.""" 40 | 41 | def __str__(self): 42 | return "Validated object is missing `{attribute}` attribute.".format( 43 | attribute=self.attribute_name 44 | ) 45 | 46 | 47 | class MethodValidationError(InterfaceComplianceError): 48 | """ 49 | Exception raised when a function is validated against :py:class:`Method` 50 | specification (e.g. by :py:meth:`Interface.check_compliance`) and some of 51 | the arguments differ. 52 | """ 53 | 54 | def __init__(self, function, expected_argspec, observed_argspec): 55 | self.function = function 56 | """ 57 | Function object that didn't pass the check. 58 | """ 59 | self.expected_argspec = expected_argspec 60 | """ 61 | An `inspect.ArgSpec` or `inspect.FullArgSpec` (depending on Python 62 | version) named tuple specifying expected function arguments. 63 | """ 64 | self.observed_argspec = observed_argspec 65 | """ 66 | An `inspect.ArgSpec` or `inspect.FullArgSpec` (depending on Python 67 | version) named tuple specifying arguments that the validated 68 | function actually takes. 69 | """ 70 | 71 | def __str__(self): 72 | return ( 73 | "Function `{function}` does not comply with interface definition." 74 | " Expected arguments: {expected}" 75 | " Observed arguments: {observed}" 76 | ).format( 77 | function=self.function.__name__, 78 | expected=inspect.formatargspec(*self.expected_argspec), 79 | observed=inspect.formatargspec(*self.observed_argspec) 80 | ) 81 | 82 | 83 | class Attribute(object): 84 | """ 85 | This class stores a specification of an object attribute, namely its 86 | docstring. It is used by :py:class:`InterfaceMetaclass` to store 87 | information about required attributes of an :term:`interface`. 88 | """ 89 | 90 | def __init__(self, docstring=None): 91 | self.docstring = docstring 92 | """ 93 | Docstring of a described attribute. 94 | """ 95 | 96 | def __repr__(self): 97 | if self.docstring: 98 | return ''.format(self.docstring) 99 | else: 100 | return '' 101 | 102 | 103 | class Method(Attribute): 104 | """ 105 | This class stores a specification of a method, describing its arguments and 106 | holding its docstring. It is used by :py:class:`InterfaceMetaclass` to 107 | store information about required methods of an :term:`interface`. 108 | """ 109 | 110 | def __init__(self, argument_specification, docstring=None): 111 | super(Method, self).__init__(docstring) 112 | self.argument_specification = argument_specification 113 | """ 114 | An `inspect.ArgSpec` or `inspect.FullArgSpec` (depending on Python 115 | version) named tuple specifying arguments taken by described method. 116 | These will not include implied `self` argument. 117 | """ 118 | 119 | def __repr__(self): 120 | return ''.format( 121 | inspect.formatargspec(*self.argument_specification) 122 | ) 123 | 124 | def check_compliance(self, function): 125 | """ 126 | Checks if a given `function` complies with this specification. If an 127 | inconsistency is detected a :py:exc:`MethodValidationError` exception 128 | is raised. 129 | 130 | .. note:: 131 | This method will not work as expected when `function` is an unbound 132 | method (``SomeClass.some_method``), as in Python 3 there is no way 133 | to recognize that this is in fact a method. Therefore, the implied 134 | `self` argument will not be ignored. 135 | 136 | :raises: 137 | :py:exc:`MethodValidationError` 138 | """ 139 | argument_specification = _get_argument_specification(function) 140 | if inspect.ismethod(function): 141 | # Remove implied `self` argument from specification if function is 142 | # a method. 143 | argument_specification = argument_specification._replace( 144 | args=argument_specification.args[1:] 145 | ) 146 | if argument_specification != self.argument_specification: 147 | raise MethodValidationError( 148 | function, 149 | self.argument_specification, 150 | argument_specification 151 | ) 152 | 153 | 154 | class InterfaceMetaclass(type): 155 | """ 156 | This metaclass analyzes declared attributes and methods of new 157 | :term:`interface` classes and turns them into a dictionary of 158 | :py:class:`Attribute` and :py:class:`Method` specifications which is stored 159 | as `attributes` dictionary of an interface class. It also handles 160 | inheritance of those declarations. 161 | 162 | It also collects (and stores in `implied` attribute) a set of this and all 163 | base interfaces, as it never changes after the class is declared and is 164 | very commonly needed. 165 | """ 166 | 167 | def __new__(cls, interface_name, bases, attributes): 168 | ignored_attribute_names = ( 169 | '__module__', 170 | '__qualname__', 171 | '__locals__', 172 | '__doc__', 173 | ) 174 | ignored_attributes = {} 175 | processed_attributes = {} 176 | 177 | # Filter out private attributes which should not be treated as 178 | # interface declarations. 179 | for name, value in six.iteritems(attributes): 180 | if (isinstance(value, classmethod) or 181 | isinstance(value, staticmethod) or 182 | name in ignored_attribute_names): 183 | ignored_attributes[name] = value 184 | else: 185 | processed_attributes[name] = value 186 | 187 | interface = super(InterfaceMetaclass, cls).__new__( 188 | cls, 189 | interface_name, 190 | bases, 191 | ignored_attributes 192 | ) 193 | 194 | # Precalculate a tuple of this and all base interfaces in method 195 | # resolution order. 196 | 197 | interface.implied = tuple(( 198 | ancestor for ancestor in inspect.getmro(interface) 199 | if cls._is_interface_class(ancestor) 200 | )) 201 | 202 | interface.attributes = {} 203 | 204 | for base in reversed(bases): 205 | if cls._is_interface_class(base): 206 | interface.attributes.update(base.attributes) 207 | 208 | for name, value in six.iteritems(processed_attributes): 209 | if isinstance(value, Attribute): 210 | interface.attributes[name] = value 211 | elif inspect.isfunction(value): 212 | docstring = inspect.getdoc(value) 213 | argument_specification = _get_argument_specification(value) 214 | interface.attributes[name] = Method( 215 | argument_specification, 216 | docstring=docstring 217 | ) 218 | else: 219 | if isinstance(value, six.string_types): 220 | docstring = value 221 | else: 222 | docstring = None 223 | interface.attributes[name] = Attribute(docstring=docstring) 224 | 225 | return interface 226 | 227 | @classmethod 228 | def _is_interface_class(cls, other_class): 229 | if not isinstance(other_class, cls): 230 | # Other class didn't came from this metaclass. 231 | return False 232 | if all(map(lambda b: not isinstance(b, cls), other_class.__bases__)): 233 | # Other class is Interface class from this module. 234 | return False 235 | return True 236 | 237 | 238 | @six.add_metaclass(InterfaceMetaclass) 239 | class Interface(object): 240 | __doc__ = """ 241 | A base class for :term:`interface` classes, using the 242 | :py:class:`InterfaceMetaclass`. 243 | """ + InterfaceMetaclass.__doc__ 244 | 245 | implied = frozenset() 246 | """ 247 | A `frozenset` of this and all base :term:`interfaces `. 248 | """ 249 | 250 | attributes = {} 251 | """ 252 | Dictionary describing provided attributes, including methods. Keys are 253 | attribute names and values are :py:class:`Attribute` or :py:class:`Method` 254 | instances. 255 | """ 256 | 257 | @classmethod 258 | def check_compliance(cls, instance): 259 | """ 260 | Checks if given `instance` complies with this :term:`interface`. If 261 | `instance` is found to be invalid a :py:exc:`InterfaceComplianceError` 262 | subclass is raised. 263 | 264 | `instance`'s class doesn't have to declare it implements an interface 265 | to be validated against it. 266 | 267 | .. note:: 268 | Classes cannot be validated against an interface, because instance 269 | attributes couldn't be checked. 270 | 271 | :raises: 272 | :py:exc:`MissingAttributeError`, 273 | :py:exc:`MethodValidationError` 274 | """ 275 | if inspect.isclass(instance): 276 | raise TypeError( 277 | "Only instances, not classes, can be validated against an" 278 | " interface." 279 | ) 280 | for name, value in six.iteritems(cls.attributes): 281 | if not hasattr(instance, name): 282 | raise MissingAttributeError(name) 283 | if isinstance(value, Method): 284 | value.check_compliance(getattr(instance, name)) 285 | 286 | 287 | def get_implemented_interfaces(cls): 288 | """ 289 | Returns a set of :term:`interfaces ` declared as implemented by 290 | class `cls`. 291 | """ 292 | if hasattr(cls, '__interfaces__'): 293 | return cls.__interfaces__ 294 | return six.moves.reduce( 295 | lambda x, y: x.union(y), 296 | map( 297 | get_implemented_interfaces, 298 | inspect.getmro(cls)[1:] 299 | ), 300 | set() 301 | ) 302 | 303 | 304 | def set_implemented_interfaces(cls, interfaces): 305 | """ 306 | Declares :term:`interfaces ` as implemented by class `cls`. 307 | Those already declared are overriden. 308 | """ 309 | setattr( 310 | cls, 311 | '__interfaces__', 312 | frozenset( 313 | six.moves.reduce( 314 | lambda x, y: x.union(y), 315 | map(operator.attrgetter('implied'), interfaces), 316 | set() 317 | ) 318 | ) 319 | ) 320 | 321 | 322 | def add_implemented_interfaces(cls, interfaces): 323 | """ 324 | Adds :term:`interfaces ` to those already declared as 325 | implemented by class `cls`. 326 | """ 327 | implemented = set( 328 | six.moves.reduce( 329 | lambda x, y: x.union(y), 330 | map(operator.attrgetter('implied'), interfaces), 331 | set() 332 | ) 333 | ) 334 | implemented.update(*map( 335 | get_implemented_interfaces, 336 | inspect.getmro(cls) 337 | )) 338 | setattr(cls, '__interfaces__', frozenset(implemented)) 339 | 340 | 341 | def implements(*interfaces): 342 | """ 343 | Decorator declaring :term:`interfaces ` implemented by 344 | a decorated class. 345 | """ 346 | def wrapper(cls): 347 | add_implemented_interfaces(cls, interfaces) 348 | return cls 349 | return wrapper 350 | 351 | 352 | def implements_only(*interfaces): 353 | """ 354 | Decorator declaring :term:`interfaces ` implemented by 355 | a decorated class. Previous declarations including inherited declarations 356 | are overridden. 357 | """ 358 | def wrapper(cls): 359 | set_implemented_interfaces(cls, interfaces) 360 | return cls 361 | return wrapper 362 | 363 | 364 | def isimplementation(obj, interfaces): 365 | """ 366 | Returns `True` if `obj` is a class implementing all of `interfaces` or an 367 | instance of such class. 368 | 369 | `interfaces` can be a single :term:`interface` class or an iterable of 370 | interface classes. 371 | """ 372 | if not inspect.isclass(obj): 373 | isimplementation(obj.__class__, interfaces) 374 | if not isinstance(interfaces, collections.Iterable): 375 | interfaces = [interfaces] 376 | return frozenset(interfaces).issubset( 377 | get_implemented_interfaces(obj) 378 | ) 379 | 380 | 381 | if six.PY3: 382 | _get_argument_specification = inspect.getfullargspec 383 | else: 384 | _get_argument_specification = inspect.getargspec 385 | -------------------------------------------------------------------------------- /wiring/graph.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | import six 4 | 5 | from wiring.dependency import Factory 6 | from wiring.providers import ( 7 | FactoryProvider, 8 | FunctionProvider, 9 | InstanceProvider 10 | ) 11 | from wiring.scopes import ProcessScope, SingletonScope, ThreadScope 12 | 13 | 14 | __all__ = ( 15 | 'GraphValidationError', 16 | 'SelfDependencyError', 17 | 'MissingDependencyError', 18 | 'DependencyCycleError', 19 | 'UnknownScopeError', 20 | 'Graph', 21 | ) 22 | 23 | 24 | class GraphValidationError(Exception): 25 | """ 26 | Common base for all :term:`object graph` validation exceptions, allowing 27 | general except clauses and `instanceof()` testing. 28 | """ 29 | 30 | 31 | class SelfDependencyError(GraphValidationError): 32 | """ 33 | Raised when one of :term:`graph ` :term:`providers 34 | ` is :term:`dependent ` on a :term:`specification` it 35 | itself provides. 36 | """ 37 | 38 | def __init__(self, specification): 39 | self.specification = specification 40 | """A :term:`specification` that was dependent on itself.""" 41 | 42 | def __str__(self): 43 | return "Provider for {} is dependent on itself.".format( 44 | repr(self.specification) 45 | ) 46 | 47 | 48 | class MissingDependencyError(GraphValidationError): 49 | """ 50 | Raised when one of :term:`provider`'s :term:`dependencies ` 51 | cannot be satisfied within the :term:`object graph`. 52 | """ 53 | 54 | def __init__(self, dependant, dependency): 55 | self.dependant = dependant 56 | """ 57 | Specification of a :term:`provider` of which :term:`dependency` cannot 58 | be satisfied. 59 | """ 60 | self.dependency = dependency 61 | """ 62 | :term:`Specification ` of a missing :term:`dependency`. 63 | """ 64 | 65 | def __str__(self): 66 | return ( 67 | "Cannot find dependency {dependency} for {dependant} provider." 68 | ).format( 69 | dependency=repr(self.dependency), 70 | dependant=repr(self.dependant), 71 | ) 72 | 73 | 74 | class DependencyCycleError(GraphValidationError): 75 | """ 76 | Raised when there is a :term:`dependency cycle` in an :term:`object graph`. 77 | """ 78 | 79 | def __init__(self, cycle): 80 | self.cycle = tuple(cycle) 81 | """ 82 | A tuple containing all :term:`specifications ` in 83 | a cycle, in such an order that each element depends on previous element 84 | and the first element depends on the last one. 85 | """ 86 | 87 | def __str__(self): 88 | return "Dependency cycle: {cycle}.".format( 89 | cycle=' -> '.join( 90 | list(map(repr, self.cycle)) + [repr(self.cycle[0])] 91 | ) 92 | ) 93 | 94 | 95 | class UnknownScopeError(Exception): 96 | """ 97 | Raised when registering :term:`provider` with a :term:`scope` type that 98 | hasn't been previously registered in the :term:`object graph`. 99 | """ 100 | 101 | def __init__(self, scope_type): 102 | self.scope_type = scope_type 103 | """Type of the scope that hasn't been properly registered.""" 104 | 105 | def __str__(self): 106 | return ( 107 | "Scope type {scope} was not registered within the object graph." 108 | ).format( 109 | scope=repr(self.scope_type) 110 | ) 111 | 112 | 113 | class Graph(object): 114 | """ 115 | Respresents an :term:`object graph`. Contains registered scopes and 116 | providers, and can be used to validate and resolve provider dependencies 117 | and creating provided objects. 118 | """ 119 | 120 | class FactoryProxy(object): 121 | """ 122 | A proxy object injected when `Factory()` is requested as 123 | a dependency. 124 | """ 125 | 126 | def __init__(self, graph, specification): 127 | self.graph = graph 128 | self.specification = specification 129 | 130 | def __call__(self, *args, **kwargs): 131 | return self.graph.get(self.specification, *args, **kwargs) 132 | 133 | def __init__(self): 134 | self.providers = {} 135 | """ 136 | Dictionary mapping :term:`specifications ` to 137 | :py:interface:`wiring.providers.IProvider` implementers that can 138 | provide the specified object. 139 | """ 140 | self.scopes = {} 141 | """ 142 | Dictionary mapping :term:`scope` types to their instances. Scope 143 | instances must conform to :py:interface:`wiring.scopes.IScope` 144 | interface. 145 | """ 146 | self.register_scope(SingletonScope, SingletonScope()) 147 | self.register_scope(ProcessScope, ProcessScope()) 148 | self.register_scope(ThreadScope, ThreadScope()) 149 | 150 | def acquire(self, specification, arguments=None): 151 | """ 152 | Returns an object for `specification` injecting its provider 153 | with a mix of its :term:`dependencies ` and given 154 | `arguments`. If there is a conflict between the injectable 155 | dependencies and `arguments`, the value from `arguments` is 156 | used. 157 | 158 | When one of `arguments` keys is neither an integer nor a string 159 | a `TypeError` is raised. 160 | 161 | :param specification: 162 | An object :term:`specification`. 163 | :param arguments: 164 | A dictionary of arguments given to the object :term:`provider`, 165 | overriding those that would be injected or filling in for those 166 | that wouldn't. Positional arguments should be stored under 0-based 167 | integer keys. 168 | :raises: 169 | TypeError 170 | """ 171 | if arguments is None: 172 | realized_dependencies = {} 173 | else: 174 | realized_dependencies = copy.copy(arguments) 175 | 176 | provider = self.providers[specification] 177 | 178 | scope = None 179 | if provider.scope is not None: 180 | try: 181 | scope = self.scopes[provider.scope] 182 | except KeyError: 183 | raise UnknownScopeError(provider.scope) 184 | 185 | if scope is not None and specification in scope: 186 | return scope[specification] 187 | 188 | dependencies = six.iteritems(provider.dependencies) 189 | for argument, dependency_specification in dependencies: 190 | if argument not in realized_dependencies: 191 | if isinstance(dependency_specification, Factory): 192 | realized_dependencies[argument] = self.FactoryProxy( 193 | self, 194 | dependency_specification.specification 195 | ) 196 | else: 197 | realized_dependencies[argument] = self.acquire( 198 | dependency_specification 199 | ) 200 | 201 | args = [] 202 | kwargs = {} 203 | for argument, value in six.iteritems(realized_dependencies): 204 | if isinstance(argument, six.integer_types): 205 | # Integer keys are for positional arguments. 206 | if len(args) <= argument: 207 | args.extend([None] * (argument + 1 - len(args))) 208 | args[argument] = value 209 | elif isinstance(argument, six.string_types): 210 | # String keys are for keyword arguments. 211 | kwargs[argument] = value 212 | else: 213 | raise TypeError( 214 | "{} is not a valid argument key".format(repr(argument)) 215 | ) 216 | 217 | instance = provider(*args, **kwargs) 218 | 219 | if scope is not None: 220 | scope[specification] = instance 221 | 222 | return instance 223 | 224 | def get(self, specification, *args, **kwargs): 225 | """ 226 | A more convenient version of :py:meth:`acquire()` for when you can 227 | provide positional arguments in a right order. 228 | """ 229 | arguments = dict(enumerate(args)) 230 | arguments.update(kwargs) 231 | return self.acquire(specification, arguments=arguments) 232 | 233 | def register_provider(self, specification, provider): 234 | """ 235 | Registers a :term:`provider` (a :py:class:`wiring.providers.Provider` 236 | instance) to be called when an object specified by 237 | :term:`specification` is needed. If there was already a provider for 238 | this specification it is overriden. 239 | """ 240 | if provider.scope is not None and provider.scope not in self.scopes: 241 | raise UnknownScopeError(provider.scope) 242 | self.providers[specification] = provider 243 | 244 | def unregister_provider(self, specification): 245 | """ 246 | Removes :term:`provider` for given `specification` from the graph. 247 | """ 248 | del self.providers[specification] 249 | 250 | def register_factory(self, specification, factory, scope=None): 251 | """ 252 | Shortcut for creating and registering 253 | a :py:class:`wiring.providers.FactoryProvider`. 254 | """ 255 | self.register_provider( 256 | specification, 257 | FactoryProvider(factory, scope=scope) 258 | ) 259 | 260 | def register_function(self, specification, function, scope=None): 261 | """ 262 | Shortcut for creating and registering 263 | a :py:class:`wiring.providers.FunctionProvider`. 264 | """ 265 | self.register_provider( 266 | specification, 267 | FunctionProvider(function, scope=scope) 268 | ) 269 | 270 | def register_instance(self, specification, instance): 271 | """ 272 | Registers given `instance` to be used as-is when an object specified by 273 | given :term:`specification` is needed. If there was already a provider 274 | for this specification it is overriden. 275 | """ 276 | self.register_provider(specification, InstanceProvider(instance)) 277 | 278 | def register_scope(self, scope_type, instance): 279 | """ 280 | Register instance of a :term:`scope` for given scope type. This scope 281 | may be later referred to by providers using this type. 282 | """ 283 | self.scopes[scope_type] = instance 284 | 285 | def unregister_scope(self, scope_type): 286 | """ 287 | Removes a :term:`scope` type from the graph. 288 | """ 289 | del self.scopes[scope_type] 290 | 291 | def validate(self): 292 | """ 293 | Asserts that every declared :term:`specification` can actually be 294 | realized, meaning that all of its :term:`dependencies ` are 295 | present and there are no self-dependencies or :term:`dependency cycles 296 | `. If such a problem is found, a proper exception 297 | (deriving from :py:class:`GraphValidationError`) is raised. 298 | 299 | :raises: 300 | :py:exc:`MissingDependencyError`, 301 | :py:exc:`SelfDependencyError`, 302 | :py:exc:`DependencyCycleError` 303 | """ 304 | # This method uses Tarjan's strongly connected components algorithm 305 | # with added self-dependency check to find dependency cyclces. 306 | 307 | # Index is just an integer, it's wrapped in a list as a workaround for 308 | # Python 2's lack of `nonlocal` keyword, so the nested 309 | # `strongconnect()` may modify it. 310 | index = [0] 311 | 312 | indices = {} 313 | lowlinks = {} 314 | stack = [] 315 | 316 | def strongconnect(specification): 317 | # Set the depth index for the node to the smallest unused index. 318 | indices[specification] = index[0] 319 | lowlinks[specification] = index[0] 320 | index[0] += 1 321 | stack.append(specification) 322 | provider = self.providers[specification] 323 | dependencies = six.itervalues(provider.dependencies) 324 | for dependency in dependencies: 325 | if isinstance(dependency, Factory): 326 | dependency = dependency.specification 327 | if dependency not in self.providers: 328 | raise MissingDependencyError(specification, dependency) 329 | if dependency == specification: 330 | raise SelfDependencyError(specification) 331 | if dependency not in indices: 332 | # Dependency has not yet been visited; recurse on it. 333 | strongconnect(dependency) 334 | lowlinks[specification] = min( 335 | lowlinks[specification], 336 | lowlinks[dependency] 337 | ) 338 | elif dependency in stack: 339 | # Dependency is in stack and hence in the current strongly 340 | # connected component. 341 | lowlinks[specification] = min( 342 | lowlinks[specification], 343 | indices[dependency] 344 | ) 345 | if lowlinks[specification] == indices[specification]: 346 | component = [] 347 | while True: 348 | component.append(stack.pop()) 349 | if component[-1] == specification: 350 | break 351 | if len(component) > 1: 352 | raise DependencyCycleError(reversed(component)) 353 | 354 | for specification, provider in six.iteritems(self.providers): 355 | if specification not in indices: 356 | strongconnect(specification) 357 | --------------------------------------------------------------------------------