From 87741edb5330f66c25f02648813eb3437b87dd89 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Sun, 8 Dec 2024 18:53:08 +0200 Subject: [PATCH 01/25] Upgrade testing deps (#837) --- requirements-ext.txt | 4 ++-- tests/typing/configuration.py | 2 +- tests/unit/samples/wiringflask/web.py | 2 -- tox.ini | 28 +++++++++++++-------------- 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/requirements-ext.txt b/requirements-ext.txt index 138447f1..5f7a9611 100644 --- a/requirements-ext.txt +++ b/requirements-ext.txt @@ -1,3 +1,3 @@ -flask==2.1.3 -werkzeug==2.2.2 +flask +werkzeug aiohttp diff --git a/tests/typing/configuration.py b/tests/typing/configuration.py index f6adf245..832281d3 100644 --- a/tests/typing/configuration.py +++ b/tests/typing/configuration.py @@ -2,7 +2,7 @@ from pathlib import Path from typing import Any from dependency_injector import providers -from pydantic import BaseSettings as PydanticSettings +from pydantic_settings import BaseSettings as PydanticSettings # Test 1: to check the getattr diff --git a/tests/unit/samples/wiringflask/web.py b/tests/unit/samples/wiringflask/web.py index 37fbd5e0..f273d8aa 100644 --- a/tests/unit/samples/wiringflask/web.py +++ b/tests/unit/samples/wiringflask/web.py @@ -1,11 +1,9 @@ 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.wiring import inject, Provide # This is here for testing wiring bypasses these objects without crashing request, current_app, session, g # noqa -_request_ctx_stack, _app_ctx_stack # noqa class Service: diff --git a/tox.ini b/tox.ini index c06b0f95..ed02f0e9 100644 --- a/tox.ini +++ b/tox.ini @@ -7,18 +7,16 @@ envlist= deps= pytest pytest-asyncio - # TODO: Hotfix, remove when fixed https://github.com/aio-libs/aiohttp/issues/5107 - typing_extensions httpx fastapi - flask<2.2 - aiohttp<=3.9.0b1 + flask + aiohttp numpy scipy boto3 mypy_boto3_s3 - pydantic<2 - werkzeug<=2.2.2 + pydantic-settings + werkzeug extras= yaml commands = pytest -c tests/.configs/pytest.ini @@ -41,13 +39,13 @@ deps = typing_extensions httpx fastapi - flask<2.2 - aiohttp<=3.9.0b1 + flask + aiohttp numpy scipy boto3 mypy_boto3_s3 - werkzeug<=2.2.2 + werkzeug commands = pytest -c tests/.configs/pytest.ini -m pydantic [testenv:coveralls] @@ -69,9 +67,9 @@ deps= pytest pytest-asyncio httpx - flask<2.2 - pydantic<2 - werkzeug<=2.2.2 + flask + pydantic-settings + werkzeug fastapi boto3 mypy_boto3_s3 @@ -83,8 +81,8 @@ commands = pytest -c tests/.configs/pytest-py35.ini [testenv:pylint] deps= pylint - flask<2.2 - werkzeug<=2.2.2 + flask + werkzeug commands= - pylint -f colorized src/dependency_injector @@ -105,7 +103,7 @@ commands= [testenv:mypy] deps= typing_extensions - pydantic<2 + pydantic-settings mypy commands= mypy tests/typing From 7f586246b43afba60aef728a324339c5c02e94b0 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Sun, 8 Dec 2024 18:53:29 +0200 Subject: [PATCH 02/25] Update examples (#838) --- docs/tutorials/asyncio-daemon.rst | 38 +++++++++---------- examples/miniapps/aiohttp/README.rst | 5 ++- .../miniapps/aiohttp/giphynavigator/tests.py | 10 +++-- examples/miniapps/aiohttp/requirements.txt | 1 + examples/miniapps/asyncio-daemon/Dockerfile | 2 +- examples/miniapps/asyncio-daemon/README.rst | 11 +++--- .../asyncio-daemon/monitoringdaemon/tests.py | 3 +- examples/miniapps/fastapi-redis/Dockerfile | 2 +- examples/miniapps/fastapi-redis/README.rst | 14 +++---- .../fastapi-redis/fastapiredis/redis.py | 2 +- .../fastapi-redis/fastapiredis/services.py | 2 +- .../miniapps/fastapi-redis/requirements.txt | 2 +- examples/miniapps/fastapi-simple/tests.py | 5 ++- .../miniapps/fastapi-sqlalchemy/Dockerfile | 2 +- .../miniapps/fastapi-sqlalchemy/README.rst | 10 ++--- .../fastapi-sqlalchemy/requirements.txt | 2 +- examples/miniapps/fastapi/README.rst | 6 +-- .../miniapps/fastapi/giphynavigator/tests.py | 3 +- examples/miniapps/flask-blueprints/README.rst | 5 ++- .../githubnavigator/application.py | 4 +- examples/miniapps/flask/README.rst | 5 ++- .../flask/githubnavigator/application.py | 4 +- examples/miniapps/movie-lister/README.rst | 4 +- examples/miniapps/sanic/README.rst | 7 ++-- .../miniapps/sanic/giphynavigator/tests.py | 25 ++++++------ examples/miniapps/sanic/requirements.txt | 4 +- 26 files changed, 93 insertions(+), 85 deletions(-) diff --git a/docs/tutorials/asyncio-daemon.rst b/docs/tutorials/asyncio-daemon.rst index 0c6d552a..09dbf29c 100644 --- a/docs/tutorials/asyncio-daemon.rst +++ b/docs/tutorials/asyncio-daemon.rst @@ -18,7 +18,7 @@ In this tutorial we will use: - Python 3 - Docker -- Docker-compose +- Docker Compose Start from the scratch or jump to the section: @@ -47,28 +47,27 @@ response it will log: Prerequisites ------------- -We will use `Docker `_ and -`docker-compose `_ in this tutorial. Let's check the versions: +We will use `docker compose `_ in this tutorial. Let's check the versions: .. code-block:: bash docker --version - docker-compose --version + docker compose version The output should look something like: .. code-block:: bash - Docker version 20.10.5, build 55c4c88 - docker-compose version 1.29.0, build 07737305 + Docker version 27.3.1, build ce12230 + Docker Compose version v2.29.7 .. 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: - `Install Docker `_ - - `Install docker-compose `_ + - `Install docker compose `_ 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 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: .. code-block:: bash - FROM python:3.10-buster + FROM python:3.13-bookworm ENV PYTHONUNBUFFERED=1 @@ -155,8 +154,6 @@ Put next lines into the ``docker-compose.yml`` file: .. code-block:: yaml - version: "3.7" - services: monitor: @@ -171,7 +168,7 @@ Run in the terminal: .. 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: @@ -184,7 +181,7 @@ After the build is done run the container: .. code-block:: bash - docker-compose up + docker compose up The output should look like: @@ -461,7 +458,7 @@ Run in the terminal: .. code-block:: bash - docker-compose up + docker compose up The output should look like: @@ -705,7 +702,7 @@ Run in the terminal: .. code-block:: bash - docker-compose up + docker compose up You should see: @@ -813,7 +810,7 @@ Run in the terminal: .. code-block:: bash - docker-compose up + docker compose up You should see: @@ -965,15 +962,16 @@ Run in the terminal: .. 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: .. 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 - 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 monitoringdaemon/tests.py .. [100%] diff --git a/examples/miniapps/aiohttp/README.rst b/examples/miniapps/aiohttp/README.rst index 10464017..017c3a93 100644 --- a/examples/miniapps/aiohttp/README.rst +++ b/examples/miniapps/aiohttp/README.rst @@ -98,8 +98,9 @@ The output should be something like: .. code-block:: - platform darwin -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 - plugins: asyncio-0.16.0, anyio-3.3.4, aiohttp-0.3.0, cov-3.0.0 + platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.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 giphynavigator/tests.py ... [100%] diff --git a/examples/miniapps/aiohttp/giphynavigator/tests.py b/examples/miniapps/aiohttp/giphynavigator/tests.py index 84eddc60..0201ed01 100644 --- a/examples/miniapps/aiohttp/giphynavigator/tests.py +++ b/examples/miniapps/aiohttp/giphynavigator/tests.py @@ -3,11 +3,15 @@ from unittest import mock import pytest +import pytest_asyncio from giphynavigator.application import create_app from giphynavigator.giphy import GiphyClient +pytestmark = pytest.mark.asyncio + + @pytest.fixture def app(): app = create_app() @@ -15,9 +19,9 @@ def app(): app.container.unwire() -@pytest.fixture -def client(app, aiohttp_client, loop): - return loop.run_until_complete(aiohttp_client(app)) +@pytest_asyncio.fixture +async def client(app, aiohttp_client): + return await aiohttp_client(app) async def test_index(client, app): diff --git a/examples/miniapps/aiohttp/requirements.txt b/examples/miniapps/aiohttp/requirements.txt index e84f6b89..16c8ba12 100644 --- a/examples/miniapps/aiohttp/requirements.txt +++ b/examples/miniapps/aiohttp/requirements.txt @@ -2,4 +2,5 @@ dependency-injector aiohttp pyyaml pytest-aiohttp +pytest-asyncio pytest-cov diff --git a/examples/miniapps/asyncio-daemon/Dockerfile b/examples/miniapps/asyncio-daemon/Dockerfile index accf7ae0..c40ff77d 100644 --- a/examples/miniapps/asyncio-daemon/Dockerfile +++ b/examples/miniapps/asyncio-daemon/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.10-buster +FROM python:3.13-bookworm ENV PYTHONUNBUFFERED=1 diff --git a/examples/miniapps/asyncio-daemon/README.rst b/examples/miniapps/asyncio-daemon/README.rst index 83848bd0..241e9f55 100644 --- a/examples/miniapps/asyncio-daemon/README.rst +++ b/examples/miniapps/asyncio-daemon/README.rst @@ -13,13 +13,13 @@ Build the Docker image: .. code-block:: bash - docker-compose build + docker compose build Run the docker-compose environment: .. code-block:: bash - docker-compose up + docker compose up The output should be something like: @@ -59,15 +59,16 @@ To run the tests do: .. 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: .. 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 - 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 monitoringdaemon/tests.py .. [100%] diff --git a/examples/miniapps/asyncio-daemon/monitoringdaemon/tests.py b/examples/miniapps/asyncio-daemon/monitoringdaemon/tests.py index 87c1a545..1c55b4ed 100644 --- a/examples/miniapps/asyncio-daemon/monitoringdaemon/tests.py +++ b/examples/miniapps/asyncio-daemon/monitoringdaemon/tests.py @@ -61,7 +61,7 @@ async def test_example_monitor(container, caplog): @pytest.mark.asyncio -async def test_dispatcher(container, caplog, event_loop): +async def test_dispatcher(container, caplog): caplog.set_level("INFO") example_monitor_mock = mock.AsyncMock() @@ -72,6 +72,7 @@ async def test_dispatcher(container, caplog, event_loop): httpbin_monitor=httpbin_monitor_mock, ): dispatcher = container.dispatcher() + event_loop = asyncio.get_running_loop() event_loop.create_task(dispatcher.start()) await asyncio.sleep(0.1) dispatcher.stop() diff --git a/examples/miniapps/fastapi-redis/Dockerfile b/examples/miniapps/fastapi-redis/Dockerfile index 8b7ce3bc..74f3a644 100644 --- a/examples/miniapps/fastapi-redis/Dockerfile +++ b/examples/miniapps/fastapi-redis/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.10-buster +FROM python:3.13-bookworm ENV PYTHONUNBUFFERED=1 diff --git a/examples/miniapps/fastapi-redis/README.rst b/examples/miniapps/fastapi-redis/README.rst index 1ef75b31..0e9a49eb 100644 --- a/examples/miniapps/fastapi-redis/README.rst +++ b/examples/miniapps/fastapi-redis/README.rst @@ -12,13 +12,13 @@ Build the Docker image: .. code-block:: bash - docker-compose build + docker compose build Run the docker-compose environment: .. code-block:: bash - docker-compose up + docker compose up The output should be something like: @@ -54,16 +54,16 @@ To run the tests do: .. 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: .. 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 - plugins: cov-4.0.0, asyncio-0.20.3 - collected 1 item + plugins: cov-6.0.0, asyncio-0.24.0, anyio-4.7.0 + asyncio: mode=Mode.STRICT, default_loop_scope=None fastapiredis/tests.py . [100%] @@ -77,4 +77,4 @@ The output should be something like: fastapiredis/services.py 7 3 57% fastapiredis/tests.py 18 0 100% ------------------------------------------------- - TOTAL 52 7 87% \ No newline at end of file + TOTAL 52 7 87% diff --git a/examples/miniapps/fastapi-redis/fastapiredis/redis.py b/examples/miniapps/fastapi-redis/fastapiredis/redis.py index e770906c..e1067f4a 100644 --- a/examples/miniapps/fastapi-redis/fastapiredis/redis.py +++ b/examples/miniapps/fastapi-redis/fastapiredis/redis.py @@ -1,6 +1,6 @@ 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]: diff --git a/examples/miniapps/fastapi-redis/fastapiredis/services.py b/examples/miniapps/fastapi-redis/fastapiredis/services.py index 0cae0731..4bee7ae7 100644 --- a/examples/miniapps/fastapi-redis/fastapiredis/services.py +++ b/examples/miniapps/fastapi-redis/fastapiredis/services.py @@ -1,6 +1,6 @@ """Services module.""" -from aioredis import Redis +from redis.asyncio import Redis class Service: diff --git a/examples/miniapps/fastapi-redis/requirements.txt b/examples/miniapps/fastapi-redis/requirements.txt index c217324a..6da76d96 100644 --- a/examples/miniapps/fastapi-redis/requirements.txt +++ b/examples/miniapps/fastapi-redis/requirements.txt @@ -1,7 +1,7 @@ dependency-injector fastapi uvicorn -aioredis +redis>=4.2 # For testing: pytest diff --git a/examples/miniapps/fastapi-simple/tests.py b/examples/miniapps/fastapi-simple/tests.py index cf033592..54cf4171 100644 --- a/examples/miniapps/fastapi-simple/tests.py +++ b/examples/miniapps/fastapi-simple/tests.py @@ -1,13 +1,14 @@ from unittest import mock import pytest +import pytest_asyncio from httpx import ASGITransport, AsyncClient from fastapi_di_example import app, container, Service -@pytest.fixture -async def client(event_loop): +@pytest_asyncio.fixture +async def client(): async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test", diff --git a/examples/miniapps/fastapi-sqlalchemy/Dockerfile b/examples/miniapps/fastapi-sqlalchemy/Dockerfile index 17676624..b36cfa63 100644 --- a/examples/miniapps/fastapi-sqlalchemy/Dockerfile +++ b/examples/miniapps/fastapi-sqlalchemy/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.10-buster +FROM python:3.13-bookworm ENV PYTHONUNBUFFERED=1 ENV HOST=0.0.0.0 diff --git a/examples/miniapps/fastapi-sqlalchemy/README.rst b/examples/miniapps/fastapi-sqlalchemy/README.rst index 9c305f18..753d20eb 100644 --- a/examples/miniapps/fastapi-sqlalchemy/README.rst +++ b/examples/miniapps/fastapi-sqlalchemy/README.rst @@ -15,13 +15,13 @@ Build the Docker image: .. code-block:: bash - docker-compose build + docker compose build Run the docker-compose environment: .. code-block:: bash - docker-compose up + docker compose up The output should be something like: @@ -67,15 +67,15 @@ To run the tests do: .. 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: .. 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 - plugins: cov-3.0.0 + plugins: cov-6.0.0, anyio-4.7.0 collected 7 items webapp/tests.py ....... [100%] diff --git a/examples/miniapps/fastapi-sqlalchemy/requirements.txt b/examples/miniapps/fastapi-sqlalchemy/requirements.txt index f2c5ade5..ef0cbbd6 100644 --- a/examples/miniapps/fastapi-sqlalchemy/requirements.txt +++ b/examples/miniapps/fastapi-sqlalchemy/requirements.txt @@ -1,5 +1,5 @@ dependency-injector -fastapi +fastapi[standard] uvicorn pyyaml sqlalchemy diff --git a/examples/miniapps/fastapi/README.rst b/examples/miniapps/fastapi/README.rst index 779ccac8..e7417c29 100644 --- a/examples/miniapps/fastapi/README.rst +++ b/examples/miniapps/fastapi/README.rst @@ -101,9 +101,9 @@ The output should be something like: .. code-block:: - platform darwin -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 - plugins: asyncio-0.16.0, cov-3.0.0 - collected 3 items + platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.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 giphynavigator/tests.py ... [100%] diff --git a/examples/miniapps/fastapi/giphynavigator/tests.py b/examples/miniapps/fastapi/giphynavigator/tests.py index c1505e78..3dabd0b2 100644 --- a/examples/miniapps/fastapi/giphynavigator/tests.py +++ b/examples/miniapps/fastapi/giphynavigator/tests.py @@ -3,13 +3,14 @@ from unittest import mock import pytest +import pytest_asyncio from httpx import ASGITransport, AsyncClient from giphynavigator.application import app from giphynavigator.giphy import GiphyClient -@pytest.fixture +@pytest_asyncio.fixture async def client(): async with AsyncClient( transport=ASGITransport(app=app), diff --git a/examples/miniapps/flask-blueprints/README.rst b/examples/miniapps/flask-blueprints/README.rst index 3d61636c..6f8385c9 100644 --- a/examples/miniapps/flask-blueprints/README.rst +++ b/examples/miniapps/flask-blueprints/README.rst @@ -81,8 +81,9 @@ The output should be something like: .. code-block:: - platform darwin -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 - plugins: cov-3.0.0, flask-1.2.0 + platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0 + plugins: cov-6.0.0, flask-1.3.0 + asyncio: mode=Mode.STRICT, default_loop_scope=None collected 2 items githubnavigator/tests.py .. [100%] diff --git a/examples/miniapps/flask-blueprints/githubnavigator/application.py b/examples/miniapps/flask-blueprints/githubnavigator/application.py index 4b1ae03b..1e489134 100644 --- a/examples/miniapps/flask-blueprints/githubnavigator/application.py +++ b/examples/miniapps/flask-blueprints/githubnavigator/application.py @@ -1,7 +1,7 @@ """Application module.""" from flask import Flask -from flask_bootstrap import Bootstrap +from flask_bootstrap import Bootstrap4 from .containers import Container from .blueprints import example @@ -15,7 +15,7 @@ def create_app() -> Flask: app.container = container app.register_blueprint(example.blueprint) - bootstrap = Bootstrap() + bootstrap = Bootstrap4() bootstrap.init_app(app) return app diff --git a/examples/miniapps/flask/README.rst b/examples/miniapps/flask/README.rst index 93d45a00..c691b2a2 100644 --- a/examples/miniapps/flask/README.rst +++ b/examples/miniapps/flask/README.rst @@ -81,8 +81,9 @@ The output should be something like: .. code-block:: - platform darwin -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 - plugins: cov-3.0.0, flask-1.2.0 + platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0 + plugins: cov-6.0.0, flask-1.3.0 + asyncio: mode=Mode.STRICT, default_loop_scope=None collected 2 items githubnavigator/tests.py .. [100%] diff --git a/examples/miniapps/flask/githubnavigator/application.py b/examples/miniapps/flask/githubnavigator/application.py index 8943c55c..2520d146 100644 --- a/examples/miniapps/flask/githubnavigator/application.py +++ b/examples/miniapps/flask/githubnavigator/application.py @@ -1,7 +1,7 @@ """Application module.""" from flask import Flask -from flask_bootstrap import Bootstrap +from flask_bootstrap import Bootstrap4 from .containers import Container from . import views @@ -15,7 +15,7 @@ def create_app() -> Flask: app.container = container app.add_url_rule("/", "index", views.index) - bootstrap = Bootstrap() + bootstrap = Bootstrap4() bootstrap.init_app(app) return app diff --git a/examples/miniapps/movie-lister/README.rst b/examples/miniapps/movie-lister/README.rst index 1600d9e3..3787e327 100644 --- a/examples/miniapps/movie-lister/README.rst +++ b/examples/miniapps/movie-lister/README.rst @@ -58,8 +58,8 @@ The output should be something like: .. code-block:: - platform darwin -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 - plugins: cov-3.0.0 + platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0 + plugins: cov-6.0.0 collected 2 items movies/tests.py .. [100%] diff --git a/examples/miniapps/sanic/README.rst b/examples/miniapps/sanic/README.rst index d50b8552..cc8ba158 100644 --- a/examples/miniapps/sanic/README.rst +++ b/examples/miniapps/sanic/README.rst @@ -27,7 +27,7 @@ To run the application do: .. code-block:: bash export GIPHY_API_KEY=wBJ2wZG7SRqfrU9nPgPiWvORmloDyuL0 - python -m giphynavigator + sanic giphynavigator.application:create_app The output should be something like: @@ -98,8 +98,9 @@ The output should be something like: .. code-block:: - platform darwin -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 - plugins: sanic-1.9.1, anyio-3.3.4, cov-3.0.0 + platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.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 giphynavigator/tests.py ... [100%] diff --git a/examples/miniapps/sanic/giphynavigator/tests.py b/examples/miniapps/sanic/giphynavigator/tests.py index 097848de..180be8c7 100644 --- a/examples/miniapps/sanic/giphynavigator/tests.py +++ b/examples/miniapps/sanic/giphynavigator/tests.py @@ -8,6 +8,8 @@ from sanic import Sanic from giphynavigator.application import create_app from giphynavigator.giphy import GiphyClient +pytestmark = pytest.mark.asyncio + @pytest.fixture def app(): @@ -17,12 +19,7 @@ def app(): app.ctx.container.unwire() -@pytest.fixture -def test_client(loop, app, sanic_client): - return loop.run_until_complete(sanic_client(app)) - - -async def test_index(app, test_client): +async def test_index(app): giphy_client_mock = mock.AsyncMock(spec=GiphyClient) giphy_client_mock.search.return_value = { "data": [ @@ -32,7 +29,7 @@ async def test_index(app, test_client): } with app.ctx.container.giphy_client.override(giphy_client_mock): - response = await test_client.get( + _, response = await app.asgi_client.get( "/", params={ "query": "test", @@ -41,7 +38,7 @@ async def test_index(app, test_client): ) assert response.status_code == 200 - data = response.json() + data = response.json assert data == { "query": "test", "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.search.return_value = { "data": [], } 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 - data = response.json() + data = response.json 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.search.return_value = { "data": [], } 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 - data = response.json() + data = response.json assert data["query"] == app.ctx.container.config.default.query() assert data["limit"] == app.ctx.container.config.default.limit() diff --git a/examples/miniapps/sanic/requirements.txt b/examples/miniapps/sanic/requirements.txt index 7e4352ab..aaa946c7 100644 --- a/examples/miniapps/sanic/requirements.txt +++ b/examples/miniapps/sanic/requirements.txt @@ -1,6 +1,6 @@ dependency-injector -sanic<=21.6 +sanic +sanic-testing aiohttp pyyaml -pytest-sanic pytest-cov From aa56b70dc8c1d50908024660030453894fbc7f2d Mon Sep 17 00:00:00 2001 From: "JC (Jonathan Chen)" Date: Mon, 9 Dec 2024 03:54:30 -0500 Subject: [PATCH 03/25] docs: fix grammar (#709) --- docs/providers/singleton.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/providers/singleton.rst b/docs/providers/singleton.rst index c0f2cc5d..5c2d517f 100644 --- a/docs/providers/singleton.rst +++ b/docs/providers/singleton.rst @@ -24,7 +24,7 @@ returns it on the rest of the calls. .. 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. Specialization of the provided type and abstract singletons work the same like like for the From 3ba4704bc1cb00310749fd2eda0c8221167c313c Mon Sep 17 00:00:00 2001 From: ZipFile Date: Sat, 14 Dec 2024 13:03:57 +0000 Subject: [PATCH 04/25] Remove six --- pyproject.toml | 1 - requirements.txt | 1 - src/dependency_injector/containers.pyx | 35 ++++++++++++-------------- tox.ini | 1 - 4 files changed, 16 insertions(+), 22 deletions(-) delete mode 100644 requirements.txt diff --git a/pyproject.toml b/pyproject.toml index d0b0f7f5..eba17764 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,6 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", ] dynamic = ["version"] -dependencies = ["six"] [project.optional-dependencies] yaml = ["pyyaml"] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 806e29ae..00000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -six>=1.7.0,<=1.16.0 diff --git a/src/dependency_injector/containers.pyx b/src/dependency_injector/containers.pyx index 988c3e16..7e922773 100644 --- a/src/dependency_injector/containers.pyx +++ b/src/dependency_injector/containers.pyx @@ -18,8 +18,6 @@ try: except ImportError: yaml = None -import six - from . import providers, errors from .providers cimport __is_future_or_coroutine @@ -201,7 +199,7 @@ class DynamicContainer(Container): :rtype: None """ - for name, provider in six.iteritems(providers): + for name, provider in providers.items(): setattr(self, name, provider) def set_provider(self, name, provider): @@ -234,7 +232,7 @@ class DynamicContainer(Container): self.overridden += (overriding,) - for name, provider in six.iteritems(overriding.providers): + for name, provider in overriding.providers.items(): try: getattr(self, name).override(provider) except AttributeError: @@ -250,7 +248,7 @@ class DynamicContainer(Container): :rtype: None """ 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.override(overriding_provider) overridden_providers.append(container_provider) @@ -266,7 +264,7 @@ class DynamicContainer(Container): self.overridden = self.overridden[:-1] - for provider in six.itervalues(self.providers): + for provider in self.providers.values(): provider.reset_last_overriding() def reset_override(self): @@ -276,7 +274,7 @@ class DynamicContainer(Container): """ self.overridden = tuple() - for provider in six.itervalues(self.providers): + for provider in self.providers.values(): provider.reset_override() def is_auto_wiring_enabled(self): @@ -495,13 +493,13 @@ class DeclarativeContainerMetaClass(type): containers = { name: container - for name, container in six.iteritems(attributes) + for name, container in attributes.items() if is_container(container) } cls_providers = { 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) } @@ -509,7 +507,7 @@ class DeclarativeContainerMetaClass(type): name: provider for base in bases 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 = {} @@ -536,10 +534,10 @@ class DeclarativeContainerMetaClass(type): self.set_container(cls) cls.__self__ = self - for provider in six.itervalues(cls.providers): + for provider in cls.providers.values(): _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): provider.assign_parent(cls) @@ -641,8 +639,7 @@ class DeclarativeContainerMetaClass(type): return self -@six.add_metaclass(DeclarativeContainerMetaClass) -class DeclarativeContainer(Container): +class DeclarativeContainer(Container, metaclass=DeclarativeContainerMetaClass): """Declarative inversion of control container. .. code-block:: python @@ -767,7 +764,7 @@ class DeclarativeContainer(Container): cls.overridden += (overriding,) - for name, provider in six.iteritems(overriding.cls_providers): + for name, provider in overriding.cls_providers.items(): try: getattr(cls, name).override(provider) except AttributeError: @@ -784,7 +781,7 @@ class DeclarativeContainer(Container): cls.overridden = cls.overridden[:-1] - for provider in six.itervalues(cls.providers): + for provider in cls.providers.values(): provider.reset_last_overriding() @classmethod @@ -795,7 +792,7 @@ class DeclarativeContainer(Container): """ cls.overridden = tuple() - for provider in six.itervalues(cls.providers): + for provider in cls.providers.values(): provider.reset_override() @@ -858,7 +855,7 @@ def copy(object base_container): """ def _get_memo_for_matching_names(new_providers, base_providers): 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: continue 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(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) return new_container diff --git a/tox.ini b/tox.ini index ed02f0e9..54f99e57 100644 --- a/tox.ini +++ b/tox.ini @@ -35,7 +35,6 @@ deps = v2: pydantic-settings pytest pytest-asyncio - -rrequirements.txt typing_extensions httpx fastapi From d82d9fb8222acc91960947d01ef310f4d5aa2b63 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Wed, 1 Jan 2025 21:22:29 +0200 Subject: [PATCH 05/25] Improve debugability of deepcopy errors (#839) --- src/dependency_injector/errors.py | 21 ++++++ src/dependency_injector/providers.pyi | 16 ++++- src/dependency_injector/providers.pyx | 65 +++++++++++++++---- .../unit/providers/utils/test_deepcopy_py3.py | 65 +++++++++++++++++++ 4 files changed, 155 insertions(+), 12 deletions(-) create mode 100644 tests/unit/providers/utils/test_deepcopy_py3.py diff --git a/src/dependency_injector/errors.py b/src/dependency_injector/errors.py index 7b11862e..407313ce 100644 --- a/src/dependency_injector/errors.py +++ b/src/dependency_injector/errors.py @@ -10,3 +10,24 @@ class Error(Exception): class NoSuchProviderError(Error, AttributeError): """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}" diff --git a/src/dependency_injector/providers.pyi b/src/dependency_injector/providers.pyi index 83d6ca88..b7fbf211 100644 --- a/src/dependency_injector/providers.pyi +++ b/src/dependency_injector/providers.pyi @@ -530,7 +530,21 @@ def is_delegated(instance: Any) -> bool: ... 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]: ... diff --git a/src/dependency_injector/providers.pyx b/src/dependency_injector/providers.pyx index 2db9fa2f..84c1fad7 100644 --- a/src/dependency_injector/providers.pyx +++ b/src/dependency_injector/providers.pyx @@ -71,6 +71,7 @@ except ImportError: from .errors import ( Error, NoSuchProviderError, + NonCopyableArgumentError, ) cimport cython @@ -1252,8 +1253,8 @@ cdef class Callable(Provider): copied = _memorized_duplicate(self, memo) copied.set_provides(_copy_if_provider(self.provides, memo)) - copied.set_args(*deepcopy(self.args, memo)) - copied.set_kwargs(**deepcopy(self.kwargs, memo)) + copied.set_args(*deepcopy_args(self, self.args, memo)) + copied.set_kwargs(**deepcopy_kwargs(self, self.kwargs, memo)) self._copy_overridings(copied, memo) return copied @@ -2539,8 +2540,8 @@ cdef class Factory(Provider): copied = _memorized_duplicate(self, memo) copied.set_provides(_copy_if_provider(self.provides, memo)) - copied.set_args(*deepcopy(self.args, memo)) - copied.set_kwargs(**deepcopy(self.kwargs, memo)) + copied.set_args(*deepcopy_args(self, self.args, memo)) + copied.set_kwargs(**deepcopy_kwargs(self, self.kwargs, memo)) copied.set_attributes(**deepcopy(self.attributes, memo)) self._copy_overridings(copied, memo) return copied @@ -2838,8 +2839,8 @@ cdef class BaseSingleton(Provider): copied = _memorized_duplicate(self, memo) copied.set_provides(_copy_if_provider(self.provides, memo)) - copied.set_args(*deepcopy(self.args, memo)) - copied.set_kwargs(**deepcopy(self.kwargs, memo)) + copied.set_args(*deepcopy_args(self, self.args, memo)) + copied.set_kwargs(**deepcopy_kwargs(self, self.kwargs, memo)) copied.set_attributes(**deepcopy(self.attributes, memo)) self._copy_overridings(copied, memo) return copied @@ -3451,7 +3452,7 @@ cdef class List(Provider): return copied 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) return copied @@ -3674,8 +3675,8 @@ cdef class Resource(Provider): copied = _memorized_duplicate(self, memo) copied.set_provides(_copy_if_provider(self.provides, memo)) - copied.set_args(*deepcopy(self.args, memo)) - copied.set_kwargs(**deepcopy(self.kwargs, memo)) + copied.set_args(*deepcopy_args(self, self.args, memo)) + copied.set_kwargs(**deepcopy_kwargs(self, self.kwargs, memo)) self._copy_overridings(copied, memo) @@ -4525,8 +4526,8 @@ cdef class MethodCaller(Provider): copied = _memorized_duplicate(self, memo) copied.set_provides(_copy_if_provider(self.provides, memo)) - copied.set_args(*deepcopy(self.args, memo)) - copied.set_kwargs(**deepcopy(self.kwargs, memo)) + copied.set_args(*deepcopy_args(self, self.args, memo)) + copied.set_kwargs(**deepcopy_kwargs(self, self.kwargs, memo)) self._copy_overridings(copied, memo) return copied @@ -4927,6 +4928,48 @@ cpdef object deepcopy(object instance, dict memo=None): 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): """Add system streams to memo dictionary. diff --git a/tests/unit/providers/utils/test_deepcopy_py3.py b/tests/unit/providers/utils/test_deepcopy_py3.py new file mode 100644 index 00000000..57f7a7da --- /dev/null +++ b/tests/unit/providers/utils/test_deepcopy_py3.py @@ -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) From f9db578c5976d7d24b114c8f951742417abd2025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Trebu=C5=88a?= <47665799+gortibaldik@users.noreply.github.com> Date: Sun, 5 Jan 2025 20:33:09 +0100 Subject: [PATCH 06/25] :art: Raise exception instead of hiding it in finally (#845) --- src/dependency_injector/providers.pyx | 4 ++-- .../test_thread_local_singleton_py3.py | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 tests/unit/providers/singleton/test_thread_local_singleton_py3.py diff --git a/src/dependency_injector/providers.pyx b/src/dependency_injector/providers.pyx index 84c1fad7..39716ea0 100644 --- a/src/dependency_injector/providers.pyx +++ b/src/dependency_injector/providers.pyx @@ -3222,8 +3222,8 @@ cdef class ThreadLocalSingleton(BaseSingleton): return future_result self._storage.instance = instance - finally: - return instance + + return instance def _async_init_instance(self, future_result, result): try: diff --git a/tests/unit/providers/singleton/test_thread_local_singleton_py3.py b/tests/unit/providers/singleton/test_thread_local_singleton_py3.py new file mode 100644 index 00000000..fb0a3638 --- /dev/null +++ b/tests/unit/providers/singleton/test_thread_local_singleton_py3.py @@ -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() From 41e18dfa900d3208fdb0b3959612e25380354cb5 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Sun, 5 Jan 2025 21:39:26 +0200 Subject: [PATCH 07/25] Add Starlette lifespan handler implementation (#683) --- .../miniapps/starlette-lifespan/README.rst | 39 ++++++++++++ .../miniapps/starlette-lifespan/example.py | 59 +++++++++++++++++++ .../starlette-lifespan/requirements.txt | 3 + src/dependency_injector/ext/starlette.py | 53 +++++++++++++++++ tests/unit/ext/test_starlette.py | 41 +++++++++++++ 5 files changed, 195 insertions(+) create mode 100644 examples/miniapps/starlette-lifespan/README.rst create mode 100755 examples/miniapps/starlette-lifespan/example.py create mode 100644 examples/miniapps/starlette-lifespan/requirements.txt create mode 100644 src/dependency_injector/ext/starlette.py create mode 100644 tests/unit/ext/test_starlette.py diff --git a/examples/miniapps/starlette-lifespan/README.rst b/examples/miniapps/starlette-lifespan/README.rst new file mode 100644 index 00000000..c6d1b2b4 --- /dev/null +++ b/examples/miniapps/starlette-lifespan/README.rst @@ -0,0 +1,39 @@ +Integration With Starlette-based Frameworks +=========================================== + +This is a `Starlette `_ + +`Dependency Injector `_ example application +utilizing `lifespan API `_. + +.. note:: + + Pretty much `any framework built on top of Starlette `_ + supports this feature (`FastAPI `_, + `Xpresso `_, 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). diff --git a/examples/miniapps/starlette-lifespan/example.py b/examples/miniapps/starlette-lifespan/example.py new file mode 100755 index 00000000..11a31e61 --- /dev/null +++ b/examples/miniapps/starlette-lifespan/example.py @@ -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, + ) diff --git a/examples/miniapps/starlette-lifespan/requirements.txt b/examples/miniapps/starlette-lifespan/requirements.txt new file mode 100644 index 00000000..966c2bb8 --- /dev/null +++ b/examples/miniapps/starlette-lifespan/requirements.txt @@ -0,0 +1,3 @@ +dependency-injector +starlette +uvicorn diff --git a/src/dependency_injector/ext/starlette.py b/src/dependency_injector/ext/starlette.py new file mode 100644 index 00000000..9498a3d9 --- /dev/null +++ b/src/dependency_injector/ext/starlette.py @@ -0,0 +1,53 @@ +import sys +from abc import ABCMeta, abstractmethod +from typing import Any, Callable, Coroutine, Optional + +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 diff --git a/tests/unit/ext/test_starlette.py b/tests/unit/ext/test_starlette.py new file mode 100644 index 00000000..e569a382 --- /dev/null +++ b/tests/unit/ext/test_starlette.py @@ -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 From 9f4e2839d250d06084465ebafa27cce3bac1d0c9 Mon Sep 17 00:00:00 2001 From: Roman Mogylatov Date: Sun, 5 Jan 2025 14:57:55 -0500 Subject: [PATCH 08/25] Remove unused imports from the starlette extension (#846) --- src/dependency_injector/ext/starlette.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/dependency_injector/ext/starlette.py b/src/dependency_injector/ext/starlette.py index 9498a3d9..becadf0a 100644 --- a/src/dependency_injector/ext/starlette.py +++ b/src/dependency_injector/ext/starlette.py @@ -1,6 +1,5 @@ import sys -from abc import ABCMeta, abstractmethod -from typing import Any, Callable, Coroutine, Optional +from typing import Any if sys.version_info >= (3, 11): # pragma: no cover from typing import Self From 9f38db6ef33b05bc90f2bf850e2de92604fc096d Mon Sep 17 00:00:00 2001 From: Roman Mogylatov Date: Sun, 5 Jan 2025 15:19:57 -0500 Subject: [PATCH 09/25] Bump version to 4.45.0 --- docs/main/changelog.rst | 15 +++++++++++++++ src/dependency_injector/__init__.py | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/main/changelog.rst b/docs/main/changelog.rst index 961fe54a..54a0e8eb 100644 --- a/docs/main/changelog.rst +++ b/docs/main/changelog.rst @@ -7,6 +7,21 @@ that were made in every particular version. From version 0.7.6 *Dependency Injector* framework strictly follows `Semantic versioning`_ +4.45.0 +-------- +- Add Starlette lifespan handler implementation (`#683 `_). +- Raise exception in ``ThreadLocalSingleton`` instead of hiding it in finally (`#845 `_). +- Improve debuggability of ``deepcopy`` errors (`#839 `_). +- Update examples (`#838 `_). +- Upgrade testing dependencies (`#837 `_). +- Add minor fixes to the documentation (`#709 `_). +- Remove ``six`` from the dependencies (`3ba4704 `_). + +Many thanks for the contributions to: +- `ZipFile `_ +- `František Trebuňa `_ +- `JC (Jonathan Chen) `_ + 4.44.0 -------- - Implement support for Pydantic 2. PR: `#832 `_. diff --git a/src/dependency_injector/__init__.py b/src/dependency_injector/__init__.py index d7b2baf4..14e3c273 100644 --- a/src/dependency_injector/__init__.py +++ b/src/dependency_injector/__init__.py @@ -1,6 +1,6 @@ """Top-level package.""" -__version__ = "4.44.0" +__version__ = "4.45.0" """Version number. :type: str From 00326e9a22b13ab1ce3ed60a85dc2d6c3dfeac1a Mon Sep 17 00:00:00 2001 From: Philip Bjorge Date: Wed, 8 Jan 2025 03:31:00 -0800 Subject: [PATCH 10/25] fix: type propogation through provided (#733) Co-authored-by: Gonzalo Martinez --- src/dependency_injector/providers.pyi | 2 +- tests/typing/callable.py | 2 +- tests/typing/dict.py | 2 +- tests/typing/factory.py | 2 +- tests/typing/list.py | 2 +- tests/typing/object.py | 2 +- tests/typing/provider.py | 2 +- tests/typing/singleton.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/dependency_injector/providers.pyi b/src/dependency_injector/providers.pyi index b7fbf211..85347084 100644 --- a/src/dependency_injector/providers.pyi +++ b/src/dependency_injector/providers.pyi @@ -69,7 +69,7 @@ class Provider(Generic[T]): @property def provider(self) -> Provider: ... @property - def provided(self) -> ProvidedInstance: ... + def provided(self) -> ProvidedInstance[T]: ... def enable_async_mode(self) -> None: ... def disable_async_mode(self) -> None: ... def reset_async_mode(self) -> None: ... diff --git a/tests/typing/callable.py b/tests/typing/callable.py index 51c8d3a7..09a5a3f8 100644 --- a/tests/typing/callable.py +++ b/tests/typing/callable.py @@ -34,7 +34,7 @@ kwargs4: Dict[str, Any] = provider4.kwargs # Test 5: to check the provided instance interface provider5 = providers.Callable(Animal) -provided5: providers.ProvidedInstance = provider5.provided +provided5: Animal = provider5.provided() attr_getter5: providers.AttributeGetter = provider5.provided.attr item_getter5: providers.ItemGetter = provider5.provided["item"] method_caller: providers.MethodCaller = provider5.provided.method.call(123, arg=324) diff --git a/tests/typing/dict.py b/tests/typing/dict.py index 31205067..f676b67d 100644 --- a/tests/typing/dict.py +++ b/tests/typing/dict.py @@ -34,7 +34,7 @@ provider5 = providers.Dict( a1=providers.Factory(object), a2=providers.Factory(object), ) -provided5: providers.ProvidedInstance = provider5.provided +provided5: dict[Any, Any] = provider5.provided() # Test 6: to check the return type with await diff --git a/tests/typing/factory.py b/tests/typing/factory.py index 132a4c29..089bef07 100644 --- a/tests/typing/factory.py +++ b/tests/typing/factory.py @@ -37,7 +37,7 @@ attributes4: Dict[str, Any] = provider4.attributes # Test 5: to check the provided instance interface provider5 = providers.Factory(Animal) -provided5: providers.ProvidedInstance = provider5.provided +provided5: Animal = provider5.provided() attr_getter5: providers.AttributeGetter = provider5.provided.attr item_getter5: providers.ItemGetter = provider5.provided["item"] method_caller5: providers.MethodCaller = provider5.provided.method.call(123, arg=324) diff --git a/tests/typing/list.py b/tests/typing/list.py index 3ceae7cc..d29baadb 100644 --- a/tests/typing/list.py +++ b/tests/typing/list.py @@ -23,7 +23,7 @@ provider3 = providers.List( providers.Factory(object), providers.Factory(object), ) -provided3: providers.ProvidedInstance = provider3.provided +provided3: List[Any] = provider3.provided() attr_getter3: providers.AttributeGetter = provider3.provided.attr item_getter3: providers.ItemGetter = provider3.provided["item"] method_caller3: providers.MethodCaller = provider3.provided.method.call(123, arg=324) diff --git a/tests/typing/object.py b/tests/typing/object.py index b099c83b..103071ae 100644 --- a/tests/typing/object.py +++ b/tests/typing/object.py @@ -9,7 +9,7 @@ var1: int = provider1() # Test 2: to check the provided instance interface provider2 = providers.Object(int) -provided2: providers.ProvidedInstance = provider2.provided +provided2: Type[int] = provider2.provided() attr_getter2: providers.AttributeGetter = provider2.provided.attr item_getter2: providers.ItemGetter = provider2.provided["item"] method_caller2: providers.MethodCaller = provider2.provided.method.call(123, arg=324) diff --git a/tests/typing/provider.py b/tests/typing/provider.py index 46dd23a0..6ceed96a 100644 --- a/tests/typing/provider.py +++ b/tests/typing/provider.py @@ -3,7 +3,7 @@ from dependency_injector import providers # Test 1: to check .provided attribute provider1: providers.Provider[int] = providers.Object(1) -provided: providers.ProvidedInstance = provider1.provided +provided: int = provider1.provided() # Test 2: to check async mode API provider2: providers.Provider = providers.Provider() diff --git a/tests/typing/singleton.py b/tests/typing/singleton.py index badfe1c6..8dc3df23 100644 --- a/tests/typing/singleton.py +++ b/tests/typing/singleton.py @@ -37,7 +37,7 @@ attributes4: Dict[str, Any] = provider4.attributes # Test 5: to check the provided instance interface provider5 = providers.Singleton(Animal) -provided5: providers.ProvidedInstance = provider5.provided +provided5: Animal = provider5.provided() attr_getter5: providers.AttributeGetter = provider5.provided.attr item_getter5: providers.ItemGetter = provider5.provided["item"] method_caller5: providers.MethodCaller = provider5.provided.method.call(123, arg=324) From ccbd5bbb8064c73fe030d2fac2231afc39a1eec3 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Wed, 8 Jan 2025 13:07:04 +0000 Subject: [PATCH 11/25] Migrate CI pipeline to actions/upload-artifact@v4 --- .github/workflows/publishing.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publishing.yml b/.github/workflows/publishing.yml index c3b085fa..5e671f89 100644 --- a/.github/workflows/publishing.yml +++ b/.github/workflows/publishing.yml @@ -49,8 +49,9 @@ jobs: - run: | python -m pip install --upgrade build python -m build --sdist - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: + name: cibw-sdist path: ./dist/* build-wheels: @@ -66,8 +67,9 @@ jobs: - uses: actions/checkout@v3 - name: Build wheels uses: pypa/cibuildwheel@v2.20.0 - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: + name: cibw-wheels-x86-${{ matrix.os }}-${{ strategy.job-index }} path: ./wheelhouse/*.whl build-wheels-linux-aarch64: @@ -85,8 +87,9 @@ jobs: uses: pypa/cibuildwheel@v2.20.0 env: CIBW_ARCHS_LINUX: aarch64 - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: + name: cibw-wheels-arm-${{ matrix.os }}-${{ strategy.job-index }} path: ./wheelhouse/*.whl publish: @@ -94,10 +97,11 @@ jobs: needs: [build-sdist, build-wheels, build-wheels-linux-aarch64] runs-on: ubuntu-22.04 steps: - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: - name: artifact + pattern: cibw-* path: dist + merge-multiple: true - uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ From de50666a13abaf464e6ad143b270359813598477 Mon Sep 17 00:00:00 2001 From: Philip Bjorge Date: Sun, 12 Jan 2025 04:14:12 -0800 Subject: [PATCH 12/25] fix: type provider (#744) --- src/dependency_injector/providers.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dependency_injector/providers.pyi b/src/dependency_injector/providers.pyi index 85347084..7f272208 100644 --- a/src/dependency_injector/providers.pyi +++ b/src/dependency_injector/providers.pyi @@ -67,7 +67,7 @@ class Provider(Generic[T]): def unregister_overrides(self, provider: Union[Provider, Any]) -> None: ... def delegate(self) -> Provider: ... @property - def provider(self) -> Provider: ... + def provider(self) -> Provider[T]: ... @property def provided(self) -> ProvidedInstance[T]: ... def enable_async_mode(self) -> None: ... From 6d9d34c0f6a4fe233e36552079577898c0be9872 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Sun, 12 Jan 2025 12:18:21 +0000 Subject: [PATCH 13/25] Add test case for Provider.provider type propagation --- tests/typing/provider.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/typing/provider.py b/tests/typing/provider.py index 6ceed96a..18afd69a 100644 --- a/tests/typing/provider.py +++ b/tests/typing/provider.py @@ -4,6 +4,7 @@ from dependency_injector import providers # Test 1: to check .provided attribute provider1: providers.Provider[int] = providers.Object(1) provided: int = provider1.provided() +provider1_delegate: providers.Provider[int] = provider1.provider # Test 2: to check async mode API provider2: providers.Provider = providers.Provider() From 3df95847d578db07c27c0a596db71ddc751ca53d Mon Sep 17 00:00:00 2001 From: Ilya Kazakov Date: Sun, 12 Jan 2025 16:46:29 +0300 Subject: [PATCH 14/25] [movie-lister] Added test fixture and updated documentation (#747) * test: add fixture for finder mock * docs: update tests code example, emphasize-lines & test coverage report results --- docs/tutorials/cli.rst | 21 +++++++++---------- .../miniapps/movie-lister/movies/tests.py | 15 +++++++------ 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/docs/tutorials/cli.rst b/docs/tutorials/cli.rst index 8b108e08..88014ff3 100644 --- a/docs/tutorials/cli.rst +++ b/docs/tutorials/cli.rst @@ -911,7 +911,7 @@ Create ``tests.py`` in the ``movies`` package: and put next into it: .. code-block:: python - :emphasize-lines: 36,51 + :emphasize-lines: 41,50 """Tests module.""" @@ -941,13 +941,18 @@ and put next into it: return container - def test_movies_directed_by(container): + @pytest.fixture + def finder_mock(container): finder_mock = mock.Mock() finder_mock.find_all.return_value = [ container.movie("The 33", 2015, "Patricia Riggen"), container.movie("The Jungle Book", 2016, "Jon Favreau"), ] + return finder_mock + + + def test_movies_directed_by(container, finder_mock): with container.finder.override(finder_mock): lister = container.lister() movies = lister.movies_directed_by("Jon Favreau") @@ -956,13 +961,7 @@ and put next into it: assert movies[0].title == "The Jungle Book" - def test_movies_released_in(container): - finder_mock = mock.Mock() - finder_mock.find_all.return_value = [ - container.movie("The 33", 2015, "Patricia Riggen"), - container.movie("The Jungle Book", 2016, "Jon Favreau"), - ] - + def test_movies_released_in(container, finder_mock): with container.finder.override(finder_mock): lister = container.lister() movies = lister.movies_released_in(2015) @@ -995,9 +994,9 @@ You should see: movies/entities.py 7 1 86% movies/finders.py 26 13 50% movies/listers.py 8 0 100% - movies/tests.py 23 0 100% + movies/tests.py 24 0 100% ------------------------------------------ - TOTAL 89 30 66% + TOTAL 90 30 67% .. note:: diff --git a/examples/miniapps/movie-lister/movies/tests.py b/examples/miniapps/movie-lister/movies/tests.py index 1b29d824..1a133a5f 100644 --- a/examples/miniapps/movie-lister/movies/tests.py +++ b/examples/miniapps/movie-lister/movies/tests.py @@ -26,13 +26,18 @@ def container(): return container -def test_movies_directed_by(container): +@pytest.fixture +def finder_mock(container): finder_mock = mock.Mock() finder_mock.find_all.return_value = [ container.movie("The 33", 2015, "Patricia Riggen"), container.movie("The Jungle Book", 2016, "Jon Favreau"), ] + return finder_mock + + +def test_movies_directed_by(container, finder_mock): with container.finder.override(finder_mock): lister = container.lister() movies = lister.movies_directed_by("Jon Favreau") @@ -41,13 +46,7 @@ def test_movies_directed_by(container): assert movies[0].title == "The Jungle Book" -def test_movies_released_in(container): - finder_mock = mock.Mock() - finder_mock.find_all.return_value = [ - container.movie("The 33", 2015, "Patricia Riggen"), - container.movie("The Jungle Book", 2016, "Jon Favreau"), - ] - +def test_movies_released_in(container, finder_mock): with container.finder.override(finder_mock): lister = container.lister() movies = lister.movies_released_in(2015) From 0fd35baee643f16e5a41a8970a2d4ddcb2f16011 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Thu, 16 Jan 2025 19:13:11 +0000 Subject: [PATCH 15/25] Use ubuntu-24.04 GHA image --- .github/workflows/publishing.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/publishing.yml b/.github/workflows/publishing.yml index 5e671f89..21f80009 100644 --- a/.github/workflows/publishing.yml +++ b/.github/workflows/publishing.yml @@ -10,7 +10,7 @@ jobs: tests: name: Run tests - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 @@ -23,7 +23,7 @@ jobs: linters: name: Run linters - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: toxenv: [flake8, pydocstyle, mypy, pylint] @@ -40,7 +40,7 @@ jobs: build-sdist: name: Build source tarball needs: [tests, linters] - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 @@ -60,7 +60,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-22.04, windows-2019, macos-14] + os: [ubuntu-24.04, windows-2019, macos-14] env: CIBW_SKIP: cp27-* steps: @@ -73,9 +73,9 @@ jobs: path: ./wheelhouse/*.whl build-wheels-linux-aarch64: - name: Build wheels (ubuntu-22.04-aarch64) + name: Build wheels (ubuntu-24.04-aarch64) needs: [tests, linters] - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 env: CIBW_SKIP: cp27-* steps: @@ -95,7 +95,7 @@ jobs: publish: name: Publish on PyPI needs: [build-sdist, build-wheels, build-wheels-linux-aarch64] - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/download-artifact@v4 with: @@ -113,7 +113,7 @@ jobs: publish-docs: name: Publish docs needs: [publish] - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 From 3893e1df818cee9f0db98b8a5916bd1ca266c398 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Thu, 16 Jan 2025 19:15:33 +0000 Subject: [PATCH 16/25] Use native GHA ubuntu-24.04-arm image for building wheels --- .github/workflows/publishing.yml | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/.github/workflows/publishing.yml b/.github/workflows/publishing.yml index 21f80009..f3bf1529 100644 --- a/.github/workflows/publishing.yml +++ b/.github/workflows/publishing.yml @@ -60,7 +60,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-24.04, windows-2019, macos-14] + os: [ubuntu-24.04, ubuntu-24.04-arm, windows-2019, macos-14] env: CIBW_SKIP: cp27-* steps: @@ -72,29 +72,9 @@ jobs: name: cibw-wheels-x86-${{ matrix.os }}-${{ strategy.job-index }} path: ./wheelhouse/*.whl - build-wheels-linux-aarch64: - name: Build wheels (ubuntu-24.04-aarch64) - needs: [tests, linters] - runs-on: ubuntu-24.04 - env: - CIBW_SKIP: cp27-* - steps: - - uses: actions/checkout@v3 - - name: Set up QEMU - if: runner.os == 'Linux' - uses: docker/setup-qemu-action@v2 - - name: Build wheels - uses: pypa/cibuildwheel@v2.20.0 - env: - CIBW_ARCHS_LINUX: aarch64 - - uses: actions/upload-artifact@v4 - with: - name: cibw-wheels-arm-${{ matrix.os }}-${{ strategy.job-index }} - path: ./wheelhouse/*.whl - publish: name: Publish on PyPI - needs: [build-sdist, build-wheels, build-wheels-linux-aarch64] + needs: [build-sdist, build-wheels] runs-on: ubuntu-24.04 steps: - uses: actions/download-artifact@v4 From 50643e0dfb84f63407af984ab4e7c88782a8a56d Mon Sep 17 00:00:00 2001 From: ZipFile Date: Sat, 18 Jan 2025 17:02:55 +0000 Subject: [PATCH 17/25] Run black --- src/dependency_injector/containers.pyi | 47 +-- src/dependency_injector/ext/aiohttp.py | 4 + src/dependency_injector/ext/aiohttp.pyi | 9 - src/dependency_injector/ext/flask.py | 10 +- src/dependency_injector/ext/flask.pyi | 11 +- src/dependency_injector/providers.pyi | 364 ++++++++++++++---------- src/dependency_injector/resources.py | 12 +- src/dependency_injector/schema.py | 68 +++-- src/dependency_injector/wiring.py | 254 +++++++++-------- 9 files changed, 439 insertions(+), 340 deletions(-) diff --git a/src/dependency_injector/containers.pyi b/src/dependency_injector/containers.pyi index e773bfb3..4b40fbba 100644 --- a/src/dependency_injector/containers.pyi +++ b/src/dependency_injector/containers.pyi @@ -19,21 +19,24 @@ from typing import ( from .providers import Provider, Self, ProviderParent - C_Base = TypeVar("C_Base", bound="Container") C = TypeVar("C", bound="DeclarativeContainer") C_Overriding = TypeVar("C_Overriding", bound="DeclarativeContainer") T = TypeVar("T") TT = TypeVar("TT") - class WiringConfiguration: modules: List[Any] packages: List[Any] from_package: Optional[str] auto_wire: bool - def __init__(self, modules: Optional[Iterable[Any]] = None, packages: Optional[Iterable[Any]] = None, from_package: Optional[str] = None, auto_wire: bool = True) -> None: ... - + def __init__( + self, + modules: Optional[Iterable[Any]] = None, + packages: Optional[Iterable[Any]] = None, + from_package: Optional[str] = None, + auto_wire: bool = True, + ) -> None: ... class Container: provider_type: Type[Provider] = Provider @@ -51,11 +54,18 @@ class Container: def set_providers(self, **providers: Provider): ... def set_provider(self, name: str, provider: Provider) -> None: ... def override(self, overriding: Union[Container, Type[Container]]) -> None: ... - def override_providers(self, **overriding_providers: Union[Provider, Any]) -> ProvidersOverridingContext[C_Base]: ... + def override_providers( + self, **overriding_providers: Union[Provider, Any] + ) -> ProvidersOverridingContext[C_Base]: ... def reset_last_overriding(self) -> None: ... def reset_override(self) -> None: ... def is_auto_wiring_enabled(self) -> bool: ... - def wire(self, modules: Optional[Iterable[Any]] = None, packages: Optional[Iterable[Any]] = None, from_package: Optional[str] = None) -> None: ... + def wire( + self, + modules: Optional[Iterable[Any]] = None, + packages: Optional[Iterable[Any]] = None, + from_package: Optional[str] = None, + ) -> None: ... def unwire(self) -> None: ... def init_resources(self) -> Optional[Awaitable]: ... def shutdown_resources(self) -> Optional[Awaitable]: ... @@ -64,7 +74,9 @@ class Container: def reset_singletons(self) -> SingletonResetContext[C_Base]: ... def check_dependencies(self) -> None: ... def from_schema(self, schema: Dict[Any, Any]) -> None: ... - def from_yaml_schema(self, filepath: Union[Path, str], loader: Optional[Any]=None) -> None: ... + def from_yaml_schema( + self, filepath: Union[Path, str], loader: Optional[Any] = None + ) -> None: ... def from_json_schema(self, filepath: Union[Path, str]) -> None: ... @overload def resolve_provider_name(self, provider: Provider) -> str: ... @@ -82,10 +94,8 @@ class Container: @overload def traverse(cls, types: Optional[Iterable[Type[TT]]] = None) -> Iterator[TT]: ... - class DynamicContainer(Container): ... - class DeclarativeContainer(Container): cls_providers: ClassVar[Dict[str, Provider]] inherited_providers: ClassVar[Dict[str, Provider]] @@ -93,29 +103,28 @@ class DeclarativeContainer(Container): @classmethod def override(cls, overriding: Union[Container, Type[Container]]) -> None: ... @classmethod - def override_providers(cls, **overriding_providers: Union[Provider, Any]) -> ProvidersOverridingContext[C_Base]: ... + def override_providers( + cls, **overriding_providers: Union[Provider, Any] + ) -> ProvidersOverridingContext[C_Base]: ... @classmethod def reset_last_overriding(cls) -> None: ... @classmethod def reset_override(cls) -> None: ... - class ProvidersOverridingContext(Generic[T]): - def __init__(self, container: T, overridden_providers: Iterable[Union[Provider, Any]]) -> None: ... + def __init__( + self, container: T, overridden_providers: Iterable[Union[Provider, Any]] + ) -> None: ... def __enter__(self) -> T: ... def __exit__(self, *_: Any) -> None: ... - class SingletonResetContext(Generic[T]): def __init__(self, container: T): ... def __enter__(self) -> T: ... def __exit__(self, *_: Any) -> None: ... - -def override(container: Type[C]) -> _Callable[[Type[C_Overriding]], Type[C_Overriding]]: ... - - +def override( + container: Type[C], +) -> _Callable[[Type[C_Overriding]], Type[C_Overriding]]: ... def copy(container: Type[C]) -> _Callable[[Type[C_Overriding]], Type[C_Overriding]]: ... - - def is_container(instance: Any) -> bool: ... diff --git a/src/dependency_injector/ext/aiohttp.py b/src/dependency_injector/ext/aiohttp.py index b132f362..976089c3 100644 --- a/src/dependency_injector/ext/aiohttp.py +++ b/src/dependency_injector/ext/aiohttp.py @@ -38,9 +38,11 @@ class View(providers.Callable): def as_view(self): """Return aiohttp view function.""" + @functools.wraps(self.provides) async def _view(request, *args, **kwargs): return await self.__call__(request, *args, **kwargs) + return _view @@ -49,6 +51,8 @@ class ClassBasedView(providers.Factory): def as_view(self): """Return aiohttp view function.""" + async def _view(request, *args, **kwargs): return await self.__call__(request, *args, **kwargs) + return _view diff --git a/src/dependency_injector/ext/aiohttp.pyi b/src/dependency_injector/ext/aiohttp.pyi index 05c68acc..370cc9b0 100644 --- a/src/dependency_injector/ext/aiohttp.pyi +++ b/src/dependency_injector/ext/aiohttp.pyi @@ -2,22 +2,13 @@ from typing import Awaitable as _Awaitable from dependency_injector import providers - class Application(providers.Singleton): ... - - class Extension(providers.Singleton): ... - - class Middleware(providers.DelegatedCallable): ... - - class MiddlewareFactory(providers.Factory): ... - class View(providers.Callable): def as_view(self) -> _Awaitable: ... - class ClassBasedView(providers.Factory): def as_view(self) -> _Awaitable: ... diff --git a/src/dependency_injector/ext/flask.py b/src/dependency_injector/ext/flask.py index b30b8797..498a9eee 100644 --- a/src/dependency_injector/ext/flask.py +++ b/src/dependency_injector/ext/flask.py @@ -45,6 +45,7 @@ class ClassBasedView(providers.Factory): def as_view(provider, name=None): """Transform class-based view provider to view function.""" if isinstance(provider, providers.Factory): + def view(*args, **kwargs): self = provider() return self.dispatch_request(*args, **kwargs) @@ -52,12 +53,13 @@ def as_view(provider, name=None): assert name, 'Argument "endpoint" is required for class-based views' view.__name__ = name elif isinstance(provider, providers.Callable): + def view(*args, **kwargs): return provider(*args, **kwargs) view.__name__ = provider.provides.__name__ else: - raise errors.Error('Undefined provider type') + raise errors.Error("Undefined provider type") view.__doc__ = provider.provides.__doc__ view.__module__ = provider.provides.__module__ @@ -65,14 +67,14 @@ def as_view(provider, name=None): if isinstance(provider.provides, type): view.view_class = provider.provides - if hasattr(provider.provides, 'decorators'): + if hasattr(provider.provides, "decorators"): for decorator in provider.provides.decorators: view = decorator(view) - if hasattr(provider.provides, 'methods'): + if hasattr(provider.provides, "methods"): view.methods = provider.provides.methods - if hasattr(provider.provides, 'provide_automatic_options'): + if hasattr(provider.provides, "provide_automatic_options"): view.provide_automatic_options = provider.provides.provide_automatic_options return view diff --git a/src/dependency_injector/ext/flask.pyi b/src/dependency_injector/ext/flask.pyi index 37bffd36..9b180c89 100644 --- a/src/dependency_injector/ext/flask.pyi +++ b/src/dependency_injector/ext/flask.pyi @@ -3,22 +3,17 @@ from typing import Union, Optional, Callable as _Callable, Any from flask import request as flask_request from dependency_injector import providers - request: providers.Object[flask_request] - class Application(providers.Singleton): ... - - class Extension(providers.Singleton): ... - class View(providers.Callable): def as_view(self) -> _Callable[..., Any]: ... - class ClassBasedView(providers.Factory): def as_view(self, name: str) -> _Callable[..., Any]: ... - -def as_view(provider: Union[View, ClassBasedView], name: Optional[str] = None) -> _Callable[..., Any]: ... +def as_view( + provider: Union[View, ClassBasedView], name: Optional[str] = None +) -> _Callable[..., Any]: ... diff --git a/src/dependency_injector/providers.pyi b/src/dependency_injector/providers.pyi index 7f272208..32534043 100644 --- a/src/dependency_injector/providers.pyi +++ b/src/dependency_injector/providers.pyi @@ -33,7 +33,6 @@ except ImportError: from . import resources - Injection = Any ProviderParent = Union["Provider", Any] T = TypeVar("T") @@ -41,16 +40,13 @@ TT = TypeVar("TT") P = TypeVar("P", bound="Provider") BS = TypeVar("BS", bound="BaseSingleton") - class Provider(Generic[T]): def __init__(self) -> None: ... - @overload def __call__(self, *args: Injection, **kwargs: Injection) -> T: ... @overload def __call__(self, *args: Injection, **kwargs: Injection) -> Awaitable[T]: ... def async_(self, *args: Injection, **kwargs: Injection) -> Awaitable[T]: ... - def __deepcopy__(self, memo: Optional[_Dict[Any, Any]]) -> Provider: ... def __str__(self) -> str: ... def __repr__(self) -> str: ... @@ -78,9 +74,12 @@ class Provider(Generic[T]): def is_async_mode_undefined(self) -> bool: ... @property def related(self) -> _Iterator[Provider]: ... - def traverse(self, types: Optional[_Iterable[Type[TT]]] = None) -> _Iterator[TT]: ... - def _copy_overridings(self, copied: Provider, memo: Optional[_Dict[Any, Any]]) -> None: ... - + def traverse( + self, types: Optional[_Iterable[Type[TT]]] = None + ) -> _Iterator[TT]: ... + def _copy_overridings( + self, copied: Provider, memo: Optional[_Dict[Any, Any]] + ) -> None: ... class Object(Provider[T]): def __init__(self, provides: Optional[T] = None) -> None: ... @@ -88,7 +87,6 @@ class Object(Provider[T]): def provides(self) -> Optional[T]: ... def set_provides(self, provides: Optional[T]) -> Object: ... - class Self(Provider[T]): def __init__(self, container: Optional[T] = None) -> None: ... def set_container(self, container: T) -> None: ... @@ -96,41 +94,51 @@ class Self(Provider[T]): @property def alt_names(self) -> Tuple[Any]: ... - class Delegate(Provider[Provider]): def __init__(self, provides: Optional[Provider] = None) -> None: ... @property def provides(self) -> Optional[Provider]: ... def set_provides(self, provides: Optional[Provider]) -> Delegate: ... - class Aggregate(Provider[T]): - def __init__(self, provider_dict: Optional[_Dict[Any, Provider[T]]] = None, **provider_kwargs: Provider[T]): ... + def __init__( + self, + provider_dict: Optional[_Dict[Any, Provider[T]]] = None, + **provider_kwargs: Provider[T], + ): ... def __getattr__(self, provider_name: Any) -> Provider[T]: ... - @overload - def __call__(self, provider_name: Optional[Any] = None, *args: Injection, **kwargs: Injection) -> T: ... + def __call__( + self, provider_name: Optional[Any] = None, *args: Injection, **kwargs: Injection + ) -> T: ... @overload - def __call__(self, provider_name: Optional[Any] = None, *args: Injection, **kwargs: Injection) -> Awaitable[T]: ... - def async_(self, provider_name: Optional[Any] = None, *args: Injection, **kwargs: Injection) -> Awaitable[T]: ... - + def __call__( + self, provider_name: Optional[Any] = None, *args: Injection, **kwargs: Injection + ) -> Awaitable[T]: ... + def async_( + self, provider_name: Optional[Any] = None, *args: Injection, **kwargs: Injection + ) -> Awaitable[T]: ... @property def providers(self) -> _Dict[Any, Provider[T]]: ... - def set_providers(self, provider_dict: Optional[_Dict[Any, Provider[T]]] = None, **provider_kwargs: Provider[T]) -> Aggregate[T]: ... - + def set_providers( + self, + provider_dict: Optional[_Dict[Any, Provider[T]]] = None, + **provider_kwargs: Provider[T], + ) -> Aggregate[T]: ... class Dependency(Provider[T]): - def __init__(self, instance_of: Type[T] = object, default: Optional[Union[Provider, Any]] = None) -> None: ... + def __init__( + self, + instance_of: Type[T] = object, + default: Optional[Union[Provider, Any]] = None, + ) -> None: ... def __getattr__(self, name: str) -> Any: ... - @property def instance_of(self) -> Type[T]: ... def set_instance_of(self, instance_of: Type[T]) -> Dependency[T]: ... - @property def default(self) -> Provider[T]: ... def set_default(self, default: Optional[Union[Provider, Any]]) -> Dependency[T]: ... - @property def is_defined(self) -> bool: ... def provided_by(self, provider: Provider) -> OverridingContext[P]: ... @@ -140,10 +148,8 @@ class Dependency(Provider[T]): def parent_name(self) -> Optional[str]: ... def assign_parent(self, parent: ProviderParent) -> None: ... - class ExternalDependency(Dependency[T]): ... - class DependenciesContainer(Object): def __init__(self, **dependencies: Provider) -> None: ... def __getattr__(self, name: str) -> Provider: ... @@ -156,12 +162,18 @@ class DependenciesContainer(Object): def parent_name(self) -> Optional[str]: ... def assign_parent(self, parent: ProviderParent) -> None: ... - class Callable(Provider[T]): - def __init__(self, provides: Optional[Union[_Callable[..., T], str]] = None, *args: Injection, **kwargs: Injection) -> None: ... + def __init__( + self, + provides: Optional[Union[_Callable[..., T], str]] = None, + *args: Injection, + **kwargs: Injection, + ) -> None: ... @property def provides(self) -> Optional[_Callable[..., T]]: ... - def set_provides(self, provides: Optional[Union[_Callable[..., T], str]]) -> Callable[T]: ... + def set_provides( + self, provides: Optional[Union[_Callable[..., T], str]] + ) -> Callable[T]: ... @property def args(self) -> Tuple[Injection]: ... def add_args(self, *args: Injection) -> Callable[T]: ... @@ -173,32 +185,23 @@ class Callable(Provider[T]): def set_kwargs(self, **kwargs: Injection) -> Callable[T]: ... def clear_kwargs(self) -> Callable[T]: ... - class DelegatedCallable(Callable[T]): ... - class AbstractCallable(Callable[T]): def override(self, provider: Callable) -> OverridingContext[P]: ... - class CallableDelegate(Delegate): def __init__(self, callable: Callable) -> None: ... - class Coroutine(Callable[T]): ... - - class DelegatedCoroutine(Coroutine[T]): ... - class AbstractCoroutine(Coroutine[T]): def override(self, provider: Coroutine) -> OverridingContext[P]: ... - class CoroutineDelegate(Delegate): def __init__(self, coroutine: Coroutine) -> None: ... - class ConfigurationOption(Provider[Any]): UNDEFINED: object def __init__(self, name: Tuple[str], root: Configuration) -> None: ... @@ -212,89 +215,137 @@ class ConfigurationOption(Provider[Any]): 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 as_( + self, callback: _Callable[..., T], *args: Injection, **kwargs: Injection + ) -> TypedConfigurationOption[T]: ... def required(self) -> ConfigurationOption: ... def is_required(self) -> bool: ... def update(self, value: Any) -> None: ... - def from_ini(self, filepath: Union[Path, str], required: bool = False, envs_required: bool = False) -> None: ... - def from_yaml(self, filepath: Union[Path, str], required: bool = False, loader: Optional[Any] = None, envs_required: bool = False) -> None: ... - def from_json(self, filepath: Union[Path, str], required: bool = False, envs_required: bool = False) -> None: ... - def from_pydantic(self, settings: PydanticSettings, required: bool = False, **kwargs: Any) -> None: ... + def from_ini( + self, + filepath: Union[Path, str], + required: bool = False, + envs_required: bool = False, + ) -> None: ... + def from_yaml( + self, + filepath: Union[Path, str], + required: bool = False, + loader: Optional[Any] = None, + envs_required: bool = False, + ) -> None: ... + def from_json( + self, + filepath: Union[Path, str], + required: bool = False, + envs_required: bool = False, + ) -> None: ... + def from_pydantic( + self, settings: PydanticSettings, required: bool = False, **kwargs: Any + ) -> None: ... def from_dict(self, options: _Dict[str, Any], required: bool = False) -> None: ... - def from_env(self, name: str, default: Optional[Any] = None, required: bool = False, as_: Optional[_Callable[..., Any]] = None) -> None: ... + def from_env( + self, + name: str, + default: Optional[Any] = None, + required: bool = False, + as_: Optional[_Callable[..., Any]] = None, + ) -> None: ... def from_value(self, value: Any) -> None: ... - class TypedConfigurationOption(Callable[T]): @property def option(self) -> ConfigurationOption: ... - class Configuration(Object[Any]): DEFAULT_NAME: str = "config" def __init__( - self, - name: str = DEFAULT_NAME, - default: Optional[Any] = None, - *, - strict: bool = False, - ini_files: Optional[_Iterable[Union[Path, str]]] = None, - yaml_files: Optional[_Iterable[Union[Path, str]]] = None, - json_files: Optional[_Iterable[Union[Path, str]]] = None, - pydantic_settings: Optional[_Iterable[PydanticSettings]] = None, + self, + name: str = DEFAULT_NAME, + default: Optional[Any] = None, + *, + strict: bool = False, + ini_files: Optional[_Iterable[Union[Path, str]]] = None, + yaml_files: Optional[_Iterable[Union[Path, str]]] = None, + json_files: Optional[_Iterable[Union[Path, str]]] = None, + pydantic_settings: Optional[_Iterable[PydanticSettings]] = None, ) -> None: ... - def __enter__(self) -> Configuration : ... + def __enter__(self) -> Configuration: ... def __exit__(self, *exc_info: Any) -> None: ... def __getattr__(self, item: str) -> ConfigurationOption: ... def __getitem__(self, item: Union[str, Provider]) -> ConfigurationOption: ... - def get_name(self) -> str: ... def set_name(self, name: str) -> Configuration: ... - def get_default(self) -> _Dict[Any, Any]: ... def set_default(self, default: _Dict[Any, Any]): ... - def get_strict(self) -> bool: ... def set_strict(self, strict: bool) -> Configuration: ... - def get_children(self) -> _Dict[str, ConfigurationOption]: ... - def set_children(self, children: _Dict[str, ConfigurationOption]) -> Configuration: ... - + def set_children( + self, children: _Dict[str, ConfigurationOption] + ) -> Configuration: ... def get_ini_files(self) -> _List[Union[Path, str]]: ... def set_ini_files(self, files: _Iterable[Union[Path, str]]) -> Configuration: ... - def get_yaml_files(self) -> _List[Union[Path, str]]: ... def set_yaml_files(self, files: _Iterable[Union[Path, str]]) -> Configuration: ... - def get_json_files(self) -> _List[Union[Path, str]]: ... def set_json_files(self, files: _Iterable[Union[Path, str]]) -> Configuration: ... - def get_pydantic_settings(self) -> _List[PydanticSettings]: ... - def set_pydantic_settings(self, settings: _Iterable[PydanticSettings]) -> Configuration: ... - + def set_pydantic_settings( + self, settings: _Iterable[PydanticSettings] + ) -> Configuration: ... def load(self, required: bool = False, envs_required: bool = False) -> None: ... - def get(self, selector: str) -> Any: ... def set(self, selector: str, value: Any) -> OverridingContext[P]: ... def reset_cache(self) -> None: ... def update(self, value: Any) -> None: ... - def from_ini(self, filepath: Union[Path, str], required: bool = False, envs_required: bool = False) -> None: ... - def from_yaml(self, filepath: Union[Path, str], required: bool = False, loader: Optional[Any] = None, envs_required: bool = False) -> None: ... - def from_json(self, filepath: Union[Path, str], required: bool = False, envs_required: bool = False) -> None: ... - def from_pydantic(self, settings: PydanticSettings, required: bool = False, **kwargs: Any) -> None: ... + def from_ini( + self, + filepath: Union[Path, str], + required: bool = False, + envs_required: bool = False, + ) -> None: ... + def from_yaml( + self, + filepath: Union[Path, str], + required: bool = False, + loader: Optional[Any] = None, + envs_required: bool = False, + ) -> None: ... + def from_json( + self, + filepath: Union[Path, str], + required: bool = False, + envs_required: bool = False, + ) -> None: ... + def from_pydantic( + self, settings: PydanticSettings, required: bool = False, **kwargs: Any + ) -> None: ... def from_dict(self, options: _Dict[str, Any], required: bool = False) -> None: ... - def from_env(self, name: str, default: Optional[Any] = None, required: bool = False, as_: Optional[_Callable[..., Any]] = None) -> None: ... + def from_env( + self, + name: str, + default: Optional[Any] = None, + required: bool = False, + as_: Optional[_Callable[..., Any]] = None, + ) -> None: ... def from_value(self, value: Any) -> None: ... - class Factory(Provider[T]): provided_type: Optional[Type] - def __init__(self, provides: Optional[Union[_Callable[..., T], str]] = None, *args: Injection, **kwargs: Injection) -> None: ... + def __init__( + self, + provides: Optional[Union[_Callable[..., T], str]] = None, + *args: Injection, + **kwargs: Injection, + ) -> None: ... @property def cls(self) -> Type[T]: ... @property def provides(self) -> Optional[_Callable[..., T]]: ... - def set_provides(self, provides: Optional[Union[_Callable[..., T], str]]) -> Factory[T]: ... + def set_provides( + self, provides: Optional[Union[_Callable[..., T], str]] + ) -> Factory[T]: ... @property def args(self) -> Tuple[Injection]: ... def add_args(self, *args: Injection) -> Factory[T]: ... @@ -311,33 +362,39 @@ class Factory(Provider[T]): def set_attributes(self, **kwargs: Injection) -> Factory[T]: ... def clear_attributes(self) -> Factory[T]: ... - class DelegatedFactory(Factory[T]): ... - class AbstractFactory(Factory[T]): def override(self, provider: Factory) -> OverridingContext[P]: ... - class FactoryDelegate(Delegate): def __init__(self, factory: Factory): ... - class FactoryAggregate(Aggregate[T]): def __getattr__(self, provider_name: Any) -> Factory[T]: ... @property def factories(self) -> _Dict[Any, Factory[T]]: ... - def set_factories(self, provider_dict: Optional[_Dict[Any, Factory[T]]] = None, **provider_kwargs: Factory[T]) -> FactoryAggregate[T]: ... - + def set_factories( + self, + provider_dict: Optional[_Dict[Any, Factory[T]]] = None, + **provider_kwargs: Factory[T], + ) -> FactoryAggregate[T]: ... class BaseSingleton(Provider[T]): provided_type = Optional[Type] - def __init__(self, provides: Optional[Union[_Callable[..., T], str]] = None, *args: Injection, **kwargs: Injection) -> None: ... + def __init__( + self, + provides: Optional[Union[_Callable[..., T], str]] = None, + *args: Injection, + **kwargs: Injection, + ) -> None: ... @property def cls(self) -> Type[T]: ... @property def provides(self) -> Optional[_Callable[..., T]]: ... - def set_provides(self, provides: Optional[Union[_Callable[..., T], str]]) -> BaseSingleton[T]: ... + def set_provides( + self, provides: Optional[Union[_Callable[..., T], str]] + ) -> BaseSingleton[T]: ... @property def args(self) -> Tuple[Injection]: ... def add_args(self, *args: Injection) -> BaseSingleton[T]: ... @@ -356,36 +413,20 @@ class BaseSingleton(Provider[T]): def reset(self) -> SingletonResetContext[BS]: ... def full_reset(self) -> SingletonFullResetContext[BS]: ... - class Singleton(BaseSingleton[T]): ... - - class DelegatedSingleton(Singleton[T]): ... - - class ThreadSafeSingleton(Singleton[T]): ... - - class DelegatedThreadSafeSingleton(ThreadSafeSingleton[T]): ... - - class ThreadLocalSingleton(BaseSingleton[T]): ... - - class ContextLocalSingleton(BaseSingleton[T]): ... - - class DelegatedThreadLocalSingleton(ThreadLocalSingleton[T]): ... - class AbstractSingleton(BaseSingleton[T]): def override(self, provider: BaseSingleton) -> OverridingContext[P]: ... - class SingletonDelegate(Delegate): def __init__(self, singleton: BaseSingleton): ... - class List(Provider[_List]): def __init__(self, *args: Injection): ... @property @@ -394,29 +435,63 @@ class List(Provider[_List]): def set_args(self, *args: Injection) -> List[T]: ... def clear_args(self) -> List[T]: ... - class Dict(Provider[_Dict]): - def __init__(self, dict_: Optional[_Dict[Any, Injection]] = None, **kwargs: Injection): ... + def __init__( + self, dict_: Optional[_Dict[Any, Injection]] = None, **kwargs: Injection + ): ... @property def kwargs(self) -> _Dict[Any, Injection]: ... - def add_kwargs(self, dict_: Optional[_Dict[Any, Injection]] = None, **kwargs: Injection) -> Dict: ... - def set_kwargs(self, dict_: Optional[_Dict[Any, Injection]] = None, **kwargs: Injection) -> Dict: ... + def add_kwargs( + self, dict_: Optional[_Dict[Any, Injection]] = None, **kwargs: Injection + ) -> Dict: ... + def set_kwargs( + self, dict_: Optional[_Dict[Any, Injection]] = None, **kwargs: Injection + ) -> Dict: ... def clear_kwargs(self) -> Dict: ... - class Resource(Provider[T]): @overload - def __init__(self, provides: Optional[Type[resources.Resource[T]]] = None, *args: Injection, **kwargs: Injection) -> None: ... + def __init__( + self, + provides: Optional[Type[resources.Resource[T]]] = None, + *args: Injection, + **kwargs: Injection, + ) -> None: ... @overload - def __init__(self, provides: Optional[Type[resources.AsyncResource[T]]] = None, *args: Injection, **kwargs: Injection) -> None: ... + def __init__( + self, + provides: Optional[Type[resources.AsyncResource[T]]] = None, + *args: Injection, + **kwargs: Injection, + ) -> None: ... @overload - def __init__(self, provides: Optional[_Callable[..., _Iterator[T]]] = None, *args: Injection, **kwargs: Injection) -> None: ... + def __init__( + self, + provides: Optional[_Callable[..., _Iterator[T]]] = None, + *args: Injection, + **kwargs: Injection, + ) -> None: ... @overload - def __init__(self, provides: Optional[_Callable[..., _AsyncIterator[T]]] = None, *args: Injection, **kwargs: Injection) -> None: ... + def __init__( + self, + provides: Optional[_Callable[..., _AsyncIterator[T]]] = None, + *args: Injection, + **kwargs: Injection, + ) -> None: ... @overload - def __init__(self, provides: Optional[_Callable[..., _Coroutine[Injection, Injection, T]]] = None, *args: Injection, **kwargs: Injection) -> None: ... + def __init__( + self, + provides: Optional[_Callable[..., _Coroutine[Injection, Injection, T]]] = None, + *args: Injection, + **kwargs: Injection, + ) -> None: ... @overload - def __init__(self, provides: Optional[Union[_Callable[..., T], str]] = None, *args: Injection, **kwargs: Injection) -> None: ... + def __init__( + self, + provides: Optional[Union[_Callable[..., T], str]] = None, + *args: Injection, + **kwargs: Injection, + ) -> None: ... @property def provides(self) -> Optional[_Callable[..., Any]]: ... def set_provides(self, provides: Optional[Any]) -> Resource[T]: ... @@ -435,9 +510,13 @@ class Resource(Provider[T]): def init(self) -> Optional[Awaitable[T]]: ... def shutdown(self) -> Optional[Awaitable]: ... - class Container(Provider[T]): - def __init__(self, container_cls: Type[T], container: Optional[T] = None, **overriding_providers: Union[Provider, Any]) -> None: ... + def __init__( + self, + container_cls: Type[T], + container: Optional[T] = None, + **overriding_providers: Union[Provider, Any], + ) -> None: ... def __getattr__(self, name: str) -> Provider: ... @property def container(self) -> T: ... @@ -448,50 +527,51 @@ class Container(Provider[T]): def parent_name(self) -> Optional[str]: ... def assign_parent(self, parent: ProviderParent) -> None: ... - class Selector(Provider[Any]): - def __init__(self, selector: Optional[_Callable[..., Any]] = None, **providers: Provider): ... + def __init__( + self, selector: Optional[_Callable[..., Any]] = None, **providers: Provider + ): ... def __getattr__(self, name: str) -> Provider: ... - @property def selector(self) -> Optional[_Callable[..., Any]]: ... def set_selector(self, selector: Optional[_Callable[..., Any]]) -> Selector: ... - @property def providers(self) -> _Dict[str, Provider]: ... def set_providers(self, **providers: Provider) -> Selector: ... - class ProvidedInstanceFluentInterface: def __getattr__(self, item: Any) -> AttributeGetter: ... def __getitem__(self, item: Any) -> ItemGetter: ... def call(self, *args: Injection, **kwargs: Injection) -> MethodCaller: ... @property def provides(self) -> Optional[Provider]: ... - def set_provides(self, provides: Optional[Provider]) -> ProvidedInstanceFluentInterface: ... - + def set_provides( + self, provides: Optional[Provider] + ) -> ProvidedInstanceFluentInterface: ... class ProvidedInstance(Provider, ProvidedInstanceFluentInterface): def __init__(self, provides: Optional[Provider] = None) -> None: ... - class AttributeGetter(Provider, ProvidedInstanceFluentInterface): - def __init__(self, provides: Optional[Provider] = None, name: Optional[str] = None) -> None: ... + def __init__( + self, provides: Optional[Provider] = None, name: Optional[str] = None + ) -> None: ... @property def name(self) -> Optional[str]: ... def set_name(self, name: Optional[str]) -> ProvidedInstanceFluentInterface: ... - class ItemGetter(Provider, ProvidedInstanceFluentInterface): - def __init__(self, provides: Optional[Provider] = None, name: Optional[str] = None) -> None: ... + def __init__( + self, provides: Optional[Provider] = None, name: Optional[str] = None + ) -> None: ... @property def name(self) -> Optional[str]: ... def set_name(self, name: Optional[str]) -> ProvidedInstanceFluentInterface: ... - class MethodCaller(Provider, ProvidedInstanceFluentInterface): - def __init__(self, provides: Optional[Provider] = None, *args: Injection, **kwargs: Injection) -> None: ... - + def __init__( + self, provides: Optional[Provider] = None, *args: Injection, **kwargs: Injection + ) -> None: ... class OverridingContext(Generic[T]): def __init__(self, overridden: Provider, overriding: Provider): ... @@ -500,61 +580,39 @@ class OverridingContext(Generic[T]): pass ... - class BaseSingletonResetContext(Generic[T]): def __init__(self, provider: T): ... def __enter__(self) -> T: ... def __exit__(self, *_: Any) -> None: ... - -class SingletonResetContext(BaseSingletonResetContext): - ... - - -class SingletonFullResetContext(BaseSingletonResetContext): - ... - +class SingletonResetContext(BaseSingletonResetContext): ... +class SingletonFullResetContext(BaseSingletonResetContext): ... CHILD_PROVIDERS: Tuple[Provider] - def is_provider(instance: Any) -> bool: ... - - def ensure_is_provider(instance: Any) -> Provider: ... - - def is_delegated(instance: Any) -> bool: ... - - def represent_provider(provider: Provider, provides: Any) -> str: ... - - 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 traverse(*providers: Provider, types: Optional[_Iterable[Type]]=None) -> _Iterator[Provider]: ... - +def traverse( + *providers: Provider, types: Optional[_Iterable[Type]] = None +) -> _Iterator[Provider]: ... if yaml: class YamlLoader(yaml.SafeLoader): ... + else: class YamlLoader: ... diff --git a/src/dependency_injector/resources.py b/src/dependency_injector/resources.py index b5946cfa..7d71d4d8 100644 --- a/src/dependency_injector/resources.py +++ b/src/dependency_injector/resources.py @@ -10,18 +10,14 @@ T = TypeVar("T") class Resource(Generic[T], metaclass=abc.ABCMeta): @abc.abstractmethod - def init(self, *args, **kwargs) -> Optional[T]: - ... + def init(self, *args, **kwargs) -> Optional[T]: ... - def shutdown(self, resource: Optional[T]) -> None: - ... + def shutdown(self, resource: Optional[T]) -> None: ... class AsyncResource(Generic[T], metaclass=abc.ABCMeta): @abc.abstractmethod - async def init(self, *args, **kwargs) -> Optional[T]: - ... + async def init(self, *args, **kwargs) -> Optional[T]: ... - async def shutdown(self, resource: Optional[T]) -> None: - ... + async def shutdown(self, resource: Optional[T]) -> None: ... diff --git a/src/dependency_injector/schema.py b/src/dependency_injector/schema.py index c224e2d1..8547ebc2 100644 --- a/src/dependency_injector/schema.py +++ b/src/dependency_injector/schema.py @@ -27,9 +27,9 @@ class SchemaProcessorV1: return self._container.providers def _create_providers( - self, - provider_schema: ProviderSchema, - container: Optional[containers.Container] = None, + self, + provider_schema: ProviderSchema, + container: Optional[containers.Container] = None, ) -> None: if container is None: container = self._container @@ -57,9 +57,9 @@ class SchemaProcessorV1: self._create_providers(provider_schema=data, container=provider) def _setup_injections( # noqa: C901 - self, - provider_schema: ProviderSchema, - container: Optional[containers.Container] = None, + self, + provider_schema: ProviderSchema, + container: Optional[containers.Container] = None, ) -> None: if container is None: container = self._container @@ -72,7 +72,7 @@ class SchemaProcessorV1: provides = data.get("provides") if provides: if isinstance(provides, str) and provides.startswith("container."): - provides = self._resolve_provider(provides[len("container."):]) + provides = self._resolve_provider(provides[len("container.") :]) else: provides = _import_string(provides) provider.set_provides(provides) @@ -83,7 +83,7 @@ class SchemaProcessorV1: injection = None if isinstance(arg, str) and arg.startswith("container."): - injection = self._resolve_provider(arg[len("container."):]) + injection = self._resolve_provider(arg[len("container.") :]) # TODO: refactoring if isinstance(arg, dict): @@ -91,16 +91,23 @@ class SchemaProcessorV1: provider_type = _get_provider_cls(arg.get("provider")) provides = arg.get("provides") if provides: - if isinstance(provides, str) and provides.startswith("container."): - provides = self._resolve_provider(provides[len("container."):]) + if isinstance(provides, str) and provides.startswith( + "container." + ): + provides = self._resolve_provider( + provides[len("container.") :] + ) else: provides = _import_string(provides) provider_args.append(provides) for provider_arg in arg.get("args", []): - if isinstance(provider_arg, str) \ - and provider_arg.startswith("container."): + if isinstance( + provider_arg, str + ) and provider_arg.startswith("container."): provider_args.append( - self._resolve_provider(provider_arg[len("container."):]), + self._resolve_provider( + provider_arg[len("container.") :] + ), ) injection = provider_type(*provider_args) @@ -117,7 +124,7 @@ class SchemaProcessorV1: injection = None if isinstance(arg, str) and arg.startswith("container."): - injection = self._resolve_provider(arg[len("container."):]) + injection = self._resolve_provider(arg[len("container.") :]) # TODO: refactoring if isinstance(arg, dict): @@ -125,16 +132,23 @@ class SchemaProcessorV1: provider_type = _get_provider_cls(arg.get("provider")) provides = arg.get("provides") if provides: - if isinstance(provides, str) and provides.startswith("container."): - provides = self._resolve_provider(provides[len("container."):]) + if isinstance(provides, str) and provides.startswith( + "container." + ): + provides = self._resolve_provider( + provides[len("container.") :] + ) else: provides = _import_string(provides) provider_args.append(provides) for provider_arg in arg.get("args", []): - if isinstance(provider_arg, str) \ - and provider_arg.startswith("container."): + if isinstance( + provider_arg, str + ) and provider_arg.startswith("container."): provider_args.append( - self._resolve_provider(provider_arg[len("container."):]), + self._resolve_provider( + provider_arg[len("container.") :] + ), ) injection = provider_type(*provider_args) @@ -158,7 +172,7 @@ class SchemaProcessorV1: for segment in segments[1:]: parentheses = "" if "(" in segment and ")" in segment: - parentheses = segment[segment.find("("):segment.rfind(")")+1] + parentheses = segment[segment.find("(") : segment.rfind(")") + 1] segment = segment.replace(parentheses, "") try: @@ -190,10 +204,12 @@ def _get_provider_cls(provider_cls_name: str) -> Type[providers.Provider]: if custom_provider_type: return custom_provider_type - raise SchemaError(f"Undefined provider class \"{provider_cls_name}\"") + raise SchemaError(f'Undefined provider class "{provider_cls_name}"') -def _fetch_provider_cls_from_std(provider_cls_name: str) -> Optional[Type[providers.Provider]]: +def _fetch_provider_cls_from_std( + provider_cls_name: str, +) -> Optional[Type[providers.Provider]]: return getattr(providers, provider_cls_name, None) @@ -201,12 +217,16 @@ def _import_provider_cls(provider_cls_name: str) -> Optional[Type[providers.Prov try: cls = _import_string(provider_cls_name) except (ImportError, ValueError) as exception: - raise SchemaError(f"Can not import provider \"{provider_cls_name}\"") from exception + raise SchemaError( + f'Can not import provider "{provider_cls_name}"' + ) from exception except AttributeError: return None else: if isinstance(cls, type) and not issubclass(cls, providers.Provider): - raise SchemaError(f"Provider class \"{cls}\" is not a subclass of providers base class") + raise SchemaError( + f'Provider class "{cls}" is not a subclass of providers base class' + ) return cls diff --git a/src/dependency_injector/wiring.py b/src/dependency_injector/wiring.py index b1f01622..b5247c9e 100644 --- a/src/dependency_injector/wiring.py +++ b/src/dependency_injector/wiring.py @@ -27,8 +27,9 @@ from typing import ( if sys.version_info < (3, 7): from typing import GenericMeta else: - class GenericMeta(type): - ... + + class GenericMeta(type): ... + # Hotfix, see: https://github.com/ets-labs/python-dependency-injector/issues/362 if sys.version_info >= (3, 9): @@ -99,7 +100,9 @@ class PatchedRegistry: def register_callable(self, patched: "PatchedCallable") -> None: self._callables[patched.patched] = patched - def get_callables_from_module(self, module: ModuleType) -> Iterator[Callable[..., Any]]: + def get_callables_from_module( + self, module: ModuleType + ) -> Iterator[Callable[..., Any]]: for patched_callable in self._callables.values(): if not patched_callable.is_in_module(module): continue @@ -114,7 +117,9 @@ class PatchedRegistry: def register_attribute(self, patched: "PatchedAttribute") -> None: self._attributes.add(patched) - def get_attributes_from_module(self, module: ModuleType) -> Iterator["PatchedAttribute"]: + def get_attributes_from_module( + self, module: ModuleType + ) -> Iterator["PatchedAttribute"]: for attribute in self._attributes: if not attribute.is_in_module(module): continue @@ -139,11 +144,11 @@ class PatchedCallable: ) def __init__( - self, - patched: Optional[Callable[..., Any]] = None, - original: Optional[Callable[..., Any]] = None, - reference_injections: Optional[Dict[Any, Any]] = None, - reference_closing: Optional[Dict[Any, Any]] = None, + self, + patched: Optional[Callable[..., Any]] = None, + original: Optional[Callable[..., Any]] = None, + reference_injections: Optional[Dict[Any, Any]] = None, + reference_closing: Optional[Dict[Any, Any]] = None, ) -> None: self.patched = patched self.original = original @@ -214,18 +219,21 @@ class ProvidersMap: ) def resolve_provider( - self, - provider: Union[providers.Provider, str], - modifier: Optional["Modifier"] = None, + self, + provider: Union[providers.Provider, str], + modifier: Optional["Modifier"] = None, ) -> Optional[providers.Provider]: if isinstance(provider, providers.Delegate): return self._resolve_delegate(provider) - elif isinstance(provider, ( - providers.ProvidedInstance, - providers.AttributeGetter, - providers.ItemGetter, - providers.MethodCaller, - )): + 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) @@ -237,9 +245,9 @@ class ProvidersMap: return self._resolve_provider(provider) def _resolve_string_id( - self, - id: str, - modifier: Optional["Modifier"] = None, + self, + id: str, + modifier: Optional["Modifier"] = None, ) -> Optional[providers.Provider]: if id == self.CONTAINER_STRING_ID: return self._container.__self__ @@ -256,16 +264,19 @@ class ProvidersMap: return provider def _resolve_provided_instance( - self, - original: providers.Provider, + self, + original: providers.Provider, ) -> Optional[providers.Provider]: modifiers = [] - while isinstance(original, ( + while isinstance( + original, + ( providers.ProvidedInstance, providers.AttributeGetter, providers.ItemGetter, providers.MethodCaller, - )): + ), + ): modifiers.insert(0, original) original = original.provides @@ -289,8 +300,8 @@ class ProvidersMap: return new def _resolve_delegate( - self, - original: providers.Delegate, + self, + original: providers.Delegate, ) -> Optional[providers.Provider]: provider = self._resolve_provider(original.provides) if provider: @@ -298,9 +309,9 @@ class ProvidersMap: return provider def _resolve_config_option( - self, - original: providers.ConfigurationOption, - as_: Any = None, + self, + original: providers.ConfigurationOption, + as_: Any = None, ) -> Optional[providers.Provider]: original_root = original.root new = self._resolve_provider(original_root) @@ -324,8 +335,8 @@ class ProvidersMap: return new def _resolve_provider( - self, - original: providers.Provider, + self, + original: providers.Provider, ) -> Optional[providers.Provider]: try: return self._map[original] @@ -334,9 +345,9 @@ class ProvidersMap: @classmethod def _create_providers_map( - cls, - current_container: Container, - original_container: Container, + cls, + current_container: Container, + original_container: Container, ) -> Dict[providers.Provider, providers.Provider]: current_providers = current_container.providers current_providers["__self__"] = current_container.__self__ @@ -349,8 +360,9 @@ class ProvidersMap: original_provider = original_providers[provider_name] providers_map[original_provider] = current_provider - if isinstance(current_provider, providers.Container) \ - and isinstance(original_provider, providers.Container): + if isinstance(current_provider, providers.Container) and isinstance( + original_provider, providers.Container + ): subcontainer_map = cls._create_providers_map( current_container=current_provider.container, original_container=original_provider.container, @@ -376,19 +388,21 @@ class InspectFilter: return werkzeug and isinstance(instance, werkzeug.local.LocalProxy) def _is_starlette_request_cls(self, instance: object) -> bool: - return starlette \ - and isinstance(instance, type) \ - and _safe_is_subclass(instance, starlette.requests.Request) + return ( + starlette + and isinstance(instance, type) + and _safe_is_subclass(instance, starlette.requests.Request) + ) def _is_builtin(self, instance: object) -> bool: return inspect.isbuiltin(instance) def wire( # noqa: C901 - container: Container, - *, - modules: Optional[Iterable[ModuleType]] = None, - packages: Optional[Iterable[ModuleType]] = None, + container: Container, + *, + modules: Optional[Iterable[ModuleType]] = None, + packages: Optional[Iterable[ModuleType]] = None, ) -> None: """Wire container providers with provided packages and modules.""" modules = [*modules] if modules else [] @@ -418,18 +432,22 @@ def wire( # noqa: C901 else: for cls_member_name, cls_member in cls_members: if _is_marker(cls_member): - _patch_attribute(cls, cls_member_name, cls_member, providers_map) + _patch_attribute( + cls, cls_member_name, cls_member, providers_map + ) elif _is_method(cls_member): - _patch_method(cls, cls_member_name, cls_member, providers_map) + _patch_method( + cls, cls_member_name, cls_member, providers_map + ) for patched in _patched_registry.get_callables_from_module(module): _bind_injections(patched, providers_map) def unwire( # noqa: C901 - *, - modules: Optional[Iterable[ModuleType]] = None, - packages: Optional[Iterable[ModuleType]] = None, + *, + modules: Optional[Iterable[ModuleType]] = None, + packages: Optional[Iterable[ModuleType]] = None, ) -> None: """Wire provided packages and modules with previous wired providers.""" modules = [*modules] if modules else [] @@ -443,7 +461,9 @@ def unwire( # noqa: C901 if inspect.isfunction(member): _unpatch(module, name, member) elif inspect.isclass(member): - for method_name, method in inspect.getmembers(member, inspect.isfunction): + for method_name, method in inspect.getmembers( + member, inspect.isfunction + ): _unpatch(member, method_name, method) for patched in _patched_registry.get_callables_from_module(module): @@ -462,10 +482,10 @@ def inject(fn: F) -> F: def _patch_fn( - module: ModuleType, - name: str, - fn: Callable[..., Any], - providers_map: ProvidersMap, + module: ModuleType, + name: str, + fn: Callable[..., Any], + providers_map: ProvidersMap, ) -> None: if not _is_patched(fn): reference_injections, reference_closing = _fetch_reference_injections(fn) @@ -479,14 +499,16 @@ def _patch_fn( def _patch_method( - cls: Type, - name: str, - method: Callable[..., Any], - providers_map: ProvidersMap, + cls: Type, + name: str, + method: Callable[..., Any], + providers_map: ProvidersMap, ) -> None: - if hasattr(cls, "__dict__") \ - and name in cls.__dict__ \ - and isinstance(cls.__dict__[name], (classmethod, staticmethod)): + if ( + hasattr(cls, "__dict__") + and name in cls.__dict__ + and isinstance(cls.__dict__[name], (classmethod, staticmethod)) + ): method = cls.__dict__[name] fn = method.__func__ else: @@ -507,13 +529,15 @@ def _patch_method( def _unpatch( - module: ModuleType, - name: str, - fn: Callable[..., Any], + module: ModuleType, + name: str, + fn: Callable[..., Any], ) -> None: - if hasattr(module, "__dict__") \ - and name in module.__dict__ \ - and isinstance(module.__dict__[name], (classmethod, staticmethod)): + if ( + hasattr(module, "__dict__") + and name in module.__dict__ + and isinstance(module.__dict__[name], (classmethod, staticmethod)) + ): method = module.__dict__[name] fn = method.__func__ @@ -524,10 +548,10 @@ def _unpatch( def _patch_attribute( - member: Any, - name: str, - marker: "_Marker", - providers_map: ProvidersMap, + member: Any, + name: str, + marker: "_Marker", + providers_map: ProvidersMap, ) -> None: provider = providers_map.resolve_provider(marker.provider, marker.modifier) if provider is None: @@ -549,15 +573,14 @@ def _unpatch_attribute(patched: PatchedAttribute) -> None: def _fetch_reference_injections( # noqa: C901 - fn: Callable[..., Any], + fn: Callable[..., Any], ) -> Tuple[Dict[str, Any], Dict[str, Any]]: # Hotfix, see: # - https://github.com/ets-labs/python-dependency-injector/issues/362 # - https://github.com/ets-labs/python-dependency-injector/issues/398 - if GenericAlias and any(( - fn is GenericAlias, - getattr(fn, "__func__", None) is GenericAlias - )): + if GenericAlias and any( + (fn is GenericAlias, getattr(fn, "__func__", None) is GenericAlias) + ): fn = fn.__init__ try: @@ -573,8 +596,9 @@ def _fetch_reference_injections( # noqa: C901 injections = {} closing = {} for parameter_name, parameter in signature.parameters.items(): - if not isinstance(parameter.default, _Marker) \ - and not _is_fastapi_depends(parameter.default): + if not isinstance(parameter.default, _Marker) and not _is_fastapi_depends( + parameter.default + ): continue marker = parameter.default @@ -593,7 +617,9 @@ def _fetch_reference_injections( # noqa: C901 return injections, closing -def _locate_dependent_closing_args(provider: providers.Provider) -> Dict[str, providers.Provider]: +def _locate_dependent_closing_args( + provider: providers.Provider, +) -> Dict[str, providers.Provider]: if not hasattr(provider, "args"): return {} @@ -647,8 +673,8 @@ def _fetch_modules(package): if not hasattr(package, "__path__") or not hasattr(package, "__name__"): return modules for module_info in pkgutil.walk_packages( - path=package.__path__, - prefix=package.__name__ + ".", + path=package.__path__, + prefix=package.__name__ + ".", ): module = importlib.import_module(module_info.name) modules.append(module) @@ -664,9 +690,9 @@ def _is_marker(member) -> bool: def _get_patched( - fn: F, - reference_injections: Dict[Any, Any], - reference_closing: Dict[Any, Any], + fn: F, + reference_injections: Dict[Any, Any], + reference_closing: Dict[Any, Any], ) -> F: patched_object = PatchedCallable( original=fn, @@ -694,9 +720,11 @@ def _is_patched(fn) -> bool: def _is_declarative_container(instance: Any) -> bool: - return (isinstance(instance, type) - and getattr(instance, "__IS_CONTAINER__", False) is True - and getattr(instance, "declarative_parent", None) is None) + return ( + isinstance(instance, type) + and getattr(instance, "__IS_CONTAINER__", False) is True + and getattr(instance, "declarative_parent", None) is None + ) def _safe_is_subclass(instance: Any, cls: Type) -> bool: @@ -709,11 +737,10 @@ def _safe_is_subclass(instance: Any, cls: Type) -> bool: class Modifier: def modify( - self, - provider: providers.ConfigurationOption, - providers_map: ProvidersMap, - ) -> providers.Provider: - ... + self, + provider: providers.ConfigurationOption, + providers_map: ProvidersMap, + ) -> providers.Provider: ... class TypeModifier(Modifier): @@ -722,9 +749,9 @@ class TypeModifier(Modifier): self.type_ = type_ def modify( - self, - provider: providers.ConfigurationOption, - providers_map: ProvidersMap, + self, + provider: providers.ConfigurationOption, + providers_map: ProvidersMap, ) -> providers.Provider: return provider.as_(self.type_) @@ -762,9 +789,9 @@ class RequiredModifier(Modifier): return self def modify( - self, - provider: providers.ConfigurationOption, - providers_map: ProvidersMap, + self, + provider: providers.ConfigurationOption, + providers_map: ProvidersMap, ) -> providers.Provider: provider = provider.required() if self.type_modifier: @@ -783,9 +810,9 @@ class InvariantModifier(Modifier): self.id = id def modify( - self, - provider: providers.ConfigurationOption, - providers_map: ProvidersMap, + self, + provider: providers.ConfigurationOption, + providers_map: ProvidersMap, ) -> providers.Provider: invariant_segment = providers_map.resolve_provider(self.id) return provider[invariant_segment] @@ -818,9 +845,9 @@ class ProvidedInstance(Modifier): return self def modify( - self, - provider: providers.Provider, - providers_map: ProvidersMap, + self, + provider: providers.Provider, + providers_map: ProvidersMap, ) -> providers.Provider: provider = provider.provided for type_, value in self.segments: @@ -851,9 +878,9 @@ class _Marker(Generic[T], metaclass=ClassGetItemMeta): __IS_MARKER__ = True def __init__( - self, - provider: Union[providers.Provider, Container, str], - modifier: Optional[Modifier] = None, + self, + provider: Union[providers.Provider, Container, str], + modifier: Optional[Modifier] = None, ) -> None: if _is_declarative_container(provider): provider = provider.__self__ @@ -869,16 +896,13 @@ class _Marker(Generic[T], metaclass=ClassGetItemMeta): return self -class Provide(_Marker): - ... +class Provide(_Marker): ... -class Provider(_Marker): - ... +class Provider(_Marker): ... -class Closing(_Marker): - ... +class Closing(_Marker): ... class AutoLoader: @@ -928,8 +952,7 @@ class AutoLoader: super().exec_module(module) loader.wire_module(module) - class ExtensionFileLoader(importlib.machinery.ExtensionFileLoader): - ... + class ExtensionFileLoader(importlib.machinery.ExtensionFileLoader): ... loader_details = [ (SourcelessFileLoader, importlib.machinery.BYTECODE_SUFFIXES), @@ -998,4 +1021,5 @@ def _get_async_patched(fn: F, patched: PatchedCallable) -> F: patched.injections, patched.closing, ) + return _patched From 29ae3e1337f5e056a7d5372cebcad4a78dbe998c Mon Sep 17 00:00:00 2001 From: ZipFile Date: Sat, 18 Jan 2025 17:39:07 +0000 Subject: [PATCH 18/25] Make flake8 config black-compatible --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index cb18dacf..9bb1e56b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,6 +2,7 @@ max_line_length = 120 max_complexity = 10 exclude = types.py +extend-ignore = E203,E701 per-file-ignores = examples/demo/*: F841 examples/containers/traverse.py: E501 From 2330122de666f025f72fffdd9e54d79844c88917 Mon Sep 17 00:00:00 2001 From: Taein Min Date: Tue, 21 Jan 2025 00:37:28 +0900 Subject: [PATCH 19/25] Add support for typing.Annotated (#721) --- examples/wiring/example.py | 15 +++++--- requirements-dev.txt | 1 + src/dependency_injector/wiring.py | 47 +++++++++++++++++++------ tests/unit/samples/wiringfastapi/web.py | 17 ++++++--- tests/unit/samples/wiringflask/web.py | 9 +++++ tests/unit/wiring/test_fastapi_py36.py | 25 ++++++++++--- tests/unit/wiring/test_flask_py36.py | 32 ++++++++++++----- 7 files changed, 115 insertions(+), 31 deletions(-) diff --git a/examples/wiring/example.py b/examples/wiring/example.py index 4221ab13..0e32b192 100644 --- a/examples/wiring/example.py +++ b/examples/wiring/example.py @@ -2,10 +2,10 @@ from dependency_injector import containers, providers from dependency_injector.wiring import Provide, inject +from typing import Annotated -class Service: - ... +class Service: ... class Container(containers.DeclarativeContainer): @@ -13,9 +13,16 @@ class Container(containers.DeclarativeContainer): service = providers.Factory(Service) +# You can place marker on parameter default value @inject -def main(service: Service = Provide[Container.service]) -> None: - ... +def main(service: Service = Provide[Container.service]) -> None: ... + + +# Also, you can place marker with typing.Annotated +@inject +def main_with_annotated( + service: Annotated[Service, Provide[Container.service]] +) -> None: ... if __name__ == "__main__": diff --git a/requirements-dev.txt b/requirements-dev.txt index bc533741..0d759d4e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -18,5 +18,6 @@ numpy scipy boto3 mypy_boto3_s3 +typing_extensions -r requirements-ext.txt diff --git a/src/dependency_injector/wiring.py b/src/dependency_injector/wiring.py index b5247c9e..67d56f29 100644 --- a/src/dependency_injector/wiring.py +++ b/src/dependency_injector/wiring.py @@ -37,6 +37,21 @@ if sys.version_info >= (3, 9): else: GenericAlias = None +if sys.version_info >= (3, 9): + from typing import Annotated, get_args, get_origin +else: + try: + from typing_extensions import Annotated, get_args, get_origin + except ImportError: + Annotated = object() + + # For preventing NameError. Never executes + def get_args(hint): + return () + + def get_origin(tp): + return None + try: import fastapi.params @@ -572,6 +587,24 @@ def _unpatch_attribute(patched: PatchedAttribute) -> None: setattr(patched.member, patched.name, patched.marker) +def _extract_marker(parameter: inspect.Parameter) -> Optional["_Marker"]: + if get_origin(parameter.annotation) is Annotated: + marker = get_args(parameter.annotation)[1] + else: + marker = parameter.default + + if not isinstance(marker, _Marker) and not _is_fastapi_depends(marker): + return None + + if _is_fastapi_depends(marker): + marker = marker.dependency + + if not isinstance(marker, _Marker): + return None + + return marker + + def _fetch_reference_injections( # noqa: C901 fn: Callable[..., Any], ) -> Tuple[Dict[str, Any], Dict[str, Any]]: @@ -596,19 +629,11 @@ def _fetch_reference_injections( # noqa: C901 injections = {} closing = {} for parameter_name, parameter in signature.parameters.items(): - if not isinstance(parameter.default, _Marker) and not _is_fastapi_depends( - parameter.default - ): + marker = _extract_marker(parameter) + + if marker is None: continue - marker = parameter.default - - if _is_fastapi_depends(marker): - marker = marker.dependency - - if not isinstance(marker, _Marker): - continue - if isinstance(marker, Closing): marker = marker.provider closing[parameter_name] = marker diff --git a/tests/unit/samples/wiringfastapi/web.py b/tests/unit/samples/wiringfastapi/web.py index 3cee5450..c1ed5102 100644 --- a/tests/unit/samples/wiringfastapi/web.py +++ b/tests/unit/samples/wiringfastapi/web.py @@ -1,7 +1,11 @@ import sys +from typing_extensions import Annotated + from fastapi import FastAPI, Depends -from fastapi import Request # See: https://github.com/ets-labs/python-dependency-injector/issues/398 +from fastapi import ( + Request, +) # See: https://github.com/ets-labs/python-dependency-injector/issues/398 from fastapi.security import HTTPBasic, HTTPBasicCredentials from dependency_injector import containers, providers from dependency_injector.wiring import inject, Provide @@ -28,11 +32,16 @@ async def index(service: Service = Depends(Provide[Container.service])): return {"result": result} +@app.api_route("/annotated") +@inject +async def annotated(service: Annotated[Service, Depends(Provide[Container.service])]): + result = await service.process() + return {"result": result} + + @app.get("/auth") @inject -def read_current_user( - credentials: HTTPBasicCredentials = Depends(security) -): +def read_current_user(credentials: HTTPBasicCredentials = Depends(security)): return {"username": credentials.username, "password": credentials.password} diff --git a/tests/unit/samples/wiringflask/web.py b/tests/unit/samples/wiringflask/web.py index f273d8aa..8bb44494 100644 --- a/tests/unit/samples/wiringflask/web.py +++ b/tests/unit/samples/wiringflask/web.py @@ -1,3 +1,5 @@ +from typing_extensions import Annotated + from flask import Flask, jsonify, request, current_app, session, g from dependency_injector import containers, providers from dependency_injector.wiring import inject, Provide @@ -26,5 +28,12 @@ def index(service: Service = Provide[Container.service]): return jsonify({"result": result}) +@app.route("/annotated") +@inject +def annotated(service: Annotated[Service, Provide[Container.service]]): + result = service.process() + return jsonify({"result": result}) + + container = Container() container.wire(modules=[__name__]) diff --git a/tests/unit/wiring/test_fastapi_py36.py b/tests/unit/wiring/test_fastapi_py36.py index 1e9ff584..491c991c 100644 --- a/tests/unit/wiring/test_fastapi_py36.py +++ b/tests/unit/wiring/test_fastapi_py36.py @@ -4,13 +4,17 @@ from pytest_asyncio import fixture as aio_fixture # Runtime import to avoid syntax errors in samples on Python < 3.5 and reach top-dir import os + _SAMPLES_DIR = os.path.abspath( - os.path.sep.join(( - os.path.dirname(__file__), - "../samples/", - )), + os.path.sep.join( + ( + os.path.dirname(__file__), + "../samples/", + ) + ), ) import sys + sys.path.append(_SAMPLES_DIR) @@ -37,6 +41,19 @@ async def test_depends_marker_injection(async_client: AsyncClient): assert response.json() == {"result": "Foo"} +@mark.asyncio +async def test_depends_with_annotated(async_client: AsyncClient): + class ServiceMock: + async def process(self): + return "Foo" + + with web.container.service.override(ServiceMock()): + response = await async_client.get("/") + + assert response.status_code == 200 + assert response.json() == {"result": "Foo"} + + @mark.asyncio async def test_depends_injection(async_client: AsyncClient): response = await async_client.get("/auth", auth=("john_smith", "secret")) diff --git a/tests/unit/wiring/test_flask_py36.py b/tests/unit/wiring/test_flask_py36.py index 751f04d8..97420275 100644 --- a/tests/unit/wiring/test_flask_py36.py +++ b/tests/unit/wiring/test_flask_py36.py @@ -2,19 +2,25 @@ import json # Runtime import to avoid syntax errors in samples on Python < 3.5 and reach top-dir import os + _TOP_DIR = os.path.abspath( - os.path.sep.join(( - os.path.dirname(__file__), - "../", - )), + os.path.sep.join( + ( + os.path.dirname(__file__), + "../", + ) + ), ) _SAMPLES_DIR = os.path.abspath( - os.path.sep.join(( - os.path.dirname(__file__), - "../samples/", - )), + os.path.sep.join( + ( + os.path.dirname(__file__), + "../samples/", + ) + ), ) import sys + sys.path.append(_TOP_DIR) sys.path.append(_SAMPLES_DIR) @@ -29,3 +35,13 @@ def test_wiring_with_flask(): assert response.status_code == 200 assert json.loads(response.data) == {"result": "OK"} + + +def test_wiring_with_annotated(): + client = web.app.test_client() + + with web.app.app_context(): + response = client.get("/annotated") + + assert response.status_code == 200 + assert json.loads(response.data) == {"result": "OK"} From 0d6fdb5b78ae7c961e3e1f5d466f9a04b7456ba6 Mon Sep 17 00:00:00 2001 From: Martin Lafrance <20482569+martlaf@users.noreply.github.com> Date: Sun, 23 Feb 2025 11:17:45 -0500 Subject: [PATCH 20/25] Fix broken wiring of sync inject-decorated methods (#673) Co-authored-by: Martin Lafrance Co-authored-by: ZipFile --- src/dependency_injector/_cwiring.pyx | 49 +++++++++----------- src/dependency_injector/wiring.py | 17 ++++++- tests/unit/wiring/test_introspection_py36.py | 7 +++ 3 files changed, 44 insertions(+), 29 deletions(-) diff --git a/src/dependency_injector/_cwiring.pyx b/src/dependency_injector/_cwiring.pyx index 88b6bc5a..84a5485f 100644 --- a/src/dependency_injector/_cwiring.pyx +++ b/src/dependency_injector/_cwiring.pyx @@ -2,44 +2,39 @@ import asyncio import collections.abc -import functools import inspect import types -from . import providers -from .wiring import _Marker, PatchedCallable +from .wiring import _Marker -from .providers cimport Provider +from .providers cimport Provider, Resource -def _get_sync_patched(fn, patched: PatchedCallable): - @functools.wraps(fn) - def _patched(*args, **kwargs): - cdef object result - cdef dict to_inject - cdef object arg_key - cdef Provider provider +def _sync_inject(object fn, tuple args, dict kwargs, dict injections, dict closings, /): + cdef object result + cdef dict to_inject + cdef object arg_key + cdef Provider provider - to_inject = kwargs.copy() - for arg_key, provider in patched.injections.items(): - if arg_key not in kwargs or isinstance(kwargs[arg_key], _Marker): - to_inject[arg_key] = provider() + to_inject = kwargs.copy() + for arg_key, provider in injections.items(): + if arg_key not in kwargs or isinstance(kwargs[arg_key], _Marker): + to_inject[arg_key] = provider() - result = fn(*args, **to_inject) + result = fn(*args, **to_inject) - if patched.closing: - for arg_key, provider in patched.closing.items(): - if arg_key in kwargs and not isinstance(kwargs[arg_key], _Marker): - continue - if not isinstance(provider, providers.Resource): - continue - provider.shutdown() + if closings: + for arg_key, provider in closings.items(): + if arg_key in kwargs and not isinstance(kwargs[arg_key], _Marker): + continue + if not isinstance(provider, Resource): + continue + provider.shutdown() - return result - return _patched + return result -async def _async_inject(object fn, tuple args, dict kwargs, dict injections, dict closings): +async def _async_inject(object fn, tuple args, dict kwargs, dict injections, dict closings, /): cdef object result cdef dict to_inject cdef list to_inject_await = [] @@ -69,7 +64,7 @@ async def _async_inject(object fn, tuple args, dict kwargs, dict injections, dic for arg_key, provider in closings.items(): if arg_key in kwargs and isinstance(kwargs[arg_key], _Marker): continue - if not isinstance(provider, providers.Resource): + if not isinstance(provider, Resource): continue shutdown = provider.shutdown() if _isawaitable(shutdown): diff --git a/src/dependency_injector/wiring.py b/src/dependency_injector/wiring.py index 67d56f29..9bb990ab 100644 --- a/src/dependency_injector/wiring.py +++ b/src/dependency_injector/wiring.py @@ -1030,7 +1030,7 @@ _inspect_filter = InspectFilter() _loader = AutoLoader() # Optimizations -from ._cwiring import _get_sync_patched # noqa +from ._cwiring import _sync_inject # noqa from ._cwiring import _async_inject # noqa @@ -1047,4 +1047,17 @@ def _get_async_patched(fn: F, patched: PatchedCallable) -> F: patched.closing, ) - return _patched + return cast(F, _patched) + + +def _get_sync_patched(fn: F, patched: PatchedCallable) -> F: + @functools.wraps(fn) + def _patched(*args, **kwargs): + return _sync_inject( + fn, + args, + kwargs, + patched.injections, + patched.closing, + ) + return cast(F, _patched) diff --git a/tests/unit/wiring/test_introspection_py36.py b/tests/unit/wiring/test_introspection_py36.py index c7149602..66b36a80 100644 --- a/tests/unit/wiring/test_introspection_py36.py +++ b/tests/unit/wiring/test_introspection_py36.py @@ -6,6 +6,13 @@ import inspect from dependency_injector.wiring import inject +def test_isfunction(): + @inject + def foo(): ... + + assert inspect.isfunction(foo) + + def test_asyncio_iscoroutinefunction(): @inject async def foo(): From 23acf01c15d8f69b155693687e059f9161cbf816 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Sun, 23 Feb 2025 18:20:38 +0200 Subject: [PATCH 21/25] Add support for inspect.iscoroutinefunction() in Coroutine provider (#830) --- src/dependency_injector/providers.pyx | 22 +++++++++++-------- .../coroutines/test_coroutine_py35.py | 15 +++++++++++++ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/dependency_injector/providers.pyx b/src/dependency_injector/providers.pyx index 39716ea0..71b048a1 100644 --- a/src/dependency_injector/providers.pyx +++ b/src/dependency_injector/providers.pyx @@ -2,6 +2,7 @@ from __future__ import absolute_import +import asyncio import copy import errno import functools @@ -27,17 +28,19 @@ except ImportError: import __builtin__ as builtins try: - import asyncio + from inspect import _is_coroutine_mark as _is_coroutine_marker except ImportError: - asyncio = None - _is_coroutine_marker = None -else: - if sys.version_info >= (3, 5, 3): - import asyncio.coroutines - _is_coroutine_marker = asyncio.coroutines._is_coroutine - else: + try: + # Python >=3.12.0,<3.12.5 + from inspect import _is_coroutine_marker + except ImportError: _is_coroutine_marker = True +try: + from asyncio.coroutines import _is_coroutine +except ImportError: + _is_coroutine = True + try: import ConfigParser as iniconfigparser except ImportError: @@ -1475,7 +1478,8 @@ cdef class Coroutine(Callable): some_coroutine.add_kwargs(keyword_argument1=3, keyword_argument=4) """ - _is_coroutine = _is_coroutine_marker + _is_coroutine_marker = _is_coroutine_marker # Python >=3.12 + _is_coroutine = _is_coroutine # Python <3.16 def set_provides(self, provides): """Set provider provides.""" diff --git a/tests/unit/providers/coroutines/test_coroutine_py35.py b/tests/unit/providers/coroutines/test_coroutine_py35.py index 22e794b1..de0e9c67 100644 --- a/tests/unit/providers/coroutines/test_coroutine_py35.py +++ b/tests/unit/providers/coroutines/test_coroutine_py35.py @@ -1,4 +1,5 @@ """Coroutine provider tests.""" +import sys from dependency_injector import providers, errors from pytest import mark, raises @@ -208,3 +209,17 @@ def test_repr(): "".format(repr(example), hex(id(provider))) ) + + +@mark.skipif(sys.version_info > (3, 15), reason="requires Python<3.16") +def test_asyncio_iscoroutinefunction() -> None: + from asyncio.coroutines import iscoroutinefunction + + assert iscoroutinefunction(providers.Coroutine(example)) + + +@mark.skipif(sys.version_info < (3, 12), reason="requires Python>=3.12") +def test_inspect_iscoroutinefunction() -> None: + from inspect import iscoroutinefunction + + assert iscoroutinefunction(providers.Coroutine(example)) From 8b625d81ad2d40d0b839c37a91b151b42f3b18cf Mon Sep 17 00:00:00 2001 From: ZipFile Date: Sun, 23 Feb 2025 18:21:31 +0200 Subject: [PATCH 22/25] Use Annotated for DI in FastAPI examples (#853) --- docs/wiring.rst | 2 +- .../fastapi-redis/fastapiredis/application.py | 12 +++++++---- .../fastapi-simple/fastapi_di_example.py | 9 ++++++-- .../fastapi-sqlalchemy/webapp/endpoints.py | 21 +++++++++++-------- .../fastapi/giphynavigator/endpoints.py | 21 ++++++++++++------- 5 files changed, 41 insertions(+), 24 deletions(-) diff --git a/docs/wiring.rst b/docs/wiring.rst index 2de708d0..74026879 100644 --- a/docs/wiring.rst +++ b/docs/wiring.rst @@ -64,7 +64,7 @@ FastAPI example: @app.api_route("/") @inject - async def index(service: Service = Depends(Provide[Container.service])): + async def index(service: Annotated[Service, Depends(Provide[Container.service])]): value = await service.process() return {"result": value} diff --git a/examples/miniapps/fastapi-redis/fastapiredis/application.py b/examples/miniapps/fastapi-redis/fastapiredis/application.py index f8e4a3bb..52f14366 100644 --- a/examples/miniapps/fastapi-redis/fastapiredis/application.py +++ b/examples/miniapps/fastapi-redis/fastapiredis/application.py @@ -1,18 +1,22 @@ """Application module.""" -from dependency_injector.wiring import inject, Provide -from fastapi import FastAPI, Depends +from typing import Annotated + +from fastapi import Depends, FastAPI + +from dependency_injector.wiring import Provide, inject from .containers import Container from .services import Service - app = FastAPI() @app.api_route("/") @inject -async def index(service: Service = Depends(Provide[Container.service])): +async def index( + service: Annotated[Service, Depends(Provide[Container.service])] +) -> dict[str, str]: value = await service.process() return {"result": value} diff --git a/examples/miniapps/fastapi-simple/fastapi_di_example.py b/examples/miniapps/fastapi-simple/fastapi_di_example.py index 9f3d3f83..6d50499c 100644 --- a/examples/miniapps/fastapi-simple/fastapi_di_example.py +++ b/examples/miniapps/fastapi-simple/fastapi_di_example.py @@ -1,4 +1,7 @@ -from fastapi import FastAPI, Depends +from typing import Annotated + +from fastapi import Depends, FastAPI + from dependency_injector import containers, providers from dependency_injector.wiring import Provide, inject @@ -18,7 +21,9 @@ app = FastAPI() @app.api_route("/") @inject -async def index(service: Service = Depends(Provide[Container.service])): +async def index( + service: Annotated[Service, Depends(Provide[Container.service])] +) -> dict[str, str]: result = await service.process() return {"result": result} diff --git a/examples/miniapps/fastapi-sqlalchemy/webapp/endpoints.py b/examples/miniapps/fastapi-sqlalchemy/webapp/endpoints.py index 4d27101e..e02c2740 100644 --- a/examples/miniapps/fastapi-sqlalchemy/webapp/endpoints.py +++ b/examples/miniapps/fastapi-sqlalchemy/webapp/endpoints.py @@ -1,11 +1,14 @@ """Endpoints module.""" +from typing import Annotated + from fastapi import APIRouter, Depends, Response, status -from dependency_injector.wiring import inject, Provide + +from dependency_injector.wiring import Provide, inject from .containers import Container -from .services import UserService from .repositories import NotFoundError +from .services import UserService router = APIRouter() @@ -13,7 +16,7 @@ router = APIRouter() @router.get("/users") @inject def get_list( - user_service: UserService = Depends(Provide[Container.user_service]), + user_service: Annotated[UserService, Depends(Provide[Container.user_service])], ): return user_service.get_users() @@ -21,8 +24,8 @@ def get_list( @router.get("/users/{user_id}") @inject def get_by_id( - user_id: int, - user_service: UserService = Depends(Provide[Container.user_service]), + user_id: int, + user_service: Annotated[UserService, Depends(Provide[Container.user_service])], ): try: return user_service.get_user_by_id(user_id) @@ -33,7 +36,7 @@ def get_by_id( @router.post("/users", status_code=status.HTTP_201_CREATED) @inject def add( - user_service: UserService = Depends(Provide[Container.user_service]), + user_service: Annotated[UserService, Depends(Provide[Container.user_service])], ): return user_service.create_user() @@ -41,9 +44,9 @@ def add( @router.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT) @inject def remove( - user_id: int, - user_service: UserService = Depends(Provide[Container.user_service]), -): + user_id: int, + user_service: Annotated[UserService, Depends(Provide[Container.user_service])], +) -> Response: try: user_service.delete_user_by_id(user_id) except NotFoundError: diff --git a/examples/miniapps/fastapi/giphynavigator/endpoints.py b/examples/miniapps/fastapi/giphynavigator/endpoints.py index 2761f203..904eb71d 100644 --- a/examples/miniapps/fastapi/giphynavigator/endpoints.py +++ b/examples/miniapps/fastapi/giphynavigator/endpoints.py @@ -1,13 +1,14 @@ """Endpoints module.""" -from typing import Optional, List +from typing import Annotated, List from fastapi import APIRouter, Depends from pydantic import BaseModel -from dependency_injector.wiring import inject, Provide -from .services import SearchService +from dependency_injector.wiring import Provide, inject + from .containers import Container +from .services import SearchService class Gif(BaseModel): @@ -26,11 +27,15 @@ router = APIRouter() @router.get("/", response_model=Response) @inject async def index( - query: Optional[str] = None, - limit: Optional[str] = None, - default_query: str = Depends(Provide[Container.config.default.query]), - default_limit: int = Depends(Provide[Container.config.default.limit.as_int()]), - search_service: SearchService = Depends(Provide[Container.search_service]), + default_query: Annotated[str, Depends(Provide[Container.config.default.query])], + default_limit: Annotated[ + int, Depends(Provide[Container.config.default.limit.as_int()]) + ], + search_service: Annotated[ + SearchService, Depends(Provide[Container.search_service]) + ], + query: str | None = None, + limit: int | None = None, ): query = query or default_query limit = limit or default_limit From 09efbffab1e347c43381c8f9adf690452d883b53 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Sun, 23 Feb 2025 18:31:34 +0200 Subject: [PATCH 23/25] Fix `Closing` dependency resolution (#852) Co-authored-by: federinik Co-authored-by: jazzthief --- src/dependency_injector/wiring.py | 44 ++++----- .../wiringstringids/resourceclosing.py | 95 +++++++++++++++---- .../unit/wiring/string_ids/test_main_py36.py | 59 ++++++++---- 3 files changed, 137 insertions(+), 61 deletions(-) diff --git a/src/dependency_injector/wiring.py b/src/dependency_injector/wiring.py index 9bb990ab..5cded9f5 100644 --- a/src/dependency_injector/wiring.py +++ b/src/dependency_injector/wiring.py @@ -1,26 +1,26 @@ """Wiring module.""" import functools -import inspect import importlib import importlib.machinery +import inspect import pkgutil -import warnings import sys +import warnings from types import ModuleType from typing import ( - Optional, - Iterable, - Iterator, - Callable, Any, - Tuple, + Callable, Dict, Generic, - TypeVar, - Type, - Union, + Iterable, + Iterator, + Optional, Set, + Tuple, + Type, + TypeVar, + Union, cast, ) @@ -643,21 +643,18 @@ def _fetch_reference_injections( # noqa: C901 def _locate_dependent_closing_args( - provider: providers.Provider, + provider: providers.Provider, closing_deps: Dict[str, providers.Provider] ) -> Dict[str, providers.Provider]: - if not hasattr(provider, "args"): - return {} - - closing_deps = {} - for arg in provider.args: - if not isinstance(arg, providers.Provider) or not hasattr(arg, "args"): + for arg in [ + *getattr(provider, "args", []), + *getattr(provider, "kwargs", {}).values(), + ]: + if not isinstance(arg, providers.Provider): continue + if isinstance(arg, providers.Resource): + closing_deps[str(id(arg))] = arg - if not arg.args and isinstance(arg, providers.Resource): - return {str(id(arg)): arg} - else: - closing_deps += _locate_dependent_closing_args(arg) - return closing_deps + _locate_dependent_closing_args(arg, closing_deps) def _bind_injections(fn: Callable[..., Any], providers_map: ProvidersMap) -> None: @@ -681,7 +678,8 @@ def _bind_injections(fn: Callable[..., Any], providers_map: ProvidersMap) -> Non if injection in patched_callable.reference_closing: patched_callable.add_closing(injection, provider) - deps = _locate_dependent_closing_args(provider) + deps = {} + _locate_dependent_closing_args(provider, deps) for key, dep in deps.items(): patched_callable.add_closing(key, dep) diff --git a/tests/unit/samples/wiringstringids/resourceclosing.py b/tests/unit/samples/wiringstringids/resourceclosing.py index 6360e15c..c4d1f20f 100644 --- a/tests/unit/samples/wiringstringids/resourceclosing.py +++ b/tests/unit/samples/wiringstringids/resourceclosing.py @@ -1,41 +1,80 @@ +from typing import Any, Dict, List, Optional + from dependency_injector import containers, providers -from dependency_injector.wiring import inject, Provide, Closing +from dependency_injector.wiring import Closing, Provide, inject + + +class Counter: + def __init__(self) -> None: + self._init = 0 + self._shutdown = 0 + + def init(self) -> None: + self._init += 1 + + def shutdown(self) -> None: + self._shutdown += 1 + + def reset(self) -> None: + self._init = 0 + self._shutdown = 0 class Service: - init_counter: int = 0 - shutdown_counter: int = 0 + def __init__(self, counter: Optional[Counter] = None, **dependencies: Any) -> None: + self.counter = counter or Counter() + self.dependencies = dependencies - @classmethod - def reset_counter(cls): - cls.init_counter = 0 - cls.shutdown_counter = 0 + def init(self) -> None: + self.counter.init() - @classmethod - def init(cls): - cls.init_counter += 1 + def shutdown(self) -> None: + self.counter.shutdown() - @classmethod - def shutdown(cls): - cls.shutdown_counter += 1 + @property + def init_counter(self) -> int: + return self.counter._init + + @property + def shutdown_counter(self) -> int: + return self.counter._shutdown class FactoryService: - def __init__(self, service: Service): + def __init__(self, service: Service, service2: Service): self.service = service + self.service2 = service2 -def init_service(): - service = Service() +class NestedService: + def __init__(self, factory_service: FactoryService): + self.factory_service = factory_service + + +def init_service(counter: Counter, _list: List[int], _dict: Dict[str, int]): + service = Service(counter, _list=_list, _dict=_dict) service.init() yield service service.shutdown() class Container(containers.DeclarativeContainer): - - service = providers.Resource(init_service) - factory_service = providers.Factory(FactoryService, service) + counter = providers.Singleton(Counter) + _list = providers.List( + providers.Callable(lambda a: a, a=1), providers.Callable(lambda b: b, 2) + ) + _dict = providers.Dict( + a=providers.Callable(lambda a: a, a=3), b=providers.Callable(lambda b: b, 4) + ) + service = providers.Resource(init_service, counter, _list, _dict=_dict) + service2 = providers.Resource(init_service, counter, _list, _dict=_dict) + factory_service = providers.Factory(FactoryService, service, service2) + factory_service_kwargs = providers.Factory( + FactoryService, + service=service, + service2=service2, + ) + nested_service = providers.Factory(NestedService, factory_service) @inject @@ -44,5 +83,21 @@ def test_function(service: Service = Closing[Provide["service"]]): @inject -def test_function_dependency(factory: FactoryService = Closing[Provide["factory_service"]]): +def test_function_dependency( + factory: FactoryService = Closing[Provide["factory_service"]], +): return factory + + +@inject +def test_function_dependency_kwargs( + factory: FactoryService = Closing[Provide["factory_service_kwargs"]], +): + return factory + + +@inject +def test_function_nested_dependency( + nested: NestedService = Closing[Provide["nested_service"]], +): + return nested diff --git a/tests/unit/wiring/string_ids/test_main_py36.py b/tests/unit/wiring/string_ids/test_main_py36.py index d4c49fe8..8125481a 100644 --- a/tests/unit/wiring/string_ids/test_main_py36.py +++ b/tests/unit/wiring/string_ids/test_main_py36.py @@ -2,13 +2,13 @@ from decimal import Decimal +from pytest import fixture, mark, raises +from samples.wiringstringids import module, package, resourceclosing +from samples.wiringstringids.container import Container, SubContainer +from samples.wiringstringids.service import Service + from dependency_injector import errors from dependency_injector.wiring import Closing, Provide, Provider, wire -from pytest import fixture, mark, raises - -from samples.wiringstringids import module, package, resourceclosing -from samples.wiringstringids.service import Service -from samples.wiringstringids.container import Container, SubContainer @fixture(autouse=True) @@ -34,10 +34,11 @@ def subcontainer(): @fixture -def resourceclosing_container(): +def resourceclosing_container(request): container = resourceclosing.Container() container.wire(modules=[resourceclosing]) - yield container + with container.reset_singletons(): + yield container container.unwire() @@ -274,42 +275,65 @@ def test_wire_multiple_containers(): @mark.usefixtures("resourceclosing_container") def test_closing_resource(): - resourceclosing.Service.reset_counter() - result_1 = resourceclosing.test_function() assert isinstance(result_1, resourceclosing.Service) assert result_1.init_counter == 1 assert result_1.shutdown_counter == 1 + assert result_1.dependencies == {"_list": [1, 2], "_dict": {"a": 3, "b": 4}} result_2 = resourceclosing.test_function() assert isinstance(result_2, resourceclosing.Service) assert result_2.init_counter == 2 assert result_2.shutdown_counter == 2 + assert result_1.dependencies == {"_list": [1, 2], "_dict": {"a": 3, "b": 4}} assert result_1 is not result_2 @mark.usefixtures("resourceclosing_container") def test_closing_dependency_resource(): - resourceclosing.Service.reset_counter() - result_1 = resourceclosing.test_function_dependency() assert isinstance(result_1, resourceclosing.FactoryService) - assert result_1.service.init_counter == 1 - assert result_1.service.shutdown_counter == 1 + assert result_1.service.init_counter == 2 + assert result_1.service.shutdown_counter == 2 result_2 = resourceclosing.test_function_dependency() + assert isinstance(result_2, resourceclosing.FactoryService) - assert result_2.service.init_counter == 2 - assert result_2.service.shutdown_counter == 2 + assert result_2.service.init_counter == 4 + assert result_2.service.shutdown_counter == 4 + + +@mark.usefixtures("resourceclosing_container") +def test_closing_dependency_resource_kwargs(): + result_1 = resourceclosing.test_function_dependency_kwargs() + assert isinstance(result_1, resourceclosing.FactoryService) + assert result_1.service.init_counter == 2 + assert result_1.service.shutdown_counter == 2 + + result_2 = resourceclosing.test_function_dependency_kwargs() + assert isinstance(result_2, resourceclosing.FactoryService) + assert result_2.service.init_counter == 4 + assert result_2.service.shutdown_counter == 4 + + +@mark.usefixtures("resourceclosing_container") +def test_closing_nested_dependency_resource(): + result_1 = resourceclosing.test_function_nested_dependency() + assert isinstance(result_1, resourceclosing.NestedService) + assert result_1.factory_service.service.init_counter == 2 + assert result_1.factory_service.service.shutdown_counter == 2 + + result_2 = resourceclosing.test_function_nested_dependency() + assert isinstance(result_2, resourceclosing.NestedService) + assert result_2.factory_service.service.init_counter == 4 + assert result_2.factory_service.service.shutdown_counter == 4 assert result_1 is not result_2 @mark.usefixtures("resourceclosing_container") def test_closing_resource_bypass_marker_injection(): - resourceclosing.Service.reset_counter() - result_1 = resourceclosing.test_function(service=Closing[Provide["service"]]) assert isinstance(result_1, resourceclosing.Service) assert result_1.init_counter == 1 @@ -325,7 +349,6 @@ def test_closing_resource_bypass_marker_injection(): @mark.usefixtures("resourceclosing_container") def test_closing_resource_context(): - resourceclosing.Service.reset_counter() service = resourceclosing.Service() result_1 = resourceclosing.test_function(service=service) From 7d4ebecd193af1016ec42a0276fef3a0587d2827 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Sun, 23 Feb 2025 19:01:01 +0200 Subject: [PATCH 24/25] Add option to disable env var interpolation in configs (#861) --- docs/providers/configuration.rst | 13 ++++ src/dependency_injector/providers.pyi | 6 +- src/dependency_injector/providers.pyx | 75 ++++++++----------- .../test_from_ini_with_env_py2_py3.py | 17 +++++ .../test_from_json_with_env_py2_py3.py | 17 +++++ .../test_from_yaml_with_env_py2_py3.py | 17 +++++ 6 files changed, 100 insertions(+), 45 deletions(-) diff --git a/docs/providers/configuration.rst b/docs/providers/configuration.rst index 582c0cc1..66c299f3 100644 --- a/docs/providers/configuration.rst +++ b/docs/providers/configuration.rst @@ -366,6 +366,19 @@ See also: :ref:`configuration-strict-mode`. assert container.config.section.option() is None +If you want to disable environment variables interpolation, pass ``envs_required=None``: + +.. code-block:: yaml + :caption: templates.yml + + template_string: 'Hello, ${name}!' + +.. code-block:: python + + >>> container.config.from_yaml("templates.yml", envs_required=None) + >>> container.config.template_string() + 'Hello, ${name}!' + Mandatory and optional sources ------------------------------ diff --git a/src/dependency_injector/providers.pyi b/src/dependency_injector/providers.pyi index 32534043..e4d62506 100644 --- a/src/dependency_injector/providers.pyi +++ b/src/dependency_injector/providers.pyi @@ -225,20 +225,20 @@ class ConfigurationOption(Provider[Any]): self, filepath: Union[Path, str], required: bool = False, - envs_required: bool = False, + envs_required: Optional[bool] = False, ) -> None: ... def from_yaml( self, filepath: Union[Path, str], required: bool = False, loader: Optional[Any] = None, - envs_required: bool = False, + envs_required: Optional[bool] = False, ) -> None: ... def from_json( self, filepath: Union[Path, str], required: bool = False, - envs_required: bool = False, + envs_required: Optional[bool] = False, ) -> None: ... def from_pydantic( self, settings: PydanticSettings, required: bool = False, **kwargs: Any diff --git a/src/dependency_injector/providers.pyx b/src/dependency_injector/providers.pyx index 71b048a1..a3620350 100644 --- a/src/dependency_injector/providers.pyx +++ b/src/dependency_injector/providers.pyx @@ -15,6 +15,7 @@ import sys import threading import types import warnings +from configparser import ConfigParser as IniConfigParser try: import contextvars @@ -41,11 +42,6 @@ try: except ImportError: _is_coroutine = True -try: - import ConfigParser as iniconfigparser -except ImportError: - import configparser as iniconfigparser - try: import yaml except ImportError: @@ -102,7 +98,7 @@ config_env_marker_pattern = re.compile( r"\${(?P[^}^{:]+)(?P:?)(?P.*?)}", ) -def _resolve_config_env_markers(config_content, envs_required=False): +cdef str _resolve_config_env_markers(config_content: str, envs_required: bool): """Replace environment variable markers with their values.""" findings = list(config_env_marker_pattern.finditer(config_content)) @@ -121,28 +117,19 @@ def _resolve_config_env_markers(config_content, envs_required=False): return config_content -if sys.version_info[0] == 3: - def _parse_ini_file(filepath, envs_required=False): - parser = iniconfigparser.ConfigParser() - with open(filepath) as config_file: - config_string = _resolve_config_env_markers( - config_file.read(), - envs_required=envs_required, - ) - parser.read_string(config_string) - return parser -else: - import StringIO +cdef object _parse_ini_file(filepath, envs_required: bool | None): + parser = IniConfigParser() - def _parse_ini_file(filepath, envs_required=False): - parser = iniconfigparser.ConfigParser() - with open(filepath) as config_file: + with open(filepath) as config_file: + config_string = config_file.read() + + if envs_required is not None: config_string = _resolve_config_env_markers( - config_file.read(), + config_string, envs_required=envs_required, ) - parser.readfp(StringIO.StringIO(config_string)) - return parser + parser.read_string(config_string) + return parser if yaml: @@ -1717,7 +1704,7 @@ cdef class ConfigurationOption(Provider): try: parser = _parse_ini_file( filepath, - envs_required=envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(), + envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(), ) except IOError as exception: if required is not False \ @@ -1776,10 +1763,11 @@ cdef class ConfigurationOption(Provider): raise return - config_content = _resolve_config_env_markers( - config_content, - envs_required=envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(), - ) + if envs_required is not None: + config_content = _resolve_config_env_markers( + config_content, + envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(), + ) config = yaml.load(config_content, loader) current_config = self.__call__() @@ -1814,10 +1802,11 @@ cdef class ConfigurationOption(Provider): raise return - config_content = _resolve_config_env_markers( - config_content, - envs_required=envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(), - ) + if envs_required is not None: + config_content = _resolve_config_env_markers( + config_content, + envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(), + ) config = json.loads(config_content) current_config = self.__call__() @@ -2270,7 +2259,7 @@ cdef class Configuration(Object): try: parser = _parse_ini_file( filepath, - envs_required=envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(), + envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(), ) except IOError as exception: if required is not False \ @@ -2329,10 +2318,11 @@ cdef class Configuration(Object): raise return - config_content = _resolve_config_env_markers( - config_content, - envs_required=envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(), - ) + if envs_required is not None: + config_content = _resolve_config_env_markers( + config_content, + envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(), + ) config = yaml.load(config_content, loader) current_config = self.__call__() @@ -2367,10 +2357,11 @@ cdef class Configuration(Object): raise return - config_content = _resolve_config_env_markers( - config_content, - envs_required=envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(), - ) + if envs_required is not None: + config_content = _resolve_config_env_markers( + config_content, + envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(), + ) config = json.loads(config_content) current_config = self.__call__() diff --git a/tests/unit/providers/configuration/test_from_ini_with_env_py2_py3.py b/tests/unit/providers/configuration/test_from_ini_with_env_py2_py3.py index e669ce48..96949a67 100644 --- a/tests/unit/providers/configuration/test_from_ini_with_env_py2_py3.py +++ b/tests/unit/providers/configuration/test_from_ini_with_env_py2_py3.py @@ -5,6 +5,23 @@ import os from pytest import mark, raises +def test_no_env_variable_interpolation(config, ini_config_file_3): + config.from_ini(ini_config_file_3, envs_required=None) + + assert config() == { + "section1": { + "value1": "${CONFIG_TEST_ENV}", + "value2": "${CONFIG_TEST_PATH}/path", + }, + } + assert config.section1() == { + "value1": "${CONFIG_TEST_ENV}", + "value2": "${CONFIG_TEST_PATH}/path", + } + assert config.section1.value1() == "${CONFIG_TEST_ENV}" + assert config.section1.value2() == "${CONFIG_TEST_PATH}/path" + + def test_env_variable_interpolation(config, ini_config_file_3): config.from_ini(ini_config_file_3) diff --git a/tests/unit/providers/configuration/test_from_json_with_env_py2_py3.py b/tests/unit/providers/configuration/test_from_json_with_env_py2_py3.py index 2bd8f9aa..4ec7c4ea 100644 --- a/tests/unit/providers/configuration/test_from_json_with_env_py2_py3.py +++ b/tests/unit/providers/configuration/test_from_json_with_env_py2_py3.py @@ -6,6 +6,23 @@ import os from pytest import mark, raises +def test_no_env_variable_interpolation(config, json_config_file_3): + config.from_json(json_config_file_3, envs_required=None) + + assert config() == { + "section1": { + "value1": "${CONFIG_TEST_ENV}", + "value2": "${CONFIG_TEST_PATH}/path", + }, + } + assert config.section1() == { + "value1": "${CONFIG_TEST_ENV}", + "value2": "${CONFIG_TEST_PATH}/path", + } + assert config.section1.value1() == "${CONFIG_TEST_ENV}" + assert config.section1.value2() == "${CONFIG_TEST_PATH}/path" + + def test_env_variable_interpolation(config, json_config_file_3): config.from_json(json_config_file_3) diff --git a/tests/unit/providers/configuration/test_from_yaml_with_env_py2_py3.py b/tests/unit/providers/configuration/test_from_yaml_with_env_py2_py3.py index 8e6e1c0d..c047659e 100644 --- a/tests/unit/providers/configuration/test_from_yaml_with_env_py2_py3.py +++ b/tests/unit/providers/configuration/test_from_yaml_with_env_py2_py3.py @@ -6,6 +6,23 @@ import yaml from pytest import mark, raises +def test_no_env_variable_interpolation(config, yaml_config_file_3): + config.from_yaml(yaml_config_file_3, envs_required=None) + + assert config() == { + "section1": { + "value1": "${CONFIG_TEST_ENV}", + "value2": "${CONFIG_TEST_PATH}/path", + }, + } + assert config.section1() == { + "value1": "${CONFIG_TEST_ENV}", + "value2": "${CONFIG_TEST_PATH}/path", + } + assert config.section1.value1() == "${CONFIG_TEST_ENV}" + assert config.section1.value2() == "${CONFIG_TEST_PATH}/path" + + def test_env_variable_interpolation(config, yaml_config_file_3): config.from_yaml(yaml_config_file_3) From 9b66d4bf169617a9858ee118e86f1250155bdac7 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Sun, 23 Feb 2025 17:18:54 +0000 Subject: [PATCH 25/25] Bump version to v4.46.0 --- docs/main/changelog.rst | 19 +++++++++++++++++++ src/dependency_injector/__init__.py | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/docs/main/changelog.rst b/docs/main/changelog.rst index 54a0e8eb..4d724da1 100644 --- a/docs/main/changelog.rst +++ b/docs/main/changelog.rst @@ -7,6 +7,25 @@ that were made in every particular version. From version 0.7.6 *Dependency Injector* framework strictly follows `Semantic versioning`_ +4.46.0 +------ + +- Add option to disable env var interpolation in configs (`#861 `_) +- Fix ``Closing`` dependency resolution (`#852 `_) +- Add support for ``inspect.iscoroutinefunction()`` in ``Coroutine`` provider (`#830 `_) +- Fix broken wiring of sync inject-decorated methods (`#673 `_) +- Add support for ``typing.Annotated`` (`#721 `_, `#853 `_) +- Documentation updates for movie-lister example (`#747 `_) +- Fix type propagation in ``Provider.provider`` (`#744 `_) + +Many thanks for the contributions to: +- `ZipFile `_ +- `Yegor Statkevich `_ +- `Federico Tomasi `_ +- `Martin Lafrance `_ +- `Philip Bjorge `_ +- `Ilya Kazakov `_ + 4.45.0 -------- - Add Starlette lifespan handler implementation (`#683 `_). diff --git a/src/dependency_injector/__init__.py b/src/dependency_injector/__init__.py index 14e3c273..faa905f5 100644 --- a/src/dependency_injector/__init__.py +++ b/src/dependency_injector/__init__.py @@ -1,6 +1,6 @@ """Top-level package.""" -__version__ = "4.45.0" +__version__ = "4.46.0" """Version number. :type: str