Merge branch 'release/4.45.0' into master

This commit is contained in:
Roman Mogylatov 2025-01-05 15:20:12 -05:00
commit 46646b1acf
46 changed files with 513 additions and 143 deletions

View File

@ -7,6 +7,21 @@ that were made in every particular version.
From version 0.7.6 *Dependency Injector* framework strictly From version 0.7.6 *Dependency Injector* framework strictly
follows `Semantic versioning`_ follows `Semantic versioning`_
4.45.0
--------
- Add Starlette lifespan handler implementation (`#683 <https://github.com/ets-labs/python-dependency-injector/pull/683>`_).
- Raise exception in ``ThreadLocalSingleton`` instead of hiding it in finally (`#845 <https://github.com/ets-labs/python-dependency-injector/pull/845>`_).
- Improve debuggability of ``deepcopy`` errors (`#839 <https://github.com/ets-labs/python-dependency-injector/pull/839>`_).
- Update examples (`#838 <https://github.com/ets-labs/python-dependency-injector/pull/838>`_).
- Upgrade testing dependencies (`#837 <https://github.com/ets-labs/python-dependency-injector/pull/837>`_).
- Add minor fixes to the documentation (`#709 <https://github.com/ets-labs/python-dependency-injector/pull/709>`_).
- Remove ``six`` from the dependencies (`3ba4704 <https://github.com/ets-labs/python-dependency-injector/commit/3ba4704bc1cb00310749fd2eda0c8221167c313c>`_).
Many thanks for the contributions to:
- `ZipFile <https://github.com/ZipFile>`_
- `František Trebuňa <https://github.com/gortibaldik>`_
- `JC (Jonathan Chen) <https://github.com/dijonkitchen>`_
4.44.0 4.44.0
-------- --------
- Implement support for Pydantic 2. PR: `#832 <https://github.com/ets-labs/python-dependency-injector/pull/832>`_. - Implement support for Pydantic 2. PR: `#832 <https://github.com/ets-labs/python-dependency-injector/pull/832>`_.

View File

@ -24,7 +24,7 @@ returns it on the rest of the calls.
.. note:: .. note::
``Singleton`` provider makes dependencies injection only when creates an object. When an object ``Singleton`` provider makes dependencies injection only when it creates an object. When an object
is created and memorized ``Singleton`` provider just returns it without applying injections. is created and memorized ``Singleton`` provider just returns it without applying injections.
Specialization of the provided type and abstract singletons work the same like like for the Specialization of the provided type and abstract singletons work the same like like for the

View File

@ -18,7 +18,7 @@ In this tutorial we will use:
- Python 3 - Python 3
- Docker - Docker
- Docker-compose - Docker Compose
Start from the scratch or jump to the section: Start from the scratch or jump to the section:
@ -47,28 +47,27 @@ response it will log:
Prerequisites Prerequisites
------------- -------------
We will use `Docker <https://www.docker.com/>`_ and We will use `docker compose <https://docs.docker.com/compose/>`_ in this tutorial. Let's check the versions:
`docker-compose <https://docs.docker.com/compose/>`_ in this tutorial. Let's check the versions:
.. code-block:: bash .. code-block:: bash
docker --version docker --version
docker-compose --version docker compose version
The output should look something like: The output should look something like:
.. code-block:: bash .. code-block:: bash
Docker version 20.10.5, build 55c4c88 Docker version 27.3.1, build ce12230
docker-compose version 1.29.0, build 07737305 Docker Compose version v2.29.7
.. note:: .. note::
If you don't have ``Docker`` or ``docker-compose`` you need to install them before proceeding. If you don't have ``Docker`` or ``docker compose`` you need to install them before proceeding.
Follow these installation guides: Follow these installation guides:
- `Install Docker <https://docs.docker.com/get-docker/>`_ - `Install Docker <https://docs.docker.com/get-docker/>`_
- `Install docker-compose <https://docs.docker.com/compose/install/>`_ - `Install docker compose <https://docs.docker.com/compose/install/>`_
The prerequisites are satisfied. Let's get started with the project layout. The prerequisites are satisfied. Let's get started with the project layout.
@ -129,13 +128,13 @@ Put next lines into the ``requirements.txt`` file:
pytest-cov pytest-cov
Second, we need to create the ``Dockerfile``. It will describe the daemon's build process and Second, we need to create the ``Dockerfile``. It will describe the daemon's build process and
specify how to run it. We will use ``python:3.9-buster`` as a base image. specify how to run it. We will use ``python:3.13-bookworm`` as a base image.
Put next lines into the ``Dockerfile`` file: Put next lines into the ``Dockerfile`` file:
.. code-block:: bash .. code-block:: bash
FROM python:3.10-buster FROM python:3.13-bookworm
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
@ -155,8 +154,6 @@ Put next lines into the ``docker-compose.yml`` file:
.. code-block:: yaml .. code-block:: yaml
version: "3.7"
services: services:
monitor: monitor:
@ -171,7 +168,7 @@ Run in the terminal:
.. code-block:: bash .. code-block:: bash
docker-compose build docker compose build
The build process may take a couple of minutes. You should see something like this in the end: The build process may take a couple of minutes. You should see something like this in the end:
@ -184,7 +181,7 @@ After the build is done run the container:
.. code-block:: bash .. code-block:: bash
docker-compose up docker compose up
The output should look like: The output should look like:
@ -461,7 +458,7 @@ Run in the terminal:
.. code-block:: bash .. code-block:: bash
docker-compose up docker compose up
The output should look like: The output should look like:
@ -705,7 +702,7 @@ Run in the terminal:
.. code-block:: bash .. code-block:: bash
docker-compose up docker compose up
You should see: You should see:
@ -813,7 +810,7 @@ Run in the terminal:
.. code-block:: bash .. code-block:: bash
docker-compose up docker compose up
You should see: You should see:
@ -965,15 +962,16 @@ Run in the terminal:
.. code-block:: bash .. code-block:: bash
docker-compose run --rm monitor py.test monitoringdaemon/tests.py --cov=monitoringdaemon docker compose run --rm monitor py.test monitoringdaemon/tests.py --cov=monitoringdaemon
You should see: You should see:
.. code-block:: bash .. code-block:: bash
platform linux -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 platform linux -- Python 3.13.1, pytest-8.3.4, pluggy-1.5.0
rootdir: /code rootdir: /code
plugins: asyncio-0.16.0, cov-3.0.0 plugins: cov-6.0.0, asyncio-0.24.0
asyncio: mode=Mode.STRICT, default_loop_scope=None
collected 2 items collected 2 items
monitoringdaemon/tests.py .. [100%] monitoringdaemon/tests.py .. [100%]

