diff --git a/README.md b/README.md index 1dd40b8..62ab802 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,6 @@ class Printer: self.history.append(message) print(message) - @injectable class Service: def __init__(self, printer: Printer): @@ -46,12 +45,10 @@ class Service: def hello(self): self.printer.print("Hello world!") - @inject def main(service: Service): service.hello() - if __name__ == "__main__": main() ``` diff --git a/documentation/integrations.md b/documentation/integrations.md index 0bc2658..691d215 100644 --- a/documentation/integrations.md +++ b/documentation/integrations.md @@ -30,3 +30,18 @@ app = Application( services=InjectionServices(custom_module), ) ``` + +## [FastAPI](https://github.com/fastapi/fastapi) + +Exemple: + +```python +from fastapi import FastAPI +from injection.integrations.fastapi import Inject + +app = FastAPI() + +@app.get("/") +async def my_endpoint(service: MyService = Inject(MyService)): + ... +``` diff --git a/injection/_core/hook.py b/injection/_core/hook.py index 9cc9ae1..c6fab88 100644 --- a/injection/_core/hook.py +++ b/injection/_core/hook.py @@ -2,7 +2,7 @@ from collections.abc import Callable, Generator, Iterator from dataclasses import dataclass, field from inspect import isclass, isgeneratorfunction -from typing import Self +from typing import Any, Self from injection.exceptions import HookError @@ -48,11 +48,11 @@ def __apply_function( handler: Callable[P, T], function: HookFunction[P, T], ) -> Callable[P, T]: - if not isgeneratorfunction(function): + if not cls.__is_generator_function(function): return function # type: ignore[return-value] def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: - hook = function(*args, **kwargs) + hook: HookGenerator[T] = function(*args, **kwargs) # type: ignore[assignment] try: next(hook) @@ -87,6 +87,14 @@ def __apply_stack( return handler + @staticmethod + def __is_generator_function(obj: Any) -> bool: + for o in obj, getattr(obj, "__call__", None): + if isgeneratorfunction(o): + return True + + return False + def apply_hooks[**P, T]( handler: Callable[P, T], diff --git a/injection/integrations/__init__.py b/injection/integrations/__init__.py index e69de29..38aee0e 100644 --- a/injection/integrations/__init__.py +++ b/injection/integrations/__init__.py @@ -0,0 +1,11 @@ +from importlib.util import find_spec +from typing import Literal + +__all__ = ("_is_installed",) + + +def _is_installed(package: str, needed_for: object, /) -> Literal[True]: + if find_spec(package) is None: + raise RuntimeError(f"To use `{needed_for}`, {package} must be installed.") + + return True diff --git a/injection/integrations/blacksheep.py b/injection/integrations/blacksheep.py index e17a5ad..9b488ef 100644 --- a/injection/integrations/blacksheep.py +++ b/injection/integrations/blacksheep.py @@ -1,15 +1,11 @@ from typing import Any, override from injection import Module, mod +from injection.integrations import _is_installed __all__ = ("InjectionServices",) - -try: - import blacksheep # noqa: F401 -except ImportError as exc: # pragma: no cover - raise ImportError(f"To use `{__name__}`, blacksheep must be installed.") from exc -else: +if _is_installed("blacksheep", __name__): from rodi import ContainerProtocol diff --git a/injection/integrations/fastapi.py b/injection/integrations/fastapi.py new file mode 100644 index 0000000..a12e5f8 --- /dev/null +++ b/injection/integrations/fastapi.py @@ -0,0 +1,37 @@ +from collections.abc import Callable +from typing import Any + +from injection import Module, mod +from injection.exceptions import InjectionError +from injection.integrations import _is_installed + +__all__ = ("Inject",) + +if _is_installed("fastapi", __name__): + from fastapi import Depends + + +def Inject[T](cls: type[T] | Any, /, module: Module | None = None) -> Any: # noqa: N802 + """ + Declare a FastAPI dependency with `python-injection`. + """ + + dependency: InjectionDependency[T] = InjectionDependency(cls, module or mod()) + return Depends(dependency) + + +class InjectionDependency[T]: + __slots__ = ("__call__",) + + __call__: Callable[[], T] + + def __init__(self, cls: type[T] | Any, module: Module): + lazy_instance = module.get_lazy_instance(cls) + self.__call__ = lambda: self.__ensure(~lazy_instance, cls) + + @staticmethod + def __ensure[_T](instance: _T | None, cls: type[_T] | Any) -> _T: + if instance is None: + raise InjectionError(f"`{cls}` is an unknown dependency.") + + return instance diff --git a/injection/testing/__init__.pyi b/injection/testing/__init__.pyi index 0b76e57..7938d15 100644 --- a/injection/testing/__init__.pyi +++ b/injection/testing/__init__.pyi @@ -8,7 +8,7 @@ test_constant = _.constant test_injectable = _.injectable test_singleton = _.singleton -def load_test_profile(*additional_names: str) -> ContextManager[None]: +def load_test_profile(*other_profile_names: str) -> ContextManager[None]: """ Context manager or decorator for temporary use test module. """ diff --git a/poetry.lock b/poetry.lock index e2db966..44d6df4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -11,6 +11,26 @@ files = [ {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] +[[package]] +name = "anyio" +version = "4.4.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, + {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] + [[package]] name = "argon2-cffi" version = "23.1.0" @@ -110,13 +130,13 @@ jinja = ["Jinja2 (>=3.1.2,<3.2.0)"] [[package]] name = "certifi" -version = "2024.7.4" +version = "2024.8.30" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, - {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, ] [[package]] @@ -423,18 +443,38 @@ full = ["click (>=8.1.3,<8.2.0)", "httpx (<1)", "jinja2 (>=3.1.2,<3.2.0)", "rich [[package]] name = "faker" -version = "28.0.0" +version = "28.1.0" description = "Faker is a Python package that generates fake data for you." optional = false python-versions = ">=3.8" files = [ - {file = "Faker-28.0.0-py3-none-any.whl", hash = "sha256:6a3a08be54c37e05f7943d7ba5211d252c1de737687a46ad6f29209d8d5db11f"}, - {file = "faker-28.0.0.tar.gz", hash = "sha256:0d3c0399204aaf8205cc1750db443474ca0436f177126b2c27b798e8336cc74f"}, + {file = "Faker-28.1.0-py3-none-any.whl", hash = "sha256:b17d69312ef6485a720e21bffa997668c88876a5298b278e903ba706243c9c6b"}, + {file = "faker-28.1.0.tar.gz", hash = "sha256:bc460a0e6020966410d0b276043879abca0fac51890f3324bc254bb0a383ee3a"}, ] [package.dependencies] python-dateutil = ">=2.4" +[[package]] +name = "fastapi" +version = "0.112.2" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fastapi-0.112.2-py3-none-any.whl", hash = "sha256:db84b470bd0e2b1075942231e90e3577e12a903c4dc8696f0d206a7904a7af1c"}, + {file = "fastapi-0.112.2.tar.gz", hash = "sha256:3d4729c038414d5193840706907a41839d839523da6ed0c2811f1168cac1798c"}, +] + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.37.2,<0.39.0" +typing-extensions = ">=4.8.0" + +[package.extras] +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"] + [[package]] name = "guardpost" version = "1.0.2" @@ -452,6 +492,38 @@ rodi = ">=2.0.0" [package.extras] jwt = ["cryptography", "pyjwt"] +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "1.0.5" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, + {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<0.26.0)"] + [[package]] name = "httptools" version = "0.6.1" @@ -500,6 +572,42 @@ files = [ [package.extras] test = ["Cython (>=0.29.24,<0.30.0)"] +[[package]] +name = "httpx" +version = "0.27.2" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, + {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "idna" +version = "3.8" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"}, + {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"}, +] + [[package]] name = "iniconfig" version = "2.0.0" @@ -1020,29 +1128,29 @@ files = [ [[package]] name = "ruff" -version = "0.6.2" +version = "0.6.3" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"}, - {file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"}, - {file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"}, - {file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"}, - {file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"}, - {file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"}, - {file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"}, - {file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"}, - {file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"}, - {file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"}, - {file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"}, + {file = "ruff-0.6.3-py3-none-linux_armv6l.whl", hash = "sha256:97f58fda4e309382ad30ede7f30e2791d70dd29ea17f41970119f55bdb7a45c3"}, + {file = "ruff-0.6.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3b061e49b5cf3a297b4d1c27ac5587954ccb4ff601160d3d6b2f70b1622194dc"}, + {file = "ruff-0.6.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:34e2824a13bb8c668c71c1760a6ac7d795ccbd8d38ff4a0d8471fdb15de910b1"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bddfbb8d63c460f4b4128b6a506e7052bad4d6f3ff607ebbb41b0aa19c2770d1"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ced3eeb44df75353e08ab3b6a9e113b5f3f996bea48d4f7c027bc528ba87b672"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47021dff5445d549be954eb275156dfd7c37222acc1e8014311badcb9b4ec8c1"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d7bd20dc07cebd68cc8bc7b3f5ada6d637f42d947c85264f94b0d1cd9d87384"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:500f166d03fc6d0e61c8e40a3ff853fa8a43d938f5d14c183c612df1b0d6c58a"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42844ff678f9b976366b262fa2d1d1a3fe76f6e145bd92c84e27d172e3c34500"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70452a10eb2d66549de8e75f89ae82462159855e983ddff91bc0bce6511d0470"}, + {file = "ruff-0.6.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:65a533235ed55f767d1fc62193a21cbf9e3329cf26d427b800fdeacfb77d296f"}, + {file = "ruff-0.6.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2e2c23cef30dc3cbe9cc5d04f2899e7f5e478c40d2e0a633513ad081f7361b5"}, + {file = "ruff-0.6.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d8a136aa7d228975a6aee3dd8bea9b28e2b43e9444aa678fb62aeb1956ff2351"}, + {file = "ruff-0.6.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f92fe93bc72e262b7b3f2bba9879897e2d58a989b4714ba6a5a7273e842ad2f8"}, + {file = "ruff-0.6.3-py3-none-win32.whl", hash = "sha256:7a62d3b5b0d7f9143d94893f8ba43aa5a5c51a0ffc4a401aa97a81ed76930521"}, + {file = "ruff-0.6.3-py3-none-win_amd64.whl", hash = "sha256:746af39356fee2b89aada06c7376e1aa274a23493d7016059c3a72e3b296befb"}, + {file = "ruff-0.6.3-py3-none-win_arm64.whl", hash = "sha256:14a9528a8b70ccc7a847637c29e56fd1f9183a9db743bbc5b8e0c4ad60592a82"}, + {file = "ruff-0.6.3.tar.gz", hash = "sha256:183b99e9edd1ef63be34a3b51fee0a9f4ab95add123dbf89a71f7b1f0c991983"}, ] [[package]] @@ -1067,6 +1175,34 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "starlette" +version = "0.38.2" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.8" +files = [ + {file = "starlette-0.38.2-py3-none-any.whl", hash = "sha256:4ec6a59df6bbafdab5f567754481657f7ed90dc9d69b0c9ff017907dd54faeff"}, + {file = "starlette-0.38.2.tar.gz", hash = "sha256:c7c0441065252160993a1a37cf2a73bb64d271b17303e0b0c1eb7191cfb12d75"}, +] + +[package.dependencies] +anyio = ">=3.4.0,<5" + +[package.extras] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] + [[package]] name = "tabulate" version = "0.9.0" @@ -1123,4 +1259,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = ">=3.12, <4" -content-hash = "48144453fcca9b92530b6c450d7d6e8a7b8796ea52f752ea7c1947f1419955dc" +content-hash = "770f6c9cd08ac3be2d060c139f03ac98517a81d0ec4a6c7745dc8e02477c7182" diff --git a/pyproject.toml b/pyproject.toml index 9d1348e..8a7da92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,8 @@ ruff = "*" [tool.poetry.group.test.dependencies] blacksheep = "*" +fastapi = "*" +httpx = "*" pydantic = "*" pytest = "*" pytest-asyncio = "*" diff --git a/tests/integrations/test_blacksheep.py b/tests/integrations/test_blacksheep.py index 6b392a4..810de49 100644 --- a/tests/integrations/test_blacksheep.py +++ b/tests/integrations/test_blacksheep.py @@ -26,7 +26,7 @@ def class_name(cls) -> str: return "tests" @post("/integration") - async def test_integration(self) -> Response: + async def integration_endpoint(self) -> Response: assert isinstance(self.__dependency, Dependency) return self.no_content() diff --git a/tests/integrations/test_fastapi.py b/tests/integrations/test_fastapi.py new file mode 100644 index 0000000..e4c9f56 --- /dev/null +++ b/tests/integrations/test_fastapi.py @@ -0,0 +1,42 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from injection import singleton +from injection.exceptions import InjectionError +from injection.integrations.fastapi import Inject + +application = FastAPI() + + +@singleton +class Dependency: ... + + +@application.post("/integration", status_code=204) +async def integration_endpoint(dependency: Dependency = Inject(Dependency)): + assert isinstance(dependency, Dependency) + + +@application.post("/integration-unknown-dependency") +async def integration_unknown_dependency_endpoint( + __dependency: object = Inject(object), +): + raise NotImplementedError + + +class TestFastAPIIntegration: + @pytest.fixture(scope="class") + def client(self) -> TestClient: + return TestClient(application) + + def test_fastapi_integration_with_success(self, client): + response = client.post("/integration") + assert response.status_code == 204 + + def test_fastapi_integration_with_unknown_dependency_raise_injection_error( + self, + client, + ): + with pytest.raises(InjectionError): + client.post("/integration-unknown-dependency")