From 5d84d1ffafcef8a1d75fbd59683448d95350fe08 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sat, 3 Oct 2026 13:03:23 +0300 Subject: [PATCH] docs: make code examples run and fix factual errors - context-resources: add default_scope to the quick-start container, pass a context item where container_context/DIContextMiddleware had no arguments, use the real sync-context error message, list only global_context and scope as middleware arguments - scopes: ContextScopes.APP, return val, nested-scope example no longer re-enters p in REQUEST, default_scope caveat - fastapi: scoped middleware example uses MyScopedContainer with scope=REQUEST, sync override helpers, missing imports - factories: call provider/provider_sync, import ContextScopes from that_depends, context example uses a Factory, drop closing sentence - injection, generator-injection, ioc-container, object, index, lazy: syntax and name errors - multiple-containers, faststream, litestar: inline definitions instead of importing the repo's tests package; Litestar lifespan type - README, resources, provider-overriding, contributing: wording fixes --- README.md | 4 ++-- docs/dev/contributing.md | 4 ++-- docs/experimental/lazy.md | 4 +++- docs/index.md | 5 +++++ docs/integrations/fastapi.md | 16 +++++++++------ docs/integrations/faststream.md | 25 ++++++++++++++++++----- docs/integrations/litestar.md | 20 ++++++++++++------ docs/introduction/generator-injection.md | 4 ++-- docs/introduction/injection.md | 6 +++--- docs/introduction/ioc-container.md | 5 +++-- docs/introduction/multiple-containers.md | 16 ++++++++++++--- docs/introduction/scopes.md | 14 +++++++------ docs/providers/context-resources.md | 14 ++++++------- docs/providers/factories.md | 26 +++++++++++++++--------- docs/providers/object.md | 2 +- docs/providers/resources.md | 11 +++++----- docs/testing/provider-overriding.md | 6 +----- 17 files changed, 114 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 886eda17..4c56cca7 100644 --- a/README.md +++ b/README.md @@ -21,11 +21,11 @@ Simple, typed dependency injection framework for Python. -It is production-ready and gives you the following: +Features: - Simple async-first DI framework with IOC-container. - Python 3.10+ support. - Full coverage by types annotations (mypy in strict mode, pyrefly). -- Inbuilt FastAPI, FastStream and LiteStar compatibility. +- Built-in FastAPI, FastStream and Litestar integrations. - Dependency context management with scopes. - Overriding dependencies for tests. - Injecting dependencies in functions and coroutines without wiring. diff --git a/docs/dev/contributing.md b/docs/dev/contributing.md index 7309825a..ebf8e69d 100644 --- a/docs/dev/contributing.md +++ b/docs/dev/contributing.md @@ -1,11 +1,11 @@ # Contributing -`that-depends` is an opensource project, and we are opened to new contributors. +`that-depends` is an opensource project, and we are open to new contributors. ## Getting started 1. Make sure that you have [uv](https://docs.astral.sh/uv/) and [just](https://just.systems/) installed. 2. Clone project: ``` -git@github.com:modern-python/that-depends.git +git clone git@github.com:modern-python/that-depends.git cd that-depends ``` 3. Install dependencies running `just install` diff --git a/docs/experimental/lazy.md b/docs/experimental/lazy.md index 3770246d..c35f23f1 100644 --- a/docs/experimental/lazy.md +++ b/docs/experimental/lazy.md @@ -29,9 +29,11 @@ You can use the lazy provider in exactly the same way as you would use the refer ```python # first_container.py +import typing + from that_depends import BaseContainer, providers, ContextScopes -def my_creator() -> int: +def my_creator() -> typing.Iterator[int]: yield 42 class FirstContainer(BaseContainer): diff --git a/docs/index.md b/docs/index.md index 91819524..7128f1ee 100644 --- a/docs/index.md +++ b/docs/index.md @@ -36,6 +36,11 @@ supports the following: ### Define a creator ```python +import logging + +logger = logging.getLogger(__name__) + + async def create_async_resource(): logger.debug("Async resource initiated") try: diff --git a/docs/integrations/fastapi.md b/docs/integrations/fastapi.md index f0c48ff0..69ef6230 100644 --- a/docs/integrations/fastapi.md +++ b/docs/integrations/fastapi.md @@ -93,7 +93,9 @@ This will enable you to use dependency injection in your `FastAPI` endpoints: 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) +from that_depends import ContextScopes + +my_route_class = create_fastapi_route_class(MyContainer, global_context={"key": "value"}, scope=ContextScopes.REQUEST) ``` In the above example, all `ContextResources` in our container that are `REQUEST` scoped will be initialized @@ -220,13 +222,14 @@ Then in your FastAPI app: ```python # main.py from fastapi import FastAPI, Depends -from that_depends.providers.context_resources import DIContextMiddleware +from that_depends.providers.context_resources import ContextScopes, DIContextMiddleware from mycontainer import MyScopedContainer app = FastAPI() app.add_middleware( DIContextMiddleware, - MyContainer, + MyScopedContainer, + scope=ContextScopes.REQUEST, ) @app.get("/") @@ -269,9 +272,9 @@ def test_read_db(client: TestClient): 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). 2. **Combining with FastAPI’s `Depends`**: Generally, you can pass `Depends(MyContainer.some_provider)` to route handlers. Under the hood, that-depends will be invoked. -3. **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(...):`. +3. **Overriding**: You can override a provider in tests by calling `MyContainer.some_provider.override_sync(...)` or using the context manager `with MyContainer.some_provider.override_context_sync(...):`. 4. **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`. -5. **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():`. +5. **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(MyContainer):`. 6. **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.” @@ -285,7 +288,7 @@ Sometimes you want to pass the `fastapi.Request` (or other request-scoped data) # request_deps.py from fastapi import Request from typing import AsyncIterator -from that_depends import container_context, fetch_context_item +from that_depends import container_context async def init_di_context(request: Request) -> AsyncIterator[None]: # We store the request in a that_depends global context @@ -299,6 +302,7 @@ Then in your `FastAPI` route: # main.py (extended) from fastapi import FastAPI, Request, Depends from starlette.responses import JSONResponse +from that_depends import fetch_context_item from request_deps import init_di_context from mycontainer import MyContainer diff --git a/docs/integrations/faststream.md b/docs/integrations/faststream.md index 323ba9d8..f860da0f 100644 --- a/docs/integrations/faststream.md +++ b/docs/integrations/faststream.md @@ -56,14 +56,29 @@ broker = RabbitBroker(middlewares=[DIContextMiddleware(Container, scope=ContextS Here is an example that includes life-cycle events: ```python -import datetime import contextlib +import dataclasses +import datetime import typing from faststream import FastStream, Depends, Logger from faststream.rabbit import RabbitBroker -from tests import container +from that_depends import BaseContainer, providers + + +async def create_async_resource() -> typing.AsyncIterator[datetime.datetime]: + yield datetime.datetime.now(tz=datetime.timezone.utc) + + +@dataclasses.dataclass(kw_only=True, slots=True) +class DependentFactory: + async_resource: datetime.datetime + + +class DIContainer(BaseContainer): + async_resource = providers.Resource(create_async_resource) + dependent_factory = providers.Factory(DependentFactory, async_resource=async_resource.cast) @contextlib.asynccontextmanager @@ -71,7 +86,7 @@ async def lifespan_manager() -> typing.AsyncIterator[None]: try: yield finally: - await container.DIContainer.tear_down() + await DIContainer.tear_down() broker = RabbitBroker() @@ -82,8 +97,8 @@ app = FastStream(broker, lifespan=lifespan_manager) async def read_root( logger: Logger, some_dependency: typing.Annotated[ - container.DependentFactory, - Depends(container.DIContainer.dependent_factory) + DependentFactory, + Depends(DIContainer.dependent_factory) ], ) -> datetime.datetime: startup_time = some_dependency.async_resource diff --git a/docs/integrations/litestar.md b/docs/integrations/litestar.md index b0aab322..c7ba682e 100644 --- a/docs/integrations/litestar.md +++ b/docs/integrations/litestar.md @@ -6,15 +6,23 @@ sibling DI framework. ```python -import typing -import fastapi import contextlib +import typing + 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 +from that_depends import BaseContainer, providers + + +async def create_async_resource() -> typing.AsyncIterator[str]: + yield "async resource" + + +class DIContainer(BaseContainer): + async_resource = providers.Resource(create_async_resource) @get("/") @@ -23,16 +31,16 @@ async def index(injected: str) -> str: @contextlib.asynccontextmanager -async def lifespan_manager(_: fastapi.FastAPI) -> typing.AsyncIterator[None]: +async def lifespan_manager(_: Litestar) -> typing.AsyncIterator[None]: try: yield finally: - await container.DIContainer.tear_down() + await DIContainer.tear_down() app = Litestar( route_handlers=[index], - dependencies={"injected": Provide(container.DIContainer.async_resource)}, + dependencies={"injected": Provide(DIContainer.async_resource)}, lifespan=[lifespan_manager], ) diff --git a/docs/introduction/generator-injection.md b/docs/introduction/generator-injection.md index 8d7afbbc..375654dc 100644 --- a/docs/introduction/generator-injection.md +++ b/docs/introduction/generator-injection.md @@ -109,7 +109,7 @@ def injected(val: float = Provide[Container.dependent_provider]) -> typing.Gener yield val # This will raise a `ContextProviderError`! -next(_injected()) +next(injected()) ``` 1. Matches context scope of `sync_provider` provider, which is a dependency of the `dependent_provider` provider. @@ -136,7 +136,7 @@ def injected(val: float = Provide[Container.dependent_provider]) -> typing.Gener with container_context(scope=ContextScopes.REQUEST): # This will resolve as expected - next(_injected()) + next(injected()) ``` Since no context initialization was needed, the generator will work as expected. diff --git a/docs/introduction/injection.md b/docs/introduction/injection.md index ae67e83f..a9d21ac1 100644 --- a/docs/introduction/injection.md +++ b/docs/introduction/injection.md @@ -84,7 +84,7 @@ def greet_user_direct( return f"Greeting: {greeting}" ``` -1. Notice that although `greeting` is a `str`, `mypy` and you IDE will not complain. +1. Notice that although `greeting` is a `str`, `mypy` and your IDE will not complain. --- @@ -123,7 +123,7 @@ For more details regarding scopes and context management, see the [Context Resou ## 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()`: +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_sync()` method or the provider’s own `override_context_sync()`: ```python def test_greet_override(): @@ -135,7 +135,7 @@ def test_greet_override(): 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](../testing/provider-overriding.md) documentation. +For more details on overriding providers, see the [Overriding Providers](../testing/provider-overriding.md) documentation. --- diff --git a/docs/introduction/ioc-container.md b/docs/introduction/ioc-container.md index edb63a19..07dac8ed 100644 --- a/docs/introduction/ioc-container.md +++ b/docs/introduction/ioc-container.md @@ -15,6 +15,7 @@ from that_depends import BaseContainer class Container(BaseContainer): # define your providers here + ... ``` Then you can build your dependency graph within the container: @@ -27,9 +28,9 @@ class Container(BaseContainer): session = providers.Factory(create_db_session, config=config.db) # (1)! user_repository = providers.Factory( - UserRepository, + UserRepository, + config.users, session=session.cast, # (3)! - config.users ) # (2)! diff --git a/docs/introduction/multiple-containers.md b/docs/introduction/multiple-containers.md index 467114aa..aabd5af9 100644 --- a/docs/introduction/multiple-containers.md +++ b/docs/introduction/multiple-containers.md @@ -2,13 +2,23 @@ You can use providers from other containers as following: ```python -from tests import container +import datetime +import typing + from that_depends import BaseContainer, providers +def create_sync_resource() -> typing.Iterator[datetime.datetime]: + yield datetime.datetime.now(tz=datetime.timezone.utc) + + +async def create_async_resource() -> typing.AsyncIterator[datetime.datetime]: + yield datetime.datetime.now(tz=datetime.timezone.utc) + + class InnerContainer(BaseContainer): - sync_resource = providers.Resource(container.create_sync_resource) - async_resource = providers.Resource(container.create_async_resource) + sync_resource = providers.Resource(create_sync_resource) + async_resource = providers.Resource(create_async_resource) class OuterContainer(BaseContainer): diff --git a/docs/introduction/scopes.md b/docs/introduction/scopes.md index ba912470..5783a2e5 100644 --- a/docs/introduction/scopes.md +++ b/docs/introduction/scopes.md @@ -8,6 +8,8 @@ Before continuing, make sure you're familiar with `ContextResource` providers by ## Quick Start By default, `ContextResources` have the named scope `ANY`, meaning they will be re-initialized each time you enter a named scope. +A container that defines a `ContextResource` without an explicit scope must assign `default_scope` before it, otherwise defining the container raises `DefaultScopeNotDefinedError`. +Set `default_scope = ContextScopes.ANY` to keep the `ANY` behavior. You can change the scope of a `ContextResource` in two ways: ### Setting the scope for providers @@ -16,13 +18,13 @@ You can change the scope of a `ContextResource` in two ways: ~~~~python hl_lines="2" class MyContainer(BaseContainer): - default_scope = ContextScope.APP + default_scope = ContextScopes.APP p = providers.ContextResource(my_resource) ~~~~ 2. 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) + p = providers.ContextResource(my_resource).with_config(scope=ContextScopes.APP) ~~~~ ### Entering and exiting scopes @@ -67,7 +69,7 @@ async with p.context_async(): 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 + # will raise an InvalidContextError since you are entering `REQUEST` scope ... ``` @@ -78,7 +80,7 @@ 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): + async with container_context(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 ``` @@ -114,7 +116,7 @@ class Container(BaseContainer): @Container.context(force=True) @inject async def injected(val = Provide[Container.p]): - return p + return val await injected() # will resolve ``` @@ -188,7 +190,7 @@ injected() 2. Context for `Container.provider` is initialized and will exit when the function returns. 3. 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: +This implementation might seem complex at first glance, but it provides 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. diff --git a/docs/providers/context-resources.md b/docs/providers/context-resources.md index 87e00c97..497bc025 100644 --- a/docs/providers/context-resources.md +++ b/docs/providers/context-resources.md @@ -20,7 +20,7 @@ You must initialize a context before you can resolve a `ContextResource`. ```python import typing -from that_depends import BaseContainer, providers, inject, Provide +from that_depends import BaseContainer, ContextScopes, providers, inject, Provide async def my_async_resource() -> typing.AsyncIterator[str]: @@ -38,6 +38,7 @@ def my_sync_resource() -> typing.Iterator[str]: print("Teardown of sync resource") class MyContainer(BaseContainer): + default_scope = ContextScopes.ANY async_resource = providers.ContextResource(my_async_resource) sync_resource = providers.ContextResource(my_sync_resource) ``` @@ -85,8 +86,8 @@ The values stored in the `global_context` can be resolved as long as: 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 + async with container_context(MyContainer, preserve_global_context=False): # this will reset all contexts, including the global context. + fetch_context_item("key") # returns None, the key is not found ``` If you want to maintain the global context, you can initialize a new context with the `preserve_global_context` argument: @@ -167,7 +168,7 @@ async def my_func(): # trying to resolve async dependency await MyContainer.async_resource.resolve() -> RuntimeError: AsyncResource cannot be resolved in a sync context. +> RuntimeError: Context is not set. Use container_context ``` ### More granular context initialization @@ -247,9 +248,6 @@ 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. +> `DIContextMiddleware` also supports the `global_context` and `scope` arguments. diff --git a/docs/providers/factories.md b/docs/providers/factories.md index e4ade978..f3904b3b 100644 --- a/docs/providers/factories.md +++ b/docs/providers/factories.md @@ -88,7 +88,7 @@ The `.provider` property gives you an *async function* to await, and `.provider_ ```python # In a synchronous function or interactive session >>> msg = MyContainer.sync_message.provider_sync ->>> print(msg) +>>> print(msg()) Hello from sync provider! ``` @@ -101,7 +101,7 @@ import asyncio async def main(): # Acquire the async resource by awaiting the provider property - msg = await MyContainer.async_message.provider + msg = await MyContainer.async_message.provider() print(msg) asyncio.run(main()) @@ -124,7 +124,7 @@ class AnotherClass: return self._factory_callable() -# Passing MyContainer.sync_message.sync_provider to AnotherClass +# Passing MyContainer.sync_message.provider_sync to AnotherClass provider_callable = MyContainer.sync_message.provider_sync another_instance = AnotherClass(provider_callable) print(another_instance.get_message()) # "Hello from sync provider!" @@ -162,19 +162,25 @@ Under the hood, `greeting` calls `greet` with the result of `name.resolve_sync() 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 +import typing + +from that_depends import BaseContainer, ContextScopes, container_context +from that_depends.providers import ContextResource, Factory + + +def create_session() -> typing.Iterator[str]: + yield "session" class ContextfulContainer(BaseContainer): default_scope = ContextScopes.REQUEST - # ... define context-based providers ... + session = ContextResource(create_session) + greeting = Factory(lambda session: f"Hello from {session}", session.cast) with container_context(ContextfulContainer, scope=ContextScopes.REQUEST): - result = ContextfulContainer.some_resource.provider_sync() - # ... + result = ContextfulContainer.greeting.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. +Call `.provider_sync` or `.provider` on the factory inside the context, so the resources it depends on are valid within the required scope. +These properties exist only on `Factory` and `AsyncFactory`; resolve a `ContextResource` with `.resolve_sync()` or `.resolve()`. diff --git a/docs/providers/object.md b/docs/providers/object.md index 72dbcb14..14af879d 100644 --- a/docs/providers/object.md +++ b/docs/providers/object.md @@ -10,5 +10,5 @@ class DIContainer(BaseContainer): object_provider = providers.Object(1) -assert DIContainer.object_provider() == 1 +assert DIContainer.object_provider.resolve_sync() == 1 ``` diff --git a/docs/providers/resources.md b/docs/providers/resources.md index 59ac623c..04166ebf 100644 --- a/docs/providers/resources.md +++ b/docs/providers/resources.md @@ -1,11 +1,10 @@ # 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`. +A `Resource` resolves once, caches the instance, and runs teardown logic from a generator or context manager. +A plain `Singleton` has no teardown step. +The creator can be a generator or async generator function, with teardown after the `yield` in `finally`, +or a class that implements `typing.ContextManager` or `typing.AsyncContextManager`. +A `Resource` does not automatically integrate with `container_context`. This makes `Resource` ideal for dependencies that need: diff --git a/docs/testing/provider-overriding.md b/docs/testing/provider-overriding.md index 6d5eeb22..30b41ae3 100644 --- a/docs/testing/provider-overriding.md +++ b/docs/testing/provider-overriding.md @@ -1,10 +1,6 @@ # 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. +Any provider in a container can be overridden, for example with a stub in tests. **Override affects all providers that use the overridden provider (_see example_)**. ## Example