Compare commits

...

22 Commits

Author SHA1 Message Date
Roman Mogylatov
b4890dcf80 Bump version to 4.0.0a2 2020-09-28 16:33:39 -04:00
Roman Mogylatov
d9334ef1fe Add protection for wiring only declarative container instances 2020-09-28 15:52:21 -04:00
Roman Mogylatov
cc1b6ba3e6 Add __all__ for wiring module 2020-09-28 15:39:41 -04:00
Roman Mogylatov
42b0bde8d2 Deprecate provider.delegate() method 2020-09-28 15:38:34 -04:00
Roman Mogylatov
5d4eeb648a Deprecate ext package modules and remove types module 2020-09-28 14:16:05 -04:00
Roman Mogylatov
26e490bf0a Add container.unwire() typing stub 2020-09-28 13:41:54 -04:00
Roman Mogylatov
6182b8448a
Wiring refactoring (#296)
* Refactor wiring

* Add todos to wiring

* Implement wiring of config invariant

* Implement sub containers wiring + add tests

* Add test for wiring config invariant
2020-09-27 23:10:11 -04:00
Roman Mogylatov
7f854548d6 Make flake8 happy 2020-09-26 01:17:42 -04:00
Roman Mogylatov
95db0eddc9 Implement Provide[foo.provided.bar.baz.call()] 2020-09-26 01:07:32 -04:00
Roman Mogylatov
6d92df32aa Implement wiring for Provide[foo.provider] 2020-09-26 00:31:29 -04:00
Roman Mogylatov
c20c57ae7c Update demo 2020-09-26 00:23:11 -04:00
Roman Mogylatov
7b2baeeb6f Remove not needed images 2020-09-24 20:51:45 -04:00
Roman Mogylatov
9653dfc263 Add sanic example 2020-09-23 18:29:13 -04:00
Roman Mogylatov
e37b5181e4 Rename views module to handlers in aiohttp example 2020-09-23 15:58:36 -04:00
Roman Mogylatov
a9970b63b9 Rename aiohttp example directory 2020-09-22 22:03:49 -04:00
Roman Mogylatov
170819c6ed Update flask example 2020-09-22 22:03:02 -04:00
Roman Mogylatov
4fab71c35b Update aiohttp example 2020-09-22 21:48:41 -04:00
Roman Mogylatov
170263de4d Add flake8 ignore for demo 2020-09-21 16:55:42 -04:00
Roman Mogylatov
b7efb1e3e2 Add pydocstyle ignore for demo 2020-09-21 16:51:57 -04:00
Roman Mogylatov
dd8778bf20 Updaet demo example 2020-09-21 16:46:02 -04:00
Roman Mogylatov
edd8979bf6 Bump version to 4.0 2020-09-20 21:51:48 -04:00
Roman Mogylatov
af7364e062
Add wiring (#294)
* Add wiring module

* Fix code style

* Fix package test

* Add version fix

* Try spike for 3.6

* Try another fix with metaclass

* Downsample required version to 3.6

* Introduce concept with annotations

* Fix bugs

* Add debug message

* Add extra tests

* Add extra debugging

* Update config resolving

* Remove 3.6 generic meta fix

* Fix Flake8

* Add spike for 3.6

* Add Python 3.6 spike

* Add unwire functionality

* Add support of corouting functions
2020-09-20 21:50:25 -04:00
68 changed files with 11643 additions and 7257 deletions

View File

@ -74,31 +74,39 @@ Key features of the ``Dependency Injector``:
.. code-block:: python
from dependency_injector import containers, providers
from dependency_injector import containers, providers
from dependency_injector.wiring import Provide
class Container(containers.DeclarativeContainer):
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
config = providers.Configuration()
api_client = providers.Singleton(
ApiClient,
api_key=config.api_key,
timeout=config.timeout.as_int(),
)
api_client = providers.Singleton(
ApiClient,
api_key=config.api_key,
timeout=config.timeout.as_int(),
)
service = providers.Factory(
Service,
api_client=api_client,
)
service = providers.Factory(
Service,
api_client=api_client,
)
if __name__ == '__main__':
container = Container()
container.config.api_key.from_env('API_KEY')
container.config.timeout.from_env('TIMEOUT')
def main(service: Service = Provide[Container.service]):
...
service = container.service()
if __name__ == '__main__':
container = Container()
container.config.api_key.from_env('API_KEY')
container.config.timeout.from_env('TIMEOUT')
container.wire(modules=[sys.modules[__name__]])
main()
With the ``Dependency Injector`` you keep **application structure in one place**.
This place is called **the container**. You use the container to manage all the components of the

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -83,31 +83,39 @@ Key features of the ``Dependency Injector``:
.. code-block:: python
from dependency_injector import containers, providers
from dependency_injector import containers, providers
from dependency_injector.wiring import Provide
class Container(containers.DeclarativeContainer):
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
config = providers.Configuration()
api_client = providers.Singleton(
ApiClient,
api_key=config.api_key,
timeout=config.timeout.as_int(),
)
api_client = providers.Singleton(
ApiClient,
api_key=config.api_key,
timeout=config.timeout.as_int(),
)
service = providers.Factory(
Service,
api_client=api_client,
)
service = providers.Factory(
Service,
api_client=api_client,
)
if __name__ == '__main__':
container = Container()
container.config.api_key.from_env('API_KEY')
container.config.timeout.from_env('TIMEOUT')
def main(service: Service = Provide[Container.service]):
...
service = container.service()
if __name__ == '__main__':
container = Container()
container.config.api_key.from_env('API_KEY')
container.config.timeout.from_env('TIMEOUT')
container.wire(modules=[sys.modules[__name__]])
main()
With the ``Dependency Injector`` you keep **application structure in one place**.
This place is called **the container**. You use the container to manage all the components of the

View File

@ -7,6 +7,17 @@ that were made in every particular version.
From version 0.7.6 *Dependency Injector* framework strictly
follows `Semantic versioning`_
4.0.0
-----
- Add ``wiring`` feature.
- Deprecate ``ext.aiohttp`` module.
- Deprecate ``ext.flask`` module.
- Remove deprecated ``types`` module.
- Deprecate ``.delegate()`` provider method in favor of ``.provider`` attribute.
- Add ``sanic`` example.
- Update ``aiohttp`` example.
- Update ``flask`` example.
3.44.0
------
- Add native support of the generics to the providers: ``some_provider = providers.Provider[SomeClass]``.

View File

@ -1,17 +1,10 @@
import sys
from unittest import mock
from dependency_injector import containers, providers
from dependency_injector.wiring import Provide
class ApiClient:
def __init__(self, api_key: str, timeout: int):
self.api_key = api_key
self.timeout = timeout
class Service:
def __init__(self, api_client: ApiClient):
self.api_client = api_client
from di import ApiClient, Service
class Container(containers.DeclarativeContainer):
@ -30,9 +23,19 @@ class Container(containers.DeclarativeContainer):
)
def main(service: Service = Provide[Container.service]):
...
if __name__ == '__main__':
container = Container()
container.config.api_key.from_env('API_KEY')
container.config.timeout.from_env('TIMEOUT')
service = container.service()
container.wire(modules=[sys.modules[__name__]])
main()
with container.api_client.override(mock.Mock()):
main()

View File

@ -14,5 +14,15 @@ class Service:
self.api_client = api_client
def main() -> None:
service = Service(
api_client=ApiClient(
api_key=os.getenv('API_KEY'),
timeout=os.getenv('TIMEOUT'),
),
)
...
if __name__ == '__main__':
service = Service(ApiClient(os.getenv('API_KEY'), os.getenv('TIMEOUT')))
main()

View File

@ -14,5 +14,10 @@ class Service:
self.api_client = ApiClient()
if __name__ == '__main__':
def main() -> None:
service = Service()
...
if __name__ == '__main__':
main()

View File

@ -1,11 +0,0 @@
from unittest import mock
from demo import Container
if __name__ == '__main__':
container = Container()
with container.api_client.override(mock.Mock()):
service = container.service()
assert isinstance(service.api_client, mock.Mock)

View File

@ -1,8 +1,8 @@
Aiohttp Dependency Injection Example
====================================
Dependency Injector + Aiohttp Example
=====================================
Application ``giphynavigator`` is an `Aiohttp <https://docs.aiohttp.org/>`_ +
`Dependency Injector <http://python-dependency-injector.ets-labs.org/>`_ application.
`Dependency Injector <http://python-dependency-injector.ets-labs.org/>`_ example application.
Run
---
@ -107,11 +107,11 @@ The output should be something like:
---------------------------------------------------
giphynavigator/__init__.py 0 0 100%
giphynavigator/__main__.py 5 5 0%
giphynavigator/application.py 10 0 100%
giphynavigator/containers.py 10 0 100%
giphynavigator/application.py 12 0 100%
giphynavigator/containers.py 6 0 100%
giphynavigator/giphy.py 14 9 36%
giphynavigator/handlers.py 9 0 100%
giphynavigator/services.py 9 1 89%
giphynavigator/tests.py 35 0 100%
giphynavigator/views.py 7 0 100%
giphynavigator/tests.py 37 0 100%
---------------------------------------------------
TOTAL 90 15 83%
TOTAL 92 15 84%

View File

@ -0,0 +1,5 @@
giphy:
request_timeout: 10
default:
query: "Dependency Injector"
limit: 10

View File

@ -2,20 +2,23 @@
from aiohttp import web
from .containers import ApplicationContainer
from .containers import Container
from . import handlers
def create_app():
"""Create and return aiohttp application."""
container = ApplicationContainer()
container = Container()
container.config.from_yaml('config.yml')
container.config.giphy.api_key.from_env('GIPHY_API_KEY')
app: web.Application = container.app()
container.wire(modules=[handlers])
app = web.Application()
app.container = container
app.add_routes([
web.get('/', container.index_view.as_view()),
web.get('/', handlers.index),
])
return app

View File

@ -0,0 +1,21 @@
"""Containers module."""
from dependency_injector import containers, providers
from . import giphy, services
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
giphy_client = providers.Factory(
giphy.GiphyClient,
api_key=config.giphy.api_key,
timeout=config.giphy.request_timeout,
)
search_service = providers.Factory(
services.SearchService,
giphy_client=giphy_client,
)

View File

@ -1,15 +1,17 @@
"""Views module."""
"""Handlers module."""
from aiohttp import web
from dependency_injector.wiring import Provide
from .services import SearchService
from .containers import Container
async def index(
request: web.Request,
search_service: SearchService,
default_query: str,
default_limit: int,
search_service: SearchService = Provide[Container.search_service],
default_query: str = Provide[Container.config.default.query],
default_limit: int = Provide[Container.config.default.limit.as_int()],
) -> web.Response:
query = request.query.get('query', default_query)
limit = int(request.query.get('limit', default_limit))

View File

@ -10,7 +10,9 @@ from giphynavigator.giphy import GiphyClient
@pytest.fixture
def app():
return create_app()
app = create_app()
yield app
app.container.unwire()
@pytest.fixture
@ -73,5 +75,5 @@ async def test_index_default_params(client, app):
assert response.status == 200
data = await response.json()
assert data['query'] == app.container.config.search.default_query()
assert data['limit'] == app.container.config.search.default_limit()
assert data['query'] == app.container.config.default.query()
assert data['limit'] == app.container.config.default.limit()

View File

@ -1,8 +1,8 @@
Flask Dependency Injection Example
==================================
Dependency Injector + Flask Example
===================================
Application ``githubnavigator`` is a `Flask <https://flask.palletsprojects.com/>`_ +
`Dependency Injector <http://python-dependency-injector.ets-labs.org/>`_ application.
`Dependency Injector <http://python-dependency-injector.ets-labs.org/>`_ example application.
.. image:: screenshot.png
@ -90,10 +90,10 @@ The output should be something like:
Name Stmts Miss Cover
----------------------------------------------------
githubnavigator/__init__.py 0 0 100%
githubnavigator/application.py 11 0 100%
githubnavigator/containers.py 13 0 100%
githubnavigator/application.py 15 0 100%
githubnavigator/containers.py 7 0 100%
githubnavigator/services.py 14 0 100%
githubnavigator/tests.py 32 0 100%
githubnavigator/views.py 7 0 100%
githubnavigator/tests.py 34 0 100%
githubnavigator/views.py 9 0 100%
----------------------------------------------------
TOTAL 77 0 100%
TOTAL 79 0 100%

View File

@ -0,0 +1,5 @@
github:
request_timeout: 10
default:
query: "Dependency Injector"
limit: 10

View File

@ -0,0 +1,25 @@
"""Application module."""
from flask import Flask
from flask_bootstrap import Bootstrap
from .containers import Container
from . import views
def create_app():
container = Container()
container.config.from_yaml('config.yml')
container.config.github.auth_token.from_env('GITHUB_TOKEN')
container.wire(modules=[views])
app = Flask(__name__)
app.container = container
bootstrap = Bootstrap()
bootstrap.init_app(app)
app.add_url_rule('/', 'index', views.index)
return app

View File

@ -0,0 +1,22 @@
"""Application containers module."""
from dependency_injector import containers, providers
from github import Github
from . import services
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
github_client = providers.Factory(
Github,
login_or_token=config.github.auth_token,
timeout=config.github.request_timeout,
)
search_service = providers.Factory(
services.SearchService,
github_client=github_client,
)

View File

@ -11,7 +11,9 @@ from .application import create_app
@pytest.fixture
def app():
return create_app()
app = create_app()
yield app
app.container.unwire()
def test_index(client, app):

View File

@ -1,14 +1,16 @@
"""Views module."""
from flask import request, render_template
from dependency_injector.wiring import Provide
from .services import SearchService
from .containers import Container
def index(
search_service: SearchService,
default_query: str,
default_limit: int,
search_service: SearchService = Provide[Container.search_service],
default_query: str = Provide[Container.config.default.query],
default_limit: int = Provide[Container.config.default.limit.as_int()],
):
query = request.args.get('query', default_query)
limit = request.args.get('limit', default_limit, int)

View File

Before

Width:  |  Height:  |  Size: 647 KiB

After

Width:  |  Height:  |  Size: 647 KiB

View File

@ -1,5 +0,0 @@
github:
request_timeout: 10
search:
default_query: "Dependency Injector"
default_limit: 10

View File

@ -1,20 +0,0 @@
"""Application module."""
from .containers import ApplicationContainer
def create_app():
"""Create and return Flask application."""
container = ApplicationContainer()
container.config.from_yaml('config.yml')
container.config.github.auth_token.from_env('GITHUB_TOKEN')
app = container.app()
app.container = container
bootstrap = container.bootstrap()
bootstrap.init_app(app)
app.add_url_rule('/', view_func=container.index_view.as_view())
return app

View File

@ -1,37 +0,0 @@
"""Application containers module."""
from dependency_injector import containers, providers
from dependency_injector.ext import flask
from flask import Flask
from flask_bootstrap import Bootstrap
from github import Github
from . import views, services
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
app = flask.Application(Flask, __name__)
bootstrap = flask.Extension(Bootstrap)
config = providers.Configuration()
github_client = providers.Factory(
Github,
login_or_token=config.github.auth_token,
timeout=config.github.request_timeout,
)
search_service = providers.Factory(
services.SearchService,
github_client=github_client,
)
index_view = flask.View(
views.index,
search_service=search_service,
default_query=config.search.default_query,
default_limit=config.search.default_limit,
)

View File

@ -1,5 +0,0 @@
giphy:
request_timeout: 10
search:
default_query: "Dependency Injector"
default_limit: 10

View File

@ -1,33 +0,0 @@
"""Application containers module."""
from dependency_injector import containers, providers
from dependency_injector.ext import aiohttp
from aiohttp import web
from . import giphy, services, views
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
app = aiohttp.Application(web.Application)
config = providers.Configuration()
giphy_client = providers.Factory(
giphy.GiphyClient,
api_key=config.giphy.api_key,
timeout=config.giphy.request_timeout,
)
search_service = providers.Factory(
services.SearchService,
giphy_client=giphy_client,
)
index_view = aiohttp.View(
views.index,
search_service=search_service,
default_query=config.search.default_query,
default_limit=config.search.default_limit,
)

View File

@ -0,0 +1,117 @@
Dependency Injector + Sanic Example
=====================================
Application ``giphynavigator`` is a `Sanic <https://sanic.readthedocs.io/en/latest/index.html>`_ +
`Dependency Injector <http://python-dependency-injector.ets-labs.org/>`_ example application.
Run
---
Create virtual environment:
.. code-block:: bash
virtualenv venv
. venv/bin/activate
Install requirements:
.. code-block:: bash
pip install -r requirements.txt
To run the application do:
.. code-block:: bash
export GIPHY_API_KEY=wBJ2wZG7SRqfrU9nPgPiWvORmloDyuL0
python -m giphynavigator
The output should be something like:
.. code-block::
[2020-09-23 18:16:31 -0400] [48258] [INFO] Goin' Fast @ http://0.0.0.0:8000
[2020-09-23 18:16:31 -0400] [48258] [INFO] Starting worker [48258]
After that visit http://127.0.0.1:8000/ in your browser or use CLI command (``curl``, ``httpie``,
etc). You should see something like:
.. code-block:: json
{
"query": "Dependency Injector",
"limit": 10,
"gifs": [
{
"url": "https://giphy.com/gifs/boxes-dependent-swbf2-6Eo7KzABxgJMY"
},
{
"url": "https://giphy.com/gifs/depends-J56qCcOhk6hKE"
},
{
"url": "https://giphy.com/gifs/web-series-ccstudios-bro-dependent-1lhU8KAVwmVVu"
},
{
"url": "https://giphy.com/gifs/TheBoysTV-friends-friend-weneedeachother-XxR9qcIwcf5Jq404Sx"
},
{
"url": "https://giphy.com/gifs/netflix-a-series-of-unfortunate-events-asoue-9rgeQXbwoK53pcxn7f"
},
{
"url": "https://giphy.com/gifs/black-and-white-sad-skins-Hs4YzLs2zJuLu"
},
{
"url": "https://giphy.com/gifs/always-there-for-you-i-am-here-PlayjhCco9jHBYrd9w"
},
{
"url": "https://giphy.com/gifs/stream-famous-dollar-YT2dvOByEwXCdoYiA1"
},
{
"url": "https://giphy.com/gifs/i-love-you-there-for-am-1BhGzgpZXYWwWMAGB1"
},
{
"url": "https://giphy.com/gifs/life-like-twerk-9hlnWxjHqmH28"
}
]
}
.. note::
To create your own Giphy API key follow this
`guide <https://support.giphy.com/hc/en-us/articles/360020283431-Request-A-GIPHY-API-Key>`_.
Test
----
This application comes with the unit tests.
To run the tests do:
.. code-block:: bash
py.test giphynavigator/tests.py --cov=giphynavigator
The output should be something like:
.. code-block::
platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
plugins: cov-2.10.0, sanic-1.6.1
collected 3 items
giphynavigator/tests.py ... [100%]
---------- coverage: platform darwin, python 3.8.3-final-0 -----------
Name Stmts Miss Cover
---------------------------------------------------
giphynavigator/__init__.py 0 0 100%
giphynavigator/__main__.py 4 4 0%
giphynavigator/application.py 12 0 100%
giphynavigator/containers.py 6 0 100%
giphynavigator/giphy.py 14 9 36%
giphynavigator/handlers.py 10 0 100%
giphynavigator/services.py 9 1 89%
giphynavigator/tests.py 34 0 100%
---------------------------------------------------
TOTAL 89 14 84%