View File

@ -98,8 +98,9 @@ The output should be something like:
.. code-block:: .. code-block::
platform darwin -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0
plugins: asyncio-0.16.0, anyio-3.3.4, aiohttp-0.3.0, cov-3.0.0 plugins: cov-6.0.0, anyio-4.4.0, asyncio-0.24.0, aiohttp-1.0.5
asyncio: mode=Mode.STRICT, default_loop_scope=None
collected 3 items collected 3 items
giphynavigator/tests.py ... [100%] giphynavigator/tests.py ... [100%]

View File

@ -3,11 +3,15 @@
from unittest import mock from unittest import mock
import pytest import pytest
import pytest_asyncio
from giphynavigator.application import create_app from giphynavigator.application import create_app
from giphynavigator.giphy import GiphyClient from giphynavigator.giphy import GiphyClient
pytestmark = pytest.mark.asyncio
@pytest.fixture @pytest.fixture
def app(): def app():
app = create_app() app = create_app()
@ -15,9 +19,9 @@ def app():
app.container.unwire() app.container.unwire()
@pytest.fixture @pytest_asyncio.fixture
def client(app, aiohttp_client, loop): async def client(app, aiohttp_client):
return loop.run_until_complete(aiohttp_client(app)) return await aiohttp_client(app)
async def test_index(client, app): async def test_index(client, app):

View File

@ -2,4 +2,5 @@ dependency-injector
aiohttp aiohttp
pyyaml pyyaml
pytest-aiohttp pytest-aiohttp
pytest-asyncio
pytest-cov pytest-cov

View File

@ -1,4 +1,4 @@
FROM python:3.10-buster FROM python:3.13-bookworm
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1

View File

@ -13,13 +13,13 @@ Build the Docker image:
.. code-block:: bash .. code-block:: bash
docker-compose build docker compose build
Run the docker-compose environment: Run the docker-compose environment:
.. code-block:: bash .. code-block:: bash
docker-compose up docker compose up
The output should be something like: The output should be something like:
@ -59,15 +59,16 @@ To run the tests do:
.. code-block:: bash .. code-block:: bash
docker-compose run --rm monitor py.test monitoringdaemon/tests.py --cov=monitoringdaemon docker compose run --rm monitor py.test monitoringdaemon/tests.py --cov=monitoringdaemon
The output should be something like: The output should be something like:
.. code-block:: .. code-block::
platform linux -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 platform linux -- Python 3.13.1, pytest-8.3.4, pluggy-1.5.0
rootdir: /code rootdir: /code
plugins: asyncio-0.16.0, cov-3.0.0 plugins: cov-6.0.0, asyncio-0.24.0
asyncio: mode=Mode.STRICT, default_loop_scope=None
collected 2 items collected 2 items
monitoringdaemon/tests.py .. [100%] monitoringdaemon/tests.py .. [100%]

View File

@ -61,7 +61,7 @@ async def test_example_monitor(container, caplog):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_dispatcher(container, caplog, event_loop): async def test_dispatcher(container, caplog):
caplog.set_level("INFO") caplog.set_level("INFO")
example_monitor_mock = mock.AsyncMock() example_monitor_mock = mock.AsyncMock()
@ -72,6 +72,7 @@ async def test_dispatcher(container, caplog, event_loop):
httpbin_monitor=httpbin_monitor_mock, httpbin_monitor=httpbin_monitor_mock,
): ):
dispatcher = container.dispatcher() dispatcher = container.dispatcher()
event_loop = asyncio.get_running_loop()
event_loop.create_task(dispatcher.start()) event_loop.create_task(dispatcher.start())
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
dispatcher.stop() dispatcher.stop()

View File

@ -1,4 +1,4 @@
FROM python:3.10-buster FROM python:3.13-bookworm
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1

View File

@ -12,13 +12,13 @@ Build the Docker image:
.. code-block:: bash .. code-block:: bash
docker-compose build docker compose build
Run the docker-compose environment: Run the docker-compose environment:
.. code-block:: bash .. code-block:: bash
docker-compose up docker compose up
The output should be something like: The output should be something like:
@ -54,16 +54,16 @@ To run the tests do:
.. code-block:: bash .. code-block:: bash
docker-compose run --rm example py.test fastapiredis/tests.py --cov=fastapiredis docker compose run --rm example py.test fastapiredis/tests.py --cov=fastapiredis
The output should be something like: The output should be something like:
.. code-block:: .. code-block::
platform linux -- Python 3.10.9, pytest-7.2.0, pluggy-1.0.0 platform linux -- Python 3.13.1, pytest-8.3.4, pluggy-1.5.0
rootdir: /code rootdir: /code
plugins: cov-4.0.0, asyncio-0.20.3 plugins: cov-6.0.0, asyncio-0.24.0, anyio-4.7.0
collected 1 item asyncio: mode=Mode.STRICT, default_loop_scope=None
fastapiredis/tests.py . [100%] fastapiredis/tests.py . [100%]

