├── tests ├── __init__.py ├── unit │ ├── __init__.py │ ├── container │ │ ├── __init__.py │ │ ├── override │ │ │ ├── __init__.py │ │ │ └── test_provide_all.py │ │ ├── pep695_new_syntax.py │ │ ├── type_alias_type_provider.py │ │ ├── test_dynamic.py │ │ ├── test_concurrency.py │ │ └── test_recursive.py │ ├── plotter │ │ ├── __init__.py │ │ └── test_wrappers.py │ ├── text_rendering │ │ ├── __init__.py │ │ ├── test_name.py │ │ └── test_suggestion.py │ ├── test_composite.py │ ├── test_quickstart_example.py │ ├── test_context_proxy.py │ ├── test_entities.py │ ├── test_registry.py │ └── test_type_match.py └── integrations │ ├── __init__.py │ ├── base │ ├── __init__.py │ ├── test_wrap_injection.py │ └── test_add_params.py │ ├── arq │ ├── __init__.py │ └── test_arq.py │ ├── click │ └── __init__.py │ ├── flask │ └── __init__.py │ ├── grpcio │ ├── __init__.py │ └── my_grpc_service.proto │ ├── sanic │ └── __init__.py │ ├── aiogram │ ├── __init__.py │ └── conftest.py │ ├── aiohttp │ └── __init__.py │ ├── celery │ └── __init__.py │ ├── fastapi │ └── __init__.py │ ├── litestar │ └── __init__.py │ ├── taskiq │ ├── __init__.py │ └── utils.py │ ├── telebot │ └── __init__.py │ ├── faststream │ └── __init__.py │ ├── starlette │ ├── __init__.py │ └── test_starlette.py │ ├── aiogram_dialog │ ├── __init__.py │ └── conftest.py │ └── conftest.py ├── src └── dishka │ ├── py.typed │ ├── entities │ ├── __init__.py │ ├── component.py │ ├── validation_settigs.py │ ├── type_alias_type.py │ ├── validation_settings.py │ ├── factory_type.py │ ├── scope.py │ ├── provides_marker.py │ ├── depends_marker.py │ └── key.py │ ├── integrations │ ├── __init__.py │ ├── exceptions.py │ ├── faststream │ │ └── __init__.py │ ├── celery.py │ ├── click.py │ ├── telebot.py │ ├── sanic.py │ └── flask.py │ ├── exception_base.py │ ├── text_rendering │ ├── __init__.py │ └── name.py │ ├── plotter │ ├── __init__.py │ ├── wrappers.py │ └── model.py │ ├── dependency_source │ ├── __init__.py │ ├── alias.py │ ├── context_var.py │ └── composite.py │ ├── provider │ ├── __init__.py │ ├── base_provider.py │ ├── root_context.py │ ├── make_alias.py │ ├── make_context_var.py │ ├── make_decorator.py │ └── unpack_provides.py │ ├── container_objects.py │ ├── _adaptix │ ├── common.py │ └── type_tools │ │ ├── __init__.py │ │ ├── norm_utils.py │ │ ├── constants.py │ │ ├── fundamentals.py │ │ ├── implicit_params.py │ │ └── type_evaler.py │ ├── context_proxy.py │ └── __init__.py ├── examples ├── integrations │ ├── __init__.py │ ├── click_app │ │ ├── __init__.py │ │ ├── sync_command.py │ │ └── async_command.py │ ├── grpcio │ │ ├── __init__.py │ │ ├── pb2 │ │ │ ├── __init__.py │ │ │ ├── service_pb2.pyi │ │ │ └── service_pb2.py │ │ ├── services │ │ │ ├── __init__.py │ │ │ └── uuid_service.py │ │ ├── di.py │ │ ├── proto │ │ │ └── service.proto │ │ └── grpc_client.py │ ├── celery_app │ │ ├── __init__.py │ │ ├── with_inject.py │ │ └── with_task_cls.py │ ├── arq │ │ ├── app.py │ │ ├── run_with_settings.py │ │ └── run_with_worker.py │ ├── taskiq_app.py │ ├── telebot_bot.py │ ├── faststream_app.py │ ├── aiohttp_app.py │ ├── flask_app.py │ ├── sanic_app.py │ ├── litestar_app.py │ ├── starlette_app.py │ ├── aiogram_bot.py │ └── fastapi_app.py ├── real_world │ ├── myapp │ │ ├── __init__.py │ │ ├── api_client.py │ │ ├── presentation_web.py │ │ ├── presentation_bot.py │ │ ├── ioc.py │ │ ├── db.py │ │ └── use_cases.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_web.py │ │ └── test_add_products.py │ ├── requirements.txt │ ├── requirements_test.txt │ ├── main_bot.py │ └── main_web.py ├── ruff.toml ├── sync_simple.py └── async_simple.py ├── requirements ├── arq-latest.txt ├── arq-0250.txt ├── flask-latest.txt ├── taskiq-latest.txt ├── aiogram-330.txt ├── aiogram-latest.txt ├── aiohttp-latest.txt ├── celery-latest.txt ├── click-latest.txt ├── flask-302.txt ├── litestar-latest.txt ├── starlette-latest.txt ├── taskiq-0110.txt ├── aiogram-3140.txt ├── aiogram-3230.txt ├── aiohttp-393.txt ├── celery-540.txt ├── click-817.txt ├── fastapi-0096.txt ├── fastapi-0109.txt ├── litestar-232.txt ├── telebot-latest.txt ├── aiohttp-31215.txt ├── starlette-0270.txt ├── telebot-415.txt ├── aiogram-dialog-210.txt ├── aiogram-dialog-latest.txt ├── fastapi-latest.txt ├── asgi.txt ├── grpcio-latest.txt ├── faststream-latest.txt ├── sanic-latest.txt ├── faststream-060.txt ├── grpcio-1641.txt ├── grpcio-1680.txt ├── grpcio-1751.txt ├── sanic-23121.txt ├── sanic-2530.txt ├── test.txt ├── faststream-0529.txt └── faststream-050.txt ├── docs ├── advanced │ ├── plotter.png │ ├── testing │ │ ├── sometest.py │ │ ├── app_before.py │ │ ├── app_factory.py │ │ ├── container_before.py │ │ ├── fixtures.py │ │ ├── index.rst │ │ └── test_example.py │ ├── plotter.rst │ ├── generics_examples │ │ ├── provide.py │ │ └── decorate.py │ ├── generics.rst │ └── context.rst ├── integrations │ ├── _websockets.rst │ ├── arq.rst │ ├── aiogram_dialog.rst │ ├── click.rst │ ├── telebot.rst │ ├── celery.rst │ ├── flask.rst │ ├── sanic.rst │ ├── taskiq.rst │ ├── adding_new.rst │ └── aiogram.rst ├── quickstart_example_full.py ├── quickstart_example.py ├── provider │ ├── alias.rst │ ├── from_context.rst │ ├── provide_all.rst │ └── decorate.rst ├── conf.py ├── index.rst └── quickstart.rst ├── .typos.toml ├── requirements_dev.txt ├── CONTRIBUTING.md ├── requirements_doc.txt ├── .coveragerc ├── .readthedocs.yaml ├── .gitignore ├── .pytest.toml ├── .github ├── dependabot.yml └── workflows │ ├── new-event.yml │ ├── coverage-pr.yml │ ├── frameworks-latest.yml │ └── publish.yml ├── .ruff.toml ├── .mypy.ini └── pyproject.toml /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/dishka/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/integrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/dishka/entities/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/container/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/plotter/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/real_world/myapp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/real_world/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/dishka/integrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integrations/base/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/text_rendering/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/integrations/click_app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/integrations/grpcio/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/container/override/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/integrations/celery_app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/integrations/grpcio/pb2/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements/arq-latest.txt: -------------------------------------------------------------------------------- 1 | -r test.txt 2 | arq -------------------------------------------------------------------------------- /examples/integrations/grpcio/services/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements/arq-0250.txt: -------------------------------------------------------------------------------- 1 | -r test.txt 2 | arq==0.25.0 -------------------------------------------------------------------------------- /requirements/flask-latest.txt: -------------------------------------------------------------------------------- 1 | -r test.txt 2 | Flask -------------------------------------------------------------------------------- /requirements/taskiq-latest.txt: -------------------------------------------------------------------------------- 1 | -r test.txt 2 | taskiq -------------------------------------------------------------------------------- /requirements/aiogram-330.txt: -------------------------------------------------------------------------------- 1 | -r test.txt 2 | aiogram==3.3.0 -------------------------------------------------------------------------------- /requirements/aiogram-latest.txt: -------------------------------------------------------------------------------- 1 | -r test.txt 2 | aiogram -------------------------------------------------------------------------------- /requirements/aiohttp-latest.txt: -------------------------------------------------------------------------------- 1 | -r test.txt 2 | aiohttp -------------------------------------------------------------------------------- /requirements/celery-latest.txt: -------------------------------------------------------------------------------- 1 | -r test.txt 2 | celery 3 | -------------------------------------------------------------------------------- /requirements/click-latest.txt: -------------------------------------------------------------------------------- 1 | -r test.txt 2 | click 3 | -------------------------------------------------------------------------------- /requirements/flask-302.txt: -------------------------------------------------------------------------------- 1 | -r test.txt 2 | Flask==3.0.2 -------------------------------------------------------------------------------- /requirements/litestar-latest.txt: -------------------------------------------------------------------------------- 1 | -r asgi.txt 2 | litestar -------------------------------------------------------------------------------- /requirements/starlette-latest.txt: -------------------------------------------------------------------------------- 1 | -r asgi.txt 2 | starlette -------------------------------------------------------------------------------- /requirements/taskiq-0110.txt: -------------------------------------------------------------------------------- 1 | -r test.txt 2 | taskiq==0.11.0 -------------------------------------------------------------------------------- /requirements/aiogram-3140.txt: -------------------------------------------------------------------------------- 1 | -r test.txt 2 | aiogram==3.14.0 -------------------------------------------------------------------------------- /requirements/aiogram-3230.txt: -------------------------------------------------------------------------------- 1 | -r test.txt 2 | aiogram==3.23.0 -------------------------------------------------------------------------------- /requirements/aiohttp-393.txt: -------------------------------------------------------------------------------- 1 | -r test.txt 2 | aiohttp==3.9.3 3 | -------------------------------------------------------------------------------- /requirements/celery-540.txt: -------------------------------------------------------------------------------- 1 | -r test.txt 2 | celery==5.4.0 3 | -------------------------------------------------------------------------------- /requirements/click-817.txt: -------------------------------------------------------------------------------- 1 | -r test.txt 2 | click==8.1.7 3 | -------------------------------------------------------------------------------- /requirements/fastapi-0096.txt: -------------------------------------------------------------------------------- 1 | -r asgi.txt 2 | fastapi==0.96.0 -------------------------------------------------------------------------------- /requirements/fastapi-0109.txt: -------------------------------------------------------------------------------- 1 | -r asgi.txt 2 | fastapi==0.109.0 -------------------------------------------------------------------------------- /requirements/litestar-232.txt: -------------------------------------------------------------------------------- 1 | -r asgi.txt 2 | litestar==2.3.2 -------------------------------------------------------------------------------- /requirements/telebot-latest.txt: -------------------------------------------------------------------------------- 1 | -r test.txt 2 | pytelegrambotapi -------------------------------------------------------------------------------- /requirements/aiohttp-31215.txt: -------------------------------------------------------------------------------- 1 | -r test.txt 2 | aiohttp==3.12.15 3 | -------------------------------------------------------------------------------- /requirements/starlette-0270.txt: -------------------------------------------------------------------------------- 1 | -r asgi.txt 2 | starlette==0.27.0 -------------------------------------------------------------------------------- /requirements/telebot-415.txt: -------------------------------------------------------------------------------- 1 | -r test.txt 2 | pytelegrambotapi==4.15.4 -------------------------------------------------------------------------------- /requirements/aiogram-dialog-210.txt: -------------------------------------------------------------------------------- 1 | -r test.txt 2 | aiogram_dialog==2.1.0 -------------------------------------------------------------------------------- /requirements/aiogram-dialog-latest.txt: -------------------------------------------------------------------------------- 1 | -r test.txt 2 | aiogram_dialog 3 | -------------------------------------------------------------------------------- /requirements/fastapi-latest.txt: -------------------------------------------------------------------------------- 1 | -r asgi.txt 2 | fastapi 3 | pydantic>=2.12.0a1 -------------------------------------------------------------------------------- /src/dishka/exception_base.py: -------------------------------------------------------------------------------- 1 | class DishkaError(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /requirements/asgi.txt: -------------------------------------------------------------------------------- 1 | -r test.txt 2 | 3 | httpx==0.27.* 4 | asgi_lifespan==2.1.* -------------------------------------------------------------------------------- /tests/integrations/arq/__init__.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytest.importorskip("arq") 4 | -------------------------------------------------------------------------------- /examples/real_world/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.115.6 2 | aiogram==3.23.0 3 | uvicorn==0.34.0 -------------------------------------------------------------------------------- /requirements/grpcio-latest.txt: -------------------------------------------------------------------------------- 1 | -r test.txt 2 | grpcio 3 | grpcio-tools 4 | grpcio-testing 5 | -------------------------------------------------------------------------------- /tests/integrations/click/__init__.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytest.importorskip("click") 4 | -------------------------------------------------------------------------------- /tests/integrations/flask/__init__.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytest.importorskip("flask") 4 | -------------------------------------------------------------------------------- /tests/integrations/grpcio/__init__.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytest.importorskip("grpc") 4 | -------------------------------------------------------------------------------- /tests/integrations/sanic/__init__.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytest.importorskip("sanic") 4 | -------------------------------------------------------------------------------- /requirements/faststream-latest.txt: -------------------------------------------------------------------------------- 1 | -r test.txt 2 | 3 | faststream[nats] 4 | pydantic>=2.12.0a1 -------------------------------------------------------------------------------- /requirements/sanic-latest.txt: -------------------------------------------------------------------------------- 1 | -r asgi.txt 2 | sanic 3 | sanic-testing 4 | setuptools>=69.5.1 5 | -------------------------------------------------------------------------------- /tests/integrations/aiogram/__init__.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytest.importorskip("aiogram") 4 | -------------------------------------------------------------------------------- /tests/integrations/aiohttp/__init__.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytest.importorskip("aiohttp") 4 | -------------------------------------------------------------------------------- /tests/integrations/celery/__init__.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytest.importorskip("celery") 4 | -------------------------------------------------------------------------------- /tests/integrations/fastapi/__init__.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytest.importorskip("fastapi") 4 | -------------------------------------------------------------------------------- /tests/integrations/litestar/__init__.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytest.importorskip("litestar") 4 | -------------------------------------------------------------------------------- /tests/integrations/taskiq/__init__.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytest.importorskip("taskiq") 4 | -------------------------------------------------------------------------------- /tests/integrations/telebot/__init__.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytest.importorskip("telebot") 4 | -------------------------------------------------------------------------------- /docs/advanced/plotter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reagento/dishka/HEAD/docs/advanced/plotter.png -------------------------------------------------------------------------------- /src/dishka/text_rendering/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["get_name"] 2 | 3 | from .name import get_name 4 | -------------------------------------------------------------------------------- /tests/integrations/faststream/__init__.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytest.importorskip("faststream") 4 | -------------------------------------------------------------------------------- /tests/integrations/starlette/__init__.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytest.importorskip("starlette") 4 | -------------------------------------------------------------------------------- /requirements/faststream-060.txt: -------------------------------------------------------------------------------- 1 | -r test.txt 2 | 3 | faststream[nats]==0.6.0rc0 4 | fast-depends==3.0.0a12 -------------------------------------------------------------------------------- /requirements/grpcio-1641.txt: -------------------------------------------------------------------------------- 1 | -r test.txt 2 | grpcio==1.64.1 3 | grpcio-tools==1.64.1 4 | grpcio-testing==1.64.1 -------------------------------------------------------------------------------- /requirements/grpcio-1680.txt: -------------------------------------------------------------------------------- 1 | -r test.txt 2 | grpcio==1.68.0 3 | grpcio-tools==1.68.0 4 | grpcio-testing==1.68.0 -------------------------------------------------------------------------------- /requirements/grpcio-1751.txt: -------------------------------------------------------------------------------- 1 | -r test.txt 2 | grpcio==1.75.1 3 | grpcio-tools==1.75.1 4 | grpcio-testing==1.75.1 -------------------------------------------------------------------------------- /requirements/sanic-23121.txt: -------------------------------------------------------------------------------- 1 | -r asgi.txt 2 | sanic==23.12.1 3 | sanic-testing==23.12.0 4 | setuptools==69.5.1 5 | -------------------------------------------------------------------------------- /requirements/sanic-2530.txt: -------------------------------------------------------------------------------- 1 | -r asgi.txt 2 | sanic==25.3.0 3 | sanic-testing==24.6.0 4 | setuptools==70.1.0 5 | -------------------------------------------------------------------------------- /requirements/test.txt: -------------------------------------------------------------------------------- 1 | pytest~=9.0.1 2 | pytest-asyncio==1.3.* 3 | pytest-repeat==0.9.* 4 | pytest-cov==7.* 5 | -------------------------------------------------------------------------------- /tests/integrations/aiogram_dialog/__init__.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytest.importorskip("aiogram_dialog") 4 | -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | [default.extend-words] 2 | asend = "asend" 3 | # Remove after dishka 2.0!!! 4 | settigs = "settigs" 5 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | uv==0.9.13 2 | ruff==0.14.7 3 | mypy==1.19.0 4 | nox==2025.11.12 5 | zizmor==1.18.0 6 | typos==1.40.0 -------------------------------------------------------------------------------- /requirements/faststream-0529.txt: -------------------------------------------------------------------------------- 1 | -r test.txt 2 | 3 | faststream[nats]==0.5.29 4 | typing-extensions==4.12.2 5 | anyio==4.6.0 -------------------------------------------------------------------------------- /requirements/faststream-050.txt: -------------------------------------------------------------------------------- 1 | -r test.txt 2 | 3 | faststream[nats]==0.5.0rc2 4 | typing-extensions==4.12.0 5 | anyio==4.6.1 6 | -------------------------------------------------------------------------------- /src/dishka/entities/component.py: -------------------------------------------------------------------------------- 1 | from typing import TypeAlias 2 | 3 | Component: TypeAlias = str 4 | 5 | DEFAULT_COMPONENT = "" 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Refer to [Contributing](https://dishka.readthedocs.io/en/latest/contributing.html) guidelines on the documentation website. -------------------------------------------------------------------------------- /requirements_doc.txt: -------------------------------------------------------------------------------- 1 | sphinx==8.1.* 2 | sphinx_copybutton==0.5.* 3 | sphinx-autodocgen==1.3 4 | furo==2024.8.* 5 | sphinx-design==0.6.1 6 | -------------------------------------------------------------------------------- /src/dishka/plotter/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "render_d2", 3 | "render_mermaid", 4 | ] 5 | 6 | from .wrappers import render_d2, render_mermaid 7 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | exclude_lines = 3 | pragma: not covered 4 | @overload 5 | [run] 6 | relative_files = true 7 | omit = 8 | src/dishka/_adaptix/** 9 | src/dishka/_version.py -------------------------------------------------------------------------------- /examples/real_world/requirements_test.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | 3 | pytest==7.* 4 | pytest-asyncio==0.23.* 5 | pytest-repeat==0.9.* 6 | pytest-cov==4.1.0 7 | 8 | httpx==0.26.* 9 | asgi_lifespan==2.1.* 10 | -------------------------------------------------------------------------------- /docs/advanced/testing/sometest.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock 2 | 3 | 4 | async def test_controller(client: TestClient, connection: Mock): 5 | response = client.get("/") 6 | assert response.status_code == 200 7 | connection.execute.assertCalled() 8 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | build: 3 | os: ubuntu-24.04 4 | tools: 5 | python: "3.12" 6 | python: 7 | install: 8 | - method: pip 9 | path: . 10 | - requirements: requirements_doc.txt 11 | sphinx: 12 | configuration: docs/conf.py 13 | -------------------------------------------------------------------------------- /tests/unit/test_composite.py: -------------------------------------------------------------------------------- 1 | from dishka.dependency_source.composite import ensure_composite 2 | 3 | 4 | def test_composite(): 5 | class A: 6 | @ensure_composite 7 | def foo(self, a, b): 8 | return a + b 9 | 10 | a = A() 11 | assert a.foo(1, 2) == 3 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # project generated files 2 | __pycache__/ 3 | /docs-build/ 4 | /dist/ 5 | /build/ 6 | /.ruff_cache/ 7 | /.tox/ 8 | .coverage 9 | *.egg-info 10 | src/dishka/_version.py 11 | 12 | # common utilities 13 | /venv 14 | /.venv 15 | .mypy_cache/ 16 | .idea/ 17 | .vscode/ 18 | .venv/ 19 | .DS_Store -------------------------------------------------------------------------------- /tests/unit/test_quickstart_example.py: -------------------------------------------------------------------------------- 1 | import runpy 2 | from pathlib import Path 3 | 4 | ROOT = Path(__file__).parent.parent.parent.resolve() 5 | QUICKSTART_EXAMPLE_PATH = ROOT / "docs/quickstart_example.py" 6 | 7 | 8 | def test_readme_example(): 9 | runpy.run_path(QUICKSTART_EXAMPLE_PATH) 10 | -------------------------------------------------------------------------------- /examples/integrations/grpcio/services/uuid_service.py: -------------------------------------------------------------------------------- 1 | from typing import Protocol 2 | from uuid import uuid4 3 | 4 | 5 | class UUIDService(Protocol): 6 | def generate_uuid(self) -> str: ... 7 | 8 | 9 | class UUIDServiceImpl: 10 | def generate_uuid(self) -> str: 11 | return str(uuid4()) 12 | -------------------------------------------------------------------------------- /examples/integrations/arq/app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from arq import create_pool 4 | from arq.connections import RedisSettings 5 | 6 | 7 | async def main(): 8 | pool = await create_pool(RedisSettings()) 9 | await pool.enqueue_job("get_content") 10 | 11 | 12 | if __name__ == "__main__": 13 | asyncio.run(main()) 14 | -------------------------------------------------------------------------------- /tests/unit/container/pep695_new_syntax.py: -------------------------------------------------------------------------------- 1 | from typing import Protocol 2 | 3 | 4 | class Base[T](Protocol): 5 | value: T 6 | 7 | 8 | class ImplBase[T](Base[T]): 9 | def __init__(self, value: T) -> None: 10 | self._value = value 11 | 12 | @property 13 | def value(self) -> T: 14 | return self._value 15 | -------------------------------------------------------------------------------- /.pytest.toml: -------------------------------------------------------------------------------- 1 | [pytest] 2 | minversion = "9.0" # config in toml only since v9.0.0 3 | addopts = [ 4 | "--cov=dishka", 5 | "--cov-append", 6 | "--cov-report=term-missing", 7 | "--verbose", 8 | ] 9 | # https://pytest-asyncio.readthedocs.io/en/latest/reference/configuration.html#asyncio-default-fixture-loop-scope 10 | asyncio_default_fixture_loop_scope = "function" 11 | -------------------------------------------------------------------------------- /examples/integrations/grpcio/di.py: -------------------------------------------------------------------------------- 1 | from dishka import Provider, Scope 2 | from grpcio.services.uuid_service import UUIDService, UUIDServiceImpl 3 | 4 | 5 | def service_provider() -> Provider: 6 | provider = Provider() 7 | 8 | provider.provide( 9 | UUIDServiceImpl, 10 | scope=Scope.REQUEST, 11 | provides=UUIDService, 12 | ) 13 | 14 | return provider 15 | -------------------------------------------------------------------------------- /docs/integrations/_websockets.rst: -------------------------------------------------------------------------------- 1 | .. include:: 2 | 3 | For most cases we operate single events like HTTP-requests. In this case we operate only 2 scopes: ``APP`` and ``REQUEST``. 4 | Websockets are different: for one application you have multiple connections (one per client) and each connection delivers multiple messages. 5 | To support this we use additional scope: ``SESSION``: 6 | 7 | ``APP`` |rarr| ``SESSION`` |rarr| ``REQUEST`` -------------------------------------------------------------------------------- /src/dishka/integrations/exceptions.py: -------------------------------------------------------------------------------- 1 | from dishka.exception_base import DishkaError 2 | 3 | 4 | class InvalidInjectedFuncTypeError(DishkaError): 5 | def __str__(self) -> str: 6 | return "An async container cannot be used in a synchronous context." 7 | 8 | 9 | class ImproperProvideContextUsageError(DishkaError): 10 | def __str__(self) -> str: 11 | return "provide_context can only be used with manage_scope=True." 12 | -------------------------------------------------------------------------------- /docs/advanced/testing/app_before.py: -------------------------------------------------------------------------------- 1 | from sqlite3 import Connection 2 | 3 | from fastapi import FastAPI, APIRouter 4 | 5 | from dishka.integrations.fastapi import FromDishka, inject 6 | 7 | router = APIRouter() 8 | 9 | 10 | @router.get("/") 11 | @inject 12 | async def index(connection: FromDishka[Connection]) -> str: 13 | connection.execute("select 1") 14 | return "Ok" 15 | 16 | 17 | app = FastAPI() 18 | app.include_router(router) 19 | -------------------------------------------------------------------------------- /examples/real_world/myapp/api_client.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from .use_cases import WarehouseClient 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | class FakeWarehouseClient(WarehouseClient): 9 | def __init__(self): 10 | logger.info("init FakeWarehouseClient as %s", self) 11 | self.products = 0 12 | 13 | def next_product(self) -> str: 14 | self.products += 1 15 | return f"Product {self.products}" 16 | -------------------------------------------------------------------------------- /examples/real_world/myapp/presentation_web.py: -------------------------------------------------------------------------------- 1 | from dishka.integrations.fastapi import ( 2 | FromDishka, 3 | inject, 4 | ) 5 | from fastapi import APIRouter 6 | 7 | from myapp.use_cases import AddProductsInteractor 8 | 9 | router = APIRouter() 10 | 11 | 12 | @router.get("/") 13 | @inject 14 | async def add_product( 15 | *, 16 | interactor: FromDishka[AddProductsInteractor], 17 | ) -> str: 18 | interactor(user_id=1) 19 | return "Ok" 20 | -------------------------------------------------------------------------------- /docs/advanced/testing/app_factory.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | from dishka import make_async_container 4 | from dishka.integrations.fastapi import setup_dishka 5 | 6 | 7 | def create_app() -> FastAPI: 8 | app = FastAPI() 9 | app.include_router(router) 10 | return app 11 | 12 | 13 | def create_production_app(): 14 | app = create_app() 15 | container = make_async_container(ConnectionProvider("sqlite:///")) 16 | setup_dishka(container, app) 17 | return app 18 | -------------------------------------------------------------------------------- /examples/real_world/myapp/presentation_bot.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router 2 | from aiogram.types import Message 3 | from dishka.integrations.aiogram import FromDishka, inject 4 | 5 | from .use_cases import AddProductsInteractor 6 | 7 | router = Router() 8 | 9 | 10 | @router.message() 11 | @inject 12 | async def start( 13 | message: Message, 14 | interactor: FromDishka[AddProductsInteractor], 15 | ): 16 | interactor(user_id=1) 17 | await message.answer("Products added!") 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" # zizmor: ignore[dependabot-cooldown] 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | target-branch: develop 8 | 9 | - package-ecosystem: "pip" # zizmor: ignore[dependabot-cooldown] 10 | directory: "/" 11 | file: "requirements_dev.txt" 12 | schedule: 13 | interval: "weekly" 14 | target-branch: develop 15 | groups: 16 | python-packages: 17 | patterns: 18 | - "*" -------------------------------------------------------------------------------- /src/dishka/plotter/wrappers.py: -------------------------------------------------------------------------------- 1 | from dishka import AsyncContainer, Container 2 | from .d2 import D2Renderer 3 | from .mermaid import MermaidRenderer 4 | from .transform import Transformer 5 | 6 | 7 | def render_mermaid(container: AsyncContainer | Container) -> str: 8 | return MermaidRenderer().render( 9 | Transformer().transform(container), 10 | ) 11 | 12 | 13 | def render_d2(container: AsyncContainer | Container) -> str: 14 | return D2Renderer().render( 15 | Transformer().transform(container), 16 | ) 17 | -------------------------------------------------------------------------------- /src/dishka/dependency_source/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "Alias", 3 | "CompositeDependencySource", 4 | "ContextVariable", 5 | "Decorator", 6 | "DependencySource", 7 | "Factory", 8 | "context_stub", 9 | "ensure_composite", 10 | ] 11 | 12 | from .alias import Alias 13 | from .composite import ( 14 | CompositeDependencySource, 15 | DependencySource, 16 | ensure_composite, 17 | ) 18 | from .context_var import ContextVariable, context_stub 19 | from .decorator import Decorator 20 | from .factory import Factory 21 | -------------------------------------------------------------------------------- /src/dishka/entities/validation_settigs.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from dishka.entities.validation_settings import ( 4 | DEFAULT_VALIDATION, 5 | STRICT_VALIDATION, 6 | ValidationSettings, 7 | ) 8 | 9 | warnings.warn( 10 | "`dishka.entities.validation_settigs`" 11 | " is deprecated and will be removed in 2.0" 12 | ", use `from dishka import ...` instead.", 13 | DeprecationWarning, 14 | stacklevel=2, 15 | ) 16 | 17 | __all__ = [ 18 | "DEFAULT_VALIDATION", 19 | "STRICT_VALIDATION", 20 | "ValidationSettings", 21 | ] 22 | -------------------------------------------------------------------------------- /tests/unit/container/type_alias_type_provider.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterable 2 | from typing import Annotated 3 | 4 | from dishka import FromComponent 5 | 6 | type Integer = int 7 | type Integer2 = int 8 | type String = str 9 | type ListFloat = list[float] 10 | type WrappedInteger = Integer 11 | type WrappedIntegerDep = Integer 12 | type IntegerWithComponent = Annotated[int, FromComponent("X")] 13 | type IterableInt = Iterable[Integer] 14 | type IntStr = Integer | String 15 | type BytesMemoryView = bytes | memoryview 16 | type StrNone = String | None 17 | -------------------------------------------------------------------------------- /src/dishka/entities/type_alias_type.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from typing import Any 3 | 4 | from dishka._adaptix.common import TypeHint 5 | 6 | if sys.version_info >= (3, 12): 7 | from typing import TypeAliasType 8 | 9 | def is_type_alias_type(tp: TypeHint) -> bool: 10 | return isinstance(tp, TypeAliasType) 11 | 12 | else: 13 | def is_type_alias_type(tp: TypeHint) -> bool: 14 | return False 15 | 16 | 17 | def unwrap_type_alias(hint: Any) -> Any: 18 | while is_type_alias_type(hint): 19 | hint = hint.__value__ 20 | return hint 21 | -------------------------------------------------------------------------------- /examples/integrations/grpcio/proto/service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package example; 4 | 5 | service ExampleService { 6 | rpc UnaryUnary(RequestMessage) returns (ResponseMessage); 7 | rpc UnaryStream(RequestMessage) returns (stream ResponseMessage); 8 | rpc StreamUnary(stream RequestMessage) returns (ResponseMessage); 9 | rpc StreamStream(stream RequestMessage) returns (stream ResponseMessage); 10 | } 11 | 12 | message RequestMessage { 13 | string message = 1; 14 | } 15 | 16 | message ResponseMessage { 17 | string message = 1; 18 | } 19 | -------------------------------------------------------------------------------- /src/dishka/provider/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "BaseProvider", 3 | "Provider", 4 | "ProviderWrapper", 5 | "alias", 6 | "decorate", 7 | "from_context", 8 | "make_root_context_provider", 9 | "provide", 10 | "provide_all", 11 | ] 12 | 13 | from .base_provider import BaseProvider, ProviderWrapper 14 | from .make_alias import alias 15 | from .make_context_var import from_context 16 | from .make_decorator import decorate 17 | from .make_factory import provide, provide_all 18 | from .provider import Provider 19 | from .root_context import make_root_context_provider 20 | -------------------------------------------------------------------------------- /docs/advanced/plotter.rst: -------------------------------------------------------------------------------- 1 | Dependency graph plotter 2 | ============================== 3 | 4 | You can visualise your dependency graph by calling one of these functions 5 | 6 | * ``dishka.plotter.render_d2(container)`` will produce a string in d2 lang format. Follow ``_ for more details how to show it. 7 | * ``dishka.plotter.render_mermaid(container)`` will produce a string containing ready to show HTML text containing graph in mermaid js format. Follow ``_ for more details on its customization. 8 | 9 | 10 | The example rendered with mermaid: 11 | 12 | .. image:: ./plotter.png -------------------------------------------------------------------------------- /src/dishka/container_objects.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from collections.abc import Callable 3 | from dataclasses import dataclass 4 | from typing import Any, Protocol 5 | 6 | from dishka.entities.factory_type import FactoryType 7 | 8 | 9 | @dataclass(slots=True) 10 | class Exit: 11 | type: FactoryType 12 | callable: Callable[..., Any] 13 | 14 | 15 | class CompiledFactory(Protocol): 16 | @abstractmethod 17 | def __call__( 18 | self, 19 | getter: Callable[..., Any], 20 | exits: list[Exit], 21 | context: Any, 22 | ) -> Any: 23 | raise NotImplementedError 24 | -------------------------------------------------------------------------------- /tests/integrations/aiogram/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from aiogram import Bot 3 | 4 | 5 | class FakeBot(Bot): 6 | def __init__(self): 7 | pass # do not call super, so it is invalid bot, used only as a stub 8 | 9 | @property 10 | def id(self): 11 | return 1 12 | 13 | def __call__(self, *args, **kwargs) -> None: 14 | raise RuntimeError("Fake bot should not be used to call telegram") 15 | 16 | def __hash__(self) -> int: 17 | return 1 18 | 19 | def __eq__(self, other) -> bool: 20 | return self is other 21 | 22 | 23 | @pytest.fixture 24 | def bot(): 25 | return FakeBot() 26 | -------------------------------------------------------------------------------- /docs/advanced/generics_examples/provide.py: -------------------------------------------------------------------------------- 1 | from typing import Generic, TypeVar 2 | 3 | from dishka import make_container, Provider, provide, Scope 4 | 5 | T = TypeVar("T", bound=int) 6 | 7 | 8 | class A(Generic[T]): 9 | pass 10 | 11 | 12 | class MyProvider(Provider): 13 | @provide(scope=Scope.APP) 14 | def make_a(self, t: type[T]) -> A[T]: 15 | print("Requested type", t) 16 | return A() 17 | 18 | 19 | container = make_container(MyProvider()) 20 | container.get(A[int]) # printed: Requested type 21 | container.get(A[bool]) # printed: Requested type 22 | container.get(A[str]) # NoFactoryError 23 | -------------------------------------------------------------------------------- /src/dishka/entities/validation_settings.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass 5 | class ValidationSettings: 6 | # check if no factory found to override when set override=True 7 | nothing_overridden: bool = False 8 | # check if factory is overridden when set override=False 9 | implicit_override: bool = False 10 | # check if decorator was not applied to any factory 11 | nothing_decorated: bool = True 12 | 13 | 14 | DEFAULT_VALIDATION = ValidationSettings() 15 | STRICT_VALIDATION = ValidationSettings( 16 | nothing_overridden=True, 17 | implicit_override=True, 18 | nothing_decorated=True, 19 | ) 20 | -------------------------------------------------------------------------------- /tests/integrations/grpcio/my_grpc_service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package my_grpc_service; 4 | 5 | message MyRequest { string name = 1; } 6 | 7 | message MyResponse { string message = 1; } 8 | 9 | service MyService { 10 | rpc MyMethod(MyRequest) returns (MyResponse); 11 | rpc MyUnaryStreamMethod(MyRequest) returns (stream MyResponse); 12 | rpc MyUnaryStreamMethodGen(MyRequest) returns (stream MyResponse); 13 | rpc MyStreamUnaryMethod(stream MyRequest) returns (MyResponse); 14 | rpc MyStreamStreamMethod(stream MyRequest) returns (stream MyResponse); 15 | rpc MyStreamStreamMethodGen(stream MyRequest) returns (stream MyResponse); 16 | } 17 | -------------------------------------------------------------------------------- /examples/ruff.toml: -------------------------------------------------------------------------------- 1 | extend = "../.ruff.toml" 2 | 3 | [lint] 4 | ignore = [ 5 | "T201", # many `print` in examples/ 6 | "S104", # in case of examples, this is not so critical 7 | "S311", # `secrets` x1.75 slower that `random` 8 | "INP001", 9 | "SIM105", # `contextlib.suppress` x2 slower that `try-except-pass` 10 | ] 11 | 12 | [lint.isort] 13 | force-wrap-aliases = true 14 | combine-as-imports = true 15 | no-lines-before = ["local-folder"] 16 | 17 | [lint.per-file-ignores] 18 | "integrations/grpcio/pb2/**.py" = ["ALL"] 19 | "integrations/grpcio/grpc_server.py" = ["N802"] 20 | "real_world/tests/test_web.py" = [ 21 | "S101", 22 | "PLR2004", 23 | ] -------------------------------------------------------------------------------- /examples/integrations/grpcio/pb2/service_pb2.pyi: -------------------------------------------------------------------------------- 1 | from typing import ClassVar as _ClassVar, Optional as _Optional 2 | 3 | from google.protobuf import descriptor as _descriptor, message as _message 4 | 5 | DESCRIPTOR: _descriptor.FileDescriptor 6 | 7 | class RequestMessage(_message.Message): 8 | __slots__ = ("message",) 9 | MESSAGE_FIELD_NUMBER: _ClassVar[int] 10 | message: str 11 | def __init__(self, message: _Optional[str] = ...) -> None: ... 12 | 13 | class ResponseMessage(_message.Message): 14 | __slots__ = ("message",) 15 | MESSAGE_FIELD_NUMBER: _ClassVar[int] 16 | message: str 17 | def __init__(self, message: _Optional[str] = ...) -> None: ... 18 | -------------------------------------------------------------------------------- /docs/advanced/testing/container_before.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterable 2 | from sqlite3 import connect, Connection 3 | 4 | from dishka import Provider, Scope, provide, make_async_container 5 | from dishka.integrations.fastapi import setup_dishka 6 | 7 | 8 | class ConnectionProvider(Provider): 9 | def __init__(self, uri): 10 | super().__init__() 11 | self.uri = uri 12 | 13 | @provide(scope=Scope.REQUEST) 14 | def get_connection(self) -> Iterable[Connection]: 15 | conn = connect(self.uri) 16 | yield conn 17 | conn.close() 18 | 19 | 20 | container = make_async_container(ConnectionProvider("sqlite:///")) 21 | setup_dishka(container, app) 22 | -------------------------------------------------------------------------------- /tests/integrations/taskiq/utils.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | 3 | from taskiq import AsyncResultBackend, TaskiqResult 4 | 5 | 6 | class PickleResultBackend(AsyncResultBackend): 7 | def __init__(self) -> None: 8 | self.results = {} 9 | 10 | async def set_result(self, task_id, result) -> None: 11 | self.results[task_id] = pickle.dumps(result) 12 | 13 | async def is_result_ready(self, task_id) -> bool: 14 | return task_id in self.results 15 | 16 | async def get_result( 17 | self, 18 | task_id: str, 19 | with_logs: bool = False, # noqa: FBT001, FBT002 20 | ) -> TaskiqResult: 21 | return pickle.loads(self.results[task_id]) # noqa: S301 22 | -------------------------------------------------------------------------------- /src/dishka/_adaptix/common.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Any, Callable, TypeVar, Union 2 | 3 | K_contra = TypeVar("K_contra", contravariant=True) 4 | V_co = TypeVar("V_co", covariant=True) 5 | T = TypeVar("T") 6 | 7 | Loader = Callable[[Any], V_co] 8 | Dumper = Callable[[K_contra], Any] 9 | Converter = Callable[..., Any] 10 | Coercer = Callable[[Any, Any], Any] 11 | OneArgCoercer = Callable[[Any], Any] 12 | 13 | TypeHint = Any 14 | 15 | VarTuple = tuple[T, ...] 16 | 17 | Catchable = Union[type[BaseException], VarTuple[type[BaseException]]] 18 | 19 | # https://github.com/python/typing/issues/684#issuecomment-548203158 20 | if TYPE_CHECKING: 21 | EllipsisType = ellipsis # noqa: F821 22 | else: 23 | EllipsisType = type(Ellipsis) 24 | -------------------------------------------------------------------------------- /tests/integrations/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from dishka import ( 4 | AsyncContainer, 5 | Container, 6 | make_async_container, 7 | make_container, 8 | ) 9 | from .common import AppProvider, WebSocketAppProvider 10 | 11 | 12 | @pytest.fixture 13 | def app_provider() -> AppProvider: 14 | return AppProvider() 15 | 16 | 17 | @pytest.fixture 18 | def ws_app_provider() -> WebSocketAppProvider: 19 | return WebSocketAppProvider() 20 | 21 | 22 | @pytest.fixture 23 | def async_container(app_provider: AppProvider) -> AsyncContainer: 24 | return make_async_container(app_provider) 25 | 26 | 27 | @pytest.fixture 28 | def container(app_provider: AppProvider) -> Container: 29 | return make_container(app_provider) 30 | -------------------------------------------------------------------------------- /examples/integrations/celery_app/with_inject.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from celery import Celery 4 | from dishka import Provider, Scope, make_container 5 | from dishka.integrations.celery import FromDishka, inject, setup_dishka 6 | 7 | provider = Provider(scope=Scope.REQUEST) 8 | provider.provide(lambda: random.random(), provides=float) 9 | 10 | 11 | app = Celery() 12 | 13 | 14 | @app.task 15 | @inject 16 | def random_task(num: FromDishka[float]) -> float: 17 | return num 18 | 19 | 20 | def main() -> None: 21 | container = make_container(provider) 22 | setup_dishka(container, app) 23 | 24 | result = random_task.apply() 25 | 26 | print(result.get()) 27 | 28 | container.close() 29 | 30 | 31 | if __name__ == "__main__": 32 | main() 33 | -------------------------------------------------------------------------------- /tests/unit/test_context_proxy.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from dishka import DEFAULT_COMPONENT, DependencyKey 4 | from dishka.context_proxy import ContextProxy 5 | 6 | 7 | def test_simple(): 8 | int_key = DependencyKey(int, DEFAULT_COMPONENT) 9 | context = {int_key: 1} 10 | cache = {**context, DependencyKey(float, DEFAULT_COMPONENT): 2} 11 | proxy = ContextProxy(context=context, cache=cache) 12 | assert len(proxy) == 2 13 | assert proxy[int_key] == proxy.get(int_key) == 1 14 | assert list(proxy) == list(cache) 15 | 16 | complex_key = DependencyKey(complex, DEFAULT_COMPONENT) 17 | proxy[complex_key] = 3 18 | assert context[complex_key] == cache[complex_key] == 3 19 | 20 | with pytest.raises(RuntimeError): 21 | del proxy[complex_key] 22 | -------------------------------------------------------------------------------- /examples/integrations/celery_app/with_task_cls.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from celery import Celery 4 | from dishka import Provider, Scope, make_container 5 | from dishka.integrations.celery import ( 6 | DishkaTask, 7 | FromDishka, 8 | setup_dishka, 9 | ) 10 | 11 | provider = Provider(scope=Scope.REQUEST) 12 | provider.provide(lambda: random.random(), provides=float) 13 | 14 | 15 | app = Celery(task_cls=DishkaTask) 16 | 17 | 18 | @app.task 19 | def random_task(num: FromDishka[float]) -> float: 20 | return num 21 | 22 | 23 | def main() -> None: 24 | container = make_container(provider) 25 | setup_dishka(container, app) 26 | 27 | result = random_task.apply() 28 | 29 | print(result.get()) 30 | 31 | container.close() 32 | 33 | 34 | if __name__ == "__main__": 35 | main() 36 | -------------------------------------------------------------------------------- /src/dishka/_adaptix/type_tools/__init__.py: -------------------------------------------------------------------------------- 1 | from .basic_utils import ( 2 | create_union, 3 | is_bare_generic, 4 | is_generic, 5 | is_generic_class, 6 | is_named_tuple_class, 7 | is_new_type, 8 | is_parametrized, 9 | is_protocol, 10 | is_subclass_soft, 11 | is_typed_dict_class, 12 | is_user_defined_generic, 13 | ) 14 | from .fundamentals import get_all_type_hints, get_generic_args, get_type_vars, is_pydantic_class, strip_alias 15 | from .norm_utils import is_class_var, strip_tags 16 | from .normalize_type import ( 17 | AnyNormTypeVarLike, 18 | BaseNormType, 19 | NormParamSpecMarker, 20 | NormTV, 21 | NormTVTuple, 22 | NormTypeAlias, 23 | make_norm_type, 24 | normalize_type, 25 | ) 26 | from .type_evaler import exec_type_checking, make_fragments_collector 27 | -------------------------------------------------------------------------------- /tests/unit/test_entities.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated, Any 2 | 3 | import pytest 4 | 5 | from dishka.entities.key import FromComponent, hint_to_dependency_key 6 | 7 | 8 | class TestDependencyKey: 9 | @pytest.mark.parametrize( 10 | ("hint", "resolved_type", "component"), 11 | [ 12 | (Any, Any, None), 13 | (str, str, None), 14 | (Annotated[str, {"foo": "bar"}], str, None), 15 | (Annotated[str, FromComponent("baz")], str, "baz"), 16 | ], 17 | ) 18 | def test_hint_to_dependency_key( 19 | self, 20 | hint: Any, 21 | resolved_type: Any, 22 | component: str | None, 23 | ): 24 | key = hint_to_dependency_key(hint) 25 | assert key.type_hint == resolved_type 26 | assert key.component == component 27 | -------------------------------------------------------------------------------- /src/dishka/integrations/faststream/__init__.py: -------------------------------------------------------------------------------- 1 | from faststream.__about__ import ( 2 | __version__ as FASTSTREAM_VERSION, # noqa: N812 3 | ) 4 | 5 | from dishka import FromDishka 6 | 7 | FASTSTREAM_05 = FASTSTREAM_VERSION.startswith("0.5") 8 | FASTSTREAM_06 = FASTSTREAM_VERSION.startswith("0.6") 9 | 10 | if FASTSTREAM_05: 11 | from .faststream_05 import FastStreamProvider, inject, setup_dishka 12 | elif FASTSTREAM_06: 13 | from .faststream_06 import ( # type: ignore[assignment] 14 | FastStreamProvider, 15 | inject, 16 | setup_dishka, 17 | ) 18 | else: 19 | raise RuntimeError( # noqa: TRY003 20 | f"FastStream {FASTSTREAM_VERSION} version not supported", 21 | ) 22 | 23 | __all__ = ( 24 | "FastStreamProvider", 25 | "FromDishka", 26 | "inject", 27 | "setup_dishka", 28 | ) 29 | -------------------------------------------------------------------------------- /src/dishka/entities/factory_type.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any 3 | 4 | from .key import DependencyKey 5 | from .scope import BaseScope 6 | 7 | 8 | class FactoryType(Enum): 9 | GENERATOR = "generator" 10 | ASYNC_GENERATOR = "async_generator" 11 | FACTORY = "factory" 12 | ASYNC_FACTORY = "async_factory" 13 | VALUE = "value" 14 | ALIAS = "alias" 15 | CONTEXT = "context" 16 | 17 | 18 | class FactoryData: 19 | __slots__ = ("provides", "scope", "source", "type") 20 | 21 | def __init__( 22 | self, 23 | *, 24 | source: Any, 25 | provides: DependencyKey, 26 | scope: BaseScope | None, 27 | type_: FactoryType, 28 | ) -> None: 29 | self.source = source 30 | self.provides = provides 31 | self.scope = scope 32 | self.type = type_ 33 | -------------------------------------------------------------------------------- /examples/real_world/main_bot.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import os 4 | 5 | from aiogram import Bot, Dispatcher 6 | from dishka import make_async_container 7 | from dishka.integrations.aiogram import setup_dishka 8 | from myapp.ioc import AdaptersProvider, InteractorProvider 9 | from myapp.presentation_bot import router 10 | 11 | 12 | async def main(): 13 | # real main 14 | logging.basicConfig(level=logging.INFO) 15 | bot = Bot(token=os.getenv("BOT_TOKEN")) 16 | dp = Dispatcher() 17 | dp.include_router(router) 18 | container = make_async_container(AdaptersProvider(), InteractorProvider()) 19 | setup_dishka(container=container, router=dp) 20 | try: 21 | await dp.start_polling(bot) 22 | finally: 23 | await container.close() 24 | await bot.session.close() 25 | 26 | 27 | if __name__ == "__main__": 28 | asyncio.run(main()) 29 | -------------------------------------------------------------------------------- /.github/workflows/new-event.yml: -------------------------------------------------------------------------------- 1 | name: Event Notifier 2 | 3 | on: 4 | issues: 5 | types: [opened, reopened] 6 | pull_request_target: 7 | types: [opened, reopened] # zizmor: ignore[dangerous-triggers] 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number }} 11 | cancel-in-progress: true 12 | 13 | permissions: 14 | issues: read 15 | pull-requests: read 16 | 17 | jobs: 18 | notify: 19 | name: "Telegram notification" 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Send Telegram notification for new issue or pull request 23 | uses: reagento/relator@919d3a1593a3ed3e8b8f2f39013cc6f5498241da # v1.6.0 24 | with: 25 | tg-bot-token: ${{ secrets.TELEGRAM_BOT_TOKEN }} 26 | tg-chat-id: ${{ vars.TELEGRAM_CHAT_ID }} 27 | github-token: ${{ secrets.GITHUB_TOKEN }} 28 | -------------------------------------------------------------------------------- /src/dishka/plotter/model.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from dataclasses import dataclass 3 | from enum import Enum 4 | from typing import Protocol 5 | 6 | 7 | class GroupType(Enum): 8 | SCOPE = "SCOPE" 9 | COMPONENT = "COMPONENT" 10 | 11 | 12 | class NodeType(Enum): 13 | CONTEXT = "Context" 14 | FACTORY = "Factory" 15 | DECORATOR = "Decorator" 16 | ALIAS = "Alias" 17 | 18 | 19 | @dataclass 20 | class Node: 21 | id: str 22 | name: str 23 | dependencies: list[str] 24 | type: NodeType 25 | is_protocol: bool 26 | source_name: str 27 | 28 | 29 | @dataclass 30 | class Group: 31 | id: str 32 | name: str 33 | children: list["Group"] 34 | nodes: list[Node] 35 | type: GroupType 36 | 37 | 38 | class Renderer(Protocol): 39 | @abstractmethod 40 | def render(self, groups: list[Group]) -> str: 41 | raise NotImplementedError 42 | -------------------------------------------------------------------------------- /src/dishka/provider/base_provider.py: -------------------------------------------------------------------------------- 1 | from dishka.dependency_source import ( 2 | Alias, 3 | ContextVariable, 4 | Decorator, 5 | Factory, 6 | ) 7 | from dishka.entities.component import Component 8 | 9 | 10 | class BaseProvider: 11 | def __init__(self, component: Component | None) -> None: 12 | if component is not None: 13 | self.component = component 14 | self.factories: list[Factory] = [] 15 | self.aliases: list[Alias] = [] 16 | self.decorators: list[Decorator] = [] 17 | self.context_vars: list[ContextVariable] = [] 18 | 19 | 20 | class ProviderWrapper(BaseProvider): 21 | def __init__(self, component: Component, provider: BaseProvider) -> None: 22 | super().__init__(component) 23 | self.factories.extend(provider.factories) 24 | self.aliases.extend(provider.aliases) 25 | self.decorators.extend(provider.decorators) 26 | -------------------------------------------------------------------------------- /docs/advanced/generics_examples/decorate.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterator 2 | from typing import TypeVar 3 | 4 | from dishka import make_container, Provider, provide, Scope, decorate 5 | 6 | T = TypeVar("T") 7 | 8 | 9 | class MyProvider(Provider): 10 | scope = Scope.APP 11 | 12 | @provide 13 | def make_int(self) -> int: 14 | return 1 15 | 16 | @provide 17 | def make_str(self) -> str: 18 | return "hello" 19 | 20 | @decorate 21 | def log(self, a: T, t: type[T]) -> Iterator[T]: 22 | print("Requested", t, "with value", a) 23 | yield a 24 | print("Requested release", a) 25 | 26 | 27 | container = make_container(MyProvider()) 28 | container.get(int) # Requested with value 1 29 | container.get(str) # Requested with value hello 30 | container.close() 31 | # Requested release object hello 32 | # Requested release object 1 33 | 34 | -------------------------------------------------------------------------------- /.github/workflows/coverage-pr.yml: -------------------------------------------------------------------------------- 1 | name: Post coverage comment 2 | 3 | on: 4 | workflow_run: # zizmor: ignore[dangerous-triggers] 5 | workflows: ["CI"] 6 | types: 7 | - completed 8 | 9 | jobs: 10 | test: 11 | name: Run tests & display coverage 12 | runs-on: ubuntu-latest 13 | if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' 14 | permissions: 15 | pull-requests: write 16 | actions: read 17 | steps: 18 | # DO NOT run actions/checkout here, for security reasons 19 | # For details, refer to https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ 20 | - name: Post comment 21 | uses: py-cov-action/python-coverage-comment-action@e623398c19eb3853a5572d4a516e10b15b5cefbc 22 | with: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | GITHUB_PR_RUN_ID: ${{ github.event.workflow_run.id }} -------------------------------------------------------------------------------- /src/dishka/_adaptix/type_tools/norm_utils.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from dataclasses import InitVar 3 | from typing import Annotated, ClassVar, Final, TypeVar 4 | 5 | from ..feature_requirement import HAS_TYPED_DICT_REQUIRED 6 | from .normalize_type import BaseNormType 7 | 8 | _TYPE_TAGS = [Final, ClassVar, InitVar, Annotated] 9 | 10 | if HAS_TYPED_DICT_REQUIRED: 11 | _TYPE_TAGS.extend([typing.Required, typing.NotRequired]) 12 | 13 | 14 | def strip_tags(norm: BaseNormType) -> BaseNormType: 15 | """Removes type hints that do not represent a type 16 | and that only indicates metadata 17 | """ 18 | if norm.origin in _TYPE_TAGS: 19 | return strip_tags(norm.args[0]) 20 | return norm 21 | 22 | 23 | N = TypeVar("N", bound=BaseNormType) 24 | 25 | 26 | def is_class_var(norm: BaseNormType) -> bool: 27 | if norm.origin == ClassVar: 28 | return True 29 | if norm.origin in _TYPE_TAGS: 30 | return is_class_var(norm.args[0]) 31 | return False 32 | -------------------------------------------------------------------------------- /tests/integrations/base/test_wrap_injection.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable, Iterable, Iterator 2 | from unittest.mock import Mock 3 | 4 | import pytest 5 | 6 | from dishka import AsyncContainer, FromDishka 7 | from dishka.integrations.base import wrap_injection 8 | from dishka.integrations.exceptions import InvalidInjectedFuncTypeError 9 | 10 | 11 | def sync_func(mock: FromDishka[Mock]) -> None: 12 | mock.some_func() 13 | 14 | 15 | def sync_gen(data: Iterable[int], x: FromDishka[Mock]) -> Iterator[int]: 16 | for i in data: 17 | yield x.some_func(i) 18 | 19 | 20 | @pytest.mark.parametrize("func", [sync_func, sync_gen]) 21 | def test_invalid_injected_func_type( 22 | func: Callable, 23 | async_container: AsyncContainer, 24 | ) -> None: 25 | with pytest.raises(InvalidInjectedFuncTypeError): 26 | wrap_injection( 27 | func=func, 28 | container_getter=lambda *_: async_container, 29 | is_async=True, 30 | ) 31 | -------------------------------------------------------------------------------- /examples/integrations/taskiq_app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import random 3 | 4 | from dishka import FromDishka, Provider, Scope, make_async_container 5 | from dishka.integrations.taskiq import TaskiqProvider, inject, setup_dishka 6 | from taskiq import AsyncTaskiqTask, InMemoryBroker 7 | 8 | provider = Provider(scope=Scope.REQUEST) 9 | provider.provide(lambda: random.random(), provides=float) 10 | 11 | broker = InMemoryBroker() 12 | 13 | 14 | @broker.task 15 | @inject 16 | async def random_task(num: FromDishka[float]) -> float: 17 | raise ValueError 18 | 19 | 20 | async def main() -> None: 21 | container = make_async_container(provider, TaskiqProvider()) 22 | setup_dishka(container, broker) 23 | await broker.startup() 24 | 25 | task: AsyncTaskiqTask[float] = await random_task.kiq() 26 | result = await task.wait_result() 27 | print(result.return_value) 28 | 29 | await broker.shutdown() 30 | await container.close() 31 | 32 | 33 | if __name__ == "__main__": 34 | asyncio.run(main()) 35 | -------------------------------------------------------------------------------- /src/dishka/entities/scope.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from enum import Enum 3 | from typing import Any 4 | 5 | 6 | @dataclass(slots=True) 7 | class _ScopeValue: 8 | name: str 9 | skip: bool 10 | 11 | 12 | def new_scope(value: str, *, skip: bool = False) -> _ScopeValue: 13 | return _ScopeValue(value, skip) 14 | 15 | 16 | class BaseScope(Enum): 17 | __slots__ = ("name", "skip") 18 | 19 | def __init__(self, value: _ScopeValue) -> None: 20 | self.name = value.name # type: ignore[misc] 21 | self.skip = value.skip 22 | 23 | 24 | class Scope(BaseScope): 25 | RUNTIME = new_scope("RUNTIME", skip=True) 26 | APP = new_scope("APP") 27 | SESSION = new_scope("SESSION", skip=True) 28 | REQUEST = new_scope("REQUEST") 29 | ACTION = new_scope("ACTION") 30 | STEP = new_scope("STEP") 31 | 32 | 33 | class InvalidScopes(BaseScope): 34 | UNKNOWN_SCOPE = new_scope("", skip=True) 35 | 36 | def __str__(self) -> Any: 37 | return str(self.value.name) 38 | -------------------------------------------------------------------------------- /docs/advanced/testing/fixtures.py: -------------------------------------------------------------------------------- 1 | from sqlite3 import Connection 2 | from unittest.mock import Mock 3 | 4 | import pytest 5 | import pytest_asyncio 6 | from fastapi.testclient import TestClient 7 | 8 | from dishka import Provider, Scope, provide, make_async_container 9 | from dishka.integrations.fastapi import setup_dishka 10 | 11 | 12 | class MockConnectionProvider(Provider): 13 | @provide(scope=Scope.APP) 14 | def get_connection(self) -> Connection: 15 | connection = Mock() 16 | connection.execute = Mock(return_value="1") 17 | return connection 18 | 19 | 20 | @pytest.fixture 21 | def container(): 22 | container = make_async_container(MockConnectionProvider()) 23 | yield container 24 | container.close() 25 | 26 | 27 | @pytest.fixture 28 | def client(container): 29 | app = create_app() 30 | setup_dishka(container, app) 31 | with TestClient(app) as client: 32 | yield client 33 | 34 | 35 | @pytest_asyncio.fixture 36 | async def connection(container): 37 | return await container.get(Connection) 38 | -------------------------------------------------------------------------------- /tests/unit/container/override/test_provide_all.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from dishka import ( 4 | STRICT_VALIDATION, 5 | Provider, 6 | Scope, 7 | make_container, 8 | provide_all, 9 | ) 10 | from dishka.exceptions import ImplicitOverrideDetectedError 11 | 12 | 13 | def test_not_override() -> None: 14 | class TestProvider(Provider): 15 | scope = Scope.APP 16 | provides = ( 17 | provide_all(int, str) 18 | + provide_all(int, str) 19 | ) 20 | 21 | with pytest.raises(ImplicitOverrideDetectedError): 22 | make_container( 23 | TestProvider(), 24 | validation_settings=STRICT_VALIDATION, 25 | ) 26 | 27 | 28 | def test_override() -> None: 29 | class TestProvider(Provider): 30 | scope = Scope.APP 31 | provides = ( 32 | provide_all(int, str) 33 | + provide_all(int, str, override=True) 34 | ) 35 | 36 | make_container( 37 | TestProvider(), 38 | validation_settings=STRICT_VALIDATION, 39 | ) 40 | -------------------------------------------------------------------------------- /tests/unit/container/test_dynamic.py: -------------------------------------------------------------------------------- 1 | from typing import NewType 2 | 3 | from dishka import ( 4 | Container, 5 | Provider, 6 | Scope, 7 | from_context, 8 | make_container, 9 | provide, 10 | ) 11 | 12 | Request = NewType("Request", int) 13 | 14 | 15 | class A: 16 | pass 17 | 18 | 19 | class A0(A): 20 | pass 21 | 22 | 23 | class A1(A): 24 | pass 25 | 26 | 27 | class MyProvider(Provider): 28 | request = from_context(Request, scope=Scope.REQUEST) 29 | a0 = provide(A0, scope=Scope.APP) 30 | a1 = provide(A1, scope=Scope.APP) 31 | 32 | @provide(scope=Scope.REQUEST) 33 | def get_a(self, container: Container, request: Request) -> A: 34 | if request == 0: 35 | return container.get(A0) 36 | else: 37 | return container.get(A1) 38 | 39 | 40 | def test_dynamic(): 41 | container = make_container(MyProvider()) 42 | with container({Request: 0}) as c: 43 | assert type(c.get(A)) is A0 44 | with container({Request: 1}) as c: 45 | assert type(c.get(A)) is A1 46 | -------------------------------------------------------------------------------- /src/dishka/context_proxy.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterator, MutableMapping 2 | from typing import Any, NoReturn 3 | 4 | from .entities.key import DependencyKey 5 | 6 | 7 | class ContextProxy(MutableMapping[DependencyKey, Any]): 8 | def __init__( 9 | self, 10 | context: dict[DependencyKey, Any], 11 | cache: dict[DependencyKey, Any], 12 | ) -> None: 13 | self._cache = cache 14 | self._context = context 15 | 16 | def __setitem__(self, key: DependencyKey, value: Any) -> None: 17 | self._cache[key] = value 18 | self._context[key] = value 19 | 20 | def __delitem__(self, key: DependencyKey) -> NoReturn: 21 | raise RuntimeError( # noqa: TRY003 22 | "Cannot delete anything from context", 23 | ) 24 | 25 | def __getitem__(self, key: DependencyKey) -> Any: 26 | return self._cache[key] 27 | 28 | def __len__(self) -> int: 29 | return len(self._cache) 30 | 31 | def __iter__(self) -> Iterator[DependencyKey]: 32 | return iter(self._cache) 33 | -------------------------------------------------------------------------------- /tests/unit/plotter/test_wrappers.py: -------------------------------------------------------------------------------- 1 | from typing import Protocol 2 | 3 | import pytest 4 | 5 | from dishka import ( 6 | Provider, 7 | Scope, 8 | alias, 9 | decorate, 10 | make_async_container, 11 | make_container, 12 | provide, 13 | ) 14 | from dishka.plotter import render_d2, render_mermaid 15 | 16 | 17 | class P(Protocol): 18 | pass 19 | 20 | 21 | class MyProvider(Provider): 22 | component = "XXX" 23 | 24 | @provide(scope=Scope.APP) 25 | def foo(self) -> int: 26 | return 1 27 | 28 | @provide(scope=Scope.REQUEST) 29 | def bar(self, i: int) -> P: 30 | ... 31 | 32 | @decorate 33 | def foobar(self, i: int) -> int: 34 | return i 35 | 36 | float_alias = alias(source=int, provides=float) 37 | 38 | 39 | @pytest.mark.parametrize("container", [ 40 | make_container(MyProvider()), 41 | make_async_container(MyProvider()), 42 | ]) 43 | @pytest.mark.parametrize("renderer", [ 44 | render_d2, render_mermaid, 45 | ]) 46 | def test_wrapper(container, renderer): 47 | assert renderer(container) 48 | -------------------------------------------------------------------------------- /examples/real_world/main_web.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from contextlib import asynccontextmanager 3 | 4 | import uvicorn 5 | from dishka import make_async_container 6 | from dishka.integrations.fastapi import setup_dishka 7 | from fastapi import FastAPI 8 | from myapp.ioc import AdaptersProvider, InteractorProvider 9 | from myapp.presentation_web import router 10 | 11 | 12 | def create_fastapi_app() -> FastAPI: 13 | app = FastAPI(lifespan=lifespan) 14 | app.include_router(router) 15 | return app 16 | 17 | 18 | @asynccontextmanager 19 | async def lifespan(app: FastAPI): 20 | yield 21 | await app.state.dishka_container.close() 22 | 23 | 24 | def create_app(): 25 | logging.basicConfig( 26 | level=logging.INFO, 27 | format="%(asctime)s %(process)-7s %(module)-20s %(message)s", 28 | ) 29 | app = create_fastapi_app() 30 | container = make_async_container(AdaptersProvider(), InteractorProvider()) 31 | setup_dishka(container, app) 32 | return app 33 | 34 | 35 | if __name__ == "__main__": 36 | uvicorn.run(create_app(), host="0.0.0.0", port=8000) 37 | -------------------------------------------------------------------------------- /tests/integrations/aiogram_dialog/conftest.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterable 2 | from typing import NewType 3 | from unittest.mock import Mock 4 | 5 | from dishka import Provider, Scope, provide 6 | 7 | AppDep = NewType("AppDep", str) 8 | APP_DEP_VALUE = "APP" 9 | 10 | RequestDep = NewType("RequestDep", str) 11 | REQUEST_DEP_VALUE = "REQUEST" 12 | 13 | WebSocketDep = NewType("WebSocketDep", str) 14 | WS_DEP_VALUE = "WS" 15 | 16 | 17 | class AppProvider(Provider): 18 | def __init__(self): 19 | super().__init__() 20 | self.app_released = Mock() 21 | self.request_released = Mock() 22 | self.websocket_released = Mock() 23 | self.mock = Mock() 24 | 25 | @provide(scope=Scope.APP) 26 | def app(self) -> Iterable[AppDep]: 27 | yield APP_DEP_VALUE 28 | self.app_released() 29 | 30 | @provide(scope=Scope.REQUEST) 31 | def request(self) -> Iterable[RequestDep]: 32 | yield REQUEST_DEP_VALUE 33 | self.request_released() 34 | 35 | @provide(scope=Scope.REQUEST) 36 | def mock(self) -> Mock: 37 | return self.mock 38 | -------------------------------------------------------------------------------- /src/dishka/entities/provides_marker.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | import threading 5 | from typing import TYPE_CHECKING, Any, Generic, TypeVar 6 | 7 | __all__ = ["AnyOf", "ProvideMultiple"] 8 | 9 | 10 | if sys.version_info >= (3, 11): 11 | from typing import TypeVarTuple, Unpack 12 | 13 | Variants = TypeVarTuple("Variants") 14 | 15 | class ProvideMultiple(Generic[Unpack[Variants]]): 16 | pass 17 | else: 18 | # in this case we simulate generics with variadic typevars 19 | # by suppressing builtin checks of parameters length 20 | Variants = TypeVar("Variants") 21 | provides_lock = threading.Lock() 22 | 23 | class ProvideMultiple(Generic[Variants]): 24 | def __class_getitem__(cls, item: tuple[Any]) -> Any: 25 | with provides_lock: 26 | cls.__parameters__ = [Variants]*len(item) # type: ignore[attr-defined, misc] 27 | return super().__class_getitem__(item) # type: ignore[misc] 28 | 29 | 30 | if TYPE_CHECKING: 31 | from typing import Union as AnyOf 32 | else: 33 | AnyOf = ProvideMultiple 34 | -------------------------------------------------------------------------------- /docs/advanced/generics.rst: -------------------------------------------------------------------------------- 1 | Generic types 2 | ===================== 3 | 4 | You can use ``dishka`` with ``TypeVars`` and ``Generic``-classes. 5 | 6 | .. note:: 7 | 8 | Though generics are supported, there are some limitations: 9 | 10 | * You cannot use ``TypeVar`` bounded to a ``Generic`` type. 11 | * ``Generic``-decorators are only applied to concrete factories or factories with more narrow ``TypeVars``. 12 | 13 | Creating objects with @provide 14 | ************************************ 15 | 16 | You can create generic factories, use ``type[T]`` to access resolved value of ``TypeVar``. ``TypeVar`` can have bound or constraints, which are checked. 17 | For example, here we have a factory providing instances of generic class ``A``. Note that ``A[int]`` and ``A[bool]`` are different types and cached separately. 18 | 19 | .. literalinclude:: ./generics_examples/provide.py 20 | 21 | Decorating objects with @decorate 22 | *************************************** 23 | 24 | You can also make ``Generic`` decorator. Here it is used to decorate any type. 25 | 26 | .. literalinclude:: ./generics_examples/decorate.py 27 | -------------------------------------------------------------------------------- /docs/integrations/arq.rst: -------------------------------------------------------------------------------- 1 | .. _arq: 2 | 3 | arq 4 | ================ 5 | 6 | Though it is not required, you can use *dishka-arq* integration. It features: 7 | 8 | * automatic *REQUEST* scope management using middleware 9 | * injection of dependencies into task handler function using decorator. 10 | 11 | 12 | How to use 13 | **************** 14 | 15 | 1. Import 16 | 17 | .. code-block:: python 18 | 19 | from dishka.integrations.arq import ( 20 | FromDishka, 21 | inject, 22 | setup_dishka, 23 | ) 24 | 25 | 2. Create provider and container as usual 26 | 27 | 3. Mark those of your handlers parameters which are to be injected with ``FromDishka[]`` and decorate them using ``@inject`` 28 | 29 | .. code-block:: python 30 | 31 | @inject 32 | async def get_content( 33 | context: dict[Any, Any], 34 | gateway: FromDishka[Gateway], 35 | ): 36 | ... 37 | 38 | 4. Setup ``dishka`` integration on your ``Worker`` class or directly on ``WorkerSettings`` 39 | 40 | .. code-block:: python 41 | 42 | setup_dishka(container=container, worker_settings=WorkerSettings) 43 | -------------------------------------------------------------------------------- /docs/integrations/aiogram_dialog.rst: -------------------------------------------------------------------------------- 1 | .. _aiogram_dialog: 2 | 3 | aiogram-dialog 4 | =========================================== 5 | 6 | 7 | Though it is not required, you can use *dishka-aiogram_dialog* integration. It allows you to inject object into ``aiogram-dialog`` handlers. 8 | 9 | 10 | How to use 11 | **************** 12 | 13 | 1. Setup :ref:`aiogram integration` 14 | 15 | 2. Import decorator 16 | 17 | .. code-block:: python 18 | 19 | from dishka.integrations.aiogram_dialog import FromDishka, inject 20 | 21 | 22 | 3. Mark those of your ``aiogram-dialog`` handlers and getters parameters which are to be injected with ``FromDishka[]`` decorate them using imported ``@inject`` decorator. 23 | 24 | .. code-block:: python 25 | 26 | @inject 27 | async def getter( 28 | a: FromDishka[RequestDep], 29 | mock: FromDishka[Mock], 30 | **kwargs, 31 | ): 32 | ... 33 | 34 | 35 | @inject 36 | async def on_click( 37 | event, 38 | widget, 39 | manager, 40 | a: FromDishka[RequestDep], 41 | mock: FromDishka[Mock], 42 | ): 43 | ... 44 | -------------------------------------------------------------------------------- /examples/integrations/arq/run_with_settings.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Any, Protocol 3 | 4 | from dishka import FromDishka, Provider, Scope, make_async_container, provide 5 | from dishka.integrations.arq import inject, setup_dishka 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class Gateway(Protocol): 11 | async def get(self) -> int: ... 12 | 13 | 14 | class MockGateway(Gateway): 15 | async def get(self) -> int: 16 | return hash(self) 17 | 18 | 19 | class GatewayProvider(Provider): 20 | get_gateway = provide(MockGateway, scope=Scope.REQUEST, provides=Gateway) 21 | 22 | 23 | @inject 24 | async def get_content( 25 | context: dict[Any, Any], 26 | gateway: FromDishka[Gateway], 27 | ): 28 | result = await gateway.get() 29 | logger.info(result) 30 | 31 | 32 | class WorkerSettings: 33 | functions = [get_content] # noqa: RUF012 34 | 35 | 36 | logging.basicConfig( 37 | level=logging.DEBUG, 38 | format="%(asctime)s %(process)-7s %(module)-20s %(message)s", 39 | ) 40 | 41 | container = make_async_container(GatewayProvider()) 42 | setup_dishka(container=container, worker_settings=WorkerSettings) 43 | -------------------------------------------------------------------------------- /examples/integrations/telebot_bot.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import random 4 | from collections.abc import Iterable 5 | 6 | import telebot 7 | from dishka import Provider, Scope, make_container, provide 8 | from dishka.integrations.telebot import ( 9 | FromDishka, 10 | TelebotProvider, 11 | inject, 12 | setup_dishka, 13 | ) 14 | from telebot.types import Message 15 | 16 | 17 | # app dependency logic 18 | class MyProvider(Provider): 19 | @provide(scope=Scope.APP) 20 | def get_int(self) -> Iterable[int]: 21 | print("solve int") 22 | yield random.randint(0, 10000) 23 | 24 | 25 | # app 26 | API_TOKEN = os.getenv("BOT_TOKEN") 27 | bot = telebot.TeleBot(API_TOKEN, use_class_middlewares=True) 28 | 29 | 30 | @bot.message_handler() 31 | @inject 32 | def start( 33 | message: Message, 34 | value: FromDishka[int], 35 | ): 36 | bot.reply_to(message, f"Hello, {value}!") 37 | 38 | 39 | logging.basicConfig(level=logging.INFO) 40 | 41 | container = make_container(MyProvider(), TelebotProvider()) 42 | setup_dishka(container=container, bot=bot) 43 | try: 44 | bot.infinity_polling() 45 | finally: 46 | container.close() 47 | -------------------------------------------------------------------------------- /src/dishka/provider/root_context.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterable 2 | from typing import Any 3 | 4 | from dishka.entities.component import DEFAULT_COMPONENT 5 | from dishka.entities.scope import BaseScope 6 | from .base_provider import BaseProvider 7 | from .provider import Provider 8 | 9 | 10 | def make_root_context_provider( 11 | providers: Iterable[BaseProvider], 12 | context: dict[Any, Any] | None, 13 | scopes: type[BaseScope], 14 | ) -> BaseProvider: 15 | """Automatically add missing `from_context`.""" 16 | # in non-default component, context vars are aliases for it 17 | # use only those, which declared in Default component provider 18 | existing_context_vars = { 19 | var.provides.type_hint 20 | for provider in providers 21 | if provider.component == DEFAULT_COMPONENT 22 | for var in provider.context_vars 23 | } 24 | p = Provider() 25 | if not context: 26 | return p 27 | root_scope = next(iter(scopes)) 28 | for type_hint in context: 29 | if type_hint not in existing_context_vars: 30 | p.from_context(provides=type_hint, scope=root_scope) 31 | return p 32 | -------------------------------------------------------------------------------- /.ruff.toml: -------------------------------------------------------------------------------- 1 | line-length = 79 2 | target-version = "py310" 3 | 4 | include = [ 5 | "src/**.py", 6 | "tests/**.py", 7 | "examples/**.py", 8 | ] 9 | exclude = [ 10 | "src/dishka/_adaptix/**", 11 | ] 12 | 13 | lint.select = [ 14 | "ALL" 15 | ] 16 | lint.ignore = [ 17 | "ARG", 18 | "ANN", 19 | "D", 20 | "EM101", 21 | "EM102", 22 | "PT001", 23 | "PT023", 24 | "SIM108", 25 | "RET505", 26 | "PLR0913", 27 | "SIM103", 28 | "ISC003", 29 | 30 | # identical by code != identical by meaning 31 | "SIM114", 32 | 33 | # awful things, never use. 34 | # It makes runtime work differently from typechecker 35 | "TC001", 36 | "TC002", 37 | "TC003", 38 | "TC006", 39 | ] 40 | 41 | [lint.per-file-ignores] 42 | "tests/**" = [ 43 | "TID252", 44 | "PLR2004", 45 | "S101", 46 | "TRY003", 47 | "PLW1641", 48 | "PYI059" 49 | ] 50 | 51 | [per-file-target-version] 52 | "tests/unit/container/type_alias_type_provider.py" = "py312" 53 | "tests/unit/container/pep695_new_syntax.py" = "py312" 54 | 55 | [lint.isort] 56 | no-lines-before = ["local-folder"] 57 | 58 | [lint.flake8-tidy-imports] 59 | ban-relative-imports = "parents" 60 | -------------------------------------------------------------------------------- /src/dishka/_adaptix/type_tools/constants.py: -------------------------------------------------------------------------------- 1 | # mypy: disable-error-code="dict-item" 2 | import collections 3 | import concurrent.futures 4 | import queue 5 | import re 6 | from collections.abc import Mapping 7 | from os import PathLike 8 | from typing import TypeVar 9 | 10 | from ..common import VarTuple 11 | 12 | _AnyStrT = TypeVar("_AnyStrT", str, bytes) 13 | _T1 = TypeVar("_T1") 14 | _T2 = TypeVar("_T2") 15 | _T1_co = TypeVar("_T1_co", covariant=True) 16 | _AnyStr_co = TypeVar("_AnyStr_co", str, bytes, covariant=True) 17 | 18 | BUILTIN_ORIGIN_TO_TYPEVARS: Mapping[type, VarTuple[TypeVar]] = { 19 | re.Pattern: (_AnyStrT, ), 20 | re.Match: (_AnyStrT, ), 21 | PathLike: (_AnyStr_co, ), 22 | type: (_T1,), 23 | list: (_T1,), 24 | set: (_T1,), 25 | frozenset: (_T1_co, ), 26 | collections.Counter: (_T1,), 27 | collections.deque: (_T1,), 28 | dict: (_T1, _T2), 29 | collections.defaultdict: (_T1, _T2), 30 | collections.OrderedDict: (_T1, _T2), 31 | collections.ChainMap: (_T1, _T2), 32 | queue.Queue: (_T1, ), 33 | queue.PriorityQueue: (_T1, ), 34 | queue.LifoQueue: (_T1, ), 35 | queue.SimpleQueue: (_T1, ), 36 | concurrent.futures.Future: (_T1, ), 37 | } 38 | -------------------------------------------------------------------------------- /src/dishka/provider/make_alias.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from dishka.dependency_source import ( 4 | Alias, 5 | CompositeDependencySource, 6 | ensure_composite, 7 | ) 8 | from dishka.entities.component import Component 9 | from dishka.entities.key import hint_to_dependency_key 10 | from .unpack_provides import unpack_alias 11 | 12 | 13 | def alias( 14 | source: Any, 15 | *, 16 | provides: Any | None = None, 17 | cache: bool = True, 18 | component: Component | None = None, 19 | override: bool = False, 20 | ) -> CompositeDependencySource: 21 | if component is provides is None: 22 | raise ValueError( # noqa: TRY003 23 | "Either component or provides must be set in alias", 24 | ) 25 | if provides is None: 26 | provides = source 27 | 28 | composite = ensure_composite(source) 29 | alias_instance = Alias( 30 | source=hint_to_dependency_key(source).with_component(component), 31 | provides=hint_to_dependency_key(provides), 32 | cache=cache, 33 | override=override, 34 | ) 35 | composite.dependency_sources.extend(unpack_alias(alias_instance)) 36 | return composite 37 | -------------------------------------------------------------------------------- /examples/real_world/myapp/ioc.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterable 2 | 3 | from dishka import ( 4 | Provider, 5 | Scope, 6 | alias, 7 | provide, 8 | ) 9 | 10 | from .api_client import FakeWarehouseClient 11 | from .db import FakeCommitter, FakeProductGateway, FakeUserGateway 12 | from .use_cases import ( 13 | AddProductsInteractor, 14 | Committer, 15 | ProductGateway, 16 | UserGateway, 17 | WarehouseClient, 18 | ) 19 | 20 | 21 | # app dependency logic 22 | class AdaptersProvider(Provider): 23 | scope = Scope.REQUEST 24 | 25 | users = provide(FakeUserGateway, provides=UserGateway) 26 | products = provide(FakeProductGateway, provides=ProductGateway) 27 | 28 | @provide 29 | def connection(self) -> Iterable[FakeCommitter]: 30 | committer = FakeCommitter() 31 | yield committer 32 | committer.close() 33 | 34 | committer = alias(source=FakeCommitter, provides=Committer) 35 | 36 | @provide(scope=Scope.APP) 37 | def warehouse(self) -> WarehouseClient: 38 | return FakeWarehouseClient() 39 | 40 | 41 | class InteractorProvider(Provider): 42 | scope = Scope.REQUEST 43 | 44 | product = provide(AddProductsInteractor) 45 | -------------------------------------------------------------------------------- /src/dishka/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "DEFAULT_COMPONENT", 3 | "STRICT_VALIDATION", 4 | "AnyOf", 5 | "AsyncContainer", 6 | "BaseScope", 7 | "Component", 8 | "Container", 9 | "DependencyKey", 10 | "FromComponent", 11 | "FromDishka", 12 | "Provider", 13 | "Scope", 14 | "ValidationSettings", 15 | "WithParents", 16 | "alias", 17 | "decorate", 18 | "from_context", 19 | "make_async_container", 20 | "make_container", 21 | "new_scope", 22 | "provide", 23 | "provide_all", 24 | ] 25 | 26 | from .async_container import AsyncContainer, make_async_container 27 | from .container import Container, make_container 28 | from .entities.component import DEFAULT_COMPONENT, Component 29 | from .entities.depends_marker import FromDishka 30 | from .entities.key import DependencyKey, FromComponent 31 | from .entities.provides_marker import AnyOf 32 | from .entities.scope import BaseScope, Scope, new_scope 33 | from .entities.validation_settings import STRICT_VALIDATION, ValidationSettings 34 | from .entities.with_parents import WithParents 35 | from .provider import ( 36 | Provider, 37 | alias, 38 | decorate, 39 | from_context, 40 | provide, 41 | provide_all, 42 | ) 43 | -------------------------------------------------------------------------------- /examples/real_world/myapp/db.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from .use_cases import Committer, Product, ProductGateway, User, UserGateway 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | class FakeCommitter(Committer): 9 | def __init__(self): 10 | logger.info("init FakeCommitter as %s", self) 11 | 12 | def commit(self) -> None: 13 | logger.info("commit as %s", self) 14 | 15 | def close(self) -> None: 16 | logger.info("close as %s", self) 17 | 18 | 19 | class FakeUserGateway(UserGateway): 20 | def __init__(self, committer: FakeCommitter): 21 | self.committer = committer 22 | logger.info("init FakeUserGateway with %s", committer) 23 | 24 | def get_user(self, user_id: int) -> User: 25 | logger.info("get_user %s as %s", user_id, self) 26 | return User() 27 | 28 | 29 | class FakeProductGateway(ProductGateway): 30 | def __init__(self, committer: FakeCommitter): 31 | self.committer = committer 32 | logger.info("init FakeProductGateway with %s", committer) 33 | 34 | def add_product(self, product: Product) -> None: 35 | logger.info( 36 | "add_product %s for user %s, by %s", 37 | product.name, product.owner_id, self, 38 | ) 39 | -------------------------------------------------------------------------------- /examples/integrations/faststream_app.py: -------------------------------------------------------------------------------- 1 | from dishka import Provider, Scope, make_async_container, provide 2 | from dishka.integrations.faststream import ( 3 | FastStreamProvider, 4 | FromDishka, 5 | setup_dishka, 6 | ) 7 | from faststream import ContextRepo, FastStream 8 | from faststream.nats import NatsBroker, NatsMessage 9 | 10 | 11 | class A: 12 | def __init__(self) -> None: 13 | pass 14 | 15 | 16 | class B: 17 | def __init__(self, a: A) -> None: 18 | self.a = a 19 | 20 | 21 | class MyProvider(Provider): 22 | @provide(scope=Scope.APP) 23 | def get_a(self) -> A: 24 | return A() 25 | 26 | @provide(scope=Scope.REQUEST) 27 | def get_b(self, a: A) -> B: 28 | return B(a) 29 | 30 | 31 | provider = MyProvider() 32 | container = make_async_container(provider, FastStreamProvider()) 33 | 34 | broker = NatsBroker() 35 | app = FastStream(broker) 36 | setup_dishka(container, app, auto_inject=True) 37 | 38 | 39 | @broker.subscriber("test") 40 | async def handler( 41 | msg: str, 42 | a: FromDishka[A], 43 | b: FromDishka[B], 44 | raw_message: FromDishka[NatsMessage], 45 | faststream_context: FromDishka[ContextRepo], 46 | ): 47 | print(msg, a, b) 48 | 49 | 50 | @app.after_startup 51 | async def t(): 52 | await broker.publish("test", "test") 53 | -------------------------------------------------------------------------------- /src/dishka/integrations/celery.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "DishkaTask", 3 | "FromDishka", 4 | "inject", 5 | "setup_dishka", 6 | ] 7 | 8 | from collections.abc import Callable 9 | from typing import Final, ParamSpec, TypeVar 10 | 11 | from celery import Celery, Task, current_app 12 | from celery.utils.functional import head_from_fun 13 | 14 | from dishka import Container, FromDishka 15 | from dishka.integrations.base import is_dishka_injected, wrap_injection 16 | 17 | CONTAINER_NAME: Final = "dishka_container" 18 | 19 | 20 | T = TypeVar("T") 21 | P = ParamSpec("P") 22 | 23 | 24 | def inject(func: Callable[P, T]) -> Callable[P, T]: 25 | return wrap_injection( 26 | func=func, 27 | is_async=False, 28 | container_getter=lambda args, kwargs: current_app.conf[ 29 | CONTAINER_NAME 30 | ], 31 | manage_scope=True, 32 | ) 33 | 34 | 35 | def setup_dishka(container: Container, app: Celery): 36 | app.conf[CONTAINER_NAME] = container 37 | 38 | 39 | class DishkaTask(Task): 40 | def __init__(self) -> None: 41 | super().__init__() 42 | 43 | run = self.run 44 | 45 | if not is_dishka_injected(run): 46 | injected_func = inject(run) 47 | self.run = injected_func 48 | 49 | self.__header__ = head_from_fun(injected_func) 50 | -------------------------------------------------------------------------------- /.github/workflows/frameworks-latest.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: TestLatest 5 | 6 | on: 7 | schedule: 8 | - cron: "0 0 * * *" 9 | 10 | jobs: 11 | cpython: 12 | runs-on: ${{ matrix.os }} 13 | permissions: 14 | contents: read 15 | strategy: 16 | matrix: 17 | os: 18 | - ubuntu-latest 19 | python-version: 20 | - "3.10" 21 | - "3.11" 22 | - "3.12" 23 | - "3.13" 24 | - "3.14" 25 | 26 | steps: 27 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 28 | with: 29 | persist-credentials: false 30 | - name: Set up ${{ matrix.python-version }} on ${{ matrix.os }} 31 | uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 32 | with: 33 | python-version: ${{ matrix.python-version }} 34 | 35 | - name: Install uv 36 | uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 37 | 38 | - name: Install dependencies 39 | run: | 40 | uv pip install . -r requirements_dev.txt --system 41 | 42 | - name: Run tests 43 | run: | 44 | nox -t latest 45 | -------------------------------------------------------------------------------- /src/dishka/entities/depends_marker.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | from typing import TYPE_CHECKING, Annotated, TypeVar 3 | 4 | from .component import DEFAULT_COMPONENT, Component 5 | from .key import FromComponent, _FromComponent 6 | 7 | T = TypeVar("T") 8 | 9 | if TYPE_CHECKING: 10 | from typing import Union 11 | FromDishka = Union[T, T] # noqa: UP007,PYI016 12 | else: 13 | class FromDishka: 14 | def __init__(self, component: Component = None): 15 | if component is None: 16 | self.component = DEFAULT_COMPONENT 17 | warnings.warn( 18 | "Annotated[Cls, FromDishka()] is deprecated " 19 | "use `FromDishka[Cls]` or " 20 | "`Annotated[Cls, FromComponent()]` instead", 21 | DeprecationWarning, 22 | stacklevel=2, 23 | ) 24 | 25 | else: 26 | self.component = component 27 | warnings.warn( 28 | "Annotated[Cls, FromDishka(component)] is deprecated " 29 | "use `Annotated[Cls, FromComponent(component)]` instead", 30 | DeprecationWarning, 31 | stacklevel=2, 32 | ) 33 | 34 | def __class_getitem__(cls, item: T) -> Annotated[T, _FromComponent]: 35 | return Annotated[item, FromComponent()] 36 | -------------------------------------------------------------------------------- /tests/unit/text_rendering/test_name.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from typing import Generic, TypeVar 3 | 4 | import pytest 5 | 6 | import dishka 7 | from dishka.text_rendering import get_name 8 | 9 | 10 | class A0: 11 | class A1: 12 | def foo(self): ... 13 | 14 | @staticmethod 15 | def foo_class(param): ... 16 | 17 | def bar(self): ... 18 | 19 | 20 | def baz(): ... 21 | 22 | 23 | T = TypeVar("T") 24 | 25 | 26 | class GenericA(Generic[T]): 27 | pass 28 | 29 | 30 | @pytest.mark.parametrize( 31 | ("obj", "include_module", "name"), [ 32 | (A0, False, "A0"), 33 | (A0, True, "tests.unit.text_rendering.test_name.A0"), 34 | (A0.A1, False, "A0.A1"), 35 | (A0.A1.foo, False, "A0.A1.foo"), 36 | (A0.A1.foo_class, False, "A0.A1.foo_class"), 37 | (A0.bar, False, "A0.bar"), 38 | (baz, False, "baz"), 39 | (int, False, "int"), 40 | (str, True, "str"), 41 | (None, False, "None"), 42 | (..., False, "..."), 43 | (dishka.Scope, True, "dishka.entities.scope.Scope"), 44 | (GenericA[int], False, "GenericA[int]"), 45 | (GenericA[T], False, "GenericA[T]"), 46 | (Callable[[str], str], False, "Callable[[str], str]"), 47 | (GenericA, False, "GenericA"), 48 | ], 49 | ) 50 | def test_get_name(obj, include_module, name): 51 | assert get_name(obj, include_module=include_module) == name 52 | -------------------------------------------------------------------------------- /examples/integrations/aiohttp_app.py: -------------------------------------------------------------------------------- 1 | from typing import Protocol 2 | 3 | from aiohttp.web import run_app 4 | from aiohttp.web_app import Application 5 | from aiohttp.web_response import Response 6 | from aiohttp.web_routedef import RouteTableDef 7 | from dishka import ( 8 | Provider, 9 | Scope, 10 | make_async_container, 11 | provide, 12 | ) 13 | from dishka.integrations.aiohttp import ( 14 | DISHKA_CONTAINER_KEY, 15 | AiohttpProvider, 16 | FromDishka, 17 | inject, 18 | setup_dishka, 19 | ) 20 | 21 | 22 | class Gateway(Protocol): 23 | async def get(self) -> int: ... 24 | 25 | 26 | class MockGateway(Gateway): 27 | async def get(self) -> int: 28 | return hash(self) 29 | 30 | 31 | class GatewayProvider(Provider): 32 | get_gateway = provide(MockGateway, scope=Scope.REQUEST, provides=Gateway) 33 | 34 | 35 | router = RouteTableDef() 36 | 37 | 38 | @router.get("/") 39 | @inject 40 | async def endpoint( 41 | request: str, gateway: FromDishka[Gateway], 42 | ) -> Response: 43 | data = await gateway.get() 44 | return Response(text=f"gateway data: {data}") 45 | 46 | 47 | async def on_shutdown(app: Application): 48 | await app[DISHKA_CONTAINER_KEY].close() 49 | 50 | 51 | app = Application() 52 | app.add_routes(router) 53 | 54 | container = make_async_container(GatewayProvider(), AiohttpProvider()) 55 | setup_dishka(container=container, app=app) 56 | app.on_shutdown.append(on_shutdown) 57 | run_app(app) 58 | -------------------------------------------------------------------------------- /examples/integrations/grpcio/grpc_client.py: -------------------------------------------------------------------------------- 1 | import grpc 2 | from grpcio.pb2.service_pb2 import RequestMessage 3 | from grpcio.pb2.service_pb2_grpc import ExampleServiceStub 4 | 5 | 6 | def run(): 7 | with grpc.insecure_channel("localhost:50051") as channel: 8 | stub = ExampleServiceStub(channel) 9 | 10 | # Unary-Unary 11 | response = stub.UnaryUnary(RequestMessage(message="Hello UnaryUnary")) 12 | print("UnaryUnary response:", response.message) 13 | 14 | # Unary-Stream 15 | responses = stub.UnaryStream(RequestMessage( 16 | message="Hello UnaryStream", 17 | )) 18 | for response in responses: 19 | print("UnaryStream response:", response.message) 20 | 21 | # Stream-Unary 22 | requests = [ 23 | RequestMessage(message="Hello StreamUnary 1"), 24 | RequestMessage(message="Hello StreamUnary 2"), 25 | ] 26 | response = stub.StreamUnary(iter(requests)) 27 | print("StreamUnary response:", response.message) 28 | 29 | # Stream-Stream 30 | requests = [ 31 | RequestMessage(message="Hello StreamStream 1"), 32 | RequestMessage(message="Hello StreamStream 2"), 33 | ] 34 | responses = stub.StreamStream(iter(requests)) 35 | for response in responses: 36 | print("StreamStream response:", response.message) 37 | 38 | 39 | if __name__ == "__main__": 40 | run() 41 | -------------------------------------------------------------------------------- /src/dishka/provider/make_context_var.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from dishka.dependency_source import ( 4 | Alias, 5 | CompositeDependencySource, 6 | ContextVariable, 7 | context_stub, 8 | ) 9 | from dishka.entities.component import DEFAULT_COMPONENT 10 | from dishka.entities.key import DependencyKey 11 | from dishka.entities.scope import BaseScope 12 | from dishka.entities.type_alias_type import ( 13 | is_type_alias_type, 14 | unwrap_type_alias, 15 | ) 16 | 17 | 18 | def from_context( 19 | provides: Any, 20 | *, 21 | scope: BaseScope | None = None, 22 | override: bool = False, 23 | ) -> CompositeDependencySource: 24 | composite = CompositeDependencySource(origin=context_stub) 25 | composite.dependency_sources.append( 26 | ContextVariable( 27 | scope=scope, 28 | override=override, 29 | provides=DependencyKey( 30 | type_hint=provides, 31 | component=DEFAULT_COMPONENT, 32 | ), 33 | ), 34 | ) 35 | 36 | if is_type_alias_type(provides): 37 | base_type = unwrap_type_alias(provides) 38 | composite.dependency_sources.append( 39 | Alias( 40 | source=DependencyKey(provides, DEFAULT_COMPONENT), 41 | provides=DependencyKey(base_type, DEFAULT_COMPONENT), 42 | cache=True, 43 | override=override, 44 | ), 45 | ) 46 | return composite 47 | -------------------------------------------------------------------------------- /src/dishka/dependency_source/alias.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any 4 | 5 | from dishka.entities.component import Component 6 | from dishka.entities.factory_type import FactoryType 7 | from dishka.entities.key import DependencyKey 8 | from dishka.entities.scope import BaseScope 9 | from .factory import Factory 10 | 11 | 12 | def _identity(x: Any) -> Any: 13 | return x 14 | 15 | 16 | class Alias: 17 | __slots__ = ("cache", "component", "override", "provides", "source") 18 | 19 | def __init__( 20 | self, *, 21 | source: DependencyKey, 22 | provides: DependencyKey, 23 | cache: bool, 24 | override: bool, 25 | ) -> None: 26 | self.source = source 27 | self.provides = provides 28 | self.cache = cache 29 | self.override = override 30 | 31 | def as_factory( 32 | self, scope: BaseScope | None, component: Component | None, 33 | ) -> Factory: 34 | return Factory( 35 | scope=scope, 36 | source=_identity, 37 | provides=self.provides.with_component(component), 38 | is_to_bind=False, 39 | dependencies=[self.source.with_component(component)], 40 | kw_dependencies={}, 41 | type_=FactoryType.ALIAS, 42 | cache=self.cache, 43 | override=self.override, 44 | ) 45 | 46 | def __get__(self, instance: Any, owner: Any) -> Alias: 47 | return self 48 | -------------------------------------------------------------------------------- /examples/real_world/tests/test_web.py: -------------------------------------------------------------------------------- 1 | """ 2 | In this test example we mock our interactors to check if web view 3 | works correctly. Other option was to only adapters or just use real providers. 4 | 5 | We use `fastapi.testclient.TestClient` to send requests to the app 6 | Additionally we need `asgi_lifespan.LifespanManager` to correctly enter 7 | app scope as it is done in real application 8 | """ 9 | from unittest.mock import Mock 10 | 11 | import pytest 12 | import pytest_asyncio 13 | from asgi_lifespan import LifespanManager 14 | from dishka import Provider, Scope, make_async_container, provide 15 | from dishka.integrations.fastapi import setup_dishka 16 | from fastapi.testclient import TestClient 17 | from main_web import create_fastapi_app 18 | from myapp.use_cases import AddProductsInteractor 19 | 20 | 21 | class FakeInteractorProvider(Provider): 22 | @provide(scope=Scope.REQUEST) 23 | def add_products(self) -> AddProductsInteractor: 24 | return Mock() 25 | 26 | 27 | @pytest.fixture 28 | def interactor_provider(): 29 | return FakeInteractorProvider() 30 | 31 | 32 | @pytest_asyncio.fixture 33 | async def client(interactor_provider): 34 | container = make_async_container(interactor_provider) 35 | app = create_fastapi_app() 36 | setup_dishka(container, app) 37 | async with LifespanManager(app): 38 | yield TestClient(app) 39 | 40 | 41 | @pytest.mark.asyncio 42 | async def test_index(client): 43 | res = client.get("/") 44 | assert res.status_code == 200 45 | assert res.json() == "Ok" 46 | -------------------------------------------------------------------------------- /.mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | files = src/dishka 3 | exclude = ^src/dishka/(_adaptix|integrations)/ 4 | 5 | strict = true 6 | strict_bytes = true 7 | local_partial_types = true 8 | 9 | [mypy-dishka._adaptix.*] 10 | disable_error_code = 11 | attr-defined, 12 | no-untyped-def, 13 | no-any-return, 14 | type-arg, 15 | no-untyped-call 16 | 17 | [mypy-dishka._adaptix.type_tools.normalize_type] 18 | disable_error_code = arg-type 19 | 20 | [mypy-pydantic.*] 21 | ignore_missing_imports = True 22 | 23 | [mypy-aiogram.*] 24 | ignore_missing_imports = True 25 | 26 | [mypy-aiogram_dialog.*] 27 | ignore_missing_imports = True 28 | 29 | [mypy-aiohttp.*] 30 | ignore_missing_imports = True 31 | 32 | [mypy-arq.*] 33 | ignore_missing_imports = True 34 | 35 | [mypy-asgi.*] 36 | ignore_missing_imports = True 37 | 38 | [mypy-click.*] 39 | ignore_missing_imports = True 40 | 41 | [mypy-fastapi.*] 42 | ignore_missing_imports = True 43 | 44 | [mypy-faststream.*] 45 | ignore_missing_imports = True 46 | 47 | [mypy-flask.*] 48 | ignore_missing_imports = True 49 | 50 | [mypy-grpc.*] 51 | ignore_missing_imports = True 52 | 53 | [mypy-grpcio.*] 54 | ignore_missing_imports = True 55 | 56 | [mypy-google.*] 57 | ignore_missing_imports = True 58 | 59 | [mypy-litestar.*] 60 | ignore_missing_imports = True 61 | 62 | [mypy-sanic.*] 63 | ignore_missing_imports = True 64 | 65 | [mypy-sanic_routing.*] 66 | ignore_missing_imports = True 67 | 68 | [mypy-starlette.*] 69 | ignore_missing_imports = True 70 | 71 | [mypy-taskiq.*] 72 | ignore_missing_imports = True 73 | 74 | [mypy-telebot.*] 75 | ignore_missing_imports = True 76 | -------------------------------------------------------------------------------- /src/dishka/entities/key.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Annotated, Any, NamedTuple, get_args, get_origin 4 | 5 | from .component import DEFAULT_COMPONENT, Component 6 | 7 | 8 | class _FromComponent(NamedTuple): 9 | component: Component 10 | 11 | 12 | def FromComponent( # noqa: N802 13 | component: Component = DEFAULT_COMPONENT, 14 | ) -> _FromComponent: 15 | return _FromComponent(component) 16 | 17 | 18 | class DependencyKey(NamedTuple): 19 | type_hint: Any 20 | component: Component | None 21 | 22 | def with_component(self, component: Component | None) -> DependencyKey: 23 | if self.component is not None: 24 | return self 25 | return DependencyKey( 26 | type_hint=self.type_hint, 27 | component=component, 28 | ) 29 | 30 | def __str__(self) -> str: 31 | return f"({self.type_hint}, component={self.component!r})" 32 | 33 | 34 | def dependency_key_to_hint(key: DependencyKey) -> Any: 35 | if key.component is None: 36 | return key.type_hint 37 | return Annotated[key.type_hint, FromComponent(key.component)] 38 | 39 | 40 | def hint_to_dependency_key(hint: Any) -> DependencyKey: 41 | if get_origin(hint) is not Annotated: 42 | return DependencyKey(hint, None) 43 | args = get_args(hint) 44 | from_component = next( 45 | (arg for arg in args if isinstance(arg, _FromComponent)), 46 | None, 47 | ) 48 | if from_component is None: 49 | return DependencyKey(args[0], None) 50 | return DependencyKey(args[0], from_component.component) 51 | -------------------------------------------------------------------------------- /examples/integrations/arq/run_with_worker.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | from typing import Any, Protocol 4 | 5 | from arq.worker import create_worker 6 | from dishka import FromDishka, Provider, Scope, make_async_container, provide 7 | from dishka.integrations.arq import inject, setup_dishka 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class Gateway(Protocol): 13 | async def get(self) -> int: ... 14 | 15 | 16 | class MockGateway(Gateway): 17 | async def get(self) -> int: 18 | return hash(self) 19 | 20 | 21 | class GatewayProvider(Provider): 22 | get_gateway = provide(MockGateway, scope=Scope.REQUEST, provides=Gateway) 23 | 24 | 25 | @inject 26 | async def get_content( 27 | context: dict[Any, Any], 28 | gateway: FromDishka[Gateway], 29 | ): 30 | result = await gateway.get() 31 | logger.info(result) 32 | 33 | 34 | class WorkerSettings: 35 | functions = [get_content] # noqa: RUF012 36 | 37 | 38 | async def main(): 39 | logging.basicConfig( 40 | level=logging.DEBUG, 41 | format="%(asctime)s %(process)-7s %(module)-20s %(message)s", 42 | ) 43 | 44 | worker = create_worker(WorkerSettings) 45 | 46 | container = make_async_container(GatewayProvider()) 47 | setup_dishka(container=container, worker_settings=worker) 48 | 49 | try: 50 | await worker.async_run() 51 | finally: 52 | await container.close() 53 | await worker.close() 54 | 55 | 56 | if __name__ == "__main__": 57 | try: 58 | asyncio.run(main()) 59 | except asyncio.CancelledError: # happens on shutdown, fine 60 | pass 61 | -------------------------------------------------------------------------------- /src/dishka/_adaptix/type_tools/fundamentals.py: -------------------------------------------------------------------------------- 1 | import types 2 | from typing import TypeVar, get_args, get_origin, get_type_hints 3 | 4 | from ..common import TypeHint, VarTuple 5 | from ..feature_requirement import HAS_SUPPORTED_PYDANTIC_PKG 6 | 7 | __all__ = ("is_pydantic_class", "strip_alias", "get_type_vars", "get_generic_args", "get_all_type_hints") 8 | 9 | 10 | if HAS_SUPPORTED_PYDANTIC_PKG: 11 | from pydantic import BaseModel 12 | 13 | _PYDANTIC_MCS = type(types.new_class("_PydanticSample", (BaseModel,), {})) 14 | 15 | def is_pydantic_class(tp) -> bool: 16 | return isinstance(tp, _PYDANTIC_MCS) and tp != BaseModel 17 | else: 18 | def is_pydantic_class(tp) -> bool: 19 | return False 20 | 21 | 22 | def strip_alias(tp: TypeHint) -> TypeHint: 23 | origin = tp.__pydantic_generic_metadata__["origin"] if is_pydantic_class(tp) else get_origin(tp) 24 | return tp if origin is None else origin 25 | 26 | 27 | def get_type_vars(tp: TypeHint) -> VarTuple[TypeVar]: 28 | if is_pydantic_class(tp): 29 | return tp.__pydantic_generic_metadata__["parameters"] 30 | 31 | type_vars = getattr(tp, "__parameters__", ()) 32 | # UnionType object contains descriptor inside `__parameters__` 33 | if not isinstance(type_vars, tuple): 34 | return () 35 | return type_vars 36 | 37 | 38 | def get_generic_args(tp: TypeHint) -> VarTuple[TypeHint]: 39 | if is_pydantic_class(tp): 40 | return tp.__pydantic_generic_metadata__["args"] 41 | return get_args(tp) 42 | 43 | 44 | def get_all_type_hints(obj, globalns=None, localns=None): 45 | return get_type_hints(obj, globalns, localns, include_extras=True) 46 | -------------------------------------------------------------------------------- /docs/integrations/click.rst: -------------------------------------------------------------------------------- 1 | .. _click: 2 | 3 | Click 4 | ================================= 5 | 6 | 7 | Though it is not required, you can use *dishka-click* integration. It features automatic injection to command handlers. 8 | In contrast with other integrations there is no scope management. 9 | 10 | 11 | 12 | How to use 13 | **************** 14 | 15 | 1. Import 16 | 17 | .. code-block:: python 18 | 19 | from dishka import make_container 20 | from dishka.integrations.click import setup_dishka, inject 21 | 22 | 2. Create container in group handler and setup it to click context. Pass ``auto_inject=True`` unless you want to use ``@inject`` decorator explicitly 23 | 24 | .. code-block:: python 25 | 26 | @click.group() 27 | @click.pass_context 28 | def main(context: click.Context): 29 | container = make_container(MyProvider()) 30 | setup_dishka(container=container, context=context, auto_inject=True) 31 | 32 | Or pass your own inject decorator 33 | 34 | .. code-block:: python 35 | 36 | @click.group() 37 | @click.pass_context 38 | def main(context: click.Context): 39 | container = make_container(MyProvider()) 40 | setup_dishka(container=container, context=context, auto_inject=my_inject) 41 | 42 | 3. Mark those of your command handlers parameters which are to be injected with ``FromDishka[]`` 43 | 44 | .. code-block:: python 45 | 46 | @main.command(name="hello") 47 | def hello(interactor: FromDishka[Interactor]): 48 | ... 49 | 50 | 3a. *(optional)* decorate them using ``@inject`` 51 | 52 | .. code-block:: python 53 | 54 | @main.command(name="hello") 55 | @inject 56 | def hello(interactor: FromDishka[Interactor]): 57 | ... -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package to PyPI when a new Release is Created 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | env: 8 | UV_PYTHON_DOWNLOADS: 0 9 | 10 | jobs: 11 | build: 12 | name: Build distribution 13 | runs-on: ubuntu-latest 14 | permissions: {} 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 18 | with: 19 | persist-credentials: false 20 | 21 | - name: Set up Python 3.13 22 | uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 23 | with: 24 | python-version: "3.13" 25 | 26 | - name: Install uv 27 | uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 28 | 29 | - name: Build 30 | run: uv build 31 | 32 | - name: Upload artifact 33 | uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 34 | with: 35 | name: python-package-distributions 36 | path: dist/* 37 | 38 | publish: 39 | name: Publish to PyPI 40 | needs: build 41 | runs-on: ubuntu-latest 42 | environment: 43 | name: pypi 44 | permissions: 45 | id-token: write 46 | contents: read 47 | 48 | steps: 49 | - name: Download artifact 50 | uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 51 | with: 52 | name: python-package-distributions 53 | path: dist/ 54 | 55 | - name: Install uv 56 | uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 57 | 58 | - name: Publish 59 | run: uv publish -------------------------------------------------------------------------------- /examples/integrations/click_app/sync_command.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from typing import Protocol 3 | 4 | import click 5 | from dishka import FromDishka, Provider, Scope, make_container, provide 6 | from dishka.integrations.click import setup_dishka 7 | 8 | 9 | class DbGateway(Protocol): 10 | @abstractmethod 11 | def get(self) -> str: 12 | raise NotImplementedError 13 | 14 | 15 | class FakeDbGateway(DbGateway): 16 | def get(self) -> str: 17 | return "Hello123" 18 | 19 | 20 | class Interactor: 21 | def __init__(self, db: DbGateway): 22 | self.db = db 23 | 24 | def __call__(self) -> str: 25 | return self.db.get() 26 | 27 | 28 | class AdaptersProvider(Provider): 29 | @provide(scope=Scope.APP) 30 | def get_db(self) -> DbGateway: 31 | return FakeDbGateway() 32 | 33 | 34 | class InteractorProvider(Provider): 35 | i1 = provide(Interactor, scope=Scope.APP) 36 | 37 | 38 | @click.group() 39 | @click.pass_context 40 | def main(context: click.Context): 41 | container = make_container(AdaptersProvider(), InteractorProvider()) 42 | setup_dishka(container=container, context=context, auto_inject=True) 43 | 44 | 45 | @click.command() 46 | @click.option("--count", default=1, help="Number of greetings.") 47 | @click.option("--name", prompt="Your name", help="The person to greet.") 48 | def hello(count: int, name: str, interactor: FromDishka[Interactor]): 49 | """Simple program that greets NAME for a total of COUNT times.""" 50 | for _ in range(count): 51 | click.echo(f"Hello {name}!") 52 | click.echo(interactor()) 53 | 54 | 55 | main.add_command(hello, name="hello") 56 | 57 | if __name__ == "__main__": 58 | main() 59 | -------------------------------------------------------------------------------- /docs/quickstart_example_full.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | from collections.abc import Iterable 3 | from sqlite3 import Connection 4 | from typing import Protocol 5 | 6 | from dishka import Provider, Scope, make_container, provide 7 | 8 | 9 | class DAO(Protocol): ... 10 | 11 | 12 | class Service: 13 | def __init__(self, dao: DAO): ... 14 | 15 | 16 | class DAOImpl(DAO): 17 | def __init__(self, connection: Connection): ... 18 | 19 | 20 | class SomeClient: ... 21 | 22 | 23 | service_provider = Provider(scope=Scope.REQUEST) 24 | service_provider.provide(Service) 25 | service_provider.provide(DAOImpl, provides=DAO) 26 | service_provider.provide( 27 | SomeClient, 28 | scope=Scope.APP, 29 | ) # override provider scope 30 | 31 | 32 | class ConnectionProvider(Provider): 33 | @provide(scope=Scope.REQUEST) 34 | def new_connection(self) -> Iterable[Connection]: 35 | conn = sqlite3.connect(":memory:") 36 | yield conn 37 | conn.close() 38 | 39 | 40 | container = make_container(service_provider, ConnectionProvider()) 41 | 42 | client = container.get( 43 | SomeClient, 44 | ) # `SomeClient` has Scope.APP, so it is accessible here 45 | client = container.get(SomeClient) # same instance of `SomeClient` 46 | 47 | # subcontainer to access shorter-living objects 48 | with container() as request_container: 49 | service = request_container.get(Service) 50 | service = request_container.get(Service) # same service instance 51 | # since we exited the context manager, the connection is now closed 52 | 53 | # new subcontainer to have a new lifespan for request processing 54 | with container() as request_container: 55 | service = request_container.get(Service) # new service instance 56 | 57 | container.close() 58 | -------------------------------------------------------------------------------- /examples/integrations/flask_app.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from typing import Protocol 3 | 4 | from dishka import ( 5 | Provider, 6 | Scope, 7 | make_container, 8 | provide, 9 | ) 10 | from dishka.integrations.flask import ( 11 | FlaskProvider, 12 | FromDishka, 13 | inject, 14 | setup_dishka, 15 | ) 16 | from flask import Flask 17 | 18 | 19 | # app core 20 | class DbGateway(Protocol): 21 | @abstractmethod 22 | def get(self) -> str: 23 | raise NotImplementedError 24 | 25 | 26 | class FakeDbGateway(DbGateway): 27 | def get(self) -> str: 28 | return "Hello" 29 | 30 | 31 | class Interactor: 32 | def __init__(self, db: DbGateway): 33 | self.db = db 34 | 35 | def __call__(self) -> str: 36 | return self.db.get() 37 | 38 | 39 | # app dependency logic 40 | class AdaptersProvider(Provider): 41 | @provide(scope=Scope.REQUEST) 42 | def get_db(self) -> DbGateway: 43 | return FakeDbGateway() 44 | 45 | 46 | class InteractorProvider(Provider): 47 | i1 = provide(Interactor, scope=Scope.REQUEST) 48 | 49 | 50 | # presentation layer 51 | app = Flask(__name__) 52 | 53 | 54 | @app.get("/") 55 | @inject 56 | def index( 57 | *, 58 | interactor: FromDishka[Interactor], 59 | ) -> str: 60 | return interactor() 61 | 62 | 63 | @app.get("/auto") 64 | def auto( 65 | *, 66 | interactor: FromDishka[Interactor], 67 | ) -> str: 68 | return interactor() 69 | 70 | 71 | container = make_container( 72 | AdaptersProvider(), 73 | InteractorProvider(), 74 | FlaskProvider(), 75 | ) 76 | setup_dishka(container=container, app=app, auto_inject=True) 77 | try: 78 | app.run() 79 | finally: 80 | container.close() 81 | -------------------------------------------------------------------------------- /examples/real_world/tests/test_add_products.py: -------------------------------------------------------------------------------- 1 | """ 2 | In this test file we use DIshka to provide mocked adapters 3 | though it is not necessary as our interactor is not bound to library 4 | """ 5 | 6 | from unittest.mock import Mock 7 | 8 | import pytest 9 | from dishka import ( 10 | Provider, 11 | Scope, 12 | make_container, 13 | provide, 14 | ) 15 | from myapp.ioc import InteractorProvider 16 | from myapp.use_cases import ( 17 | AddProductsInteractor, 18 | Committer, 19 | ProductGateway, 20 | User, 21 | UserGateway, 22 | WarehouseClient, 23 | ) 24 | 25 | 26 | # app dependency logic 27 | class AdaptersProvider(Provider): 28 | scope = Scope.APP 29 | 30 | @provide 31 | def users(self) -> UserGateway: 32 | gateway = Mock() 33 | gateway.get_user = Mock(return_value=User()) 34 | return gateway 35 | 36 | @provide 37 | def products(self) -> ProductGateway: 38 | gateway = Mock() 39 | gateway.add_product = Mock() 40 | return gateway 41 | 42 | @provide 43 | def committer(self) -> Committer: 44 | committer = Mock() 45 | committer.commit = Mock() 46 | return committer 47 | 48 | @provide 49 | def warehouse(self) -> WarehouseClient: 50 | warehouse = Mock() 51 | warehouse.next_product = Mock(return_value=["a", "b"]) 52 | return warehouse 53 | 54 | 55 | @pytest.fixture 56 | def container(): 57 | c = make_container(AdaptersProvider(), InteractorProvider()) 58 | with c() as request_c: 59 | yield request_c 60 | 61 | 62 | def test_interactor(container): 63 | interactor = container.get(AddProductsInteractor) 64 | interactor(1) 65 | container.get(Committer).commit.assert_called_once_with() 66 | -------------------------------------------------------------------------------- /docs/quickstart_example.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | from typing import Protocol, Iterable 4 | from sqlite3 import Connection 5 | 6 | class DAO(Protocol): 7 | ... 8 | 9 | 10 | class Service: 11 | def __init__(self, dao: DAO): 12 | ... 13 | 14 | 15 | class DAOImpl(DAO): 16 | def __init__(self, connection: Connection): 17 | ... 18 | 19 | 20 | class SomeClient: 21 | ... 22 | 23 | 24 | from dishka import Provider, Scope 25 | 26 | 27 | service_provider = Provider(scope=Scope.REQUEST) 28 | service_provider.provide(Service) 29 | service_provider.provide(DAOImpl, provides=DAO) 30 | service_provider.provide(SomeClient, scope=Scope.APP) # override provider scope 31 | 32 | 33 | from dishka import Provider, provide, Scope 34 | 35 | 36 | class ConnectionProvider(Provider): 37 | @provide(scope=Scope.REQUEST) 38 | def new_connection(self) -> Iterable[Connection]: 39 | conn = sqlite3.connect(":memory:") 40 | yield conn 41 | conn.close() 42 | 43 | 44 | from dishka import make_container 45 | 46 | 47 | container = make_container(service_provider, ConnectionProvider()) 48 | 49 | client = container.get(SomeClient) # `SomeClient` has Scope.APP, so it is accessible here 50 | client = container.get(SomeClient) # same instance of `SomeClient` 51 | 52 | # subcontainer to access shorter-living objects 53 | with container() as request_container: 54 | service = request_container.get(Service) 55 | service = request_container.get(Service) # same service instance 56 | # since we exited the context manager, the connection is now closed 57 | 58 | # new subcontainer to have a new lifespan for request processing 59 | with container() as request_container: 60 | service = request_container.get(Service) # new service instance 61 | 62 | container.close() 63 | -------------------------------------------------------------------------------- /examples/integrations/sanic_app.py: -------------------------------------------------------------------------------- 1 | from typing import Protocol 2 | 3 | from dishka import Provider, Scope, make_async_container, provide 4 | from dishka.integrations.sanic import ( 5 | FromDishka, 6 | SanicProvider, 7 | inject, 8 | setup_dishka, 9 | ) 10 | from sanic import Blueprint, HTTPResponse, Request, Sanic 11 | 12 | 13 | class DbGateway(Protocol): 14 | def get(self) -> str: 15 | raise NotImplementedError 16 | 17 | 18 | class FakeDbGateway(DbGateway): 19 | def get(self) -> str: 20 | return "Hello, world!" 21 | 22 | 23 | class Interactor: 24 | def __init__(self, gateway: DbGateway) -> None: 25 | self.gateway = gateway 26 | 27 | def __call__(self) -> str: 28 | return self.gateway.get() 29 | 30 | 31 | class AdaptersProvider(Provider): 32 | @provide(scope=Scope.REQUEST) 33 | def get_db_gateway(self) -> DbGateway: 34 | return FakeDbGateway() 35 | 36 | 37 | class InteractorsProvider(Provider): 38 | interactor = provide(Interactor, scope=Scope.REQUEST) 39 | 40 | 41 | bp = Blueprint("example") 42 | 43 | 44 | @bp.get("/") 45 | @inject 46 | async def index( 47 | _: Request, 48 | interactor: FromDishka[Interactor], 49 | ) -> HTTPResponse: 50 | return HTTPResponse(interactor()) 51 | 52 | 53 | @bp.get("/auto") 54 | async def auto( 55 | _: Request, 56 | interactor: FromDishka[Interactor], 57 | ) -> HTTPResponse: 58 | return HTTPResponse(interactor()) 59 | 60 | 61 | if __name__ == "__main__": 62 | app = Sanic(__name__) 63 | 64 | app.blueprint(bp) 65 | container = make_async_container( 66 | AdaptersProvider(), InteractorsProvider(), SanicProvider(), 67 | ) 68 | 69 | setup_dishka(container, app, auto_inject=True) 70 | 71 | app.run(host="127.0.0.1", port=8002, single_process=True) 72 | -------------------------------------------------------------------------------- /docs/provider/alias.rst: -------------------------------------------------------------------------------- 1 | .. _alias: 2 | 3 | alias 4 | **************** 5 | 6 | ``alias`` is used to allow retrieving of the same object by different type hints. E.g. you have configured how to provide ``A`` object and want to use it as AProtocol: ``container.get(A)==container.get(AProtocol)``. 7 | 8 | Provider object has also a ``.alias`` method with the same logic. 9 | 10 | .. code-block:: python 11 | 12 | from dishka import alias, provide, Provider, Scope 13 | 14 | class UserDAO(Protocol): ... 15 | class UserDAOImpl(UserDAO): ... 16 | 17 | class MyProvider(Provider): 18 | user_dao = provide(UserDAOImpl, scope=Scope.REQUEST) 19 | user_dao_proto = alias(source=UserDAOImpl, provides=UserDAO) 20 | 21 | Additionally, alias has own setting for caching: it caches by default regardless if source is cached. You can disable it providing ``cache=False`` argument. 22 | 23 | Do you want to override the alias? To do this, specify the parameter ``override=True``. This can be checked when passing proper ``validation_settings`` when creating container. 24 | 25 | .. code-block:: python 26 | 27 | from dishka import provide, Provider, Scope, alias, make_container 28 | 29 | class UserDAO(Protocol): ... 30 | class UserDAOImpl(UserDAO): ... 31 | class UserDAOMock(UserDAO): ... 32 | 33 | class MyProvider(Provider): 34 | scope = Scope.APP # should be REQUEST, but set to APP for the sake of simplicity 35 | 36 | user_dao = provide(UserDAOImpl) 37 | user_dao_mock = provide(UserDAOMock) 38 | 39 | user_dao_proto = alias(UserDAOImpl, provides=UserDAO) 40 | user_dao_override = alias( 41 | UserDAOMock, provides=UserDAO, override=True 42 | ) 43 | 44 | container = make_container(MyProvider()) 45 | dao = container.get(UserDAO) # UserDAOMock 46 | -------------------------------------------------------------------------------- /tests/unit/test_registry.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from dishka.dependency_source.factory import Factory 4 | from dishka.entities.component import DEFAULT_COMPONENT 5 | from dishka.entities.key import DependencyKey 6 | from dishka.entities.scope import Scope 7 | from dishka.provider.make_factory import make_factory 8 | from dishka.registry import Registry 9 | 10 | 11 | class Abstract: ... 12 | 13 | 14 | class Provided(Abstract): ... 15 | 16 | 17 | class Concrete(Provided): ... 18 | 19 | 20 | @pytest.fixture 21 | def factory() -> Factory: 22 | return make_factory( 23 | provides=Provided, 24 | scope=Scope.APP, 25 | source=Provided, 26 | cache=True, 27 | is_in_class=False, 28 | override=False, 29 | ) 30 | 31 | 32 | @pytest.fixture 33 | def registry(factory: Factory) -> Registry: 34 | registry = Registry(scope=Scope.APP, has_fallback=True) 35 | 36 | privede_key = DependencyKey(Provided, DEFAULT_COMPONENT) 37 | registry.add_factory(factory, privede_key) 38 | 39 | return registry 40 | 41 | 42 | def test_get_abstract_factories(registry: Registry, factory: Factory) -> None: 43 | result = registry.get_more_abstract_factories( 44 | DependencyKey(Concrete, DEFAULT_COMPONENT), 45 | ) 46 | 47 | assert result == [factory] 48 | 49 | 50 | def test_get_concrete_factories(registry: Registry, factory: Factory) -> None: 51 | result = registry.get_more_concrete_factories( 52 | DependencyKey(Abstract, DEFAULT_COMPONENT), 53 | ) 54 | 55 | assert result == [factory] 56 | 57 | 58 | def test_get_more_concrete_factories_return_empty_for_object( 59 | registry: Registry, 60 | ) -> None: 61 | result = registry.get_more_concrete_factories( 62 | DependencyKey(object, DEFAULT_COMPONENT), 63 | ) 64 | 65 | assert result == [] 66 | -------------------------------------------------------------------------------- /src/dishka/text_rendering/name.py: -------------------------------------------------------------------------------- 1 | from typing import Any, get_args, get_origin 2 | 3 | from dishka.entities.factory_type import FactoryData, FactoryType 4 | from dishka.entities.key import DependencyKey 5 | 6 | 7 | def _render_args(hint: Any) -> str: 8 | args = get_args(hint) 9 | return ", ".join( 10 | get_name(arg, include_module=False) 11 | for arg in args 12 | ) 13 | 14 | 15 | def get_name(hint: Any, *, include_module: bool) -> str: 16 | if isinstance(hint, list): 17 | res = ",".join( 18 | get_name(item, include_module=include_module) 19 | for item in hint 20 | ) 21 | return f"[{res}]" 22 | if hint is ...: 23 | return "..." 24 | if func := getattr(object, "__func__", None): 25 | return get_name(func, include_module=include_module) 26 | 27 | if include_module: 28 | module = getattr(hint, "__module__", "") 29 | if module == "builtins": 30 | module = "" 31 | elif module: 32 | module += "." 33 | else: 34 | module = "" 35 | 36 | name = ( 37 | getattr(hint, "__qualname__", None) or 38 | getattr(hint, "__name__", None) 39 | ) 40 | if name: 41 | if get_origin(hint): 42 | args = f"[{_render_args(hint)}]" 43 | else: 44 | args = "" 45 | return f"{module}{name}{args}" 46 | return str(hint) 47 | 48 | 49 | def get_source_name(factory: FactoryData) -> str: 50 | source = factory.source 51 | if source == factory.provides.type_hint: 52 | return "" 53 | if factory.type is FactoryType.ALIAS: 54 | return "alias" 55 | 56 | return get_name(source, include_module=False) 57 | 58 | 59 | def get_key_name(key: DependencyKey) -> str: 60 | return get_name(key.type_hint, include_module=True) 61 | -------------------------------------------------------------------------------- /examples/integrations/grpcio/pb2/service_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: proto/service.proto 4 | # Protobuf Python Version: 5.26.1 5 | """Generated protocol buffer code.""" 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import descriptor_pool as _descriptor_pool 8 | from google.protobuf import symbol_database as _symbol_database 9 | from google.protobuf.internal import builder as _builder 10 | # @@protoc_insertion_point(imports) 11 | 12 | _sym_db = _symbol_database.Default() 13 | 14 | 15 | 16 | 17 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13proto/service.proto\x12\x07\x65xample\"!\n\x0eRequestMessage\x12\x0f\n\x07message\x18\x01 \x01(\t\"\"\n\x0fResponseMessage\x12\x0f\n\x07message\x18\x01 \x01(\t2\xa0\x02\n\x0e\x45xampleService\x12?\n\nUnaryUnary\x12\x17.example.RequestMessage\x1a\x18.example.ResponseMessage\x12\x42\n\x0bUnaryStream\x12\x17.example.RequestMessage\x1a\x18.example.ResponseMessage0\x01\x12\x42\n\x0bStreamUnary\x12\x17.example.RequestMessage\x1a\x18.example.ResponseMessage(\x01\x12\x45\n\x0cStreamStream\x12\x17.example.RequestMessage\x1a\x18.example.ResponseMessage(\x01\x30\x01\x62\x06proto3') 18 | 19 | _globals = globals() 20 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) 21 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'proto.service_pb2', _globals) 22 | if not _descriptor._USE_C_DESCRIPTORS: 23 | DESCRIPTOR._loaded_options = None 24 | _globals['_REQUESTMESSAGE']._serialized_start=32 25 | _globals['_REQUESTMESSAGE']._serialized_end=65 26 | _globals['_RESPONSEMESSAGE']._serialized_start=67 27 | _globals['_RESPONSEMESSAGE']._serialized_end=101 28 | _globals['_EXAMPLESERVICE']._serialized_start=104 29 | _globals['_EXAMPLESERVICE']._serialized_end=392 30 | # @@protoc_insertion_point(module_scope) 31 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools==80.9.0", 4 | "setuptools-scm[simple]==9.2.0", 5 | ] 6 | build-backend = "setuptools.build_meta" 7 | 8 | [tool.setuptools.packages.find] 9 | where = ["src"] 10 | 11 | [project] 12 | name = "dishka" 13 | dynamic = ["version"] 14 | readme = "README.md" 15 | authors = [ 16 | { name = "Andrey Tikhonov", email = "17@itishka.org" }, 17 | ] 18 | license = "Apache-2.0" 19 | license-files = ["LICENSE"] 20 | description = "Cute DI framework with scopes and agreeable API" 21 | requires-python = ">=3.10" 22 | classifiers = [ 23 | "Programming Language :: Python :: 3.10", 24 | "Programming Language :: Python :: 3.11", 25 | "Programming Language :: Python :: 3.12", 26 | "Programming Language :: Python :: 3.13", 27 | "Programming Language :: Python :: 3.14", 28 | "Topic :: Software Development :: Libraries", 29 | "Typing :: Typed", 30 | "Intended Audience :: Developers", 31 | "Operating System :: OS Independent", 32 | ] 33 | dependencies = [ 34 | 'exceptiongroup>=1.1.3; python_version<"3.11"', 35 | ] 36 | 37 | [project.urls] 38 | "Source" = "https://github.com/reagento/dishka" 39 | "Homepage" = "https://github.com/reagento/dishka" 40 | "Documentation" = "https://dishka.readthedocs.io/en/stable/" 41 | "Bug Tracker" = "https://github.com/reagento/dishka/issues" 42 | 43 | [tool.setuptools_scm] 44 | version_file = "src/dishka/_version.py" 45 | version_file_template = """\ 46 | # This file auto generated! 47 | # DO NOT EDIT MANUALLY! 48 | 49 | version: str = "{version}" 50 | __version__: str = "{version}" 51 | 52 | commit_hash: str = "{scm_version.node}" 53 | version_timestamp: str = "{scm_version.time}" 54 | 55 | tag: str = "{scm_version.tag}" 56 | branch: str = "{scm_version.branch}" 57 | commit_date: str = "{scm_version.node_date}" 58 | commit_count_since_tag: int = {scm_version.distance} 59 | """ -------------------------------------------------------------------------------- /examples/real_world/myapp/use_cases.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from dataclasses import dataclass 3 | from typing import Protocol 4 | 5 | 6 | class User: 7 | pass 8 | 9 | 10 | @dataclass 11 | class Product: 12 | owner_id: int 13 | name: str 14 | 15 | 16 | class UserNotFoundError(Exception): 17 | pass 18 | 19 | 20 | class UserGateway(Protocol): 21 | @abstractmethod 22 | def get_user(self, user_id: int) -> User: 23 | raise NotImplementedError 24 | 25 | 26 | class ProductGateway(Protocol): 27 | @abstractmethod 28 | def add_product(self, product: Product) -> None: 29 | raise NotImplementedError 30 | 31 | 32 | class Committer(Protocol): 33 | @abstractmethod 34 | def commit(self) -> None: 35 | raise NotImplementedError 36 | 37 | 38 | class WarehouseClient(Protocol): 39 | @abstractmethod 40 | def next_product(self) -> str: 41 | raise NotImplementedError 42 | 43 | 44 | class AddProductsInteractor: 45 | def __init__( 46 | self, 47 | user_gateway: UserGateway, 48 | product_gateway: ProductGateway, 49 | committer: Committer, 50 | warehouse_client: WarehouseClient, 51 | ) -> None: 52 | self.user_gateway = user_gateway 53 | self.product_gateway = product_gateway 54 | self.committer = committer 55 | self.warehouse_client = warehouse_client 56 | 57 | def __call__(self, user_id: int): 58 | user = self.user_gateway.get_user(user_id) 59 | if user is None: 60 | raise UserNotFoundError 61 | 62 | product = Product(user_id, self.warehouse_client.next_product()) 63 | self.product_gateway.add_product(product) 64 | product2 = Product(user_id, self.warehouse_client.next_product()) 65 | self.product_gateway.add_product(product2) 66 | self.committer.commit() 67 | -------------------------------------------------------------------------------- /docs/provider/from_context.rst: -------------------------------------------------------------------------------- 1 | .. _from-context: 2 | 3 | from_context 4 | **************** 5 | 6 | You can put some data manually when entering scope and rely on it in your provider factories. To make it work you need to mark a dependency as retrieved from context using ``from_context`` and then use it as usual. Later, set ``context=`` argument when you enter corresponding scope. 7 | 8 | 9 | .. code-block:: python 10 | 11 | from dishka import from_context, Provider, provide, Scope 12 | 13 | class MyProvider(Provider): 14 | scope = Scope.REQUEST 15 | 16 | app = from_context(provides=App, scope=Scope.APP) 17 | request = from_context(provides=RequestClass) 18 | 19 | @provide 20 | def get_a(self, request: RequestClass, app: App) -> A: 21 | ... 22 | 23 | container = make_container(MyProvider(), context={App: app}) 24 | with container(context={RequestClass: request_instance}) as request_container: 25 | pass 26 | 27 | 28 | Do you want to override factory with ``from_context``? To do this, specify the parameter ``override=True``. This can be checked when passing proper ``validation_settings`` when creating container. 29 | 30 | .. code-block:: python 31 | 32 | from dishka import from_context, Provider, Scope, make_container, provide 33 | 34 | class Config: ... 35 | 36 | class MainProvider(Provider): 37 | scope = Scope.APP 38 | config = provide(Config) 39 | 40 | class TestProvider(Provider): 41 | scope = Scope.APP 42 | config_override = from_context(provides=Config, override=True) 43 | 44 | prod_container = make_container(MainProvider()) 45 | 46 | test_config = Config() 47 | test_container = make_container( 48 | MainProvider(), 49 | TestProvider(), 50 | context={Config: test_config} 51 | ) 52 | assert test_container.get(Config) is test_config # True 53 | -------------------------------------------------------------------------------- /examples/integrations/litestar_app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from abc import abstractmethod 3 | from typing import Protocol 4 | 5 | import uvicorn 6 | from dishka import Provider, Scope, make_async_container, provide 7 | from dishka.integrations.base import FromDishka 8 | from dishka.integrations.litestar import LitestarProvider, inject, setup_dishka 9 | from litestar import Controller, Litestar, get 10 | 11 | 12 | # app core 13 | class DbGateway(Protocol): 14 | @abstractmethod 15 | def get(self) -> str: 16 | raise NotImplementedError 17 | 18 | 19 | class FakeDbGateway(DbGateway): 20 | def get(self) -> str: 21 | return "Hello123" 22 | 23 | 24 | class Interactor: 25 | def __init__(self, db: DbGateway): 26 | self.db = db 27 | 28 | def __call__(self) -> str: 29 | return self.db.get() 30 | 31 | 32 | # app dependency logic 33 | class AdaptersProvider(Provider): 34 | @provide(scope=Scope.REQUEST) 35 | def get_db(self) -> DbGateway: 36 | return FakeDbGateway() 37 | 38 | 39 | class InteractorProvider(Provider): 40 | i1 = provide(Interactor, scope=Scope.REQUEST) 41 | 42 | 43 | class MainController(Controller): 44 | path = "/" 45 | 46 | @get() 47 | @inject 48 | async def index( 49 | self, *, interactor: FromDishka[Interactor], 50 | ) -> str: 51 | return interactor() 52 | 53 | 54 | def create_app(): 55 | logging.basicConfig( 56 | level=logging.WARNING, 57 | format="%(asctime)s %(process)-7s %(module)-20s %(message)s", 58 | ) 59 | app = Litestar(route_handlers=[MainController]) 60 | container = make_async_container( 61 | InteractorProvider(), 62 | AdaptersProvider(), 63 | LitestarProvider(), 64 | ) 65 | setup_dishka(container, app) 66 | return app 67 | 68 | 69 | if __name__ == "__main__": 70 | uvicorn.run(create_app(), host="0.0.0.0", port=8000) 71 | -------------------------------------------------------------------------------- /docs/integrations/telebot.rst: -------------------------------------------------------------------------------- 1 | .. _telebot: 2 | 3 | pyTelegramBotAPI 4 | =========================================== 5 | 6 | Though it is not required, you can use *dishka-pyTelegramBotAPI* integration. It features: 7 | 8 | * automatic *REQUEST* scope management using middleware 9 | * passing ``dishka.integrations.telebot.TelebotEvent`` object as a context data to providers for telegram events (update object fields) 10 | * injection of dependencies into handler function using decorator. 11 | 12 | Only sync handlers are supported. 13 | 14 | How to use 15 | **************** 16 | 17 | 1. Import 18 | 19 | .. code-block:: python 20 | 21 | from dishka.integrations.telebot import ( 22 | FromDishka, 23 | inject, 24 | setup_dishka, 25 | TelebotProvider, 26 | TelebotEvent, 27 | ) 28 | from dishka import make_async_container, Provider, provide, Scope 29 | 30 | 2. Create provider. You can use ``dishka.integrations.telebot.TelebotEvent`` as a factory parameter to access on *REQUEST*-scope 31 | 32 | .. code-block:: python 33 | 34 | class YourProvider(Provider): 35 | @provide(scope=Scope.REQUEST) 36 | def create_x(self, event: TelebotEvent) -> X: 37 | ... 38 | 39 | 40 | 3. Mark those of your handlers parameters which are to be injected with ``FromDishka[]`` and decorate them using ``@inject`` 41 | 42 | .. code-block:: python 43 | 44 | @bot.message() 45 | @inject 46 | def start( 47 | message: Message, 48 | gateway: FromDishka[Gateway], 49 | ): 50 | 51 | 52 | 4. *(optional)* Use ``TelebotProvider()`` when creating container if you are going to use ``dishka.integrations.telebot.TelebotEvent`` in providers 53 | 54 | .. code-block:: python 55 | 56 | container = make_async_container(YourProvider(), TelebotProvider()) 57 | 58 | 59 | 5. Setup ``dishka`` integration. 60 | 61 | .. code-block:: python 62 | 63 | setup_dishka(container=container, bot=bot) 64 | 65 | -------------------------------------------------------------------------------- /examples/integrations/starlette_app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from abc import abstractmethod 3 | from typing import Protocol 4 | 5 | import uvicorn 6 | from dishka import Provider, Scope, make_async_container, provide 7 | from dishka.integrations.starlette import FromDishka, inject, setup_dishka 8 | from starlette.applications import Starlette 9 | from starlette.requests import Request 10 | from starlette.responses import PlainTextResponse 11 | from starlette.routing import Route 12 | 13 | 14 | # app core 15 | class DbGateway(Protocol): 16 | @abstractmethod 17 | def get(self) -> str: 18 | raise NotImplementedError 19 | 20 | 21 | class FakeDbGateway(DbGateway): 22 | def get(self) -> str: 23 | return "Hello from Starlette" 24 | 25 | 26 | class Interactor: 27 | def __init__(self, db: DbGateway): 28 | self.db = db 29 | 30 | def __call__(self) -> str: 31 | return self.db.get() 32 | 33 | 34 | # app dependency logic 35 | class AdaptersProvider(Provider): 36 | @provide(scope=Scope.REQUEST) 37 | def get_db(self) -> DbGateway: 38 | return FakeDbGateway() 39 | 40 | 41 | class InteractorProvider(Provider): 42 | i1 = provide(Interactor, scope=Scope.REQUEST) 43 | 44 | 45 | # presentation layer 46 | @inject 47 | async def index( 48 | request: Request, *, interactor: FromDishka[Interactor], 49 | ) -> PlainTextResponse: 50 | result = interactor() 51 | return PlainTextResponse(result) 52 | 53 | 54 | def create_app(): 55 | logging.basicConfig( 56 | level=logging.WARNING, 57 | format="%(asctime)s %(process)-7s %(module)-20s %(message)s", 58 | ) 59 | 60 | app = Starlette(routes=[Route("/", endpoint=index, methods=["GET"])]) 61 | container = make_async_container(AdaptersProvider(), InteractorProvider()) 62 | setup_dishka(container, app) 63 | return app 64 | 65 | 66 | if __name__ == "__main__": 67 | uvicorn.run(create_app(), host="0.0.0.0", port=8000, lifespan="on") 68 | -------------------------------------------------------------------------------- /examples/sync_simple.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterable 2 | from dataclasses import dataclass 3 | from typing import Protocol 4 | 5 | from dishka import Provider, Scope, alias, make_container, provide 6 | 7 | 8 | @dataclass 9 | class Config: 10 | value: int 11 | 12 | 13 | class Gateway(Protocol): 14 | pass 15 | 16 | 17 | class Connection: 18 | def close(self): 19 | print("Connection closed") 20 | 21 | 22 | class GatewayImplementation(Gateway): 23 | def __init__(self, config: Config, connection: Connection): 24 | self.value = config.value 25 | self.connection = connection 26 | 27 | def __repr__(self): 28 | return f"A(value={self.value}, connection={self.connection})" 29 | 30 | 31 | class MyProvider(Provider): 32 | scope = Scope.REQUEST 33 | 34 | def __init__(self, config: Config): 35 | super().__init__() 36 | self.config = config 37 | 38 | # simple factory with explicit scope 39 | @provide(scope=Scope.APP) 40 | def get_config(self) -> Config: 41 | return self.config 42 | 43 | # object with finalization and provider-defined scope 44 | @provide 45 | def get_conn(self) -> Iterable[Connection]: 46 | connection = Connection() 47 | yield connection 48 | connection.close() 49 | 50 | # object by `__init__` 51 | gw = provide(GatewayImplementation) 52 | # another type for same object 53 | base_gw = alias(source=GatewayImplementation, provides=Gateway) 54 | 55 | 56 | def main(): 57 | config = Config(1) 58 | provider = MyProvider(config) 59 | container = make_container(provider) 60 | 61 | print(container.get(Config)) 62 | with container() as c_request: 63 | print(c_request.get(GatewayImplementation)) 64 | print(c_request.get(Gateway)) 65 | with container() as c_request: 66 | print(c_request.get(Gateway)) 67 | 68 | container.close() 69 | 70 | 71 | if __name__ == "__main__": 72 | main() 73 | -------------------------------------------------------------------------------- /tests/integrations/arq/test_arq.py: -------------------------------------------------------------------------------- 1 | from contextlib import asynccontextmanager 2 | from typing import Any 3 | from unittest.mock import Mock 4 | 5 | import pytest 6 | 7 | from dishka import FromDishka, make_async_container 8 | from dishka.integrations.arq import inject, setup_dishka 9 | from ..common import ( 10 | APP_DEP_VALUE, 11 | REQUEST_DEP_VALUE, 12 | AppDep, 13 | AppProvider, 14 | RequestDep, 15 | ) 16 | 17 | 18 | class WorkerSettings: 19 | pass 20 | 21 | 22 | @asynccontextmanager 23 | async def dishka_app(provider): 24 | container = make_async_container(provider) 25 | setup_dishka(container, worker_settings=WorkerSettings) 26 | yield WorkerSettings 27 | await container.close() 28 | 29 | 30 | @inject 31 | async def get_with_app( 32 | _: dict[str, Any], 33 | a: FromDishka[AppDep], 34 | mock: FromDishka[Mock], 35 | ) -> None: 36 | mock(a) 37 | 38 | 39 | @inject 40 | async def get_with_request( 41 | _: dict[str, Any], 42 | a: FromDishka[RequestDep], 43 | mock: FromDishka[Mock], 44 | ) -> None: 45 | mock(a) 46 | 47 | 48 | @pytest.mark.asyncio 49 | async def test_app_dependency(app_provider: AppProvider): 50 | async with dishka_app(app_provider) as settings: 51 | await settings.on_job_start(settings.ctx) 52 | await get_with_app(settings.ctx) 53 | await settings.on_job_end(settings.ctx) 54 | app_provider.mock.assert_called_with(APP_DEP_VALUE) 55 | app_provider.app_released.assert_not_called() 56 | 57 | app_provider.app_released.assert_called() 58 | 59 | 60 | @pytest.mark.asyncio 61 | async def test_request_dependency(app_provider: AppProvider): 62 | async with dishka_app(app_provider) as settings: 63 | await settings.on_job_start(settings.ctx) 64 | await get_with_request(settings.ctx) 65 | await settings.on_job_end(settings.ctx) 66 | app_provider.mock.assert_called_with(REQUEST_DEP_VALUE) 67 | app_provider.app_released.assert_not_called() 68 | -------------------------------------------------------------------------------- /src/dishka/_adaptix/type_tools/implicit_params.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import typing 3 | from typing import Any, ForwardRef, TypeVar 4 | 5 | from ..common import TypeHint, VarTuple 6 | from ..feature_requirement import HAS_PARAM_SPEC, HAS_TV_TUPLE 7 | from .basic_utils import create_union, eval_forward_ref, is_user_defined_generic, strip_alias 8 | from .constants import BUILTIN_ORIGIN_TO_TYPEVARS 9 | 10 | 11 | class ImplicitParamsGetter: 12 | def _process_limit_element(self, type_var: TypeVar, tp: TypeHint) -> TypeHint: 13 | if isinstance(tp, ForwardRef): 14 | return eval_forward_ref(vars(sys.modules[type_var.__module__]), tp) 15 | return tp 16 | 17 | def _process_type_var(self, type_var) -> TypeHint: 18 | if HAS_PARAM_SPEC and isinstance(type_var, typing.ParamSpec): 19 | return ... 20 | if HAS_TV_TUPLE and isinstance(type_var, typing.TypeVarTuple): 21 | return typing.Unpack[tuple[Any, ...]] 22 | if type_var.__constraints__: 23 | return create_union( 24 | tuple( 25 | self._process_limit_element(type_var, constraint) 26 | for constraint in type_var.__constraints__ 27 | ), 28 | ) 29 | if type_var.__bound__ is None: 30 | return Any 31 | return self._process_limit_element(type_var, type_var.__bound__) 32 | 33 | def get_implicit_params(self, origin) -> VarTuple[TypeHint]: 34 | if is_user_defined_generic(origin): 35 | type_vars = origin.__parameters__ 36 | else: 37 | type_vars = BUILTIN_ORIGIN_TO_TYPEVARS.get(origin, ()) 38 | 39 | return tuple( 40 | self._process_type_var(type_var) 41 | for type_var in type_vars 42 | ) 43 | 44 | 45 | def fill_implicit_params(tp: TypeHint) -> TypeHint: 46 | params = ImplicitParamsGetter().get_implicit_params(strip_alias(tp)) 47 | if params: 48 | return tp[params] 49 | raise ValueError(f"Can not derive implicit parameters for {tp}") 50 | -------------------------------------------------------------------------------- /tests/unit/text_rendering/test_suggestion.py: -------------------------------------------------------------------------------- 1 | from dishka.entities.factory_type import FactoryData, FactoryType 2 | from dishka.entities.key import DependencyKey 3 | from dishka.entities.scope import Scope 4 | from dishka.text_rendering.suggestion import render_suggestions_for_missing 5 | 6 | 7 | def test_suggest_abstract_factories() -> None: 8 | expected = ( 9 | "\n * Try use `AnyOf` " 10 | "or changing the requested dependency to a more abstract. " 11 | "Found factories for more abstract dependencies: (object, int);" 12 | ) 13 | suggest_abstract_factories = [ 14 | FactoryData( 15 | source=int, 16 | provides=DependencyKey(object, ""), 17 | scope=Scope.APP, 18 | type_=FactoryType.FACTORY, 19 | ), 20 | ] 21 | 22 | result = render_suggestions_for_missing( 23 | requested_for=None, 24 | requested_key=DependencyKey(int, ""), 25 | suggest_other_scopes=[], 26 | suggest_other_components=[], 27 | suggest_abstract_factories=suggest_abstract_factories, 28 | suggest_concrete_factories=[], 29 | ) 30 | 31 | assert result == expected 32 | 33 | 34 | def test_suggest_concrete_factories() -> None: 35 | expected = ( 36 | "\n * Try use `WithParents` " 37 | "or changing `provides` to `object`. " 38 | "Found factories for more concrete dependencies: (float, int);" 39 | ) 40 | suggest_concrete_factories = [ 41 | FactoryData( 42 | source=int, 43 | provides=DependencyKey(float, ""), 44 | scope=Scope.APP, 45 | type_=FactoryType.FACTORY, 46 | ), 47 | ] 48 | 49 | result = render_suggestions_for_missing( 50 | requested_for=None, 51 | requested_key=DependencyKey(object, ""), 52 | suggest_other_scopes=[], 53 | suggest_other_components=[], 54 | suggest_abstract_factories=[], 55 | suggest_concrete_factories=suggest_concrete_factories, 56 | ) 57 | 58 | assert result == expected 59 | -------------------------------------------------------------------------------- /examples/integrations/click_app/async_command.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from abc import abstractmethod 3 | from functools import wraps 4 | from typing import Protocol 5 | 6 | import click 7 | from dishka import ( 8 | FromDishka, 9 | Provider, 10 | Scope, 11 | make_container, 12 | provide, 13 | ) 14 | from dishka.integrations.click import setup_dishka 15 | 16 | 17 | class DbGateway(Protocol): 18 | @abstractmethod 19 | async def get(self) -> str: 20 | raise NotImplementedError 21 | 22 | 23 | class FakeDbGateway(DbGateway): 24 | async def get(self) -> str: 25 | await asyncio.sleep(0.1) 26 | return "Hello123" 27 | 28 | 29 | class Interactor: 30 | def __init__(self, db: DbGateway): 31 | self.db = db 32 | 33 | async def __call__(self) -> str: 34 | return await self.db.get() 35 | 36 | 37 | class AdaptersProvider(Provider): 38 | @provide(scope=Scope.APP) 39 | def get_db(self) -> DbGateway: 40 | return FakeDbGateway() 41 | 42 | 43 | class InteractorProvider(Provider): 44 | i1 = provide(Interactor, scope=Scope.APP) 45 | 46 | 47 | def async_command(f): 48 | @wraps(f) 49 | def wrapper(*args, **kwargs): 50 | return asyncio.run(f(*args, **kwargs)) 51 | 52 | return wrapper 53 | 54 | 55 | @click.group() 56 | @click.pass_context 57 | def main(context: click.Context): 58 | container = make_container(AdaptersProvider(), InteractorProvider()) 59 | setup_dishka(container=container, context=context, auto_inject=True) 60 | 61 | 62 | @click.command() 63 | @click.option("--count", default=1, help="Number of greetings.") 64 | @click.option("--name", prompt="Your name", help="The person to greet.") 65 | @async_command 66 | async def hello(count: int, name: str, interactor: FromDishka[Interactor]): 67 | """Simple program that greets NAME for a total of COUNT times.""" 68 | for _ in range(count): 69 | click.echo(f"Hello {name}!") 70 | click.echo(await interactor()) 71 | 72 | 73 | main.add_command(hello, name="hello") 74 | 75 | if __name__ == "__main__": 76 | main() 77 | -------------------------------------------------------------------------------- /src/dishka/integrations/click.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "FromDishka", 3 | "inject", 4 | "setup_dishka", 5 | ] 6 | 7 | from collections.abc import Callable 8 | from typing import Final, TypeVar 9 | 10 | from click import ( 11 | Command, 12 | Context, 13 | Group, 14 | get_current_context, 15 | ) 16 | 17 | from dishka import Container, FromDishka 18 | from .base import InjectFunc, is_dishka_injected, wrap_injection 19 | 20 | T = TypeVar("T") 21 | CONTAINER_NAME: Final = "dishka_container" 22 | 23 | 24 | def inject(func: Callable[..., T]) -> Callable[..., T]: 25 | return wrap_injection( 26 | func=func, 27 | container_getter=lambda _, __: get_current_context().meta[ 28 | CONTAINER_NAME 29 | ], 30 | remove_depends=True, 31 | is_async=False, 32 | ) 33 | 34 | 35 | def _inject_commands( 36 | context: Context, 37 | command: Command | None, 38 | inject_func: InjectFunc[..., T], 39 | ) -> None: 40 | if isinstance(command, Command) and not is_dishka_injected( 41 | command.callback, # type: ignore[arg-type] 42 | ): 43 | command.callback = inject_func(command.callback) # type: ignore[arg-type] 44 | 45 | if isinstance(command, Group): 46 | for command_name in command.list_commands(context): 47 | child_command = command.get_command(context, command_name) 48 | _inject_commands(context, child_command, inject_func) 49 | 50 | 51 | def setup_dishka( 52 | container: Container, 53 | context: Context, 54 | *, 55 | finalize_container: bool = True, 56 | auto_inject: bool | InjectFunc[..., T] = False, 57 | ) -> None: 58 | context.meta[CONTAINER_NAME] = container 59 | 60 | if finalize_container: 61 | context.call_on_close(container.close) 62 | 63 | if auto_inject is not False: 64 | inject_func: InjectFunc[..., T] 65 | 66 | if auto_inject is True: 67 | inject_func = inject 68 | else: 69 | inject_func = auto_inject 70 | 71 | _inject_commands(context, context.command, inject_func) 72 | -------------------------------------------------------------------------------- /examples/async_simple.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from collections.abc import AsyncIterable 3 | from dataclasses import dataclass 4 | from typing import Protocol 5 | 6 | from dishka import Provider, Scope, alias, make_async_container, provide 7 | 8 | 9 | @dataclass 10 | class Config: 11 | value: int 12 | 13 | 14 | class Gateway(Protocol): 15 | pass 16 | 17 | 18 | class Connection: 19 | async def close(self): 20 | print("Connection closed") 21 | 22 | 23 | class GatewayImplementation(Gateway): 24 | def __init__(self, config: Config, connection: Connection): 25 | self.value = config.value 26 | self.connection = connection 27 | 28 | def __repr__(self): 29 | return f"A(value={self.value}, connection={self.connection})" 30 | 31 | 32 | class MyProvider(Provider): 33 | scope = Scope.REQUEST 34 | 35 | def __init__(self, config: Config): 36 | super().__init__() 37 | self.config = config 38 | 39 | # simple factory with explicit scope 40 | @provide(scope=Scope.APP) 41 | def get_config(self) -> Config: 42 | return self.config 43 | 44 | # async factory with object finalization and provider-defined scope 45 | @provide 46 | async def get_conn(self) -> AsyncIterable[Connection]: 47 | connection = Connection() 48 | yield connection 49 | await connection.close() 50 | 51 | # object by `__init__` 52 | gw = provide(GatewayImplementation) 53 | # another type for same object 54 | base_gw = alias(source=GatewayImplementation, provides=Gateway) 55 | 56 | 57 | async def main(): 58 | config = Config(1) 59 | provider = MyProvider(config) 60 | container = make_async_container(provider) 61 | 62 | print(await container.get(Config)) 63 | async with container() as c_request: 64 | print(await c_request.get(GatewayImplementation)) 65 | print(await c_request.get(Gateway)) 66 | async with container() as c_request: 67 | print(await c_request.get(Gateway)) 68 | 69 | await container.close() 70 | 71 | 72 | if __name__ == "__main__": 73 | asyncio.run(main()) 74 | -------------------------------------------------------------------------------- /src/dishka/dependency_source/context_var.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any, NoReturn 4 | 5 | from dishka.entities.component import DEFAULT_COMPONENT, Component 6 | from dishka.entities.factory_type import FactoryType 7 | from dishka.entities.key import DependencyKey 8 | from dishka.entities.scope import BaseScope 9 | from .alias import Alias 10 | from .factory import Factory 11 | 12 | 13 | def context_stub() -> NoReturn: 14 | raise NotImplementedError 15 | 16 | 17 | class ContextVariable: 18 | __slots__ = ("override", "provides", "scope") 19 | 20 | def __init__( 21 | self, *, 22 | provides: DependencyKey, 23 | scope: BaseScope | None, 24 | override: bool, 25 | ) -> None: 26 | self.provides = provides 27 | self.scope = scope 28 | self.override = override 29 | 30 | def as_factory( 31 | self, component: Component, 32 | ) -> Factory: 33 | if component == DEFAULT_COMPONENT: 34 | return Factory( 35 | scope=self.scope, 36 | source=context_stub, 37 | provides=self.provides, 38 | is_to_bind=False, 39 | dependencies=[], 40 | kw_dependencies={}, 41 | type_=FactoryType.CONTEXT, 42 | cache=False, 43 | override=self.override, 44 | ) 45 | else: 46 | aliased = Alias( 47 | source=self.provides.with_component(DEFAULT_COMPONENT), 48 | cache=False, 49 | override=self.override, 50 | provides=DependencyKey( 51 | component=component, 52 | type_hint=self.provides.type_hint, 53 | ), 54 | ) 55 | return aliased.as_factory(scope=self.scope, component=component) 56 | 57 | def __get__(self, instance: Any, owner: Any) -> ContextVariable: 58 | scope = self.scope or instance.scope 59 | return ContextVariable( 60 | scope=scope, 61 | provides=self.provides, 62 | override=self.override, 63 | ) 64 | -------------------------------------------------------------------------------- /docs/provider/provide_all.rst: -------------------------------------------------------------------------------- 1 | .. _provide_all: 2 | 3 | provide_all 4 | ****************** 5 | 6 | ``provide_all`` is a helper function which can be used instead of repeating ``provide`` call with just class passed and same scope. 7 | 8 | These two providers are equal for the container: 9 | 10 | .. code-block:: python 11 | 12 | from dishka import provide, provide_all, Provider, Scope 13 | 14 | class OneByOne(Provider): 15 | scope = Scope.APP 16 | 17 | register_user = provide(RegisterUserInteractor) 18 | update_pfp = provide(UpdateProfilePicInteractor) 19 | 20 | class AllAtOnce(Provider): 21 | scope = Scope.APP 22 | 23 | interactors = provide_all( 24 | RegisterUserInteractor, 25 | UpdateProfilePicInteractor 26 | ) 27 | 28 | 29 | It is also available as a method: 30 | 31 | .. code-block:: python 32 | 33 | provider = Provider(scope=Scope.APP) 34 | provider.provide_all( 35 | RegisterUserInteractor, 36 | UpdateProfilePicInteractor 37 | ) 38 | 39 | 40 | You can combine different ``provide``, ``alias``, ``decorate``, ``from_context``, ``provide_all`` and others with each other without thinking about the variable names for each of them. 41 | 42 | These two providers are equal for the container: 43 | 44 | .. code-block:: python 45 | 46 | from dishka import alias, decorate, provide, provide_all, Provider, Scope 47 | 48 | class OneByOne(Provider): 49 | scope = Scope.APP 50 | 51 | config = from_context(Config) 52 | user_dao = provide(UserDAOImpl, provides=UserDAO) 53 | post_dao = provide(PostDAOImpl, provides=PostDAO) 54 | post_reader = alias(source=PostDAOImpl, provides=PostReader) 55 | decorator = decorate(SomeDecorator, provides=SomeClass) 56 | 57 | class AllAtOnce(Provider): 58 | scope = Scope.APP 59 | 60 | provides = ( 61 | provide(UserDAOImpl, provides=UserDAO) 62 | + provide(PostDAOImpl, provides=PostDAO) 63 | + alias(source=PostDAOImpl, provides=PostReader) 64 | + decorate(SomeDecorator, provides=SomeClass) 65 | + from_context(Config) 66 | ) 67 | -------------------------------------------------------------------------------- /src/dishka/dependency_source/composite.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Sequence 4 | from typing import Any, ClassVar 5 | 6 | from .alias import Alias 7 | from .context_var import ContextVariable 8 | from .decorator import Decorator 9 | from .factory import Factory 10 | 11 | DependencySource = Alias | Factory | Decorator | ContextVariable 12 | 13 | 14 | class CompositeDependencySource: 15 | _instances: ClassVar[int] = 0 16 | 17 | def __init__( 18 | self, 19 | origin: Any, 20 | dependency_sources: Sequence[DependencySource] = (), 21 | number: int | None = None, 22 | ) -> None: 23 | self.dependency_sources = list(dependency_sources) 24 | self.origin = origin 25 | if number is None: 26 | self.number = self._instances 27 | CompositeDependencySource._instances += 1 28 | else: 29 | self.number = number 30 | 31 | def __get__(self, instance: Any, owner: Any) -> CompositeDependencySource: 32 | try: 33 | origin = self.origin.__get__(instance, owner) 34 | except AttributeError: # not a valid descriptor 35 | origin = self.origin 36 | return CompositeDependencySource( 37 | origin=origin, 38 | dependency_sources=[ 39 | s.__get__(instance, owner) for s in self.dependency_sources 40 | ], 41 | number=self.number, 42 | ) 43 | 44 | def __call__(self, *args: Any, **kwargs: Any) -> Any: 45 | return self.origin(*args, **kwargs) 46 | 47 | def __add__( 48 | self, other: CompositeDependencySource, 49 | ) -> CompositeDependencySource: 50 | return CompositeDependencySource( 51 | origin=None, 52 | dependency_sources=( 53 | self.dependency_sources 54 | + other.dependency_sources 55 | ), 56 | ) 57 | 58 | 59 | def ensure_composite(origin: Any) -> CompositeDependencySource: 60 | if isinstance(origin, CompositeDependencySource): 61 | return origin 62 | else: 63 | return CompositeDependencySource(origin) 64 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = "dishka" 21 | copyright = "2022, reagento" 22 | author = "Tishka17" 23 | master_doc = "index" 24 | 25 | # -- General configuration --------------------------------------------------- 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be 28 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 29 | # ones. 30 | extensions = [ 31 | "sphinx_copybutton", 32 | "sphinx.ext.autodoc", 33 | "sphinx_design", 34 | ] 35 | autodoc_type_aliases = { 36 | } 37 | autodoc_typehints = "description" 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ["_templates"] 41 | 42 | # List of patterns, relative to source directory, that match files and 43 | # directories to ignore when looking for source files. 44 | # This pattern also affects html_static_path and html_extra_path. 45 | exclude_patterns = [] 46 | 47 | 48 | # -- Options for HTML output ------------------------------------------------- 49 | 50 | # The theme to use for HTML and HTML Help pages. See the documentation for 51 | # a list of builtin themes. 52 | # 53 | html_theme = "furo" 54 | 55 | 56 | # Add any paths that contain custom static files (such as style sheets) here, 57 | # relative to this directory. They are copied after the builtin static files, 58 | # so a file named "default.css" will overwrite the builtin "default.css". 59 | # html_static_path = ["_static"] 60 | -------------------------------------------------------------------------------- /examples/integrations/aiogram_bot.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import os 4 | import random 5 | from collections.abc import AsyncIterator 6 | 7 | from aiogram import Bot, Dispatcher, Router 8 | from aiogram.types import Chat, Message, TelegramObject, User 9 | from dishka import Provider, Scope, make_async_container, provide 10 | from dishka.integrations.aiogram import ( 11 | AiogramMiddlewareData, 12 | AiogramProvider, 13 | FromDishka, 14 | inject, 15 | setup_dishka, 16 | ) 17 | 18 | 19 | # app dependency logic 20 | class MyProvider(Provider): 21 | @provide(scope=Scope.APP) 22 | async def get_int(self) -> AsyncIterator[int]: 23 | print("solve int") 24 | yield random.randint(0, 10000) 25 | 26 | @provide(scope=Scope.REQUEST) 27 | async def get_user(self, obj: TelegramObject) -> User: 28 | return obj.from_user 29 | 30 | @provide(scope=Scope.REQUEST) 31 | async def get_chat( 32 | self, 33 | middleware_data: AiogramMiddlewareData, 34 | ) -> Chat | None: 35 | return middleware_data.get("event_chat") 36 | 37 | 38 | # app 39 | API_TOKEN = os.getenv("BOT_TOKEN") 40 | router = Router() 41 | 42 | 43 | @router.message() 44 | @inject # if auto_inject=True is specified in the setup_dishka, then you do not need to specify a decorator # noqa: E501 45 | async def start( 46 | message: Message, 47 | user: FromDishka[User], 48 | value: FromDishka[int], 49 | chat: FromDishka[Chat | None], 50 | ): 51 | chat_name = chat.username if chat else None 52 | await message.answer(f"Hello, {value}, {chat_name}, {user.full_name}!") 53 | 54 | 55 | async def main(): 56 | # real main 57 | logging.basicConfig(level=logging.INFO) 58 | bot = Bot(token=API_TOKEN) 59 | dp = Dispatcher() 60 | dp.include_router(router) 61 | 62 | container = make_async_container( 63 | MyProvider(), 64 | AiogramProvider(), 65 | ) 66 | setup_dishka(container=container, router=dp) 67 | try: 68 | await dp.start_polling(bot) 69 | finally: 70 | await container.close() 71 | await bot.session.close() 72 | 73 | 74 | if __name__ == "__main__": 75 | asyncio.run(main()) 76 | -------------------------------------------------------------------------------- /docs/advanced/testing/index.rst: -------------------------------------------------------------------------------- 1 | Testing with dishka 2 | *************************** 3 | 4 | Testing your code does not always require the whole application to be started. You can have unit tests for separate components and even integration tests which check only specific links. In many cases you do not need IoC-container: you create objects with a power of **Dependency Injection** and not framework. 5 | 6 | For other cases which require calling functions located on application boundaries you need a container. These cases include testing your view functions with mocks of business logic and testing the application as a whole. Comparing to a production mode you will still have same implementations for some classes and others will be replaced with mocks. Luckily, in ``dishka`` your container is not an implicit global thing and can be replaced easily. 7 | 8 | There are many options to make providers with mock objects. If you are using ``pytest`` then you can 9 | 10 | * use fixtures to configure mocks and then pass those objects to a provider 11 | * create mocks in a provider and retrieve them in pytest fixtures from a container. 12 | 13 | The main limitation here is that a container itself cannot be adjusted after creation. You can configure providers whenever you want before you make a container. Once it is created dependency graph is build and validated, and all you can do is to provide context data when entering a scope. 14 | 15 | 16 | Example 17 | =================== 18 | 19 | Imagine, you have a service built with ``FastAPI``: 20 | 21 | .. literalinclude:: ./app_before.py 22 | 23 | And a container: 24 | 25 | .. literalinclude:: ./container_before.py 26 | 27 | First of all - split your application factory and container setup. 28 | 29 | .. literalinclude:: ./app_factory.py 30 | 31 | Create a provider with your mock objects. You can still use production providers and override dependencies in a new one. Or you can build container only with new providers. It depends on the structure of your application and type of a test. 32 | 33 | .. literalinclude:: ./fixtures.py 34 | 35 | Write tests. 36 | 37 | .. literalinclude:: ./sometest.py 38 | 39 | 40 | Bringing all together 41 | ============================ 42 | 43 | 44 | .. literalinclude:: ./test_example.py 45 | -------------------------------------------------------------------------------- /src/dishka/integrations/telebot.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "FromDishka", 3 | "TelebotProvider", 4 | "inject", 5 | "setup_dishka", 6 | ] 7 | 8 | from collections.abc import Callable 9 | from inspect import Parameter 10 | from typing import Any, NewType, ParamSpec, TypeVar 11 | 12 | import telebot # type: ignore[import-untyped] 13 | from telebot import BaseMiddleware, TeleBot 14 | 15 | from dishka import Container, FromDishka, Provider, Scope, from_context 16 | from .base import wrap_injection 17 | 18 | CONTAINER_NAME = "dishka_container" 19 | 20 | T = TypeVar("T") 21 | P = ParamSpec("P") 22 | TelebotEvent = NewType("TelebotEvent", object) 23 | 24 | 25 | def inject(func: Callable[P, T]) -> Callable[P, T]: 26 | additional_params = [Parameter( 27 | name=CONTAINER_NAME, 28 | annotation=Container, 29 | kind=Parameter.KEYWORD_ONLY, 30 | )] 31 | 32 | return wrap_injection( 33 | func=func, 34 | additional_params=additional_params, 35 | container_getter=lambda _, p: p[CONTAINER_NAME], 36 | ) 37 | 38 | 39 | class TelebotProvider(Provider): 40 | message = from_context(TelebotEvent, scope=Scope.REQUEST) 41 | 42 | 43 | class ContainerMiddleware(BaseMiddleware): # type: ignore[misc] 44 | update_types = telebot.util.update_types 45 | 46 | def __init__(self, container: Container) -> None: 47 | super().__init__() 48 | self.container = container 49 | 50 | def pre_process( 51 | self, 52 | message: Any, 53 | data: dict[str, Any], 54 | ) -> None: 55 | dishka_container_wrapper = self.container( 56 | {TelebotEvent(type(message)): message}, 57 | ) 58 | data[CONTAINER_NAME + "_wrapper"] = dishka_container_wrapper 59 | data[CONTAINER_NAME] = dishka_container_wrapper.__enter__() 60 | 61 | def post_process( 62 | self, 63 | message: Any, 64 | data: dict[str, Any], 65 | exception: Exception, 66 | ) -> None: 67 | data[CONTAINER_NAME + "_wrapper"].__exit__(None, None, None) 68 | 69 | 70 | def setup_dishka(container: Container, bot: TeleBot) -> Container: 71 | middleware = ContainerMiddleware(container) 72 | bot.setup_middleware(middleware) 73 | return container 74 | -------------------------------------------------------------------------------- /docs/advanced/testing/test_example.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterable 2 | from sqlite3 import Connection, connect 3 | from unittest.mock import Mock 4 | 5 | import pytest 6 | import pytest_asyncio 7 | from fastapi import APIRouter, FastAPI 8 | from fastapi.testclient import TestClient 9 | 10 | from dishka import Provider, Scope, make_async_container, provide 11 | from dishka.integrations.fastapi import FromDishka, inject, setup_dishka 12 | 13 | router = APIRouter() 14 | 15 | 16 | @router.get("/") 17 | @inject 18 | async def index(connection: FromDishka[Connection]) -> str: 19 | connection.execute("select 1") 20 | return "Ok" 21 | 22 | 23 | def create_app() -> FastAPI: 24 | app = FastAPI() 25 | app.include_router(router) 26 | return app 27 | 28 | 29 | class ConnectionProvider(Provider): 30 | def __init__(self, uri): 31 | super().__init__() 32 | self.uri = uri 33 | 34 | @provide(scope=Scope.REQUEST) 35 | def get_connection(self) -> Iterable[Connection]: 36 | conn = connect(self.uri) 37 | yield conn 38 | conn.close() 39 | 40 | 41 | def create_production_app(): 42 | app = create_app() 43 | container = make_async_container(ConnectionProvider("sqlite:///")) 44 | setup_dishka(container, app) 45 | return app 46 | 47 | 48 | class MockConnectionProvider(Provider): 49 | @provide(scope=Scope.APP) 50 | def get_connection(self) -> Connection: 51 | connection = Mock() 52 | connection.execute = Mock(return_value="1") 53 | return connection 54 | 55 | 56 | @pytest_asyncio.fixture 57 | async def container(): 58 | container = make_async_container(MockConnectionProvider()) 59 | yield container 60 | await container.close() 61 | 62 | 63 | @pytest.fixture 64 | def client(container): 65 | app = create_app() 66 | setup_dishka(container, app) 67 | with TestClient(app) as client: 68 | yield client 69 | 70 | 71 | @pytest_asyncio.fixture 72 | async def connection(container): 73 | return await container.get(Connection) 74 | 75 | 76 | @pytest.mark.asyncio 77 | async def test_controller(client: TestClient, connection: Mock): 78 | response = client.get("/") 79 | assert response.status_code == 200 80 | connection.execute.assert_called() 81 | -------------------------------------------------------------------------------- /docs/integrations/celery.rst: -------------------------------------------------------------------------------- 1 | .. _celery: 2 | 3 | Celery 4 | ============================ 5 | 6 | 7 | Though it is not required, you can use *dishka-celery* integration. It features: 8 | 9 | * automatic *REQUEST* scope management using signals 10 | * injection of dependencies into task handler function using decorator 11 | * automatic injection of dependencies into task handler function. 12 | 13 | 14 | How to use 15 | **************** 16 | 17 | 1. Import 18 | 19 | .. code-block:: python 20 | 21 | from dishka.integrations.celery import ( 22 | DishkaTask, 23 | FromDishka, 24 | inject, 25 | setup_dishka, 26 | ) 27 | from dishka import make_container, Provider, provide, Scope 28 | 29 | 30 | 2. Create provider and container as usual 31 | 32 | 3. *(optional)* Set task class to your celery app to enable automatic injection for all task handlers 33 | 34 | .. code-block:: python 35 | 36 | celery_app = Celery(task_cls=DishkaTask) 37 | 38 | or for one task handler 39 | 40 | .. code-block:: python 41 | 42 | @celery_app.task(base=DishkaTask) 43 | def start( 44 | gateway: FromDishka[Gateway], 45 | ): 46 | ... 47 | 48 | 4. Mark those of your task handlers parameters which are to be injected with ``FromDishka[]`` 49 | 50 | .. code-block:: python 51 | 52 | @celery_app.task 53 | def start( 54 | gateway: FromDishka[Gateway], 55 | ): 56 | ... 57 | 58 | 5. *(optional)* Decorate them using ``@inject`` if you are not using ``DishkaTask``. 59 | 60 | .. code-block:: python 61 | 62 | @celery_app.task 63 | @inject 64 | def start( 65 | gateway: FromDishka[Gateway], 66 | ): 67 | ... 68 | 69 | 6. *(optional)* Setup signal to close container when worker process shutdown 70 | 71 | .. code-block:: python 72 | 73 | from celery import current_app 74 | from celery.signals import worker_process_shutdown 75 | from dishka import Container 76 | 77 | @worker_process_shutdown.connect() 78 | def close_dishka(*args, **kwargs): 79 | container: Container = current_app.conf["dishka_container"] 80 | container.close() 81 | 82 | 7. Setup ``dishka`` integration 83 | 84 | .. code-block:: python 85 | 86 | setup_dishka(container=container, app=celery_app) 87 | -------------------------------------------------------------------------------- /docs/integrations/flask.rst: -------------------------------------------------------------------------------- 1 | .. _flask: 2 | 3 | Flask 4 | =========================================== 5 | 6 | Though it is not required, you can use *dishka-flask* integration. It features: 7 | 8 | * automatic *REQUEST* scope management using middleware 9 | * passing ``Request`` object as a context data to providers for **HTTP** requests 10 | * automatic injection of dependencies into handler function. 11 | 12 | 13 | How to use 14 | **************** 15 | 16 | 1. Import 17 | 18 | .. code-block:: python 19 | 20 | from dishka.integrations.flask import ( 21 | FlaskProvider, 22 | FromDishka, 23 | inject, 24 | setup_dishka, 25 | ) 26 | from dishka import make_container, Provider, provide, Scope 27 | 28 | 2. Create provider. You can use ``flask.Request`` as a factory parameter to access HTTP or Websocket request. 29 | It is available on *REQUEST*-scope 30 | 31 | .. code-block:: python 32 | 33 | class YourProvider(Provider): 34 | @provide(scope=Scope.REQUEST) 35 | def create_x(self, request: Request) -> X: 36 | ... 37 | 38 | 3. Mark those of your handlers parameters which are to be injected with ``FromDishka[]`` 39 | 40 | .. code-block:: python 41 | 42 | @router.get('/') 43 | async def endpoint( 44 | gateway: FromDishka[Gateway], 45 | ): 46 | ... 47 | 48 | 3a. *(optional)* decorate them using ``@inject`` 49 | 50 | .. code-block:: python 51 | 52 | @router.get('/') 53 | @inject 54 | async def endpoint( 55 | gateway: FromDishka[Gateway], 56 | ) -> Response: 57 | ... 58 | 59 | 60 | 4. *(optional)* Use ``FlaskProvider()`` when creating container if you are going to use ``flask.Request`` in providers 61 | 62 | .. code-block:: python 63 | 64 | container = make_container(YourProvider(), FlaskProvider()) 65 | 66 | 5. Setup ``dishka`` integration. ``auto_inject=True`` is required unless you explicitly use ``@inject`` decorator. It is important here to call it after registering all views and blueprints 67 | 68 | .. code-block:: python 69 | 70 | setup_dishka(container=container, app=app, auto_inject=True) 71 | 72 | Or pass your own inject decorator 73 | 74 | .. code-block:: python 75 | 76 | setup_dishka(container=container, app=app, auto_inject=my_inject) 77 | -------------------------------------------------------------------------------- /docs/integrations/sanic.rst: -------------------------------------------------------------------------------- 1 | .. _sanic: 2 | 3 | Sanic 4 | =========================================== 5 | 6 | Though it is not required, you can use *dishka-sanic* integration. It features: 7 | 8 | * automatic *REQUEST* scope management using middleware 9 | * passing ``Request`` object as a context data to providers for **HTTP** requests 10 | * automatic injection of dependencies into handler function. 11 | 12 | 13 | How to use 14 | **************** 15 | 16 | 1. Import 17 | 18 | .. code-block:: python 19 | 20 | from dishka.integrations.sanic import ( 21 | FromDishka, 22 | SanicProvider, 23 | inject, 24 | setup_dishka, 25 | ) 26 | from dishka import make_async_container, Provider, provide, Scope 27 | 28 | 2. Create provider. You can use ``sanic.Request`` as a factory parameter to access on *REQUEST*-scope 29 | 30 | .. code-block:: python 31 | 32 | class YourProvider(Provider): 33 | @provide(scope=Scope.REQUEST) 34 | def create_x(self, request: Request) -> X: 35 | ... 36 | 37 | 38 | 3. Mark those of your handlers parameters which are to be injected with ``FromDishka[]`` 39 | 40 | .. code-block:: python 41 | 42 | @app.get('/') 43 | async def endpoint( 44 | request: str, gateway: FromDishka[Gateway], 45 | ) -> Response: 46 | ... 47 | 48 | 3a. *(optional)* decorate them using ``@inject`` if you are not using auto-injection 49 | 50 | .. code-block:: python 51 | 52 | @app.get('/') 53 | @inject 54 | async def endpoint( 55 | gateway: FromDishka[Gateway], 56 | ) -> ResponseModel: 57 | ... 58 | 59 | 60 | 4. *(optional)* Use ``SanicProvider()`` when creating container if you are going to use ``sanic.Request`` in providers 61 | 62 | .. code-block:: python 63 | 64 | container = make_async_container(YourProvider(), SanicProvider()) 65 | 66 | 67 | 5. Setup ``dishka`` integration. ``auto_inject=True`` is required unless you explicitly use ``@inject`` decorator 68 | 69 | .. code-block:: python 70 | 71 | setup_dishka(container=container, app=app, auto_inject=True) 72 | 73 | Or pass your own inject decorator 74 | 75 | .. code-block:: python 76 | 77 | setup_dishka(container=container, app=app, auto_inject=my_inject) 78 | 79 | Websockets 80 | ********************** 81 | 82 | Not supported yet 83 | -------------------------------------------------------------------------------- /tests/unit/test_type_match.py: -------------------------------------------------------------------------------- 1 | from typing import Generic, Literal, TypeVar 2 | 3 | import pytest 4 | 5 | from dishka.dependency_source.decorator import is_broader_or_same_type 6 | 7 | T = TypeVar("T") 8 | T2 = TypeVar("T2") 9 | T3 = TypeVar("T3") 10 | T4 = TypeVar("T4", bound=str) 11 | T5 = TypeVar("T5", int, str) 12 | 13 | 14 | class AGeneric(Generic[T]): ... 15 | 16 | 17 | class SubAGeneric(AGeneric[T], Generic[T]): ... 18 | 19 | 20 | class BGeneric(Generic[T]): ... 21 | 22 | 23 | class Multiple(Generic[T, T2, T3]): ... 24 | 25 | 26 | class C: ... 27 | 28 | 29 | class SubC(C): ... 30 | 31 | 32 | class D: ... 33 | 34 | 35 | class CGeneric(Generic[T4]): ... 36 | 37 | 38 | class DGeneric(Generic[T5]): ... 39 | 40 | 41 | TC = TypeVar("TC", bound=C) 42 | TCD = TypeVar("TCD", C, D) 43 | TSubCCD = TypeVar("TSubCCD", "C", SubC, D) 44 | 45 | 46 | @pytest.mark.parametrize( 47 | ("first", "second", "match"), [ 48 | (C, C, True), 49 | (C, D, False), 50 | (TC, C, True), 51 | (TC, SubC, True), 52 | (TC, D, False), 53 | (TSubCCD, TCD, True), 54 | (AGeneric[C], AGeneric[C], True), 55 | (AGeneric[TC], AGeneric[C], True), 56 | (AGeneric[TC], AGeneric[SubC], True), 57 | (AGeneric[C], BGeneric[C], False), 58 | ( 59 | Multiple[AGeneric[T], AGeneric[AGeneric[T2]], T3], 60 | Multiple[AGeneric[int], AGeneric[AGeneric[T]], T2], 61 | True, 62 | ), 63 | ( 64 | Multiple[AGeneric[T], AGeneric[AGeneric[T]], T3], 65 | Multiple[AGeneric[int], AGeneric[AGeneric[int]], T2], 66 | True, 67 | ), 68 | ( 69 | Multiple[AGeneric[T], AGeneric[AGeneric[T]], T3], 70 | Multiple[AGeneric[int], AGeneric[AGeneric[str]], T2], 71 | False, 72 | ), 73 | (CGeneric[T4], CGeneric[Literal["a"]], True), 74 | (CGeneric[T4], CGeneric[Literal[1]], False), 75 | (DGeneric[T5], DGeneric[Literal[1]], True), 76 | (DGeneric[T5], DGeneric[Literal["a"]], True), 77 | (DGeneric[T5], DGeneric[Literal[True]], True), 78 | (DGeneric[T5], DGeneric[Literal["a", 1]], False), 79 | ], 80 | ) 81 | def test_is_broader_or_same_type(*, first: T, second: T, match: bool): 82 | assert is_broader_or_same_type(first, second) == match 83 | -------------------------------------------------------------------------------- /docs/advanced/context.rst: -------------------------------------------------------------------------------- 1 | Context data 2 | ==================== 3 | 4 | Often, your scopes are assigned with some external events: HTTP-requests, message from queue, callbacks from framework. You can use those objects when creating dependencies. 5 | 6 | The difference from normal factories is that they are not created inside some ``Provider``, but passed to the scope. 7 | 8 | Working with context data consists of three parts: 9 | 10 | 1. Declaration that object is received from context using :ref:`from-context`. You need to provide the type and scope. 11 | * For the context passed to ``make_container`` and ``make_async_container`` functions it is done automatically in default component. 12 | * Context is shared across all providers. You do not need to specify it in each provider. 13 | * For the frameworks integrations you can use predefined providers instead of defining context data manually 14 | * To access context from additional components you need to use ``from_context`` is each of them in addition to default component. 15 | 2. Usage of that object in providers. 16 | 3. Passing actual values on scope entrance. It can be container creation for top level scope or container calls for nested ones. Use it in form ``context={Type: value,...}``. 17 | 18 | .. code-block:: python 19 | 20 | from framework import Request 21 | from dishka import Provider, make_container, Scope, from_context, provide 22 | 23 | 24 | class MyProvider(Provider): 25 | scope = Scope.REQUEST 26 | 27 | # declare context data for nested scope 28 | request = from_context(provides=Request, scope=Scope.REQUEST) 29 | 30 | # use objects as usual 31 | @provide 32 | def a(self, request: Request, broker: Broker) -> A: 33 | return A(data=request.contents) 34 | 35 | # passed APP-scoped context variable is automatically available as a dependency 36 | container = make_container(MyProvider(), context={Broker: broker}) 37 | 38 | while True: 39 | request = broker.recv() 40 | # provide REQUEST-scoped context variable 41 | with container(context={Request: request}) as request_container: 42 | a = request_container.get(A) 43 | 44 | .. note:: 45 | 46 | If you are using *multiple components*, you need to specify ``from_context`` in them separately though the context is share. 47 | -------------------------------------------------------------------------------- /docs/integrations/taskiq.rst: -------------------------------------------------------------------------------- 1 | .. _taskiq: 2 | 3 | taskiq 4 | =========================================== 5 | 6 | Though it is not required, you can use *dishka-taskiq* integration. It features: 7 | 8 | * automatic *REQUEST* scope management using middleware 9 | * passing ``TaskiqMessage`` object as a context data to providers 10 | * injection of dependencies into task handler function using decorator. 11 | 12 | 13 | How to use 14 | **************** 15 | 16 | 1. Import 17 | 18 | .. code-block:: python 19 | 20 | from dishka.integrations.taskiq import ( 21 | FromDishka, 22 | inject, 23 | setup_dishka, 24 | TaskiqProvider, 25 | ) 26 | from dishka import make_async_container, Provider, provide, Scope 27 | 28 | 2. Create provider. You can use ``taskiq.TaskiqMessage`` as a factory parameter to access on *REQUEST*-scope 29 | 30 | .. code-block:: python 31 | 32 | class YourProvider(Provider): 33 | @provide(scope=Scope.REQUEST) 34 | def create_x(self, event: TaskiqMessage) -> X: 35 | ... 36 | 37 | 38 | 3. Mark those of your handlers parameters which are to be injected with ``FromDishka[]`` and decorate them using ``@inject`` 39 | 40 | .. code-block:: python 41 | 42 | @broker.task 43 | @inject(patch_module=True) 44 | async def start( 45 | gateway: FromDishka[Gateway], 46 | ): 47 | ... 48 | 49 | 50 | .. warning:: 51 | In version 1.5, the ``patch_module`` parameter was added to the ``inject`` decorator, which is responsible for overriding the ``__module__`` attribute of the function that participates in the formation of ``task_name``. 52 | 53 | It is recommended to use the value ``patch_module=True``, to correctly generate the default ``task_name`` according to the module in which the task handler was defined. 54 | 55 | The default value is ``False``, for backward compatibility with versions < 1.5. In future releases, the default value may be changed to ``True``. 56 | 57 | 58 | 4. *(optional)* Use ``TaskiqProvider()`` when creating container if you are going to use ``taskiq.TaskiqMessage`` in providers 59 | 60 | .. code-block:: python 61 | 62 | container = make_async_container(YourProvider(), TaskiqProvider()) 63 | 64 | 65 | 5. Setup ``dishka`` integration. 66 | 67 | .. code-block:: python 68 | 69 | setup_dishka(container=container, broker=broker) 70 | 71 | -------------------------------------------------------------------------------- /tests/integrations/base/test_add_params.py: -------------------------------------------------------------------------------- 1 | from inspect import Parameter, Signature, signature 2 | 3 | import pytest 4 | 5 | from dishka.integrations.base import _add_params 6 | 7 | 8 | def func( 9 | pos_only, 10 | /, 11 | pos_keyword, 12 | *, 13 | keyword_only, 14 | ) -> None: ... 15 | 16 | 17 | def func_expected( 18 | pos_only, 19 | add_pos_only, 20 | /, 21 | pos_keyword, 22 | add_pos_keyword, 23 | *add_var_pos, 24 | keyword_only, 25 | add_keyword_only, 26 | **add_var_keyword, 27 | ) -> None: ... 28 | 29 | 30 | def func_with_args_kwargs(*args, **kwargs): ... 31 | 32 | 33 | def test_add_all_params(): 34 | additional_params = [ 35 | Parameter("add_pos_only", Parameter.POSITIONAL_ONLY), 36 | Parameter("add_pos_keyword", Parameter.POSITIONAL_OR_KEYWORD), 37 | Parameter("add_var_pos", Parameter.VAR_POSITIONAL), 38 | Parameter("add_keyword_only", Parameter.KEYWORD_ONLY), 39 | Parameter("add_var_keyword", Parameter.VAR_KEYWORD), 40 | ] 41 | func_signature = signature(func) 42 | func_params = list(func_signature.parameters.values()) 43 | 44 | result_params = _add_params(func_params, additional_params) 45 | new_signature = Signature( 46 | parameters=result_params, 47 | return_annotation=func_signature.return_annotation, 48 | ) 49 | 50 | assert new_signature == signature(func_expected) 51 | 52 | 53 | def test_fail_add_second_args(): 54 | additional_params = [ 55 | Parameter("add_var_pos", Parameter.VAR_POSITIONAL), 56 | ] 57 | 58 | func_signature = signature(func_with_args_kwargs) 59 | func_params = list(func_signature.parameters.values()) 60 | 61 | with pytest.raises( 62 | ValueError, match="more than one variadic positional parameter", 63 | ): 64 | _add_params(func_params, additional_params) 65 | 66 | 67 | def test_fail_add_second_kwargs(): 68 | additional_params = [ 69 | Parameter("add_var_keyword", Parameter.VAR_KEYWORD), 70 | ] 71 | 72 | func_signature = signature(func_with_args_kwargs) 73 | func_params = list(func_signature.parameters.values()) 74 | 75 | with pytest.raises( 76 | ValueError, match="more than one variadic keyword parameter", 77 | ): 78 | _add_params(func_params, additional_params) 79 | -------------------------------------------------------------------------------- /docs/integrations/adding_new.rst: -------------------------------------------------------------------------------- 1 | .. _adding_new: 2 | 3 | Adding new integrations 4 | =========================== 5 | 6 | Though there are some integrations in library you are not limited to use them. 7 | 8 | The main points are: 9 | 10 | 1. Find a way to pass a global container instance. Often it is attached to application instance or passed by a middleware. 11 | 2. Find a place to enter and exit request scope and how to pass the container to a handler. Usually, it is entered in a middleware and container is stored in some kind of request context. 12 | 13 | Alternatively, you can use the ``wrap_injection`` function with ``manage_scope=True`` to automate entering and exiting the request scope without relying on middleware. When enabled, ``manage_scope`` ensures that the container passed to ``wrap_injection`` enters and exits the next scope. 14 | **Custom Scope Targeting** 15 | 16 | For more complex cases, you can pass a ``scope`` argument to ``wrap_injection`` to specify a custom scope to enter on function call. 17 | For example, you can create decorators that target specific scopes in the dependency hierarchy: 18 | 19 | .. code-block:: python 20 | 21 | @inject(scope=Scope.STEP) 22 | async def handler(step_dep: StepDep = FromDishka[StepDep]): 23 | ... 24 | 25 | @inject(scope=Scope.ACTION) 26 | def process_action(action_dep: ActionDep = FromDishka[ActionDep]): 27 | ... 28 | 29 | When you use ``manage_scope=True`` or specify a custom ``scope``, you can pass ``provide_context`` function that allows to populate the container context with passed arguments. This is useful when you want to pass the function scoped arguments to other dependencies without relying on middleware. 30 | 3. Configure a decorator. The main option here is to provide a way for retrieving container. Often, need to modify handler signature adding additional parameters. It is also available. 31 | 4. Check if you can apply decorator automatically. 32 | 33 | While writing middlewares and working with scopes is done by your custom code, we have a helper for creating ``@inject`` decorators - a ``wrap_injection`` function. 34 | 35 | * ``container_getter`` is a function with two params ``(args, kwargs)`` which is called to get a container used to retrieve dependencies within scope. 36 | * ``additional_params`` is a list of ``inspect.Parameter`` which should be added to handler signature. 37 | 38 | For more details, check existing integrations. 39 | -------------------------------------------------------------------------------- /tests/unit/container/test_concurrency.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import threading 3 | import time 4 | from concurrent.futures import ThreadPoolExecutor 5 | from unittest.mock import Mock 6 | 7 | import pytest 8 | 9 | from dishka import ( 10 | AsyncContainer, 11 | Container, 12 | Provider, 13 | Scope, 14 | make_async_container, 15 | make_container, 16 | provide, 17 | ) 18 | 19 | 20 | class SyncProvider(Provider): 21 | def __init__(self, event: threading.Event, mock: Mock): 22 | super().__init__() 23 | self.event = event 24 | self.mock = mock 25 | 26 | @provide(scope=Scope.APP) 27 | def get_int(self) -> int: 28 | self.event.wait() 29 | return self.mock() 30 | 31 | @provide(scope=Scope.APP) 32 | def get_str(self, value: int) -> str: 33 | return "str" 34 | 35 | 36 | def sync_get(container: Container): 37 | container.get(str) 38 | 39 | 40 | @pytest.mark.repeat(10) 41 | def test_cache_sync(): 42 | int_getter = Mock(return_value=123) 43 | event = threading.Event() 44 | provider = SyncProvider(event, int_getter) 45 | with ThreadPoolExecutor() as pool: 46 | container = make_container(provider, lock_factory=threading.Lock) 47 | pool.submit(sync_get, container) 48 | pool.submit(sync_get, container) 49 | time.sleep(0.01) 50 | event.set() 51 | int_getter.assert_called_once_with() 52 | 53 | 54 | class AsyncProvider(Provider): 55 | def __init__(self, event: asyncio.Event, mock: Mock): 56 | super().__init__() 57 | self.event = event 58 | self.mock = mock 59 | 60 | @provide(scope=Scope.APP) 61 | async def get_int(self) -> int: 62 | await self.event.wait() 63 | return self.mock() 64 | 65 | @provide(scope=Scope.APP) 66 | def get_str(self, value: int) -> str: 67 | return "str" 68 | 69 | 70 | async def async_get(container: AsyncContainer): 71 | await container.get(str) 72 | 73 | 74 | @pytest.mark.repeat(10) 75 | @pytest.mark.asyncio 76 | async def test_cache_async(): 77 | int_getter = Mock(return_value=123) 78 | event = asyncio.Event() 79 | provider = AsyncProvider(event, int_getter) 80 | 81 | container = make_async_container(provider, lock_factory=asyncio.Lock) 82 | t1 = asyncio.create_task(async_get(container)) 83 | t2 = asyncio.create_task(async_get(container)) 84 | await asyncio.sleep(0.01) 85 | event.set() 86 | await t1 87 | await t2 88 | 89 | int_getter.assert_called_once_with() 90 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | dishka 2 | ============================================= 3 | 4 | Cute DI framework with scopes and agreeable API. 5 | 6 | This library provides **IoC container** that's genuinely useful. 7 | If you're exhausted from endlessly passing objects just to create other objects, only to have those objects create even 8 | more — you're not alone, and we have a solution. 9 | Not every project requires IoC container, but take a look at what we offer. 10 | 11 | Unlike other tools, ``dishka`` focuses **only** 12 | on **dependency injection** without trying to solve unrelated tasks. 13 | It keeps DI in place without cluttering your code with global variables and scattered specifiers. 14 | 15 | Key features: 16 | 17 | * **Scopes**. Any object can have a lifespan for the entire app, a single request, or even more fractionally. Many 18 | frameworks either lack scopes completely or offer only two. Here, you can define as many scopes as needed. 19 | * **Finalization**. Some dependencies, like database connections, need not only to be created but also carefully 20 | released. Many frameworks lack this essential feature. 21 | * **Modular providers**. Instead of creating many separate functions or one large class, you can split factories 22 | into smaller classes for easier reuse. 23 | * **Clean dependencies**. You don't need to add custom markers to dependency code just to make it visible to the 24 | library. 25 | * **Simple API**. Only a few objects are needed to start using the library. 26 | * **Framework integrations**. Popular frameworks are supported out of the box. You can simply extend it for your needs. 27 | * **Speed**. The library is fast enough that performance is not a concern. In fact, it outperforms many 28 | alternatives. 29 | 30 | .. toctree:: 31 | :hidden: 32 | :caption: Contents: 33 | 34 | quickstart 35 | di_intro 36 | concepts 37 | provider/index 38 | container/index 39 | integrations/index 40 | errors 41 | alternatives 42 | 43 | .. toctree:: 44 | :hidden: 45 | :caption: Advanced usage: 46 | 47 | advanced/components 48 | advanced/context 49 | advanced/generics 50 | advanced/scopes 51 | advanced/testing/index 52 | advanced/plotter 53 | 54 | .. toctree:: 55 | :hidden: 56 | :caption: For developers: 57 | 58 | requirements/technical 59 | contributing 60 | 61 | .. toctree:: 62 | :hidden: 63 | :caption: Project Links 64 | 65 | GitHub 66 | PyPI 67 | Chat 68 | -------------------------------------------------------------------------------- /docs/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quickstart 2 | ******************** 3 | 4 | 1. **Install dishka** 5 | 6 | .. code-block:: shell 7 | 8 | pip install dishka 9 | 10 | 2. **Define your classes with type hints.** Imagine you have two classes: ``Service`` (business logic) and 11 | ``DAO`` (data access), along with an external API client: 12 | 13 | .. literalinclude:: ./quickstart_example.py 14 | :language: python 15 | :lines: 6-21 16 | 17 | 3. **Create** ``Provider`` instance and specify how to provide dependencies 18 | 19 | Providers are used only to set up factories providing your objects. 20 | 21 | Use ``scope=Scope.APP`` for dependencies created once for the entire application lifetime, 22 | and ``scope=Scope.REQUEST`` for those that need to be recreated for each request, event, etc. 23 | To learn more about scopes, see :ref:`scopes` 24 | 25 | .. literalinclude:: ./quickstart_example.py 26 | :language: python 27 | :lines: 24-30 28 | 29 | To provide a releasable connection, you might need some custom code: 30 | 31 | .. literalinclude:: ./quickstart_example.py 32 | :language: python 33 | :lines: 33-41 34 | 35 | 4. **Create main** ``Container`` instance, passing providers, and enter *APP*-scope 36 | 37 | .. literalinclude:: ./quickstart_example.py 38 | :language: python 39 | :lines: 44-47 40 | 41 | 5. **Access dependencies using container.** Container holds a cache of dependencies and is used to retrieve them. 42 | You can use ``.get`` method to access *APP*-scoped dependencies: 43 | 44 | .. literalinclude:: ./quickstart_example.py 45 | :language: python 46 | :lines: 49-50 47 | 48 | 6. **Enter and exit** ``REQUEST`` **scope repeatedly using a context manager** 49 | 50 | .. literalinclude:: ./quickstart_example.py 51 | :language: python 52 | :lines: 52-60 53 | 54 | 7. **Close container** when done 55 | 56 | .. literalinclude:: ./quickstart_example.py 57 | :language: python 58 | :lines: 62 59 | 60 | .. dropdown:: Full example 61 | 62 | .. literalinclude:: ./quickstart_example_full.py 63 | :language: python 64 | 65 | 8. **Integrate with your framework.** If you are using a supported framework, add decorators and middleware for it. 66 | For more details, see :ref:`integrations` 67 | 68 | .. code-block:: python 69 | 70 | from dishka.integrations.fastapi import ( 71 | FromDishka, inject, setup_dishka, 72 | ) 73 | 74 | 75 | @router.get("/") 76 | @inject 77 | async def index(service: FromDishka[Service]) -> str: 78 | ... 79 | 80 | 81 | ... 82 | setup_dishka(container, app) 83 | -------------------------------------------------------------------------------- /src/dishka/_adaptix/type_tools/type_evaler.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import inspect 3 | from collections.abc import Callable, Sequence 4 | from types import ModuleType 5 | 6 | 7 | def make_fragments_collector(*, typing_modules: Sequence[str]) -> Callable[[ast.Module], list[ast.stmt]]: 8 | def check_condition(expr: ast.expr) -> bool: 9 | # searches for `TYPE_CHECKING` 10 | if ( 11 | isinstance(expr, ast.Name) 12 | and isinstance(expr.ctx, ast.Load) 13 | and expr.id == "TYPE_CHECKING" 14 | ): 15 | return True 16 | 17 | # searches for `typing.TYPE_CHECKING` 18 | if ( # noqa: SIM103 19 | isinstance(expr, ast.Attribute) 20 | and expr.attr == "TYPE_CHECKING" 21 | and isinstance(expr.ctx, ast.Load) 22 | and isinstance(expr.value, ast.Name) 23 | and expr.value.id in typing_modules 24 | and isinstance(expr.value.ctx, ast.Load) 25 | ): 26 | return True 27 | return False 28 | 29 | def collect_type_checking_only_fragments(module: ast.Module) -> list[ast.stmt]: 30 | fragments = [] 31 | for stmt in module.body: 32 | if isinstance(stmt, ast.If) and not stmt.orelse and check_condition(stmt.test): 33 | fragments.extend(stmt.body) 34 | 35 | return fragments 36 | 37 | return collect_type_checking_only_fragments 38 | 39 | 40 | default_collector = make_fragments_collector(typing_modules=["typing"]) 41 | 42 | 43 | def exec_type_checking( 44 | module: ModuleType, 45 | *, 46 | collector: Callable[[ast.Module], list[ast.stmt]] = default_collector, 47 | ) -> None: 48 | """This function scans module source code, 49 | collects fragments under ``if TYPE_CHECKING`` and ``if typing.TYPE_CHECKING`` 50 | and executes them in the context of module. 51 | After these, all imports and type definitions became available at runtime for analysis. 52 | 53 | By default, it ignores ``if`` with ``else`` branch. 54 | 55 | :param module: A module for processing 56 | :param collector: A function collecting code fragments to execute 57 | """ 58 | source = inspect.getsource(module) 59 | fragments = collector(ast.parse(source)) 60 | code = compile(ast.Module(fragments, type_ignores=[]), f"", "exec") 61 | namespace = module.__dict__.copy() 62 | exec(code, namespace) # noqa: S102 63 | for k, v in namespace.items(): 64 | if not hasattr(module, k): 65 | setattr(module, k, v) 66 | -------------------------------------------------------------------------------- /src/dishka/integrations/sanic.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "FromDishka", 3 | "SanicProvider", 4 | "inject", 5 | "setup_dishka", 6 | ] 7 | 8 | from collections.abc import Iterable 9 | from typing import Any, cast 10 | 11 | from sanic import HTTPResponse, Request, Sanic 12 | from sanic.models.handler_types import RouteHandler 13 | from sanic_routing import Route 14 | 15 | from dishka import AsyncContainer, FromDishka, Provider, Scope, from_context 16 | from dishka.integrations.base import ( 17 | InjectFunc, 18 | is_dishka_injected, 19 | wrap_injection, 20 | ) 21 | 22 | 23 | def inject(func: RouteHandler) -> RouteHandler: 24 | return cast( 25 | RouteHandler, 26 | wrap_injection( 27 | func=func, 28 | is_async=True, 29 | container_getter=lambda args, _: args[0].ctx.dishka_container, 30 | ), 31 | ) 32 | 33 | 34 | class SanicProvider(Provider): 35 | request = from_context(Request, scope=Scope.REQUEST) 36 | 37 | 38 | class ContainerMiddleware: 39 | def __init__(self, container: AsyncContainer) -> None: 40 | self.container = container 41 | 42 | async def on_request(self, request: Request) -> None: 43 | request.ctx.container_wrapper = self.container({Request: request}) 44 | request.ctx.dishka_container = await request.ctx.container_wrapper.__aenter__() # noqa: E501 45 | 46 | async def on_response(self, request: Request, _: HTTPResponse) -> None: 47 | await request.ctx.dishka_container.close() 48 | 49 | 50 | def _inject_routes( 51 | routes: Iterable[Route], 52 | inject: InjectFunc[[RouteHandler], RouteHandler], 53 | ) -> None: 54 | for route in routes: 55 | if not is_dishka_injected(route.handler): 56 | route.handler = inject(route.handler) 57 | 58 | 59 | def setup_dishka( 60 | container: AsyncContainer, 61 | app: Sanic[Any, Any], 62 | *, 63 | auto_inject: bool | InjectFunc[[RouteHandler], RouteHandler] = False, 64 | ) -> None: 65 | middleware = ContainerMiddleware(container) 66 | app.on_request(middleware.on_request) 67 | app.on_response(middleware.on_response) # type: ignore[no-untyped-call] 68 | 69 | if auto_inject is not False: 70 | inject_func: InjectFunc[[RouteHandler], RouteHandler] 71 | if auto_inject is True: 72 | inject_func = inject 73 | else: 74 | inject_func = auto_inject 75 | 76 | _inject_routes(app.router.routes, inject_func) 77 | for blueprint in app.blueprints.values(): 78 | _inject_routes(blueprint.routes, inject_func) 79 | -------------------------------------------------------------------------------- /examples/integrations/fastapi_app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from abc import abstractmethod 3 | from contextlib import asynccontextmanager 4 | from typing import Protocol 5 | 6 | import uvicorn 7 | from dishka import ( 8 | Provider, 9 | Scope, 10 | make_async_container, 11 | provide, 12 | ) 13 | from dishka.integrations.fastapi import ( 14 | DishkaRoute, 15 | FastapiProvider, 16 | FromDishka, 17 | inject, 18 | setup_dishka, 19 | ) 20 | from fastapi import APIRouter, FastAPI 21 | 22 | 23 | # app core 24 | class DbGateway(Protocol): 25 | @abstractmethod 26 | def get(self) -> str: 27 | raise NotImplementedError 28 | 29 | 30 | class FakeDbGateway(DbGateway): 31 | def get(self) -> str: 32 | return "Hello" 33 | 34 | 35 | class Interactor: 36 | def __init__(self, db: DbGateway): 37 | self.db = db 38 | 39 | def __call__(self) -> str: 40 | return self.db.get() 41 | 42 | 43 | # app dependency logic 44 | class AdaptersProvider(Provider): 45 | @provide(scope=Scope.REQUEST) 46 | def get_db(self) -> DbGateway: 47 | return FakeDbGateway() 48 | 49 | 50 | class InteractorProvider(Provider): 51 | i1 = provide(Interactor, scope=Scope.REQUEST) 52 | 53 | 54 | # presentation layer 55 | router = APIRouter() 56 | 57 | 58 | @router.get("/") 59 | @inject 60 | async def index( 61 | *, 62 | interactor: FromDishka[Interactor], 63 | ) -> str: 64 | return interactor() 65 | 66 | 67 | # with this router you do not need `@inject` on each view 68 | second_router = APIRouter(route_class=DishkaRoute) 69 | 70 | 71 | @second_router.get("/auto") 72 | async def auto( 73 | *, 74 | interactor: FromDishka[Interactor], 75 | ) -> str: 76 | return interactor() 77 | 78 | 79 | @asynccontextmanager 80 | async def lifespan(app: FastAPI): 81 | yield 82 | await app.state.dishka_container.close() 83 | 84 | 85 | def create_app(): 86 | logging.basicConfig( 87 | level=logging.WARNING, 88 | format="%(asctime)s %(process)-7s %(module)-20s %(message)s", 89 | ) 90 | 91 | app = FastAPI(lifespan=lifespan) 92 | app.include_router(router) 93 | app.include_router(second_router) 94 | container = make_async_container( 95 | AdaptersProvider(), 96 | InteractorProvider(), 97 | FastapiProvider(), 98 | ) 99 | setup_dishka(container, app) 100 | return app 101 | 102 | 103 | if __name__ == "__main__": 104 | uvicorn.run(create_app(), host="0.0.0.0", port=8000, lifespan="on") 105 | -------------------------------------------------------------------------------- /src/dishka/integrations/flask.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "FlaskProvider", 3 | "FromDishka", 4 | "inject", 5 | "setup_dishka", 6 | ] 7 | 8 | from collections.abc import Callable 9 | from typing import Any, ParamSpec, TypeVar, cast 10 | 11 | from flask import Flask, Request, g, request 12 | from flask.sansio.scaffold import Scaffold 13 | from flask.typing import RouteCallable 14 | 15 | from dishka import Container, FromDishka, Provider, Scope, from_context 16 | from .base import InjectFunc, is_dishka_injected, wrap_injection 17 | 18 | T = TypeVar("T") 19 | P = ParamSpec("P") 20 | 21 | 22 | def inject(func: Callable[P, T]) -> Callable[P, T]: 23 | return wrap_injection( 24 | func=func, 25 | is_async=False, 26 | container_getter=lambda _, p: g.dishka_container, 27 | ) 28 | 29 | 30 | class FlaskProvider(Provider): 31 | request = from_context(Request, scope=Scope.REQUEST) 32 | 33 | 34 | class ContainerMiddleware: 35 | def __init__(self, container: Container) -> None: 36 | self.container = container 37 | 38 | def enter_request(self) -> None: 39 | g.dishka_container_wrapper = self.container({Request: request}) 40 | g.dishka_container = g.dishka_container_wrapper.__enter__() 41 | 42 | def exit_request(self, *_args: Any, **_kwargs: Any) -> None: 43 | dishka_container = getattr(g, "dishka_container", None) 44 | if dishka_container is not None: 45 | g.dishka_container.close() 46 | 47 | 48 | def _inject_routes(scaffold: Scaffold, inject_func: InjectFunc[P, T]) -> None: 49 | for key, func in scaffold.view_functions.items(): 50 | if not is_dishka_injected(func): 51 | # typing.cast is applied because there 52 | # are RouteCallable objects in dict value 53 | scaffold.view_functions[key] = cast( 54 | RouteCallable, 55 | inject_func(func), 56 | ) 57 | 58 | 59 | def setup_dishka( 60 | container: Container, 61 | app: Flask, 62 | *, 63 | auto_inject: bool | InjectFunc[P, T] = False, 64 | ) -> None: 65 | middleware = ContainerMiddleware(container) 66 | app.before_request(middleware.enter_request) 67 | app.teardown_appcontext(middleware.exit_request) 68 | if auto_inject is not False: 69 | inject_func: InjectFunc[P, T] 70 | if auto_inject is True: 71 | inject_func = inject 72 | else: 73 | inject_func = auto_inject 74 | 75 | _inject_routes(app, inject_func) 76 | for blueprint in app.blueprints.values(): 77 | _inject_routes(blueprint, inject_func) 78 | -------------------------------------------------------------------------------- /src/dishka/provider/make_decorator.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from typing import Any, overload 3 | 4 | from dishka.dependency_source import ( 5 | CompositeDependencySource, 6 | Decorator, 7 | ensure_composite, 8 | ) 9 | from dishka.entities.scope import BaseScope 10 | from .exceptions import IndependentDecoratorError 11 | from .make_factory import make_factory 12 | from .unpack_provides import unpack_decorator 13 | 14 | 15 | def _decorate( 16 | source: Callable[..., Any] | type, 17 | provides: Any, 18 | scope: BaseScope | None, 19 | *, 20 | is_in_class: bool = True, 21 | ) -> CompositeDependencySource: 22 | composite = ensure_composite(source) 23 | decorator = Decorator( 24 | make_factory( 25 | provides=provides, 26 | scope=None, 27 | source=source, 28 | cache=False, 29 | is_in_class=is_in_class, 30 | override=False, 31 | ), 32 | scope=scope, 33 | ) 34 | if ( 35 | decorator.provides not in decorator.factory.kw_dependencies.values() 36 | and decorator.provides not in decorator.factory.dependencies 37 | ): 38 | raise IndependentDecoratorError(source) 39 | 40 | composite.dependency_sources.extend(unpack_decorator(decorator)) 41 | return composite 42 | 43 | 44 | @overload 45 | def decorate( 46 | *, 47 | provides: Any = None, 48 | scope: BaseScope | None = None, 49 | ) -> Callable[ 50 | [Callable[..., Any]], CompositeDependencySource, 51 | ]: 52 | ... 53 | 54 | 55 | @overload 56 | def decorate( 57 | source: Callable[..., Any] | type, 58 | *, 59 | provides: Any = None, 60 | scope: BaseScope | None = None, 61 | ) -> CompositeDependencySource: 62 | ... 63 | 64 | 65 | def decorate( 66 | source: Callable[..., Any] | type | None = None, 67 | provides: Any = None, 68 | scope: BaseScope | None = None, 69 | ) -> CompositeDependencySource | Callable[ 70 | [Callable[..., Any]], CompositeDependencySource, 71 | ]: 72 | if source is not None: 73 | return _decorate(source, provides, scope=scope, is_in_class=True) 74 | 75 | def scoped(func: Callable[..., Any]) -> CompositeDependencySource: 76 | return _decorate(func, provides, scope=scope, is_in_class=True) 77 | 78 | return scoped 79 | 80 | 81 | def decorate_on_instance( 82 | source: Callable[..., Any] | type, 83 | provides: Any, 84 | scope: BaseScope | None, 85 | ) -> CompositeDependencySource: 86 | return _decorate(source, provides, scope=scope, is_in_class=False) 87 | -------------------------------------------------------------------------------- /docs/integrations/aiogram.rst: -------------------------------------------------------------------------------- 1 | .. _aiogram: 2 | 3 | aiogram 4 | =========================================== 5 | 6 | Though it is not required, you can use *dishka-aiogram* integration. It features: 7 | 8 | * automatic *REQUEST* scope management using middleware 9 | * passing ``TelegramObject`` object and ``AiogramMiddlewareData`` dict as a context data to providers for telegram events (update object fields) 10 | * automatic injection of dependencies into handler function. 11 | 12 | Only async handlers are supported. 13 | 14 | How to use 15 | **************** 16 | 17 | 1. Import 18 | 19 | .. code-block:: python 20 | 21 | from dishka.integrations.aiogram import ( 22 | AiogramProvider, 23 | FromDishka, 24 | inject, 25 | setup_dishka, 26 | ) 27 | from dishka import make_async_container, Provider, provide, Scope 28 | 29 | 2. Create provider. You can use ``aiogram.types.TelegramObject`` and ``dishka.integrations.aiogram.AiogramMiddlewareData`` as a factory parameter to access on *REQUEST*-scope 30 | 31 | .. code-block:: python 32 | 33 | class YourProvider(Provider): 34 | @provide(scope=Scope.REQUEST) 35 | def create_x(self, event: TelegramObject, middleware_data: AiogramMiddlewareData) -> X: 36 | ... 37 | 38 | 39 | 3. Mark those of your handlers parameters which are to be injected with ``FromDishka[]`` 40 | 41 | .. code-block:: python 42 | 43 | @dp.message() 44 | async def start( 45 | message: Message, 46 | gateway: FromDishka[Gateway], 47 | ): 48 | 49 | 50 | 3a. *(optional)* decorate them using ``@inject`` if you are not using auto-injection 51 | 52 | .. code-block:: python 53 | 54 | @dp.message() 55 | @inject 56 | async def start( 57 | message: Message, 58 | gateway: FromDishka[Gateway], 59 | ): 60 | 61 | 62 | 4. *(optional)* Use ``AiogramProvider()`` when creating container if you are going to use ``aiogram.types.TelegramObject`` or ``dishka.integrations.aiogram.AiogramMiddlewareData`` in providers. 63 | 64 | .. code-block:: python 65 | 66 | container = make_async_container(YourProvider(), AiogramProvider()) 67 | 68 | 69 | 5. Setup ``dishka`` integration. ``auto_inject=True`` is required unless you explicitly use ``@inject`` decorator 70 | 71 | .. code-block:: python 72 | 73 | setup_dishka(container=container, router=dp, auto_inject=True) 74 | 75 | Or pass your own inject decorator 76 | 77 | .. code-block:: python 78 | 79 | setup_dishka(container=container, router=dp, auto_inject=my_inject) 80 | 81 | 6. *(optional)* Close container on dispatcher shutdown 82 | 83 | .. code-block:: python 84 | 85 | dispatcher.shutdown.register(container.close) 86 | 87 | -------------------------------------------------------------------------------- /src/dishka/provider/unpack_provides.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Sequence 2 | from typing import get_args, get_origin 3 | 4 | from dishka.dependency_source import ( 5 | Alias, 6 | Decorator, 7 | DependencySource, 8 | Factory, 9 | ) 10 | from dishka.entities.key import hint_to_dependency_key 11 | from dishka.entities.provides_marker import ProvideMultiple 12 | 13 | 14 | def unpack_factory(factory: Factory) -> Sequence[DependencySource]: 15 | if get_origin(factory.provides.type_hint) is not ProvideMultiple: 16 | return [factory] 17 | 18 | provides_first, *provides_others = get_args(factory.provides.type_hint) 19 | 20 | res: list[DependencySource] = [ 21 | Alias( 22 | provides=hint_to_dependency_key( 23 | provides_other, 24 | ).with_component(factory.provides.component), 25 | source=hint_to_dependency_key( 26 | provides_first, 27 | ).with_component(factory.provides.component), 28 | cache=factory.cache, 29 | override=factory.override, 30 | ) 31 | for provides_other in provides_others 32 | ] 33 | res.append( 34 | Factory( 35 | dependencies=factory.dependencies, 36 | kw_dependencies=factory.kw_dependencies, 37 | type_=factory.type, 38 | source=factory.source, 39 | scope=factory.scope, 40 | is_to_bind=factory.is_to_bind, 41 | cache=factory.cache, 42 | override=factory.override, 43 | provides=hint_to_dependency_key( 44 | provides_first, 45 | ).with_component(factory.provides.component), 46 | ), 47 | ) 48 | return res 49 | 50 | 51 | def unpack_decorator(decorator: Decorator) -> Sequence[DependencySource]: 52 | if get_origin(decorator.provides.type_hint) is not ProvideMultiple: 53 | return [decorator] 54 | 55 | return [ 56 | Decorator( 57 | factory=decorator.factory, 58 | provides=hint_to_dependency_key( 59 | provides, 60 | ).with_component(decorator.provides.component), 61 | ) 62 | for provides in get_args(decorator.provides.type_hint) 63 | ] 64 | 65 | 66 | def unpack_alias(alias: Alias) -> Sequence[DependencySource]: 67 | if get_origin(alias.provides.type_hint) is not ProvideMultiple: 68 | return [alias] 69 | 70 | return [ 71 | Alias( 72 | provides=hint_to_dependency_key( 73 | provides, 74 | ).with_component(alias.provides.component), 75 | source=alias.source, 76 | cache=alias.cache, 77 | override=alias.override, 78 | ) 79 | for provides in get_args(alias.provides.type_hint) 80 | ] 81 | -------------------------------------------------------------------------------- /docs/provider/decorate.rst: -------------------------------------------------------------------------------- 1 | .. _decorate: 2 | 3 | decorate 4 | ********************* 5 | 6 | ``decorate`` is used to modify or wrap an object which is already configured in another ``Provider``. 7 | 8 | Provider object has also a ``.decorate`` method with the same logic. 9 | 10 | If you want to apply decorator pattern and do not want to alter existing provide method, then it is a place for ``decorate``. It will construct object using earlier defined provider and then pass it to your decorator before returning from the container. 11 | 12 | 13 | .. code-block:: python 14 | 15 | from dishka import decorate, Provider, provide, Scope 16 | 17 | class UserDAO(Protocol): ... 18 | class UserDAOImpl(UserDAO): ... 19 | 20 | class UserDAOWithMetrics(UserDAO): 21 | def __init__(self, dao: UserDAO) -> None: 22 | self.dao = dao 23 | self.prometheus = Prometheus() 24 | 25 | def get_by_id(self, uid: UserID) -> User: 26 | self.prometheus.get_by_id_metric.inc() 27 | return self.dao.get_by_id(uid) 28 | 29 | 30 | class MyProvider(Provider): 31 | user_dao = provide( 32 | UserDAOImpl, scope=Scope.REQUEST, provides=UserDAO 33 | ) 34 | 35 | @decorate 36 | def decorate_user_dao(self, dao: UserDAO) -> UserDAO: 37 | return UserDAOWithMetrics(dao) 38 | 39 | Such decorator function can also have **additional parameters**. 40 | 41 | .. code-block:: python 42 | 43 | from dishka import decorate, Provider, provide, Scope 44 | 45 | class UserDAO(Protocol): ... 46 | class UserDAOImpl(UserDAO): ... 47 | 48 | class UserDAOWithMetrics(UserDAO): 49 | def __init__(self, dao: UserDAO, prom: Prometheus) -> None: 50 | self.dao = dao 51 | self.prometheus = prom 52 | 53 | def get_by_id(self, uid: UserID) -> User: 54 | self.prometheus.get_by_id_metric.inc() 55 | return self.dao.get_by_id(uid) 56 | 57 | 58 | class MyProvider(Provider): 59 | user_dao = provide( 60 | UserDAOImpl, scope=Scope.REQUEST, provides=UserDAO 61 | ) 62 | prometheus = provide(Prometheus) 63 | 64 | @decorate 65 | def decorate_user_dao( 66 | self, dao: UserDAO, prom: Prometheus 67 | ) -> UserDAO: 68 | return UserDAOWithMetrics(dao, prom) 69 | 70 | 71 | Decorator can change the **Scope** of an object to more narrow one, just pass ``scope=`` argument. 72 | 73 | The limitation is that you cannot use ``decorate`` in the same provider as you declare factory or alias for dependency. But you won't need it because you can update the factory code. 74 | 75 | The idea of ``decorate`` is to postprocess dependencies provided by some external source, when you combine multiple ``Provider`` objects into one container. -------------------------------------------------------------------------------- /tests/integrations/starlette/test_starlette.py: -------------------------------------------------------------------------------- 1 | from contextlib import asynccontextmanager 2 | from unittest.mock import Mock 3 | 4 | import pytest 5 | from asgi_lifespan import LifespanManager 6 | from starlette.applications import Starlette 7 | from starlette.requests import Request 8 | from starlette.responses import PlainTextResponse 9 | from starlette.routing import Route 10 | from starlette.testclient import TestClient 11 | 12 | from dishka import make_async_container 13 | from dishka.integrations.starlette import ( 14 | FromDishka, 15 | inject, 16 | setup_dishka, 17 | ) 18 | from ..common import ( 19 | APP_DEP_VALUE, 20 | REQUEST_DEP_VALUE, 21 | AppDep, 22 | AppProvider, 23 | RequestDep, 24 | ) 25 | 26 | 27 | @asynccontextmanager 28 | async def dishka_app(view, provider) -> TestClient: 29 | app = Starlette(routes=[Route("/", inject(view), methods=["GET"])]) 30 | container = make_async_container(provider) 31 | setup_dishka(container, app) 32 | async with LifespanManager(app): 33 | yield TestClient(app) 34 | await container.close() 35 | 36 | 37 | async def get_with_app( 38 | _: Request, 39 | a: FromDishka[AppDep], 40 | mock: FromDishka[Mock], 41 | ) -> PlainTextResponse: 42 | mock(a) 43 | return PlainTextResponse("passed") 44 | 45 | 46 | @pytest.mark.asyncio 47 | async def test_app_dependency(app_provider: AppProvider): 48 | async with dishka_app(get_with_app, app_provider) as client: 49 | client.get("/") 50 | app_provider.mock.assert_called_with(APP_DEP_VALUE) 51 | app_provider.app_released.assert_not_called() 52 | app_provider.app_released.assert_called() 53 | 54 | 55 | async def get_with_request( 56 | _: Request, 57 | a: FromDishka[RequestDep], 58 | mock: FromDishka[Mock], 59 | ) -> PlainTextResponse: 60 | mock(a) 61 | return PlainTextResponse("passed") 62 | 63 | 64 | @pytest.mark.asyncio 65 | async def test_request_dependency(app_provider: AppProvider): 66 | async with dishka_app(get_with_request, app_provider) as client: 67 | client.get("/") 68 | app_provider.mock.assert_called_with(REQUEST_DEP_VALUE) 69 | app_provider.request_released.assert_called_once() 70 | 71 | 72 | @pytest.mark.asyncio 73 | async def test_request_dependency2(app_provider: AppProvider): 74 | async with dishka_app(get_with_request, app_provider) as client: 75 | client.get("/") 76 | app_provider.mock.assert_called_with(REQUEST_DEP_VALUE) 77 | app_provider.mock.reset_mock() 78 | app_provider.request_released.assert_called_once() 79 | app_provider.request_released.reset_mock() 80 | client.get("/") 81 | app_provider.mock.assert_called_with(REQUEST_DEP_VALUE) 82 | app_provider.request_released.assert_called_once() 83 | -------------------------------------------------------------------------------- /tests/unit/container/test_recursive.py: -------------------------------------------------------------------------------- 1 | from typing import Protocol 2 | 3 | from dishka import ( 4 | AnyOf, 5 | Provider, 6 | Scope, 7 | ValidationSettings, 8 | make_container, 9 | provide, 10 | provide_all, 11 | ) 12 | 13 | 14 | class A1: 15 | pass 16 | 17 | 18 | class A2: 19 | pass 20 | 21 | 22 | class BProto(Protocol): 23 | pass 24 | 25 | 26 | class B(BProto): 27 | def __init__(self, a1: A1, a2: A2): 28 | self.a1 = a1 29 | self.a2 = a2 30 | 31 | 32 | class C(B): 33 | pass 34 | 35 | 36 | def test_provide_class(): 37 | class MyProvider(Provider): 38 | x = provide(B, scope=Scope.APP, recursive=True) 39 | 40 | container = make_container(MyProvider()) 41 | b = container.get(B) 42 | assert isinstance(b, B) 43 | assert isinstance(b.a1, A1) 44 | assert isinstance(b.a2, A2) 45 | 46 | 47 | def test_provide_instance(): 48 | provider = Provider(scope=Scope.APP) 49 | provider.provide(B, recursive=True) 50 | container = make_container(provider) 51 | b = container.get(B) 52 | assert isinstance(b, B) 53 | assert isinstance(b.a1, A1) 54 | assert isinstance(b.a2, A2) 55 | 56 | 57 | def test_provide_any_of(): 58 | provider = Provider(scope=Scope.APP) 59 | provider.provide(source=B, provides=AnyOf[B, BProto], recursive=True) 60 | container = make_container(provider) 61 | b = container.get(B) 62 | assert isinstance(b, B) 63 | assert isinstance(b.a1, A1) 64 | assert isinstance(b.a2, A2) 65 | assert b is container.get(BProto) 66 | 67 | 68 | def test_provide_all_class(): 69 | class MyProvider(Provider): 70 | x = provide_all(B, C, scope=Scope.APP, recursive=True) 71 | 72 | container = make_container( 73 | MyProvider(), 74 | validation_settings=ValidationSettings( 75 | implicit_override=False, 76 | ), 77 | ) 78 | b = container.get(B) 79 | assert isinstance(b, B) 80 | assert isinstance(b.a1, A1) 81 | assert isinstance(b.a2, A2) 82 | c = container.get(C) 83 | assert isinstance(c, B) 84 | assert isinstance(c.a1, A1) 85 | assert isinstance(c.a2, A2) 86 | 87 | 88 | def test_provide_all_instance(): 89 | provider = Provider(scope=Scope.APP) 90 | provider.provide_all(B, C, recursive=True) 91 | container = make_container( 92 | provider, 93 | validation_settings=ValidationSettings( 94 | implicit_override=False, 95 | ), 96 | ) 97 | b = container.get(B) 98 | assert isinstance(b, B) 99 | assert isinstance(b.a1, A1) 100 | assert isinstance(b.a2, A2) 101 | c = container.get(C) 102 | assert isinstance(c, B) 103 | assert isinstance(c.a1, A1) 104 | assert isinstance(c.a2, A2) 105 | --------------------------------------------------------------------------------