View File

@ -0,0 +1,5 @@
giphy:
request_timeout: 10
default:
query: "Dependency Injector"
limit: 10

View File

@ -0,0 +1 @@
"""Top-level package."""

View File

@ -0,0 +1,8 @@
"""Main module."""
from .application import create_app
if __name__ == '__main__':
app = create_app()
app.run(host='0.0.0.0', port=8000, debug=True)

View File

@ -0,0 +1,22 @@
"""Application module."""
from sanic import Sanic
from .containers import Container
from . import handlers
def create_app():
"""Create and return aiohttp application."""
container = Container()
container.config.from_yaml('config.yml')
container.config.giphy.api_key.from_env('GIPHY_API_KEY')
container.wire(modules=[handlers])
app = Sanic('Giphy Navigator')
app.container = container
app.add_route(handlers.index, '/')
return app

View File

@ -0,0 +1,21 @@
"""Containers module."""
from dependency_injector import containers, providers
from . import giphy, services
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
giphy_client = providers.Factory(
giphy.GiphyClient,
api_key=config.giphy.api_key,
timeout=config.giphy.request_timeout,
)
search_service = providers.Factory(
services.SearchService,
giphy_client=giphy_client,
)

View File

@ -0,0 +1,26 @@
"""Giphy client module."""
from aiohttp import ClientSession, ClientTimeout
class GiphyClient:
API_URL = 'http://api.giphy.com/v1'
def __init__(self, api_key, timeout):
self._api_key = api_key
self._timeout = ClientTimeout(timeout)
async def search(self, query, limit):
"""Make search API call and return result."""
url = f'{self.API_URL}/gifs/search'
params = {
'q': query,
'api_key': self._api_key,
'limit': limit,
}
async with ClientSession(timeout=self._timeout) as session:
async with session.get(url, params=params) as response:
if response.status != 200:
response.raise_for_status()
return await response.json()