View File

@ -1,6 +1,6 @@
from typing import AsyncIterator from typing import AsyncIterator
from aioredis import from_url, Redis from redis.asyncio import from_url, Redis
async def init_redis_pool(host: str, password: str) -> AsyncIterator[Redis]: async def init_redis_pool(host: str, password: str) -> AsyncIterator[Redis]:

View File

@ -1,6 +1,6 @@
"""Services module.""" """Services module."""
from aioredis import Redis from redis.asyncio import Redis
class Service: class Service:

View File

@ -1,7 +1,7 @@
dependency-injector dependency-injector
fastapi fastapi
uvicorn uvicorn
aioredis redis>=4.2
# For testing: # For testing:
pytest pytest

View File

@ -1,13 +1,14 @@
from unittest import mock from unittest import mock
import pytest import pytest
import pytest_asyncio
from httpx import ASGITransport, AsyncClient from httpx import ASGITransport, AsyncClient
from fastapi_di_example import app, container, Service from fastapi_di_example import app, container, Service
@pytest.fixture @pytest_asyncio.fixture
async def client(event_loop): async def client():
async with AsyncClient( async with AsyncClient(
transport=ASGITransport(app=app), transport=ASGITransport(app=app),
base_url="http://test", base_url="http://test",

View File

@ -1,4 +1,4 @@
FROM python:3.10-buster FROM python:3.13-bookworm
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
ENV HOST=0.0.0.0 ENV HOST=0.0.0.0

View File

@ -15,13 +15,13 @@ Build the Docker image:
.. code-block:: bash .. code-block:: bash
docker-compose build docker compose build
Run the docker-compose environment: Run the docker-compose environment:
.. code-block:: bash .. code-block:: bash
docker-compose up docker compose up
The output should be something like: The output should be something like:
@ -67,15 +67,15 @@ To run the tests do:
.. code-block:: bash .. code-block:: bash
docker-compose run --rm webapp py.test webapp/tests.py --cov=webapp docker compose run --rm webapp py.test webapp/tests.py --cov=webapp
The output should be something like: The output should be something like:
.. code-block:: .. code-block::
platform linux -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 platform linux -- Python 3.13.1, pytest-8.3.4, pluggy-1.5.0
rootdir: /code rootdir: /code
plugins: cov-3.0.0 plugins: cov-6.0.0, anyio-4.7.0
collected 7 items collected 7 items
webapp/tests.py ....... [100%] webapp/tests.py ....... [100%]

View File

@ -1,5 +1,5 @@
dependency-injector dependency-injector
fastapi fastapi[standard]
uvicorn uvicorn
pyyaml pyyaml
sqlalchemy sqlalchemy

View File

@ -101,9 +101,9 @@ The output should be something like:
.. code-block:: .. code-block::
platform darwin -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0
plugins: asyncio-0.16.0, cov-3.0.0 plugins: cov-6.0.0, anyio-4.4.0, asyncio-0.24.0, aiohttp-1.0.5
collected 3 items asyncio: mode=Mode.STRICT, default_loop_scope=None
giphynavigator/tests.py ... [100%] giphynavigator/tests.py ... [100%]

View File

@ -3,13 +3,14 @@
from unittest import mock from unittest import mock
import pytest import pytest
import pytest_asyncio
from httpx import ASGITransport, AsyncClient from httpx import ASGITransport, AsyncClient
from giphynavigator.application import app from giphynavigator.application import app
from giphynavigator.giphy import GiphyClient from giphynavigator.giphy import GiphyClient
@pytest.fixture @pytest_asyncio.fixture
async def client(): async def client():
async with AsyncClient( async with AsyncClient(
transport=ASGITransport(app=app), transport=ASGITransport(app=app),

View File

@ -81,8 +81,9 @@ The output should be something like:
.. code-block:: .. code-block::
platform darwin -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0
plugins: cov-3.0.0, flask-1.2.0 plugins: cov-6.0.0, flask-1.3.0
asyncio: mode=Mode.STRICT, default_loop_scope=None
collected 2 items collected 2 items
githubnavigator/tests.py .. [100%] githubnavigator/tests.py .. [100%]

View File

@ -1,7 +1,7 @@
"""Application module.""" """Application module."""
from flask import Flask from flask import Flask
from flask_bootstrap import Bootstrap from flask_bootstrap import Bootstrap4
from .containers import Container from .containers import Container
from .blueprints import example from .blueprints import example
@ -15,7 +15,7 @@ def create_app() -> Flask:
app.container = container app.container = container
app.register_blueprint(example.blueprint) app.register_blueprint(example.blueprint)
bootstrap = Bootstrap() bootstrap = Bootstrap4()
bootstrap.init_app(app) bootstrap.init_app(app)
return app return app

View File

@ -81,8 +81,9 @@ The output should be something like:
.. code-block:: .. code-block::
platform darwin -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0
plugins: cov-3.0.0, flask-1.2.0 plugins: cov-6.0.0, flask-1.3.0
asyncio: mode=Mode.STRICT, default_loop_scope=None
collected 2 items collected 2 items
githubnavigator/tests.py .. [100%] githubnavigator/tests.py .. [100%]

View File

@ -1,7 +1,7 @@
"""Application module.""" """Application module."""
from flask import Flask from flask import Flask
from flask_bootstrap import Bootstrap from flask_bootstrap import Bootstrap4
from .containers import Container from .containers import Container
from . import views from . import views
@ -15,7 +15,7 @@ def create_app() -> Flask:
app.container = container app.container = container
app.add_url_rule("/", "index", views.index) app.add_url_rule("/", "index", views.index)
bootstrap = Bootstrap() bootstrap = Bootstrap4()
bootstrap.init_app(app) bootstrap.init_app(app)
return app return app

View File

@ -58,8 +58,8 @@ The output should be something like:
.. code-block:: .. code-block::
platform darwin -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0
plugins: cov-3.0.0 plugins: cov-6.0.0
collected 2 items collected 2 items
movies/tests.py .. [100%] movies/tests.py .. [100%]

View File

@ -27,7 +27,7 @@ To run the application do:
.. code-block:: bash .. code-block:: bash
export GIPHY_API_KEY=wBJ2wZG7SRqfrU9nPgPiWvORmloDyuL0 export GIPHY_API_KEY=wBJ2wZG7SRqfrU9nPgPiWvORmloDyuL0
python -m giphynavigator sanic giphynavigator.application:create_app
The output should be something like: The output should be something like:
@ -98,8 +98,9 @@ The output should be something like:
.. code-block:: .. code-block::
platform darwin -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0
plugins: sanic-1.9.1, anyio-3.3.4, cov-3.0.0 plugins: cov-6.0.0, anyio-4.4.0, asyncio-0.24.0
asyncio: mode=Mode.STRICT, default_loop_scope=None
collected 3 items collected 3 items
giphynavigator/tests.py ... [100%] giphynavigator/tests.py ... [100%]

View File

@ -8,6 +8,8 @@ from sanic import Sanic
from giphynavigator.application import create_app from giphynavigator.application import create_app
from giphynavigator.giphy import GiphyClient from giphynavigator.giphy import GiphyClient
pytestmark = pytest.mark.asyncio
@pytest.fixture @pytest.fixture
def app(): def app():
@ -17,12 +19,7 @@ def app():
app.ctx.container.unwire() app.ctx.container.unwire()
@pytest.fixture async def test_index(app):
def test_client(loop, app, sanic_client):
return loop.run_until_complete(sanic_client(app))
async def test_index(app, test_client):
giphy_client_mock = mock.AsyncMock(spec=GiphyClient) giphy_client_mock = mock.AsyncMock(spec=GiphyClient)
giphy_client_mock.search.return_value = { giphy_client_mock.search.return_value = {
"data": [ "data": [
@ -32,7 +29,7 @@ async def test_index(app, test_client):
} }
with app.ctx.container.giphy_client.override(giphy_client_mock): with app.ctx.container.giphy_client.override(giphy_client_mock):
response = await test_client.get( _, response = await app.asgi_client.get(
"/", "/",
params={ params={
"query": "test", "query": "test",
@ -41,7 +38,7 @@ async def test_index(app, test_client):
) )
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json
assert data == { assert data == {
"query": "test", "query": "test",
"limit": 10, "limit": 10,
@ -52,30 +49,30 @@ async def test_index(app, test_client):
} }
async def test_index_no_data(app, test_client): async def test_index_no_data(app):
giphy_client_mock = mock.AsyncMock(spec=GiphyClient) giphy_client_mock = mock.AsyncMock(spec=GiphyClient)
giphy_client_mock.search.return_value = { giphy_client_mock.search.return_value = {
"data": [], "data": [],
} }
with app.ctx.container.giphy_client.override(giphy_client_mock): with app.ctx.container.giphy_client.override(giphy_client_mock):
response = await test_client.get("/") _, response = await app.asgi_client.get("/")
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json
assert data["gifs"] == [] assert data["gifs"] == []
async def test_index_default_params(app, test_client): async def test_index_default_params(app):
giphy_client_mock = mock.AsyncMock(spec=GiphyClient) giphy_client_mock = mock.AsyncMock(spec=GiphyClient)
giphy_client_mock.search.return_value = { giphy_client_mock.search.return_value = {
"data": [], "data": [],
} }
with app.ctx.container.giphy_client.override(giphy_client_mock): with app.ctx.container.giphy_client.override(giphy_client_mock):
response = await test_client.get("/") _, response = await app.asgi_client.get("/")
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json
assert data["query"] == app.ctx.container.config.default.query() assert data["query"] == app.ctx.container.config.default.query()
assert data["limit"] == app.ctx.container.config.default.limit() assert data["limit"] == app.ctx.container.config.default.limit()

View File

@ -1,6 +1,6 @@
dependency-injector dependency-injector
sanic<=21.6 sanic
sanic-testing
aiohttp aiohttp
pyyaml pyyaml
pytest-sanic
pytest-cov pytest-cov

View File

@ -0,0 +1,39 @@
Integration With Starlette-based Frameworks
===========================================
This is a `Starlette <https://www.starlette.io/>`_ +
`Dependency Injector <https://python-dependency-injector.ets-labs.org/>`_ example application
utilizing `lifespan API <https://www.starlette.io/lifespan/>`_.
.. note::
Pretty much `any framework built on top of Starlette <https://www.starlette.io/third-party-packages/#frameworks>`_
supports this feature (`FastAPI <https://fastapi.tiangolo.com/advanced/events/#lifespan>`_,
`Xpresso <https://xpresso-api.dev/latest/tutorial/lifespan/>`_, etc...).
Run
---
Create virtual environment:
.. code-block:: bash
python -m venv env
. env/bin/activate
Install requirements:
.. code-block:: bash
pip install -r requirements.txt
To run the application do:
.. code-block:: bash
python example.py
# or (logging won't be configured):
uvicorn --factory example:container.app
After that visit http://127.0.0.1:8000/ in your browser or use CLI command (``curl``, ``httpie``,
etc).

View File

@ -0,0 +1,59 @@
#!/usr/bin/env python
from logging import basicConfig, getLogger
from dependency_injector.containers import DeclarativeContainer
from dependency_injector.ext.starlette import Lifespan
from dependency_injector.providers import Factory, Resource, Self, Singleton
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.routing import Route
count = 0
def init():
log = getLogger(__name__)
log.info("Inittializing resources")
yield
log.info("Cleaning up resources")
async def homepage(request: Request) -> JSONResponse:
global count
response = JSONResponse({"hello": "world", "count": count})
count += 1
return response
class Container(DeclarativeContainer):
__self__ = Self()
lifespan = Singleton(Lifespan, __self__)
logging = Resource(
basicConfig,
level="DEBUG",
datefmt="%Y-%m-%d %H:%M",
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
init = Resource(init)
app = Factory(
Starlette,
debug=True,
lifespan=lifespan,
routes=[Route("/", homepage)],
)
container = Container()
if __name__ == "__main__":
import uvicorn
uvicorn.run(
container.app,
factory=True,
# NOTE: `None` prevents uvicorn from configuring logging, which is
# impossible via CLI
log_config=None,
)

View File

@ -0,0 +1,3 @@
dependency-injector
starlette
uvicorn

View File

@ -53,7 +53,6 @@ classifiers = [
"Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Libraries :: Python Modules",
] ]
dynamic = ["version"] dynamic = ["version"]
dependencies = ["six"]
[project.optional-dependencies] [project.optional-dependencies]
yaml = ["pyyaml"] yaml = ["pyyaml"]

View File

@ -1,3 +1,3 @@
flask==2.1.3 flask
werkzeug==2.2.2 werkzeug
aiohttp aiohttp

View File

@ -1 +0,0 @@
six>=1.7.0,<=1.16.0

View File

@ -1,6 +1,6 @@
"""Top-level package.""" """Top-level package."""
__version__ = "4.44.0" __version__ = "4.45.0"
"""Version number. """Version number.
:type: str :type: str

View File

@ -18,8 +18,6 @@ try:
except ImportError: except ImportError:
yaml = None yaml = None
import six
from . import providers, errors from . import providers, errors
from .providers cimport __is_future_or_coroutine from .providers cimport __is_future_or_coroutine
@ -201,7 +199,7 @@ class DynamicContainer(Container):
:rtype: None :rtype: None
""" """
for name, provider in six.iteritems(providers): for name, provider in providers.items():
setattr(self, name, provider) setattr(self, name, provider)
def set_provider(self, name, provider): def set_provider(self, name, provider):
@ -234,7 +232,7 @@ class DynamicContainer(Container):
self.overridden += (overriding,) self.overridden += (overriding,)
for name, provider in six.iteritems(overriding.providers): for name, provider in overriding.providers.items():
try: try:
getattr(self, name).override(provider) getattr(self, name).override(provider)
except AttributeError: except AttributeError:
@ -250,7 +248,7 @@ class DynamicContainer(Container):
:rtype: None :rtype: None
""" """
overridden_providers = [] overridden_providers = []
for name, overriding_provider in six.iteritems(overriding_providers): for name, overriding_provider in overriding_providers.items():
container_provider = getattr(self, name) container_provider = getattr(self, name)
container_provider.override(overriding_provider) container_provider.override(overriding_provider)
overridden_providers.append(container_provider) overridden_providers.append(container_provider)
@ -266,7 +264,7 @@ class DynamicContainer(Container):
self.overridden = self.overridden[:-1] self.overridden = self.overridden[:-1]
for provider in six.itervalues(self.providers): for provider in self.providers.values():
provider.reset_last_overriding() provider.reset_last_overriding()
def reset_override(self): def reset_override(self):
@ -276,7 +274,7 @@ class DynamicContainer(Container):
""" """
self.overridden = tuple() self.overridden = tuple()
for provider in six.itervalues(self.providers): for provider in self.providers.values():
provider.reset_override() provider.reset_override()
def is_auto_wiring_enabled(self): def is_auto_wiring_enabled(self):
@ -495,13 +493,13 @@ class DeclarativeContainerMetaClass(type):
containers = { containers = {
name: container name: container
for name, container in six.iteritems(attributes) for name, container in attributes.items()
if is_container(container) if is_container(container)
} }
cls_providers = { cls_providers = {
name: provider name: provider
for name, provider in six.iteritems(attributes) for name, provider in attributes.items()
if isinstance(provider, providers.Provider) and not isinstance(provider, providers.Self) if isinstance(provider, providers.Provider) and not isinstance(provider, providers.Self)
} }
@ -509,7 +507,7 @@ class DeclarativeContainerMetaClass(type):
name: provider name: provider
for base in bases for base in bases
if is_container(base) and base is not DynamicContainer if is_container(base) and base is not DynamicContainer
for name, provider in six.iteritems(base.providers) for name, provider in base.providers.items()
} }
all_providers = {} all_providers = {}
@ -536,10 +534,10 @@ class DeclarativeContainerMetaClass(type):
self.set_container(cls) self.set_container(cls)
cls.__self__ = self cls.__self__ = self
for provider in six.itervalues(cls.providers): for provider in cls.providers.values():
_check_provider_type(cls, provider) _check_provider_type(cls, provider)
for provider in six.itervalues(cls.cls_providers): for provider in cls.cls_providers.values():
if isinstance(provider, providers.CHILD_PROVIDERS): if isinstance(provider, providers.CHILD_PROVIDERS):
provider.assign_parent(cls) provider.assign_parent(cls)
@ -641,8 +639,7 @@ class DeclarativeContainerMetaClass(type):
return self return self
@six.add_metaclass(DeclarativeContainerMetaClass) class DeclarativeContainer(Container, metaclass=DeclarativeContainerMetaClass):
class DeclarativeContainer(Container):
"""Declarative inversion of control container. """Declarative inversion of control container.
.. code-block:: python .. code-block:: python
@ -767,7 +764,7 @@ class DeclarativeContainer(Container):
cls.overridden += (overriding,) cls.overridden += (overriding,)
for name, provider in six.iteritems(overriding.cls_providers): for name, provider in overriding.cls_providers.items():
try: try:
getattr(cls, name).override(provider) getattr(cls, name).override(provider)
except AttributeError: except AttributeError:
@ -784,7 +781,7 @@ class DeclarativeContainer(Container):
cls.overridden = cls.overridden[:-1] cls.overridden = cls.overridden[:-1]
for provider in six.itervalues(cls.providers): for provider in cls.providers.values():
provider.reset_last_overriding() provider.reset_last_overriding()
@classmethod @classmethod
@ -795,7 +792,7 @@ class DeclarativeContainer(Container):
""" """
cls.overridden = tuple() cls.overridden = tuple()
for provider in six.itervalues(cls.providers): for provider in cls.providers.values():
provider.reset_override() provider.reset_override()
@ -858,7 +855,7 @@ def copy(object base_container):
""" """
def _get_memo_for_matching_names(new_providers, base_providers): def _get_memo_for_matching_names(new_providers, base_providers):
memo = {} memo = {}
for new_provider_name, new_provider in six.iteritems(new_providers): for new_provider_name, new_provider in new_providers.items():
if new_provider_name not in base_providers: if new_provider_name not in base_providers:
continue continue
source_provider = base_providers[new_provider_name] source_provider = base_providers[new_provider_name]
@ -877,7 +874,7 @@ def copy(object base_container):
new_providers.update(providers.deepcopy(base_container.providers, memo)) new_providers.update(providers.deepcopy(base_container.providers, memo))
new_providers.update(providers.deepcopy(new_container.cls_providers, memo)) new_providers.update(providers.deepcopy(new_container.cls_providers, memo))
for name, provider in six.iteritems(new_providers): for name, provider in new_providers.items():
setattr(new_container, name, provider) setattr(new_container, name, provider)
return new_container return new_container

View File

@ -10,3 +10,24 @@ class Error(Exception):
class NoSuchProviderError(Error, AttributeError): class NoSuchProviderError(Error, AttributeError):
"""Error that is raised when provider lookup is failed.""" """Error that is raised when provider lookup is failed."""
class NonCopyableArgumentError(Error):
"""Error that is raised when provider argument is not deep-copyable."""
index: int
keyword: str
provider: object
def __init__(self, provider: object, index: int = -1, keyword: str = "") -> None:
self.provider = provider
self.index = index
self.keyword = keyword
def __str__(self) -> str:
s = (
f"keyword argument {self.keyword}"
if self.keyword
else f"argument at index {self.index}"
)
return f"Couldn't copy {s} for provider {self.provider!r}"

View File

@ -0,0 +1,52 @@
import sys
from typing import Any
if sys.version_info >= (3, 11): # pragma: no cover
from typing import Self
else: # pragma: no cover
from typing_extensions import Self
from dependency_injector.containers import Container
class Lifespan:
"""A starlette lifespan handler performing container resource initialization and shutdown.
See https://www.starlette.io/lifespan/ for details.
Usage:
.. code-block:: python
from dependency_injector.containers import DeclarativeContainer
from dependency_injector.ext.starlette import Lifespan
from dependency_injector.providers import Factory, Self, Singleton
from starlette.applications import Starlette
class Container(DeclarativeContainer):
__self__ = Self()
lifespan = Singleton(Lifespan, __self__)
app = Factory(Starlette, lifespan=lifespan)
:param container: container instance
"""
container: Container
def __init__(self, container: Container) -> None:
self.container = container
def __call__(self, app: Any) -> Self:
return self
async def __aenter__(self) -> None:
result = self.container.init_resources()
if result is not None:
await result
async def __aexit__(self, *exc_info: Any) -> None:
result = self.container.shutdown_resources()
if result is not None:
await result

View File

@ -530,7 +530,21 @@ def is_delegated(instance: Any) -> bool: ...
def represent_provider(provider: Provider, provides: Any) -> str: ... def represent_provider(provider: Provider, provides: Any) -> str: ...
def deepcopy(instance: Any, memo: Optional[_Dict[Any, Any]] = None): Any: ... def deepcopy(instance: Any, memo: Optional[_Dict[Any, Any]] = None) -> Any: ...
def deepcopy_args(
provider: Provider[Any],
args: Tuple[Any, ...],
memo: Optional[_Dict[int, Any]] = None,
) -> Tuple[Any, ...]: ...
def deepcopy_kwargs(
provider: Provider[Any],
kwargs: _Dict[str, Any],
memo: Optional[_Dict[int, Any]] = None,
) -> Dict[str, Any]: ...
def merge_dicts(dict1: _Dict[Any, Any], dict2: _Dict[Any, Any]) -> _Dict[Any, Any]: ... def merge_dicts(dict1: _Dict[Any, Any], dict2: _Dict[Any, Any]) -> _Dict[Any, Any]: ...

View File

@ -71,6 +71,7 @@ except ImportError:
from .errors import ( from .errors import (
Error, Error,
NoSuchProviderError, NoSuchProviderError,
NonCopyableArgumentError,
) )
cimport cython cimport cython
@ -1252,8 +1253,8 @@ cdef class Callable(Provider):
copied = _memorized_duplicate(self, memo) copied = _memorized_duplicate(self, memo)
copied.set_provides(_copy_if_provider(self.provides, memo)) copied.set_provides(_copy_if_provider(self.provides, memo))
copied.set_args(*deepcopy(self.args, memo)) copied.set_args(*deepcopy_args(self, self.args, memo))
copied.set_kwargs(**deepcopy(self.kwargs, memo)) copied.set_kwargs(**deepcopy_kwargs(self, self.kwargs, memo))
self._copy_overridings(copied, memo) self._copy_overridings(copied, memo)
return copied return copied
@ -2539,8 +2540,8 @@ cdef class Factory(Provider):
copied = _memorized_duplicate(self, memo) copied = _memorized_duplicate(self, memo)
copied.set_provides(_copy_if_provider(self.provides, memo)) copied.set_provides(_copy_if_provider(self.provides, memo))
copied.set_args(*deepcopy(self.args, memo)) copied.set_args(*deepcopy_args(self, self.args, memo))
copied.set_kwargs(**deepcopy(self.kwargs, memo)) copied.set_kwargs(**deepcopy_kwargs(self, self.kwargs, memo))
copied.set_attributes(**deepcopy(self.attributes, memo)) copied.set_attributes(**deepcopy(self.attributes, memo))
self._copy_overridings(copied, memo) self._copy_overridings(copied, memo)
return copied return copied
@ -2838,8 +2839,8 @@ cdef class BaseSingleton(Provider):
copied = _memorized_duplicate(self, memo) copied = _memorized_duplicate(self, memo)
copied.set_provides(_copy_if_provider(self.provides, memo)) copied.set_provides(_copy_if_provider(self.provides, memo))
copied.set_args(*deepcopy(self.args, memo)) copied.set_args(*deepcopy_args(self, self.args, memo))
copied.set_kwargs(**deepcopy(self.kwargs, memo)) copied.set_kwargs(**deepcopy_kwargs(self, self.kwargs, memo))
copied.set_attributes(**deepcopy(self.attributes, memo)) copied.set_attributes(**deepcopy(self.attributes, memo))
self._copy_overridings(copied, memo) self._copy_overridings(copied, memo)
return copied return copied
@ -3221,7 +3222,7 @@ cdef class ThreadLocalSingleton(BaseSingleton):
return future_result return future_result
self._storage.instance = instance self._storage.instance = instance
finally:
return instance return instance
def _async_init_instance(self, future_result, result): def _async_init_instance(self, future_result, result):
@ -3451,7 +3452,7 @@ cdef class List(Provider):
return copied return copied
copied = _memorized_duplicate(self, memo) copied = _memorized_duplicate(self, memo)
copied.set_args(*deepcopy(self.args, memo)) copied.set_args(*deepcopy_args(self, self.args, memo))
self._copy_overridings(copied, memo) self._copy_overridings(copied, memo)
return copied return copied
@ -3674,8 +3675,8 @@ cdef class Resource(Provider):
copied = _memorized_duplicate(self, memo) copied = _memorized_duplicate(self, memo)
copied.set_provides(_copy_if_provider(self.provides, memo)) copied.set_provides(_copy_if_provider(self.provides, memo))
copied.set_args(*deepcopy(self.args, memo)) copied.set_args(*deepcopy_args(self, self.args, memo))
copied.set_kwargs(**deepcopy(self.kwargs, memo)) copied.set_kwargs(**deepcopy_kwargs(self, self.kwargs, memo))
self._copy_overridings(copied, memo) self._copy_overridings(copied, memo)
@ -4525,8 +4526,8 @@ cdef class MethodCaller(Provider):
copied = _memorized_duplicate(self, memo) copied = _memorized_duplicate(self, memo)
copied.set_provides(_copy_if_provider(self.provides, memo)) copied.set_provides(_copy_if_provider(self.provides, memo))
copied.set_args(*deepcopy(self.args, memo)) copied.set_args(*deepcopy_args(self, self.args, memo))
copied.set_kwargs(**deepcopy(self.kwargs, memo)) copied.set_kwargs(**deepcopy_kwargs(self, self.kwargs, memo))
self._copy_overridings(copied, memo) self._copy_overridings(copied, memo)
return copied return copied
@ -4927,6 +4928,48 @@ cpdef object deepcopy(object instance, dict memo=None):
return copy.deepcopy(instance, memo) return copy.deepcopy(instance, memo)
cpdef tuple deepcopy_args(
Provider provider,
tuple args,
dict[int, object] memo = None,
):
"""A wrapper for deepcopy for positional arguments.
Used to improve debugability of objects that cannot be deep-copied.
"""
cdef list[object] out = []
for i, arg in enumerate(args):
try:
out.append(copy.deepcopy(arg, memo))
except Exception as e:
raise NonCopyableArgumentError(provider, index=i) from e
return tuple(out)
cpdef dict[str, object] deepcopy_kwargs(
Provider provider,
dict[str, object] kwargs,
dict[int, object] memo = None,
):
"""A wrapper for deepcopy for keyword arguments.
Used to improve debugability of objects that cannot be deep-copied.
"""
cdef dict[str, object] out = {}
for name, arg in kwargs.items():
try:
out[name] = copy.deepcopy(arg, memo)
except Exception as e:
raise NonCopyableArgumentError(provider, keyword=name) from e
return out
def __add_sys_streams(memo): def __add_sys_streams(memo):
"""Add system streams to memo dictionary. """Add system streams to memo dictionary.

View File

@ -2,7 +2,7 @@ from pathlib import Path
from typing import Any from typing import Any
from dependency_injector import providers from dependency_injector import providers
from pydantic import BaseSettings as PydanticSettings from pydantic_settings import BaseSettings as PydanticSettings
# Test 1: to check the getattr # Test 1: to check the getattr

View File

@ -0,0 +1,41 @@
from typing import AsyncIterator, Iterator
from unittest.mock import ANY
from pytest import mark
from dependency_injector.containers import DeclarativeContainer
from dependency_injector.ext.starlette import Lifespan
from dependency_injector.providers import Resource
class TestLifespan:
@mark.parametrize("sync", [False, True])
@mark.asyncio
async def test_context_manager(self, sync: bool) -> None:
init, shutdown = False, False
def sync_resource() -> Iterator[None]:
nonlocal init, shutdown
init = True
yield
shutdown = True
async def async_resource() -> AsyncIterator[None]:
nonlocal init, shutdown
init = True
yield
shutdown = True
class Container(DeclarativeContainer):
x = Resource(sync_resource if sync else async_resource)
container = Container()
lifespan = Lifespan(container)
async with lifespan(ANY) as scope:
assert scope is None
assert init
assert shutdown

View File

@ -0,0 +1,20 @@
import pytest
from dependency_injector.containers import Container
from dependency_injector.providers import ThreadLocalSingleton
class FailingClass:
def __init__(self):
raise ValueError("FAILING CLASS")
class TestContainer(Container):
failing_class = ThreadLocalSingleton(FailingClass)
def test_on_failure_value_error_is_raised():
container = TestContainer()
with pytest.raises(ValueError, match="FAILING CLASS"):
container.failing_class()

View File

@ -0,0 +1,65 @@
import sys
from typing import Any, Dict, NoReturn
from pytest import raises
from dependency_injector.errors import NonCopyableArgumentError
from dependency_injector.providers import (
Provider,
deepcopy,
deepcopy_args,
deepcopy_kwargs,
)
class NonCopiable:
def __deepcopy__(self, memo: Dict[int, Any]) -> NoReturn:
raise NotImplementedError
def test_deepcopy_streams_not_copied() -> None:
l = [sys.stdin, sys.stdout, sys.stderr]
assert deepcopy(l) == l
def test_deepcopy_args() -> None:
provider = Provider[None]()
copiable = NonCopiable()
memo: Dict[int, Any] = {id(copiable): copiable}
assert deepcopy_args(provider, (1, copiable), memo) == (1, copiable)
def test_deepcopy_args_non_copiable() -> None:
provider = Provider[None]()
copiable = NonCopiable()
memo: Dict[int, Any] = {id(copiable): copiable}
with raises(
NonCopyableArgumentError,
match=r"^Couldn't copy argument at index 3 for provider ",
):
deepcopy_args(provider, (1, copiable, object(), NonCopiable()), memo)
def test_deepcopy_kwargs() -> None:
provider = Provider[None]()
copiable = NonCopiable()
memo: Dict[int, Any] = {id(copiable): copiable}
assert deepcopy_kwargs(provider, {"x": 1, "y": copiable}, memo) == {
"x": 1,
"y": copiable,
}
def test_deepcopy_kwargs_non_copiable() -> None:
provider = Provider[None]()
copiable = NonCopiable()
memo: Dict[int, Any] = {id(copiable): copiable}
with raises(
NonCopyableArgumentError,
match=r"^Couldn't copy keyword argument z for provider ",
):
deepcopy_kwargs(provider, {"x": 1, "y": copiable, "z": NonCopiable()}, memo)

View File

@ -1,11 +1,9 @@
from flask import Flask, jsonify, request, current_app, session, g from flask import Flask, jsonify, request, current_app, session, g
from flask import _request_ctx_stack, _app_ctx_stack
from dependency_injector import containers, providers from dependency_injector import containers, providers
from dependency_injector.wiring import inject, Provide from dependency_injector.wiring import inject, Provide
# This is here for testing wiring bypasses these objects without crashing # This is here for testing wiring bypasses these objects without crashing
request, current_app, session, g # noqa request, current_app, session, g # noqa
_request_ctx_stack, _app_ctx_stack # noqa
class Service: class Service:

29
tox.ini
View File

@ -7,18 +7,16 @@ envlist=
deps= deps=
pytest pytest
pytest-asyncio pytest-asyncio
# TODO: Hotfix, remove when fixed https://github.com/aio-libs/aiohttp/issues/5107
typing_extensions
httpx httpx
fastapi fastapi
flask<2.2 flask
aiohttp<=3.9.0b1 aiohttp
numpy numpy
scipy scipy
boto3 boto3
mypy_boto3_s3 mypy_boto3_s3
pydantic<2 pydantic-settings
werkzeug<=2.2.2 werkzeug
extras= extras=
yaml yaml
commands = pytest -c tests/.configs/pytest.ini commands = pytest -c tests/.configs/pytest.ini
@ -37,17 +35,16 @@ deps =
v2: pydantic-settings v2: pydantic-settings
pytest pytest
pytest-asyncio pytest-asyncio
-rrequirements.txt
typing_extensions typing_extensions
httpx httpx
fastapi fastapi
flask<2.2 flask
aiohttp<=3.9.0b1 aiohttp
numpy numpy
scipy scipy
boto3 boto3
mypy_boto3_s3 mypy_boto3_s3
werkzeug<=2.2.2 werkzeug
commands = pytest -c tests/.configs/pytest.ini -m pydantic commands = pytest -c tests/.configs/pytest.ini -m pydantic
[testenv:coveralls] [testenv:coveralls]
@ -69,9 +66,9 @@ deps=
pytest pytest
pytest-asyncio pytest-asyncio
httpx httpx
flask<2.2 flask
pydantic<2 pydantic-settings
werkzeug<=2.2.2 werkzeug
fastapi fastapi
boto3 boto3
mypy_boto3_s3 mypy_boto3_s3
@ -83,8 +80,8 @@ commands = pytest -c tests/.configs/pytest-py35.ini
[testenv:pylint] [testenv:pylint]
deps= deps=
pylint pylint
flask<2.2 flask
werkzeug<=2.2.2 werkzeug
commands= commands=
- pylint -f colorized src/dependency_injector - pylint -f colorized src/dependency_injector
@ -105,7 +102,7 @@ commands=
[testenv:mypy] [testenv:mypy]
deps= deps=
typing_extensions typing_extensions
pydantic<2 pydantic-settings
mypy mypy
commands= commands=
mypy tests/typing mypy tests/typing