├── 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 |
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