mirror of
https://github.com/ets-labs/python-dependency-injector.git
synced 2025-01-30 19:24:31 +03:00
Merge branch 'release/4.45.0' into master
This commit is contained in:
commit
46646b1acf
|
@ -7,6 +7,21 @@ that were made in every particular version.
|
||||||
From version 0.7.6 *Dependency Injector* framework strictly
|
From version 0.7.6 *Dependency Injector* framework strictly
|
||||||
follows `Semantic versioning`_
|
follows `Semantic versioning`_
|
||||||
|
|
||||||
|
4.45.0
|
||||||
|
--------
|
||||||
|
- Add Starlette lifespan handler implementation (`#683 <https://github.com/ets-labs/python-dependency-injector/pull/683>`_).
|
||||||
|
- Raise exception in ``ThreadLocalSingleton`` instead of hiding it in finally (`#845 <https://github.com/ets-labs/python-dependency-injector/pull/845>`_).
|
||||||
|
- Improve debuggability of ``deepcopy`` errors (`#839 <https://github.com/ets-labs/python-dependency-injector/pull/839>`_).
|
||||||
|
- Update examples (`#838 <https://github.com/ets-labs/python-dependency-injector/pull/838>`_).
|
||||||
|
- Upgrade testing dependencies (`#837 <https://github.com/ets-labs/python-dependency-injector/pull/837>`_).
|
||||||
|
- Add minor fixes to the documentation (`#709 <https://github.com/ets-labs/python-dependency-injector/pull/709>`_).
|
||||||
|
- Remove ``six`` from the dependencies (`3ba4704 <https://github.com/ets-labs/python-dependency-injector/commit/3ba4704bc1cb00310749fd2eda0c8221167c313c>`_).
|
||||||
|
|
||||||
|
Many thanks for the contributions to:
|
||||||
|
- `ZipFile <https://github.com/ZipFile>`_
|
||||||
|
- `František Trebuňa <https://github.com/gortibaldik>`_
|
||||||
|
- `JC (Jonathan Chen) <https://github.com/dijonkitchen>`_
|
||||||
|
|
||||||
4.44.0
|
4.44.0
|
||||||
--------
|
--------
|
||||||
- Implement support for Pydantic 2. PR: `#832 <https://github.com/ets-labs/python-dependency-injector/pull/832>`_.
|
- Implement support for Pydantic 2. PR: `#832 <https://github.com/ets-labs/python-dependency-injector/pull/832>`_.
|
||||||
|
|
|
@ -24,7 +24,7 @@ returns it on the rest of the calls.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
``Singleton`` provider makes dependencies injection only when creates an object. When an object
|
``Singleton`` provider makes dependencies injection only when it creates an object. When an object
|
||||||
is created and memorized ``Singleton`` provider just returns it without applying injections.
|
is created and memorized ``Singleton`` provider just returns it without applying injections.
|
||||||
|
|
||||||
Specialization of the provided type and abstract singletons work the same like like for the
|
Specialization of the provided type and abstract singletons work the same like like for the
|
||||||
|
|
|
@ -18,7 +18,7 @@ In this tutorial we will use:
|
||||||
|
|
||||||
- Python 3
|
- Python 3
|
||||||
- Docker
|
- Docker
|
||||||
- Docker-compose
|
- Docker Compose
|
||||||
|
|
||||||
Start from the scratch or jump to the section:
|
Start from the scratch or jump to the section:
|
||||||
|
|
||||||
|
@ -47,28 +47,27 @@ response it will log:
|
||||||
Prerequisites
|
Prerequisites
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
We will use `Docker <https://www.docker.com/>`_ and
|
We will use `docker compose <https://docs.docker.com/compose/>`_ in this tutorial. Let's check the versions:
|
||||||
`docker-compose <https://docs.docker.com/compose/>`_ in this tutorial. Let's check the versions:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
docker --version
|
docker --version
|
||||||
docker-compose --version
|
docker compose version
|
||||||
|
|
||||||
The output should look something like:
|
The output should look something like:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
Docker version 20.10.5, build 55c4c88
|
Docker version 27.3.1, build ce12230
|
||||||
docker-compose version 1.29.0, build 07737305
|
Docker Compose version v2.29.7
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
If you don't have ``Docker`` or ``docker-compose`` you need to install them before proceeding.
|
If you don't have ``Docker`` or ``docker compose`` you need to install them before proceeding.
|
||||||
Follow these installation guides:
|
Follow these installation guides:
|
||||||
|
|
||||||
- `Install Docker <https://docs.docker.com/get-docker/>`_
|
- `Install Docker <https://docs.docker.com/get-docker/>`_
|
||||||
- `Install docker-compose <https://docs.docker.com/compose/install/>`_
|
- `Install docker compose <https://docs.docker.com/compose/install/>`_
|
||||||
|
|
||||||
The prerequisites are satisfied. Let's get started with the project layout.
|
The prerequisites are satisfied. Let's get started with the project layout.
|
||||||
|
|
||||||
|
@ -129,13 +128,13 @@ Put next lines into the ``requirements.txt`` file:
|
||||||
pytest-cov
|
pytest-cov
|
||||||
|
|
||||||
Second, we need to create the ``Dockerfile``. It will describe the daemon's build process and
|
Second, we need to create the ``Dockerfile``. It will describe the daemon's build process and
|
||||||
specify how to run it. We will use ``python:3.9-buster`` as a base image.
|
specify how to run it. We will use ``python:3.13-bookworm`` as a base image.
|
||||||
|
|
||||||
Put next lines into the ``Dockerfile`` file:
|
Put next lines into the ``Dockerfile`` file:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
FROM python:3.10-buster
|
FROM python:3.13-bookworm
|
||||||
|
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
@ -155,8 +154,6 @@ Put next lines into the ``docker-compose.yml`` file:
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
|
|
||||||
version: "3.7"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
||||||
monitor:
|
monitor:
|
||||||
|
@ -171,7 +168,7 @@ Run in the terminal:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
docker-compose build
|
docker compose build
|
||||||
|
|
||||||
The build process may take a couple of minutes. You should see something like this in the end:
|
The build process may take a couple of minutes. You should see something like this in the end:
|
||||||
|
|
||||||
|
@ -184,7 +181,7 @@ After the build is done run the container:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
docker-compose up
|
docker compose up
|
||||||
|
|
||||||
The output should look like:
|
The output should look like:
|
||||||
|
|
||||||
|
@ -461,7 +458,7 @@ Run in the terminal:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
docker-compose up
|
docker compose up
|
||||||
|
|
||||||
The output should look like:
|
The output should look like:
|
||||||
|
|
||||||
|
@ -705,7 +702,7 @@ Run in the terminal:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
docker-compose up
|
docker compose up
|
||||||
|
|
||||||
You should see:
|
You should see:
|
||||||
|
|
||||||
|
@ -813,7 +810,7 @@ Run in the terminal:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
docker-compose up
|
docker compose up
|
||||||
|
|
||||||
You should see:
|
You should see:
|
||||||
|
|
||||||
|
@ -965,15 +962,16 @@ Run in the terminal:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
docker-compose run --rm monitor py.test monitoringdaemon/tests.py --cov=monitoringdaemon
|
docker compose run --rm monitor py.test monitoringdaemon/tests.py --cov=monitoringdaemon
|
||||||
|
|
||||||
You should see:
|
You should see:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
platform linux -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
|
platform linux -- Python 3.13.1, pytest-8.3.4, pluggy-1.5.0
|
||||||
rootdir: /code
|
rootdir: /code
|
||||||
plugins: asyncio-0.16.0, cov-3.0.0
|
plugins: cov-6.0.0, asyncio-0.24.0
|
||||||
|
asyncio: mode=Mode.STRICT, default_loop_scope=None
|
||||||
collected 2 items
|
collected 2 items
|
||||||
|
|
||||||
monitoringdaemon/tests.py .. [100%]
|
monitoringdaemon/tests.py .. [100%]
|
||||||
|
|
|
@ -98,8 +98,9 @@ The output should be something like:
|
||||||
|
|
||||||
.. code-block::
|
.. code-block::
|
||||||
|
|
||||||
platform darwin -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
|
platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0
|
||||||
plugins: asyncio-0.16.0, anyio-3.3.4, aiohttp-0.3.0, cov-3.0.0
|
plugins: cov-6.0.0, anyio-4.4.0, asyncio-0.24.0, aiohttp-1.0.5
|
||||||
|
asyncio: mode=Mode.STRICT, default_loop_scope=None
|
||||||
collected 3 items
|
collected 3 items
|
||||||
|
|
||||||
giphynavigator/tests.py ... [100%]
|
giphynavigator/tests.py ... [100%]
|
||||||
|
|
|
@ -3,11 +3,15 @@
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import pytest_asyncio
|
||||||
|
|
||||||
from giphynavigator.application import create_app
|
from giphynavigator.application import create_app
|
||||||
from giphynavigator.giphy import GiphyClient
|
from giphynavigator.giphy import GiphyClient
|
||||||
|
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.asyncio
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def app():
|
def app():
|
||||||
app = create_app()
|
app = create_app()
|
||||||
|
@ -15,9 +19,9 @@ def app():
|
||||||
app.container.unwire()
|
app.container.unwire()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest_asyncio.fixture
|
||||||
def client(app, aiohttp_client, loop):
|
async def client(app, aiohttp_client):
|
||||||
return loop.run_until_complete(aiohttp_client(app))
|
return await aiohttp_client(app)
|
||||||
|
|
||||||
|
|
||||||
async def test_index(client, app):
|
async def test_index(client, app):
|
||||||
|
|
|
@ -2,4 +2,5 @@ dependency-injector
|
||||||
aiohttp
|
aiohttp
|
||||||
pyyaml
|
pyyaml
|
||||||
pytest-aiohttp
|
pytest-aiohttp
|
||||||
|
pytest-asyncio
|
||||||
pytest-cov
|
pytest-cov
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM python:3.10-buster
|
FROM python:3.13-bookworm
|
||||||
|
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
|
|
@ -13,13 +13,13 @@ Build the Docker image:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
docker-compose build
|
docker compose build
|
||||||
|
|
||||||
Run the docker-compose environment:
|
Run the docker-compose environment:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
docker-compose up
|
docker compose up
|
||||||
|
|
||||||
The output should be something like:
|
The output should be something like:
|
||||||
|
|
||||||
|
@ -59,15 +59,16 @@ To run the tests do:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
docker-compose run --rm monitor py.test monitoringdaemon/tests.py --cov=monitoringdaemon
|
docker compose run --rm monitor py.test monitoringdaemon/tests.py --cov=monitoringdaemon
|
||||||
|
|
||||||
The output should be something like:
|
The output should be something like:
|
||||||
|
|
||||||
.. code-block::
|
.. code-block::
|
||||||
|
|
||||||
platform linux -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
|
platform linux -- Python 3.13.1, pytest-8.3.4, pluggy-1.5.0
|
||||||
rootdir: /code
|
rootdir: /code
|
||||||
plugins: asyncio-0.16.0, cov-3.0.0
|
plugins: cov-6.0.0, asyncio-0.24.0
|
||||||
|
asyncio: mode=Mode.STRICT, default_loop_scope=None
|
||||||
collected 2 items
|
collected 2 items
|
||||||
|
|
||||||
monitoringdaemon/tests.py .. [100%]
|
monitoringdaemon/tests.py .. [100%]
|
||||||
|
|
|
@ -61,7 +61,7 @@ async def test_example_monitor(container, caplog):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_dispatcher(container, caplog, event_loop):
|
async def test_dispatcher(container, caplog):
|
||||||
caplog.set_level("INFO")
|
caplog.set_level("INFO")
|
||||||
|
|
||||||
example_monitor_mock = mock.AsyncMock()
|
example_monitor_mock = mock.AsyncMock()
|
||||||
|
@ -72,6 +72,7 @@ async def test_dispatcher(container, caplog, event_loop):
|
||||||
httpbin_monitor=httpbin_monitor_mock,
|
httpbin_monitor=httpbin_monitor_mock,
|
||||||
):
|
):
|
||||||
dispatcher = container.dispatcher()
|
dispatcher = container.dispatcher()
|
||||||
|
event_loop = asyncio.get_running_loop()
|
||||||
event_loop.create_task(dispatcher.start())
|
event_loop.create_task(dispatcher.start())
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
dispatcher.stop()
|
dispatcher.stop()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM python:3.10-buster
|
FROM python:3.13-bookworm
|
||||||
|
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
|
|
@ -12,13 +12,13 @@ Build the Docker image:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
docker-compose build
|
docker compose build
|
||||||
|
|
||||||
Run the docker-compose environment:
|
Run the docker-compose environment:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
docker-compose up
|
docker compose up
|
||||||
|
|
||||||
The output should be something like:
|
The output should be something like:
|
||||||
|
|
||||||
|
@ -54,16 +54,16 @@ To run the tests do:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
docker-compose run --rm example py.test fastapiredis/tests.py --cov=fastapiredis
|
docker compose run --rm example py.test fastapiredis/tests.py --cov=fastapiredis
|
||||||
|
|
||||||
The output should be something like:
|
The output should be something like:
|
||||||
|
|
||||||
.. code-block::
|
.. code-block::
|
||||||
|
|
||||||
platform linux -- Python 3.10.9, pytest-7.2.0, pluggy-1.0.0
|
platform linux -- Python 3.13.1, pytest-8.3.4, pluggy-1.5.0
|
||||||
rootdir: /code
|
rootdir: /code
|
||||||
plugins: cov-4.0.0, asyncio-0.20.3
|
plugins: cov-6.0.0, asyncio-0.24.0, anyio-4.7.0
|
||||||
collected 1 item
|
asyncio: mode=Mode.STRICT, default_loop_scope=None
|
||||||
|
|
||||||
fastapiredis/tests.py . [100%]
|
fastapiredis/tests.py . [100%]
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from typing import AsyncIterator
|
from typing import AsyncIterator
|
||||||
|
|
||||||
from aioredis import from_url, Redis
|
from redis.asyncio import from_url, Redis
|
||||||
|
|
||||||
|
|
||||||
async def init_redis_pool(host: str, password: str) -> AsyncIterator[Redis]:
|
async def init_redis_pool(host: str, password: str) -> AsyncIterator[Redis]:
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"""Services module."""
|
"""Services module."""
|
||||||
|
|
||||||
from aioredis import Redis
|
from redis.asyncio import Redis
|
||||||
|
|
||||||
|
|
||||||
class Service:
|
class Service:
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
dependency-injector
|
dependency-injector
|
||||||
fastapi
|
fastapi
|
||||||
uvicorn
|
uvicorn
|
||||||
aioredis
|
redis>=4.2
|
||||||
|
|
||||||
# For testing:
|
# For testing:
|
||||||
pytest
|
pytest
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import pytest_asyncio
|
||||||
from httpx import ASGITransport, AsyncClient
|
from httpx import ASGITransport, AsyncClient
|
||||||
|
|
||||||
from fastapi_di_example import app, container, Service
|
from fastapi_di_example import app, container, Service
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest_asyncio.fixture
|
||||||
async def client(event_loop):
|
async def client():
|
||||||
async with AsyncClient(
|
async with AsyncClient(
|
||||||
transport=ASGITransport(app=app),
|
transport=ASGITransport(app=app),
|
||||||
base_url="http://test",
|
base_url="http://test",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM python:3.10-buster
|
FROM python:3.13-bookworm
|
||||||
|
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
ENV HOST=0.0.0.0
|
ENV HOST=0.0.0.0
|
||||||
|
|
|
@ -15,13 +15,13 @@ Build the Docker image:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
docker-compose build
|
docker compose build
|
||||||
|
|
||||||
Run the docker-compose environment:
|
Run the docker-compose environment:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
docker-compose up
|
docker compose up
|
||||||
|
|
||||||
The output should be something like:
|
The output should be something like:
|
||||||
|
|
||||||
|
@ -67,15 +67,15 @@ To run the tests do:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
docker-compose run --rm webapp py.test webapp/tests.py --cov=webapp
|
docker compose run --rm webapp py.test webapp/tests.py --cov=webapp
|
||||||
|
|
||||||
The output should be something like:
|
The output should be something like:
|
||||||
|
|
||||||
.. code-block::
|
.. code-block::
|
||||||
|
|
||||||
platform linux -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
|
platform linux -- Python 3.13.1, pytest-8.3.4, pluggy-1.5.0
|
||||||
rootdir: /code
|
rootdir: /code
|
||||||
plugins: cov-3.0.0
|
plugins: cov-6.0.0, anyio-4.7.0
|
||||||
collected 7 items
|
collected 7 items
|
||||||
|
|
||||||
webapp/tests.py ....... [100%]
|
webapp/tests.py ....... [100%]
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
dependency-injector
|
dependency-injector
|
||||||
fastapi
|
fastapi[standard]
|
||||||
uvicorn
|
uvicorn
|
||||||
pyyaml
|
pyyaml
|
||||||
sqlalchemy
|
sqlalchemy
|
||||||
|
|
|
@ -101,9 +101,9 @@ The output should be something like:
|
||||||
|
|
||||||
.. code-block::
|
.. code-block::
|
||||||
|
|
||||||
platform darwin -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
|
platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0
|
||||||
plugins: asyncio-0.16.0, cov-3.0.0
|
plugins: cov-6.0.0, anyio-4.4.0, asyncio-0.24.0, aiohttp-1.0.5
|
||||||
collected 3 items
|
asyncio: mode=Mode.STRICT, default_loop_scope=None
|
||||||
|
|
||||||
giphynavigator/tests.py ... [100%]
|
giphynavigator/tests.py ... [100%]
|
||||||
|
|
||||||
|
|
|
@ -3,13 +3,14 @@
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import pytest_asyncio
|
||||||
from httpx import ASGITransport, AsyncClient
|
from httpx import ASGITransport, AsyncClient
|
||||||
|
|
||||||
from giphynavigator.application import app
|
from giphynavigator.application import app
|
||||||
from giphynavigator.giphy import GiphyClient
|
from giphynavigator.giphy import GiphyClient
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest_asyncio.fixture
|
||||||
async def client():
|
async def client():
|
||||||
async with AsyncClient(
|
async with AsyncClient(
|
||||||
transport=ASGITransport(app=app),
|
transport=ASGITransport(app=app),
|
||||||
|
|
|
@ -81,8 +81,9 @@ The output should be something like:
|
||||||
|
|
||||||
.. code-block::
|
.. code-block::
|
||||||
|
|
||||||
platform darwin -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
|
platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0
|
||||||
plugins: cov-3.0.0, flask-1.2.0
|
plugins: cov-6.0.0, flask-1.3.0
|
||||||
|
asyncio: mode=Mode.STRICT, default_loop_scope=None
|
||||||
collected 2 items
|
collected 2 items
|
||||||
|
|
||||||
githubnavigator/tests.py .. [100%]
|
githubnavigator/tests.py .. [100%]
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"""Application module."""
|
"""Application module."""
|
||||||
|
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask_bootstrap import Bootstrap
|
from flask_bootstrap import Bootstrap4
|
||||||
|
|
||||||
from .containers import Container
|
from .containers import Container
|
||||||
from .blueprints import example
|
from .blueprints import example
|
||||||
|
@ -15,7 +15,7 @@ def create_app() -> Flask:
|
||||||
app.container = container
|
app.container = container
|
||||||
app.register_blueprint(example.blueprint)
|
app.register_blueprint(example.blueprint)
|
||||||
|
|
||||||
bootstrap = Bootstrap()
|
bootstrap = Bootstrap4()
|
||||||
bootstrap.init_app(app)
|
bootstrap.init_app(app)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
|
@ -81,8 +81,9 @@ The output should be something like:
|
||||||
|
|
||||||
.. code-block::
|
.. code-block::
|
||||||
|
|
||||||
platform darwin -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
|
platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0
|
||||||
plugins: cov-3.0.0, flask-1.2.0
|
plugins: cov-6.0.0, flask-1.3.0
|
||||||
|
asyncio: mode=Mode.STRICT, default_loop_scope=None
|
||||||
collected 2 items
|
collected 2 items
|
||||||
|
|
||||||
githubnavigator/tests.py .. [100%]
|
githubnavigator/tests.py .. [100%]
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"""Application module."""
|
"""Application module."""
|
||||||
|
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask_bootstrap import Bootstrap
|
from flask_bootstrap import Bootstrap4
|
||||||
|
|
||||||
from .containers import Container
|
from .containers import Container
|
||||||
from . import views
|
from . import views
|
||||||
|
@ -15,7 +15,7 @@ def create_app() -> Flask:
|
||||||
app.container = container
|
app.container = container
|
||||||
app.add_url_rule("/", "index", views.index)
|
app.add_url_rule("/", "index", views.index)
|
||||||
|
|
||||||
bootstrap = Bootstrap()
|
bootstrap = Bootstrap4()
|
||||||
bootstrap.init_app(app)
|
bootstrap.init_app(app)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
|
@ -58,8 +58,8 @@ The output should be something like:
|
||||||
|
|
||||||
.. code-block::
|
.. code-block::
|
||||||
|
|
||||||
platform darwin -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
|
platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0
|
||||||
plugins: cov-3.0.0
|
plugins: cov-6.0.0
|
||||||
collected 2 items
|
collected 2 items
|
||||||
|
|
||||||
movies/tests.py .. [100%]
|
movies/tests.py .. [100%]
|
||||||
|
|
|
@ -27,7 +27,7 @@ To run the application do:
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
export GIPHY_API_KEY=wBJ2wZG7SRqfrU9nPgPiWvORmloDyuL0
|
export GIPHY_API_KEY=wBJ2wZG7SRqfrU9nPgPiWvORmloDyuL0
|
||||||
python -m giphynavigator
|
sanic giphynavigator.application:create_app
|
||||||
|
|
||||||
The output should be something like:
|
The output should be something like:
|
||||||
|
|
||||||
|
@ -98,8 +98,9 @@ The output should be something like:
|
||||||
|
|
||||||
.. code-block::
|
.. code-block::
|
||||||
|
|
||||||
platform darwin -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
|
platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0
|
||||||
plugins: sanic-1.9.1, anyio-3.3.4, cov-3.0.0
|
plugins: cov-6.0.0, anyio-4.4.0, asyncio-0.24.0
|
||||||
|
asyncio: mode=Mode.STRICT, default_loop_scope=None
|
||||||
collected 3 items
|
collected 3 items
|
||||||
|
|
||||||
giphynavigator/tests.py ... [100%]
|
giphynavigator/tests.py ... [100%]
|
||||||
|
|
|
@ -8,6 +8,8 @@ from sanic import Sanic
|
||||||
from giphynavigator.application import create_app
|
from giphynavigator.application import create_app
|
||||||
from giphynavigator.giphy import GiphyClient
|
from giphynavigator.giphy import GiphyClient
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.asyncio
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def app():
|
def app():
|
||||||
|
@ -17,12 +19,7 @@ def app():
|
||||||
app.ctx.container.unwire()
|
app.ctx.container.unwire()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
async def test_index(app):
|
||||||
def test_client(loop, app, sanic_client):
|
|
||||||
return loop.run_until_complete(sanic_client(app))
|
|
||||||
|
|
||||||
|
|
||||||
async def test_index(app, test_client):
|
|
||||||
giphy_client_mock = mock.AsyncMock(spec=GiphyClient)
|
giphy_client_mock = mock.AsyncMock(spec=GiphyClient)
|
||||||
giphy_client_mock.search.return_value = {
|
giphy_client_mock.search.return_value = {
|
||||||
"data": [
|
"data": [
|
||||||
|
@ -32,7 +29,7 @@ async def test_index(app, test_client):
|
||||||
}
|
}
|
||||||
|
|
||||||
with app.ctx.container.giphy_client.override(giphy_client_mock):
|
with app.ctx.container.giphy_client.override(giphy_client_mock):
|
||||||
response = await test_client.get(
|
_, response = await app.asgi_client.get(
|
||||||
"/",
|
"/",
|
||||||
params={
|
params={
|
||||||
"query": "test",
|
"query": "test",
|
||||||
|
@ -41,7 +38,7 @@ async def test_index(app, test_client):
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
data = response.json()
|
data = response.json
|
||||||
assert data == {
|
assert data == {
|
||||||
"query": "test",
|
"query": "test",
|
||||||
"limit": 10,
|
"limit": 10,
|
||||||
|
@ -52,30 +49,30 @@ async def test_index(app, test_client):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_index_no_data(app, test_client):
|
async def test_index_no_data(app):
|
||||||
giphy_client_mock = mock.AsyncMock(spec=GiphyClient)
|
giphy_client_mock = mock.AsyncMock(spec=GiphyClient)
|
||||||
giphy_client_mock.search.return_value = {
|
giphy_client_mock.search.return_value = {
|
||||||
"data": [],
|
"data": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
with app.ctx.container.giphy_client.override(giphy_client_mock):
|
with app.ctx.container.giphy_client.override(giphy_client_mock):
|
||||||
response = await test_client.get("/")
|
_, response = await app.asgi_client.get("/")
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
data = response.json()
|
data = response.json
|
||||||
assert data["gifs"] == []
|
assert data["gifs"] == []
|
||||||
|
|
||||||
|
|
||||||
async def test_index_default_params(app, test_client):
|
async def test_index_default_params(app):
|
||||||
giphy_client_mock = mock.AsyncMock(spec=GiphyClient)
|
giphy_client_mock = mock.AsyncMock(spec=GiphyClient)
|
||||||
giphy_client_mock.search.return_value = {
|
giphy_client_mock.search.return_value = {
|
||||||
"data": [],
|
"data": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
with app.ctx.container.giphy_client.override(giphy_client_mock):
|
with app.ctx.container.giphy_client.override(giphy_client_mock):
|
||||||
response = await test_client.get("/")
|
_, response = await app.asgi_client.get("/")
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
data = response.json()
|
data = response.json
|
||||||
assert data["query"] == app.ctx.container.config.default.query()
|
assert data["query"] == app.ctx.container.config.default.query()
|
||||||
assert data["limit"] == app.ctx.container.config.default.limit()
|
assert data["limit"] == app.ctx.container.config.default.limit()
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
dependency-injector
|
dependency-injector
|
||||||
sanic<=21.6
|
sanic
|
||||||
|
sanic-testing
|
||||||
aiohttp
|
aiohttp
|
||||||
pyyaml
|
pyyaml
|
||||||
pytest-sanic
|
|
||||||
pytest-cov
|
pytest-cov
|
||||||
|
|
39
examples/miniapps/starlette-lifespan/README.rst
Normal file
39
examples/miniapps/starlette-lifespan/README.rst
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
Integration With Starlette-based Frameworks
|
||||||
|
===========================================
|
||||||
|
|
||||||
|
This is a `Starlette <https://www.starlette.io/>`_ +
|
||||||
|
`Dependency Injector <https://python-dependency-injector.ets-labs.org/>`_ example application
|
||||||
|
utilizing `lifespan API <https://www.starlette.io/lifespan/>`_.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Pretty much `any framework built on top of Starlette <https://www.starlette.io/third-party-packages/#frameworks>`_
|
||||||
|
supports this feature (`FastAPI <https://fastapi.tiangolo.com/advanced/events/#lifespan>`_,
|
||||||
|
`Xpresso <https://xpresso-api.dev/latest/tutorial/lifespan/>`_, etc...).
|
||||||
|
|
||||||
|
Run
|
||||||
|
---
|
||||||
|
|
||||||
|
Create virtual environment:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
python -m venv env
|
||||||
|
. env/bin/activate
|
||||||
|
|
||||||
|
Install requirements:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
To run the application do:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
python example.py
|
||||||
|
# or (logging won't be configured):
|
||||||
|
uvicorn --factory example:container.app
|
||||||
|
|
||||||
|
After that visit http://127.0.0.1:8000/ in your browser or use CLI command (``curl``, ``httpie``,
|
||||||
|
etc).
|
59
examples/miniapps/starlette-lifespan/example.py
Executable file
59
examples/miniapps/starlette-lifespan/example.py
Executable file
|
@ -0,0 +1,59 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
from logging import basicConfig, getLogger
|
||||||
|
|
||||||
|
from dependency_injector.containers import DeclarativeContainer
|
||||||
|
from dependency_injector.ext.starlette import Lifespan
|
||||||
|
from dependency_injector.providers import Factory, Resource, Self, Singleton
|
||||||
|
from starlette.applications import Starlette
|
||||||
|
from starlette.requests import Request
|
||||||
|
from starlette.responses import JSONResponse
|
||||||
|
from starlette.routing import Route
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
|
||||||
|
def init():
|
||||||
|
log = getLogger(__name__)
|
||||||
|
log.info("Inittializing resources")
|
||||||
|
yield
|
||||||
|
log.info("Cleaning up resources")
|
||||||
|
|
||||||
|
|
||||||
|
async def homepage(request: Request) -> JSONResponse:
|
||||||
|
global count
|
||||||
|
response = JSONResponse({"hello": "world", "count": count})
|
||||||
|
count += 1
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class Container(DeclarativeContainer):
|
||||||
|
__self__ = Self()
|
||||||
|
lifespan = Singleton(Lifespan, __self__)
|
||||||
|
logging = Resource(
|
||||||
|
basicConfig,
|
||||||
|
level="DEBUG",
|
||||||
|
datefmt="%Y-%m-%d %H:%M",
|
||||||
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||||
|
)
|
||||||
|
init = Resource(init)
|
||||||
|
app = Factory(
|
||||||
|
Starlette,
|
||||||
|
debug=True,
|
||||||
|
lifespan=lifespan,
|
||||||
|
routes=[Route("/", homepage)],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
container = Container()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
uvicorn.run(
|
||||||
|
container.app,
|
||||||
|
factory=True,
|
||||||
|
# NOTE: `None` prevents uvicorn from configuring logging, which is
|
||||||
|
# impossible via CLI
|
||||||
|
log_config=None,
|
||||||
|
)
|
3
examples/miniapps/starlette-lifespan/requirements.txt
Normal file
3
examples/miniapps/starlette-lifespan/requirements.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
dependency-injector
|
||||||
|
starlette
|
||||||
|
uvicorn
|
|
@ -53,7 +53,6 @@ classifiers = [
|
||||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||||
]
|
]
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
dependencies = ["six"]
|
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
yaml = ["pyyaml"]
|
yaml = ["pyyaml"]
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
flask==2.1.3
|
flask
|
||||||
werkzeug==2.2.2
|
werkzeug
|
||||||
aiohttp
|
aiohttp
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
six>=1.7.0,<=1.16.0
|
|
|
@ -1,6 +1,6 @@
|
||||||
"""Top-level package."""
|
"""Top-level package."""
|
||||||
|
|
||||||
__version__ = "4.44.0"
|
__version__ = "4.45.0"
|
||||||
"""Version number.
|
"""Version number.
|
||||||
|
|
||||||
:type: str
|
:type: str
|
||||||
|
|
|
@ -18,8 +18,6 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
yaml = None
|
yaml = None
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from . import providers, errors
|
from . import providers, errors
|
||||||
from .providers cimport __is_future_or_coroutine
|
from .providers cimport __is_future_or_coroutine
|
||||||
|
|
||||||
|
@ -201,7 +199,7 @@ class DynamicContainer(Container):
|
||||||
|
|
||||||
:rtype: None
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
for name, provider in six.iteritems(providers):
|
for name, provider in providers.items():
|
||||||
setattr(self, name, provider)
|
setattr(self, name, provider)
|
||||||
|
|
||||||
def set_provider(self, name, provider):
|
def set_provider(self, name, provider):
|
||||||
|
@ -234,7 +232,7 @@ class DynamicContainer(Container):
|
||||||
|
|
||||||
self.overridden += (overriding,)
|
self.overridden += (overriding,)
|
||||||
|
|
||||||
for name, provider in six.iteritems(overriding.providers):
|
for name, provider in overriding.providers.items():
|
||||||
try:
|
try:
|
||||||
getattr(self, name).override(provider)
|
getattr(self, name).override(provider)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
@ -250,7 +248,7 @@ class DynamicContainer(Container):
|
||||||
:rtype: None
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
overridden_providers = []
|
overridden_providers = []
|
||||||
for name, overriding_provider in six.iteritems(overriding_providers):
|
for name, overriding_provider in overriding_providers.items():
|
||||||
container_provider = getattr(self, name)
|
container_provider = getattr(self, name)
|
||||||
container_provider.override(overriding_provider)
|
container_provider.override(overriding_provider)
|
||||||
overridden_providers.append(container_provider)
|
overridden_providers.append(container_provider)
|
||||||
|
@ -266,7 +264,7 @@ class DynamicContainer(Container):
|
||||||
|
|
||||||
self.overridden = self.overridden[:-1]
|
self.overridden = self.overridden[:-1]
|
||||||
|
|
||||||
for provider in six.itervalues(self.providers):
|
for provider in self.providers.values():
|
||||||
provider.reset_last_overriding()
|
provider.reset_last_overriding()
|
||||||
|
|
||||||
def reset_override(self):
|
def reset_override(self):
|
||||||
|
@ -276,7 +274,7 @@ class DynamicContainer(Container):
|
||||||
"""
|
"""
|
||||||
self.overridden = tuple()
|
self.overridden = tuple()
|
||||||
|
|
||||||
for provider in six.itervalues(self.providers):
|
for provider in self.providers.values():
|
||||||
provider.reset_override()
|
provider.reset_override()
|
||||||
|
|
||||||
def is_auto_wiring_enabled(self):
|
def is_auto_wiring_enabled(self):
|
||||||
|
@ -495,13 +493,13 @@ class DeclarativeContainerMetaClass(type):
|
||||||
|
|
||||||
containers = {
|
containers = {
|
||||||
name: container
|
name: container
|
||||||
for name, container in six.iteritems(attributes)
|
for name, container in attributes.items()
|
||||||
if is_container(container)
|
if is_container(container)
|
||||||
}
|
}
|
||||||
|
|
||||||
cls_providers = {
|
cls_providers = {
|
||||||
name: provider
|
name: provider
|
||||||
for name, provider in six.iteritems(attributes)
|
for name, provider in attributes.items()
|
||||||
if isinstance(provider, providers.Provider) and not isinstance(provider, providers.Self)
|
if isinstance(provider, providers.Provider) and not isinstance(provider, providers.Self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -509,7 +507,7 @@ class DeclarativeContainerMetaClass(type):
|
||||||
name: provider
|
name: provider
|
||||||
for base in bases
|
for base in bases
|
||||||
if is_container(base) and base is not DynamicContainer
|
if is_container(base) and base is not DynamicContainer
|
||||||
for name, provider in six.iteritems(base.providers)
|
for name, provider in base.providers.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
all_providers = {}
|
all_providers = {}
|
||||||
|
@ -536,10 +534,10 @@ class DeclarativeContainerMetaClass(type):
|
||||||
self.set_container(cls)
|
self.set_container(cls)
|
||||||
cls.__self__ = self
|
cls.__self__ = self
|
||||||
|
|
||||||
for provider in six.itervalues(cls.providers):
|
for provider in cls.providers.values():
|
||||||
_check_provider_type(cls, provider)
|
_check_provider_type(cls, provider)
|
||||||
|
|
||||||
for provider in six.itervalues(cls.cls_providers):
|
for provider in cls.cls_providers.values():
|
||||||
if isinstance(provider, providers.CHILD_PROVIDERS):
|
if isinstance(provider, providers.CHILD_PROVIDERS):
|
||||||
provider.assign_parent(cls)
|
provider.assign_parent(cls)
|
||||||
|
|
||||||
|
@ -641,8 +639,7 @@ class DeclarativeContainerMetaClass(type):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(DeclarativeContainerMetaClass)
|
class DeclarativeContainer(Container, metaclass=DeclarativeContainerMetaClass):
|
||||||
class DeclarativeContainer(Container):
|
|
||||||
"""Declarative inversion of control container.
|
"""Declarative inversion of control container.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
@ -767,7 +764,7 @@ class DeclarativeContainer(Container):
|
||||||
|
|
||||||
cls.overridden += (overriding,)
|
cls.overridden += (overriding,)
|
||||||
|
|
||||||
for name, provider in six.iteritems(overriding.cls_providers):
|
for name, provider in overriding.cls_providers.items():
|
||||||
try:
|
try:
|
||||||
getattr(cls, name).override(provider)
|
getattr(cls, name).override(provider)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
@ -784,7 +781,7 @@ class DeclarativeContainer(Container):
|
||||||
|
|
||||||
cls.overridden = cls.overridden[:-1]
|
cls.overridden = cls.overridden[:-1]
|
||||||
|
|
||||||
for provider in six.itervalues(cls.providers):
|
for provider in cls.providers.values():
|
||||||
provider.reset_last_overriding()
|
provider.reset_last_overriding()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -795,7 +792,7 @@ class DeclarativeContainer(Container):
|
||||||
"""
|
"""
|
||||||
cls.overridden = tuple()
|
cls.overridden = tuple()
|
||||||
|
|
||||||
for provider in six.itervalues(cls.providers):
|
for provider in cls.providers.values():
|
||||||
provider.reset_override()
|
provider.reset_override()
|
||||||
|
|
||||||
|
|
||||||
|
@ -858,7 +855,7 @@ def copy(object base_container):
|
||||||
"""
|
"""
|
||||||
def _get_memo_for_matching_names(new_providers, base_providers):
|
def _get_memo_for_matching_names(new_providers, base_providers):
|
||||||
memo = {}
|
memo = {}
|
||||||
for new_provider_name, new_provider in six.iteritems(new_providers):
|
for new_provider_name, new_provider in new_providers.items():
|
||||||
if new_provider_name not in base_providers:
|
if new_provider_name not in base_providers:
|
||||||
continue
|
continue
|
||||||
source_provider = base_providers[new_provider_name]
|
source_provider = base_providers[new_provider_name]
|
||||||
|
@ -877,7 +874,7 @@ def copy(object base_container):
|
||||||
new_providers.update(providers.deepcopy(base_container.providers, memo))
|
new_providers.update(providers.deepcopy(base_container.providers, memo))
|
||||||
new_providers.update(providers.deepcopy(new_container.cls_providers, memo))
|
new_providers.update(providers.deepcopy(new_container.cls_providers, memo))
|
||||||
|
|
||||||
for name, provider in six.iteritems(new_providers):
|
for name, provider in new_providers.items():
|
||||||
setattr(new_container, name, provider)
|
setattr(new_container, name, provider)
|
||||||
return new_container
|
return new_container
|
||||||
|
|
||||||
|
|
|
@ -10,3 +10,24 @@ class Error(Exception):
|
||||||
|
|
||||||
class NoSuchProviderError(Error, AttributeError):
|
class NoSuchProviderError(Error, AttributeError):
|
||||||
"""Error that is raised when provider lookup is failed."""
|
"""Error that is raised when provider lookup is failed."""
|
||||||
|
|
||||||
|
|
||||||
|
class NonCopyableArgumentError(Error):
|
||||||
|
"""Error that is raised when provider argument is not deep-copyable."""
|
||||||
|
|
||||||
|
index: int
|
||||||
|
keyword: str
|
||||||
|
provider: object
|
||||||
|
|
||||||
|
def __init__(self, provider: object, index: int = -1, keyword: str = "") -> None:
|
||||||
|
self.provider = provider
|
||||||
|
self.index = index
|
||||||
|
self.keyword = keyword
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
s = (
|
||||||
|
f"keyword argument {self.keyword}"
|
||||||
|
if self.keyword
|
||||||
|
else f"argument at index {self.index}"
|
||||||
|
)
|
||||||
|
return f"Couldn't copy {s} for provider {self.provider!r}"
|
||||||
|
|
52
src/dependency_injector/ext/starlette.py
Normal file
52
src/dependency_injector/ext/starlette.py
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import sys
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 11): # pragma: no cover
|
||||||
|
from typing import Self
|
||||||
|
else: # pragma: no cover
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
from dependency_injector.containers import Container
|
||||||
|
|
||||||
|
|
||||||
|
class Lifespan:
|
||||||
|
"""A starlette lifespan handler performing container resource initialization and shutdown.
|
||||||
|
|
||||||
|
See https://www.starlette.io/lifespan/ for details.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from dependency_injector.containers import DeclarativeContainer
|
||||||
|
from dependency_injector.ext.starlette import Lifespan
|
||||||
|
from dependency_injector.providers import Factory, Self, Singleton
|
||||||
|
from starlette.applications import Starlette
|
||||||
|
|
||||||
|
class Container(DeclarativeContainer):
|
||||||
|
__self__ = Self()
|
||||||
|
lifespan = Singleton(Lifespan, __self__)
|
||||||
|
app = Factory(Starlette, lifespan=lifespan)
|
||||||
|
|
||||||
|
:param container: container instance
|
||||||
|
"""
|
||||||
|
|
||||||
|
container: Container
|
||||||
|
|
||||||
|
def __init__(self, container: Container) -> None:
|
||||||
|
self.container = container
|
||||||
|
|
||||||
|
def __call__(self, app: Any) -> Self:
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aenter__(self) -> None:
|
||||||
|
result = self.container.init_resources()
|
||||||
|
|
||||||
|
if result is not None:
|
||||||
|
await result
|
||||||
|
|
||||||
|
async def __aexit__(self, *exc_info: Any) -> None:
|
||||||
|
result = self.container.shutdown_resources()
|
||||||
|
|
||||||
|
if result is not None:
|
||||||
|
await result
|
|
@ -530,7 +530,21 @@ def is_delegated(instance: Any) -> bool: ...
|
||||||
def represent_provider(provider: Provider, provides: Any) -> str: ...
|
def represent_provider(provider: Provider, provides: Any) -> str: ...
|
||||||
|
|
||||||
|
|
||||||
def deepcopy(instance: Any, memo: Optional[_Dict[Any, Any]] = None): Any: ...
|
def deepcopy(instance: Any, memo: Optional[_Dict[Any, Any]] = None) -> Any: ...
|
||||||
|
|
||||||
|
|
||||||
|
def deepcopy_args(
|
||||||
|
provider: Provider[Any],
|
||||||
|
args: Tuple[Any, ...],
|
||||||
|
memo: Optional[_Dict[int, Any]] = None,
|
||||||
|
) -> Tuple[Any, ...]: ...
|
||||||
|
|
||||||
|
|
||||||
|
def deepcopy_kwargs(
|
||||||
|
provider: Provider[Any],
|
||||||
|
kwargs: _Dict[str, Any],
|
||||||
|
memo: Optional[_Dict[int, Any]] = None,
|
||||||
|
) -> Dict[str, Any]: ...
|
||||||
|
|
||||||
|
|
||||||
def merge_dicts(dict1: _Dict[Any, Any], dict2: _Dict[Any, Any]) -> _Dict[Any, Any]: ...
|
def merge_dicts(dict1: _Dict[Any, Any], dict2: _Dict[Any, Any]) -> _Dict[Any, Any]: ...
|
||||||
|
|
|
@ -71,6 +71,7 @@ except ImportError:
|
||||||
from .errors import (
|
from .errors import (
|
||||||
Error,
|
Error,
|
||||||
NoSuchProviderError,
|
NoSuchProviderError,
|
||||||
|
NonCopyableArgumentError,
|
||||||
)
|
)
|
||||||
|
|
||||||
cimport cython
|
cimport cython
|
||||||
|
@ -1252,8 +1253,8 @@ cdef class Callable(Provider):
|
||||||
|
|
||||||
copied = _memorized_duplicate(self, memo)
|
copied = _memorized_duplicate(self, memo)
|
||||||
copied.set_provides(_copy_if_provider(self.provides, memo))
|
copied.set_provides(_copy_if_provider(self.provides, memo))
|
||||||
copied.set_args(*deepcopy(self.args, memo))
|
copied.set_args(*deepcopy_args(self, self.args, memo))
|
||||||
copied.set_kwargs(**deepcopy(self.kwargs, memo))
|
copied.set_kwargs(**deepcopy_kwargs(self, self.kwargs, memo))
|
||||||
self._copy_overridings(copied, memo)
|
self._copy_overridings(copied, memo)
|
||||||
return copied
|
return copied
|
||||||
|
|
||||||
|
@ -2539,8 +2540,8 @@ cdef class Factory(Provider):
|
||||||
|
|
||||||
copied = _memorized_duplicate(self, memo)
|
copied = _memorized_duplicate(self, memo)
|
||||||
copied.set_provides(_copy_if_provider(self.provides, memo))
|
copied.set_provides(_copy_if_provider(self.provides, memo))
|
||||||
copied.set_args(*deepcopy(self.args, memo))
|
copied.set_args(*deepcopy_args(self, self.args, memo))
|
||||||
copied.set_kwargs(**deepcopy(self.kwargs, memo))
|
copied.set_kwargs(**deepcopy_kwargs(self, self.kwargs, memo))
|
||||||
copied.set_attributes(**deepcopy(self.attributes, memo))
|
copied.set_attributes(**deepcopy(self.attributes, memo))
|
||||||
self._copy_overridings(copied, memo)
|
self._copy_overridings(copied, memo)
|
||||||
return copied
|
return copied
|
||||||
|
@ -2838,8 +2839,8 @@ cdef class BaseSingleton(Provider):
|
||||||
|
|
||||||
copied = _memorized_duplicate(self, memo)
|
copied = _memorized_duplicate(self, memo)
|
||||||
copied.set_provides(_copy_if_provider(self.provides, memo))
|
copied.set_provides(_copy_if_provider(self.provides, memo))
|
||||||
copied.set_args(*deepcopy(self.args, memo))
|
copied.set_args(*deepcopy_args(self, self.args, memo))
|
||||||
copied.set_kwargs(**deepcopy(self.kwargs, memo))
|
copied.set_kwargs(**deepcopy_kwargs(self, self.kwargs, memo))
|
||||||
copied.set_attributes(**deepcopy(self.attributes, memo))
|
copied.set_attributes(**deepcopy(self.attributes, memo))
|
||||||
self._copy_overridings(copied, memo)
|
self._copy_overridings(copied, memo)
|
||||||
return copied
|
return copied
|
||||||
|
@ -3221,7 +3222,7 @@ cdef class ThreadLocalSingleton(BaseSingleton):
|
||||||
return future_result
|
return future_result
|
||||||
|
|
||||||
self._storage.instance = instance
|
self._storage.instance = instance
|
||||||
finally:
|
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
def _async_init_instance(self, future_result, result):
|
def _async_init_instance(self, future_result, result):
|
||||||
|
@ -3451,7 +3452,7 @@ cdef class List(Provider):
|
||||||
return copied
|
return copied
|
||||||
|
|
||||||
copied = _memorized_duplicate(self, memo)
|
copied = _memorized_duplicate(self, memo)
|
||||||
copied.set_args(*deepcopy(self.args, memo))
|
copied.set_args(*deepcopy_args(self, self.args, memo))
|
||||||
self._copy_overridings(copied, memo)
|
self._copy_overridings(copied, memo)
|
||||||
return copied
|
return copied
|
||||||
|
|
||||||
|
@ -3674,8 +3675,8 @@ cdef class Resource(Provider):
|
||||||
|
|
||||||
copied = _memorized_duplicate(self, memo)
|
copied = _memorized_duplicate(self, memo)
|
||||||
copied.set_provides(_copy_if_provider(self.provides, memo))
|
copied.set_provides(_copy_if_provider(self.provides, memo))
|
||||||
copied.set_args(*deepcopy(self.args, memo))
|
copied.set_args(*deepcopy_args(self, self.args, memo))
|
||||||
copied.set_kwargs(**deepcopy(self.kwargs, memo))
|
copied.set_kwargs(**deepcopy_kwargs(self, self.kwargs, memo))
|
||||||
|
|
||||||
self._copy_overridings(copied, memo)
|
self._copy_overridings(copied, memo)
|
||||||
|
|
||||||
|
@ -4525,8 +4526,8 @@ cdef class MethodCaller(Provider):
|
||||||
|
|
||||||
copied = _memorized_duplicate(self, memo)
|
copied = _memorized_duplicate(self, memo)
|
||||||
copied.set_provides(_copy_if_provider(self.provides, memo))
|
copied.set_provides(_copy_if_provider(self.provides, memo))
|
||||||
copied.set_args(*deepcopy(self.args, memo))
|
copied.set_args(*deepcopy_args(self, self.args, memo))
|
||||||
copied.set_kwargs(**deepcopy(self.kwargs, memo))
|
copied.set_kwargs(**deepcopy_kwargs(self, self.kwargs, memo))
|
||||||
self._copy_overridings(copied, memo)
|
self._copy_overridings(copied, memo)
|
||||||
return copied
|
return copied
|
||||||
|
|
||||||
|
@ -4927,6 +4928,48 @@ cpdef object deepcopy(object instance, dict memo=None):
|
||||||
return copy.deepcopy(instance, memo)
|
return copy.deepcopy(instance, memo)
|
||||||
|
|
||||||
|
|
||||||
|
cpdef tuple deepcopy_args(
|
||||||
|
Provider provider,
|
||||||
|
tuple args,
|
||||||
|
dict[int, object] memo = None,
|
||||||
|
):
|
||||||
|
"""A wrapper for deepcopy for positional arguments.
|
||||||
|
|
||||||
|
Used to improve debugability of objects that cannot be deep-copied.
|
||||||
|
"""
|
||||||
|
|
||||||
|
cdef list[object] out = []
|
||||||
|
|
||||||
|
for i, arg in enumerate(args):
|
||||||
|
try:
|
||||||
|
out.append(copy.deepcopy(arg, memo))
|
||||||
|
except Exception as e:
|
||||||
|
raise NonCopyableArgumentError(provider, index=i) from e
|
||||||
|
|
||||||
|
return tuple(out)
|
||||||
|
|
||||||
|
|
||||||
|
cpdef dict[str, object] deepcopy_kwargs(
|
||||||
|
Provider provider,
|
||||||
|
dict[str, object] kwargs,
|
||||||
|
dict[int, object] memo = None,
|
||||||
|
):
|
||||||
|
"""A wrapper for deepcopy for keyword arguments.
|
||||||
|
|
||||||
|
Used to improve debugability of objects that cannot be deep-copied.
|
||||||
|
"""
|
||||||
|
|
||||||
|
cdef dict[str, object] out = {}
|
||||||
|
|
||||||
|
for name, arg in kwargs.items():
|
||||||
|
try:
|
||||||
|
out[name] = copy.deepcopy(arg, memo)
|
||||||
|
except Exception as e:
|
||||||
|
raise NonCopyableArgumentError(provider, keyword=name) from e
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def __add_sys_streams(memo):
|
def __add_sys_streams(memo):
|
||||||
"""Add system streams to memo dictionary.
|
"""Add system streams to memo dictionary.
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from dependency_injector import providers
|
from dependency_injector import providers
|
||||||
from pydantic import BaseSettings as PydanticSettings
|
from pydantic_settings import BaseSettings as PydanticSettings
|
||||||
|
|
||||||
|
|
||||||
# Test 1: to check the getattr
|
# Test 1: to check the getattr
|
||||||
|
|
41
tests/unit/ext/test_starlette.py
Normal file
41
tests/unit/ext/test_starlette.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
from typing import AsyncIterator, Iterator
|
||||||
|
from unittest.mock import ANY
|
||||||
|
|
||||||
|
from pytest import mark
|
||||||
|
|
||||||
|
from dependency_injector.containers import DeclarativeContainer
|
||||||
|
from dependency_injector.ext.starlette import Lifespan
|
||||||
|
from dependency_injector.providers import Resource
|
||||||
|
|
||||||
|
|
||||||
|
class TestLifespan:
|
||||||
|
@mark.parametrize("sync", [False, True])
|
||||||
|
@mark.asyncio
|
||||||
|
async def test_context_manager(self, sync: bool) -> None:
|
||||||
|
init, shutdown = False, False
|
||||||
|
|
||||||
|
def sync_resource() -> Iterator[None]:
|
||||||
|
nonlocal init, shutdown
|
||||||
|
|
||||||
|
init = True
|
||||||
|
yield
|
||||||
|
shutdown = True
|
||||||
|
|
||||||
|
async def async_resource() -> AsyncIterator[None]:
|
||||||
|
nonlocal init, shutdown
|
||||||
|
|
||||||
|
init = True
|
||||||
|
yield
|
||||||
|
shutdown = True
|
||||||
|
|
||||||
|
class Container(DeclarativeContainer):
|
||||||
|
x = Resource(sync_resource if sync else async_resource)
|
||||||
|
|
||||||
|
container = Container()
|
||||||
|
lifespan = Lifespan(container)
|
||||||
|
|
||||||
|
async with lifespan(ANY) as scope:
|
||||||
|
assert scope is None
|
||||||
|
assert init
|
||||||
|
|
||||||
|
assert shutdown
|
|
@ -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()
|
65
tests/unit/providers/utils/test_deepcopy_py3.py
Normal file
65
tests/unit/providers/utils/test_deepcopy_py3.py
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import sys
|
||||||
|
from typing import Any, Dict, NoReturn
|
||||||
|
|
||||||
|
from pytest import raises
|
||||||
|
|
||||||
|
from dependency_injector.errors import NonCopyableArgumentError
|
||||||
|
from dependency_injector.providers import (
|
||||||
|
Provider,
|
||||||
|
deepcopy,
|
||||||
|
deepcopy_args,
|
||||||
|
deepcopy_kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NonCopiable:
|
||||||
|
def __deepcopy__(self, memo: Dict[int, Any]) -> NoReturn:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def test_deepcopy_streams_not_copied() -> None:
|
||||||
|
l = [sys.stdin, sys.stdout, sys.stderr]
|
||||||
|
assert deepcopy(l) == l
|
||||||
|
|
||||||
|
|
||||||
|
def test_deepcopy_args() -> None:
|
||||||
|
provider = Provider[None]()
|
||||||
|
copiable = NonCopiable()
|
||||||
|
memo: Dict[int, Any] = {id(copiable): copiable}
|
||||||
|
|
||||||
|
assert deepcopy_args(provider, (1, copiable), memo) == (1, copiable)
|
||||||
|
|
||||||
|
|
||||||
|
def test_deepcopy_args_non_copiable() -> None:
|
||||||
|
provider = Provider[None]()
|
||||||
|
copiable = NonCopiable()
|
||||||
|
memo: Dict[int, Any] = {id(copiable): copiable}
|
||||||
|
|
||||||
|
with raises(
|
||||||
|
NonCopyableArgumentError,
|
||||||
|
match=r"^Couldn't copy argument at index 3 for provider ",
|
||||||
|
):
|
||||||
|
deepcopy_args(provider, (1, copiable, object(), NonCopiable()), memo)
|
||||||
|
|
||||||
|
|
||||||
|
def test_deepcopy_kwargs() -> None:
|
||||||
|
provider = Provider[None]()
|
||||||
|
copiable = NonCopiable()
|
||||||
|
memo: Dict[int, Any] = {id(copiable): copiable}
|
||||||
|
|
||||||
|
assert deepcopy_kwargs(provider, {"x": 1, "y": copiable}, memo) == {
|
||||||
|
"x": 1,
|
||||||
|
"y": copiable,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_deepcopy_kwargs_non_copiable() -> None:
|
||||||
|
provider = Provider[None]()
|
||||||
|
copiable = NonCopiable()
|
||||||
|
memo: Dict[int, Any] = {id(copiable): copiable}
|
||||||
|
|
||||||
|
with raises(
|
||||||
|
NonCopyableArgumentError,
|
||||||
|
match=r"^Couldn't copy keyword argument z for provider ",
|
||||||
|
):
|
||||||
|
deepcopy_kwargs(provider, {"x": 1, "y": copiable, "z": NonCopiable()}, memo)
|
|
@ -1,11 +1,9 @@
|
||||||
from flask import Flask, jsonify, request, current_app, session, g
|
from flask import Flask, jsonify, request, current_app, session, g
|
||||||
from flask import _request_ctx_stack, _app_ctx_stack
|
|
||||||
from dependency_injector import containers, providers
|
from dependency_injector import containers, providers
|
||||||
from dependency_injector.wiring import inject, Provide
|
from dependency_injector.wiring import inject, Provide
|
||||||
|
|
||||||
# This is here for testing wiring bypasses these objects without crashing
|
# This is here for testing wiring bypasses these objects without crashing
|
||||||
request, current_app, session, g # noqa
|
request, current_app, session, g # noqa
|
||||||
_request_ctx_stack, _app_ctx_stack # noqa
|
|
||||||
|
|
||||||
|
|
||||||
class Service:
|
class Service:
|
||||||
|
|
29
tox.ini
29
tox.ini
|
@ -7,18 +7,16 @@ envlist=
|
||||||
deps=
|
deps=
|
||||||
pytest
|
pytest
|
||||||
pytest-asyncio
|
pytest-asyncio
|
||||||
# TODO: Hotfix, remove when fixed https://github.com/aio-libs/aiohttp/issues/5107
|
|
||||||
typing_extensions
|
|
||||||
httpx
|
httpx
|
||||||
fastapi
|
fastapi
|
||||||
flask<2.2
|
flask
|
||||||
aiohttp<=3.9.0b1
|
aiohttp
|
||||||
numpy
|
numpy
|
||||||
scipy
|
scipy
|
||||||
boto3
|
boto3
|
||||||
mypy_boto3_s3
|
mypy_boto3_s3
|
||||||
pydantic<2
|
pydantic-settings
|
||||||
werkzeug<=2.2.2
|
werkzeug
|
||||||
extras=
|
extras=
|
||||||
yaml
|
yaml
|
||||||
commands = pytest -c tests/.configs/pytest.ini
|
commands = pytest -c tests/.configs/pytest.ini
|
||||||
|
@ -37,17 +35,16 @@ deps =
|
||||||
v2: pydantic-settings
|
v2: pydantic-settings
|
||||||
pytest
|
pytest
|
||||||
pytest-asyncio
|
pytest-asyncio
|
||||||
-rrequirements.txt
|
|
||||||
typing_extensions
|
typing_extensions
|
||||||
httpx
|
httpx
|
||||||
fastapi
|
fastapi
|
||||||
flask<2.2
|
flask
|
||||||
aiohttp<=3.9.0b1
|
aiohttp
|
||||||
numpy
|
numpy
|
||||||
scipy
|
scipy
|
||||||
boto3
|
boto3
|
||||||
mypy_boto3_s3
|
mypy_boto3_s3
|
||||||
werkzeug<=2.2.2
|
werkzeug
|
||||||
commands = pytest -c tests/.configs/pytest.ini -m pydantic
|
commands = pytest -c tests/.configs/pytest.ini -m pydantic
|
||||||
|
|
||||||
[testenv:coveralls]
|
[testenv:coveralls]
|
||||||
|
@ -69,9 +66,9 @@ deps=
|
||||||
pytest
|
pytest
|
||||||
pytest-asyncio
|
pytest-asyncio
|
||||||
httpx
|
httpx
|
||||||
flask<2.2
|
flask
|
||||||
pydantic<2
|
pydantic-settings
|
||||||
werkzeug<=2.2.2
|
werkzeug
|
||||||
fastapi
|
fastapi
|
||||||
boto3
|
boto3
|
||||||
mypy_boto3_s3
|
mypy_boto3_s3
|
||||||
|
@ -83,8 +80,8 @@ commands = pytest -c tests/.configs/pytest-py35.ini
|
||||||
[testenv:pylint]
|
[testenv:pylint]
|
||||||
deps=
|
deps=
|
||||||
pylint
|
pylint
|
||||||
flask<2.2
|
flask
|
||||||
werkzeug<=2.2.2
|
werkzeug
|
||||||
commands=
|
commands=
|
||||||
- pylint -f colorized src/dependency_injector
|
- pylint -f colorized src/dependency_injector
|
||||||
|
|
||||||
|
@ -105,7 +102,7 @@ commands=
|
||||||
[testenv:mypy]
|
[testenv:mypy]
|
||||||
deps=
|
deps=
|
||||||
typing_extensions
|
typing_extensions
|
||||||
pydantic<2
|
pydantic-settings
|
||||||
mypy
|
mypy
|
||||||
commands=
|
commands=
|
||||||
mypy tests/typing
|
mypy tests/typing
|
||||||
|
|
Loading…
Reference in New Issue
Block a user