From 87741edb5330f66c25f02648813eb3437b87dd89 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Sun, 8 Dec 2024 18:53:08 +0200 Subject: [PATCH 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] :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 7/9] 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 8/9] 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 9/9] 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