From 89e95dae5393534fc1c5f5e5300b4b57518c3df2 Mon Sep 17 00:00:00 2001 From: Andrey Tikhonov <17@itishka.org> Date: Thu, 8 Feb 2024 19:12:42 +0100 Subject: [PATCH] coverage and flask --- examples/flask_app.py | 66 ++++++++++++++++++++++ examples/real_world/requirements_test.txt | 1 + requirements/flask-302.txt | 2 + requirements/flask-latest.txt | 2 + requirements/test.txt | 3 +- src/dishka/integrations/flask.py | 41 ++++++++++++++ tests/integrations/flask/__init__.py | 3 + tests/integrations/flask/test_flask.py | 69 +++++++++++++++++++++++ tox.ini | 27 +++++++-- 9 files changed, 207 insertions(+), 7 deletions(-) create mode 100644 examples/flask_app.py create mode 100644 requirements/flask-302.txt create mode 100644 requirements/flask-latest.txt create mode 100644 src/dishka/integrations/flask.py create mode 100644 tests/integrations/flask/__init__.py create mode 100644 tests/integrations/flask/test_flask.py diff --git a/examples/flask_app.py b/examples/flask_app.py new file mode 100644 index 00000000..42916431 --- /dev/null +++ b/examples/flask_app.py @@ -0,0 +1,66 @@ +from abc import abstractmethod +from typing import Annotated, Protocol + +from flask import Flask + +from dishka import ( + Provider, Scope, provide, +) +from dishka.integrations.flask import ( + Depends, inject, setup_dishka, +) + + +# app core +class DbGateway(Protocol): + @abstractmethod + def get(self) -> str: + raise NotImplementedError + + +class FakeDbGateway(DbGateway): + def get(self) -> str: + return "Hello" + + +class Interactor: + def __init__(self, db: DbGateway): + self.db = db + + def __call__(self) -> str: + return self.db.get() + + +# app dependency logic +class AdaptersProvider(Provider): + @provide(scope=Scope.REQUEST) + def get_db(self) -> DbGateway: + return FakeDbGateway() + + +class InteractorProvider(Provider): + i1 = provide(Interactor, scope=Scope.REQUEST) + + +# presentation layer +app = Flask(__name__) + + +@app.get("/") +@inject +def index( + *, + interactor: Annotated[Interactor, Depends()], +) -> str: + result = interactor() + return result + + +container = setup_dishka( + providers=[AdaptersProvider(), InteractorProvider()], + app=app, +) +try: + app.run() +finally: + container.close() diff --git a/examples/real_world/requirements_test.txt b/examples/real_world/requirements_test.txt index acd6ee64..97a78740 100644 --- a/examples/real_world/requirements_test.txt +++ b/examples/real_world/requirements_test.txt @@ -3,6 +3,7 @@ pytest==7.* pytest-asyncio==0.23.* pytest-repeat==0.9.* +pytest-cov==4.1.0 httpx==0.26.* asgi_lifespan==2.1.* diff --git a/requirements/flask-302.txt b/requirements/flask-302.txt new file mode 100644 index 00000000..9e5a8818 --- /dev/null +++ b/requirements/flask-302.txt @@ -0,0 +1,2 @@ +-r test.txt +Flask==3.0.2 \ No newline at end of file diff --git a/requirements/flask-latest.txt b/requirements/flask-latest.txt new file mode 100644 index 00000000..9b121ddb --- /dev/null +++ b/requirements/flask-latest.txt @@ -0,0 +1,2 @@ +-r test.txt +Flask \ No newline at end of file diff --git a/requirements/test.txt b/requirements/test.txt index 30a4d50b..c8dc2786 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,3 +1,4 @@ pytest==7.* pytest-asyncio==0.23.* -pytest-repeat==0.9.* \ No newline at end of file +pytest-repeat==0.9.* +pytest-cov==4.1.0 \ No newline at end of file diff --git a/src/dishka/integrations/flask.py b/src/dishka/integrations/flask.py new file mode 100644 index 00000000..ff29b9fb --- /dev/null +++ b/src/dishka/integrations/flask.py @@ -0,0 +1,41 @@ +__all__ = [ + "inject", "setup_dishka", "Depends", +] + +from typing import Sequence + +from flask import Flask, Request, g, request + +from dishka import Container, Provider, make_container +from .base import Depends, wrap_injection + + +def inject(func): + return wrap_injection( + func=func, + remove_depends=True, + container_getter=lambda _, p: g.dishka_container, + additional_params=[], + is_async=False, + ) + + +class ContainerMiddleware: + def __init__(self, container): + self.container = container + + def enter_request(self): + g.dishka_container_wrapper = self.container({Request: request}) + g.dishka_container = g.dishka_container_wrapper.__enter__() + + def exit_request(self, *_args, **_kwargs): + g.dishka_container_wrapper.__exit__(None, None, None) + + +def setup_dishka(providers: Sequence[Provider], app: Flask) -> Container: + container_wrapper = make_container(*providers) + container = container_wrapper.__enter__() + middleware = ContainerMiddleware(container) + app.before_request(middleware.enter_request) + app.teardown_appcontext(middleware.exit_request) + return container diff --git a/tests/integrations/flask/__init__.py b/tests/integrations/flask/__init__.py new file mode 100644 index 00000000..601f9ed8 --- /dev/null +++ b/tests/integrations/flask/__init__.py @@ -0,0 +1,3 @@ +import pytest + +pytest.importorskip("flask") diff --git a/tests/integrations/flask/test_flask.py b/tests/integrations/flask/test_flask.py new file mode 100644 index 00000000..d8c8b1d6 --- /dev/null +++ b/tests/integrations/flask/test_flask.py @@ -0,0 +1,69 @@ +from contextlib import contextmanager +from typing import Annotated +from unittest.mock import Mock + +from flask import Flask + +from dishka.integrations.flask import Depends, inject, setup_dishka +from ..common import ( + APP_DEP_VALUE, + REQUEST_DEP_VALUE, + AppDep, + AppProvider, + RequestDep, +) + + +@contextmanager +def dishka_app(view, provider): + app = Flask(__name__) + app.get("/")(inject(view)) + container = setup_dishka( + providers=[provider], + app=app, + ) + yield app + container.close() + + +def handle_with_app( + a: Annotated[AppDep, Depends()], + mock: Annotated[Mock, Depends()], +) -> None: + mock(a) + + +def test_app_dependency(app_provider: AppProvider): + with dishka_app(handle_with_app, app_provider) as app: + app.test_client().get("/") + app_provider.mock.assert_called_with(APP_DEP_VALUE) + app_provider.app_released.assert_not_called() + app_provider.app_released.assert_called() + + +def handle_with_request( + a: Annotated[RequestDep, Depends()], + mock: Annotated[Mock, Depends()], +) -> None: + mock(a) + + +def test_request_dependency(app_provider: AppProvider): + with dishka_app(handle_with_request, app_provider) as app: + app.test_client().get("/") + app_provider.mock.assert_called_with(REQUEST_DEP_VALUE) + app_provider.request_released.assert_called_once() + + +def test_request_dependency2(app_provider: AppProvider): + with dishka_app(handle_with_request, app_provider) as app: + app.test_client().get("/") + app_provider.mock.assert_called_with(REQUEST_DEP_VALUE) + app_provider.request_released.assert_called_once() + app_provider.mock.assert_called_with(REQUEST_DEP_VALUE) + app_provider.mock.reset_mock() + app_provider.request_released.assert_called_once() + app_provider.request_released.reset_mock() + app.test_client().get("/") + app_provider.mock.assert_called_with(REQUEST_DEP_VALUE) + app_provider.request_released.assert_called_once() diff --git a/tox.ini b/tox.ini index 60f5874a..dcd3b7ab 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,16 @@ [tox] requires = tox>=4 -env_list = unit,fastapi-{0092,0109},aiogram-330,real_world_example,telebot-415 +env_list = + unit, + real_world_example, + fastapi-{0092,0109}, + flask-302, + aiogram-330, + telebot-415, + +[pytest] +addopts = --cov=dishka --cov-append -v [testenv] deps = @@ -12,19 +21,25 @@ deps = aiogram-330: -r requirements/aiogram-330.txt telebot-latest: -r requirements/telebot-latest.txt telebot-415: -r requirements/telebot-415.txt + flask-latest: -r requirements/flask-latest.txt + flask-302: -r requirements/flask-302.txt commands = - fastapi: pytest -v tests/integrations/fastapi - aiogram: pytest -v tests/integrations/aiogram - telebot: pytest -v tests/integrations/telebot + fastapi: pytest tests/integrations/fastapi + aiogram: pytest tests/integrations/aiogram + telebot: pytest tests/integrations/telebot + flask: pytest tests/integrations/flask + +package = editable + [testenv:unit] deps = -r requirements/test.txt -commands = pytest -v tests/unit +commands = pytest tests/unit [testenv:latest] install_commands = python -m pip install -U {opts} {packages} [testenv:real_world_example] deps = -r examples/real_world/requirements_test.txt -commands = pytest -v examples/real_world/tests/ +commands = pytest examples/real_world/tests/ \ No newline at end of file