View File

@ -0,0 +1,28 @@
"""Handlers module."""
from sanic.request import Request
from sanic.response import HTTPResponse, json
from dependency_injector.wiring import Provide
from .services import SearchService
from .containers import Container
async def index(
request: Request,
search_service: SearchService = Provide[Container.search_service],
default_query: str = Provide[Container.config.default.query],
default_limit: int = Provide[Container.config.default.limit.as_int()],
) -> HTTPResponse:
query = request.args.get('query', default_query)
limit = int(request.args.get('limit', default_limit))
gifs = await search_service.search(query, limit)
return json(
{
'query': query,
'limit': limit,
'gifs': gifs,
},
)

View File

@ -0,0 +1,18 @@
"""Services module."""
from .giphy import GiphyClient
class SearchService:
def __init__(self, giphy_client: GiphyClient):
self._giphy_client = giphy_client
async def search(self, query, limit):
"""Search for gifs and return formatted data."""
if not query:
return []
result = await self._giphy_client.search(query, limit)
return [{'url': gif['url']} for gif in result['data']]

View File

@ -0,0 +1,74 @@
"""Tests module."""
from unittest import mock
import pytest
from giphynavigator.application import create_app
from giphynavigator.giphy import GiphyClient
@pytest.fixture
def app():
app = create_app()
yield app
app.container.unwire()
async def test_index(app):
giphy_client_mock = mock.AsyncMock(spec=GiphyClient)
giphy_client_mock.search.return_value = {
'data': [
{'url': 'https://giphy.com/gif1.gif'},
{'url': 'https://giphy.com/gif2.gif'},
],
}
with app.container.giphy_client.override(giphy_client_mock):
_, response = await app.asgi_client.get(
'/',
params={
'query': 'test',
'limit': 10,
},
)
assert response.status == 200
data = response.json()
assert data == {
'query': 'test',
'limit': 10,
'gifs': [
{'url': 'https://giphy.com/gif1.gif'},
{'url': 'https://giphy.com/gif2.gif'},
],
}
async def test_index_no_data(app):
giphy_client_mock = mock.AsyncMock(spec=GiphyClient)
giphy_client_mock.search.return_value = {
'data': [],
}
with app.container.giphy_client.override(giphy_client_mock):
_, response = await app.asgi_client.get('/')
assert response.status == 200
data = response.json()
assert data['gifs'] == []
async def test_index_default_params(app):
giphy_client_mock = mock.AsyncMock(spec=GiphyClient)
giphy_client_mock.search.return_value = {
'data': [],
}
with app.container.giphy_client.override(giphy_client_mock):
_, response = await app.asgi_client.get('/')
assert response.status == 200
data = response.json()
assert data['query'] == app.container.config.default.query()
assert data['limit'] == app.container.config.default.limit()

