mirror of
https://github.com/ets-labs/python-dependency-injector.git
synced 2025-01-27 17:54:35 +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
|
||||
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
|
||||
--------
|
||||
- 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::
|
||||
|
||||
``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
|
||||
|
|
|
@ -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 <https://www.docker.com/>`_ and
|
||||
`docker-compose <https://docs.docker.com/compose/>`_ in this tutorial. Let's check the versions:
|
||||
We will use `docker compose <https://docs.docker.com/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 <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.
|
||||
|
||||
|
@ -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%]
|
||||
|
|
|
@ -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%]
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -2,4 +2,5 @@ dependency-injector
|
|||
aiohttp
|
||||
pyyaml
|
||||
pytest-aiohttp
|
||||
pytest-asyncio
|
||||
pytest-cov
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.10-buster
|
||||
FROM python:3.13-bookworm
|
||||
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
|
|
|
@ -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%]
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.10-buster
|
||||
FROM python:3.13-bookworm
|
||||
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
|
|
|
@ -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%
|
||||
TOTAL 52 7 87%
|
||||
|
|
|
@ -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]:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"""Services module."""
|
||||
|
||||
from aioredis import Redis
|
||||
from redis.asyncio import Redis
|
||||
|
||||
|
||||
class Service:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
dependency-injector
|
||||
fastapi
|
||||
uvicorn
|
||||
aioredis
|
||||
redis>=4.2
|
||||
|
||||
# For testing:
|
||||
pytest
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.10-buster
|
||||
FROM python:3.13-bookworm
|
||||
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV HOST=0.0.0.0
|
||||
|
|
|
@ -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%]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
dependency-injector
|
||||
fastapi
|
||||
fastapi[standard]
|
||||
uvicorn
|
||||
pyyaml
|
||||
sqlalchemy
|
||||
|
|
|
@ -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%]
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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%]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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%]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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%]
|
||||
|
|
|
@ -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%]
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
dependency-injector
|
||||
sanic<=21.6
|
||||
sanic
|
||||
sanic-testing
|
||||
aiohttp
|
||||
pyyaml
|
||||
pytest-sanic
|
||||
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",
|
||||
]
|
||||
dynamic = ["version"]
|
||||
dependencies = ["six"]
|
||||
|
||||
[project.optional-dependencies]
|
||||
yaml = ["pyyaml"]
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
flask==2.1.3
|
||||
werkzeug==2.2.2
|
||||
flask
|
||||
werkzeug
|
||||
aiohttp
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
six>=1.7.0,<=1.16.0
|
|
@ -1,6 +1,6 @@
|
|||
"""Top-level package."""
|
||||
|
||||
__version__ = "4.44.0"
|
||||
__version__ = "4.45.0"
|
||||
"""Version number.
|
||||
|
||||
:type: str
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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}"
|
||||
|
|
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 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]: ...
|
||||
|
|
|
@ -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
|
||||
|
@ -3221,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:
|
||||
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
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 _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:
|
||||
|
|
29
tox.ini
29
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
|
||||
|
@ -37,17 +35,16 @@ deps =
|
|||
v2: pydantic-settings
|
||||
pytest
|
||||
pytest-asyncio
|
||||
-rrequirements.txt
|
||||
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 +66,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 +80,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 +102,7 @@ commands=
|
|||
[testenv:mypy]
|
||||
deps=
|
||||
typing_extensions
|
||||
pydantic<2
|
||||
pydantic-settings
|
||||
mypy
|
||||
commands=
|
||||
mypy tests/typing
|
||||
|
|
Loading…
Reference in New Issue
Block a user