├── tests ├── __init__.py ├── async │ ├── __init__.py │ ├── test_class.py │ └── test_cast.py ├── sync │ ├── __init__.py │ ├── test_class.py │ └── test_cast.py ├── library │ ├── __init__.py │ └── test_custom.py ├── serializers │ ├── __init__.py │ ├── msgspec │ │ ├── __init__.py │ │ ├── test_encode.py │ │ └── test_custom_type.py │ ├── pydantic │ │ ├── __init__.py │ │ ├── test_overrides.py │ │ └── test_encode.py │ └── params.py ├── pydantic_specific │ ├── async │ │ ├── __init__.py │ │ ├── test_config.py │ │ └── test_cast.py │ ├── sync │ │ ├── __init__.py │ │ ├── test_config.py │ │ └── test_cast.py │ ├── __init__.py │ ├── test_locals.py │ ├── wrapper.py │ ├── test_custom.py │ └── test_prebuild.py ├── test_typealiastype_depends │ ├── __init__.py │ └── test_typealiastype_depends.py ├── conftest.py ├── test_no_serializer.py ├── test_utils.py ├── marks.py ├── test_params.py └── test_overrides.py ├── fast_depends ├── py.typed ├── msgspec │ ├── __init__.py │ └── serializer.py ├── pydantic │ ├── __init__.py │ ├── schema.py │ ├── _compat.py │ └── serializer.py ├── dependencies │ ├── __init__.py │ ├── model.py │ └── provider.py ├── core │ ├── __init__.py │ └── builder.py ├── library │ ├── __init__.py │ ├── model.py │ └── serializer.py ├── __about__.py ├── __init__.py ├── exceptions.py ├── _compat.py ├── utils.py └── use.py ├── scripts ├── test.sh ├── test-cov.sh └── lint.sh ├── .github ├── FUNDING.yml ├── workflows │ ├── documentation.yml │ ├── automerge.yml │ ├── publish_pypi.yml │ ├── publish_coverage.yml │ ├── relator.yml │ └── tests.yml └── dependabot.yml ├── docs ├── docs_src │ ├── tutorial_3_yield │ │ ├── tutorial_1.py │ │ └── tutorial_2.py │ ├── advanced │ │ └── custom │ │ │ ├── usage.py │ │ │ ├── class_declaration.py │ │ │ ├── cast_arg.py │ │ │ └── starlette.py │ ├── tutorial_4_annotated │ │ ├── annotated_variants_39.py │ │ ├── not_annotated.py │ │ └── annotated_39.py │ ├── tutorial_1_quickstart │ │ ├── 1_sync.py │ │ ├── 2_sync.py │ │ ├── 1_async.py │ │ └── 2_async.py │ ├── tutorial_2_classes │ │ ├── tutorial_4.py │ │ ├── tutorial_2.py │ │ ├── tutorial_5.py │ │ ├── tutorial_3.py │ │ └── tutorial_1.py │ ├── home │ │ ├── 1_sync_tutor.py │ │ └── 1_async_tutor.py │ ├── usages │ │ ├── flask.py │ │ └── starlette.py │ ├── tutorial_5_overrides │ │ ├── example.py │ │ └── fixture.py │ └── how-it-works │ │ └── works.py ├── docs │ ├── usages.md │ ├── tutorial │ │ ├── yield.md │ │ ├── classes.md │ │ ├── overrides.md │ │ ├── validations.md │ │ ├── index.md │ │ └── annotated.md │ ├── works.md │ ├── advanced │ │ ├── starlette.md │ │ └── index.md │ ├── contributing.md │ ├── assets │ │ ├── stylesheets │ │ │ ├── termynal.css │ │ │ └── custom.css │ │ └── javascripts │ │ │ ├── custom.js │ │ │ └── termynal.js │ ├── alternatives.md │ └── index.md └── mkdocs.yml ├── .gitignore ├── LICENSE ├── CONTRIBUTING.md ├── pyproject.toml └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fast_depends/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/async/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/sync/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/library/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/serializers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | coverage run -m pytest "$@" -------------------------------------------------------------------------------- /tests/pydantic_specific/async/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/pydantic_specific/sync/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | thanks_dev: gh/lancetnik 2 | custom: [https://pay.cloudtips.ru/p/0558c54a] -------------------------------------------------------------------------------- /tests/pydantic_specific/__init__.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytest.importorskip("pydantic") 4 | -------------------------------------------------------------------------------- /tests/serializers/msgspec/__init__.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytest.importorskip("msgspec") 4 | -------------------------------------------------------------------------------- /tests/serializers/pydantic/__init__.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytest.importorskip("pydantic") 4 | -------------------------------------------------------------------------------- /scripts/test-cov.sh: -------------------------------------------------------------------------------- 1 | bash scripts/test.sh "$@" 2 | coverage combine 3 | coverage report --show-missing 4 | 5 | rm .coverage* -------------------------------------------------------------------------------- /docs/docs_src/tutorial_3_yield/tutorial_1.py: -------------------------------------------------------------------------------- 1 | def dependency(): 2 | db = DBSession() 3 | yield db 4 | db.close() 5 | -------------------------------------------------------------------------------- /fast_depends/msgspec/__init__.py: -------------------------------------------------------------------------------- 1 | from fast_depends.msgspec.serializer import MsgSpecSerializer 2 | 3 | __all__ = ("MsgSpecSerializer",) 4 | -------------------------------------------------------------------------------- /fast_depends/pydantic/__init__.py: -------------------------------------------------------------------------------- 1 | from fast_depends.pydantic.serializer import PydanticSerializer 2 | 3 | __all__ = ("PydanticSerializer",) 4 | -------------------------------------------------------------------------------- /docs/docs_src/tutorial_3_yield/tutorial_2.py: -------------------------------------------------------------------------------- 1 | def dependency(): 2 | db = DBSession() 3 | try: 4 | yield db 5 | finally: 6 | db.close() 7 | -------------------------------------------------------------------------------- /fast_depends/dependencies/__init__.py: -------------------------------------------------------------------------------- 1 | from .model import Dependant 2 | from .provider import Provider 3 | 4 | __all__ = ( 5 | "Dependant", 6 | "Provider", 7 | ) 8 | -------------------------------------------------------------------------------- /fast_depends/core/__init__.py: -------------------------------------------------------------------------------- 1 | from .builder import build_call_model 2 | from .model import CallModel 3 | 4 | __all__ = ( 5 | "CallModel", 6 | "build_call_model", 7 | ) 8 | -------------------------------------------------------------------------------- /tests/test_typealiastype_depends/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import pytest 3 | 4 | if sys.version_info < (3, 12): 5 | pytest.skip("Test for Python >= 3.12",allow_module_level=True) 6 | -------------------------------------------------------------------------------- /fast_depends/library/__init__.py: -------------------------------------------------------------------------------- 1 | from fast_depends.library.model import CustomField 2 | from fast_depends.library.serializer import Serializer 3 | 4 | __all__ = ( 5 | "CustomField", 6 | "Serializer", 7 | ) 8 | -------------------------------------------------------------------------------- /fast_depends/__about__.py: -------------------------------------------------------------------------------- 1 | """FastDepends - extracted and cleared from HTTP domain FastAPI Dependency Injection System""" 2 | 3 | from importlib.metadata import version 4 | 5 | __version__ = version("fast_depends") 6 | -------------------------------------------------------------------------------- /docs/docs_src/advanced/custom/usage.py: -------------------------------------------------------------------------------- 1 | from fast_depends import inject 2 | 3 | @inject 4 | def my_func(header_field: int = Header()): 5 | return header_field 6 | 7 | assert h( 8 | headers={"header_field": "1"} 9 | ) == 1 10 | -------------------------------------------------------------------------------- /docs/docs_src/tutorial_4_annotated/annotated_variants_39.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | from pydantic import Field 3 | from fast_depends import Depends 4 | 5 | CurrentUser = Annotated[User, Depends(get_user)] 6 | MaxLenField = Annotated[str, Field(..., max_length="32")] 7 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from fast_depends.dependencies.provider import Provider 4 | 5 | 6 | @pytest.fixture 7 | def anyio_backend() -> str: 8 | return "asyncio" 9 | 10 | 11 | @pytest.fixture 12 | def provider() -> Provider: 13 | return Provider() 14 | -------------------------------------------------------------------------------- /docs/docs_src/tutorial_1_quickstart/1_sync.py: -------------------------------------------------------------------------------- 1 | from fast_depends import Depends, inject 2 | 3 | def simple_dependency(a: int, b: int = 3): 4 | return a + b 5 | 6 | @inject 7 | def method(a: int, d: int = Depends(simple_dependency)): 8 | return a + d 9 | 10 | assert method("1") == 5 11 | -------------------------------------------------------------------------------- /tests/test_no_serializer.py: -------------------------------------------------------------------------------- 1 | from fast_depends import inject 2 | 3 | 4 | def test_generator(): 5 | @inject(serializer_cls=None) 6 | def simple_func(a: str) -> str: 7 | for _ in range(2): 8 | yield a 9 | 10 | for i in simple_func(1): 11 | assert i == 1 12 | -------------------------------------------------------------------------------- /docs/docs_src/advanced/custom/class_declaration.py: -------------------------------------------------------------------------------- 1 | from fast_depends.library import CustomField 2 | 3 | class Header(CustomField): 4 | def use(self, /, **kwargs): 5 | kwargs = super().use(**kwargs) 6 | kwargs[self.param_name] = kwargs["headers"][self.param_name] 7 | return kwargs 8 | -------------------------------------------------------------------------------- /docs/docs_src/tutorial_2_classes/tutorial_4.py: -------------------------------------------------------------------------------- 1 | from fast_depends import Depends, inject 2 | 3 | class MyDependency: 4 | @staticmethod 5 | def dep(a: int): 6 | return a ** 2 7 | 8 | @inject 9 | def func(d: int = Depends(MyDependency.dep)): 10 | return d 11 | 12 | assert func(a=3) == 9 13 | -------------------------------------------------------------------------------- /docs/docs_src/home/1_sync_tutor.py: -------------------------------------------------------------------------------- 1 | from fast_depends import Depends, inject 2 | 3 | def dependency(a: int) -> int: 4 | return a 5 | 6 | @inject 7 | def main( 8 | a: int, 9 | b: int, 10 | c: int = Depends(dependency) 11 | ) -> float: 12 | return a + b + c 13 | 14 | assert main("1", 2) == 4.0 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | __pycache__ 3 | .vscode 4 | .pypirc 5 | venv 6 | .venv 7 | app 8 | serve.py 9 | test.py 10 | poetry.lock 11 | docker-compose.yaml 12 | .pytest_cache 13 | poetry.lock 14 | .coverage* 15 | htmlcov 16 | wtf 17 | .ruff_cache 18 | .mypy_cache 19 | coverage.json 20 | site 21 | wtf.py 22 | .DS_Store 23 | .idea/ -------------------------------------------------------------------------------- /docs/docs_src/tutorial_2_classes/tutorial_2.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from fast_depends import Depends, inject 4 | 5 | class MyDependency: 6 | def __init__(self, a: int): 7 | self.field = a 8 | 9 | @inject 10 | def func(d: Any = Depends(MyDependency)): 11 | return d.field 12 | 13 | assert func(a=3) == 3 14 | -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | echo "Running mypy..." 2 | mypy fast_depends 3 | 4 | echo "Running ruff linter (isort, flake, pyupgrade, etc. replacement)..." 5 | ruff check fast_depends tests --fix 6 | 7 | echo "Running ruff formatter (black replacement)..." 8 | ruff format fast_depends tests 9 | 10 | echo "Running codespell to find typos..." 11 | codespell 12 | -------------------------------------------------------------------------------- /docs/docs_src/home/1_async_tutor.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from fast_depends import Depends, inject 4 | 5 | async def dependency(a: int) -> int: 6 | return a 7 | 8 | @inject 9 | async def main( 10 | a: int, 11 | b: int, 12 | c: int = Depends(dependency) 13 | ) -> float: 14 | return a + b + c 15 | 16 | assert asyncio.run(main("1", 2)) == 4.0 17 | -------------------------------------------------------------------------------- /docs/docs_src/tutorial_2_classes/tutorial_5.py: -------------------------------------------------------------------------------- 1 | from fast_depends import Depends, inject 2 | 3 | class MyDependency: 4 | def __init__(self, a): 5 | self.field = a 6 | 7 | def dep(self, a: int): 8 | return self.field + a 9 | 10 | @inject 11 | def func(d: int = Depends(MyDependency(3).dep)): 12 | return d 13 | 14 | assert func(a=3) == 6 15 | -------------------------------------------------------------------------------- /docs/docs_src/tutorial_2_classes/tutorial_3.py: -------------------------------------------------------------------------------- 1 | from fast_depends import Depends, inject 2 | 3 | class MyDependency: 4 | def __init__(self, a: int): 5 | self.field = a 6 | 7 | def __call__(self, b: int): 8 | return self.field + b 9 | 10 | @inject 11 | def func(d: int = Depends(MyDependency(3))): 12 | return d 13 | 14 | assert func(b=3) == 6 15 | -------------------------------------------------------------------------------- /docs/docs_src/usages/flask.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from pydantic import Field 3 | 4 | from fast_depends import Depends, inject 5 | 6 | app = Flask(__name__) 7 | 8 | def get_user(user_id: int = Field(..., alias="id")): 9 | return f"user {user_id}" 10 | 11 | @app.get("/") 12 | @inject 13 | def hello(user: str = Depends(get_user)): 14 | return f"

Hello, {user}!

" 15 | -------------------------------------------------------------------------------- /fast_depends/__init__.py: -------------------------------------------------------------------------------- 1 | from fast_depends.dependencies import Provider 2 | from fast_depends.exceptions import ValidationError 3 | from fast_depends.use import Depends, inject 4 | from fast_depends.use import global_provider as dependency_provider 5 | 6 | __all__ = ( 7 | "Depends", 8 | "ValidationError", 9 | "Provider", 10 | "dependency_provider", 11 | "inject", 12 | ) 13 | -------------------------------------------------------------------------------- /docs/docs_src/advanced/custom/cast_arg.py: -------------------------------------------------------------------------------- 1 | class Header(CustomField): 2 | def __init__(self): 3 | super().__init__(cast=True) 4 | 5 | class NotCastHeader(CustomField): 6 | def __init__(self): 7 | super().__init__(cast=False) 8 | 9 | def func( 10 | h1: int = Header(), # <-- casts to int 11 | h2: int = NotCastHeader() # <-- just an annotation 12 | ): ... 13 | -------------------------------------------------------------------------------- /tests/pydantic_specific/test_locals.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pydantic import BaseModel 4 | 5 | from fast_depends import inject 6 | 7 | 8 | def wrap(func): 9 | return inject(func) 10 | 11 | 12 | def test_localns(): 13 | class M(BaseModel): 14 | a: str 15 | 16 | @wrap 17 | def m(a: M) -> M: 18 | return a 19 | 20 | m(a={"a": "Hi!"}) 21 | -------------------------------------------------------------------------------- /tests/pydantic_specific/wrapper.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Callable 4 | from functools import wraps 5 | from typing import Any 6 | 7 | 8 | def noop_wrap(func: Callable[..., Any]) -> Callable[..., Any]: 9 | @wraps(func) 10 | def wrapper(*args: Any, **kwargs: Any) -> Any: 11 | return func(*args, **kwargs) 12 | 13 | return wrapper 14 | -------------------------------------------------------------------------------- /tests/sync/test_class.py: -------------------------------------------------------------------------------- 1 | from fast_depends import Depends, inject 2 | 3 | 4 | def _get_var(): 5 | return 1 6 | 7 | 8 | class Class: 9 | @inject 10 | def __init__(self, a=Depends(_get_var)) -> None: 11 | self.a = a 12 | 13 | @inject 14 | def calc(self, a=Depends(_get_var)) -> int: 15 | return a + self.a 16 | 17 | 18 | def test_class(): 19 | assert Class().calc() == 2 20 | -------------------------------------------------------------------------------- /docs/docs_src/tutorial_1_quickstart/2_sync.py: -------------------------------------------------------------------------------- 1 | from fast_depends import Depends, inject 2 | 3 | def another_dependency(a: int): 4 | return a * 2 5 | 6 | def simple_dependency(a: int, b: int = Depends(another_dependency)): # (1) 7 | return a + b 8 | 9 | @inject 10 | def method( 11 | a: int, 12 | b: int = Depends(another_dependency), 13 | c: int = Depends(simple_dependency) 14 | ): 15 | return a + b + c 16 | 17 | assert method("1") == 6 18 | -------------------------------------------------------------------------------- /docs/docs_src/tutorial_4_annotated/not_annotated.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, PositiveInt 2 | 3 | from fast_depends import Depends, inject 4 | 5 | class User(BaseModel): 6 | user_id: PositiveInt 7 | 8 | def get_user(user: id) -> User: 9 | return User(user_id=user) 10 | 11 | @inject 12 | def do_smth_with_user(user: User = Depends(get_user)): 13 | ... 14 | 15 | @inject 16 | def do_another_smth_with_user(user: User = Depends(get_user)): 17 | ... 18 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import AsyncMock 2 | 3 | from fast_depends.utils import is_coroutine_callable 4 | 5 | 6 | def test_is_coroutine_callable() -> None: 7 | async def coroutine_func() -> int: 8 | return 1 9 | 10 | assert is_coroutine_callable(coroutine_func) 11 | 12 | def sync_func() -> int: 13 | return 1 14 | 15 | assert not is_coroutine_callable(sync_func) 16 | 17 | assert is_coroutine_callable(AsyncMock()) 18 | -------------------------------------------------------------------------------- /docs/docs_src/tutorial_1_quickstart/1_async.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from fast_depends import Depends, inject 4 | 5 | async def simple_dependency(a: int, b: int = 3): 6 | return a + b 7 | 8 | def another_dependency(a: int): 9 | return a 10 | 11 | @inject 12 | async def method( 13 | a: int, 14 | b: int = Depends(simple_dependency), 15 | c: int = Depends(another_dependency), 16 | ): 17 | return a + b + c 18 | 19 | assert asyncio.run(method("1")) == 6 20 | -------------------------------------------------------------------------------- /tests/async/test_class.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from fast_depends import Depends, inject 4 | 5 | 6 | def _get_var(): 7 | return 1 8 | 9 | 10 | class Class: 11 | @inject 12 | def __init__(self, a=Depends(_get_var)) -> None: 13 | self.a = a 14 | 15 | @inject 16 | async def calc(self, a=Depends(_get_var)) -> int: 17 | return a + self.a 18 | 19 | 20 | @pytest.mark.anyio 21 | async def test_class(): 22 | assert await Class().calc() == 2 23 | -------------------------------------------------------------------------------- /docs/docs_src/tutorial_2_classes/tutorial_1.py: -------------------------------------------------------------------------------- 1 | class MyClass: pass 2 | 3 | MyClass() # It is a "call"! 1-st call 4 | 5 | 6 | class MyClass: 7 | def __call__(): pass 8 | 9 | m = MyClass() 10 | m() # It is a "call" too! 2-nd call 11 | 12 | 13 | class MyClass: 14 | @classmethod 15 | def f(): pass 16 | 17 | MyClass.f() # Yet another "call"! 3-rd call 18 | 19 | 20 | class MyClass 21 | def f(self): pass 22 | 23 | MyClass().f() # "call"? 4-th call 24 | -------------------------------------------------------------------------------- /docs/docs_src/tutorial_1_quickstart/2_async.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from fast_depends import Depends, inject 4 | 5 | def another_dependency(a: int): 6 | return a * 2 7 | 8 | async def simple_dependency(a: int, b: int = Depends(another_dependency)): # (1) 9 | return a + b 10 | 11 | @inject 12 | async def method( 13 | a: int, 14 | b: int = Depends(simple_dependency), 15 | c: int = Depends(another_dependency), 16 | ): 17 | return a + b + c 18 | 19 | assert asyncio.run(method("1")) == 6 20 | -------------------------------------------------------------------------------- /docs/docs_src/tutorial_5_overrides/example.py: -------------------------------------------------------------------------------- 1 | from fast_depends import Depends, dependency_provider, inject 2 | 3 | def original_dependency(): 4 | raise NotImplementedError() 5 | 6 | def override_dependency(): 7 | return 1 8 | 9 | @inject 10 | def func(d = Depends(original_dependency)): 11 | return d 12 | 13 | dependency_provider.override(original_dependency, override_dependency) 14 | # or 15 | dependency_provider[original_dependency] = override_dependency 16 | 17 | def test(): 18 | assert func() == 1 19 | -------------------------------------------------------------------------------- /docs/docs_src/tutorial_4_annotated/annotated_39.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | from pydantic import BaseModel, PositiveInt 3 | from fast_depends import Depends, inject 4 | 5 | class User(BaseModel): 6 | user_id: PositiveInt 7 | 8 | def get_user(user: id) -> User: 9 | return User(user_id=user) 10 | 11 | CurrentUser = Annotated[User, Depends(get_user)] 12 | 13 | @inject 14 | def do_smth_with_user(user: CurrentUser): 15 | ... 16 | 17 | @inject 18 | def do_another_smth_with_user(user: CurrentUser): 19 | ... 20 | -------------------------------------------------------------------------------- /docs/docs_src/tutorial_5_overrides/fixture.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from fast_depends import Depends, dependency_provider, inject 4 | 5 | # Base code 6 | def base_dep(): 7 | return 1 8 | 9 | def override_dep(): 10 | return 2 11 | 12 | @inject 13 | def func(d = Depends(base_dep)): 14 | return d 15 | 16 | # Tests 17 | @pytest.fixture 18 | def provider(): 19 | yield dependency_provider 20 | dependency_provider.clear() # (1)! 21 | 22 | def test_sync_overide(provider): 23 | provider.override(base_dep, override_dep) 24 | assert func() == 2 25 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - docs/** 9 | - .github/workflows/documentation.yml 10 | - fast_depends/__about__.py 11 | 12 | permissions: 13 | contents: write 14 | 15 | jobs: 16 | deploy: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v6 20 | - uses: actions/setup-python@v6 21 | with: 22 | python-version: 3.x 23 | - uses: actions/cache@v5 24 | with: 25 | key: ${{ github.ref }} 26 | path: .cache 27 | - run: pip install --group docs . 28 | - working-directory: ./docs 29 | run: mkdocs gh-deploy --force -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | # GitHub Actions 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" 13 | groups: 14 | github-actions: 15 | patterns: 16 | - "*" 17 | # Python 18 | - package-ecosystem: "uv" 19 | directory: "/" 20 | schedule: 21 | interval: "weekly" 22 | groups: 23 | pip: 24 | patterns: 25 | - "*" 26 | -------------------------------------------------------------------------------- /tests/serializers/msgspec/test_encode.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | import pytest 4 | from msgspec import Struct 5 | 6 | from fast_depends.msgspec.serializer import MsgSpecSerializer 7 | from tests.serializers.params import comptex_params, parametrized 8 | 9 | 10 | class SimpleStruct(Struct): 11 | r: str 12 | 13 | 14 | @pytest.mark.parametrize( 15 | ("message", "expected_message"), 16 | ( 17 | *parametrized, 18 | *comptex_params, 19 | pytest.param( 20 | SimpleStruct(r="hello!"), 21 | b'{"r":"hello!"}', 22 | id="struct", 23 | ), 24 | ), 25 | ) 26 | def test_encode( 27 | message: Any, 28 | expected_message: bytes, 29 | ) -> None: 30 | msg = MsgSpecSerializer.encode(message) 31 | assert msg == expected_message 32 | -------------------------------------------------------------------------------- /docs/docs_src/usages/starlette.py: -------------------------------------------------------------------------------- 1 | # Is that FastAPI??? 2 | from pydantic import Field 3 | from starlette.applications import Starlette 4 | from starlette.responses import PlainTextResponse 5 | from starlette.routing import Route 6 | 7 | from fast_depends import Depends, inject 8 | 9 | def unwrap_path(func): 10 | async def wrapper(request): # unwrap incoming params to **kwargs here 11 | return await func(**request.path_params) 12 | return wrapper 13 | 14 | async def get_user(user_id: int = Field(..., alias="id")): 15 | return f"user {user_id}" 16 | 17 | @unwrap_path 18 | @inject # cast incoming kwargs here 19 | async def hello(user: str = Depends(get_user)): 20 | return PlainTextResponse(f"Hello, {user}!") 21 | 22 | app = Starlette(debug=True, routes=[ 23 | Route("/{id}", hello) 24 | ]) 25 | -------------------------------------------------------------------------------- /fast_depends/dependencies/model.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from inspect import unwrap 3 | from typing import Any 4 | 5 | 6 | class Dependant: 7 | use_cache: bool 8 | cast: bool 9 | 10 | def __init__( 11 | self, 12 | dependency: Callable[..., Any], 13 | *, 14 | use_cache: bool, 15 | cast: bool, 16 | cast_result: bool, 17 | ) -> None: 18 | self.dependency = dependency 19 | self.use_cache = use_cache 20 | self.cast = cast 21 | self.cast_result = cast_result 22 | 23 | def __repr__(self) -> str: 24 | call = unwrap(self.dependency) 25 | attr = getattr(call, "__name__", type(call).__name__) 26 | cache = "" if self.use_cache else ", use_cache=False" 27 | return f"{self.__class__.__name__}({attr}{cache})" 28 | -------------------------------------------------------------------------------- /tests/pydantic_specific/sync/test_config.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from fast_depends import Depends, Provider, inject 4 | from fast_depends.exceptions import ValidationError 5 | from fast_depends.pydantic import PydanticSerializer 6 | from tests.marks import PYDANTIC_V2 7 | 8 | 9 | def dep(a: str): 10 | return a 11 | 12 | 13 | @inject( 14 | serializer_cls=PydanticSerializer( 15 | {"str_max_length" if PYDANTIC_V2 else "max_anystr_length": 1}, 16 | use_fastdepends_errors=True, 17 | ), 18 | dependency_provider=Provider(), 19 | ) 20 | def limited_str(a=Depends(dep)): ... 21 | 22 | 23 | @inject(dependency_provider=Provider()) 24 | def regular(a=Depends(dep)): 25 | return a 26 | 27 | 28 | def test_config(): 29 | regular("123") 30 | 31 | with pytest.raises(ValidationError): 32 | limited_str("123") 33 | -------------------------------------------------------------------------------- /tests/test_typealiastype_depends/test_typealiastype_depends.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | 3 | import pytest 4 | 5 | from fast_depends import Depends, inject 6 | 7 | 8 | @pytest.mark.anyio 9 | async def test_typealiastype_depends_async() -> None: 10 | 11 | async def dep_func(b): 12 | return b 13 | 14 | type D = Annotated[int, Depends(dep_func)] 15 | 16 | @inject 17 | async def some_async_func(a: int, b: D) -> int: 18 | assert isinstance(b, int) 19 | return a + b 20 | 21 | assert await some_async_func(1, 2) == 3 22 | 23 | 24 | def test_typealiastype_depends_sync() -> None: 25 | def dep_func(b): 26 | return b 27 | 28 | type D = Annotated[int, Depends(dep_func)] 29 | 30 | @inject 31 | def some_func(a: int, b: D) -> int: 32 | assert isinstance(b, int) 33 | return a + b 34 | 35 | assert some_func(1, 2) == 3 36 | -------------------------------------------------------------------------------- /docs/docs_src/advanced/custom/starlette.py: -------------------------------------------------------------------------------- 1 | from starlette.applications import Starlette 2 | from starlette.responses import PlainTextResponse 3 | from starlette.routing import Route 4 | 5 | from fast_depends import inject 6 | from fast_depends.library import CustomField 7 | 8 | class Path(CustomField): 9 | def use(self, /, *, request, **kwargs): 10 | return { 11 | **super().use(request=request, **kwargs), 12 | self.param_name: request.path_params.get(self.param_name) 13 | } 14 | 15 | def wrap_starlette(func): 16 | async def wrapper(request): 17 | return await inject(func)( 18 | request=request 19 | ) 20 | return wrapper 21 | 22 | @wrap_starlette 23 | async def hello(user: str = Path()): 24 | return PlainTextResponse(f"Hello, {user}!") 25 | 26 | app = Starlette(debug=True, routes=[ 27 | Route("/{user}", hello) 28 | ]) 29 | -------------------------------------------------------------------------------- /docs/docs_src/how-it-works/works.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | from fast_depends import Depends 4 | 5 | def simple_dependency(a: int, **kwargs): 6 | return a 7 | 8 | def my_function(a: int, b: int, d = Depends(simple_dependency)) -> float: 9 | return a + b + d 10 | 11 | # Declare function representation model 12 | class MyFunctionRepresentation(BaseModel): 13 | a: int # used twice: for original function and dependency 14 | b: int 15 | 16 | kwargs = {"a": 1, "b": "3"} 17 | 18 | # Cast incomint arguments 19 | arguments_model = MyFunctionRepresentation(**kwargs) 20 | 21 | # Use them 22 | new_kwargs = arguments_model.model_dump() 23 | base_response = my_function( 24 | **new_kwargs, 25 | d=simple_dependency(**new_kwargs) 26 | ) 27 | 28 | class ResponseModel(BaseModel): 29 | field: float 30 | 31 | # Cast response 32 | real_response = ResponseModel(field=base_response).field 33 | -------------------------------------------------------------------------------- /tests/pydantic_specific/async/test_config.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from fast_depends import Depends, Provider, inject 4 | from fast_depends.exceptions import ValidationError 5 | from fast_depends.pydantic import PydanticSerializer 6 | from tests.marks import PYDANTIC_V2 7 | 8 | 9 | async def dep(a: str): 10 | return a 11 | 12 | 13 | @inject( 14 | serializer_cls=PydanticSerializer( 15 | {"str_max_length" if PYDANTIC_V2 else "max_anystr_length": 1}, 16 | use_fastdepends_errors=True, 17 | ), 18 | dependency_provider=Provider(), 19 | ) 20 | async def limited_str(a=Depends(dep)): ... 21 | 22 | 23 | @inject(dependency_provider=Provider()) 24 | async def regular(a=Depends(dep)): 25 | return a 26 | 27 | 28 | @pytest.mark.anyio 29 | async def test_config() -> None: 30 | await regular("123") 31 | 32 | with pytest.raises(ValidationError): 33 | await limited_str("123") 34 | -------------------------------------------------------------------------------- /tests/pydantic_specific/test_custom.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated, Any 2 | 3 | import pytest 4 | from annotated_types import Ge 5 | 6 | from fast_depends import inject 7 | from fast_depends.exceptions import ValidationError 8 | from fast_depends.library import CustomField 9 | from tests.marks import pydanticV2 10 | 11 | 12 | class Header(CustomField): 13 | def use(self, /, **kwargs: Any) -> dict[str, Any]: 14 | kwargs = super().use(**kwargs) 15 | if v := kwargs.get("headers", {}).get(self.param_name): 16 | kwargs[self.param_name] = v 17 | return kwargs 18 | 19 | 20 | @pydanticV2 21 | def test_annotated_header_with_meta() -> None: 22 | @inject() 23 | def sync_catch(key: Annotated[int, Header(), Ge(3)] = 3) -> int: 24 | return key 25 | 26 | assert sync_catch(headers={"key": "4"}) == 4 27 | 28 | assert sync_catch(headers={}) == 3 29 | 30 | with pytest.raises(ValidationError): 31 | sync_catch(headers={"key": "2"}) 32 | -------------------------------------------------------------------------------- /tests/marks.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | try: 4 | from fast_depends.pydantic._compat import PYDANTIC_V2 5 | 6 | HAS_PYDANTIC = True 7 | 8 | except ImportError: 9 | HAS_PYDANTIC = False 10 | PYDANTIC_V2 = False 11 | 12 | try: 13 | from fast_depends.msgspec import MsgSpecSerializer # noqa: F401 14 | 15 | HAS_MSGSPEC = True 16 | except ImportError: 17 | HAS_MSGSPEC = False 18 | 19 | 20 | serializer = pytest.mark.skipif( 21 | not HAS_MSGSPEC and not HAS_PYDANTIC, reason="requires serializer" 22 | ) # noqa: N816 23 | 24 | msgspec = pytest.mark.skipif(not HAS_MSGSPEC, reason="requires Msgspec") # noqa: N816 25 | 26 | pydantic = pytest.mark.skipif(not HAS_PYDANTIC, reason="requires Pydantic") # noqa: N816 27 | 28 | pydanticV1 = pytest.mark.skipif( 29 | not HAS_PYDANTIC or PYDANTIC_V2, reason="requires PydanticV1" 30 | ) # noqa: N816 31 | 32 | pydanticV2 = pytest.mark.skipif( 33 | not HAS_PYDANTIC or not PYDANTIC_V2, reason="requires PydanticV2" 34 | ) # noqa: N816 35 | -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | name: Automerge 2 | 3 | on: 4 | pull_request_target: 5 | 6 | jobs: 7 | automerge: 8 | name: Enable pull request automerge 9 | runs-on: ubuntu-latest 10 | if: github.event.pull_request.user.login == 'dependabot[bot]' 11 | 12 | permissions: 13 | pull-requests: write 14 | contents: write 15 | 16 | steps: 17 | - uses: alexwilson/enable-github-automerge-action@56e3117d1ae1540309dc8f7a9f2825bc3c5f06ff # 2.0.0 18 | with: 19 | github-token: ${{ secrets.AUTOMERGE_TOKEN }} 20 | merge-method: REBASE 21 | 22 | autoapprove: 23 | name: Automatically approve pull request 24 | needs: [automerge] 25 | runs-on: ubuntu-latest 26 | if: github.event.pull_request.user.login == 'dependabot[bot]' 27 | 28 | permissions: 29 | pull-requests: write 30 | 31 | steps: 32 | - uses: hmarr/auto-approve-action@f0939ea97e9205ef24d872e76833fa908a770363 # v4.0.0 33 | with: 34 | github-token: ${{ secrets.AUTOMERGE_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/publish_pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | workflow_dispatch: null 5 | push: 6 | tags: 7 | - "*" 8 | 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | id-token: write 14 | 15 | steps: 16 | - name: Dump GitHub context 17 | env: 18 | GITHUB_CONTEXT: ${{ toJson(github) }} 19 | run: echo "$GITHUB_CONTEXT" 20 | 21 | - uses: actions/checkout@v6 22 | 23 | - name: Set up Python 24 | uses: actions/setup-python@v6 25 | with: 26 | python-version: "3.13" 27 | 28 | - name: Install build dependencies 29 | run: pip install build 30 | 31 | - name: Build distribution 32 | run: python -m build 33 | 34 | - name: Publish 35 | uses: pypa/gh-action-pypi-publish@release/v1 36 | with: 37 | skip-existing: true 38 | 39 | - name: Dump GitHub context 40 | env: 41 | GITHUB_CONTEXT: ${{ toJson(github) }} 42 | run: echo "$GITHUB_CONTEXT" 43 | -------------------------------------------------------------------------------- /.github/workflows/publish_coverage.yml: -------------------------------------------------------------------------------- 1 | name: Smokeshow 2 | 3 | on: 4 | workflow_run: 5 | workflows: [Test] 6 | types: [completed] 7 | 8 | permissions: 9 | statuses: write 10 | 11 | jobs: 12 | smokeshow: 13 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/setup-python@v6 18 | with: 19 | python-version: '3.10' 20 | 21 | - run: pip install smokeshow 22 | 23 | - uses: dawidd6/action-download-artifact@v11 24 | with: 25 | workflow: tests.yml 26 | commit: ${{ github.event.workflow_run.head_sha }} 27 | 28 | - run: smokeshow upload coverage-html 29 | env: 30 | SMOKESHOW_GITHUB_STATUS_DESCRIPTION: Coverage {coverage-percentage} 31 | SMOKESHOW_GITHUB_COVERAGE_THRESHOLD: 100 32 | SMOKESHOW_GITHUB_CONTEXT: coverage 33 | SMOKESHOW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | SMOKESHOW_GITHUB_PR_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} 35 | SMOKESHOW_AUTH_KEY: ${{ secrets.SMOKESHOW_AUTH_KEY }} 36 | -------------------------------------------------------------------------------- /tests/serializers/params.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import datetime, timezone 3 | 4 | import pytest 5 | 6 | 7 | @dataclass 8 | class SimpleDataclass: 9 | r: str 10 | 11 | 12 | now = datetime.now(timezone.utc) 13 | 14 | parametrized = ( 15 | pytest.param( 16 | "hello", 17 | b'"hello"', 18 | id="str", 19 | ), 20 | pytest.param( 21 | b"hello", 22 | b"hello", 23 | id="bytes", 24 | ), 25 | pytest.param( 26 | 1.0, 27 | b"1.0", 28 | id="float", 29 | ), 30 | pytest.param( 31 | 1, 32 | b"1", 33 | id="int", 34 | ), 35 | pytest.param( 36 | False, 37 | b"false", 38 | id="bool", 39 | ), 40 | ) 41 | 42 | comptex_params = [ 43 | pytest.param( 44 | {"m": 1}, 45 | b'{"m":1}', 46 | id="dict", 47 | ), 48 | pytest.param( 49 | [1, 2, 3], 50 | b"[1,2,3]", 51 | id="list", 52 | ), 53 | pytest.param( 54 | SimpleDataclass(r="hello!"), 55 | b'{"r":"hello!"}', 56 | id="dataclass", 57 | ), 58 | ] 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2025 Pastukhov Nikita 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /fast_depends/library/model.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from typing import Any, TypeVar 3 | 4 | Cls = TypeVar("Cls", bound="CustomField") 5 | 6 | 7 | class CustomField(ABC): 8 | param_name: str | None 9 | cast: bool 10 | required: bool 11 | 12 | __slots__ = ( 13 | "cast", 14 | "param_name", 15 | "required", 16 | "field", 17 | ) 18 | 19 | def __init__( 20 | self, 21 | *, 22 | cast: bool = True, 23 | required: bool = True, 24 | ) -> None: 25 | self.cast = cast 26 | self.param_name = None 27 | self.required = required 28 | self.field = False 29 | 30 | def set_param_name(self: Cls, name: str) -> Cls: 31 | self.param_name = name 32 | return self 33 | 34 | def use(self, /, **kwargs: Any) -> dict[str, Any]: 35 | assert self.param_name, "You should specify `param_name` before using" 36 | return kwargs 37 | 38 | def use_field(self, kwargs: dict[str, Any]) -> None: 39 | raise NotImplementedError 40 | 41 | def __repr__(self) -> str: 42 | return f"{self.__class__.__name__}(required={self.required}, cast={self.cast})" 43 | -------------------------------------------------------------------------------- /tests/pydantic_specific/test_prebuild.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pydantic import BaseModel 4 | 5 | from fast_depends import Provider 6 | from fast_depends.core import build_call_model 7 | from fast_depends.pydantic import PydanticSerializer 8 | from fast_depends.pydantic._compat import PYDANTIC_V2 9 | 10 | from .wrapper import noop_wrap 11 | 12 | 13 | class Model(BaseModel): 14 | a: str 15 | 16 | 17 | def model_func(m: Model) -> str: 18 | return m.a 19 | 20 | 21 | def test_prebuild_with_wrapper() -> None: 22 | func = noop_wrap(model_func) 23 | assert func(Model(a="Hi!")) == "Hi!" 24 | 25 | # build_call_model should work even if function is wrapped with a 26 | # wrapper that is imported from different module 27 | call_model = build_call_model( 28 | func, 29 | dependency_provider=Provider(), 30 | serializer_cls=PydanticSerializer(use_fastdepends_errors=True), 31 | ) 32 | 33 | model = call_model.serializer.model 34 | assert model 35 | # Fails if function unwrapping is not done at type introspection 36 | 37 | if PYDANTIC_V2: 38 | model.model_rebuild() 39 | else: 40 | # pydantic v1 41 | model.update_forward_refs() 42 | -------------------------------------------------------------------------------- /docs/docs/usages.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - toc 4 | --- 5 | 6 | `FastDepends` is a great instrument to integrate with any frameworks you are already using. 7 | It can also be a part of your own tools and frameworks (HTTP or [not*](https://lancetnik.github.io/Propan/) ) 8 | 9 | There are some usage examples with popular Python HTTP Frameworks: 10 | 11 | === "Flask" 12 | ```python hl_lines="11-12" linenums="1" 13 | {!> docs_src/usages/flask.py !} 14 | ``` 15 | 16 | === "Starlette" 17 | ```python hl_lines="9 17-19" linenums="1" 18 | {!> docs_src/usages/starlette.py !} 19 | ``` 20 | 21 | As you can see above, library, some middlewares and supporting classes... And you can use the whole power of *typed* Python everywhere. 22 | 23 | !!! tip 24 | `FastDepends` raises `pydantic.error_wrappers.ValidationError` at type casting exceptions. 25 | 26 | You need to handle them and wrap them in your own response with your custom middleware if you want to use it 27 | in production. 28 | 29 | !!! note 30 | 31 | If you are interested in using `FastDepends` in other frameworks, please take a look 32 | at my own [**Propan**](https://lancetnik.github.io/Propan/) framework for working with various Message Brokers. 33 | -------------------------------------------------------------------------------- /tests/serializers/msgspec/test_custom_type.py: -------------------------------------------------------------------------------- 1 | from typing import Any, TypeVar 2 | 3 | import pytest 4 | 5 | from fast_depends import Depends, Provider, inject 6 | from fast_depends.exceptions import ValidationError 7 | from fast_depends.msgspec import MsgSpecSerializer 8 | 9 | T = TypeVar("T") 10 | 11 | 12 | class CustomType: 13 | def __init__(self, value): 14 | self.value = value 15 | 16 | 17 | def msgspec_custom_type_decoder(t: type[T], obj: Any) -> T: 18 | if not isinstance(obj, t): 19 | return t(obj) 20 | return obj 21 | 22 | 23 | def dep(a: CustomType) -> str: 24 | return a.value 25 | 26 | 27 | @inject( 28 | serializer_cls=MsgSpecSerializer(use_fastdepends_errors=True), 29 | dependency_provider=Provider(), 30 | ) 31 | def custom_type_without_decoder(a: CustomType = Depends(dep)): ... 32 | 33 | 34 | @inject( 35 | serializer_cls=MsgSpecSerializer( 36 | use_fastdepends_errors=True, 37 | dec_hook=msgspec_custom_type_decoder, 38 | ), 39 | dependency_provider=Provider(), 40 | ) 41 | def custom_type_with_decoder(a: CustomType = Depends(dep)) -> str: 42 | assert isinstance(a, CustomType) 43 | return a.value 44 | 45 | 46 | def test_custom_type_cast(): 47 | custom_type_with_decoder("123") 48 | 49 | with pytest.raises(ValidationError): 50 | custom_type_without_decoder("123") 51 | -------------------------------------------------------------------------------- /fast_depends/exceptions.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Sequence 2 | from typing import Any 3 | 4 | from fast_depends.library.serializer import OptionItem 5 | 6 | 7 | class FastDependsError(Exception): 8 | pass 9 | 10 | 11 | class ValidationError(ValueError, FastDependsError): 12 | def __init__( 13 | self, 14 | *, 15 | incoming_options: Any, 16 | locations: Sequence[Any], 17 | expected: dict[str, OptionItem], 18 | original_error: Exception, 19 | ) -> None: 20 | self.original_error = original_error 21 | self.incoming_options = incoming_options 22 | 23 | self.error_fields: tuple[OptionItem, ...] = tuple( 24 | expected[x] for x in locations if x in expected 25 | ) 26 | if not self.error_fields: 27 | self.error_fields = tuple(expected.values()) 28 | 29 | super().__init__() 30 | 31 | def __str__(self) -> str: 32 | if isinstance(self.incoming_options, dict): 33 | content = ", ".join(f"{k}=`{v}`" for k, v in self.incoming_options.items()) 34 | else: 35 | content = f"`{self.incoming_options}`" 36 | 37 | return ( 38 | "\n Incoming options: " 39 | + content 40 | + "\n In the following option types error occurred:\n " 41 | + "\n ".join(map(str, self.error_fields)) 42 | ) 43 | -------------------------------------------------------------------------------- /docs/docs/tutorial/yield.md: -------------------------------------------------------------------------------- 1 | # Generators 2 | 3 | Sometimes we want to call something *before* and *after* original function call. 4 | 5 | That purpouse can be reached by using `yield` keyword. 6 | 7 | ## A database dependency with yield 8 | 9 | For example, you could use this to create a database session and close it after finishing. 10 | 11 | Only the code prior `yield` statement is executed before sending a response 12 | ```python linenums="1" hl_lines="1-2" 13 | {!> docs_src/tutorial_3_yield/tutorial_1.py !} 14 | ``` 15 | 16 | The *yielded* value is what is injected into original function 17 | ```python linenums="1" hl_lines="3" 18 | {!> docs_src/tutorial_3_yield/tutorial_1.py !} 19 | ``` 20 | 21 | The code following the `yield` statement is executed after the original function has been called 22 | ```python linenums="1" hl_lines="4" 23 | {!> docs_src/tutorial_3_yield/tutorial_1.py !} 24 | ``` 25 | 26 | !!! tip 27 | As same as a regular depends behavior you can use `async` and `sync` declarations both with an `async` original function 28 | and only `sync` declaration with a `sync` one. 29 | 30 | !!! warning 31 | All errors occurs at original function or another dependencies will be raised this place 32 | ```python linenums="1" hl_lines="3" 33 | {!> docs_src/tutorial_3_yield/tutorial_1.py !} 34 | ``` 35 | To guarantee `db.close()` execution use the following code: 36 | ```python linenums="1" hl_lines="3 5" 37 | {!> docs_src/tutorial_3_yield/tutorial_2.py !} 38 | ``` -------------------------------------------------------------------------------- /docs/docs/works.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - toc 4 | --- 5 | 6 | # How it works 7 | 8 | At first, I suppose, we need to discuss about this tool's key concept. 9 | 10 | It is very simple: 11 | 12 | 1. At your code's initialization time `FastDepends` builds special *pydantic* model with your function's expected arguments as a model fields, builds the dependencies graph 13 | 2. At runtime `FastDepends` grabs all incoming functions' `*args, **kwargs` and initializes functions' representation models with them 14 | 3. At the next step `FastDepends` execute functions' dependensies with the model fields as an arguments, calls the original function 15 | 4. Finally, `FastDepends` catches functions' outputs and casts it to expected `return` type 16 | 17 | This is pretty close to the following code: 18 | 19 | ```python linenums="1" 20 | {!> docs_src/how-it-works/works.py !} 21 | ``` 22 | 23 | !!! note 24 | It is not the real code, but generally `FastDepends` works this way 25 | 26 | So, the biggest part of the `FastDepends` code execution happens on application startup. 27 | At runtime the library just casts types to already built models. It works really fast. 28 | Generally, the library works with the same speed as the `pydantic` - the main dependency. 29 | 30 | On the other hand, working with only `*args, **kwargs` allows the library to be independent 31 | from other frameworks, business domains, technologies, etc. You are free to decide for 32 | yourself, how exactly to use this tool. -------------------------------------------------------------------------------- /tests/serializers/pydantic/test_overrides.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | 3 | import pytest 4 | from pydantic import Field 5 | 6 | from fast_depends import Depends, Provider, inject 7 | from fast_depends.pydantic import PydanticSerializer 8 | 9 | 10 | def dep(a: Annotated[int, Field()] = 1) -> int: 11 | return a 12 | 13 | 14 | def dep2(a: Annotated[int, Field()] = 2) -> int: 15 | return a 16 | 17 | 18 | @pytest.mark.parametrize( 19 | "fastdepends_error", 20 | [ 21 | pytest.param( 22 | False, 23 | id="Disabled Fastepends Error", 24 | ), 25 | pytest.param( 26 | True, 27 | id="Enabled Fastepends Error", 28 | ), 29 | ], 30 | ) 31 | def test_overrides_after_root_func_creation( 32 | provider: Provider, fastdepends_error: bool 33 | ) -> None: 34 | @inject( 35 | serializer_cls=PydanticSerializer(use_fastdepends_errors=fastdepends_error), 36 | dependency_provider=provider, 37 | ) 38 | def func(a: Annotated[int, Depends(dep)]) -> int: 39 | return a 40 | 41 | assert func() == 1 42 | 43 | with provider.scope(dep, dep2): 44 | assert func() == 2 45 | 46 | 47 | def test_overrides_before_root_func_creation(provider: Provider) -> None: 48 | provider.override(dep, dep2) 49 | 50 | @inject(serializer_cls=PydanticSerializer(), dependency_provider=provider) 51 | def func(a: Annotated[int, Depends(dep)]) -> int: 52 | return a 53 | 54 | assert func() == 2 55 | -------------------------------------------------------------------------------- /tests/serializers/pydantic/test_encode.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | import pytest 4 | from pydantic import BaseModel 5 | 6 | from fast_depends.pydantic.serializer import PydanticSerializer 7 | from tests.marks import pydanticV1, pydanticV2 8 | from tests.serializers.params import comptex_params, parametrized 9 | 10 | 11 | class SimpleModel(BaseModel): 12 | r: str 13 | 14 | 15 | @pytest.mark.parametrize( 16 | ("message", "expected_message"), 17 | ( 18 | *parametrized, 19 | *comptex_params, 20 | pytest.param( 21 | SimpleModel(r="hello!"), 22 | b'{"r":"hello!"}', 23 | id="model", 24 | ), 25 | ), 26 | ) 27 | @pydanticV2 28 | def test_encode_v2( 29 | message: Any, 30 | expected_message: bytes, 31 | ) -> None: 32 | msg = PydanticSerializer.encode(message) 33 | assert msg == expected_message 34 | 35 | 36 | @pytest.mark.parametrize( 37 | ("message", "expected_message"), 38 | ( 39 | *parametrized, 40 | pytest.param( 41 | {"m": 1}, 42 | b'{"m": 1}', 43 | id="dict", 44 | ), 45 | pytest.param( 46 | [1, 2, 3], 47 | b"[1, 2, 3]", 48 | id="list", 49 | ), 50 | pytest.param( 51 | SimpleModel(r="hello!"), 52 | b'{"r": "hello!"}', 53 | id="model", 54 | ), 55 | ), 56 | ) 57 | @pydanticV1 58 | def test_encode_v1( 59 | message: Any, 60 | expected_message: bytes, 61 | ) -> None: 62 | msg = PydanticSerializer.encode(message) 63 | assert msg == expected_message 64 | -------------------------------------------------------------------------------- /.github/workflows/relator.yml: -------------------------------------------------------------------------------- 1 | name: Relator 2 | 3 | on: 4 | issues: 5 | types: [opened, labeled, 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: "Send Telegram notification for new issue or opened pull request" 20 | if: github.actor != 'dependabot[bot]' && (github.event.action == 'opened' || github.event.action == 'reopened') 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Send Telegram notification for new issue or opened pull request 24 | uses: reagento/relator@919d3a1593a3ed3e8b8f2f39013cc6f5498241da # v1.6.0 25 | with: 26 | tg-bot-token: ${{ secrets.TELEGRAM_TOKEN }} 27 | tg-chat-id: ${{ secrets.TELEGRAM_TO }} 28 | github-token: ${{ secrets.GITHUB_TOKEN }} 29 | join-input-with-list: "1" 30 | 31 | notify-oss-board: 32 | name: "Send Telegram notification to OSS board" 33 | if: github.event_name == 'issues' && github.event.label.name == 'good first issue' 34 | runs-on: ubuntu-latest 35 | steps: 36 | - name: Send Telegram notification to OSS board 37 | uses: reagento/relator@919d3a1593a3ed3e8b8f2f39013cc6f5498241da # v1.6.0 38 | with: 39 | tg-bot-token: ${{ secrets.TELEGRAM_TOKEN }} 40 | tg-chat-id: ${{ secrets.GFI_CHAT }} 41 | github-token: ${{ secrets.GITHUB_TOKEN }} 42 | join-input-with-list: "1" 43 | -------------------------------------------------------------------------------- /docs/docs/tutorial/classes.md: -------------------------------------------------------------------------------- 1 | # Classes as Dependencies 2 | 3 | ### "Callable", remember? 4 | 5 | ```python linenums="1" 6 | {!> docs_src/tutorial_2_classes/tutorial_1.py !} 7 | ``` 8 | 9 | Yep, all of these examples can be used as a dependency! 10 | 11 | --- 12 | 13 | ### INIT (1-st call) 14 | 15 | You can use class initializer as a dependency. This way, object of this class 16 | will be the type of your dependency: 17 | 18 | ```python linenums="1" hl_lines="5-7 10" 19 | {!> docs_src/tutorial_2_classes/tutorial_2.py !} 20 | ``` 21 | 22 | !!! warning 23 | You should use `Any` annotation if `MyDependency` is not a `pydantic.BaseModel` subclass. 24 | Using `MyDependency` annotation raises `ValueError` exception at code initialization time as the pydantic 25 | can't cast any value to not-pydantic class. 26 | 27 | --- 28 | 29 | ### CALL (2-nd call) 30 | 31 | If you wish to specify your dependency behavior earlier, you can use `__call__` method of 32 | already initialized class object. 33 | 34 | ```python linenums="1" hl_lines="7-8 11" 35 | {!> docs_src/tutorial_2_classes/tutorial_3.py !} 36 | ``` 37 | 38 | --- 39 | 40 | ### CLASSMETHOD or STATICMETHOD (3-rd call) 41 | 42 | Also, you can use classmethods or staticmethod as dependencies. 43 | It can be helpful with some OOP patterns (Strategy as an example). 44 | 45 | ```python linenums="1" hl_lines="4-6 9" 46 | {!> docs_src/tutorial_2_classes/tutorial_4.py !} 47 | ``` 48 | 49 | --- 50 | 51 | ### ANY METHOD (4-th call) 52 | 53 | ```python linenums="1" hl_lines="7-8 11" 54 | {!> docs_src/tutorial_2_classes/tutorial_5.py !} 55 | ``` 56 | 57 | 58 | !!! tip "Async" 59 | Only *3-rd* and *4-th* call methods are able to be `async` type 60 | -------------------------------------------------------------------------------- /docs/docs/advanced/starlette.md: -------------------------------------------------------------------------------- 1 | # Let's write some code 2 | 3 | Now we take the *starlette example* from [usages](/FastDepends/usages/) and specify it to use *Path* now. 4 | 5 | ## Handle *request* specific fields 6 | 7 | First of all, **Starlette** pass to a handler the only one argument - `request` 8 | To use them with `FastDepends` we need unwrap `request` to kwargs. 9 | 10 | ```python hl_lines="6-8" linenums="1" 11 | {!> docs_src/advanced/custom/starlette.py [ln:1,15-21] !} 12 | ``` 13 | 14 | !!! note "" 15 | Also, we wraps an original handler to `fast_depends.inject` too at *3* line 16 | 17 | ## Declare *Custom Field* 18 | 19 | Next step, define *Path* custom field 20 | 21 | ```python linenums="1" hl_lines="8" 22 | {!> docs_src/advanced/custom/starlette.py [ln:2,8-13] !} 23 | ``` 24 | 25 | ## Usage with the *Starlette* 26 | 27 | And use it at our *Starlette* application: 28 | ```python linenums="1" hl_lines="6 7 10" 29 | {!> docs_src/advanced/custom/starlette.py [ln:4-6,23-30] !} 30 | ``` 31 | 32 | Depends is working as expected too 33 | 34 | ```python linenums="1" hl_lines="1 4-5 9" 35 | def get_user(user_id: int = Path()): 36 | return f"user {user_id}" 37 | 38 | @wrap_starlette 39 | async def hello(user: str = Depends(get_user)): 40 | return PlainTextResponse(f"Hello, {user}!") 41 | 42 | app = Starlette(debug=True, routes=[ 43 | Route("/{user_id}", hello) 44 | ]) 45 | ``` 46 | 47 | As an *Annotated* does 48 | ```python linenums="1" hl_lines="2" 49 | @wrap_starlette 50 | async def get_user(user: Annotated[int, Path()]): 51 | return PlainTextResponse(f"Hello, {user}!") 52 | ``` 53 | 54 | ## Full example 55 | 56 | ```python linenums="1" 57 | {!> docs_src/advanced/custom/starlette.py !} 58 | ``` 59 | 60 | The code above works "as it". You can copy it and declare other *Header*, *Cookie*, *Query* fields by yourself. Just try, it's fun! -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Developing 2 | 3 | If you already cloned the repository and you know that you need to deep dive in the code, here are some guidelines to set up your environment. 4 | 5 | ## Virtual environment with `venv` 6 | 7 | You can create a virtual environment in a directory using Python's `venv` module: 8 | 9 | ```bash 10 | python -m venv venv 11 | ``` 12 | 13 | That will create a directory `./venv/` with the Python binaries and then you will be able to install packages for that isolated environment. 14 | 15 | ## Activate the environment 16 | 17 | Activate the new environment with: 18 | 19 | ```bash 20 | source ./venv/bin/activate 21 | ``` 22 | 23 | Make sure you have the latest pip version on your virtual environment to 24 | 25 | ```bash 26 | python -m pip install --upgrade pip 27 | ``` 28 | 29 | ## pip 30 | 31 | After activating the environment as described above: 32 | 33 | ```bash 34 | pip install --group dev -e . 35 | ``` 36 | 37 | or 38 | 39 | ```bash 40 | uv sync --group dev 41 | ``` 42 | 43 | It will install all the dependencies and your local FastDepends in your local environment. 44 | 45 | ### Using your local FastDepends 46 | 47 | If you create a Python file that imports and uses FastDepends, and run it with the Python from your local environment, it will use your local FastDepends source code. 48 | 49 | And if you update that local FastDepends source code, as it is installed with `-e`, when you run that Python file again, it will use the fresh version of FastDepends you just edited. 50 | 51 | That way, you don't have to "install" your local version to be able to test every change. 52 | 53 | ## Tests 54 | 55 | ### Pytests 56 | 57 | To run tests with your current FastDepends application and Python environment use: 58 | 59 | ```bash 60 | pytest tests 61 | # or 62 | bash ./scripts/test.sh 63 | # with coverage output 64 | bash ./scripts/test-cov.sh 65 | ``` 66 | -------------------------------------------------------------------------------- /docs/docs/contributing.md: -------------------------------------------------------------------------------- 1 | # Developing 2 | 3 | If you already cloned the repository and you know that you need to deep dive in the code, here are some guidelines to set up your environment. 4 | 5 | ## Virtual environment with `venv` 6 | 7 | You can create a virtual environment in a directory using Python's `venv` module: 8 | 9 | ```bash 10 | python -m venv venv 11 | ``` 12 | 13 | That will create a directory `./venv/` with the Python binaries and then you will be able to install packages for that isolated environment. 14 | 15 | ## Activate the environment 16 | 17 | Activate the new environment with: 18 | 19 | ```bash 20 | source ./venv/bin/activate 21 | ``` 22 | 23 | ## Install dependencies 24 | 25 | === "pip" 26 | Make sure you have the latest pip version on your virtual environment to 27 | 28 | ```bash 29 | python -m pip install -U pip 30 | ``` 31 | 32 | After activating the environment as described above: 33 | 34 | ```bash 35 | pip install --group dev -e . 36 | ``` 37 | 38 | === "uv" 39 | 40 | ```bash 41 | uv sync --group dev 42 | ``` 43 | 44 | It will install all the dependencies and your local FastDepends in your local environment. 45 | 46 | ### Using your local FastDepends 47 | 48 | If you create a Python file that imports and uses FastDepends, and run it with the Python from your local environment, it will use your local FastDepends source code. 49 | 50 | And if you update that local FastDepends source code, as it is installed with `-e`, when you run that Python file again, it will use the fresh version of FastDepends you just edited. 51 | 52 | That way, you don't have to "install" your local version to be able to test every change. 53 | 54 | ## Tests 55 | 56 | ### Pytests 57 | 58 | To run tests with your current FastDepends application and Python environment use: 59 | 60 | ```bash 61 | pytest tests 62 | # or 63 | bash ./scripts/test.sh 64 | # with coverage output 65 | bash ./scripts/test-cov.sh 66 | ``` 67 | -------------------------------------------------------------------------------- /fast_depends/pydantic/schema.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from fast_depends.core import CallModel 4 | from fast_depends.pydantic._compat import PYDANTIC_V2, create_model, model_schema 5 | 6 | 7 | def get_schema( 8 | call: CallModel, 9 | embed: bool = False, 10 | resolve_refs: bool = False, 11 | ) -> dict[str, Any]: 12 | class_options: dict[str, Any] = { 13 | i.field_name: (i.field_type, i.default_value) for i in call.flat_params 14 | } 15 | 16 | name = getattr(call.serializer, "name", "Undefined") 17 | 18 | if not class_options: 19 | return {"title": name, "type": "null"} 20 | 21 | params_model = create_model(name, **class_options) 22 | 23 | body = model_schema(params_model) 24 | 25 | if resolve_refs: 26 | pydantic_key = "$defs" if PYDANTIC_V2 else "definitions" 27 | body = _move_pydantic_refs(body, pydantic_key) 28 | body.pop(pydantic_key, None) 29 | 30 | if embed and len(body["properties"]) == 1: 31 | body = list(body["properties"].values())[0] 32 | 33 | return body 34 | 35 | 36 | def _move_pydantic_refs( 37 | original: Any, key: str, refs: dict[str, Any] | None = None 38 | ) -> Any: 39 | if not isinstance(original, dict): 40 | return original 41 | 42 | data = original.copy() 43 | 44 | if refs is None: 45 | raw_refs = data.get(key, {}) 46 | refs = _move_pydantic_refs(raw_refs, key, raw_refs) 47 | 48 | name: str | None = None 49 | for k in data: 50 | if k == "$ref": 51 | name = data[k].replace(f"#/{key}/", "") 52 | 53 | elif isinstance(data[k], dict): 54 | data[k] = _move_pydantic_refs(data[k], key, refs) 55 | 56 | elif isinstance(data[k], list): 57 | for i in range(len(data[k])): 58 | data[k][i] = _move_pydantic_refs(data[k][i], key, refs) 59 | 60 | if name: 61 | assert refs, "Smth wrong" 62 | data = refs[name] 63 | 64 | return data 65 | -------------------------------------------------------------------------------- /tests/pydantic_specific/sync/test_cast.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | 3 | import pytest 4 | from annotated_types import Ge 5 | from pydantic import BaseModel, Field 6 | 7 | from fast_depends import inject 8 | from fast_depends.exceptions import ValidationError 9 | from tests.marks import pydanticV2 10 | 11 | 12 | def test_pydantic_types_casting(): 13 | class SomeModel(BaseModel): 14 | field: int 15 | 16 | @inject 17 | def some_func(a: SomeModel): 18 | return a.field 19 | 20 | assert isinstance(some_func({"field": "31"}), int) 21 | 22 | 23 | def test_pydantic_field_types_casting(): 24 | @inject 25 | def some_func(a: int = Field(..., alias="b")) -> float: 26 | assert isinstance(a, int) 27 | return a 28 | 29 | @inject 30 | def another_func(a=Field(..., alias="b")) -> float: 31 | assert isinstance(a, str) 32 | return a 33 | 34 | assert isinstance(some_func(b="2", c=3), float) 35 | assert isinstance(another_func(b="2"), float) 36 | 37 | 38 | def test_annotated(): 39 | A = Annotated[int, Field(..., alias="b")] 40 | 41 | @inject 42 | def some_func(a: A) -> float: 43 | assert isinstance(a, int) 44 | return a 45 | 46 | assert isinstance(some_func(b="2"), float) 47 | 48 | 49 | def test_generator(): 50 | @inject 51 | def simple_func(a: str) -> int: 52 | for _ in range(2): 53 | yield a 54 | 55 | for i in simple_func("1"): 56 | assert i == 1 57 | 58 | 59 | def test_validation_error(): 60 | @inject 61 | def some_func(a, b: str = Field(..., max_length=1)): 62 | return 1 63 | 64 | assert some_func(1, "a") == 1 65 | 66 | with pytest.raises(ValidationError): 67 | assert some_func() 68 | 69 | with pytest.raises(ValidationError): 70 | assert some_func(1, "dsdas") 71 | 72 | 73 | @pydanticV2 74 | def test_multi_annotated(): 75 | from pydantic.functional_validators import AfterValidator 76 | 77 | @inject() 78 | def f(a: Annotated[int, Ge(10), AfterValidator(lambda x: x + 10)]) -> int: 79 | return a 80 | 81 | with pytest.raises(ValidationError): 82 | f(1) 83 | 84 | assert f(10) == 20 85 | -------------------------------------------------------------------------------- /fast_depends/library/serializer.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import json 3 | from abc import ABC, abstractmethod 4 | from typing import Any, Protocol 5 | 6 | 7 | class OptionItem: 8 | __slots__ = ( 9 | "field_name", 10 | "field_type", 11 | "default_value", 12 | "kind", 13 | "source", 14 | ) 15 | 16 | def __init__( 17 | self, 18 | field_name: str, 19 | field_type: Any, 20 | source: Any = None, 21 | default_value: Any = ..., 22 | kind: inspect._ParameterKind = inspect.Parameter.POSITIONAL_OR_KEYWORD, 23 | ) -> None: 24 | self.field_name = field_name 25 | self.field_type = field_type 26 | self.default_value = default_value 27 | self.source = source 28 | self.kind = kind 29 | 30 | def __repr__(self) -> str: 31 | type_name = getattr(self.field_type, "__name__", str(self.field_type)) 32 | content = f"{self.field_name}, type=`{type_name}`" 33 | if self.default_value is not Ellipsis: 34 | content = f"{content}, default=`{self.default_value}`" 35 | if self.source: 36 | content = f"{content}, source=`{self.source}`" 37 | return f"OptionItem[{content}]" 38 | 39 | 40 | class Serializer(ABC): 41 | def __init__( 42 | self, 43 | *, 44 | name: str, 45 | options: list[OptionItem], 46 | response_type: Any, 47 | ): 48 | self.name = name 49 | self.options = {i.field_name: i for i in options} 50 | self.response_option = { 51 | "return": OptionItem(field_name="return", field_type=response_type), 52 | } 53 | 54 | def get_aliases(self) -> tuple[str, ...]: 55 | return () 56 | 57 | @abstractmethod 58 | def __call__(self, call_kwargs: dict[str, Any]) -> dict[str, Any]: 59 | raise NotImplementedError 60 | 61 | def response(self, value: Any) -> Any: 62 | return value 63 | 64 | 65 | class SerializerProto(Protocol): 66 | def __call__( 67 | self, 68 | *, 69 | name: str, 70 | options: list[OptionItem], 71 | response_type: Any, 72 | ) -> Serializer: ... 73 | 74 | @staticmethod 75 | def encode(message: Any) -> bytes: 76 | return json.dumps(message).encode("utf-8") 77 | -------------------------------------------------------------------------------- /fast_depends/dependencies/provider.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable, Hashable, Iterator 2 | from contextlib import contextmanager 3 | from typing import TYPE_CHECKING, Any, TypeAlias 4 | 5 | from fast_depends.core import build_call_model 6 | 7 | if TYPE_CHECKING: 8 | from fast_depends.core import CallModel 9 | 10 | 11 | Key: TypeAlias = Hashable 12 | 13 | 14 | class Provider: 15 | dependencies: dict[Key, "CallModel"] 16 | overrides: dict[Key, "CallModel"] 17 | 18 | def __init__(self) -> None: 19 | self.dependencies = {} 20 | self.overrides = {} 21 | 22 | def clear(self) -> None: 23 | self.overrides = {} 24 | 25 | def add_dependant( 26 | self, 27 | dependant: "CallModel", 28 | ) -> Key: 29 | key = self.__get_original_key(dependant.call) 30 | self.dependencies[key] = dependant 31 | return key 32 | 33 | def get_dependant(self, key: Key) -> "CallModel": 34 | return self.overrides.get(key) or self.dependencies[key] 35 | 36 | def override( 37 | self, 38 | original: Callable[..., Any], 39 | override: Callable[..., Any], 40 | ) -> None: 41 | key = self.__get_original_key(original) 42 | 43 | serializer_cls = None 44 | 45 | if original_dependant := self.dependencies.get(key): 46 | serializer_cls = original_dependant.serializer_cls 47 | 48 | else: 49 | self.dependencies[key] = build_call_model( 50 | original, 51 | dependency_provider=self, 52 | ) 53 | 54 | override_model = build_call_model( 55 | override, 56 | dependency_provider=self, 57 | serializer_cls=serializer_cls, 58 | ) 59 | 60 | self.overrides[key] = override_model 61 | 62 | def __setitem__( 63 | self, 64 | key: Callable[..., Any], 65 | value: Callable[..., Any], 66 | ) -> None: 67 | """Alias for `provider[key] = value` syntax""" 68 | self.override(key, value) 69 | 70 | @contextmanager 71 | def scope( 72 | self, 73 | original: Callable[..., Any], 74 | override: Callable[..., Any], 75 | ) -> Iterator[None]: 76 | self.override(original, override) 77 | yield 78 | self.overrides.pop(self.__get_original_key(original), None) 79 | 80 | def __get_original_key(self, original: Callable[..., Any]) -> Key: 81 | return original 82 | -------------------------------------------------------------------------------- /tests/pydantic_specific/async/test_cast.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | 3 | import pytest 4 | from annotated_types import Ge 5 | from pydantic import BaseModel, Field 6 | 7 | from fast_depends import inject 8 | from fast_depends.exceptions import ValidationError 9 | from tests.marks import pydanticV2 10 | 11 | 12 | @pytest.mark.anyio 13 | async def test_pydantic_types_casting(): 14 | class SomeModel(BaseModel): 15 | field: int 16 | 17 | @inject 18 | async def some_func(a: SomeModel): 19 | return a.field 20 | 21 | assert isinstance(await some_func({"field": "31"}), int) 22 | 23 | 24 | @pytest.mark.anyio 25 | async def test_pydantic_field_types_casting(): 26 | @inject 27 | async def some_func(a: int = Field(..., alias="b")) -> float: 28 | assert isinstance(a, int) 29 | return a 30 | 31 | @inject 32 | async def another_func(a=Field(..., alias="b")) -> float: 33 | assert isinstance(a, str) 34 | return a 35 | 36 | assert isinstance(await some_func(b="2", c=3), float) 37 | assert isinstance(await another_func(b="2"), float) 38 | 39 | 40 | @pytest.mark.anyio 41 | async def test_annotated(): 42 | A = Annotated[int, Field(..., alias="b")] 43 | 44 | @inject 45 | async def some_func(a: A) -> float: 46 | assert isinstance(a, int) 47 | return a 48 | 49 | assert isinstance(await some_func(b="2"), float) 50 | 51 | 52 | @pytest.mark.anyio 53 | async def test_generator(): 54 | @inject 55 | async def simple_func(a: str) -> int: 56 | for _ in range(2): 57 | yield a 58 | 59 | async for i in simple_func("1"): 60 | assert i == 1 61 | 62 | 63 | @pytest.mark.anyio 64 | async def test_validation_error(): 65 | @inject 66 | async def some_func(a, b: str = Field(..., max_length=1)): 67 | return 1 68 | 69 | assert await some_func(1, "a") == 1 70 | 71 | with pytest.raises(ValidationError): 72 | assert await some_func() 73 | 74 | with pytest.raises(ValidationError): 75 | assert await some_func(1, "dsdas") 76 | 77 | 78 | @pytest.mark.anyio 79 | @pydanticV2 80 | async def test_multi_annotated(): 81 | from pydantic.functional_validators import AfterValidator 82 | 83 | @inject() 84 | async def f(a: Annotated[int, Ge(10), AfterValidator(lambda x: x + 10)]) -> int: 85 | return a 86 | 87 | with pytest.raises(ValidationError): 88 | await f(1) 89 | 90 | assert await f(10) == 20 91 | -------------------------------------------------------------------------------- /docs/docs/assets/stylesheets/termynal.css: -------------------------------------------------------------------------------- 1 | /** 2 | * termynal.js 3 | * 4 | * @author Ines Montani 5 | * @version 0.0.1 6 | * @license MIT 7 | */ 8 | 9 | :root { 10 | --color-bg: #252a33; 11 | --color-text: #eee; 12 | --color-text-subtle: #a2a2a2; 13 | } 14 | 15 | [data-termynal] { 16 | width: 750px; 17 | max-width: 100%; 18 | background: var(--color-bg); 19 | color: var(--color-text); 20 | /* font-size: 18px; */ 21 | font-size: 15px; 22 | /* font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; */ 23 | font-family: 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; 24 | border-radius: 4px; 25 | padding: 75px 45px 35px; 26 | position: relative; 27 | -webkit-box-sizing: border-box; 28 | box-sizing: border-box; 29 | } 30 | 31 | [data-termynal]:before { 32 | content: ''; 33 | position: absolute; 34 | top: 15px; 35 | left: 15px; 36 | display: inline-block; 37 | width: 15px; 38 | height: 15px; 39 | border-radius: 50%; 40 | /* A little hack to display the window buttons in one pseudo element. */ 41 | background: #d9515d; 42 | -webkit-box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930; 43 | box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930; 44 | } 45 | 46 | [data-termynal]:after { 47 | content: 'bash'; 48 | position: absolute; 49 | color: var(--color-text-subtle); 50 | top: 5px; 51 | left: 0; 52 | width: 100%; 53 | text-align: center; 54 | } 55 | 56 | a[data-terminal-control] { 57 | text-align: right; 58 | display: block; 59 | color: #aebbff; 60 | } 61 | 62 | [data-ty] { 63 | display: block; 64 | line-height: 2; 65 | } 66 | 67 | [data-ty]:before { 68 | /* Set up defaults and ensure empty lines are displayed. */ 69 | content: ''; 70 | display: inline-block; 71 | vertical-align: middle; 72 | } 73 | 74 | [data-ty="input"]:before, 75 | [data-ty-prompt]:before { 76 | margin-right: 0.75em; 77 | color: var(--color-text-subtle); 78 | } 79 | 80 | [data-ty="input"]:before { 81 | content: '$'; 82 | } 83 | 84 | [data-ty][data-ty-prompt]:before { 85 | content: attr(data-ty-prompt); 86 | } 87 | 88 | [data-ty-cursor]:after { 89 | content: attr(data-ty-cursor); 90 | font-family: monospace; 91 | margin-left: 0.5em; 92 | -webkit-animation: blink 1s infinite; 93 | animation: blink 1s infinite; 94 | } 95 | 96 | 97 | /* Cursor animation */ 98 | 99 | @-webkit-keyframes blink { 100 | 50% { 101 | opacity: 0; 102 | } 103 | } 104 | 105 | @keyframes blink { 106 | 50% { 107 | opacity: 0; 108 | } 109 | } -------------------------------------------------------------------------------- /tests/test_params.py: -------------------------------------------------------------------------------- 1 | from fast_depends import Depends, Provider 2 | from fast_depends.core import build_call_model 3 | from fast_depends.library import CustomField 4 | 5 | 6 | def test_params(): 7 | def func1(m): ... 8 | 9 | def func2(c, b=Depends(func1), d=CustomField()): # noqa: B008 10 | ... 11 | 12 | def func3(b): ... 13 | 14 | def main(a, b, m=Depends(func2), k=Depends(func3)): ... 15 | 16 | def extra_func(n): ... 17 | 18 | model = build_call_model( 19 | main, extra_dependencies=(Depends(extra_func),), dependency_provider=Provider() 20 | ) 21 | 22 | assert {p.field_name for p in model.params} == {"a", "b"} 23 | assert {p.field_name for p in model.flat_params} == {"a", "b", "c", "m", "n"} 24 | 25 | 26 | def test_args_kwargs_params(): 27 | def func1(m): ... 28 | 29 | def func2(c, b=Depends(func1), d=CustomField()): # noqa: B008 30 | ... 31 | 32 | def func3(b): ... 33 | 34 | def default_var_names(a, *args, b, m=Depends(func2), k=Depends(func3), **kwargs): 35 | return a, args, b, kwargs 36 | 37 | def extra_func(n): ... 38 | 39 | model = build_call_model( 40 | default_var_names, 41 | extra_dependencies=(Depends(extra_func),), 42 | dependency_provider=Provider(), 43 | ) 44 | 45 | assert {p.field_name for p in model.params} == {"a", "args", "b", "kwargs"} 46 | assert {p.field_name for p in model.flat_params} == { 47 | "a", 48 | "args", 49 | "b", 50 | "kwargs", 51 | "c", 52 | "m", 53 | "n", 54 | } 55 | 56 | assert default_var_names(1, *("a"), b=2, **{"kw": "kw"}) == ( 57 | 1, 58 | ("a",), 59 | 2, 60 | {"kw": "kw"}, 61 | ) 62 | 63 | 64 | def test_custom_args_kwargs_params(): 65 | def func1(m): ... 66 | 67 | def func2(c, b=Depends(func1), d=CustomField()): # noqa: B008 68 | ... 69 | 70 | def func3(b): ... 71 | 72 | def extra_func(n): ... 73 | 74 | def custom_var_names(a, *args_, b, m=Depends(func2), k=Depends(func3), **kwargs_): 75 | return a, args_, b, kwargs_ 76 | 77 | model = build_call_model( 78 | custom_var_names, 79 | extra_dependencies=(Depends(extra_func),), 80 | dependency_provider=Provider(), 81 | ) 82 | 83 | assert {p.field_name for p in model.params} == {"a", "args_", "b", "kwargs_"} 84 | assert {p.field_name for p in model.flat_params} == { 85 | "a", 86 | "args_", 87 | "b", 88 | "kwargs_", 89 | "c", 90 | "m", 91 | "n", 92 | } 93 | 94 | assert custom_var_names(1, *("a"), b=2, **{"kw": "kw"}) == ( 95 | 1, 96 | ("a",), 97 | 2, 98 | {"kw": "kw"}, 99 | ) 100 | -------------------------------------------------------------------------------- /docs/docs/tutorial/overrides.md: -------------------------------------------------------------------------------- 1 | # Testing Dependencies with Overrides 2 | 3 | ## Overriding dependencies during testing 4 | 5 | There are some scenarios where you might want to override a dependency during testing. 6 | 7 | You don't want the original dependency to run (nor any of the sub-dependencies it might have). 8 | 9 | Instead, you want to provide a different dependency that will be used only during tests (possibly only some specific tests), and will provide a value that can be used where the value of the original dependency was used. 10 | 11 | ### Use cases: external service 12 | 13 | An example could be that you have an external authentication provider that you need to call. 14 | 15 | You send it a token and it returns an authenticated user. 16 | 17 | This provider might be charging you per request, and calling it might take some extra time than if you had a fixed mock user for tests. 18 | 19 | You probably want to test the external provider once, but not necessarily call it for every test that runs. 20 | 21 | In this case, you can override the dependency that calls that provider, and use a custom dependency that returns a mock user, only for your tests. 22 | 23 | ### Use the `fast_depends.dependency_provider` object 24 | 25 | For these cases, your **FastDepends** library has an object `dependency_provider` with `dependency_overrides` attribute, it is a simple `dict`. 26 | 27 | To override a dependency for testing, you put as a key the original dependency (a function), and as the value, your dependency override (another function). 28 | 29 | And then **FastDepends** will call that override instead of the original dependency. 30 | 31 | ```python hl_lines="4 7 10 13 18" linenums="1" 32 | {!> docs_src/tutorial_5_overrides/example.py !} 33 | ``` 34 | 35 | ### Use `pytest.fixture` 36 | 37 | `dependency_provider` is a library global object. Override dependency at one place, you override it everywhere. 38 | 39 | So, if you don't wish to override dependency everywhere, I extremely recommend to use the following fixture for your tests 40 | 41 | ```python linenums="1" hl_lines="18-21" 42 | {!> docs_src/tutorial_5_overrides/fixture.py !} 43 | ``` 44 | 45 | 1. Drop all overridings 46 | 47 | !!! tip 48 | Alternatively, you can create you own dependency provider in pass it in the functions you want. 49 | 50 | ```python linenums="1" hl_lines="4 12 18" 51 | from typing import Annotated 52 | from fast_depends import Depends, Provider, inject 53 | 54 | provider = Provider() 55 | 56 | def abc_func() -> int: 57 | raise 2 58 | 59 | def real_func() -> int: 60 | return 1 61 | 62 | @inject(dependency_overrides_provider=provider) 63 | def func( 64 | dependency: Annotated[int, Depends(abc_func)] 65 | ) -> int: 66 | return dependency 67 | 68 | with provider.scope(abc_func, real_func): 69 | assert func() == 1 70 | ``` -------------------------------------------------------------------------------- /docs/docs/assets/stylesheets/custom.css: -------------------------------------------------------------------------------- 1 | .termynal-comment { 2 | color: #4a968f; 3 | font-style: italic; 4 | display: block; 5 | } 6 | 7 | .termy { 8 | /* For right to left languages */ 9 | direction: ltr; 10 | } 11 | 12 | .termy [data-termynal] { 13 | white-space: pre-wrap; 14 | } 15 | 16 | a.external-link { 17 | /* For right to left languages */ 18 | direction: ltr; 19 | display: inline-block; 20 | } 21 | 22 | a.external-link::after { 23 | /* \00A0 is a non-breaking space 24 | to make the mark be on the same line as the link 25 | */ 26 | content: "\00A0[↪]"; 27 | } 28 | 29 | a.internal-link::after { 30 | /* \00A0 is a non-breaking space 31 | to make the mark be on the same line as the link 32 | */ 33 | content: "\00A0↪"; 34 | } 35 | 36 | .shadow { 37 | box-shadow: 5px 5px 10px #999; 38 | } 39 | 40 | /* Give space to lower icons so Gitter chat doesn't get on top of them */ 41 | .md-footer-meta { 42 | padding-bottom: 2em; 43 | } 44 | 45 | .user-list { 46 | display: flex; 47 | flex-wrap: wrap; 48 | margin-bottom: 2rem; 49 | } 50 | 51 | .user-list-center { 52 | justify-content: space-evenly; 53 | } 54 | 55 | .user { 56 | margin: 1em; 57 | min-width: 7em; 58 | } 59 | 60 | .user .avatar-wrapper { 61 | width: 80px; 62 | height: 80px; 63 | margin: 10px auto; 64 | overflow: hidden; 65 | border-radius: 50%; 66 | position: relative; 67 | } 68 | 69 | .user .avatar-wrapper img { 70 | position: absolute; 71 | top: 50%; 72 | left: 50%; 73 | transform: translate(-50%, -50%); 74 | } 75 | 76 | .user .title { 77 | text-align: center; 78 | } 79 | 80 | .user .count { 81 | font-size: 80%; 82 | text-align: center; 83 | } 84 | 85 | a.announce-link:link, 86 | a.announce-link:visited { 87 | color: #fff; 88 | } 89 | 90 | a.announce-link:hover { 91 | color: var(--md-accent-fg-color); 92 | } 93 | 94 | .announce-wrapper { 95 | display: flex; 96 | justify-content: space-between; 97 | flex-wrap: wrap; 98 | align-items: center; 99 | } 100 | 101 | .announce-wrapper div.item { 102 | display: none; 103 | } 104 | 105 | .announce-wrapper .sponsor-badge { 106 | display: block; 107 | position: absolute; 108 | top: -10px; 109 | right: 0; 110 | font-size: 0.5rem; 111 | color: #999; 112 | background-color: #666; 113 | border-radius: 10px; 114 | padding: 0 10px; 115 | z-index: 10; 116 | } 117 | 118 | .announce-wrapper .sponsor-image { 119 | display: block; 120 | border-radius: 20px; 121 | } 122 | 123 | .announce-wrapper>div { 124 | min-height: 40px; 125 | display: flex; 126 | align-items: center; 127 | } 128 | 129 | .twitter { 130 | color: #00acee; 131 | } 132 | 133 | /* Right to left languages */ 134 | code { 135 | direction: ltr; 136 | display: inline-block; 137 | } 138 | 139 | .md-content__inner h1 { 140 | direction: ltr !important; 141 | } 142 | 143 | .illustration { 144 | margin-top: 2em; 145 | margin-bottom: 2em; 146 | } -------------------------------------------------------------------------------- /docs/docs/alternatives.md: -------------------------------------------------------------------------------- 1 | # Some more featured DI python libraries 2 | 3 | `FastDepend` is a very small toolkit to achieve one point: provide you with opportunity to use **FastAPI** `Depends` and typecasting everywhere. 4 | 5 | Sometimes, more complex tools are required. In these cases I can recommend you to take a look at the following projects 6 | 7 | ## [Dishka](https://dishka.readthedocs.io/en/stable/) 8 | 9 | Cute DI framework with scopes and agreeable API. 10 | 11 | This library provides **IoC container** that’s genuinely useful. If you’re exhausted from endlessly passing objects just to create other objects, only to have those objects create even more — you’re not alone, and we have a solution. Not every project requires IoC container, but take a look at what we offer. 12 | 13 | Unlike other tools, `dishka` focuses **only** on **dependency injection** without trying to solve unrelated tasks. 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 frameworks either lack scopes completely or offer only two. Here, you can define as many scopes as needed. 18 | * **Finalization**. Some dependencies, like database connections, need not only to be created but also carefully released. Many frameworks lack this essential feature. 19 | * **Modular providers**. Instead of creating many separate functions or one large class, you can split factories into smaller classes for easier reuse. 20 | * **Clean dependencies**. You don’t need to add custom markers to dependency code just to make it visible to the library. 21 | * **Simple API**. Only a few objects are needed to start using the library. 22 | * **Framework integrations**. Popular frameworks are supported out of the box. You can simply extend it for your needs. 23 | 24 | Speed. The library is fast enough that performance is not a concern. In fact, it outperforms many alternatives. 25 | 26 | ## [DI](https://adriangb.com/di/) 27 | 28 | `di` is a modern dependency injection toolkit, modeled around the simplicity of **FastAPI**'s dependency injection. 29 | 30 | Key features: 31 | 32 | * Intuitive: simple API, inspired by FastAPI 33 | * Auto-wiring: `di` also supports auto-wiring using type annotations 34 | * Scopes: inspired by `pytest scopes`, but defined by users 35 | * Composable: decoupled internal APIs give you the flixibility to customize wiring, execution and binding. 36 | * Performance: `di` can execute dependencies in parallel and cache results ins scopes. 37 | 38 | ## [Dependency Injector](https://python-dependency-injector.etc-labs.org) 39 | 40 | Dependency Injector is a dependency injection framework for Python. 41 | 42 | It helps implementing the dependency injection principle. 43 | 44 | Key features: 45 | 46 | * Providers 47 | * Overriding on the fly 48 | * Configuration (yaml, ini, json, pydantic, .env, etc) 49 | * Resources 50 | * Containers 51 | * Wiring 52 | * Asynchronous 53 | * Typing 54 | * Performance 55 | * Maturity 56 | -------------------------------------------------------------------------------- /fast_depends/_compat.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import typing 3 | from types import NoneType 4 | 5 | __all__ = ( 6 | "ExceptionGroup", 7 | "evaluate_forwardref", 8 | ) 9 | 10 | 11 | if sys.version_info < (3, 11): 12 | from exceptiongroup import ExceptionGroup as ExceptionGroup 13 | else: 14 | ExceptionGroup = ExceptionGroup 15 | 16 | 17 | def evaluate_forwardref( 18 | value: typing.Any, 19 | globalns: dict[str, typing.Any] | None = None, 20 | localns: dict[str, typing.Any] | None = None, 21 | type_params: tuple[typing.Any, ...] | None = None, 22 | ) -> typing.Any: 23 | """Behaves like typing._eval_type, except it won't raise an error if a forward reference can't be resolved.""" 24 | if value is None: 25 | value = NoneType 26 | 27 | elif isinstance(value, str): 28 | value = typing.ForwardRef(value, is_argument=False, is_class=True) 29 | 30 | try: 31 | return eval_type_backport(value, globalns, localns, type_params=type_params) 32 | except NameError: 33 | # the point of this function is to be tolerant to this case 34 | return value 35 | 36 | 37 | def eval_type_backport( 38 | value: typing.Any, 39 | globalns: dict[str, typing.Any] | None = None, 40 | localns: dict[str, typing.Any] | None = None, 41 | type_params: tuple[typing.Any, ...] | None = None, 42 | ) -> typing.Any: 43 | """Like `typing._eval_type`, but falls back to the `eval_type_backport` package if it's 44 | installed to let older Python versions use newer typing features. 45 | Specifically, this transforms `X | Y` into `typing.Union[X, Y]` 46 | and `list[X]` into `typing.List[X]` etc. (for all the types made generic in PEP 585) 47 | if the original syntax is not supported in the current Python version. 48 | """ 49 | try: 50 | if sys.version_info >= (3, 13): 51 | return typing._eval_type( # type: ignore 52 | value, globalns, localns, type_params=type_params 53 | ) 54 | 55 | else: 56 | return typing._eval_type( # type: ignore 57 | value, globalns, localns 58 | ) 59 | 60 | except TypeError as e: 61 | if not (isinstance(value, typing.ForwardRef) and is_backport_fixable_error(e)): 62 | raise 63 | 64 | try: 65 | from eval_type_backport import eval_type_backport as _eval_type_backport 66 | 67 | except ImportError: 68 | raise TypeError( 69 | f"You have a type annotation {value.__forward_arg__!r} " 70 | f"which makes use of newer typing features than are supported in your version of Python. " 71 | f"To handle this error, you should either remove the use of new syntax " 72 | f"or install the `eval_type_backport` package." 73 | ) from e 74 | 75 | else: 76 | return _eval_type_backport(value, globalns, localns, try_default=False) 77 | 78 | 79 | def is_backport_fixable_error(e: TypeError) -> bool: 80 | msg = str(e) 81 | return ( 82 | msg.startswith("unsupported operand type(s) for |: ") 83 | or "' object is not subscriptable" in msg 84 | ) 85 | -------------------------------------------------------------------------------- /docs/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: FastDepends 2 | site_description: FastDepends - extracted and cleared from HTTP domain logic FastAPI Dependency Injection System 3 | site_url: https://lancetnik.github.io/FastDepends/ 4 | 5 | repo_name: lancetnik/fastdepends 6 | repo_url: https://github.com/lancetnik/FastDepends 7 | edit_uri: https://github.com/lancetnik/FastDepends 8 | 9 | copyright: Copyright © 2023 - 2024 Pastukhov Nikita 10 | 11 | docs_dir: docs 12 | watch: 13 | - docs 14 | - docs_src 15 | 16 | extra_css: 17 | - assets/stylesheets/termynal.css 18 | - assets/stylesheets/custom.css 19 | 20 | extra_javascript: 21 | - assets/javascripts/termynal.js 22 | - assets/javascripts/custom.js 23 | 24 | theme: 25 | name: material 26 | palette: 27 | - media: "(prefers-color-scheme: light)" 28 | scheme: default 29 | primary: teal 30 | accent: teal 31 | toggle: 32 | icon: material/weather-sunny 33 | name: Switch to light mode 34 | - media: "(prefers-color-scheme: dark)" 35 | scheme: slate 36 | primary: teal 37 | accent: teal 38 | toggle: 39 | icon: material/weather-night 40 | name: Switch to dark mode 41 | features: 42 | - search.suggest 43 | - search.highlight 44 | - content.tabs.link 45 | - content.code.copy 46 | - content.code.annotate 47 | - navigation.top 48 | - navigation.footer 49 | i18n: 50 | prev: 'Previous' 51 | next: 'Next' 52 | icon: 53 | repo: fontawesome/brands/github 54 | 55 | plugins: 56 | - search 57 | - markdownextradata: 58 | data: data 59 | - minify: 60 | minify_html: true 61 | minify_js: true 62 | minify_css: true 63 | htmlmin_opts: 64 | remove_comments: true 65 | cache_safe: true 66 | 67 | markdown_extensions: # do not reorder 68 | - toc: 69 | permalink: true 70 | - markdown.extensions.codehilite: 71 | guess_lang: false 72 | - mdx_include: 73 | base_path: . 74 | - admonition 75 | - codehilite 76 | - extra 77 | - pymdownx.details 78 | - footnotes 79 | - pymdownx.superfences: 80 | custom_fences: 81 | - name: mermaid 82 | class: mermaid 83 | format: !!python/name:pymdownx.superfences.fence_code_format '' 84 | - pymdownx.emoji: 85 | emoji_index: !!python/name:material.extensions.emoji.twemoji 86 | emoji_generator: !!python/name:material.extensions.emoji.to_svg 87 | - pymdownx.tabbed: 88 | alternate_style: true 89 | - attr_list 90 | - md_in_html 91 | 92 | nav: 93 | - Welcome: index.md 94 | - How It Works: works.md 95 | - Potential Usages: usages.md 96 | - Tutorial: 97 | - tutorial/index.md 98 | - Class Based Dependencies: tutorial/classes.md 99 | - Yield Dependencies: tutorial/yield.md 100 | - Validations: tutorial/validations.md 101 | - Annotated: tutorial/annotated.md 102 | - Override Dependencies: tutorial/overrides.md 103 | - Advanced: 104 | - advanced/index.md 105 | - More Complex Example: advanced/starlette.md 106 | - Alternatives: alternatives.md 107 | - Contributing: contributing.md 108 | -------------------------------------------------------------------------------- /docs/docs/tutorial/validations.md: -------------------------------------------------------------------------------- 1 | # Pydantic Field 2 | 3 | `FastDepends` is able to use `pydantic.Field` as a default parameter to validate incoming argument 4 | 5 | ```python linenums="1" hl_lines="5" 6 | from pydantic import Field 7 | from fast_depends import inject 8 | 9 | @inject 10 | def func(a: str = Field(..., max_length=32)): 11 | ... 12 | ``` 13 | 14 | !!! note "Pydantic Documentation" 15 | To get more information and usage examples, please visit official [pydantic documentation](https://docs.pydantic.dev/usage/schema/#field-customization) 16 | 17 | All available fields are: 18 | 19 | * `default`: (a positional argument) the default value of the field. 20 | Since the `Field` replaces the field's default, this first argument can be used to set the default. 21 | Use ellipsis (`...`) to indicate the field is required. 22 | * `default_factory`: a zero-argument callable that will be called when a default value is needed for this field. 23 | Among other purposes, this can be used to set dynamic default values. 24 | It is forbidden to set both `default` and `default_factory`. 25 | * `alias`: the public name of the field 26 | * `const`: this argument *must* be the same as the field's default value if present. 27 | * `gt`: for numeric values (``int``, `float`, `Decimal`), adds a validation of "greater than" and an annotation 28 | of `exclusiveMinimum` to the JSON Schema 29 | * `ge`: for numeric values, this adds a validation of "greater than or equal" and an annotation of `minimum` to the 30 | JSON Schema 31 | * `lt`: for numeric values, this adds a validation of "less than" and an annotation of `exclusiveMaximum` to the 32 | JSON Schema 33 | * `le`: for numeric values, this adds a validation of "less than or equal" and an annotation of `maximum` to the 34 | JSON Schema 35 | * `multiple_of`: for numeric values, this adds a validation of "a multiple of" and an annotation of `multipleOf` to the 36 | JSON Schema 37 | * `max_digits`: for `Decimal` values, this adds a validation to have a maximum number of digits within the decimal. It 38 | does not include a zero before the decimal point or trailing decimal zeroes. 39 | * `decimal_places`: for `Decimal` values, this adds a validation to have at most a number of decimal places allowed. It 40 | does not include trailing decimal zeroes. 41 | * `min_items`: for list values, this adds a corresponding validation and an annotation of `minItems` to the 42 | JSON Schema 43 | * `max_items`: for list values, this adds a corresponding validation and an annotation of `maxItems` to the 44 | JSON Schema 45 | * `unique_items`: for list values, this adds a corresponding validation and an annotation of `uniqueItems` to the 46 | JSON Schema 47 | * `min_length`: for string values, this adds a corresponding validation and an annotation of `minLength` to the 48 | JSON Schema 49 | * `max_length`: for string values, this adds a corresponding validation and an annotation of `maxLength` to the 50 | JSON Schema 51 | * `allow_mutation`: a boolean which defaults to `True`. When False, the field raises a `TypeError` if the field is 52 | assigned on an instance. The model config must set `validate_assignment` to `True` for this check to be performed. 53 | * `regex`: for string values, this adds a Regular Expression validation generated from the passed string and an 54 | annotation of `pattern` to the JSON Schema -------------------------------------------------------------------------------- /docs/docs/advanced/index.md: -------------------------------------------------------------------------------- 1 | # CustomField 2 | 3 | !!! warning "Packages developers" 4 | This is the part of documentation will talks you about some features, that can be helpful to develop your own packages with `FastDepends` 5 | 6 | ## Custom Arguments Field 7 | 8 | If you wish to write your own **FastAPI** or another closely by architecture tool, you 9 | should define your own custom fields to specify application behavior. At **FastAPI** these fields are: 10 | 11 | * Body 12 | * Path 13 | * Query 14 | * Header 15 | * Cookie 16 | * Form 17 | * File 18 | * Security 19 | 20 | Custom fields can be used to adding something specific to a function arguments (like a *BackgroundTask*) or 21 | parsing incoming objects special way. You able decide by own, why and how you will use these tools. 22 | 23 | `FastDepends` grants you this opportunity a very intuitive and comfortable way. 24 | 25 | ### Let's write *Header* 26 | 27 | As an example, will try to implement **FastAPI** *Header* field 28 | 29 | ```python linenums="1" hl_lines="1 3-4" 30 | {!> docs_src/advanced/custom/class_declaration.py !} 31 | ``` 32 | 33 | Just import `fast_depends.library.CustomField` and implements `use` (async or sync) method. 34 | That's all. We already have own *Header* field to parse **kwargs** the special way. 35 | 36 | ### *Header* usage 37 | 38 | Now we already can use the *Header* field 39 | 40 | ```python linenums="1" hl_lines="4 8" 41 | {!> docs_src/advanced/custom/usage.py !} 42 | ``` 43 | 44 | As we defined, *Header* parse incoming **headers kwargs field**, get a parameter by name and put it to 45 | original function as an argument. 46 | 47 | ### More details 48 | 49 | `CustomField` has some fields you should know about 50 | 51 | ```python 52 | class CustomField: 53 | param_name: str 54 | cast: bool 55 | required: bool 56 | ``` 57 | 58 | * `param_name` - an original function argument name to replace by your field instance. It was `header_field` at the example above. 59 | * `required` - if CustomField is **required**, raises `pydantic.error_wrappers.ValidationError` if it is not present at final **kwargs** 60 | * `cast` - specify the typecasting behavior. Use *False* to disable pydantic typecasting for fields using with your *CustomField* 61 | 62 | ```python linenums="1" hl_lines="3 7 10-11" 63 | {!> docs_src/advanced/custom/cast_arg.py !} 64 | ``` 65 | 66 | !!! note 67 | Pydantic understands only python-native annotation or Pydantic classes. If users will annotate your fields by other classes, 68 | you should set `cast=False` to avoid pydantic exceptions. 69 | 70 | ```python 71 | def use(self, **kwargs: AnyDict) -> AnyDict: ... 72 | ``` 73 | 74 | Your *CustimField* objects receive casted to *kwargs* an original function incoming arguments at `use` method. 75 | Returning from the `use` method dict replace an original one. Original function will be executed **with a returned from your fields kwargs**. 76 | Be accurate with. 77 | 78 | And one more time: 79 | 80 | ```python linenums="1" hl_lines="6 9" 81 | original_kwargs = { "headers": { "field": 1 }} 82 | 83 | new_kwargs = Header().set_param_name("field").use(**kwargs) 84 | # new_kwargs = { 85 | # "headers": { "field": 1 }, 86 | # "field": 1 <-- new field from Header 87 | # } 88 | 89 | original_function(**new_kwargs) 90 | ``` 91 | 92 | I hope it was clearly enough right now. 93 | 94 | Also, custom fields using according their definition: from left to right. 95 | Next Custom Fields **kwargs** is a return of previous. 96 | 97 | An example: 98 | 99 | ```python linenums="1" 100 | @inject 101 | def func(field1 = Header(), field2 = Header()): ... 102 | ``` 103 | 104 | **field2** incoming kwargs is an output of **field1.use()** -------------------------------------------------------------------------------- /fast_depends/pydantic/_compat.py: -------------------------------------------------------------------------------- 1 | import json 2 | from collections.abc import Callable 3 | from typing import Any 4 | 5 | from pydantic import BaseModel, create_model 6 | from pydantic.version import VERSION as PYDANTIC_VERSION 7 | 8 | __all__ = ( 9 | "BaseModel", 10 | "create_model", 11 | "PYDANTIC_V2", 12 | "get_config_base", 13 | "ConfigDict", 14 | "TypeAdapter", 15 | "PydanticUserError", 16 | "dump_json", 17 | ) 18 | 19 | 20 | json_dumps: Callable[..., bytes] 21 | orjson: Any 22 | ujson: Any 23 | 24 | try: 25 | import orjson as orjson_ 26 | except ImportError: 27 | orjson = None 28 | else: 29 | orjson = orjson_ 30 | 31 | try: 32 | import ujson as ujson_ 33 | except ImportError: 34 | ujson = None 35 | else: 36 | ujson = ujson_ 37 | 38 | if orjson: 39 | json_loads = orjson.loads 40 | json_dumps = orjson.dumps 41 | 42 | elif ujson: 43 | json_loads = ujson.loads 44 | 45 | def json_dumps(*a: Any, **kw: Any) -> bytes: 46 | return ujson.dumps(*a, **kw).encode() # type: ignore[no-any-return] 47 | 48 | else: 49 | json_loads = json.loads 50 | 51 | def json_dumps(*a: Any, **kw: Any) -> bytes: 52 | return json.dumps(*a, **kw).encode() 53 | 54 | 55 | PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.") 56 | 57 | default_pydantic_config = {"arbitrary_types_allowed": True} 58 | 59 | # isort: off 60 | if PYDANTIC_V2: 61 | from pydantic import ConfigDict, TypeAdapter 62 | from pydantic.fields import FieldInfo 63 | from pydantic.errors import PydanticUserError 64 | from pydantic_core import to_json 65 | 66 | def model_schema(model: type[BaseModel]) -> dict[str, Any]: 67 | schema: dict[str, Any] = model.model_json_schema() 68 | return schema 69 | 70 | def get_config_base(config_data: ConfigDict | None = None) -> ConfigDict: 71 | return config_data or ConfigDict(**default_pydantic_config) # type: ignore[typeddict-item] 72 | 73 | def get_aliases(model: type[BaseModel]) -> tuple[str, ...]: 74 | return tuple(f.alias or name for name, f in get_model_fields(model).items()) 75 | 76 | def get_model_fields(model: type[BaseModel]) -> dict[str, FieldInfo]: 77 | fields: dict[str, FieldInfo] | None = getattr(model, "__pydantic_fields__", None) 78 | 79 | if fields is not None: 80 | return fields 81 | 82 | # Deprecated in Pydantic V2.11 to be removed in V3.0. 83 | model_fields: dict[str, FieldInfo] = model.model_fields 84 | return model_fields 85 | 86 | def dump_json(data: Any) -> bytes: 87 | return to_json(data) 88 | 89 | else: 90 | from pydantic.config import get_config, ConfigDict, BaseConfig 91 | from pydantic.fields import ModelField, FieldInfo 92 | from pydantic.json import pydantic_encoder 93 | 94 | TypeAdapter = None # type: ignore[assignment, misc] 95 | PydanticUserError = Exception # type: ignore[assignment, misc] 96 | 97 | def get_config_base(config_data: ConfigDict | None = None) -> type[BaseConfig]: # type: ignore[misc] 98 | return get_config(config_data or ConfigDict(**default_pydantic_config)) # type: ignore[typeddict-item, no-any-return] 99 | 100 | def model_schema(model: type[BaseModel]) -> dict[str, Any]: 101 | return model.schema() 102 | 103 | def get_aliases(model: type[BaseModel]) -> tuple[str, ...]: 104 | return tuple(f.alias or name for name, f in model.__fields__.items()) 105 | 106 | def get_model_fields(model: type[BaseModel]) -> dict[str, ModelField]: # type: ignore[misc] 107 | return model.__fields__ # type: ignore[return-value] 108 | 109 | def dump_json(data: Any) -> bytes: 110 | return json_dumps(data, default=pydantic_encoder) 111 | -------------------------------------------------------------------------------- /docs/docs/tutorial/index.md: -------------------------------------------------------------------------------- 1 | # Quickstart 2 | 3 | I suppose, if you are already here, you are exactly known about this library usage. 4 | 5 | It is using the same way as [FastAPI](https://fastapi.tiangolo.com/tutorial/dependencies/) is. 6 | 7 | But, I can remember you, if it's necessary. 8 | 9 | ## Basic usage 10 | 11 | === "Sync" 12 | ```python hl_lines="3-4" linenums="1" 13 | {!> docs_src/tutorial_1_quickstart/1_sync.py !} 14 | ``` 15 | 16 | === "Async" 17 | ```python hl_lines="4-5 7-8" linenums="1" 18 | {!> docs_src/tutorial_1_quickstart/1_async.py !} 19 | ``` 20 | 21 | !!! tip "Be accurate" 22 | At **async** code we can use **sync and async **dependencies both, but at **sync** runtime 23 | only **sync** dependencies are available. 24 | 25 | **First step**: we need to declare our dependency: it can be any Callable object. 26 | 27 | ??? note "Callable" 28 | "Callable" - object is able to be "called". It can be any function, class, or class method. 29 | 30 | Another words: if we can write following the code `my_object()` - `my_object` is "Callable" 31 | 32 | === "Sync" 33 | ```python hl_lines="2" linenums="6" 34 | {!> docs_src/tutorial_1_quickstart/1_sync.py [ln:5-8]!} 35 | ``` 36 | 37 | === "Async" 38 | ```python hl_lines="1 4 5" linenums="10" 39 | {!> docs_src/tutorial_1_quickstart/1_async.py [ln:10-16]!} 40 | ``` 41 | 42 | **Second step**: declare dependency required with `Depends` 43 | 44 | === "Sync" 45 | ```python hl_lines="3 5" linenums="6" 46 | {!> docs_src/tutorial_1_quickstart/1_sync.py [ln:5-10]!} 47 | ``` 48 | 49 | === "Async" 50 | ```python hl_lines="7 9" linenums="10" 51 | {!> docs_src/tutorial_1_quickstart/1_async.py [ln:10-18]!} 52 | ``` 53 | 54 | **Last step**: just use the dependencies calling result! 55 | 56 | That was easy, isn't it? 57 | 58 | !!! tip "Auto @inject" 59 | At the code above you can note, that original `Depends` functions wasn't decorated by `@inject`. 60 | 61 | It's true: all dependencies are decorated by default at using. Keep it at your mind. 62 | 63 | ## Nested Dependencies 64 | 65 | Dependencies are also able to contain their own dependencies. There is nothing unexpected with this case: 66 | just declare `Depends` requirement at original dependency function. 67 | 68 | === "Sync" 69 | ```python linenums="1" hl_lines="3-4 6-7 12-13" 70 | {!> docs_src/tutorial_1_quickstart/2_sync.py !} 71 | ``` 72 | 73 | 1. Call another_dependency here 74 | 75 | === "Async" 76 | ```python linenums="1" hl_lines="4-5 7-8 13-14" 77 | {!> docs_src/tutorial_1_quickstart/2_async.py !} 78 | ``` 79 | 80 | 1. Call another_dependency here 81 | 82 | !!! Tip "Cache" 83 | At the examples above `another_dependency` was called **AT ONCE!**. 84 | `FastDepends` caches all dependencies responses throw **ONE** `@inject` callstask. 85 | It means, that all nested dependencies give a one-time cached response. But, 86 | with different injected function calls, cache will differ too. 87 | 88 | To disable that behavior, just use `Depends(..., cache=False)`. This way dependency will 89 | be executed each time. 90 | 91 | ## Dependencies type casting 92 | 93 | If you remember, `FastDepends` casts function `return` too. This means, dependency output 94 | will be casted twice: at dependency function *out* and at the injector *in*. Nothing bad, 95 | if they are the same type, nothing overhead occurs. Just keep it in your mind. Or don't... 96 | My work is done anyway. 97 | 98 | ```python linenums="1" 99 | from fast_depends import inject, Depends 100 | 101 | def simple_dependency(a: int, b: int = 3) -> str: 102 | return a + b # cast 'return' to str first time 103 | 104 | @inject 105 | def method(a: int, d: int = Depends(simple_dependency)): 106 | # cast 'd' to int second time 107 | return a + d 108 | 109 | assert method("1") == 5 110 | ``` 111 | 112 | Also, `return` type will be cached. If you are using this dependency at `N` functions, 113 | cached return will be casted `N` times. 114 | 115 | To avoid this problem use [mypy](https://www.mypy-lang.org) to check types at your project or 116 | just be accurate with your outer annotations. -------------------------------------------------------------------------------- /docs/docs/tutorial/annotated.md: -------------------------------------------------------------------------------- 1 | # Using Annotated 2 | 3 | ## Why? 4 | 5 | Using `Annotated` has several benefits, one of the main ones is that now the parameters of your functions with `Annotated` would not be affected at all. 6 | 7 | If you call those functions in other places in your code, the actual default values will be kept, your editor will help you notice missing required arguments, Python will require you to pass required arguments at runtime, you will be able to use the same functions for different things and with different libraries. 8 | 9 | Because `Annotated` is standard **Python**, you still get all the benefits from editors and tools, like autocompletion, inline errors, etc. 10 | 11 | One of the biggest benefits is that now you can create `Annotated` dependencies that are then shared by multiple path operation functions, this will allow you to reduce a lot of code duplication in your codebase, while keeping all the support from editors and tools. 12 | 13 | ## Example 14 | 15 | For example, you could have code like this: 16 | 17 | ```python linenums="1" hl_lines="12 16" 18 | {!> docs_src/tutorial_4_annotated/not_annotated.py !} 19 | ``` 20 | 21 | There's a bit of code duplication for the dependency: 22 | ```python 23 | user: User = Depends(get_user) 24 | ``` 25 | 26 | ...the bigger the codebase, the more noticeable it is. 27 | 28 | Now you can create an annotated dependency once, like this: 29 | 30 | ```python 31 | CurrentUser = Annotated[User, Depends(get_user)] 32 | ``` 33 | 34 | And then you can reuse this `Annotated` dependency: 35 | 36 | ```python linenums="1" hl_lines="11 14 18" 37 | {!> docs_src/tutorial_4_annotated/annotated_39.py !} 38 | ``` 39 | 40 | ...and `CurrentUser` has all the typing information as `User`, so your editor will work as expected (autocompletion and everything), and **FastDepends** will be able to understand the dependency defined in `Annotated`. :sunglasses: 41 | 42 | ## Annotatable variants 43 | 44 | You able to use `Field` and `Depends` with `Annotated` as well 45 | 46 | ```python linenums="1" 47 | {!> docs_src/tutorial_4_annotated/annotated_variants_39.py !} 48 | ``` 49 | 50 | ## Limitations 51 | 52 | Python has a very structured function arguments declaration rules. 53 | 54 | ```python 55 | def function( 56 | required_positional_or_keyword_arguments_first, 57 | default_positional_or_keyword_arguments_second = None. 58 | *all_unrecognized_positional_arguments, 59 | required_keyword_only_arguments, 60 | default_keyword_only_arguments = None, 61 | **all_unrecognized_keyword_arguments, 62 | ): ... 63 | ``` 64 | 65 | !!! warning 66 | You can not declare **arguments without default** after **default arguments** was declared 67 | 68 | So 69 | 70 | ```python 71 | def func(user_id: int, user: CurrentUser): ... 72 | ``` 73 | 74 | ... is a **valid** python code 75 | 76 | But 77 | ```python 78 | def func(user_id: int | None = None, user: CurrentUser): ... # invalid code! 79 | ``` 80 | 81 | ... is **not**! You can't use the `Annotated` only argument after default argument declaration. 82 | 83 | --- 84 | 85 | There are some ways to write code above correct way: 86 | 87 | You can use `Annotated` with a default value 88 | 89 | ```python 90 | def func(user_id: int | None = None, user: CurrentUser = None): ... 91 | ``` 92 | 93 | Or you you can use `Annotated` with all arguments 94 | 95 | ```python 96 | UserId = Annotated[int, Field(...)] # Field(...) is a required 97 | def func(user_id: UserId, user: CurrentUser): ... 98 | ``` 99 | 100 | Also, from the Python view, the following code 101 | 102 | ```python 103 | # Potential invalid code! 104 | def func(user: CurrentUser, user_id: int | None = None): ... 105 | ``` 106 | 107 | But, **FastDepends** parse positional arguments according their position. 108 | 109 | So, calling the function above this way 110 | ```python 111 | func(1) 112 | ``` 113 | 114 | Will parses as the following kwargs 115 | ```python 116 | { "user": 1 } 117 | ``` 118 | And raises error 119 | 120 | But, named calling will be correct 121 | ```python 122 | func(user_id=1) # correct calling 123 | ``` 124 | 125 | !!! tip "" 126 | I really recommend *do not use* `Annotated` as a positional argument 127 | 128 | The best way to avoid all misunderstanding between you and Python - use `pydantic.Field` with `Annotated` everywhere 129 | 130 | Like in the following example 131 | 132 | ```python 133 | def func(user_id: Annotated[int, Field(...)], user: CurrentUser): ... 134 | ``` -------------------------------------------------------------------------------- /tests/sync/test_cast.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterator 2 | 3 | import pytest 4 | 5 | from fast_depends import inject 6 | from fast_depends.exceptions import ValidationError 7 | from tests.marks import serializer 8 | 9 | 10 | def test_skip_not_required(): 11 | @inject(serializer_cls=None) 12 | def some_func() -> int: 13 | return 1 14 | 15 | assert some_func(useless=object()) == 1 16 | 17 | 18 | def test_not_annotated(): 19 | @inject 20 | def some_func(a, b): 21 | return a + b 22 | 23 | assert isinstance(some_func("1", "2"), str) 24 | 25 | 26 | def test_arbitrary_args(): 27 | class ArbitraryType: 28 | def __init__(self): 29 | self.value = "value" 30 | 31 | @inject 32 | def some_func(a: ArbitraryType): 33 | return a 34 | 35 | assert isinstance(some_func(ArbitraryType()), ArbitraryType) 36 | 37 | 38 | def test_arbitrary_response(): 39 | class ArbitraryType: 40 | def __init__(self): 41 | self.value = "value" 42 | 43 | @inject 44 | def some_func(a: ArbitraryType) -> ArbitraryType: 45 | return a 46 | 47 | assert isinstance(some_func(ArbitraryType()), ArbitraryType) 48 | 49 | 50 | def test_args(): 51 | @inject 52 | def some_func(a, *ar): 53 | return a, ar 54 | 55 | assert (1, (2,)) == some_func(1, 2) 56 | 57 | 58 | def test_args_kwargs_1(): 59 | @inject 60 | def simple_func( 61 | a: int, 62 | *args: tuple[float, ...], 63 | b: int, 64 | **kwargs: dict[str, int], 65 | ): 66 | return a, args, b, kwargs 67 | 68 | assert (1, (2.0, 3.0), 3, {"key": 1}) == simple_func(1.0, 2.0, 3, b=3.0, key=1.0) 69 | 70 | 71 | def test_args_kwargs_2(): 72 | @inject 73 | def simple_func( 74 | a: int, 75 | *args: tuple[float, ...], 76 | b: int, 77 | ): 78 | return a, args, b 79 | 80 | assert (1, (2.0, 3.0), 3) == simple_func( 81 | 1.0, 82 | 2.0, 83 | 3, 84 | b=3.0, 85 | ) 86 | 87 | 88 | def test_args_kwargs_3(): 89 | @inject 90 | def simple_func(a: int, *, b: int): 91 | return a, b 92 | 93 | assert (1, 3) == simple_func( 94 | 1.0, 95 | b=3.0, 96 | ) 97 | 98 | 99 | def test_args_kwargs_4(): 100 | @inject 101 | def simple_func( 102 | *args: tuple[float, ...], 103 | **kwargs: dict[str, int], 104 | ): 105 | return args, kwargs 106 | 107 | assert ( 108 | (1.0, 2.0, 3.0), 109 | { 110 | "key": 1, 111 | "b": 3, 112 | }, 113 | ) == simple_func(1.0, 2.0, 3, b=3.0, key=1.0) 114 | 115 | 116 | def test_args_kwargs_5(): 117 | @inject 118 | def simple_func( 119 | *a: tuple[float, ...], 120 | **kw: dict[str, int], 121 | ): 122 | return a, kw 123 | 124 | assert ( 125 | (1.0, 2.0, 3.0), 126 | { 127 | "key": 1, 128 | "b": 3, 129 | }, 130 | ) == simple_func(1.0, 2.0, 3, b=3.0, key=1.0) 131 | 132 | 133 | @serializer 134 | class TestSerializer: 135 | def test_no_cast_result(self): 136 | @inject(cast_result=False) 137 | def some_func(a: int, b: int) -> str: 138 | return a + b 139 | 140 | assert some_func("1", "2") == 3 141 | 142 | def test_annotated_partial(self): 143 | @inject 144 | def some_func(a, b: int): 145 | assert isinstance(b, int) 146 | return a + b 147 | 148 | assert isinstance(some_func(1, "2"), int) 149 | 150 | def test_types_casting(self): 151 | @inject 152 | def some_func(a: int, b: int) -> float: 153 | assert isinstance(a, int) 154 | assert isinstance(b, int) 155 | r = a + b 156 | assert isinstance(r, int) 157 | return r 158 | 159 | assert isinstance(some_func("1", "2"), float) 160 | 161 | def test_types_casting_from_str(self): 162 | @inject 163 | def some_func(a: "int") -> float: 164 | return a 165 | 166 | assert isinstance(some_func("1"), float) 167 | 168 | def test_wrong_incoming_types(self): 169 | @inject 170 | def some_func(a: int): # pragma: no cover 171 | return a 172 | 173 | with pytest.raises(ValidationError): 174 | some_func({"key", 1}) 175 | 176 | def test_wrong_return_type(self): 177 | @inject 178 | def some_func(a: int) -> dict: 179 | return a 180 | 181 | with pytest.raises(ValidationError): 182 | some_func("2") 183 | 184 | def test_generator(self): 185 | @inject 186 | def simple_func(a: str) -> int: 187 | for _ in range(2): 188 | yield a 189 | 190 | for i in simple_func("1"): 191 | assert i == 1 192 | 193 | def test_generator_iterator_type(self): 194 | @inject 195 | def simple_func(a: str) -> Iterator[int]: 196 | for _ in range(2): 197 | yield a 198 | 199 | for i in simple_func("1"): 200 | assert i == 1 201 | -------------------------------------------------------------------------------- /tests/async/test_cast.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterator 2 | 3 | import pytest 4 | 5 | from fast_depends import inject 6 | from fast_depends.exceptions import ValidationError 7 | from tests.marks import serializer 8 | 9 | 10 | @pytest.mark.anyio 11 | async def test_skip_not_required() -> None: 12 | @inject(serializer_cls=None) 13 | async def some_func() -> int: 14 | return 1 15 | 16 | assert await some_func(useless=object()) == 1 17 | 18 | 19 | @pytest.mark.anyio 20 | async def test_not_annotated() -> None: 21 | @inject 22 | async def some_func(a, b): 23 | return a + b 24 | 25 | assert isinstance(await some_func("1", "2"), str) 26 | 27 | 28 | @pytest.mark.anyio 29 | async def test_arbitrary_args() -> None: 30 | class ArbitraryType: 31 | def __init__(self): 32 | self.value = "value" 33 | 34 | @inject 35 | async def some_func(a: ArbitraryType): 36 | return a 37 | 38 | assert isinstance(await some_func(ArbitraryType()), ArbitraryType) 39 | 40 | 41 | @pytest.mark.anyio 42 | async def test_arbitrary_response() -> None: 43 | class ArbitraryType: 44 | def __init__(self): 45 | self.value = "value" 46 | 47 | @inject 48 | async def some_func(a: ArbitraryType) -> ArbitraryType: 49 | return a 50 | 51 | assert isinstance(await some_func(ArbitraryType()), ArbitraryType) 52 | 53 | 54 | @pytest.mark.anyio 55 | async def test_args() -> None: 56 | @inject 57 | async def some_func(a, *ar): 58 | return a, ar 59 | 60 | assert (1, (2,)) == await some_func(1, 2) 61 | 62 | 63 | @pytest.mark.anyio 64 | async def test_args_kwargs_1() -> None: 65 | @inject 66 | async def simple_func( 67 | a: int, 68 | *args: tuple[float, ...], 69 | b: int, 70 | **kwargs: dict[str, int], 71 | ): 72 | return a, args, b, kwargs 73 | 74 | assert (1, (2.0, 3.0), 3, {"key": 1}) == await simple_func( 75 | 1.0, 2.0, 3, b=3.0, key=1.0 76 | ) 77 | 78 | 79 | @pytest.mark.anyio 80 | async def test_args_kwargs_2() -> None: 81 | @inject 82 | async def simple_func( 83 | a: int, 84 | *args: tuple[float, ...], 85 | b: int, 86 | ): 87 | return a, args, b 88 | 89 | assert (1, (2.0, 3.0), 3) == await simple_func( 90 | 1.0, 91 | 2.0, 92 | 3, 93 | b=3.0, 94 | ) 95 | 96 | 97 | @pytest.mark.anyio 98 | async def test_args_kwargs_3() -> None: 99 | @inject 100 | async def simple_func(a: int, *, b: int): 101 | return a, b 102 | 103 | assert (1, 3) == await simple_func( 104 | 1.0, 105 | b=3.0, 106 | ) 107 | 108 | 109 | @pytest.mark.anyio 110 | async def test_args_kwargs_4() -> None: 111 | @inject 112 | async def simple_func( 113 | *args: tuple[float, ...], 114 | **kwargs: dict[str, int], 115 | ): 116 | return args, kwargs 117 | 118 | assert ( 119 | (1.0, 2.0, 3.0), 120 | { 121 | "key": 1, 122 | "b": 3, 123 | }, 124 | ) == await simple_func(1.0, 2.0, 3, b=3.0, key=1.0) 125 | 126 | 127 | @pytest.mark.anyio 128 | async def test_args_kwargs_5() -> None: 129 | @inject 130 | async def simple_func( 131 | *a: tuple[float, ...], 132 | **kw: dict[str, int], 133 | ): 134 | return a, kw 135 | 136 | assert ( 137 | (1.0, 2.0, 3.0), 138 | { 139 | "key": 1, 140 | "b": 3, 141 | }, 142 | ) == await simple_func(1.0, 2.0, 3, b=3.0, key=1.0) 143 | 144 | 145 | @serializer 146 | @pytest.mark.anyio 147 | class TestSerialization: 148 | async def test_no_cast_result(self) -> None: 149 | @inject(cast_result=False) 150 | async def some_func(a: int, b: int) -> str: 151 | return a + b 152 | 153 | assert await some_func("1", "2") == 3 154 | 155 | async def test_annotated_partial(self) -> None: 156 | @inject 157 | async def some_func(a, b: int): 158 | assert isinstance(b, int) 159 | return a + b 160 | 161 | assert isinstance(await some_func(1, "2"), int) 162 | 163 | async def test_types_casting(self) -> None: 164 | @inject 165 | async def some_func(a: int, b: int) -> float: 166 | assert isinstance(a, int) 167 | assert isinstance(b, int) 168 | r = a + b 169 | assert isinstance(r, int) 170 | return r 171 | 172 | assert isinstance(await some_func("1", "2"), float) 173 | 174 | async def test_types_casting_from_str(self) -> None: 175 | @inject 176 | async def some_func(a: "int") -> float: 177 | return a 178 | 179 | assert isinstance(await some_func("1"), float) 180 | 181 | async def test_wrong_incoming_types(self) -> None: 182 | @inject 183 | async def some_func(a: int): # pragma: no cover 184 | return a 185 | 186 | with pytest.raises(ValidationError): 187 | await some_func({"key", 1}) 188 | 189 | async def test_wrong_return_types(self) -> None: 190 | @inject 191 | async def some_func(a: int) -> dict: 192 | return a 193 | 194 | with pytest.raises(ValidationError): 195 | await some_func("2") 196 | 197 | async def test_generator(self) -> None: 198 | @inject 199 | async def simple_func(a: str) -> int: 200 | for _ in range(2): 201 | yield a 202 | 203 | async for i in simple_func("1"): 204 | assert i == 1 205 | 206 | async def test_generator_iterator_type(self) -> None: 207 | @inject 208 | async def simple_func(a: str) -> Iterator[int]: 209 | for _ in range(2): 210 | yield a 211 | 212 | async for i in simple_func("1"): 213 | assert i == 1 214 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | pull_request: 7 | types: 8 | - opened 9 | - synchronize 10 | - ready_for_review 11 | 12 | jobs: 13 | test-pydantic: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"] 18 | pydantic-version: ["pydantic-v1", "pydantic-v2"] 19 | exclude: 20 | - python-version: "3.14" 21 | pydantic-version: "pydantic-v1" 22 | - python-version: "3.14t" 23 | pydantic-version: "pydantic-v1" 24 | fail-fast: false 25 | 26 | steps: 27 | - uses: actions/checkout@v6 28 | - name: Set up Python 29 | uses: actions/setup-python@v6 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | - uses: actions/cache@v5 33 | id: cache 34 | with: 35 | path: ${{ env.pythonLocation }} 36 | key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-test-v03 37 | - name: Install Dependencies 38 | if: steps.cache.outputs.cache-hit != 'true' 39 | run: pip install --group test . 40 | - name: Install Pydantic v1 41 | if: matrix.pydantic-version == 'pydantic-v1' 42 | run: pip install "pydantic>=1.10.0,<2.0.0" 43 | - name: Install Pydantic v2 44 | if: matrix.pydantic-version == 'pydantic-v2' 45 | run: pip install "pydantic>=2.0.0,<3.0.0" 46 | - run: mkdir coverage 47 | - name: Test 48 | run: bash scripts/test.sh 49 | env: 50 | COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}-${{ matrix.pydantic-version }} 51 | CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}-${{ matrix.pydantic-version }} 52 | - name: Store coverage files 53 | uses: actions/upload-artifact@v6 54 | with: 55 | name: .coverage.${{ runner.os }}-py${{ matrix.python-version }}-${{ matrix.pydantic-version }} 56 | path: coverage 57 | if-no-files-found: error 58 | include-hidden-files: true 59 | 60 | test-msgspec: 61 | runs-on: ubuntu-latest 62 | strategy: 63 | matrix: 64 | python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"] 65 | fail-fast: false 66 | 67 | steps: 68 | - uses: actions/checkout@v6 69 | - name: Set up Python 70 | uses: actions/setup-python@v6 71 | with: 72 | python-version: ${{ matrix.python-version }} 73 | - uses: actions/cache@v5 74 | id: cache 75 | with: 76 | path: ${{ env.pythonLocation }} 77 | key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-test-v03 78 | - name: Install Dependencies 79 | if: steps.cache.outputs.cache-hit != 'true' 80 | run: pip install --group test ".[msgspec]" 81 | - run: mkdir coverage 82 | - name: Test 83 | run: bash scripts/test.sh 84 | env: 85 | COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}-msgspec 86 | CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}-msgspec 87 | - name: Store coverage files 88 | uses: actions/upload-artifact@v6 89 | with: 90 | name: .coverage.${{ runner.os }}-py${{ matrix.python-version }}-msgspec 91 | path: coverage 92 | if-no-files-found: error 93 | include-hidden-files: true 94 | 95 | test-no-serializer: 96 | runs-on: ubuntu-latest 97 | 98 | steps: 99 | - uses: actions/checkout@v6 100 | - uses: actions/setup-python@v6 101 | with: 102 | python-version: '3.12' 103 | - uses: actions/cache@v5 104 | id: cache 105 | with: 106 | path: ${{ env.pythonLocation }} 107 | key: no-pydantic-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }} 108 | - name: Install Dependencies 109 | if: steps.cache.outputs.cache-hit != 'true' 110 | run: pip install --group test . 111 | - run: mkdir coverage 112 | - name: Test 113 | run: bash scripts/test.sh 114 | env: 115 | COVERAGE_FILE: coverage/.coverage.no-serializer 116 | CONTEXT: no-serializer 117 | - name: Store coverage files 118 | uses: actions/upload-artifact@v6 119 | with: 120 | name: .coverage.no-serializer 121 | path: coverage 122 | if-no-files-found: error 123 | include-hidden-files: true 124 | 125 | coverage-combine: 126 | needs: [test-pydantic,test-msgspec,test-no-serializer] 127 | runs-on: ubuntu-latest 128 | 129 | steps: 130 | - uses: actions/checkout@v6 131 | 132 | - uses: actions/setup-python@v6 133 | with: 134 | python-version: '3.13' 135 | 136 | - name: Get coverage files 137 | uses: actions/download-artifact@v7 138 | with: 139 | pattern: .coverage* 140 | path: coverage 141 | merge-multiple: true 142 | 143 | - run: pip install coverage[toml] 144 | 145 | - run: ls -la coverage 146 | - run: coverage combine coverage 147 | - run: coverage report 148 | - run: coverage html --show-contexts --title "FastDepends coverage for ${{ github.sha }}" 149 | 150 | - name: Store coverage html 151 | uses: actions/upload-artifact@v6 152 | with: 153 | name: coverage-html 154 | path: htmlcov 155 | include-hidden-files: true 156 | 157 | # https://github.com/marketplace/actions/alls-green#why 158 | check: # This job does nothing and is only used for the branch protection 159 | if: always() 160 | 161 | needs: 162 | - coverage-combine 163 | 164 | runs-on: ubuntu-latest 165 | 166 | steps: 167 | - name: Decide whether the needed jobs succeeded or failed 168 | uses: re-actors/alls-green@release/v1 169 | with: 170 | jobs: ${{ toJSON(needs) }} 171 | -------------------------------------------------------------------------------- /docs/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - toc 4 | --- 5 | 6 | # FastDepends 7 | 8 | 9 | Tests coverage 10 | 11 | 12 | Coverage 13 | 14 | 15 | Package version 16 | 17 | 18 | downloads 19 | 20 | 21 | Supported Python versions 22 | 23 | 24 | GitHub 25 | 26 | 27 | FastDepends - FastAPI Dependency Injection system extracted from FastAPI and cleared of all HTTP logic. 28 | This is a small library which provides you with the ability to use lovely FastAPI interfaces in your own 29 | projects or tools. 30 | 31 | Thanks to [*fastapi*](https://fastapi.tiangolo.com/) and [*pydantic*](https://docs.pydantic.dev/) projects for this 32 | great functionality. This package is just a small change of the original FastAPI sources to provide DI functionality in a pure-Python way. 33 | 34 | Async and sync modes are both supported. 35 | 36 | # For why? 37 | 38 | This project should be extremely helpful to boost your not-**FastAPI** applications (even **Flask**, I know that u like some legacy). 39 | 40 | Also the project can be a core of your own framework for anything. Actually, it was build for my another project - :rocket:[**Propan**](https://github.com/Lancetnik/Propan):rocket: (and [**FastStream**](https://github.com/airtai/faststream)), check it to see full-featured **FastDepends** usage example. 41 | 42 | ## Installation 43 | 44 |
45 | ```console 46 | $ pip install fast-depends 47 | ---> 100% 48 | ``` 49 |
50 | 51 | ## Usage 52 | 53 | There is no way to make Dependency Injection easier 54 | 55 | You can use this library without any frameworks in both **sync** and **async** code. 56 | 57 | === "Async code" 58 | ```python hl_lines="8-13" linenums="1" 59 | {!> docs_src/home/1_async_tutor.py !} 60 | ``` 61 | 62 | === "Sync code" 63 | ```python hl_lines="6-11" linenums="1" 64 | {!> docs_src/home/1_sync_tutor.py !} 65 | ``` 66 | 67 | `@inject` decorator plays multiple roles at the same time: 68 | 69 | * resolve *Depends* classes 70 | * cast types according to Python annotation 71 | * validate incoming parameters using *pydantic* 72 | 73 | !!! tip 74 | Synchronous code is fully supported in this package: without any `async_to_sync`, `run_sync`, `syncify` or any other tricks. 75 | 76 | Also, *FastDepends* casts functions' return values the same way, it can be very helpful in building your own tools. 77 | 78 | These are two main defferences from native FastAPI DI System. 79 | 80 | ## Dependencies Overriding 81 | 82 | Also, **FastDepends** can be used as a lightweight DI container. Using it, you can easily override basic dependencies with application startup or in tests. 83 | 84 | ```python linenums="1" hl_lines="16" 85 | from typing import Annotated 86 | from fast_depends import Depends, dependency_provider, inject 87 | 88 | def abc_func() -> int: 89 | raise 2 90 | 91 | def real_func() -> int: 92 | return 1 93 | 94 | @inject 95 | def func( 96 | dependency: Annotated[int, Depends(abc_func)] 97 | ) -> int: 98 | return dependency 99 | 100 | with dependency_provider.scope(abc_func, real_func): 101 | assert func() == 1 102 | ``` 103 | 104 | `dependency_provider` in this case is just a default container already declared in the library. But you can use your own the same way: 105 | 106 | ```python linenums="1" hl_lines="4 12 18" 107 | from typing import Annotated 108 | from fast_depends import Depends, Provider, inject 109 | 110 | provider = Provider() 111 | 112 | def abc_func() -> int: 113 | raise 2 114 | 115 | def real_func() -> int: 116 | return 1 117 | 118 | @inject(dependency_overrides_provider=provider) 119 | def func( 120 | dependency: Annotated[int, Depends(abc_func)] 121 | ) -> int: 122 | return dependency 123 | 124 | with provider.scope(abc_func, real_func): 125 | assert func() == 1 126 | ``` 127 | 128 | This way you can inherit the basic `Provider` class and define any extra logic you want! 129 | 130 | --- 131 | 132 | ## Custom Fields 133 | 134 | If you wish to write your own FastAPI or another closely by architecture tool, you should define your own custom fields to specify application behavior. 135 | 136 | Custom fields can be used to adding something specific to a function arguments (like a BackgroundTask) or parsing incoming objects special way. You able decide by own, why and how you will use these tools. 137 | 138 | FastDepends grants you this opportunity a very intuitive and comfortable way. 139 | 140 | ```python linenums="1" 141 | from fast_depends import inject 142 | from fast_depends.library import CustomField 143 | 144 | class Header(CustomField): 145 | def use(self, /, **kwargs: AnyDict) -> AnyDict: 146 | kwargs = super().use(**kwargs) 147 | kwargs[self.param_name] = kwargs["headers"][self.param_name] 148 | return kwargs 149 | 150 | @inject 151 | def my_func(header_field: int = Header()): 152 | return header_field 153 | 154 | assert my_func( 155 | headers={"header_field": "1"} 156 | ) == 1 157 | ``` 158 | 159 | More details you can find at [advanced](/FastDepends/advanced) tutorial 160 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["uv_build>=0.9.2"] 3 | build-backend = "uv_build" 4 | 5 | [tool.uv.build-backend] 6 | module-root = "" 7 | 8 | [project] 9 | name = "fast-depends" 10 | description = "FastDepends - extracted and cleared from HTTP domain logic FastAPI Dependency Injection System. Async and sync are both supported." 11 | readme = "README.md" 12 | authors = [ 13 | { name = "Nikita Pastukhov", email = "nikita@pastukhov-dev.ru" }, 14 | ] 15 | license = "MIT" 16 | license-files = ["LICENSE"] 17 | 18 | keywords = ["fastapi", "dependency injection"] 19 | 20 | version = "3.0.5" 21 | 22 | requires-python = ">=3.10" 23 | 24 | classifiers = [ 25 | "Development Status :: 5 - Production/Stable", 26 | "License :: OSI Approved :: MIT License", 27 | "Programming Language :: Python", 28 | "Programming Language :: Python :: Implementation :: CPython", 29 | "Programming Language :: Python :: 3", 30 | "Programming Language :: Python :: 3 :: Only", 31 | "Programming Language :: Python :: 3.10", 32 | "Programming Language :: Python :: 3.11", 33 | "Programming Language :: Python :: 3.12", 34 | "Programming Language :: Python :: 3.13", 35 | "Programming Language :: Python :: 3.14", 36 | "Operating System :: OS Independent", 37 | "Topic :: Software Development :: Libraries :: Python Modules", 38 | "Topic :: Software Development :: Libraries", 39 | "Topic :: Software Development", 40 | "Typing :: Typed", 41 | "Intended Audience :: Developers", 42 | "Intended Audience :: Information Technology", 43 | "Framework :: Pydantic", 44 | "Framework :: Pydantic :: 1", 45 | "Framework :: Pydantic :: 2", 46 | ] 47 | 48 | dependencies = [ 49 | "anyio>=4.0.0,<5.0.0", 50 | "typing-extensions!=4.12.1", 51 | ] 52 | 53 | [project.optional-dependencies] 54 | pydantic = [ 55 | "pydantic>=1.7.4,!=1.8,!=1.8.1,<3.0.0", 56 | ] 57 | 58 | msgspec = [ 59 | "msgspec", 60 | ] 61 | 62 | [dependency-groups] 63 | docs = [ 64 | "mkdocs-material>=9.4.0,<10.0.0", 65 | "mkdocs-minify-plugin>=0.7.0,<1.0.0", 66 | "mdx-include>=1.4.1,<2.0.0", 67 | "mkdocs-markdownextradata-plugin>=0.1.7,<0.3.0", 68 | ] 69 | 70 | test = [ 71 | "coverage[toml]>=7.2.0,<8.0.0", 72 | "pytest>=8.0.0,<10", 73 | "dirty-equals>=0.7.0,<0.12", 74 | "annotated_types", 75 | ] 76 | 77 | lint = [ 78 | "mypy==1.19.1", 79 | "ruff==0.14.10", 80 | "codespell==2.4.1", 81 | "types-ujson>=5.10.0.20250822", 82 | ] 83 | 84 | dev = [ 85 | "fast-depends[pydantic,msgspec]", 86 | {include-group = "docs"}, 87 | {include-group = "test"}, 88 | {include-group = "lint"}, 89 | ] 90 | 91 | [project.urls] 92 | Homepage = "https://lancetnik.github.io/FastDepends/" 93 | Documentation = "https://lancetnik.github.io/FastDepends/" 94 | Tracker = "https://github.com/Lancetnik/FastDepends/issues" 95 | Source = "https://github.com/Lancetnik/FastDepends" 96 | 97 | [tool.hatch.metadata] 98 | allow-direct-references = true 99 | allow-ambiguous-features = true 100 | 101 | [tool.hatch.build] 102 | skip-excluded-dirs = true 103 | exclude = [ 104 | "/tests", 105 | "/docs", 106 | ] 107 | 108 | [tool.mypy] 109 | strict = true 110 | ignore_missing_imports = true 111 | python_version = "3.10" 112 | files = [ 113 | "fast_depends", 114 | ] 115 | 116 | [tool.isort] 117 | profile = "black" 118 | known_third_party = ["pydantic", "anyio"] 119 | 120 | [tool.ruff] 121 | target-version = "py310" 122 | 123 | fix = true 124 | 125 | line-length = 90 126 | indent-width = 4 127 | 128 | exclude = [ 129 | "docs/docs_src", 130 | ] 131 | 132 | [tool.ruff.lint] 133 | select = [ 134 | "E", # pycodestyle errors https://docs.astral.sh/ruff/rules/#error-e 135 | "W", # pycodestyle warnings https://docs.astral.sh/ruff/rules/#warning-w 136 | "I", # isort https://docs.astral.sh/ruff/rules/#isort-i 137 | "F", # pyflakes https://docs.astral.sh/ruff/rules/#pyflakes-f 138 | "C4", # flake8-comprehensions https://docs.astral.sh/ruff/rules/#flake8-comprehensions-c4 139 | "B", # flake8-bugbear https://docs.astral.sh/ruff/rules/#flake8-bugbear-b 140 | "Q", # flake8-quotes https://docs.astral.sh/ruff/rules/#flake8-quotes-q 141 | "T20", # flake8-print https://docs.astral.sh/ruff/rules/#flake8-print-t20 142 | "PERF", # Perflint https://docs.astral.sh/ruff/rules/#perflint-perf 143 | "UP", # pyupgrade https://docs.astral.sh/ruff/rules/#pyupgrade-up 144 | ] 145 | 146 | ignore = [ 147 | "E501", # line too long, handled by black 148 | "C901", # too complex 149 | ] 150 | 151 | [tool.ruff.lint.flake8-bugbear] 152 | extend-immutable-calls = [ 153 | "fast_depends.Depends", 154 | "pydantic.Field", 155 | "AsyncHeader", 156 | "Header", 157 | "MyDep", 158 | ] 159 | 160 | [tool.pytest.ini_options] 161 | minversion = "7.0" 162 | addopts = "-q" 163 | testpaths = [ 164 | "tests", 165 | ] 166 | 167 | [tool.coverage.run] 168 | parallel = true 169 | branch = true 170 | concurrency = [ 171 | "multiprocessing", 172 | "thread" 173 | ] 174 | source = [ 175 | "fast_depends", 176 | "tests" 177 | ] 178 | context = '${CONTEXT}' 179 | omit = [ 180 | "**/__init__.py", 181 | ] 182 | 183 | [tool.coverage.report] 184 | show_missing = true 185 | skip_empty = true 186 | exclude_also = [ 187 | "if __name__ == .__main__.:", 188 | "self.logger", 189 | "def __repr__", 190 | "lambda: None", 191 | "from .*", 192 | "import .*", 193 | '@(abc\.)?abstractmethod', 194 | "raise NotImplementedError", 195 | 'raise AssertionError', 196 | 'raise ValueError', 197 | 'logger\..*', 198 | "pass", 199 | '\.\.\.', 200 | ] 201 | omit = [ 202 | '*/__about__.py', 203 | '*/__main__.py', 204 | '*/__init__.py', 205 | ] 206 | 207 | [tool.codespell] 208 | skip = "venv,./docs/site/*,*.js,*.css" 209 | 210 | [[tool.mypy.overrides]] 211 | module = ["fast_depends.pydantic._compat.*"] 212 | disable_error_code = ["attr-defined"] 213 | 214 | [[tool.mypy.overrides]] 215 | module = ["fast_depends.pydantic.*"] 216 | disable_error_code = ["unused-ignore"] 217 | -------------------------------------------------------------------------------- /tests/library/test_custom.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from time import monotonic_ns 3 | from typing import Annotated, Any 4 | 5 | import anyio 6 | import pytest 7 | 8 | from fast_depends import Depends, inject 9 | from fast_depends.exceptions import ValidationError 10 | from fast_depends.library import CustomField 11 | from tests.marks import serializer 12 | 13 | 14 | class Header(CustomField): 15 | def use(self, /, **kwargs: Any) -> dict[str, Any]: 16 | kwargs = super().use(**kwargs) 17 | if v := kwargs.get("headers", {}).get(self.param_name): 18 | kwargs[self.param_name] = v 19 | return kwargs 20 | 21 | 22 | class FieldHeader(Header): 23 | def __init__(self, *, cast: bool = True, required: bool = True) -> None: 24 | super().__init__(cast=cast, required=required) 25 | self.field = True 26 | 27 | def use_field(self, kwargs: Any) -> None: 28 | if v := kwargs.get("headers", {}).get(self.param_name): # pragma: no branch 29 | kwargs[self.param_name] = v 30 | 31 | 32 | class AsyncHeader(Header): 33 | async def use(self, /, **kwargs: Any) -> dict[str, Any]: 34 | return super().use(**kwargs) 35 | 36 | 37 | class AsyncFieldHeader(Header): 38 | def __init__(self, *, cast: bool = True, required: bool = True) -> None: 39 | super().__init__(cast=cast, required=required) 40 | self.field = True 41 | 42 | async def use_field(self, kwargs: Any) -> None: 43 | await anyio.sleep(0.1) 44 | if v := kwargs.get("headers", {}).get(self.param_name): # pragma: no branch 45 | kwargs[self.param_name] = v 46 | 47 | 48 | def test_header(): 49 | @inject 50 | def sync_catch(key: int = Header()): # noqa: B008 51 | return key 52 | 53 | assert sync_catch(headers={"key": 1}) == 1 54 | 55 | 56 | def test_custom_with_class(): 57 | class T: 58 | @inject 59 | def __init__(self, key: int = Header()): 60 | self.key = key 61 | 62 | assert T(headers={"key": 1}).key == 1 63 | 64 | 65 | def test_reusable_annotated() -> None: 66 | HeaderKey = Annotated[float, Header(cast=False)] 67 | 68 | @inject 69 | def sync_catch(key: HeaderKey) -> float: 70 | return key 71 | 72 | @inject 73 | def sync_catch2(key2: HeaderKey) -> float: 74 | return key2 75 | 76 | assert sync_catch(headers={"key": 1}) == 1 77 | assert sync_catch2(headers={"key2": 1}) == 1 78 | 79 | 80 | def test_arguments_mapping(): 81 | @inject 82 | def func( 83 | d: int = CustomField(cast=False), 84 | b: int = CustomField(cast=False), 85 | c: int = CustomField(cast=False), 86 | a: int = CustomField(cast=False), 87 | ): 88 | assert d == 4 89 | assert b == 2 90 | assert c == 3 91 | assert a == 1 92 | 93 | for _ in range(50): 94 | func(4, 2, 3, 1) 95 | 96 | 97 | @serializer 98 | class TestSerializer: 99 | @pytest.mark.anyio 100 | async def test_header_async(self): 101 | @inject 102 | async def async_catch(key: int = Header()): # noqa: B008 103 | return key 104 | 105 | assert (await async_catch(headers={"key": "1"})) == 1 106 | 107 | def test_multiple_header(self): 108 | @inject 109 | def sync_catch(key: str = Header(), key2: int = Header()): # noqa: B008 110 | assert key == "1" 111 | assert key2 == 2 112 | 113 | sync_catch(headers={"key": "1", "key2": "2"}) 114 | 115 | @pytest.mark.anyio 116 | async def test_async_header_async(self): 117 | @inject 118 | async def async_catch( # noqa: B008 119 | key: float = AsyncHeader(), key2: int = AsyncHeader() 120 | ): 121 | return key, key2 122 | 123 | assert (await async_catch(headers={"key": "1", "key2": 1})) == (1.0, 1) 124 | 125 | def test_sync_field_header(self): 126 | @inject 127 | def sync_catch(key: float = FieldHeader(), key2: int = FieldHeader()): # noqa: B008 128 | return key, key2 129 | 130 | assert sync_catch(headers={"key": "1", "key2": 1}) == (1.0, 1) 131 | 132 | @pytest.mark.anyio 133 | async def test_async_field_header(self): 134 | @inject 135 | async def async_catch( # noqa: B008 136 | key: float = AsyncFieldHeader(), key2: int = AsyncFieldHeader() 137 | ): 138 | return key, key2 139 | 140 | start = monotonic_ns() 141 | assert (await async_catch(headers={"key": "1", "key2": 1})) == (1.0, 1) 142 | assert (monotonic_ns() - start) / 10**9 < 0.2 143 | 144 | def test_async_header_sync(self): 145 | with pytest.raises(AssertionError): 146 | 147 | @inject 148 | def sync_catch(key: str = AsyncHeader()): # pragma: no cover # noqa: B008 149 | return key 150 | 151 | def test_header_annotated(self): 152 | @inject 153 | def sync_catch(key: Annotated[int, Header()]): 154 | return key 155 | 156 | assert sync_catch(headers={"key": 1}) == 1 157 | 158 | def test_header_required(self): 159 | @inject 160 | def sync_catch(key=Header()): # pragma: no cover # noqa: B008 161 | return key 162 | 163 | with pytest.raises(ValidationError): 164 | sync_catch() 165 | 166 | def test_header_not_required(self): 167 | @inject 168 | def sync_catch(key2=Header(required=False)): # noqa: B008 169 | assert key2 is None 170 | 171 | sync_catch() 172 | 173 | def test_header_not_required_with_default(self): 174 | @inject 175 | def sync_catch(key2: Annotated[str, Header(required=False)] = "1"): # noqa: B008 176 | return key2 == "1" 177 | 178 | assert sync_catch() 179 | 180 | def test_depends(self): 181 | def dep(key: Annotated[int, Header()]): 182 | return key 183 | 184 | @inject 185 | def sync_catch(k=Depends(dep)): 186 | return k 187 | 188 | assert sync_catch(headers={"key": 1}) == 1 189 | 190 | def test_not_cast(self): 191 | @inject 192 | def sync_catch(key: Annotated[float, Header(cast=False)]): 193 | return key 194 | 195 | assert sync_catch(headers={"key": 1}) == 1 196 | 197 | @inject 198 | def sync_catch(key: logging.Logger = Header(cast=False)): # noqa: B008 199 | return key 200 | 201 | assert sync_catch(headers={"key": 1}) == 1 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FastDepends 2 | 3 |

4 | 5 | Tests coverage 6 | 7 | 8 | Coverage 9 | 10 | 11 | Package version 12 | 13 | 14 | downloads 15 | 16 |
17 | 18 | Supported Python versions 19 | 20 | 21 | GitHub 22 | 23 |

24 | 25 | --- 26 | 27 | Documentation: 28 | 29 | --- 30 | 31 | FastDepends - FastAPI Dependency Injection system extracted from FastAPI and cleared of all HTTP logic. 32 | This is a small library which provides you with the ability to use lovely FastAPI interfaces in your own 33 | projects or tools. 34 | 35 | Thanks to [*fastapi*](https://fastapi.tiangolo.com/) and [*pydantic*](https://docs.pydantic.dev/) projects for this 36 | great functionality. This package is just a small change of the original FastAPI sources to provide DI functionality in a pure-Python way. 37 | 38 | Async and sync modes are both supported. 39 | 40 | # For why? 41 | 42 | This project should be extremely helpful to boost your not-**FastAPI** applications (even **Flask**, I know that u like some legacy). 43 | 44 | Also the project can be a core of your own framework for anything. Actually, it was build for my another project - :rocket:[**Propan**](https://github.com/Lancetnik/Propan):rocket: (and [**FastStream**](https://github.com/airtai/faststream)), check it to see full-featured **FastDepends** usage example. 45 | 46 | ## Installation 47 | 48 | ```bash 49 | pip install fast-depends 50 | ``` 51 | 52 | ## Usage 53 | 54 | There is no way to make Dependency Injection easier 55 | 56 | You can use this library without any frameworks in both **sync** and **async** code. 57 | 58 | ### Async code 59 | 60 | ```python 61 | import asyncio 62 | 63 | from fast_depends import inject, Depends 64 | 65 | async def dependency(a: int) -> int: 66 | return a 67 | 68 | @inject 69 | async def main( 70 | a: int, 71 | b: int, 72 | c: int = Depends(dependency) 73 | ) -> float: 74 | return a + b + c 75 | 76 | assert asyncio.run(main("1", 2)) == 4.0 77 | ``` 78 | 79 | ### Sync code 80 | 81 | ```python 82 | from fast_depends import inject, Depends 83 | 84 | def dependency(a: int) -> int: 85 | return a 86 | 87 | @inject 88 | def main( 89 | a: int, 90 | b: int, 91 | c: int = Depends(dependency) 92 | ) -> float: 93 | return a + b + c 94 | 95 | assert main("1", 2) == 4.0 96 | ``` 97 | 98 | `@inject` decorator plays multiple roles at the same time: 99 | 100 | * resolve *Depends* classes 101 | * cast types according to Python annotation 102 | * validate incoming parameters using *pydantic* 103 | 104 | --- 105 | 106 | ### Features 107 | 108 | Synchronous code is fully supported in this package: without any `async_to_sync`, `run_sync`, `syncify` or any other tricks. 109 | 110 | Also, *FastDepends* casts functions' return values the same way, it can be very helpful in building your own tools. 111 | 112 | These are two main defferences from native FastAPI DI System. 113 | 114 | --- 115 | 116 | ### Dependencies Overriding 117 | 118 | Also, **FastDepends** can be used as a lightweight DI container. Using it, you can easily override basic dependencies with application startup or in tests. 119 | 120 | ```python 121 | from typing import Annotated 122 | 123 | from fast_depends import Depends, dependency_provider, inject 124 | 125 | def abc_func() -> int: 126 | raise NotImplementedError() 127 | 128 | def real_func() -> int: 129 | return 1 130 | 131 | @inject 132 | def func( 133 | dependency: Annotated[int, Depends(abc_func)] 134 | ) -> int: 135 | return dependency 136 | 137 | with dependency_provider.scope(abc_func, real_func): 138 | assert func() == 1 139 | ``` 140 | 141 | `dependency_provider` in this case is just a default container already declared in the library. But you can use your own the same way: 142 | 143 | ```python 144 | from typing import Annotated 145 | 146 | from fast_depends import Depends, Provider, inject 147 | 148 | provider = Provider() 149 | 150 | def abc_func() -> int: 151 | raise NotImplementedError() 152 | 153 | def real_func() -> int: 154 | return 1 155 | 156 | @inject(dependency_overrides_provider=provider) 157 | def func( 158 | dependency: Annotated[int, Depends(abc_func)] 159 | ) -> int: 160 | return dependency 161 | 162 | with provider.scope(abc_func, real_func): 163 | assert func() == 1 164 | ``` 165 | 166 | This way you can inherit the basic `Provider` class and define any extra logic you want! 167 | 168 | --- 169 | 170 | ### Custom Fields 171 | 172 | If you wish to write your own FastAPI or another closely by architecture tool, you should define your own custom fields to specify application behavior. 173 | 174 | Custom fields can be used to adding something specific to a function arguments (like a BackgroundTask) or parsing incoming objects special way. You able decide by own, why and how you will use these tools. 175 | 176 | FastDepends grants you this opportunity a very intuitive and comfortable way. 177 | 178 | ```python 179 | from fast_depends import inject 180 | from fast_depends.library import CustomField 181 | 182 | class Header(CustomField): 183 | def use(self, /, **kwargs: AnyDict) -> AnyDict: 184 | kwargs = super().use(**kwargs) 185 | kwargs[self.param_name] = kwargs["headers"][self.param_name] 186 | return kwargs 187 | 188 | @inject 189 | def my_func(header_field: int = Header()): 190 | return header_field 191 | 192 | assert my_func( 193 | headers={ "header_field": "1" } 194 | ) == 1 195 | ``` 196 | -------------------------------------------------------------------------------- /fast_depends/utils.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import inspect 3 | import sys 4 | from collections.abc import AsyncGenerator, AsyncIterable, Awaitable, Callable 5 | from contextlib import ( 6 | AbstractContextManager, 7 | AsyncExitStack, 8 | ExitStack, 9 | asynccontextmanager, 10 | contextmanager, 11 | ) 12 | from typing import ( 13 | TYPE_CHECKING, 14 | Annotated, 15 | Any, 16 | ForwardRef, 17 | TypeVar, 18 | cast, 19 | get_args, 20 | get_origin, 21 | ) 22 | 23 | if sys.version_info >= (3, 12): 24 | # to support PydanticV1 we should switch it expicitly 25 | from typing import TypeAliasType 26 | else: 27 | from typing_extensions import TypeAliasType 28 | 29 | import anyio 30 | from typing_extensions import ParamSpec 31 | 32 | from fast_depends._compat import evaluate_forwardref 33 | 34 | if TYPE_CHECKING: 35 | from types import FrameType 36 | 37 | P = ParamSpec("P") 38 | T = TypeVar("T") 39 | 40 | 41 | async def run_async( 42 | func: Callable[P, T] | Callable[P, Awaitable[T]], 43 | *args: P.args, 44 | **kwargs: P.kwargs, 45 | ) -> T: 46 | if is_coroutine_callable(func): 47 | return await cast(Callable[P, Awaitable[T]], func)(*args, **kwargs) 48 | else: 49 | return await run_in_threadpool(cast(Callable[P, T], func), *args, **kwargs) 50 | 51 | 52 | async def run_in_threadpool( 53 | func: Callable[P, T], 54 | *args: P.args, 55 | **kwargs: P.kwargs, 56 | ) -> T: 57 | if kwargs: 58 | func = functools.partial(func, **kwargs) 59 | return await anyio.to_thread.run_sync(func, *args) 60 | 61 | 62 | async def solve_generator_async( 63 | *sub_args: Any, 64 | call: Callable[..., Any], 65 | stack: AsyncExitStack, 66 | **sub_values: Any, 67 | ) -> Any: 68 | if is_gen_callable(call): 69 | cm = contextmanager_in_threadpool(contextmanager(call)(**sub_values)) 70 | elif is_async_gen_callable(call): # pragma: no branch 71 | cm = asynccontextmanager(call)(*sub_args, **sub_values) 72 | return await stack.enter_async_context(cm) 73 | 74 | 75 | def solve_generator_sync( 76 | *sub_args: Any, 77 | call: Callable[..., Any], 78 | stack: ExitStack, 79 | **sub_values: Any, 80 | ) -> Any: 81 | cm = contextmanager(call)(*sub_args, **sub_values) 82 | return stack.enter_context(cm) 83 | 84 | 85 | def get_typed_signature(call: Callable[..., Any]) -> tuple[inspect.Signature, Any]: 86 | signature = inspect.signature(call) 87 | 88 | locals = collect_outer_stack_locals() 89 | 90 | # We unwrap call to get the original unwrapped function 91 | call = inspect.unwrap(call) 92 | 93 | type_params = getattr(call, "__type_params__", ()) or None 94 | 95 | globalns = getattr(call, "__globals__", {}) 96 | typed_params = [ 97 | inspect.Parameter( 98 | name=param.name, 99 | kind=param.kind, 100 | default=param.default, 101 | annotation=get_typed_annotation( 102 | param.annotation, 103 | globalns, 104 | locals, 105 | type_params=type_params, 106 | ), 107 | ) 108 | for param in signature.parameters.values() 109 | ] 110 | 111 | return inspect.Signature(typed_params), get_typed_annotation( 112 | signature.return_annotation, 113 | globalns, 114 | locals, 115 | type_params=type_params, 116 | ) 117 | 118 | 119 | def collect_outer_stack_locals() -> dict[str, Any]: 120 | frame = inspect.currentframe() 121 | 122 | frames: list[FrameType] = [] 123 | while frame is not None: 124 | if "fast_depends" not in frame.f_code.co_filename: 125 | frames.append(frame) 126 | frame = frame.f_back 127 | 128 | locals = {} 129 | for f in frames[::-1]: 130 | locals.update(f.f_locals) 131 | 132 | return locals 133 | 134 | 135 | def get_typed_annotation( 136 | annotation: Any, 137 | globalns: dict[str, Any], 138 | locals: dict[str, Any], 139 | type_params: tuple[Any, ...] | None = None, 140 | ) -> Any: 141 | if isinstance(annotation, TypeAliasType): 142 | annotation = annotation.__value__ 143 | 144 | if isinstance(annotation, str): 145 | annotation = ForwardRef(annotation) 146 | 147 | if isinstance(annotation, ForwardRef): 148 | annotation = evaluate_forwardref( 149 | annotation, globalns, locals, type_params=type_params 150 | ) 151 | 152 | if get_origin(annotation) is Annotated and (args := get_args(annotation)): 153 | solved_args = [ 154 | get_typed_annotation(x, globalns, locals, type_params=type_params) 155 | for x in args 156 | ] 157 | 158 | annotation.__origin__, annotation.__metadata__ = ( 159 | solved_args[0], 160 | tuple(solved_args[1:]), 161 | ) 162 | 163 | return annotation 164 | 165 | 166 | @asynccontextmanager 167 | async def contextmanager_in_threadpool( 168 | cm: AbstractContextManager[T], 169 | ) -> AsyncGenerator[T, None]: 170 | exit_limiter = anyio.CapacityLimiter(1) 171 | try: 172 | yield await run_in_threadpool(cm.__enter__) 173 | except Exception as e: 174 | ok = bool( 175 | await anyio.to_thread.run_sync( 176 | cm.__exit__, type(e), e, None, limiter=exit_limiter 177 | ) 178 | ) 179 | if not ok: # pragma: no branch 180 | raise e 181 | else: 182 | await anyio.to_thread.run_sync( 183 | cm.__exit__, None, None, None, limiter=exit_limiter 184 | ) 185 | 186 | 187 | def is_gen_callable(call: Callable[..., Any]) -> bool: 188 | if inspect.isgeneratorfunction(call): 189 | return True 190 | dunder_call = getattr(call, "__call__", None) # noqa: B004 191 | return inspect.isgeneratorfunction(dunder_call) 192 | 193 | 194 | def is_async_gen_callable(call: Callable[..., Any]) -> bool: 195 | if inspect.isasyncgenfunction(call): 196 | return True 197 | dunder_call = getattr(call, "__call__", None) # noqa: B004 198 | return inspect.isasyncgenfunction(dunder_call) 199 | 200 | 201 | def is_coroutine_callable(call: Callable[..., Any]) -> bool: 202 | if inspect.isclass(call): 203 | return False 204 | 205 | if inspect.iscoroutinefunction(call): 206 | return True 207 | 208 | dunder_call = getattr(call, "__call__", None) # noqa: B004 209 | return inspect.iscoroutinefunction(dunder_call) 210 | 211 | 212 | async def async_map( 213 | func: Callable[..., T], async_iterable: AsyncIterable[Any] 214 | ) -> AsyncIterable[T]: 215 | async for i in async_iterable: 216 | yield func(i) 217 | -------------------------------------------------------------------------------- /fast_depends/pydantic/serializer.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from collections.abc import Callable, Iterator, Sequence 3 | from contextlib import contextmanager 4 | from itertools import chain 5 | from typing import Any 6 | 7 | from pydantic import ValidationError as PValidationError 8 | 9 | from fast_depends.exceptions import ValidationError 10 | from fast_depends.library.serializer import OptionItem, Serializer, SerializerProto 11 | from fast_depends.pydantic._compat import ( 12 | PYDANTIC_V2, 13 | BaseModel, 14 | ConfigDict, 15 | PydanticUserError, 16 | TypeAdapter, 17 | create_model, 18 | dump_json, 19 | get_aliases, 20 | get_config_base, 21 | get_model_fields, 22 | ) 23 | 24 | 25 | class PydanticSerializer(SerializerProto): 26 | __slots__ = ( 27 | "config", 28 | "use_fastdepends_errors", 29 | ) 30 | 31 | def __init__( 32 | self, 33 | pydantic_config: ConfigDict | None = None, 34 | use_fastdepends_errors: bool = True, 35 | ) -> None: 36 | self.config = pydantic_config 37 | self.use_fastdepends_errors = use_fastdepends_errors 38 | 39 | def __call__( 40 | self, 41 | *, 42 | name: str, 43 | options: list[OptionItem], 44 | response_type: Any, 45 | ) -> "_PydanticSerializer": 46 | if self.use_fastdepends_errors: 47 | if response_type is not inspect.Parameter.empty: 48 | return _PydanticWrappedSerializerWithResponse( 49 | name=name, 50 | options=options, 51 | response_type=response_type, 52 | pydantic_config=self.config, 53 | ) 54 | 55 | return _PydanticWrappedSerializer( 56 | name=name, 57 | options=options, 58 | pydantic_config=self.config, 59 | ) 60 | 61 | if response_type is not inspect.Parameter.empty: 62 | return _PydanticSerializerWithResponse( 63 | name=name, 64 | options=options, 65 | response_type=response_type, 66 | pydantic_config=self.config, 67 | ) 68 | 69 | return _PydanticSerializer( 70 | name=name, 71 | options=options, 72 | pydantic_config=self.config, 73 | ) 74 | 75 | @staticmethod 76 | def encode(message: Any) -> bytes: 77 | if isinstance(message, bytes): 78 | return message 79 | return dump_json(message) 80 | 81 | 82 | class _PydanticSerializer(Serializer): 83 | __slots__ = ( 84 | "model", 85 | "name", 86 | "options", 87 | "config", 88 | "response_option", 89 | ) 90 | 91 | def __init__( 92 | self, 93 | *, 94 | name: str, 95 | options: list[OptionItem], 96 | response_type: Any = None, 97 | pydantic_config: ConfigDict | None = None, 98 | ): 99 | class_options: dict[str, Any] = { 100 | i.field_name: (i.field_type, i.default_value) for i in options 101 | } 102 | 103 | self.config = get_config_base(pydantic_config) 104 | 105 | self.model = create_model( # type: ignore[call-overload] 106 | name, 107 | __config__=self.config, 108 | **class_options, 109 | ) 110 | 111 | super().__init__(name=name, options=options, response_type=response_type) 112 | 113 | def get_aliases(self) -> tuple[str, ...]: 114 | return get_aliases(self.model) 115 | 116 | def __call__(self, call_kwargs: dict[str, Any]) -> dict[str, Any]: 117 | casted_model = self.model(**call_kwargs) 118 | 119 | return { 120 | i: getattr(casted_model, i) for i in get_model_fields(casted_model).keys() 121 | } 122 | 123 | 124 | class _PydanticSerializerWithResponse(_PydanticSerializer): 125 | __slots__ = ("response_callback",) 126 | 127 | response_callback: Callable[[Any], Any] 128 | 129 | def __init__( 130 | self, 131 | *, 132 | name: str, 133 | options: list[OptionItem], 134 | response_type: Any, 135 | pydantic_config: ConfigDict | None = None, 136 | ): 137 | super().__init__( 138 | name=name, 139 | options=options, 140 | response_type=response_type, 141 | pydantic_config=pydantic_config, 142 | ) 143 | 144 | response_callback: Callable[[Any], Any] | None = None 145 | try: 146 | is_model = issubclass(response_type or object, BaseModel) 147 | except Exception: 148 | is_model = False 149 | 150 | if is_model: 151 | if PYDANTIC_V2: 152 | response_callback = response_type.model_validate 153 | else: 154 | response_callback = response_type.validate 155 | 156 | elif PYDANTIC_V2: 157 | try: 158 | response_pydantic_type = TypeAdapter(response_type, config=self.config) 159 | except PydanticUserError: 160 | response_pydantic_type = TypeAdapter(response_type) 161 | response_callback = response_pydantic_type.validate_python 162 | 163 | if response_callback is None and not PYDANTIC_V2: 164 | response_model = create_model( # type: ignore[call-overload] 165 | "ResponseModel", 166 | __config__=self.config, 167 | r=(response_type or Any, ...), 168 | ) 169 | 170 | def response_callback(x: Any) -> Any: 171 | return response_model(r=x).r # type: ignore[attr-defined] 172 | 173 | assert response_callback 174 | self.response_callback = response_callback 175 | 176 | def response(self, value: Any) -> Any: 177 | return self.response_callback(value) 178 | 179 | 180 | class _PydanticWrappedSerializer(_PydanticSerializer): 181 | def __call__(self, call_kwargs: dict[str, Any]) -> dict[str, Any]: 182 | with self._try_pydantic(call_kwargs, self.options): 183 | casted_model = self.model(**call_kwargs) 184 | 185 | return { 186 | i: getattr(casted_model, i) for i in get_model_fields(casted_model).keys() 187 | } 188 | 189 | @contextmanager 190 | def _try_pydantic( 191 | self, 192 | call_kwargs: Any, 193 | options: dict[str, OptionItem], 194 | locations: Sequence[Any] = (), 195 | ) -> Iterator[None]: 196 | try: 197 | yield 198 | except PValidationError as er: 199 | raise ValidationError( 200 | incoming_options=call_kwargs, 201 | expected=options, 202 | locations=locations 203 | or tuple(chain(*(one_error["loc"] for one_error in er.errors()))), 204 | original_error=er, 205 | ) from er 206 | 207 | 208 | class _PydanticWrappedSerializerWithResponse( 209 | _PydanticWrappedSerializer, 210 | _PydanticSerializerWithResponse, 211 | ): 212 | def response(self, value: Any) -> Any: 213 | with self._try_pydantic(value, self.response_option, ("return",)): 214 | return self.response_callback(value) 215 | -------------------------------------------------------------------------------- /fast_depends/msgspec/serializer.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import re 3 | from collections.abc import Callable, Iterator, Sequence 4 | from contextlib import contextmanager 5 | from typing import Any, TypeVar 6 | 7 | import msgspec 8 | 9 | from fast_depends.exceptions import ValidationError 10 | from fast_depends.library.serializer import OptionItem, Serializer, SerializerProto 11 | 12 | T = TypeVar("T") 13 | 14 | 15 | class MsgSpecSerializer(SerializerProto): 16 | __slots__ = ("use_fastdepends_errors", "dec_hook") 17 | 18 | def __init__( 19 | self, 20 | use_fastdepends_errors: bool = True, 21 | dec_hook: Callable[[type[T], Any], T] | None = None, 22 | ) -> None: 23 | self.use_fastdepends_errors = use_fastdepends_errors 24 | self.dec_hook = dec_hook 25 | 26 | def __call__( 27 | self, 28 | *, 29 | name: str, 30 | options: list[OptionItem], 31 | response_type: Any, 32 | ) -> "_MsgSpecSerializer": 33 | if self.use_fastdepends_errors: 34 | if response_type is not inspect.Parameter.empty: 35 | return _MsgSpecWrappedSerializerWithResponse( 36 | name=name, 37 | options=options, 38 | response_type=response_type, 39 | dec_hook=self.dec_hook, 40 | ) 41 | 42 | return _MsgSpecWrappedSerializer( 43 | name=name, 44 | options=options, 45 | dec_hook=self.dec_hook, 46 | ) 47 | 48 | if response_type is not inspect.Parameter.empty: 49 | return _MsgSpecSerializerWithResponse( 50 | name=name, 51 | options=options, 52 | response_type=response_type, 53 | dec_hook=self.dec_hook, 54 | ) 55 | 56 | return _MsgSpecSerializer( 57 | name=name, 58 | options=options, 59 | dec_hook=self.dec_hook, 60 | ) 61 | 62 | @staticmethod 63 | def encode(message: Any) -> bytes: 64 | if isinstance(message, bytes): 65 | return message 66 | return msgspec.json.encode(message) 67 | 68 | 69 | class _MsgSpecSerializer(Serializer): 70 | __slots__ = ( 71 | "aliases", 72 | "model", 73 | "response_type", 74 | "name", 75 | "options", 76 | "response_option", 77 | "dec_hook", 78 | ) 79 | 80 | def __init__( 81 | self, 82 | *, 83 | name: str, 84 | options: list[OptionItem], 85 | response_type: Any = None, 86 | dec_hook: Callable[[type[T], Any], T] | None = None, 87 | ): 88 | model_options: list[str | tuple[str, type] | tuple[str, type, Any]] = [] 89 | aliases = {} 90 | for i in options: 91 | default_value = i.default_value 92 | 93 | if isinstance(default_value, msgspec._core.Field) and default_value.name: 94 | aliases[i.field_name] = default_value.name 95 | else: 96 | aliases[i.field_name] = i.field_name 97 | 98 | if default_value is Ellipsis: 99 | model_options.append( 100 | ( 101 | i.field_name, 102 | i.field_type, 103 | ) 104 | ) 105 | else: 106 | model_options.append( 107 | ( 108 | i.field_name, 109 | i.field_type, 110 | default_value, 111 | ) 112 | ) 113 | 114 | self.aliases = aliases 115 | self.model = msgspec.defstruct(name, model_options, kw_only=True) 116 | self.dec_hook = dec_hook 117 | super().__init__(name=name, options=options, response_type=response_type) 118 | 119 | def get_aliases(self) -> tuple[str, ...]: 120 | return tuple(self.aliases.values()) 121 | 122 | def __call__(self, call_kwargs: dict[str, Any]) -> dict[str, Any]: 123 | casted_model = msgspec.convert( 124 | call_kwargs, 125 | type=self.model, 126 | strict=False, 127 | str_keys=True, 128 | dec_hook=self.dec_hook, 129 | ) 130 | 131 | return { 132 | out_field: getattr(casted_model, out_field, None) 133 | for out_field in self.aliases.keys() 134 | } 135 | 136 | 137 | class _MsgSpecSerializerWithResponse(_MsgSpecSerializer): 138 | def __init__( 139 | self, 140 | *, 141 | name: str, 142 | options: list[OptionItem], 143 | response_type: Any, 144 | dec_hook: Callable[[type[T], Any], T] | None = None, 145 | ): 146 | super().__init__( 147 | name=name, 148 | options=options, 149 | response_type=response_type, 150 | dec_hook=dec_hook, 151 | ) 152 | self.response_type = response_type 153 | 154 | def response(self, value: Any) -> Any: 155 | return msgspec.convert( 156 | value, 157 | type=self.response_type, 158 | strict=False, 159 | dec_hook=self.dec_hook, 160 | ) 161 | 162 | 163 | class _MsgSpecWrappedSerializer(_MsgSpecSerializer): 164 | def __call__(self, call_kwargs: dict[str, Any]) -> dict[str, Any]: 165 | with self._try_msgspec(call_kwargs, self.options): 166 | casted_model = msgspec.convert( 167 | call_kwargs, 168 | type=self.model, 169 | strict=False, 170 | str_keys=True, 171 | dec_hook=self.dec_hook, 172 | ) 173 | 174 | return { 175 | out_field: getattr(casted_model, out_field, None) 176 | for out_field in self.aliases.keys() 177 | } 178 | 179 | @contextmanager 180 | def _try_msgspec( 181 | self, 182 | call_kwargs: Any, 183 | options: dict[str, OptionItem], 184 | locations: Sequence[str] = (), 185 | ) -> Iterator[None]: 186 | try: 187 | yield 188 | except msgspec.ValidationError as er: 189 | raise ValidationError( 190 | incoming_options=call_kwargs, 191 | expected=options, 192 | locations=locations or re.findall(r"at `\$\.(.)`", str(er.args)), 193 | original_error=er, 194 | ) from er 195 | 196 | 197 | class _MsgSpecWrappedSerializerWithResponse(_MsgSpecWrappedSerializer): 198 | def __init__( 199 | self, 200 | *, 201 | name: str, 202 | options: list[OptionItem], 203 | response_type: Any, 204 | dec_hook: Callable[[type[T], Any], T] | None = None, 205 | ): 206 | super().__init__( 207 | name=name, 208 | options=options, 209 | response_type=response_type, 210 | dec_hook=dec_hook, 211 | ) 212 | self.response_type = response_type 213 | 214 | def response(self, value: Any) -> Any: 215 | with self._try_msgspec(value, self.response_option, ("return",)): 216 | return msgspec.convert( 217 | value, 218 | type=self.response_type, 219 | strict=False, 220 | dec_hook=self.dec_hook, 221 | ) 222 | -------------------------------------------------------------------------------- /docs/docs/assets/javascripts/custom.js: -------------------------------------------------------------------------------- 1 | const div = document.querySelector('.github-topic-projects') 2 | 3 | async function getDataBatch(page) { 4 | const response = await fetch(`https://api.github.com/search/repositories?q=topic:propan&per_page=100&page=${page}`, { headers: { Accept: 'application/vnd.github.mercy-preview+json' } }) 5 | const data = await response.json() 6 | return data 7 | } 8 | 9 | async function getData() { 10 | let page = 1 11 | let data = [] 12 | let dataBatch = await getDataBatch(page) 13 | data = data.concat(dataBatch.items) 14 | const totalCount = dataBatch.total_count 15 | while (data.length < totalCount) { 16 | page += 1 17 | dataBatch = await getDataBatch(page) 18 | data = data.concat(dataBatch.items) 19 | } 20 | return data 21 | } 22 | 23 | function setupTermynal() { 24 | document.querySelectorAll(".use-termynal").forEach(node => { 25 | node.style.display = "block"; 26 | new Termynal(node, { 27 | lineDelay: 500 28 | }); 29 | }); 30 | const progressLiteralStart = "---> 100%"; 31 | const promptLiteralStart = "$ "; 32 | const customPromptLiteralStart = "# "; 33 | const termynalActivateClass = "termy"; 34 | let termynals = []; 35 | 36 | function createTermynals() { 37 | document 38 | .querySelectorAll(`.${termynalActivateClass} .highlight`) 39 | .forEach(node => { 40 | const text = node.textContent; 41 | const lines = text.split("\n"); 42 | const useLines = []; 43 | let buffer = []; 44 | function saveBuffer() { 45 | if (buffer.length) { 46 | let isBlankSpace = true; 47 | buffer.forEach(line => { 48 | if (line) { 49 | isBlankSpace = false; 50 | } 51 | }); 52 | dataValue = {}; 53 | if (isBlankSpace) { 54 | dataValue["delay"] = 0; 55 | } 56 | if (buffer[buffer.length - 1] === "") { 57 | // A last single
won't have effect 58 | // so put an additional one 59 | buffer.push(""); 60 | } 61 | const bufferValue = buffer.join("
"); 62 | dataValue["value"] = bufferValue; 63 | useLines.push(dataValue); 64 | buffer = []; 65 | } 66 | } 67 | for (let line of lines) { 68 | if (line === progressLiteralStart) { 69 | saveBuffer(); 70 | useLines.push({ 71 | type: "progress" 72 | }); 73 | } else if (line.startsWith(promptLiteralStart)) { 74 | saveBuffer(); 75 | const value = line.replace(promptLiteralStart, "").trimEnd(); 76 | useLines.push({ 77 | type: "input", 78 | value: value 79 | }); 80 | } else if (line.startsWith("// ")) { 81 | saveBuffer(); 82 | const value = "💬 " + line.replace("// ", "").trimEnd(); 83 | useLines.push({ 84 | value: value, 85 | class: "termynal-comment", 86 | delay: 0 87 | }); 88 | } else if (line.startsWith(customPromptLiteralStart)) { 89 | saveBuffer(); 90 | const promptStart = line.indexOf(promptLiteralStart); 91 | if (promptStart === -1) { 92 | console.error("Custom prompt found but no end delimiter", line) 93 | } 94 | const prompt = line.slice(0, promptStart).replace(customPromptLiteralStart, "") 95 | let value = line.slice(promptStart + promptLiteralStart.length); 96 | useLines.push({ 97 | type: "input", 98 | value: value, 99 | prompt: prompt 100 | }); 101 | } else { 102 | buffer.push(line); 103 | } 104 | } 105 | saveBuffer(); 106 | const div = document.createElement("div"); 107 | node.replaceWith(div); 108 | const termynal = new Termynal(div, { 109 | lineData: useLines, 110 | noInit: true, 111 | lineDelay: 500 112 | }); 113 | termynals.push(termynal); 114 | }); 115 | } 116 | 117 | function loadVisibleTermynals() { 118 | termynals = termynals.filter(termynal => { 119 | if (termynal.container.getBoundingClientRect().top - innerHeight <= 0) { 120 | termynal.init(); 121 | return false; 122 | } 123 | return true; 124 | }); 125 | } 126 | window.addEventListener("scroll", loadVisibleTermynals); 127 | createTermynals(); 128 | loadVisibleTermynals(); 129 | } 130 | 131 | function shuffle(array) { 132 | var currentIndex = array.length, temporaryValue, randomIndex; 133 | while (0 !== currentIndex) { 134 | randomIndex = Math.floor(Math.random() * currentIndex); 135 | currentIndex -= 1; 136 | temporaryValue = array[currentIndex]; 137 | array[currentIndex] = array[randomIndex]; 138 | array[randomIndex] = temporaryValue; 139 | } 140 | return array; 141 | } 142 | 143 | async function showRandomAnnouncement(groupId, timeInterval) { 144 | const announceFastAPI = document.getElementById(groupId); 145 | if (announceFastAPI) { 146 | let children = [].slice.call(announceFastAPI.children); 147 | children = shuffle(children) 148 | let index = 0 149 | const announceRandom = () => { 150 | children.forEach((el, i) => {el.style.display = "none"}); 151 | children[index].style.display = "block" 152 | index = (index + 1) % children.length 153 | } 154 | announceRandom() 155 | setInterval(announceRandom, timeInterval 156 | ) 157 | } 158 | } 159 | 160 | async function main() { 161 | if (div) { 162 | data = await getData() 163 | div.innerHTML = '
    ' 164 | const ul = document.querySelector('.github-topic-projects ul') 165 | data.forEach(v => { 166 | if (v.full_name === 'lancetnik/propan') { 167 | return 168 | } 169 | const li = document.createElement('li') 170 | li.innerHTML = `★ ${v.stargazers_count} - ${v.full_name} by @${v.owner.login}` 171 | ul.append(li) 172 | }) 173 | } 174 | 175 | setupTermynal(); 176 | showRandomAnnouncement('announce-left', 5000) 177 | showRandomAnnouncement('announce-right', 10000) 178 | } 179 | 180 | main() -------------------------------------------------------------------------------- /tests/test_overrides.py: -------------------------------------------------------------------------------- 1 | from collections.abc import AsyncGenerator, Generator 2 | from typing import Annotated 3 | from unittest.mock import Mock 4 | 5 | import pytest 6 | 7 | from fast_depends import Depends, Provider, inject 8 | 9 | 10 | def test_not_override(provider: Provider) -> None: 11 | mock = Mock() 12 | 13 | def base_dep(): 14 | mock.original() 15 | return 1 16 | 17 | @inject(dependency_provider=provider) 18 | def func(d=Depends(base_dep)): 19 | assert d == 1 20 | 21 | func() 22 | 23 | mock.original.assert_called_once() 24 | 25 | 26 | def test_sync_override(provider: Provider) -> None: 27 | mock = Mock() 28 | 29 | def base_dep(): 30 | raise NotImplementedError 31 | 32 | def override_dep(): 33 | mock.override() 34 | return 2 35 | 36 | provider.override(base_dep, override_dep) 37 | 38 | @inject(dependency_provider=provider) 39 | def func(d=Depends(base_dep)): 40 | assert d == 2 41 | 42 | func() 43 | 44 | provider.clear() 45 | 46 | 47 | def test_override_by_key(provider: Provider) -> None: 48 | mock = Mock() 49 | 50 | def base_dep(): 51 | raise NotImplementedError 52 | 53 | def override_dep(): 54 | mock.override() 55 | return 2 56 | 57 | provider[base_dep] = override_dep 58 | 59 | @inject(dependency_provider=provider) 60 | def func(d=Depends(base_dep)): 61 | assert d == 2 62 | 63 | func() 64 | 65 | provider.clear() 66 | 67 | 68 | def test_override_context(provider: Provider) -> None: 69 | def base_dep(): 70 | return 1 71 | 72 | def override_dep(): 73 | return 2 74 | 75 | @inject(dependency_provider=provider) 76 | def func(d=Depends(base_dep)): 77 | return d 78 | 79 | with provider.scope(base_dep, override_dep): 80 | assert func() == 2 81 | 82 | assert func() == 1 83 | 84 | 85 | def test_sync_by_async_override(provider: Provider) -> None: 86 | def base_dep(): 87 | raise NotImplementedError 88 | 89 | async def override_dep(): # pragma: no cover 90 | return 2 91 | 92 | provider.override(base_dep, override_dep) 93 | 94 | with pytest.raises(AssertionError): 95 | 96 | @inject(dependency_provider=provider) 97 | def func(d=Depends(base_dep)): 98 | pass 99 | 100 | 101 | def test_sync_by_async_override_in_extra(provider: Provider) -> None: 102 | def base_dep(): 103 | raise NotImplementedError 104 | 105 | async def override_dep(): # pragma: no cover 106 | return 2 107 | 108 | provider.override(base_dep, override_dep) 109 | 110 | with pytest.raises(AssertionError): 111 | 112 | @inject( 113 | dependency_provider=provider, 114 | extra_dependencies=(Depends(base_dep),), 115 | ) 116 | def func(): 117 | pass 118 | 119 | 120 | @pytest.mark.anyio 121 | async def test_async_override(provider: Provider) -> None: 122 | mock = Mock() 123 | 124 | async def base_dep(): 125 | raise NotImplementedError 126 | 127 | async def override_dep(): 128 | mock.override() 129 | return 2 130 | 131 | provider.override(base_dep, override_dep) 132 | 133 | @inject(dependency_provider=provider) 134 | async def func(d=Depends(base_dep)): 135 | assert d == 2 136 | 137 | await func() 138 | mock.override.assert_called_once() 139 | 140 | 141 | @pytest.mark.anyio 142 | async def test_async_by_sync_override(provider: Provider) -> None: 143 | mock = Mock() 144 | 145 | async def base_dep(): 146 | raise NotImplementedError 147 | 148 | def override_dep(): 149 | mock.override() 150 | return 2 151 | 152 | provider.override(base_dep, override_dep) 153 | 154 | @inject(dependency_provider=provider) 155 | async def func(d=Depends(base_dep)): 156 | assert d == 2 157 | 158 | await func() 159 | mock.override.assert_called_once() 160 | 161 | 162 | def test_deep_overrides(provider: Provider) -> None: 163 | mock = Mock() 164 | 165 | def dep1(c=Depends(mock.dep2)): 166 | mock.dep1() 167 | 168 | def dep3(c=Depends(mock.dep4)): 169 | mock.dep3() 170 | 171 | @inject( 172 | dependency_provider=provider, 173 | extra_dependencies=(Depends(dep1),), 174 | ) 175 | def func() -> None: 176 | return 177 | 178 | func() 179 | mock.dep1.assert_called_once() 180 | mock.dep2.assert_called_once() 181 | assert not mock.dep3.called 182 | assert not mock.dep4.called 183 | mock.reset_mock() 184 | 185 | with provider.scope(dep1, dep3): 186 | func() 187 | assert not mock.dep1.called 188 | assert not mock.dep2.called 189 | mock.dep3.assert_called_once() 190 | mock.dep4.assert_called_once() 191 | 192 | 193 | def test_deep_overrides_with_different_signatures(provider: Provider) -> None: 194 | mock = Mock() 195 | 196 | def dep1(c=Depends(mock.dep2)): 197 | mock.dep1() 198 | 199 | def dep3(): 200 | mock.dep3() 201 | 202 | @inject( 203 | dependency_provider=provider, 204 | extra_dependencies=(Depends(dep1),), 205 | ) 206 | def func(): 207 | return 208 | 209 | func() 210 | mock.dep1.assert_called_once() 211 | mock.dep2.assert_called_once() 212 | assert not mock.dep3.called 213 | mock.reset_mock() 214 | 215 | with provider.scope(dep1, dep3): 216 | func() 217 | assert not mock.dep1.called 218 | assert not mock.dep2.called 219 | mock.dep3.assert_called_once() 220 | 221 | 222 | def test_override_context_with_generator(provider: Provider) -> None: 223 | def base_dep() -> Generator[int, None, None]: 224 | raise NotImplementedError 225 | 226 | def override_dep() -> Generator[int, None, None]: 227 | yield 2 228 | 229 | @inject(dependency_provider=provider) 230 | def func(d=Depends(base_dep)): 231 | return d 232 | 233 | with provider.scope(base_dep, override_dep): 234 | assert func() == 2 235 | 236 | 237 | def test_override_context_with_undefined_generator(provider: Provider) -> None: 238 | def base_dep() -> Generator[int, None, None]: 239 | raise NotImplementedError 240 | 241 | def override_dep() -> Generator[int, None, None]: 242 | yield 2 243 | 244 | @inject(dependency_provider=provider) 245 | def func(d=Depends(base_dep)): 246 | return d 247 | 248 | with provider.scope(base_dep, override_dep): 249 | assert func() == 2 250 | 251 | 252 | @pytest.mark.anyio 253 | async def test_async_override_context_with_generator(provider: Provider) -> None: 254 | async def base_dep() -> AsyncGenerator[int, None]: 255 | raise NotImplementedError 256 | 257 | async def override_dep() -> AsyncGenerator[int, None]: 258 | yield 2 259 | 260 | @inject(dependency_provider=provider) 261 | async def func(d=Depends(base_dep)): 262 | return d 263 | 264 | with provider.scope(base_dep, override_dep): 265 | assert await func() == 2 266 | 267 | 268 | @pytest.mark.anyio 269 | async def test_async_override_context_with_undefined_generator( 270 | provider: Provider, 271 | ) -> None: 272 | async def base_dep() -> AsyncGenerator[int, None]: 273 | raise NotImplementedError 274 | 275 | async def override_dep() -> AsyncGenerator[int, None]: 276 | yield 2 277 | 278 | @inject(dependency_provider=provider) 279 | async def func(d=Depends(base_dep)): 280 | return d 281 | 282 | with provider.scope(base_dep, override_dep): 283 | assert await func() == 2 284 | 285 | 286 | def test_clear_overrides(provider: Provider) -> None: 287 | def base_dep() -> int: 288 | return 1 289 | 290 | def override_dep() -> int: 291 | return 2 292 | 293 | @inject(dependency_provider=provider) 294 | def func(d: Annotated[int, Depends(base_dep)]) -> int: 295 | return d 296 | 297 | provider.override(base_dep, override_dep) 298 | 299 | assert len(provider.overrides) == 1 300 | assert len(provider.dependencies) == 1 301 | assert func() == 2 # override dependency called 302 | 303 | provider.clear() 304 | 305 | assert len(provider.overrides) == 0 306 | assert len(provider.dependencies) == 1 307 | assert func() == 1 # original dep called 308 | -------------------------------------------------------------------------------- /fast_depends/use.py: -------------------------------------------------------------------------------- 1 | from collections.abc import AsyncIterator, Callable, Iterator, Sequence 2 | from contextlib import AsyncExitStack, ExitStack 3 | from functools import partial, wraps 4 | from typing import ( 5 | TYPE_CHECKING, 6 | Any, 7 | Optional, 8 | Protocol, 9 | TypeVar, 10 | Union, 11 | cast, 12 | overload, 13 | ) 14 | 15 | from typing_extensions import ParamSpec 16 | 17 | from fast_depends.core import CallModel, build_call_model 18 | from fast_depends.dependencies import Dependant, Provider 19 | from fast_depends.library.serializer import SerializerProto 20 | 21 | SerializerCls: Optional["SerializerProto"] = None 22 | 23 | if SerializerCls is None: 24 | try: 25 | from fast_depends.pydantic import PydanticSerializer 26 | 27 | SerializerCls = PydanticSerializer() 28 | except ImportError: 29 | pass 30 | 31 | if SerializerCls is None: 32 | try: 33 | from fast_depends.msgspec import MsgSpecSerializer 34 | 35 | SerializerCls = MsgSpecSerializer() 36 | except ImportError: 37 | pass 38 | 39 | 40 | P = ParamSpec("P") 41 | T = TypeVar("T") 42 | 43 | if TYPE_CHECKING: 44 | from fast_depends.library.serializer import SerializerProto 45 | 46 | class InjectWrapper(Protocol[P, T]): 47 | def __call__( 48 | self, 49 | func: Callable[P, T], 50 | model: CallModel | None = None, 51 | ) -> Callable[P, T]: ... 52 | 53 | 54 | global_provider = Provider() 55 | 56 | 57 | def Depends( 58 | dependency: Callable[..., Any], 59 | *, 60 | use_cache: bool = True, 61 | cast: bool = True, 62 | cast_result: bool = False, 63 | ) -> Any: 64 | return Dependant( 65 | dependency=dependency, 66 | use_cache=use_cache, 67 | cast=cast, 68 | cast_result=cast_result, 69 | ) 70 | 71 | 72 | @overload 73 | def inject( 74 | func: Callable[P, T] = ..., 75 | *, 76 | cast: bool = True, 77 | cast_result: bool = True, 78 | extra_dependencies: Sequence["Dependant"] = (), 79 | dependency_provider: Optional["Provider"] = None, 80 | wrap_model: Callable[["CallModel"], "CallModel"] = lambda x: x, 81 | serializer_cls: Optional["SerializerProto"] = SerializerCls, 82 | **call_extra: Any, 83 | ) -> Callable[P, T]: ... 84 | 85 | 86 | @overload 87 | def inject( 88 | func: None = None, 89 | *, 90 | cast: bool = True, 91 | cast_result: bool = True, 92 | extra_dependencies: Sequence["Dependant"] = (), 93 | dependency_provider: Optional["Provider"] = None, 94 | wrap_model: Callable[["CallModel"], "CallModel"] = lambda x: x, 95 | serializer_cls: Optional["SerializerProto"] = SerializerCls, 96 | **call_extra: Any, 97 | ) -> "InjectWrapper[..., Any]": ... 98 | 99 | 100 | def inject( 101 | func: Callable[P, T] | None = None, 102 | *, 103 | cast: bool = True, 104 | cast_result: bool = True, 105 | extra_dependencies: Sequence[Dependant] = (), 106 | dependency_provider: Optional["Provider"] = None, 107 | wrap_model: Callable[["CallModel"], "CallModel"] = lambda x: x, 108 | serializer_cls: Optional["SerializerProto"] = SerializerCls, 109 | **call_extra: Any, 110 | ) -> Union[Callable[P, T], "InjectWrapper[P, T]"]: 111 | if dependency_provider is None: 112 | dependency_provider = global_provider 113 | 114 | if not cast: 115 | serializer_cls = None 116 | 117 | decorator: InjectWrapper[P, T] = _wrap_inject( 118 | dependency_provider=dependency_provider, 119 | wrap_model=wrap_model, 120 | extra_dependencies=extra_dependencies, 121 | serializer_cls=serializer_cls, 122 | cast_result=cast_result, 123 | **call_extra, 124 | ) 125 | 126 | if func is None: 127 | return decorator 128 | return decorator(func) 129 | 130 | 131 | def _wrap_inject( 132 | *, 133 | dependency_provider: "Provider", 134 | wrap_model: Callable[["CallModel"], "CallModel"], 135 | extra_dependencies: Sequence[Dependant], 136 | serializer_cls: Optional["SerializerProto"], 137 | cast_result: bool, 138 | **call_extra: Any, 139 | ) -> "InjectWrapper[P, T]": 140 | def func_wrapper( 141 | func: Callable[P, T], 142 | model: Optional["CallModel"] = None, 143 | ) -> Callable[P, T]: 144 | if model is None: 145 | real_model = wrap_model( 146 | build_call_model( 147 | call=func, 148 | extra_dependencies=extra_dependencies, 149 | dependency_provider=dependency_provider, 150 | serializer_cls=serializer_cls, 151 | serialize_result=cast_result, 152 | ) 153 | ) 154 | else: 155 | real_model = model 156 | 157 | if real_model.is_async: 158 | injected_wrapper: Callable[P, T] 159 | 160 | if real_model.is_generator: 161 | injected_wrapper = partial( # type: ignore[assignment] 162 | solve_async_gen, 163 | real_model, 164 | ) 165 | 166 | else: 167 | 168 | async def injected_wrapper(*args: P.args, **kwargs: P.kwargs) -> T: # type: ignore[misc] 169 | async with AsyncExitStack() as stack: 170 | return await real_model.asolve( # type: ignore[no-any-return] 171 | *args, 172 | stack=stack, 173 | cache_dependencies={}, 174 | nested=False, 175 | **(call_extra | kwargs), 176 | ) 177 | 178 | raise AssertionError("unreachable") 179 | 180 | else: 181 | if real_model.is_generator: 182 | injected_wrapper = partial( # type: ignore[assignment] 183 | solve_gen, 184 | real_model, 185 | ) 186 | 187 | else: 188 | 189 | def injected_wrapper(*args: P.args, **kwargs: P.kwargs) -> T: 190 | with ExitStack() as stack: 191 | return real_model.solve( # type: ignore[no-any-return] 192 | *args, 193 | stack=stack, 194 | cache_dependencies={}, 195 | nested=False, 196 | **(call_extra | kwargs), 197 | ) 198 | 199 | raise AssertionError("unreachable") 200 | 201 | injected_wrapper._fastdepends_call_ = real_model.call # type: ignore[attr-defined] 202 | return wraps(func)(injected_wrapper) 203 | 204 | return func_wrapper 205 | 206 | 207 | class solve_async_gen: 208 | _iter: AsyncIterator[Any] | None = None 209 | 210 | def __init__( 211 | self, 212 | model: "CallModel", 213 | *args: Any, 214 | **kwargs: Any, 215 | ): 216 | self.call = model 217 | self.args = args 218 | self.kwargs = kwargs 219 | 220 | def __aiter__(self) -> "solve_async_gen": 221 | self.stack = AsyncExitStack() 222 | return self 223 | 224 | async def __anext__(self) -> Any: 225 | if self._iter is None: 226 | stack = self.stack = AsyncExitStack() 227 | await self.stack.__aenter__() 228 | self._iter = cast( 229 | AsyncIterator[Any], 230 | ( 231 | await self.call.asolve( 232 | *self.args, 233 | stack=stack, 234 | cache_dependencies={}, 235 | nested=False, 236 | **self.kwargs, 237 | ) 238 | ).__aiter__(), 239 | ) 240 | 241 | try: 242 | r = await self._iter.__anext__() 243 | except StopAsyncIteration: 244 | await self.stack.__aexit__(None, None, None) 245 | raise 246 | else: 247 | return r 248 | 249 | 250 | class solve_gen: 251 | _iter: Iterator[Any] | None = None 252 | 253 | def __init__( 254 | self, 255 | model: "CallModel", 256 | *args: Any, 257 | **kwargs: Any, 258 | ): 259 | self.call = model 260 | self.args = args 261 | self.kwargs = kwargs 262 | 263 | def __iter__(self) -> "solve_gen": 264 | self.stack = ExitStack() 265 | return self 266 | 267 | def __next__(self) -> Any: 268 | if self._iter is None: 269 | stack = self.stack = ExitStack() 270 | self.stack.__enter__() 271 | self._iter = cast( 272 | Iterator[Any], 273 | iter( 274 | self.call.solve( 275 | *self.args, 276 | stack=stack, 277 | cache_dependencies={}, 278 | nested=False, 279 | **self.kwargs, 280 | ) 281 | ), 282 | ) 283 | 284 | try: 285 | r = next(self._iter) 286 | except StopIteration: 287 | self.stack.__exit__(None, None, None) 288 | raise 289 | else: 290 | return r 291 | -------------------------------------------------------------------------------- /docs/docs/assets/javascripts/termynal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * termynal.js 3 | * A lightweight, modern and extensible animated terminal window, using 4 | * async/await. 5 | * 6 | * @author Ines Montani 7 | * @version 0.0.1 8 | * @license MIT 9 | */ 10 | 11 | 'use strict'; 12 | 13 | /** Generate a terminal widget. */ 14 | class Termynal { 15 | /** 16 | * Construct the widget's settings. 17 | * @param {(string|Node)=} container - Query selector or container element. 18 | * @param {Object=} options - Custom settings. 19 | * @param {string} options.prefix - Prefix to use for data attributes. 20 | * @param {number} options.startDelay - Delay before animation, in ms. 21 | * @param {number} options.typeDelay - Delay between each typed character, in ms. 22 | * @param {number} options.lineDelay - Delay between each line, in ms. 23 | * @param {number} options.progressLength - Number of characters displayed as progress bar. 24 | * @param {string} options.progressChar – Character to use for progress bar, defaults to █. 25 | * @param {number} options.progressPercent - Max percent of progress. 26 | * @param {string} options.cursor – Character to use for cursor, defaults to ▋. 27 | * @param {Object[]} lineData - Dynamically loaded line data objects. 28 | * @param {boolean} options.noInit - Don't initialise the animation. 29 | */ 30 | constructor(container = '#termynal', options = {}) { 31 | this.container = (typeof container === 'string') ? document.querySelector(container) : container; 32 | this.pfx = `data-${options.prefix || 'ty'}`; 33 | this.originalStartDelay = this.startDelay = options.startDelay 34 | || parseFloat(this.container.getAttribute(`${this.pfx}-startDelay`)) || 600; 35 | this.originalTypeDelay = this.typeDelay = options.typeDelay 36 | || parseFloat(this.container.getAttribute(`${this.pfx}-typeDelay`)) || 90; 37 | this.originalLineDelay = this.lineDelay = options.lineDelay 38 | || parseFloat(this.container.getAttribute(`${this.pfx}-lineDelay`)) || 1500; 39 | this.progressLength = options.progressLength 40 | || parseFloat(this.container.getAttribute(`${this.pfx}-progressLength`)) || 40; 41 | this.progressChar = options.progressChar 42 | || this.container.getAttribute(`${this.pfx}-progressChar`) || '█'; 43 | this.progressPercent = options.progressPercent 44 | || parseFloat(this.container.getAttribute(`${this.pfx}-progressPercent`)) || 100; 45 | this.cursor = options.cursor 46 | || this.container.getAttribute(`${this.pfx}-cursor`) || '▋'; 47 | this.lineData = this.lineDataToElements(options.lineData || []); 48 | this.loadLines() 49 | if (!options.noInit) this.init() 50 | } 51 | 52 | loadLines() { 53 | // Load all the lines and create the container so that the size is fixed 54 | // Otherwise it would be changing and the user viewport would be constantly 55 | // moving as she/he scrolls 56 | const finish = this.generateFinish() 57 | finish.style.visibility = 'hidden' 58 | this.container.appendChild(finish) 59 | // Appends dynamically loaded lines to existing line elements. 60 | this.lines = [...this.container.querySelectorAll(`[${this.pfx}]`)].concat(this.lineData); 61 | for (let line of this.lines) { 62 | line.style.visibility = 'hidden' 63 | this.container.appendChild(line) 64 | } 65 | const restart = this.generateRestart() 66 | restart.style.visibility = 'hidden' 67 | this.container.appendChild(restart) 68 | this.container.setAttribute('data-termynal', ''); 69 | } 70 | 71 | /** 72 | * Initialise the widget, get lines, clear container and start animation. 73 | */ 74 | init() { 75 | /** 76 | * Calculates width and height of Termynal container. 77 | * If container is empty and lines are dynamically loaded, defaults to browser `auto` or CSS. 78 | */ 79 | const containerStyle = getComputedStyle(this.container); 80 | this.container.style.width = containerStyle.width !== '0px' ? 81 | containerStyle.width : undefined; 82 | this.container.style.minHeight = containerStyle.height !== '0px' ? 83 | containerStyle.height : undefined; 84 | 85 | this.container.setAttribute('data-termynal', ''); 86 | this.container.innerHTML = ''; 87 | for (let line of this.lines) { 88 | line.style.visibility = 'visible' 89 | } 90 | this.start(); 91 | } 92 | 93 | /** 94 | * Start the animation and rener the lines depending on their data attributes. 95 | */ 96 | async start() { 97 | this.addFinish() 98 | await this._wait(this.startDelay); 99 | 100 | for (let line of this.lines) { 101 | const type = line.getAttribute(this.pfx); 102 | const delay = line.getAttribute(`${this.pfx}-delay`) || this.lineDelay; 103 | 104 | if (type == 'input') { 105 | line.setAttribute(`${this.pfx}-cursor`, this.cursor); 106 | await this.type(line); 107 | await this._wait(delay); 108 | } 109 | 110 | else if (type == 'progress') { 111 | await this.progress(line); 112 | await this._wait(delay); 113 | } 114 | 115 | else { 116 | this.container.appendChild(line); 117 | await this._wait(delay); 118 | } 119 | 120 | line.removeAttribute(`${this.pfx}-cursor`); 121 | } 122 | this.addRestart() 123 | this.finishElement.style.visibility = 'hidden' 124 | this.lineDelay = this.originalLineDelay 125 | this.typeDelay = this.originalTypeDelay 126 | this.startDelay = this.originalStartDelay 127 | } 128 | 129 | generateRestart() { 130 | const restart = document.createElement('a') 131 | restart.onclick = (e) => { 132 | e.preventDefault() 133 | this.container.innerHTML = '' 134 | this.init() 135 | } 136 | restart.href = '#' 137 | restart.setAttribute('data-terminal-control', '') 138 | restart.innerHTML = "restart ↻" 139 | return restart 140 | } 141 | 142 | generateFinish() { 143 | const finish = document.createElement('a') 144 | finish.onclick = (e) => { 145 | e.preventDefault() 146 | this.lineDelay = 0 147 | this.typeDelay = 0 148 | this.startDelay = 0 149 | } 150 | finish.href = '#' 151 | finish.setAttribute('data-terminal-control', '') 152 | finish.innerHTML = "fast →" 153 | this.finishElement = finish 154 | return finish 155 | } 156 | 157 | addRestart() { 158 | const restart = this.generateRestart() 159 | this.container.appendChild(restart) 160 | } 161 | 162 | addFinish() { 163 | const finish = this.generateFinish() 164 | this.container.appendChild(finish) 165 | } 166 | 167 | /** 168 | * Animate a typed line. 169 | * @param {Node} line - The line element to render. 170 | */ 171 | async type(line) { 172 | const chars = [...line.textContent]; 173 | line.textContent = ''; 174 | this.container.appendChild(line); 175 | 176 | for (let char of chars) { 177 | const delay = line.getAttribute(`${this.pfx}-typeDelay`) || this.typeDelay; 178 | await this._wait(delay); 179 | line.textContent += char; 180 | } 181 | } 182 | 183 | /** 184 | * Animate a progress bar. 185 | * @param {Node} line - The line element to render. 186 | */ 187 | async progress(line) { 188 | const progressLength = line.getAttribute(`${this.pfx}-progressLength`) 189 | || this.progressLength; 190 | const progressChar = line.getAttribute(`${this.pfx}-progressChar`) 191 | || this.progressChar; 192 | const chars = progressChar.repeat(progressLength); 193 | const progressPercent = line.getAttribute(`${this.pfx}-progressPercent`) 194 | || this.progressPercent; 195 | line.textContent = ''; 196 | this.container.appendChild(line); 197 | 198 | for (let i = 1; i < chars.length + 1; i++) { 199 | await this._wait(this.typeDelay); 200 | const percent = Math.round(i / chars.length * 100); 201 | line.textContent = `${chars.slice(0, i)} ${percent}%`; 202 | if (percent>progressPercent) { 203 | break; 204 | } 205 | } 206 | } 207 | 208 | /** 209 | * Helper function for animation delays, called with `await`. 210 | * @param {number} time - Timeout, in ms. 211 | */ 212 | _wait(time) { 213 | return new Promise(resolve => setTimeout(resolve, time)); 214 | } 215 | 216 | /** 217 | * Converts line data objects into line elements. 218 | * 219 | * @param {Object[]} lineData - Dynamically loaded lines. 220 | * @param {Object} line - Line data object. 221 | * @returns {Element[]} - Array of line elements. 222 | */ 223 | lineDataToElements(lineData) { 224 | return lineData.map(line => { 225 | let div = document.createElement('div'); 226 | div.innerHTML = `${line.value || ''}`; 227 | 228 | return div.firstElementChild; 229 | }); 230 | } 231 | 232 | /** 233 | * Helper function for generating attributes string. 234 | * 235 | * @param {Object} line - Line data object. 236 | * @returns {string} - String of attributes. 237 | */ 238 | _attributes(line) { 239 | let attrs = ''; 240 | for (let prop in line) { 241 | // Custom add class 242 | if (prop === 'class') { 243 | attrs += ` class=${line[prop]} ` 244 | continue 245 | } 246 | if (prop === 'type') { 247 | attrs += `${this.pfx}="${line[prop]}" ` 248 | } else if (prop !== 'value') { 249 | attrs += `${this.pfx}-${prop}="${line[prop]}" ` 250 | } 251 | } 252 | 253 | return attrs; 254 | } 255 | } 256 | 257 | /** 258 | * HTML API: If current script has container(s) specified, initialise Termynal. 259 | */ 260 | if (document.currentScript.hasAttribute('data-termynal-container')) { 261 | const containers = document.currentScript.getAttribute('data-termynal-container'); 262 | containers.split('|') 263 | .forEach(container => new Termynal(container)) 264 | } -------------------------------------------------------------------------------- /fast_depends/core/builder.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from collections.abc import Callable, Sequence 3 | from copy import deepcopy 4 | from typing import ( 5 | TYPE_CHECKING, 6 | Annotated, 7 | Any, 8 | Optional, 9 | TypeVar, 10 | get_args, 11 | get_origin, 12 | ) 13 | 14 | from typing_extensions import ( 15 | ParamSpec, 16 | ) 17 | 18 | from fast_depends.dependencies.model import Dependant 19 | from fast_depends.library import CustomField 20 | from fast_depends.library.serializer import OptionItem, Serializer, SerializerProto 21 | from fast_depends.utils import ( 22 | get_typed_signature, 23 | is_async_gen_callable, 24 | is_coroutine_callable, 25 | is_gen_callable, 26 | ) 27 | 28 | from .model import CallModel 29 | 30 | if TYPE_CHECKING: 31 | from fast_depends.dependencies.provider import Key, Provider 32 | 33 | 34 | CUSTOM_ANNOTATIONS = ( 35 | Dependant, 36 | CustomField, 37 | ) 38 | 39 | 40 | P = ParamSpec("P") 41 | T = TypeVar("T") 42 | 43 | 44 | def build_call_model( 45 | call: Callable[..., Any], 46 | *, 47 | dependency_provider: "Provider", 48 | use_cache: bool = True, 49 | is_sync: bool | None = None, 50 | extra_dependencies: Sequence[Dependant] = (), 51 | serializer_cls: Optional["SerializerProto"] = None, 52 | serialize_result: bool = True, 53 | ) -> CallModel: 54 | if hasattr(call, "_fastdepends_call_") and not hasattr(call, "_mock_name"): 55 | call = call._fastdepends_call_ 56 | 57 | name = getattr(inspect.unwrap(call), "__name__", type(call).__name__) 58 | 59 | is_call_async = is_coroutine_callable(call) or is_async_gen_callable(call) 60 | if is_sync is None: 61 | is_sync = not is_call_async 62 | else: 63 | assert not (is_sync and is_call_async), ( 64 | f"You cannot use async dependency `{name}` at sync main" 65 | ) 66 | 67 | typed_params, return_annotation = get_typed_signature(call) 68 | if (is_call_generator := is_gen_callable(call) or is_async_gen_callable(call)) and ( 69 | return_args := get_args(return_annotation) 70 | ): 71 | return_annotation = return_args[0] 72 | 73 | if not serialize_result: 74 | return_annotation = inspect.Parameter.empty 75 | 76 | class_fields: list[OptionItem] = [] 77 | dependencies: dict[str, Key] = {} 78 | custom_fields: dict[str, CustomField] = {} 79 | positional_args: list[str] = [] 80 | keyword_args: list[str] = [] 81 | args_name: str | None = None 82 | kwargs_name: str | None = None 83 | 84 | for param_name, param in typed_params.parameters.items(): 85 | dep: Dependant | None = None 86 | custom: CustomField | None = None 87 | 88 | if param.annotation is inspect.Parameter.empty: 89 | annotation = Any 90 | 91 | elif get_origin(param.annotation) is Annotated: 92 | annotated_args = get_args(param.annotation) 93 | type_annotation = annotated_args[0] 94 | 95 | custom_annotations = [] 96 | regular_annotations = [] 97 | for arg in annotated_args[1:]: 98 | if isinstance(arg, CUSTOM_ANNOTATIONS): 99 | custom_annotations.append(arg) 100 | else: 101 | regular_annotations.append(arg) 102 | 103 | assert len(custom_annotations) <= 1, ( 104 | f"Cannot specify multiple `Annotated` Custom arguments for `{param_name}`!" 105 | ) 106 | 107 | next_custom = next(iter(custom_annotations), None) 108 | if next_custom is not None: 109 | if isinstance(next_custom, Dependant): 110 | dep = next_custom 111 | elif isinstance(next_custom, CustomField): 112 | custom = deepcopy(next_custom) 113 | else: # pragma: no cover 114 | raise AssertionError("unreachable") 115 | 116 | if regular_annotations: 117 | annotation = param.annotation 118 | else: 119 | annotation = type_annotation 120 | else: 121 | annotation = param.annotation 122 | else: 123 | annotation = param.annotation 124 | 125 | default: Any 126 | if param.kind is inspect.Parameter.VAR_POSITIONAL: 127 | default = () 128 | elif param.kind is inspect.Parameter.VAR_KEYWORD: 129 | default = {} 130 | else: 131 | default = param.default 132 | 133 | if isinstance(default, Dependant): 134 | assert not dep, "You can not use `Depends` with `Annotated` and default both" 135 | dep, default = default, Ellipsis 136 | 137 | elif isinstance(default, CustomField): 138 | assert not custom, ( 139 | "You can not use `CustomField` with `Annotated` and default both" 140 | ) 141 | custom, default = default, Ellipsis 142 | 143 | elif not dep and not custom: 144 | class_fields.append( 145 | OptionItem( 146 | field_name=param_name, 147 | field_type=annotation, 148 | default_value=Ellipsis 149 | if default is inspect.Parameter.empty 150 | else default, 151 | kind=param.kind, 152 | ) 153 | ) 154 | 155 | if dep: 156 | dependency = build_call_model( 157 | dep.dependency, 158 | dependency_provider=dependency_provider, 159 | use_cache=dep.use_cache, 160 | is_sync=is_sync, 161 | serializer_cls=serializer_cls, 162 | serialize_result=dep.cast_result, 163 | ) 164 | 165 | key = dependency_provider.add_dependant(dependency) 166 | 167 | _rebuild_override_model( 168 | dependency_provider=dependency_provider, 169 | dependency=dependency, 170 | key=key, 171 | serializer_cls=serializer_cls, 172 | ) 173 | 174 | overrided_dependency = dependency_provider.get_dependant(key) 175 | 176 | assert not (is_sync and is_coroutine_callable(overrided_dependency.call)), ( 177 | f"You cannot use async dependency `{overrided_dependency.call_name}` at sync main" 178 | ) 179 | 180 | dependencies[param_name] = key 181 | 182 | if not dep.cast: 183 | annotation = Any 184 | 185 | class_fields.append( 186 | OptionItem( 187 | field_name=param_name, 188 | field_type=annotation, 189 | source=dep, 190 | kind=param.kind, 191 | ) 192 | ) 193 | 194 | keyword_args.append(param_name) 195 | 196 | elif custom: 197 | assert not (is_sync and is_coroutine_callable(custom.use)), ( 198 | f"You cannot use async custom field `{type(custom).__name__}` at sync `{name}`" 199 | ) 200 | 201 | custom.set_param_name(param_name) 202 | custom_fields[param_name] = custom 203 | 204 | if not custom.cast: 205 | annotation = Any 206 | 207 | if custom.required: 208 | class_fields.append( 209 | OptionItem( 210 | field_name=param_name, 211 | field_type=annotation, 212 | default_value=Ellipsis 213 | if default is inspect.Parameter.empty 214 | else default, 215 | source=custom, 216 | kind=param.kind, 217 | ) 218 | ) 219 | 220 | else: 221 | class_fields.append( 222 | OptionItem( 223 | field_name=param_name, 224 | field_type=Optional[annotation], # noqa: UP045 225 | default_value=None 226 | if (default is inspect.Parameter.empty or default is Ellipsis) 227 | else default, 228 | source=custom, 229 | kind=param.kind, 230 | ) 231 | ) 232 | 233 | keyword_args.append(param_name) 234 | 235 | else: 236 | if param.kind is param.KEYWORD_ONLY: 237 | keyword_args.append(param_name) 238 | elif param.kind is param.VAR_KEYWORD: 239 | kwargs_name = param_name 240 | elif param.kind is param.VAR_POSITIONAL: 241 | args_name = param_name 242 | else: 243 | positional_args.append(param_name) 244 | 245 | serializer: Serializer | None = None 246 | if serializer_cls is not None: 247 | serializer = serializer_cls( 248 | name=name, 249 | options=class_fields, 250 | response_type=return_annotation, 251 | ) 252 | 253 | solved_extra_dependencies: list[Key] = [] 254 | for dep in extra_dependencies: 255 | dependency = build_call_model( 256 | dep.dependency, 257 | dependency_provider=dependency_provider, 258 | use_cache=dep.use_cache, 259 | is_sync=is_sync, 260 | serializer_cls=serializer_cls, 261 | ) 262 | 263 | key = dependency_provider.add_dependant(dependency) 264 | 265 | overrided_dependency = dependency_provider.get_dependant(key) 266 | 267 | assert not (is_sync and is_coroutine_callable(overrided_dependency.call)), ( 268 | f"You cannot use async dependency `{overrided_dependency.call_name}` at sync main" 269 | ) 270 | 271 | solved_extra_dependencies.append(key) 272 | 273 | return CallModel( 274 | call=call, 275 | serializer=serializer, 276 | params=tuple( 277 | i 278 | for i in class_fields 279 | if (i.field_name not in dependencies and i.field_name not in custom_fields) 280 | ), 281 | use_cache=use_cache, 282 | is_async=is_call_async, 283 | is_generator=is_call_generator, 284 | dependencies=dependencies, 285 | custom_fields=custom_fields, 286 | positional_args=positional_args, 287 | keyword_args=keyword_args, 288 | args_name=args_name, 289 | kwargs_name=kwargs_name, 290 | extra_dependencies=solved_extra_dependencies, 291 | dependency_provider=dependency_provider, 292 | serializer_cls=serializer_cls, 293 | ) 294 | 295 | 296 | def _rebuild_override_model( 297 | dependency_provider: "Provider", 298 | dependency: CallModel, 299 | key: "Key", 300 | serializer_cls: Optional["SerializerProto"], 301 | ) -> None: 302 | """Rebuild override model in case of a different serializer class""" 303 | override_model = dependency_provider.overrides.get(key) 304 | if override_model is not None and override_model.serializer_cls != serializer_cls: 305 | dependency_provider.override(dependency.call, override_model.call) 306 | --------------------------------------------------------------------------------