View File

@ -0,0 +1,5 @@
dependency-injector
sanic
pyyaml
pytest-sanic
pytest-cov

View File

@ -2,6 +2,8 @@
max_line_length = 100
max_complexity = 10
exclude = types.py
per-file-ignores =
examples/demo/*: F841
[pydocstyle]
ignore = D100,D101,D102,D105,D106,D107,D203,D213

View File

@ -1,6 +1,6 @@
"""Top-level package."""
__version__ = '3.44.0'
__version__ = '4.0.0a2'
"""Version number.
:type: str

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,5 @@
from typing import Type, Dict, Tuple, Optional, Any, Union, ClassVar, Callable as _Callable
from types import ModuleType
from typing import Type, Dict, Tuple, Optional, Any, Union, ClassVar, Callable as _Callable, Iterable
from .providers import Provider
@ -16,6 +17,9 @@ class Container:
def override_providers(self, **overriding_providers: Provider) -> None: ...
def reset_last_overriding(self) -> None: ...
def reset_override(self) -> None: ...
def resolve_provider_name(self, provider_to_resolve: Provider) -> Optional[str]: ...
def wire(self, modules: Optional[Iterable[ModuleType]] = None, packages: Optional[Iterable[ModuleType]] = None) -> None: ...
def unwire(self) -> None: ...
class DynamicContainer(Container): ...

View File

@ -1,15 +1,26 @@
"""Containers module."""
import sys
import six
from .errors import Error
from .providers cimport (
Provider,
deepcopy,
)
if sys.version_info[:2] >= (3, 6):
from .wiring import wire, unwire
else:
def wire(*args, **kwargs):
raise NotImplementedError('Wiring requires Python 3.6 or above')
def unwire(*args, **kwargs):
raise NotImplementedError('Wiring requires Python 3.6 or above')
class DynamicContainer(object):
"""Dynamic inversion of control container.
@ -47,8 +58,11 @@ class DynamicContainer(object):
:rtype: None
"""
self.provider_type = Provider
self.providers = dict()
self.providers = {}
self.overridden = tuple()
self.declarative_parent = None
self.wired_to_modules = []
self.wired_to_packages = []
super(DynamicContainer, self).__init__()
def __deepcopy__(self, memo):
@ -60,6 +74,7 @@ class DynamicContainer(object):
copied = self.__class__()
copied.provider_type = Provider
copied.overridden = deepcopy(self.overridden, memo)
copied.declarative_parent = self.declarative_parent
for name, provider in deepcopy(self.providers, memo).items():
setattr(copied, name, provider)
@ -171,6 +186,34 @@ class DynamicContainer(object):
for provider in six.itervalues(self.providers):
provider.reset_override()
def wire(self, modules=None, packages=None):
"""Wire container providers with provided packages and modules.
:rtype: None
"""
wire(
container=self,
modules=modules,
packages=packages,
)
if modules:
self.wired_to_modules.extend(modules)
if packages:
self.wired_to_packages.extend(packages)
def unwire(self):
"""Unwire container providers from previously wired packages and modules."""
unwire(
modules=self.wired_to_modules,
packages=self.wired_to_packages,
)
self.wired_to_modules.clear()
self.wired_to_packages.clear()
class DeclarativeContainerMetaClass(type):
"""Declarative inversion of control container meta class."""
@ -310,6 +353,7 @@ class DeclarativeContainer(object):
"""
container = cls.instance_type()
container.provider_type = cls.provider_type
container.declarative_parent = cls
container.set_providers(**deepcopy(cls.providers))
container.override_providers(**overriding_providers)
return container
@ -363,6 +407,15 @@ class DeclarativeContainer(object):
for provider in six.itervalues(cls.providers):
provider.reset_override()
@classmethod
def resolve_provider_name(cls, provider_to_resolve):
"""Try to resolve provider name by its instance."""
for provider_name, container_provider in cls.providers.items():
if container_provider is provider_to_resolve:
return provider_name
else:
return None
def override(object container):
""":py:class:`DeclarativeContainer` overriding decorator.

View File

@ -3,10 +3,18 @@
from __future__ import absolute_import
import functools
import warnings
from dependency_injector import providers
warnings.warn(
'Module "dependency_injector.ext.aiohttp" is deprecated since '
'version 4.0.0. Use "dependency_injector.wiring" module instead.',
category=DeprecationWarning,
)
class Application(providers.Singleton):
"""Aiohttp application provider."""

View File

@ -1,12 +1,20 @@
"""Flask extension module."""
from __future__ import absolute_import
import warnings
from flask import request as flask_request
from dependency_injector import providers, errors
warnings.warn(
'Module "dependency_injector.ext.aiohttp" is deprecated since '
'version 4.0.0. Use "dependency_injector.wiring" module instead.',
category=DeprecationWarning,
)
request = providers.Object(flask_request)

File diff suppressed because it is too large Load Diff

View File

@ -93,6 +93,10 @@ cdef class ConfigurationOption(Provider):
cdef object __cache
cdef class TypedConfigurationOption(Callable):
pass
cdef class Configuration(Object):
cdef str __name
cdef dict __children
@ -181,9 +185,9 @@ cdef class List(Provider):
cdef class Container(Provider):
cdef object container_cls
cdef dict overriding_providers
cdef object container
cdef object __container_cls
cdef dict __overriding_providers
cdef object __container
cpdef object _provide(self, tuple args, dict kwargs)

View File

@ -54,6 +54,8 @@ class Object(Provider, Generic[T]):
class Delegate(Provider):
def __init__(self, provides: Provider) -> None: ...
def __call__(self, *args: Injection, **kwargs: Injection) -> Provider: ...
@property
def provides(self) -> Provider: ...
class Dependency(Provider, Generic[T]):
@ -122,14 +124,17 @@ class CoroutineDelegate(Delegate):
class ConfigurationOption(Provider):
UNDEFINED: object
def __init__(self, name: str, root: Configuration) -> None: ...
def __init__(self, name: Tuple[str], root: Configuration) -> None: ...
def __call__(self, *args: Injection, **kwargs: Injection) -> Any: ...
def __getattr__(self, item: str) -> ConfigurationOption: ...
def __getitem__(self, item: str) -> ConfigurationOption: ...
def __getitem__(self, item: Union[str, Provider]) -> ConfigurationOption: ...
@property
def root(self) -> Configuration: ...
def get_name(self) -> str: ...
def as_int(self) -> Callable[int]: ...
def as_float(self) -> Callable[float]: ...
def as_(self, callback: _Callable[..., T], *args: Injection, **kwargs: Injection) -> Callable[T]: ...
def get_name_segments(self) -> Tuple[Union[str, Provider]]: ...
def as_int(self) -> TypedConfigurationOption[int]: ...
def as_float(self) -> TypedConfigurationOption[float]: ...
def as_(self, callback: _Callable[..., T], *args: Injection, **kwargs: Injection) -> TypedConfigurationOption[T]: ...
def update(self, value: Any) -> None: ...
def from_ini(self, filepath: str) -> None: ...
def from_yaml(self, filepath: str) -> None: ...
@ -137,11 +142,16 @@ class ConfigurationOption(Provider):
def from_env(self, name: str, default: Optional[Any] = None) -> None: ...
class TypedConfigurationOption(Callable[T]):
@property
def option(self) -> ConfigurationOption: ...
class Configuration(Object):
DEFAULT_NAME: str = 'config'
def __init__(self, name: str = DEFAULT_NAME, default: Optional[Any] = None) -> None: ...
def __getattr__(self, item: str) -> ConfigurationOption: ...
def __getitem__(self, item: str) -> ConfigurationOption: ...
def __getitem__(self, item: Union[str, Provider]) -> ConfigurationOption: ...
def get_name(self) -> str: ...
def get(self, selector: str) -> Any: ...
def set(self, selector: str, value: Any) -> OverridingContext: ...
@ -268,6 +278,8 @@ class Container(Provider):
def __init__(self, container_cls: Type[T], container: Optional[T] = None, **overriding_providers: Provider) -> None: ...
def __call__(self, *args: Injection, **kwargs: Injection) -> T: ...
def __getattr__(self, name: str) -> Provider: ...
@property
def container(self) -> T: ...
class Selector(Provider):

View File

@ -8,6 +8,7 @@ import re
import sys
import types
import threading
import warnings
import weakref
try:
@ -257,6 +258,11 @@ cdef class Provider(object):
:rtype: :py:class:`Delegate`
"""
warnings.warn(
'Method ".delegate()" is deprecated since version 4.0.0. '
'Use ".provider" attribute instead.',
category=DeprecationWarning,
)
return Delegate(self)
@property
@ -391,6 +397,11 @@ cdef class Delegate(Provider):
"""
return self.__str__()
@property
def provides(self):
"""Return provider."""
return self.__provides
cpdef object _provide(self, tuple args, dict kwargs):
"""Return provided instance.
@ -1139,18 +1150,25 @@ cdef class ConfigurationOption(Provider):
segment() if is_provider(segment) else segment for segment in self.__name
)
@property
def root(self):
return self.__root_ref()
def get_name(self):
root = self.__root_ref()
return '.'.join((root.get_name(), self._get_self_name()))
def get_name_segments(self):
return self.__name
def as_int(self):
return Callable(int, self)
return TypedConfigurationOption(int, self)
def as_float(self):
return Callable(float, self)
return TypedConfigurationOption(float, self)
def as_(self, callback, *args, **kwargs):
return Callable(callback, self, *args, **kwargs)
return TypedConfigurationOption(callback, self, *args, **kwargs)
def override(self, value):
if isinstance(value, Provider):
@ -1262,6 +1280,13 @@ cdef class ConfigurationOption(Provider):
self.override(value)
cdef class TypedConfigurationOption(Callable):
@property
def option(self):
return self.args[0]
cdef class Configuration(Object):
"""Configuration provider provides configuration options to the other providers.
@ -2458,13 +2483,13 @@ cdef class Container(Provider):
def __init__(self, container_cls, container=None, **overriding_providers):
"""Initialize provider."""
self.container_cls = container_cls
self.overriding_providers = overriding_providers
self.__container_cls = container_cls
self.__overriding_providers = overriding_providers
if container is None:
container = container_cls()
container.override_providers(**overriding_providers)
self.container = container
self.__container = container
super(Container, self).__init__()
@ -2475,9 +2500,9 @@ cdef class Container(Provider):
return copied
copied = self.__class__(
self.container_cls,
deepcopy(self.container, memo),
**deepcopy(self.overriding_providers, memo),
self.__container_cls,
deepcopy(self.__container, memo),
**deepcopy(self.__overriding_providers, memo),
)
return copied
@ -2489,7 +2514,11 @@ cdef class Container(Provider):
'\'{cls}\' object has no attribute '
'\'{attribute_name}\''.format(cls=self.__class__.__name__,
attribute_name=name))
return getattr(self.container, name)
return getattr(self.__container, name)
@property
def container(self):
return self.__container
def override(self, provider):
"""Override provider with another provider."""
@ -2497,7 +2526,7 @@ cdef class Container(Provider):
cpdef object _provide(self, tuple args, dict kwargs):
"""Return single instance."""
return self.container
return self.__container
cdef class Selector(Provider):
@ -2657,6 +2686,11 @@ cdef class ProvidedInstance(Provider):
def __getitem__(self, item):
return ItemGetter(self, item)
@property
def provides(self):
"""Return provider."""
return self.__provider
def call(self, *args, **kwargs):
return MethodCaller(self, *args, **kwargs)
@ -2696,6 +2730,16 @@ cdef class AttributeGetter(Provider):
def __getitem__(self, item):
return ItemGetter(self, item)
@property
def provides(self):
"""Return provider."""
return self.__provider
@property
def name(self):
"""Return name of the attribute."""
return self.__attribute
def call(self, *args, **kwargs):
return MethodCaller(self, *args, **kwargs)
@ -2736,6 +2780,16 @@ cdef class ItemGetter(Provider):
def __getitem__(self, item):
return ItemGetter(self, item)
@property
def provides(self):
"""Return provider."""
return self.__provider
@property
def name(self):
"""Return name of the item."""
return self.__item
def call(self, *args, **kwargs):
return MethodCaller(self, *args, **kwargs)
@ -2787,6 +2841,37 @@ cdef class MethodCaller(Provider):
def __getitem__(self, item):
return ItemGetter(self, item)
@property
def provides(self):
"""Return provider."""
return self.__provider
@property
def args(self):
"""Return positional argument injections."""
cdef int index
cdef PositionalInjection arg
cdef list args
args = list()
for index in range(self.__args_len):
arg = self.__args[index]
args.append(arg.__value)
return tuple(args)
@property
def kwargs(self):
"""Return keyword argument injections."""
cdef int index
cdef NamedInjection kwarg
cdef dict kwargs
kwargs = dict()
for index in range(self.__kwargs_len):
kwarg = self.__kwargs[index]
kwargs[kwarg.__name] = kwarg.__value
return kwargs
def call(self, *args, **kwargs):
return MethodCaller(self, *args, **kwargs)

View File

@ -1,17 +0,0 @@
from typing import TypeVar, Generic, Any
import warnings
warnings.warn(
'Types module is deprecated since version 3.44.0. Use "providers" module instead: '
'providers.Provider[SomeClass]',
category=DeprecationWarning,
)
Injection = Any
T = TypeVar('T')
class Provider(Generic[T]):
def __call__(self, *args: Injection, **kwargs: Injection) -> T: ...

View File

@ -0,0 +1,321 @@
"""Wiring module."""
import functools
import inspect
import pkgutil
import sys
from types import ModuleType
from typing import Optional, Iterable, Callable, Any, Type, Dict, Generic, TypeVar, cast
if sys.version_info < (3, 7):
from typing import GenericMeta
else:
class GenericMeta(type):
...
from . import providers
__all__ = (
'wire',
'unwire',
'Provide',
'Provider',
)
T = TypeVar('T')
Container = Any
class ProvidersMap:
def __init__(self, container):
self._container = container
self._map = self._create_providers_map(
current_providers=container.providers,
original_providers=container.declarative_parent.providers,
)
def resolve_provider(self, provider: providers.Provider) -> providers.Provider:
if isinstance(provider, providers.Delegate):
return self._resolve_delegate(provider)
elif isinstance(provider, (
providers.ProvidedInstance,
providers.AttributeGetter,
providers.ItemGetter,
providers.MethodCaller,
)):
return self._resolve_provided_instance(provider)
elif isinstance(provider, providers.ConfigurationOption):
return self._resolve_config_option(provider)
elif isinstance(provider, providers.TypedConfigurationOption):
return self._resolve_config_option(provider.option, as_=provider.provides)
else:
return self._resolve_provider(provider)
def _resolve_delegate(self, original: providers.Delegate) -> providers.Provider:
return self._resolve_provider(original.provides)
def _resolve_provided_instance(self, original: providers.Provider) -> providers.Provider:
modifiers = []
while isinstance(original, (
providers.ProvidedInstance,
providers.AttributeGetter,
providers.ItemGetter,
providers.MethodCaller,
)):
modifiers.insert(0, original)
original = original.provides
new = self._resolve_provider(original)
for modifier in modifiers:
if isinstance(modifier, providers.ProvidedInstance):
new = new.provided
elif isinstance(modifier, providers.AttributeGetter):
new = getattr(new, modifier.name)
elif isinstance(modifier, providers.ItemGetter):
new = new[modifier.name]
elif isinstance(modifier, providers.MethodCaller):
new = new.call(
*modifier.args,
**modifier.kwargs,
)
return new
def _resolve_config_option(
self,
original: providers.ConfigurationOption,
as_: Any = None,
) -> providers.Provider:
original_root = original.root
new = self._resolve_provider(original_root)
new = cast(providers.Configuration, new)
for segment in original.get_name_segments():
if providers.is_provider(segment):
segment = self.resolve_provider(segment)
new = new[segment]
else:
new = getattr(new, segment)
if as_:
new = new.as_(as_)
return new
def _resolve_provider(self, original: providers.Provider) -> providers.Provider:
try:
return self._map[original]
except KeyError:
raise Exception('Unable to resolve original provider')
@classmethod
def _create_providers_map(
cls,
current_providers: Dict[str, providers.Provider],
original_providers: Dict[str, providers.Provider],
) -> Dict[providers.Provider, providers.Provider]:
providers_map = {}
for provider_name, current_provider in current_providers.items():
original_provider = original_providers[provider_name]
providers_map[original_provider] = current_provider
if isinstance(current_provider, providers.Container) \
and isinstance(original_provider, providers.Container):
subcontainer_map = cls._create_providers_map(
current_providers=current_provider.container.providers,
original_providers=original_provider.container.providers,
)
providers_map.update(subcontainer_map)
return providers_map
def wire(
container: Container,
*,
modules: Optional[Iterable[ModuleType]] = None,
packages: Optional[Iterable[ModuleType]] = None,
) -> None:
"""Wire container providers with provided packages and modules."""
if not _is_declarative_container_instance(container):
raise Exception('Can wire only an instance of the declarative container')
if not modules:
modules = []
if packages:
for package in packages:
modules.extend(_fetch_modules(package))
providers_map = ProvidersMap(container)
for module in modules:
for name, member in inspect.getmembers(module):
if inspect.isfunction(member):
_patch_fn(module, name, member, providers_map)
elif inspect.isclass(member):
_patch_cls(member, providers_map)
def unwire(
*,
modules: Optional[Iterable[ModuleType]] = None,
packages: Optional[Iterable[ModuleType]] = None,
) -> None:
"""Wire provided packages and modules with previous wired providers."""
if not modules:
modules = []
if packages:
for package in packages:
modules.extend(_fetch_modules(package))
for module in modules:
for name, member in inspect.getmembers(module):
if inspect.isfunction(member):
_unpatch_fn(module, name, member)
elif inspect.isclass(member):
_unpatch_cls(member,)
def _patch_cls(
cls: Type[Any],
providers_map: ProvidersMap,
) -> None:
if not hasattr(cls, '__init__'):
return
init_method = getattr(cls, '__init__')
injections = _resolve_injections(init_method, providers_map)
if not injections:
return
setattr(cls, '__init__', _patch_with_injections(init_method, injections))
def _unpatch_cls(cls: Type[Any]) -> None:
if not hasattr(cls, '__init__'):
return
init_method = getattr(cls, '__init__')
if not _is_patched(init_method):
return
setattr(cls, '__init__', _get_original_from_patched(init_method))
def _patch_fn(
module: ModuleType,
name: str,
fn: Callable[..., Any],
providers_map: ProvidersMap,
) -> None:
injections = _resolve_injections(fn, providers_map)
if not injections:
return
setattr(module, name, _patch_with_injections(fn, injections))
def _unpatch_fn(
module: ModuleType,
name: str,
fn: Callable[..., Any],
) -> None:
if not _is_patched(fn):
return
setattr(module, name, _get_original_from_patched(fn))
def _resolve_injections(fn: Callable[..., Any], providers_map: ProvidersMap) -> Dict[str, Any]: # noqa
signature = inspect.signature(fn)
injections = {}
for parameter_name, parameter in signature.parameters.items():
if not isinstance(parameter.default, _Marker):
continue
marker = parameter.default
provider = providers_map.resolve_provider(marker.provider)
if isinstance(marker, Provide):
injections[parameter_name] = provider
elif isinstance(marker, Provider):
injections[parameter_name] = provider.provider
return injections
def _fetch_modules(package):
modules = []
for loader, module_name, is_pkg in pkgutil.walk_packages(
path=package.__path__,
prefix=package.__name__ + '.',
):
module = loader.find_module(module_name).load_module(module_name)
modules.append(module)
return modules
def _patch_with_injections(fn, injections):
if inspect.iscoroutinefunction(fn):
@functools.wraps(fn)
async def _patched(*args, **kwargs):
to_inject = {}
for injection, provider in injections.items():
to_inject[injection] = provider()
to_inject.update(kwargs)
return await fn(*args, **to_inject)
else:
@functools.wraps(fn)
def _patched(*args, **kwargs):
to_inject = {}
for injection, provider in injections.items():
to_inject[injection] = provider()
to_inject.update(kwargs)
return fn(*args, **to_inject)
_patched.__wired__ = True
_patched.__original__ = fn
_patched.__injections__ = injections
return _patched
def _is_patched(fn):
return getattr(fn, '__wired__', False) is True
def _get_original_from_patched(fn):
return getattr(fn, '__original__')
def _is_declarative_container_instance(instance: Any) -> bool:
return (not isinstance(instance, type)
and getattr(instance, '__IS_CONTAINER__', False) is True
and getattr(instance, 'declarative_parent', None) is not None)
class ClassGetItemMeta(GenericMeta):
def __getitem__(cls, item):
# Spike for Python 3.6
return cls(item)
class _Marker(Generic[T], metaclass=ClassGetItemMeta):
def __init__(self, provider: providers.Provider) -> None:
self.provider = provider
def __class_getitem__(cls, item) -> T:
return cls(item)
class Provide(_Marker):
...
class Provider(_Marker):
...

View File

@ -0,0 +1 @@
"""Wiring tests."""

View File

@ -0,0 +1,17 @@
from dependency_injector import containers, providers
from .service import Service
class SubContainer(containers.DeclarativeContainer):
int_object = providers.Object(1)
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
service = providers.Factory(Service)
sub = providers.Container(SubContainer)

View File

@ -0,0 +1,49 @@
"""Test module for wiring."""
from decimal import Decimal
from typing import Callable
from dependency_injector.wiring import Provide, Provider
from .container import Container
from .service import Service
class TestClass:
def __init__(self, service: Service = Provide[Container.service]):
self.service = service
def test_function(service: Service = Provide[Container.service]):
return service
def test_function_provider(service_provider: Callable[..., Service] = Provider[Container.service]):
service = service_provider()
return service
def test_config_value(
some_value_int: int = Provide[Container.config.a.b.c.as_int()],
some_value_str: str = Provide[Container.config.a.b.c.as_(str)],
some_value_decimal: Decimal = Provide[Container.config.a.b.c.as_(Decimal)],
):
return some_value_int, some_value_str, some_value_decimal
def test_provide_provider(service_provider: Callable[..., Service] = Provider[Container.service.provider]):
service = service_provider()
return service
def test_provided_instance(some_value: int = Provide[Container.service.provided.foo['bar'].call()]):
return some_value
def test_subcontainer_provider(some_value: int = Provide[Container.sub.int_object]):
return some_value
def test_config_invariant(some_value: int = Provide[Container.config.option[Container.config.switch]]):
return some_value

View File

View File

@ -0,0 +1,8 @@
from dependency_injector.wiring import Provide
from ...container import Container
from ...service import Service
def test_function(service: Service = Provide[Container.service]):
return service

View File

@ -0,0 +1,2 @@
class Service:
service_attr: int

View File

@ -0,0 +1,106 @@
from decimal import Decimal
import unittest
from dependency_injector.wiring import wire
from . import module, package
from .service import Service
from .container import Container
class WiringTest(unittest.TestCase):
container: Container
def setUp(self) -> None:
self.container = Container(config={'a': {'b': {'c': 10}}})
self.container.wire(
modules=[module],
packages=[package],
)
self.addCleanup(self.container.unwire)
def test_package_lookup(self):
from .package.subpackage.submodule import test_function
service = test_function()
self.assertIsInstance(service, Service)
def test_class_wiring(self):
test_class_object = module.TestClass()
self.assertIsInstance(test_class_object.service, Service)
def test_class_wiring_context_arg(self):
test_service = self.container.service()
test_class_object = module.TestClass(service=test_service)
self.assertIs(test_class_object.service, test_service)
def test_function_wiring(self):
service = module.test_function()
self.assertIsInstance(service, Service)
def test_function_wiring_context_arg(self):
test_service = self.container.service()
service = module.test_function(service=test_service)
self.assertIs(service, test_service)
def test_function_wiring_provider(self):
service = module.test_function_provider()
self.assertIsInstance(service, Service)
def test_function_wiring_provider_context_arg(self):
test_service = self.container.service()
service = module.test_function_provider(service_provider=lambda: test_service)
self.assertIs(service, test_service)
def test_configuration_option(self):
int_value, str_value, decimal_value = module.test_config_value()
self.assertEqual(int_value, 10)
self.assertEqual(str_value, '10')
self.assertEqual(decimal_value, Decimal(10))
def test_provide_provider(self):
service = module.test_provide_provider()
self.assertIsInstance(service, Service)
def test_provided_instance(self):
class TestService:
foo = {
'bar': lambda: 10,
}
with self.container.service.override(TestService()):
some_value = module.test_provided_instance()
self.assertEqual(some_value, 10)
def test_subcontainer(self):
some_value = module.test_subcontainer_provider()
self.assertEqual(some_value, 1)
def test_config_invariant(self):
config = {
'option': {
'a': 1,
'b': 2,
},
'switch': 'a',
}
self.container.config.from_dict(config)
with self.container.config.switch.override('a'):
value_a = module.test_config_invariant()
self.assertEqual(value_a, 1)
with self.container.config.switch.override('b'):
value_b = module.test_config_invariant()
self.assertEqual(value_b, 2)
def test_wire_with_class_error(self):
with self.assertRaises(Exception):
wire(
container=Container,
modules=[module],
)