# that-depends A simple dependency injection framework for Python. # Quickstart # `that-depends` is a python dependency injection framework which, among other things, supports the following: - Async and sync dependency resolution - Scopes and granular context management - Dependency injection anywhere - Fully typed and tested - Compatibility with popular frameworks like `FastAPI` and `LiteStar` - Python 3.10+ support ______________________________________________________________________ ## Installation ```bash pip install that-depends ``` ```bash uv add that-depends ``` ______________________________________________________________________ ## Quickstart ### Define a creator ```python async def create_async_resource(): logger.debug("Async resource initiated") try: yield "async resource" finally: logger.debug("Async resource destructed") ``` ### Setup Dependency Injection Container with Providers ```python from that_depends import BaseContainer, providers class Container(BaseContainer): provider = providers.Resource(create_async_resource) ``` See the [containers documentation](https://that-depends.modern-python.org/introduction/ioc-container/index.md) for more information on defining the container. For a list of providers and their usage, see the [providers section](https://that-depends.modern-python.org/providers/collections/index.md). ### Resolve dependencies in your code ```python await Container.provider() ``` ### Inject providers in function arguments ```python from that_depends import inject, Provide @inject async def some_foo(value: str = Provide[Container.provider]): return value await some_foo() # "async resource" ``` See the [injection documentation](https://that-depends.modern-python.org/introduction/injection/index.md) for more information. ## Agents ### Skills `that-depends` ships with an [agent skill in the repository](https://github.com/modern-python/that-depends/tree/main/that_depends/.agents/skills/that-depends). This can be installed using an appropriate tool, such as [skilly](https://pypi.org/project/skilly/): ```shell uvx skilly scan ``` ### llms.txt `that-depends` provides a [llms.txt](https://that-depends.modern-python.org/llms.txt) file. You can find documentation on how to use it [here](https://llmstxt.org/). # Containers # The Dependency Injection Container Containers serve as a central place to store and manage providers. You also define your dependency graph in the containers. While providers can be defined outside of containers with that depends, this is not recommended if you want to use any [context features](https://that-depends.modern-python.org/providers/context-resources/index.md) ## Quickstart Define a container by subclassing `BaseContainer` and define your providers as class attributes. ```python from that_depends import BaseContainer class Container(BaseContainer): # define your providers here ``` Then you can build your dependency graph within the container: ```python from that_depends import providers class Container(BaseContainer): config = providers.Singleton(Config) session = providers.Factory(create_db_session, config=config.db) # (1)! user_repository = providers.Factory( UserRepository, session=session.cast, # (3)! config.users ) # (2)! ``` 1. The configuration will be resolved and then the `.db` attribute will be passed to the `create_db_session` creator as a keyword argument when resolving the `session` provider. 1. Depends on both the session and configuration providers. 1. Providers have the `cast` property that will change their type to the return type of their creator, use it to prevent type errors. # Usage with multiple containers You can use providers from other containers as following: ```python from tests import container from that_depends import BaseContainer, providers class InnerContainer(BaseContainer): sync_resource = providers.Resource(container.create_sync_resource) async_resource = providers.Resource(container.create_async_resource) class OuterContainer(BaseContainer): sequence = providers.List(InnerContainer.sync_resource, InnerContainer.async_resource) ``` But this way you have to manage `InnerContainer` lifecycle: ```python await InnerContainer.tear_down() ``` Or you can connect sub-containers to the main container: ```python OuterContainer.connect_containers(InnerContainer) # this will init resources for `InnerContainer` also await OuterContainer.init_resources() # and this will tear down resources for `InnerContainer` also await OuterContainer.tear_down() ``` # Injecting Dependencies # Injecting Providers in **that-depends** `that-depends` uses a decorator-based approach for both synchronous and asynchronous functions. By decorating a function with `@inject` and marking certain parameters as `Provide[...]`, **that-depends** will automatically resolve the specified providers at call time. ______________________________________________________________________ ## Overview In **that-depends**, you define your dependencies as `AbstractProvider` instances—e.g., `Singleton`, `Factory`, `Resource`, or others. These providers typically live inside a subclass of `BaseContainer`, making them globally accessible. When you want to use a provider in a function, you can mark a parameter’s **default value** as: ```python my_param = Provide[MyContainer.some_provider] ``` You then decorate the function with `@inject`. This tells `that-depends` to automatically resolve providers when you call your function. ______________________________________________________________________ ## Quick Start Below is a simple example demonstrating how to define a container, declare a provider, and inject that provider into a function. ### 1. Define a Container and a Provider ```python from that_depends import BaseContainer from that_depends.providers import Singleton class MyContainer(BaseContainer): greeting_provider = Singleton(lambda: "Hello from MyContainer") ``` For more details on Containers, refer to the [Containers](https://that-depends.modern-python.org/introduction/ioc-container/index.md) documentation. ### 2. Inject the Provider into a Function ```python from that_depends import inject, Provide @inject def greet_user(greeting: str = Provide[MyContainer.greeting_provider]) -> str: return f"Greeting: {greeting}" ``` Here: 1. We used `@inject` above `greet_user`. 1. We declared a parameter `greeting`, whose default value is `Provide[MyContainer.greeting_provider]`. ### 3. Call the Function ```python print(greet_user()) # "Greeting: Hello from MyContainer" ``` ______________________________________________________________________ ## The `@inject` Decorator in Detail ### Synchronous vs Asynchronous Functions `@inject` works on both sync and async functions. Just note that injecting async providers into sync functions is not supported. ```python @inject async def async_greet_user(greeting: str = Provide[MyContainer.greeting_provider]) -> str: # asynchronous operations... return f"Greeting: {greeting}" ``` ______________________________________________________________________ ## Using `Provide[...]` as a Default It is recommended to wrap your provider in `Provide[...]` when using it as a default in an injected function since it provides correct type resolution: ```python @inject def greet_user_direct( greeting: str = Provide[MyContainer.greeting_provider] # (1)! ) -> str: return f"Greeting: {greeting}" ``` 1. Notice that although `greeting` is a `str`, `mypy` and you IDE will not complain. ______________________________________________________________________ ## Injection Warnings If `@inject` finds **no** parameters whose default values are providers, it will issue a warning: > `Expected injection, but nothing found. Remove @inject decorator.` This is to avoid accidentally decorating a function that doesn’t actually require injection. ______________________________________________________________________ ## Specifying a Scope By default, `@inject` uses the `ContextScopes.INJECT` scope. If you want to override that, do: ```python from that_depends import inject from that_depends.providers.context_resources import ContextScopes @inject(scope=ContextScopes.REQUEST) def greet_user(greeting: str = Provide[MyContainer.greeting_provider]): ... ``` When `greet_user` is called, **that-depends**: 1. Initializes the context for all `REQUEST` (or `ANY`) scoped `args` and `kwargs`. 1. Resolves all providers in the `args` and `kwargs` of the function. 1. Calls your function with the resolved dependencies. For more details regarding scopes and context management, see the [Context Resources](https://that-depends.modern-python.org/providers/context-resources/index.md) documentation and the [Scopes](https://that-depends.modern-python.org/introduction/scopes/index.md) documentation. ______________________________________________________________________ ## Overriding Providers In tests or specialized scenarios, you may want to override a provider’s value temporarily. You can do so with the container’s `override_providers()` method or the provider’s own `override_context()`: ```python def test_greet_override(): # Override the greeting_provider with a mock value with MyContainer.override_providers_sync({"greeting_provider": "TestHello"}): result = greet_user() assert result == "Greeting: TestHello" ``` This is especially helpful for unit tests where you want to substitute real dependencies (e.g., database connections) with mocks or stubs. For more details on overring providers, see the [Overriding Providers](https://that-depends.modern-python.org/testing/provider-overriding/index.md) documentation. ______________________________________________________________________ ## Frequently Asked Questions 1. **Do I need to call `@inject` every time I reference a provider?**\ No—only when you want **automatic** injection of providers into function parameters. If you are resolving dependencies manually (e.g., `MyContainer.greeting_provider.resolve_sync()`), then `@inject` is not needed. 1. **What if I provide a custom argument to a parameter that has a default provider?**\ If you explicitly pass a value, that value overrides the injected default: ```python @inject def foo(x: int = Provide[MyContainer.number_factory]) -> int: return x print(foo()) # uses number_factory -> 42 print(foo(99)) # explicitly uses 99 ``` 1. **Can I combine `@inject` with other decorators?**\ Yes, you can. Generally, put `@inject` **below** others, depending on the order you need. If you run into issues, experiment with the order or handle context manually. ______________________________________________________________________ # Injection into Generator Functions `that-depends` supports dependency injections into generator functions. However, this comes with some minor limitations compared to regular functions. ## Quickstart You can use the `@inject` decorator to inject dependencies into generator functions: ```python @inject async def my_generator(value: str = Provide[Container.factory]) -> typing.AsyncGenerator[str, None]: yield value ``` ```python @inject def my_generator(value: str = Provide[Container.factory]) -> typing.Generator[str, None, None]: yield value ``` ```python @contextlib.asynccontextmanager @inject async def my_generator(value: str = Provide[Container.factory]) -> typing.AsyncIterator[str]: yield value ``` ```python @contextlib.contextmanager @inject def my_generator(value: str = Provide[Container.factory]) -> typing.Iterator[str]: yield value ``` ## Supported Generators ### Synchronous Generators `that-depends` supports injection into sync generator functions with the following signature: ```python Callable[P, Generator[, , ]] ``` This means that wrapping a sync generator with `@inject` will always preserve all the behaviour of the wrapped generator: - It will yield as expected - It will accept sending values via `send()` - It will raise `StopIteration` when the generator is exhausted or otherwise returns. ### Asynchronous Generators `that-depends` supports injection into async generator functions with the following signature: ```python Callable[P, AsyncGenerator[, None]] ``` This means that wrapping an async generator with `@inject` will have the following effects: - The generator will yield as expected - The generator will **not** accept values via `asend()` If you need to send values to an async generator, you can simply resolve dependencies in the generator body: ```python async def my_generator() -> typing.AsyncGenerator[float, float]: value = await Container.factory.resolve() receive = yield value # (1)! yield receive + value ``` 1. This receive will always be `None` if you would wrap this generator with @inject. ## ContextResources `that-depends` will **not** allow context initialization for [ContextResource](https://that-depends.modern-python.org/providers/context-resources/index.md) providers as part of dependency injection into a generator. This is the case for both async and sync injection. **For example:** ```python def sync_resource() -> typing.Iterator[float]: yield random.random() class Container(BaseContainer): sync_provider = providers.ContextResource(sync_resource).with_config(scope=ContextScopes.INJECT) dependent_provider = providers.Factory(lambda x: x, sync_provider.cast) @inject(scope=ContextScopes.INJECT) # (1)! def injected(val: float = Provide[Container.dependent_provider]) -> typing.Generator[float, None, None]: yield val # This will raise a `ContextProviderError`! next(_injected()) ``` 1. Matches context scope of `sync_provider` provider, which is a dependency of the `dependent_provider` provider. When calling `next(injected())`, `that-depends` will try to initialize a new context for the `sync_provider`, however, this is not permitted for generators, thus it will raise a `ContextProviderError`. Keep in mind that if context does not need to be initialized, the generator injection will work as expected: ```python def sync_resource() -> typing.Iterator[float]: yield random.random() class Container(BaseContainer): sync_provider = providers.ContextResource(sync_resource).with_config(scope=ContextScopes.REQUEST) dependent_provider = providers.Factory(lambda x: x, sync_provider.cast) @inject(scope=ContextScopes.INJECT) # (1)! def injected(val: float = Provide[Container.dependent_provider]) -> typing.Generator[float, None, None]: yield val with container_context(scope=ContextScopes.REQUEST): # This will resolve as expected next(_injected()) ``` Since no context initialization was needed, the generator will work as expected. 1. Scope provided to `@inject` no longer matches scope of the `sync_provider` ### Container Context Similarly to above, the `@container_context` also does **not** support generators: ```python @container_context(Container) async def my_generator() -> typing.AsyncIterator[None]: yield ``` The above code will raise a `UserWarning`. # Type based injection `that-depends` also supports dependency injection without explicitly referencing the provider of the dependency. ## Quick Start In order to make use of this, you need to bind providers to the type they will provide: ```python class Container(BaseContainer): my_provider = providers.Factory(lambda: random.random()).bind(float) ``` Then provide inject into your functions or generators: ```python @Container.inject async def foo(v: float = Provide()): return v ``` ```python @inject(container=Container) async def foo(v: float = Provide()): return v ``` ## Default bind Per default, providers will **not** be bound to any type, even if your creator function has type hints. So make sure to always bind your providers. You can also bind multiple types to the same provider: ```python class Container(BaseContainer): my_provider = providers.Factory(lambda: random.random()).bind(float, complex) ``` ## Contravariant binding Per default injection will be invariant to the bound types. If you wish to enable contravariance for your bound types you can do so by setting `contravariant=True` in the `bind` method: ```python class A: ... class B(A): ... class Container(BaseContainer): my_provider = providers.Factory(lambda: B()).bind(B, contravariant=True) @Container.inject async def foo(v: A = Provide()) -> A: # (1)! return v ``` 1. `v` will receive an instance of `B` since `A` is a supertype of `B`. ## Provider resolution by name The `@inject` decorator can be used to inject a provider by its name. This is useful when you want to inject a provider that is not directly imported in the current module. This serves two primary purposes: - A higher level of decoupling between container and your code. - Avoiding circular imports. ______________________________________________________________________ ## Usage To inject a provider by name, use the `Provide` marker with a string argument that has the following format: ```text Container.Provider[.attribute.attribute...] ``` The string will be validated when it is passed to `Provide[]`, thus will raise an exception immediately. **For example**: ```python from that_depends import BaseContainer, inject, Provide class Config(BaseSettings): name: str = "Damian" class A(BaseContainer): b = providers.Factory(Config) @inject def read(val = Provide["A.b.name"]): return val assert read() == "Damian" ``` ### Container alias Containers support aliases: ```python class A(BaseContainer): alias = "C" # replaces the container name. b = providers.Factory(Config) @inject def read(val = Provide["C.b.name"]): # `A` can no longer be used. return val assert read() == "Damian" ``` ______________________________________________________________________ ## Considerations This feature is primarily intended as a fallback when other options are not optimal or simply not available, thus is recommended to be used sparingly. If you do decide to use injection by name, consider the following: - In order for this type of injection to work, your container must be in scope when the injected function is called: ```python from that_depends import BaseContainer, inject, Provide @inject def injected(f = Provide["MyContainer.my_provider"]): ... injected() # will raise an Exception class MyContainer(BaseContainer): my_provider = providers.Factory(some_creator) injected() # will resolve ``` - Validation of whether you have provided a correct container name and provider name will only happen when the function is called. # Simple Providers # Singleton Provider A **Singleton** provider creates its instance once and caches it for all future injections or resolutions. When the instance is first requested (via `resolve_sync()` or `resolve()`), the underlying factory is called. On subsequent calls, the cached instance is returned without calling the factory again. ## How it Works ```python import random from that_depends import BaseContainer, Provide, inject, providers def some_function() -> float: """Generate number between 0.0 and 1.0""" return random.random() # define container with `Singleton` provider: class MyContainer(BaseContainer): singleton = providers.Singleton(some_function) # The provider will call `some_function` once and cache the return value # 1) Synchronous resolution MyContainer.singleton.resolve_sync() # e.g. 0.3 MyContainer.singleton.resolve_sync() # 0.3 (cached) # 2) Asynchronous resolution await MyContainer.singleton.resolve() # 0.3 (same cached value) # 3) Injection example @inject async def with_singleton(number: float = Provide[MyContainer.singleton]): # number == 0.3 ... ``` ### Teardown Support If you need to reset the singleton (for example, in tests or at application shutdown), you can call: ```python await MyContainer.singleton.tear_down() ``` This clears the cached instance, causing a new one to be created the next time `resolve_sync()` or `resolve()` is called.\ *(If you only ever use synchronous resolution, you can call `MyContainer.singleton.tear_down_sync()` instead.)* For further details refer to the [teardown documentation](https://that-depends.modern-python.org/introduction/tear-down/index.md). ______________________________________________________________________ ## Concurrency Safety `Singleton` is **thread-safe** and **async-safe**: 1. **Async Concurrency**\ If multiple coroutines call `resolve()` concurrently, the factory function is guaranteed to be called only once. All callers receive the same cached instance. 1. **Thread Concurrency**\ If multiple threads call `resolve_sync()` at the same time, the factory is only called once. All threads receive the same cached instance. ```python import threading import asyncio # In async code: async def main(): # calling resolve concurrently in different coroutines results = await asyncio.gather( MyContainer.singleton.resolve(), MyContainer.singleton.resolve(), ) # Both results point to the same instance # In threaded code: def thread_task(): instance = MyContainer.singleton.resolve_sync() ... threads = [threading.Thread(target=thread_task) for _ in range(5)] for t in threads: t.start() ``` ______________________________________________________________________ ## ThreadLocalSingleton Provider If you want each *thread* to have its own, separately cached instance, use **ThreadLocalSingleton**. This provider creates a new instance per thread and reuses that instance on subsequent calls *within the same thread*. ```python import random import threading from that_depends.providers import ThreadLocalSingleton def factory() -> int: """Return a random int between 1 and 100.""" return random.randint(1, 100) # ThreadLocalSingleton caches an instance per thread singleton = ThreadLocalSingleton(factory) # In a single thread: instance1 = singleton.resolve_sync() # e.g. 56 instance2 = singleton.resolve_sync() # 56 (cached in the same thread) # In multiple threads: def thread_task(): return singleton.resolve_sync() thread1 = threading.Thread(target=thread_task) thread2 = threading.Thread(target=thread_task) thread1.start() thread2.start() # thread1 and thread2 each get a different cached value ``` You can still use `.resolve()` with `ThreadLocalSingleton`, which will also maintain isolation per thread. However, note that this does *not* isolate instances per asynchronous Task – only per OS thread. ______________________________________________________________________ ## Example with `pydantic-settings` Consider a scenario where your application configuration is defined via [**pydantic-settings**](https://docs.pydantic.dev/latest/concepts/pydantic_settings/). Often, you only want to parse this configuration (e.g., from environment variables) once, then reuse it throughout the application. ```python from pydantic_settings import BaseSettings from pydantic import BaseModel class DatabaseConfig(BaseModel): address: str = "127.0.0.1" port: int = 5432 db_name: str = "postgres" class Settings(BaseSettings): auth_key: str = "my_auth_key" db: DatabaseConfig = DatabaseConfig() ``` ### Defining the Container Below, we define a container with a **Singleton** provider for our settings. We also define a separate async factory that connects to the database using those settings. ```python from that_depends import BaseContainer, providers async def get_db_connection(address: str, port: int, db_name: str): # e.g., create an async DB connection ... class MyContainer(BaseContainer): # We'll parse settings only once config = providers.Singleton(Settings) # We'll pass the config's DB fields into an async factory for a DB connection db_connection = providers.AsyncFactory( get_db_connection, config.db.address, config.db.port, config.db.db_name, ) ``` ### Injecting or Resolving in Code You can now inject these values directly into your functions with the `@inject` decorator: ```python from that_depends import inject, Provide @inject async def with_db_connection(conn = Provide[MyContainer.db_connection]): # conn is the created DB connection ... ``` Or you can manually resolve them when needed: ```python # Synchronously resolve the config cfg = MyContainer.config.resolve_sync() # Asynchronously resolve the DB connection connection = await MyContainer.db_connection.resolve() ``` By using `Singleton` for `Settings`, you avoid re-parsing the environment or re-initializing the configuration on each request. # Factories Factories are initialized on every call. ## Factory - Class or simple function is allowed. ```python import dataclasses from that_depends import BaseContainer, providers @dataclasses.dataclass(kw_only=True, slots=True) class IndependentFactory: dep1: str dep2: int class DIContainer(BaseContainer): independent_factory = providers.Factory(IndependentFactory, dep1="text", dep2=123) ``` ## AsyncFactory - Allows both sync and async creators. - Can only be resolved asynchronously, even if the creator is sync. ```python import datetime from that_depends import BaseContainer, providers async def async_factory() -> datetime.datetime: return datetime.datetime.now(tz=datetime.timezone.utc) class DIContainer(BaseContainer): async_factory = providers.AsyncFactory(async_factory) ``` > Note: If you have a class that has dependencies which need to be resolved asynchronously, you can use `AsyncFactory` to create instances of that class. The factory will handle the async resolution of dependencies. ## Retrieving provider as a Callable When you use a factory‑based provider such as `Factory` (for sync logic) or `AsyncFactory` (for async logic), the resulting provider instance has two special properties: - **`.provider`** — returns an *async callable* that, when awaited, resolves the resource. - **`.provider_sync`** — returns a *sync callable* that, when called, resolves the resource. You can think of these as no-argument functions that produce the resource you defined—similar to calling `resolve()` or `resolve_sync()` directly, but in a more convenient form when you want a standalone function handle. ______________________________________________________________________ ### Basic Usage #### Defining Providers in a Container Suppose you have a `BaseContainer` subclass that defines both a sync and an async resource: ```python from that_depends import BaseContainer from that_depends.providers import Factory, AsyncFactory def build_sync_message() -> str: return "Hello from sync provider!" async def build_async_message() -> str: # Possibly perform async setup, I/O, etc. return "Hello from async provider!" class MyContainer(BaseContainer): # Synchronous Factory sync_message = Factory(build_sync_message) # Asynchronous Factory async_message = AsyncFactory(build_async_message) ``` Here, `sync_message` is a `Factory` which calls a plain function, while `async_message` is an `AsyncFactory` which calls an async function. ______________________________________________________________________ #### Resolving Resources via `.provider` and `.provider_sync` The `.provider` property gives you an *async function* to await, and `.provider_sync` gives you a *synchronous* callable. They effectively wrap `.resolve()` and `.resolve_sync()`. **Synchronous Resolution** ```python # In a synchronous function or interactive session >>> msg = MyContainer.sync_message.provider_sync >>> print(msg) Hello from sync provider! ``` Here, `provider_sync` is a no-argument function that immediately returns the resolved value. **Asynchronous Resolution** ```python import asyncio async def main(): # Acquire the async resource by awaiting the provider property msg = await MyContainer.async_message.provider print(msg) asyncio.run(main()) ``` Within an async function, `MyContainer.async_message.provider` gives a no-argument async function to `await`. ______________________________________________________________________ ### Passing the Provider Function Around Sometimes you may want to store or pass around the provider function itself (rather than resolving it immediately): ```python class AnotherClass: def __init__(self, sync_factory_callable: callable): self._factory_callable = sync_factory_callable def get_message(self) -> str: return self._factory_callable() # Passing MyContainer.sync_message.sync_provider to AnotherClass provider_callable = MyContainer.sync_message.provider_sync another_instance = AnotherClass(provider_callable) print(another_instance.get_message()) # "Hello from sync provider!" ``` Because `.provider_sync` is just a callable returning your dependency, it can be shared easily throughout your code. ______________________________________________________________________ ### Example: Using Factories with Parameters `Factory` and `AsyncFactory` can accept dependencies (including other providers) as parameters: ```python def greet(name: str) -> str: return f"Hello, {name}!" class MyContainer(BaseContainer): name = Factory(lambda: "Alice") greeting = Factory(greet, name) ``` ```python >>> greeting_sync_fn = MyContainer.greeting.provider_sync >>> print(greeting_sync_fn()) Hello, Alice! ``` Under the hood, `greeting` calls `greet` with the result of `name.resolve_sync()`. ______________________________________________________________________ ### Context Considerations If your providers use `ContextResource` or require a named scope (for instance, `REQUEST`), you need to wrap your resolves in a context manager: ```python from that_depends.providers import container_context, ContextScopes class ContextfulContainer(BaseContainer): default_scope = ContextScopes.REQUEST # ... define context-based providers ... with container_context(ContextfulContainer, scope=ContextScopes.REQUEST): result = ContextfulContainer.some_resource.provider_sync() # ... ``` You still call `.provider_sync` or `.provider`, but the container or context usage ensures resources are valid within the required scope. This pattern simplifies passing creation logic around in your code, preserving testability and clarity—whether you need sync or async behavior. # Object Object provider returns an object “as is”. ```python from that_depends import BaseContainer, providers class DIContainer(BaseContainer): object_provider = providers.Object(1) assert DIContainer.object_provider() == 1 ``` # Resource Provider A **Resource** is a special provider that: - **Resolves** its dependency only **once** and **caches** the resolved instance for future injections. - **Includes** teardown (finalization) logic, unlike a plain `Singleton`. - **Supports** generator or async generator functions for creation (allowing a `yield` plus teardown in `finally`). - **Also** allows usage of classes that implement standard Python context managers (`typing.ContextManager` or `typing.AsyncContextManager`), but *does not* automatically integrate with `container_context`. This makes `Resource` ideal for dependencies that need: 1. A **single creation** step, 1. A **single finalization** step, 1. **Thread/async safety**—all consumers receive the same resource object, and concurrency is handled. ______________________________________________________________________ ## How It Works ### Defining a Sync or Async Resource You can define your creation logic as either a **generator** or a **context manager** class (sync or async). **Synchronous generator** example: ```python import typing def create_sync_resource() -> typing.Iterator[str]: print("Creating sync resource") try: yield "sync resource" finally: print("Tearing down sync resource") ``` **Asynchronous generator** example: ```python import typing async def create_async_resource() -> typing.AsyncIterator[str]: print("Creating async resource") try: yield "async resource" finally: print("Tearing down async resource") ``` You then attach them to a container: ```python from that_depends import BaseContainer from that_depends.providers import Resource class MyContainer(BaseContainer): sync_resource = Resource(create_sync_resource) async_resource = Resource(create_async_resource) ``` ______________________________________________________________________ ## Resolving and Teardown Once defined, you can explicitly **resolve** the resource and **tear it down**: ```python # Synchronous resource usage value_sync = MyContainer.sync_resource.resolve_sync() print(value_sync) # "sync resource" MyContainer.sync_resource.tear_down_sync() # Asynchronous resource usage import asyncio async def main(): value_async = await MyContainer.async_resource.resolve() print(value_async) # "async resource" await MyContainer.async_resource.tear_down() asyncio.run(main()) ``` - **`resolve_sync()`** or **`resolve()`**: Creates (if needed) and returns the resource instance. - **`tear_down_sync()`** or **`tear_down()`**: Closes/cleans up the resource (triggering your `finally` block or exiting the context manager) and resets the cached instance to `None`. A subsequent resolve call will then recreate it. ______________________________________________________________________ ## Concurrency Safety `Resource` is **safe** to use under **threading** and **asyncio** concurrency. Internally, a lock ensures only one resource instance is created per container: - Multiple threads calling `resolve_sync()` simultaneously will produce a **single** instance for that container. - Multiple coroutines calling `resolve()` simultaneously will likewise produce **only one** instance for that container in an async environment. ```python # Even if multiple coroutines call resolve in parallel, # only one instance is created at a time: await MyContainer.async_resource.resolve() # Similarly, multiple threads calling resolve_sync concurrently # still yield just one instance until teardown: MyContainer.sync_resource.resolve_sync() ``` ______________________________________________________________________ ## Using Context Managers Directly If your resource is a standard **context manager** or **async context manager** class, `Resource` will handle entering and exiting it under the hood. For example: ```python import typing from that_depends.providers import Resource class SyncFileManager: def __enter__(self) -> str: print("Opening file") return "/path/to/file" def __exit__(self, exc_type, exc_val, exc_tb): print("Closing file") sync_file_resource = Resource(SyncFileManager) # usage file_path = sync_file_resource.resolve_sync() print(file_path) sync_file_resource.tear_down_sync() ``` # Tear-down Certain providers in `that-depends` require explicit finalization. These providers implement the `SupportsTeardown` API. Containers also support finalizing their resources. ## Quick-start If you are using a provider that supports finalization such as a [Singleton](https://that-depends.modern-python.org/providers/singleton/index.md): First, define the container & provider. ```python from that_depends import BaseContainer, providers import random class MyContainer(BaseContainer): config = providers.Singleton(lambda: random.random()) ``` Resolve the provider somewhere in your code ```python await MyContainer.config() ``` When you want to reset the cached value: ```python await MyContainer.config.tear_down() ``` For [Resources](https://that-depends.modern-python.org/providers/resources/index.md) this will also call any finalization logic in your context manager. ______________________________________________________________________ ## Propagation Per default `that-depends` will propagate tear-down to dependent providers. This means that if you have defined a provider `A` that is dependent on provider `B`, when calling `await B.tear_down()`, this will also execute `await A.tear_down()`. **For example:** ```python class MyContainer(BaseContainer): B = providers.Singleton(lambda: random.random()) A = providers.Singleton(lambda x: x, B) b = await MyContainer.B() a = await MyContainer.A() assert a == b await MyContainer.B.tear_down() a_new = await MyContainer.A() assert a_new != a ``` If you do not wish to propagate tear-down simply call `tear_down(propagate=False)` or `tear_down_sync(propagate=False)`. ______________________________________________________________________ ## Sync tear-down If you need to call tear-down from a sync context you can use the `tear_down_sync()` method. However, keep in mind that because dependent resources might be async, this will fail to correctly finalize these async resources. Per default this will raise a `CannotTearDownSyncError`: ```python async def async_creator(val: float) -> typing.AsyncIterator[float]: yield val print("Finalization!") class MyContainer(BaseContainer): B = providers.Singleton(lambda: random.random()) A = providers.Resource(async_creator, B.cast) b = await MyContainer.B() a = await MyContainer.A() MyContainer.B.tear_down_sync() # raises ``` If you do not want to see these errors you can reduce this to a `RuntimeWarning`: ```python MyContainer.B.tear_down_sync(raise_on_async=False) ``` ______________________________________________________________________ ## Containers and tear-down Containers also support the `tear_down` and `tear_down_sync` methods. When calling `await Container.tear_down()` all providers in the container will be torn down. `Container.tear_down_sync()` is also implemented but not recommended unless you are sure all providers in your container are sync. > Container methods do not support the `propagate` & `raise_on_async` arguments, so if you need more granular control try to tear down the providers explicitly. # Context Providers # Context-Dependent Resources `that-depends` provides a way to manage two types of contexts: - A *global context* (a dictionary) where you can store objects for later retrieval. - *Resource-specific contexts*, which are managed by the `ContextResource` provider. To interact with both types of contexts, there are two separate interfaces: 1. Use the `container_context()` context manager to interact with the global context and manage `ContextResource` providers. 1. Directly manage a `ContextResource` context by using the `SupportsContext` protocol, which both containers and `ContextResource` providers implement. ______________________________________________________________________ ## Quick Start You must initialize a context before you can resolve a `ContextResource`. **Setup:** ```python import typing from that_depends import BaseContainer, providers, inject, Provide async def my_async_resource() -> typing.AsyncIterator[str]: print("Initializing async resource") try: yield "async resource" finally: print("Teardown of async resource") def my_sync_resource() -> typing.Iterator[str]: print("Initializing sync resource") try: yield "sync resource" finally: print("Teardown of sync resource") class MyContainer(BaseContainer): async_resource = providers.ContextResource(my_async_resource) sync_resource = providers.ContextResource(my_sync_resource) ``` Then, you can resolve the resource by initializing its context: ```python @MyContainer.async_resource.context @inject async def func(dep: str = Provide[MyContainer.async_resource]): return dep await func() # returns "async resource" ``` This will initialize a new context for `async_resource` each time `func` is called. ______________________________________________________________________ ## Global Context A global context can be initialized by using the `container_context` context manager. ```python from that_depends import container_context, fetch_context_item async with container_context(global_context={"key": "value"}): # run some code fetch_context_item("key") # returns 'value' ``` You can also use `container_context` as a decorator: ```python @container_context(global_context={"key": "value"}) async def func(): # run some code fetch_context_item("key") ``` The values stored in the `global_context` can be resolved as long as: 1. You are still within the scope of the context manager. 1. You have not initialized a new context: ```python async with container_context(global_context={"key": "value"}): # run some code fetch_context_item("key") async with container_context(preserve_global_context=False): # this will reset all contexts, including the global context. fetch_context_item("key") # Error! key not found ``` If you want to maintain the global context, you can initialize a new context with the `preserve_global_context` argument: ```python async with container_context(global_context={"key": "value"}): # run some code fetch_context_item("key") async with container_context(MyContainer, preserve_global_context=True): # preserves the global context fetch_context_item("key") # returns 'value' ``` Additionally, you can use the `global_context` argument in combination with `preserve_global_context` to extend the global context. This merges the two contexts together by key, with the new `global_context` taking precedence: ```python async with container_context(global_context={"key_1": "value_1", "key_2": "value_2"}): # run some code fetch_context_item("key_1") # returns 'value_1' async with container_context( global_context={"key_2": "new_value", "key_3": "value_3"}, preserve_global_context=True ): fetch_context_item("key_1") # returns 'value_1' fetch_context_item("key_2") # returns 'new_value' fetch_context_item("key_3") # returns 'value_3' ``` You can also retrieve items from the global context by type: ```python from that_depends import fetch_context_item_by_type with container_context(global_context={"key": 4}): fetch_context_item_by_type(int) # returns 4 ``` > Note: this will only return the first item of the type found if there are multiple candidates. ______________________________________________________________________ ## Context Resources To resolve a `ContextResource`, you must first initialize a new context for that resource. ```python async with container_context(MyContainer): # this will initialize a new context for MyContainer await MyContainer.async_resource.resolve() # "async resource" MyContainer.sync_resource.resolve_sync() # "sync resource" ``` Trying to resolve a `ContextResource` without first entering `container_context` will yield a `RuntimeError`: ```python value = MyContainer.sync_resource.resolve_sync() > RuntimeError: Context is not set. Use container_context ``` ### Resolving async and sync dependencies `container_context` implements both `AsyncContextManager` and `ContextManager`.\ This means you can enter an async context with: ```python async with container_context(MyContainer): ... ``` An async context allows resolution of both sync and async dependencies. A sync context can be entered using: ```python with container_context(MyContainer): ... ``` A sync context will only allow resolution of sync dependencies: ```python async def my_func(): with container_context(MyContainer): # enter sync context # trying to resolve async dependency await MyContainer.async_resource.resolve() > RuntimeError: AsyncResource cannot be resolved in a sync context. ``` ### More granular context initialization If you do not wish to simply reinitialize the context for all containers, you can initialize a context for a specific container: ```python # this will init a new context for all ContextResources in MyContainer and any connected containers. async with container_context(MyContainer): ... ``` Or for a specific resource: ```python # this will init a new context for the specific resource only. async with container_context(MyContainer.async_resource): ... ``` It is not necessary to use `container_context()` to do this. Instead, you can use the `SupportsContext` protocol described [here](#quick-reference). ### Context Hierarchy Resources are cached in the context after their first resolution.\ They are torn down when `container_context` exits: ```python async with container_context(MyContainer): value_outer = await MyContainer.resource.resolve() async with container_context(MyContainer): # new context -> resource will be resolved anew value_inner = await MyContainer.resource.resolve() assert value_inner != value_outer # previously resolved value is cached in the outer context assert value_outer == await MyContainer.resource.resolve() ``` ### Resolving resources whenever a function is called `ContextResource.context()` can also be used as a decorator: ```python @MyContainer._context_provider.context # wrap with a session-specific context @inject async def insert_into_database(session=Provide[MyContainer._context_provider]): ... ``` Each time you call `await insert_into_database()`, a new instance of `session` will be injected. ### Quick reference | Intention | Using `container_context()` | Using `SupportsContext` explicitly | Using `SupportsContext` decorator | | ----------------------------------------------- | --------------------------------------------- | ------------------------------------------ | --------------------------------- | | Reset a `provider.ContextResource` context | `async with container_context(my_provider):` | `async with my_provider.context_async():` | `@my_provider.context` | | Reset a sync `provider.ContextResource` context | `with container_context(my_provider):` | `with my_provider.context_sync():` | `@my_provider.context` | | Reset all resources in a container | `async with container_context(my_container):` | `async with my_container.context_async():` | `@my_container.context` | | Reset all sync resources in a container | `with container_context(my_container):` | `with my_container.context_sync():` | `@my_container.context` | > **Note:** the `context()` wrapper is technically not part of the `SupportsContext` API, however all classes which implement this `SupportsContext` also implement this method. ______________________________________________________________________ ## Middleware For `ASGI` applications, `that-depends` provides the `DIContextMiddleware` to manage context resources. The `DIContextMiddleware` accepts containers and resources as arguments and automatically initializes the context for the provided resources when an endpoint is called. **Example with `FastAPI`:** ```python import fastapi from that_depends.providers import DIContextMiddleware, ContextResource from that_depends import BaseContainer MyContainer: BaseContainer my_context_resource_provider: ContextResource my_app: fastapi.FastAPI # This will initialize the context for `my_context_resource_provider` and `MyContainer` whenever an endpoint is called. my_app.add_middleware(DIContextMiddleware, MyContainer, my_context_resource_provider) # This will initialize the context for all containers when an endpoint is called. my_app.add_middleware(DIContextMiddleware) ``` > `DIContextMiddleware` also supports the `global_context` and `preserve_global_context` arguments. # Named Scopes Named scopes allow you to define the lifecycle of a `ContextResource`. In essence, they provide a tool to manage when `ContextResources` can be resolved and when they should be finalized. Before continuing, make sure you're familiar with `ContextResource` providers by reading their [documentation](https://that-depends.modern-python.org/providers/context-resources/index.md). ## Quick Start By default, `ContextResources` have the named scope `ANY`, meaning they will be re-initialized each time you enter a named scope. You can change the scope of a `ContextResource` in two ways: ### Setting the scope for providers 1. By setting the `default_scope` attribute in the container class: ```python class MyContainer(BaseContainer): default_scope = ContextScope.APP p = providers.ContextResource(my_resource) ``` 1. By calling the `with_config()` method when creating a `ContextResource`. This also overrides the class default: ```python p = providers.ContextResource(my_resource).with_config(scope=ContextScope.APP) ``` ### Entering and exiting scopes Once you have assigned scopes to providers, you can enter a named scope using `container_context(scope=)`.\ After entering a scope, you can resolve resources that have been defined with that scope: ```python from that_depends import container_context async with container_context(scope=ContextScopes.APP): # resolve resources with scope APP await my_app_scoped_provider.resolve() ``` ## Checking the current scope If you want to check the current scope, you can use the `get_current_scope()` function: ```python from that_depends.providers.context_resources import get_current_scope, ContextScopes async with container_context(scope=ContextScopes.APP): assert get_current_scope() == ContextScopes.APP ``` ## Understanding resolution & strict scope providers In order for a `ContextResource` to be resolved, you must first initialize the context for that resource.\ When you call `container_context(scope=ContextScopes.APP)` this both enters the `APP` scope and (re-)initializes context for all providers that have `APP` scope. Scoped resources will prevent their context initialization if the current scope does not match their scope: ```python p = providers.ContextResource(my_resource).with_config(scope=ContextScopes.APP) async with p.context_async(): # will raise an InvalidContextError since current scope is `None` ... ``` Similarly, this will also not work: ```python async with container_context(p, scope=ContextScopes.REQUEST): # will raise and InvalidContextError since you are entering `REQUEST` scope ... ``` Once the context has been initialized, a resource can be resolved regardless of the current scope. For example: ```python await p.resolve() # will raise an exception async with container_context(p, scope=ContextScopes.APP): val_1 = await p.resolve() # will resolve async with container_context(p, scope=ContextScopes.REQUEST): val_2 = await p.resolve() # will resolve assert val_1 == val_2 # but value stays the same since context is the same ``` If you want resources to be resolved **only** in the specified scope, enable strict resolution: ```python p = providers.ContextResource(my_resource).with_config(scope=ContextScopes.APP, strict_scope=True) async with container_context(p, scope=ContextScopes.APP): await p.resolve() # will resolve async with container_context(scope=ContextScopes.REQUEST): await p.resolve() # will raise an exception ``` ## Entering a context by force If you for some reason need to (re-)initialize a context for a `ContextResource` outside of its defined scope, you can force enter its context: ```python p = providers.ContextResource(my_resource).with_config(scope=ContextScopes.APP) async with p.context_async(force=True): assert get_current_scope() == None await p.resolve() # will resolve ``` Or similarly using the `context` wrapper (both `ContextResource` providers and containers provide this API): ```python class Container(BaseContainer): p = providers.ContextResource(my_resource).with_config(scope=ContextScopes.APP) @Container.context(force=True) @inject async def injected(val = Provide[Container.p]): return p await injected() # will resolve ``` ## Predefined scopes `that-depends` includes four predefined scopes in the `ContextScopes` class: - `ANY`: Indicates that a resource can be resolved in any scope (even `None`). This scope cannot be entered, so it won’t be accepted by any class or method that requires entering a named scope. - `APP`: A convenience scope with no special behavior. - `REQUEST`: A convenience scope with no special behavior. - `INJECT`: The default scope of the `@inject` wrapper. Read more in the [Named scopes with the @inject wrapper](#named-scopes-with-the-inject-wrapper) section. > **Note:** The default scope, before entering any named scope, is `None`. You can pass `None` as a scope to providers, but since it cannot be entered, in most scenarios passing `None` simply means you did not specify a scope. ## Named scopes with the `@inject` wrapper The `@inject` wrapper also supports named scopes. Its default scope is `INJECT`, but you can pass any scope you like: ```python @inject(scope=ContextScopes.APP) def foo(...): ... ``` The `@inject` wrapper will enter a new context for each injected provider that matches the specified scope. However, it will not enter the scope by default! Here is a simple example: ```python def iterator() -> typing.Iterator[float]: yield random.random() class Container(BaseContainer): default_scope = ContextScopes.INJECT provider = providers.ContextResource(iterator) @inject(scope=ContextScopes.INJECT) def injected(v: int = Provide[Container.provider]) -> int: assert get_current_scope() == None # (1)! return v injected() ``` 1. Notice that `v` was resolved although the scope is still `None`. No scope was actually entered. This means that you will **not** be able to resolve `INJECT` scoped providers in a function annotated with `@inject` unless the provider is specified as the default in the `args` or `kwargs`: ```python class Container(BaseContainer): default_scope = ContextScopes.INJECT provider = providers.ContextResource(iterator).with_config(scope=ContextScopes.INJECT) another_provider = providers.ContextResource(iterator).with_config(scope=ContextScopes.INJECT) @inject(scope=ContextScopes.INJECT) def injected(v: int = Provide[Container.provider]) -> float: # (2)! assert get_current_scope() == None assert v == Container.provider.resolve_sync() # (3)! Container.another_provider.resolve_sync() # (1)! return v injected() ``` 1. This will raise a `RuntimeError`. Context for this provider was never initialized! 1. Context for `Container.provider` is initialized and will exit when the function returns. 1. This assertion will pass since the context for this provider is still the same. This implementation might seem complex at first glance, but it providers the following advantages: - Only context for `ContextResource` providers you need is initialized. This improves performance. - It discourages explicit resolution via `.resolve()` or `.resolve_sync()` in the function body. This pattern should be avoided since defining providers in function parameters allows for overriding by just passing an argument instead of having to override the provider. ### Entering a scope with `@inject` If you want to enter a scope for the duration of the function you can set `enter_scope=True` when using `@inject`: ```python class Container(BaseContainer): default_scope = ContextScopes.INJECT provider = providers.ContextResource(iterator).with_config(scope=ContextScopes.INJECT) another_provider = providers.ContextResource(iterator).with_config(scope=ContextScopes.INJECT) @inject(scope=ContextScopes.INJECT, enter_scope=True) def injected(v: int = Provide[Container.provider]) -> int: assert get_current_scope() == ContextScopes.INJECT Container.another_provider.resolve_sync() # (1)! return v ``` 1. This will resolve since this resource has been initialized when you entered the `INJECT` scope. ## Implementing custom scopes If the default scopes don’t fit your needs, you can define custom scopes by creating a `ContextScope` object: ```python from that_depends.providers.context_resources import ContextScope CUSTOM = ContextScope("CUSTOM") ``` If you want to group all of your scopes in one place, you can extend the `ContextScopes` class: ```python from that_depends.providers.context_resources import ContextScopes, ContextScope class MyContextScopes(ContextScopes): CUSTOM = ContextScope("CUSTOM") ``` ## Named scopes with middleware You can pass a named scope to the `DIContextMiddleware` to set the scope and pre-initialize scoped `ContextResources` for the entire request: ```python middleware = DIContextMiddleware(app, scope=ContextScopes.REQUEST) ``` # State The `State` provider stores a value as part of a context. It is useful when you want to pass a value into your Container that other providers depend on. ## Creating a state provider The `State` provider does not accept any arguments when it created. ```python from that_depends import BaseContainer, providers class Container(BaseContainer): my_state: providers.State[int] = providers.State() ``` ## Initializing state ```python with Container.my_state.init(42): print(await Container.my_state.resolve()) # 42 ``` ```python with Container.my_state.init(42): print(Container.my_state.resolve_sync()) # 42 ``` > Note: If you try to resolve a `State` provider without initializing it first it will raise an `StateNotInitializedError`. ## Nested state The `State` provider will always resolve the last initialize value. ```python with Container.my_state.init(1): print(Container.my_state.resolve_sync()) # 1 with Container.my_state.init(2): print(Container.my_state.resolve_sync()) # 2 print(Container.my_state.resolve_sync()) # 1 ``` # Providers that interact with other providers # Collections There are several collection providers: `List` and `Dict` ## List - List provider contains other providers. - Resolves into an immutable sequence of dependencies. ```python import random from that_depends import BaseContainer, providers class DIContainer(BaseContainer): random_number = providers.Factory(random.random) numbers_sequence = providers.List(random_number, random_number) DIContainer.numbers_sequence.resolve_sync() # (0.3035656170071561, 0.8280498192037787) ``` ## Dict - Dict provider is a collection of named providers. - Resolves into a read-only mapping of dependencies. ```python import random from that_depends import BaseContainer, providers class DIContainer(BaseContainer): random_number = providers.Factory(random.random) numbers_map = providers.Dict(key1=random_number, key2=random_number) DIContainer.numbers_map.resolve_sync() # mappingproxy({'key1': 0.6851384528299208, 'key2': 0.41044920948045294}) ``` # Selector The Selector provider chooses between provider based on a key. This resolves into a single dependency. The selector can be a callable that returns a string, an instance of `AbstractProvider` or a string. ## Callable selectors ```python import os from typing import Protocol from that_depends import BaseContainer, providers class StorageService(Protocol): ... class StorageServiceLocal(StorageService): ... class StorageServiceRemote(StorageService): ... class DIContainer(BaseContainer): storage_service = providers.Selector( lambda: os.getenv("STORAGE_BACKEND", "local"), local=providers.Factory(StorageServiceLocal), remote=providers.Factory(StorageServiceRemote), ) ``` ## Provider selectors In this example, we have a Pydantic-Settings class that contains the key to select the provider. ```python from typing import Literal from pydantic_settings import BaseSettings class Settings(BaseSettings): storage_backend: Literal["local", "remote"] = "remote" class DIContainer(BaseContainer): settings = providers.Singleton(Settings) selector = providers.Selector( settings.cast.storage_backend, local=providers.Factory(StorageServiceLocal), remote=providers.Factory(StorageServiceRemote), ) ``` ## Fixed string selectors This can be useful for quickly testing. ```python class DIContainer(BaseContainer): selector = providers.Selector( "local", local=providers.Factory(StorageServiceLocal), remote=providers.Factory(StorageServiceRemote), ) ``` # Integrations with other Frameworks # Usage with FastAPI See also [`modern-di-fastapi`](https://github.com/modern-python/modern-di-fastapi) — the equivalent FastAPI integration for [`modern-di`](https://github.com/modern-python/modern-di), the newer sibling DI framework. ## Installation To use **`that-depends`** with FastAPI, you need to install the package with the `fastapi` extra. You can do this using pip: ```bash pip install that-depends[fastapi] ``` ______________________________________________________________________ ## Creating a Container Suppose you have a simple container in a file called `mycontainer.py`: ```python # mycontainer.py import datetime from that_depends import BaseContainer, providers def create_time() -> datetime.datetime: """Example sync resource creator.""" return datetime.datetime.now() class MyContainer(BaseContainer): # A simple provider that always returns a "datetime.datetime" object current_time = providers.Factory(create_time) ``` Here, `MyContainer.current_time` is a provider that, when called, creates a new `datetime.datetime` using `create_time()`. ______________________________________________________________________ ## Using a custom Router class You can use the `create_fastapi_route_class()` method to create custom Route class for your application: ```python from that_depends.integrations.fastapi import create_fastapi_route_class my_route_class = create_fastapi_route_class() ``` You can then use this class in your `FastAPI` router: ```python from fastapi import APIRouter router = APIRouter(route_class=my_route_class) ``` This will enable you to use dependency injection in your `FastAPI` endpoints: > **Note**: If you don't want to use the custom router class, you can make use of `fastapi.Depends` instead. ```python from that_depends import Provide @router.get("/time") async def get_time(current_time: datetime.datetime = Provide[MyContainer.current_time]) -> datetime.datetime: return current_time ``` ```python from that_depends import Provide @router.get("/time") async def get_time(current_time: datetime.datetime = Provide["MyContainer.current_time"]) -> datetime.datetime: return current_time ``` ```python from fastapi import Depends @router.get("/time") async def get_time(current_time: datetime.datetime = Depends(MyContainer.current_time)) -> datetime.datetime: return current_time ``` ### Managing container context If you wish to initialize the container context you can simply pass arguments to `create_fastapi_route_class()`: ```python my_route_class = create_fastapi_route_class(Container, global_context={"key": "value"}, scope=ContextScopes.REQUEST) ``` In the above example, all `ContextResources` in our container that are `REQUEST` scoped will be initialized and the global context will be set. ## Integrating with FastAPI Using DIContextMiddleware The `DIContextMiddleware` can be used to manage context, but its features overlap with the [custom router class](#using-a-custom-router-class). The main advantage of using middleware is that you can set it up for your entire `FastAPI` application. > **Note:** If you want to use both the `DIContextMiddleware` and the custom router class, you should not pass any arguments to `create_fastapi_route_class()`. ### Setting Up the FastAPI App You can use **`that-depends`**’s `DIContextMiddleware` so that any request automatically initializes the context for your container(s). This approach is convenient if you want to: - Automatically initialize or tear down resources on each request. - Provide a global or request-level context dictionary you can read from your container. A minimal example in `main.py`: ```python # main.py from fastapi import FastAPI, Depends from starlette.responses import Response from that_depends.providers import DIContextMiddleware from mycontainer import MyContainer app = FastAPI() # Attach the middleware, optionally passing the container and/or a global_context app.add_middleware( DIContextMiddleware, MyContainer, global_context={"app_name": "MyApp"}, # optional dictionary available in the context ) @app.get("/") def get_time( # Using container's provider as a dependency: current_time: str = Depends(MyContainer.current_time) ) -> Response: return Response( content=f"Current time is: {current_time}", media_type="text/plain", ) ``` - **`DIContextMiddleware`** automatically sets a “global context” for every request. - The `Depends(MyContainer.current_time)` call is how you reference the container’s provider using the standard FastAPI injection system. To run this app: ```bash uvicorn main:app --reload ``` When you make a request to `/`, you will see the current time printed, and behind the scenes the that-depends container is in a context. > **Note**: If your container uses advanced context-based resources (e.g. `ContextResource`), you may also set `default_scope` in your container, or configure an explicit scope. See the advanced section below. ______________________________________________________________________ ## Examples of different Providers in FastAPI ### Singleton Providers ```python # Suppose in mycontainer.py from that_depends import BaseContainer, providers from pydantic import BaseModel class AppSettings(BaseModel): database_url: str = "sqlite:///:memory:" class MyAdvancedContainer(BaseContainer): # Provide a single, shared settings instance settings = providers.Singleton(AppSettings) ``` In your route: ```python from fastapi import FastAPI, Depends from mycontainer import MyAdvancedContainer app = FastAPI() @app.get("/settings") def read_settings(settings: AppSettings = Depends(MyAdvancedContainer.settings)): return {"db_url": settings.database_url} ``` ### Context Resources For “request-scoped” resources (e.g. a DB connection per request), you can use `ContextResource` in your container. This typically works in conjunction with `DIContextMiddleware` or a manual `container_context(...)` call. For example: ```python # Suppose in mycontainer.py import typing from that_depends import BaseContainer, providers from that_depends.providers.context_resources import ContextScopes async def db_session_creator() -> typing.AsyncIterator[str]: print("Opening DB connection") yield "fake_db_session" print("Closing DB connection") class MyScopedContainer(BaseContainer): # Tells that-depends that each resource has a context scope (like "request"). default_scope = ContextScopes.REQUEST db_session = providers.ContextResource(db_session_creator) ``` Then in your FastAPI app: ```python # main.py from fastapi import FastAPI, Depends from that_depends.providers.context_resources import DIContextMiddleware from mycontainer import MyScopedContainer app = FastAPI() app.add_middleware( DIContextMiddleware, MyContainer, ) @app.get("/") async def read_db( session: str = Depends(MyScopedContainer.db_session) ): return {"session": session} ``` - Because `MyScopedContainer.default_scope == ContextScopes.REQUEST`, each incoming request initializes a new DB session and tears it down automatically once the request completes (thanks to `DIContextMiddleware`). ______________________________________________________________________ ## Testing with FastAPI When writing unit tests, you can use `TestClient` from `starlette.testclient` or `pytest-asyncio` with standard FastAPI patterns. The `DIContextMiddleware` approach ensures resources are created and torn down automatically each request, so no special arrangement is necessary. **Example**: ```python import pytest from starlette.testclient import TestClient from main import app # The FastAPI app @pytest.fixture def client() -> TestClient: return TestClient(app) def test_read_db(client: TestClient): response = client.get("/") assert response.status_code == 200 data = response.json() assert data["session"] == "fake_db_session" ``` ______________________________________________________________________ ## Common Patterns and Tips 1. **Global vs. Request Context**: Decide whether your container’s dependencies should be globally shared (e.g., singletons) or created anew per request (e.g., database or session). 1. **Combining with FastAPI’s `Depends`**: Generally, you can pass `Depends(MyContainer.some_provider)` to route handlers. Under the hood, that-depends will be invoked. 1. **Overriding**: You can override a provider in tests by calling `MyContainer.some_provider.override(...)` or using the context manager `with MyContainer.some_provider.override_context(...):`. 1. **Performance**: If you have expensive creation logic (like a DB engine that can be reused globally), prefer using a `Singleton` or `Object` provider. If you need ephemeral resources, use `ContextResource` with the `DIContextMiddleware`. 1. **Custom Context**: If you do not want to rely on the middleware, you can manually create a context in any async function by calling `async with container_context():`. 1. **Multiple Containers**: You can define multiple containers and connect them (e.g., `ContainerA.connect_containers(ContainerB)`), or add them all to the `DIContextMiddleware`. For advanced usage, see the that-depends documentation on “container connection.” ### Accessing the FastAPI Request or Other Context Items Sometimes you want to pass the `fastapi.Request` (or other request-scoped data) into the container context so that providers can read it. You can do that either via the `DIContextMiddleware` (by customizing the `global_context` dynamically) or by writing your own dependency that calls `container_context()`. **Example**: Writing a custom dependency that sets up the context with the current `Request`: ```python # request_deps.py from fastapi import Request from typing import AsyncIterator from that_depends import container_context, fetch_context_item async def init_di_context(request: Request) -> AsyncIterator[None]: # We store the request in a that_depends global context async with container_context(global_context={"request": request}): yield ``` Then in your `FastAPI` route: ```python # main.py (extended) from fastapi import FastAPI, Request, Depends from starlette.responses import JSONResponse from request_deps import init_di_context from mycontainer import MyContainer app = FastAPI() # Notice no DIContextMiddleware here, but you could combine them @app.get("/request-based", dependencies=[Depends(init_di_context)]) async def get_request_info( # This provider fetches the request from context: request_in_container: Request = Depends(MyContainer.resolver(lambda: fetch_context_item("request"))), ): # The provider can read from that-depends context. We used .resolver(...) here as an example, # but you can define a dedicated provider in MyContainer that returns fetch_context_item("request"). return JSONResponse({"request_url": str(request_in_container.url)}) ``` Now each request calls `init_di_context(...)`, sets the request object into the global context, and any provider that reads from `"request"` can retrieve it. ______________________________________________________________________ # Usage with `FastStream` See also [`modern-di-faststream`](https://github.com/modern-python/modern-di-faststream) — the equivalent FastStream integration for [`modern-di`](https://github.com/modern-python/modern-di), the newer sibling DI framework. `that-depends` is out of the box compatible with `faststream.Depends()`: ```python from typing import Annotated from faststream import Depends from faststream.asgi import AsgiFastStream from faststream.rabbit import RabbitBroker broker = RabbitBroker() app = AsgiFastStream(broker) @broker.subscriber(queue="queue") async def process( text: str, suffix: Annotated[ str, Depends(Container.suffix_factory) # (1)! ], ) -> None: return text + suffix ``` 1. This would be the same as `Provide[Container.suffix_factory]` ## Context Middleware If you are using [ContextResource](https://that-depends.modern-python.org/providers/context-resources/index.md) provider, you likely will want to initialize a context before processing message with `faststream.` `that-depends` provides integration for these use cases: ```shell pip install that-depends[faststream] ``` Then you can use the `DIContextMiddleware` with your broker: ```python from that_depends.integrations.faststream import DIContextMiddleware from that_depends import ContextScopes from faststream.rabbit import RabbitBroker broker = RabbitBroker(middlewares=[DIContextMiddleware(Container, scope=ContextScopes.REQUEST)]) ``` ## Example Here is an example that includes life-cycle events: ```python import datetime import contextlib import typing from faststream import FastStream, Depends, Logger from faststream.rabbit import RabbitBroker from tests import container @contextlib.asynccontextmanager async def lifespan_manager() -> typing.AsyncIterator[None]: try: yield finally: await container.DIContainer.tear_down() broker = RabbitBroker() app = FastStream(broker, lifespan=lifespan_manager) @broker.subscriber("in") async def read_root( logger: Logger, some_dependency: typing.Annotated[ container.DependentFactory, Depends(container.DIContainer.dependent_factory) ], ) -> datetime.datetime: startup_time = some_dependency.async_resource logger.info(startup_time) return startup_time @app.after_startup async def t() -> None: await broker.publish(None, "in") ``` # Usage with `Litestar` See also [`modern-di-litestar`](https://github.com/modern-python/modern-di-litestar) — the equivalent Litestar integration for [`modern-di`](https://github.com/modern-python/modern-di), the newer sibling DI framework. ```python import typing import fastapi import contextlib from litestar import Litestar, get from litestar.di import Provide from litestar.status_codes import HTTP_200_OK from litestar.testing import TestClient from tests import container @get("/") async def index(injected: str) -> str: return injected @contextlib.asynccontextmanager async def lifespan_manager(_: fastapi.FastAPI) -> typing.AsyncIterator[None]: try: yield finally: await container.DIContainer.tear_down() app = Litestar( route_handlers=[index], dependencies={"injected": Provide(container.DIContainer.async_resource)}, lifespan=[lifespan_manager], ) def test_litestar_di() -> None: with (TestClient(app=app) as client): response = client.get("/") assert response.status_code == HTTP_200_OK, response.text assert response.text == "async resource" ``` # Testing # Fixture ## Dependencies teardown When using dependency injection in tests, it's important to properly tear down resources after tests complete. Without proper teardown, the Python event loop might close before resources have a chance to shut down properly, leading to errors like `RuntimeError: Event loop is closed`. You can set up automatic teardown using a pytest fixture: ```python import pytest_asyncio from typing import AsyncGenerator from my_project import DIContainer @pytest_asyncio.fixture(autouse=True) async def di_container_teardown() -> AsyncGenerator[None]: try: yield finally: await DIContainer.tear_down() ``` # Provider overriding DI container provides, in addition to direct dependency injection, another very important functionality: **dependencies or providers overriding**. Any provider registered with the container can be overridden. This can help you replace objects with simple stubs, or with other objects. **Override affects all providers that use the overridden provider (*see example*)**. ## Example ```python from pydantic_settings import BaseSettings from sqlalchemy import create_engine, Engine, text from testcontainers.postgres import PostgresContainer from that_depends import BaseContainer, providers, Provide, inject class SomeSQLADao: def __init__(self, *, sqla_engine: Engine): self.engine = sqla_engine self._connection = None def __enter__(self): self._connection = self.engine.connect() return self def __exit__(self, exc_type, exc_val, exc_tb): self._connection.close() def exec_query(self, query: str): return self._connection.execute(text(query)) class Settings(BaseSettings): db_url: str = 'some_production_db_url' class DIContainer(BaseContainer): settings = providers.Singleton(Settings) sqla_engine = providers.Singleton(create_engine, settings.db_url) some_sqla_dao = providers.Factory(SomeSQLADao, sqla_engine=sqla_engine) @inject def exec_query_example(some_sqla_dao=Provide[DIContainer.some_sqla_dao]): with some_sqla_dao: result = some_sqla_dao.exec_query('SELECT 234') return next(result) def main(): pg_container = PostgresContainer(image='postgres:alpine3.19') pg_container.start() db_url = pg_container.get_connection_url() """ We override only settings, but this override will also affect the 'sqla_engine' and 'some_sqla_dao' providers because the 'settings' provider is used by them! """ local_testing_settings = Settings(db_url=db_url) DIContainer.settings.override_sync(local_testing_settings) try: result = exec_query_example() assert result == (234,) finally: DIContainer.settings.reset_override_sync() pg_container.stop() if __name__ == '__main__': main() ``` The example above shows how overriding a nested provider ('*settings*') affects another provider ('*engine*' and '*some_sqla_dao*'). ## Override multiple providers The example above looked at overriding only one settings provider, but the container also provides the ability to override multiple providers at once with method `override_providers_sync`. The code above could remain the same except that the single provider override could be replaced with the following code: ```python def main(): pg_container = PostgresContainer(image='postgres:alpine3.19') pg_container.start() db_url = pg_container.get_connection_url() local_testing_settings = Settings(db_url=db_url) providers_for_overriding = { 'settings': local_testing_settings, # more values... } with DIContainer.override_providers_sync(providers_for_overriding): try: result = exec_query_example() assert result == (234,) finally: pg_container.stop() ``` ______________________________________________________________________ ## Using with Litestar In order to be able to inject dependencies of any type instead of existing objects, we need to **change the typing** for the injected parameter as follows: ```python3 import typing from functools import partial from typing import Annotated from unittest.mock import Mock from litestar import Litestar, Router, get from litestar.di import Provide from litestar.params import Dependency from litestar.testing import TestClient from that_depends import BaseContainer, providers class ExampleService: def do_smth(self) -> str: return "something" class DIContainer(BaseContainer): example_service = providers.Factory(ExampleService) @get(path="/another-endpoint", dependencies={"example_service": Provide(DIContainer.example_service)}) async def endpoint_handler( example_service: Annotated[ExampleService, Dependency(skip_validation=True)], ) -> dict[str, typing.Any]: return {"object": example_service.do_smth()} # or if you want a little less code NoValidationDependency = partial(Dependency, skip_validation=True) @get(path="/another-endpoint", dependencies={"example_service": Provide(DIContainer.example_service)}) async def endpoint_handler( example_service: Annotated[ExampleService, NoValidationDependency()], ) -> dict[str, typing.Any]: return {"object": example_service.do_smth()} router = Router( path="/router", route_handlers=[endpoint_handler], ) app = Litestar(route_handlers=[router]) ``` Now we are ready to write tests with **overriding** and this will work with **any types**: ```python3 def test_litestar_endpoint_with_overriding() -> None: some_service_mock = Mock(do_smth=lambda: "mock func") with DIContainer.example_service.override_context_sync(some_service_mock), TestClient(app=app) as client: response = client.get("/router/another-endpoint") assert response.status_code == 200 assert response.json()["object"] == "mock func" ``` More about `Dependency` in the [Litestar documentation](https://docs.litestar.dev/2/usage/dependency-injection.html#the-dependency-function). ______________________________________________________________________ ## Overriding and tear-down If you have a provider `A` that caches the resolved value, which depends on a provider `B` that you wish to override you might experience the following behavior: ```python class MyContainer(BaseContainer): B = providers.Singleton(lambda: 1) A = providers.Singleton(lambda x: x, B) a_old = await MyContainer.A() MyContainer.B.override_sync(32) # will not reset A's cached value a_new = await MyContainer.A() assert a_old != a_new # raises ``` This is due to the fact that `A` caches the value and doesn't get reset when you override `B`. If you wish to fix this you can tell the provider to tear-down children on override: ```python MyContainer.B.override_sync(32, tear_down_children=True) ``` # Experimental features # Lazy Provider The `LazyProvider` enables you to reference other providers without explicitly importing them into your module. This can be helpful if you have a circular dependency between providers in multiple containers. ## Creating a Lazy Provider ```python from that_depends.experimental import LazyProvider lazy_p = LazyProvider("my.module.attribute.path") ``` ```python from that_depends.experimental import LazyProvider lazy_p = LazyProvider(module_string="my.module", provider_string="attribute.path") ``` ## Usage You can use the lazy provider in exactly the same way as you would use the referenced provider. ```python # first_container.py from that_depends import BaseContainer, providers, ContextScopes def my_creator() -> int: yield 42 class FirstContainer(BaseContainer): value_provider = providers.ContextResource(my_creator).with_config(scope=ContextScopes.APP) ``` You can lazily import this provider: ```python # second_container.py from that_depends.experimental import LazyProvider from that_depends import BaseContainer, providers class SecondContainer(BaseContainer): lazy_value: LazyProvider[int] = LazyProvider("first_container.FirstContainer.value_provider") with SecondContainer.lazy_value.context_sync(force=True): SecondContainer.lazy_value.resolve_sync() # 42 ``` # Ecosystem # Ecosystem `that-depends` is part of the [`modern-python`](https://github.com/modern-python) organization — a collection of open-source templates and libraries for production-ready Python applications. ## Newer DI framework: `modern-di` If you're starting a new project, consider [`modern-di`](https://github.com/modern-python/modern-di) — the newer DI framework from the same author. It ships as a small core plus a family of thin framework adapters, in contrast to `that-depends`'s batteries-included approach. `that-depends` remains actively maintained. The [migration guide on the modern-di docs](https://modern-di.modern-python.org/migration/from-that-depends/) walks through the API differences if you want to move an existing project across. ### `modern-di` family | Package | What it does | | ------------------------------------------------------------------------------- | ---------------------------------------- | | [`modern-di`](https://github.com/modern-python/modern-di) | Core DI framework with scopes and groups | | [`modern-di-fastapi`](https://github.com/modern-python/modern-di-fastapi) | FastAPI integration | | [`modern-di-litestar`](https://github.com/modern-python/modern-di-litestar) | Litestar integration | | [`modern-di-faststream`](https://github.com/modern-python/modern-di-faststream) | FastStream integration | | [`modern-di-typer`](https://github.com/modern-python/modern-di-typer) | Typer (CLI) integration | | [`modern-di-pytest`](https://github.com/modern-python/modern-di-pytest) | Pytest fixtures from DI providers | ## Project templates End-to-end examples using `modern-di` for dependency injection: - [`fastapi-sqlalchemy-template`](https://github.com/modern-python/fastapi-sqlalchemy-template) — dockerized web application with DI on FastAPI, SQLAlchemy 2, PostgreSQL - [`litestar-sqlalchemy-template`](https://github.com/modern-python/litestar-sqlalchemy-template) — dockerized web application on LiteStar, SQLAlchemy 2, PostgreSQL ## Full project index See the [`modern-python` organization profile](https://github.com/modern-python) for the complete categorized list, including microservice utilities (`lite-bootstrap`, the `faststream-*` family) and helper packages (`db-retry`, `eof-fixer`).