From e692c3f3fb9f656d2fd44d48acf5e29310d85501 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Wed, 8 Jul 2026 09:11:45 +0300 Subject: [PATCH 1/5] feat: Factory accepts the creator as first positional argument (API-5) Co-Authored-By: Claude Opus 4.8 (1M context) --- modern_di/providers/factory.py | 2 +- .../2026-07-08.01-positional-subject-args.md | 79 +++++++++++++++++++ tests/providers/test_factory.py | 14 ++++ 3 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 planning/changes/2026-07-08.01-positional-subject-args.md diff --git a/modern_di/providers/factory.py b/modern_di/providers/factory.py index 6cd479b..ac5876f 100644 --- a/modern_di/providers/factory.py +++ b/modern_di/providers/factory.py @@ -33,9 +33,9 @@ class Factory(AbstractProvider[types.T_co]): def __init__( # noqa: C901, PLR0913 self, + creator: typing.Callable[..., types.T_co], *, scope: enum.IntEnum = Scope.APP, - creator: typing.Callable[..., types.T_co], bound_type: type | None | types.UnsetType = types.UNSET, kwargs: dict[str, typing.Any] | None = None, cache: bool | CacheSettings[types.T_co] | None = None, diff --git a/planning/changes/2026-07-08.01-positional-subject-args.md b/planning/changes/2026-07-08.01-positional-subject-args.md new file mode 100644 index 0000000..e1070f3 --- /dev/null +++ b/planning/changes/2026-07-08.01-positional-subject-args.md @@ -0,0 +1,79 @@ +--- +summary: API-5 — Factory, ContextProvider, and Alias accept their subject (creator / context_type / source_type) as the first positional-or-keyword argument; docs teach the positional spelling throughout. +--- + +# Design: Positional subject arguments for Factory, ContextProvider, Alias + +## Summary + +The accepted API-5 item from the 2026-07-05 3.0 UX research, extended by +ruling to all three registerable providers. Each provider's "subject" +parameter — `Factory(creator=...)`, `ContextProvider(context_type=...)`, +`Alias(source_type=...)` — moves ahead of the `*` in its `__init__`, +becoming positional-or-keyword. `Factory(UserService)` is the new shortest +registration; every existing keyword call site keeps working unchanged. +Docs and README switch to the positional spelling as the taught form. + +## Motivation + +Research item API-5 (verified): modern-di is the only framework in the +13-framework study whose registration call cannot lead with the thing being +registered — dishka `provide(Service)`, dependency-injector +`Factory(Service, ...)`, Fx `fx.Provide(ctor)`, svcs +`register_factory(T, fn)` all read subject-first. The keyword-only shape +also makes migration tables read worse than they are: +`Factory(UserService)` vs `providers.Factory(creator=UserService)` for the +identical concept. + +## Design + +Signature edits only — the subject parameter moves before the `*`, +everything after it stays keyword-only. No defaults added, no deprecation +of the keyword spelling, no warnings, no `/` (positional-only would break +every existing `creator=` call — ruled out in the research amendment). + +```python +class Factory(AbstractProvider[types.T_co]): + def __init__(self, creator: typing.Callable[..., types.T_co], *, scope=Scope.APP, ...) -> None: ... + +class ContextProvider(AbstractProvider[types.T_co]): + def __init__(self, context_type: type[types.T_co], *, scope=Scope.APP, ...) -> None: ... + +class Alias(AbstractProvider[types.T_co]): + def __init__(self, source_type: type[types.T_co], *, scope=UNSET, bound_type=UNSET) -> None: ... +``` + +Passing the subject both positionally and by keyword raises Python's +native `TypeError` ("multiple values for argument") — no custom guard. +`__repr__` output is unchanged. + +Docs sweep: every sample in `docs/` and `README.md` switches to the +positional form (~114 `creator=`, ~14 `context_type=`, ~10 `source_type=` +occurrences); the keyword form remains documented once where the API +reference states each signature. The from-dependency-injector and +from-that-depends migration tables get the direct subject-first mapping. + +## Non-goals + +- No deprecation or removal of the keyword spellings — both stay first-class. +- No positional promotion for any other parameter (`scope`, `bound_type`, + `kwargs`, ...) — subject-first only. +- No test-suite sweep: existing keyword call sites stay as-is; they are + living proof the keyword form keeps working. +- No change to `container_provider` (it takes no subject argument). + +## Testing + +TDD — failing tests first: each provider constructed positionally resolves +correctly through a container; positional + keyword duplicate raises +`TypeError`. `just test-ci` green (100% line coverage gate), `just lint-ci` +green (`ruff`, `ty`, planning validation), `just docs-build` strict-green +after the docs sweep. + +## Risk + +- Sibling integrations or user code subclassing a provider and calling + `super().__init__(creator=...)` — unaffected: keyword spelling keeps + working; positional-or-keyword is a strict widening. Low. +- Docs sweep churn introducing a broken sample — mitigated by the strict + docs build and reviewer pass over the diff. Low. diff --git a/tests/providers/test_factory.py b/tests/providers/test_factory.py index d931233..5275fa9 100644 --- a/tests/providers/test_factory.py +++ b/tests/providers/test_factory.py @@ -714,3 +714,17 @@ def test_cache_and_cache_settings_together_raise() -> None: def test_repr_reports_cached_for_cache_true() -> None: provider = providers.Factory(creator=SimpleCreator, kwargs={"dep1": "x"}, cache=True) assert "cached=True" in repr(provider) + + +def test_factory_accepts_positional_creator() -> None: + class G(Group): + factory = providers.Factory(SimpleCreator, kwargs={"dep1": "positional"}) + + container = Container(groups=[G], validate=True) + instance = container.resolve(SimpleCreator) + assert instance.dep1 == "positional" + + +def test_factory_rejects_creator_passed_twice() -> None: + with pytest.raises(TypeError, match="creator"): + providers.Factory(SimpleCreator, creator=SimpleCreator) # ty: ignore[parameter-already-assigned] From d2114af5ccfb63ee1b9ae75b0e9d86b8dc5aa14a Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Wed, 8 Jul 2026 09:16:26 +0300 Subject: [PATCH 2/5] feat: ContextProvider accepts the context type as first positional argument (API-5) Co-Authored-By: Claude Opus 4.8 (1M context) --- modern_di/providers/context_provider.py | 2 +- tests/providers/test_context_provider.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/modern_di/providers/context_provider.py b/modern_di/providers/context_provider.py index 2329c49..b66cd23 100644 --- a/modern_di/providers/context_provider.py +++ b/modern_di/providers/context_provider.py @@ -25,9 +25,9 @@ class ContextProvider(AbstractProvider[types.T_co]): def __init__( self, + context_type: type[types.T_co], *, scope: enum.IntEnum = Scope.APP, - context_type: type[types.T_co], bound_type: type | None | types.UnsetType = types.UNSET, ) -> None: super().__init__( diff --git a/tests/providers/test_context_provider.py b/tests/providers/test_context_provider.py index b30e4eb..8d50c7c 100644 --- a/tests/providers/test_context_provider.py +++ b/tests/providers/test_context_provider.py @@ -303,3 +303,15 @@ def test_set_context_provider_direct_resolve_does_not_warn() -> None: with warnings.catch_warnings(): warnings.simplefilter("error") assert app_container.resolve_provider(MyGroup.context_provider) is now + + +def test_context_provider_accepts_positional_context_type() -> None: + provider = providers.ContextProvider(datetime.datetime) + now = datetime.datetime.now(tz=datetime.timezone.utc) + app_container = Container(context={datetime.datetime: now}) + assert app_container.resolve_provider(provider) is now + + +def test_context_provider_rejects_context_type_passed_twice() -> None: + with pytest.raises(TypeError, match="context_type"): + providers.ContextProvider(datetime.datetime, context_type=datetime.datetime) # ty: ignore[parameter-already-assigned] From 774d841b4b2f10204b2a9d4f66ccd4951a2ca141 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Wed, 8 Jul 2026 09:20:34 +0300 Subject: [PATCH 3/5] feat: Alias accepts the source type as first positional argument (API-5) Co-Authored-By: Claude Opus 4.8 (1M context) --- modern_di/providers/alias.py | 2 +- tests/providers/test_alias.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/modern_di/providers/alias.py b/modern_di/providers/alias.py index f1dea31..bf99a7c 100644 --- a/modern_di/providers/alias.py +++ b/modern_di/providers/alias.py @@ -16,8 +16,8 @@ class Alias(AbstractProvider[types.T_co]): def __init__( self, - *, source_type: type[types.T_co], + *, scope: enum.IntEnum | types.UnsetType = types.UNSET, bound_type: type | None | types.UnsetType = types.UNSET, ) -> None: diff --git a/tests/providers/test_alias.py b/tests/providers/test_alias.py index 8691d2d..70e9459 100644 --- a/tests/providers/test_alias.py +++ b/tests/providers/test_alias.py @@ -363,3 +363,17 @@ def test_alias_without_scope_emits_no_deprecation_warning() -> None: warnings.simplefilter("error") alias = providers.Alias(source_type=_DepSrc, bound_type=object) assert alias is not None + + +def test_alias_accepts_positional_source_type() -> None: + class G(Group): + repo = providers.Factory(creator=PostgresRepository, cache=True) + abstract = providers.Alias(PostgresRepository, bound_type=AbstractRepository) + + container = Container(groups=[G], validate=True) + assert isinstance(container.resolve(AbstractRepository), PostgresRepository) + + +def test_alias_rejects_source_type_passed_twice() -> None: + with pytest.raises(TypeError, match="source_type"): + providers.Alias(PostgresRepository, source_type=PostgresRepository) # ty: ignore[parameter-already-assigned] From 8957a03e3f12ba231ecaeae0dfd7cfa33745b25c Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Wed, 8 Jul 2026 09:35:37 +0300 Subject: [PATCH 4/5] docs: teach the positional subject spelling throughout (API-5) Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 4 +-- docs/index.md | 10 +++--- docs/integrations/aiohttp.md | 2 +- docs/integrations/fastapi.md | 6 ++-- docs/integrations/faststream.md | 8 ++--- docs/integrations/litestar.md | 10 +++--- docs/integrations/starlette.md | 6 ++-- docs/integrations/typer.md | 6 ++-- docs/integrations/writing-integrations.md | 4 +-- docs/introduction/comparison.md | 8 ++--- docs/introduction/for-fastapi-users.md | 10 +++--- docs/introduction/resolving.md | 4 +-- docs/migration/from-dependency-injector.md | 34 +++++++++---------- docs/migration/from-that-depends.md | 30 ++++++++-------- docs/migration/to-2.x.md | 12 ++++--- docs/migration/to-3.x.md | 8 ++--- docs/providers/alias.md | 7 ++-- docs/providers/container.md | 4 +-- docs/providers/context.md | 9 +++-- docs/providers/factories.md | 17 ++++++---- docs/providers/lifecycle.md | 2 +- docs/providers/scopes.md | 2 +- docs/recipes/async-lifespan.md | 6 ++-- docs/recipes/good-and-bad-practices.md | 16 ++++----- docs/recipes/multi-group.md | 14 ++++---- docs/recipes/request-scoped-engine.md | 8 ++--- docs/recipes/sqlalchemy.md | 6 ++-- .../alias-source-not-registered-error.md | 6 ++-- .../argument-resolution-error.md | 6 ++-- docs/troubleshooting/context-not-set.md | 6 ++-- docs/troubleshooting/creator-call-error.md | 4 +-- docs/troubleshooting/duplicate-type-error.md | 8 ++--- .../group-instantiation-error.md | 2 +- docs/troubleshooting/scope-chain.md | 6 ++-- .../unknown-factory-kwarg-error.md | 4 +-- .../unsupported-creator-parameter-error.md | 4 +-- 36 files changed, 155 insertions(+), 144 deletions(-) diff --git a/README.md b/README.md index 2ed5da3..1dee8fc 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,8 @@ class UserRepository: class Dependencies(Group): - settings = providers.Factory(scope=Scope.APP, creator=Settings) - user_repository = providers.Factory(scope=Scope.REQUEST, creator=UserRepository) + settings = providers.Factory(Settings, scope=Scope.APP) + user_repository = providers.Factory(UserRepository, scope=Scope.REQUEST) with Container(groups=[Dependencies], validate=True) as container: diff --git a/docs/index.md b/docs/index.md index 5491ad5..65a87d1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -66,7 +66,7 @@ class Settings: class Dependencies(Group): - settings = providers.Factory(creator=Settings) + settings = providers.Factory(Settings) # Pass validate=True to detect cycles and scope-chain errors at startup @@ -100,7 +100,7 @@ def close_settings(settings: Settings) -> None: class Dependencies(Group): settings = providers.Factory( - creator=Settings, + Settings, cache=providers.CacheSettings(finalizer=close_settings), ) @@ -150,11 +150,11 @@ class UserRepository: class Dependencies(Group): settings = providers.Factory( - creator=Settings, + Settings, cache=providers.CacheSettings(finalizer=close_settings), ) - request_id = providers.ContextProvider(scope=Scope.REQUEST, context_type=RequestId) - user_repository = providers.Factory(scope=Scope.REQUEST, creator=UserRepository) + request_id = providers.ContextProvider(RequestId, scope=Scope.REQUEST) + user_repository = providers.Factory(UserRepository, scope=Scope.REQUEST) with Container(groups=[Dependencies], validate=True) as container: diff --git a/docs/integrations/aiohttp.md b/docs/integrations/aiohttp.md index 8137095..24f1c95 100644 --- a/docs/integrations/aiohttp.md +++ b/docs/integrations/aiohttp.md @@ -38,7 +38,7 @@ class Settings: class AppGroup(Group): - settings = providers.Factory(scope=Scope.APP, creator=Settings) + settings = providers.Factory(Settings, scope=Scope.APP) @inject diff --git a/docs/integrations/fastapi.md b/docs/integrations/fastapi.md index 37e65b3..1688555 100644 --- a/docs/integrations/fastapi.md +++ b/docs/integrations/fastapi.md @@ -44,8 +44,8 @@ def create_singleton() -> datetime.datetime: class AppGroup(Group): singleton = providers.Factory( + create_singleton, scope=Scope.APP, - creator=create_singleton, cache=True ) @@ -130,8 +130,8 @@ def create_request_info(request: fastapi.Request) -> dict[str, str]: class AppGroup(Group): # Factory automatically resolves the request dependency based on type annotation request_info = providers.Factory( + create_request_info, scope=Scope.REQUEST, - creator=create_request_info, ) ``` @@ -154,8 +154,8 @@ def create_request_info(request: fastapi.Request) -> dict[str, str]: class AppGroup(Group): # Factory explicitly uses the request provider from the integration request_info = providers.Factory( + create_request_info, scope=Scope.REQUEST, - creator=create_request_info, kwargs={"request": modern_di_fastapi.fastapi_request_provider} ) ``` diff --git a/docs/integrations/faststream.md b/docs/integrations/faststream.md index 9e8cf13..3968584 100644 --- a/docs/integrations/faststream.md +++ b/docs/integrations/faststream.md @@ -49,13 +49,13 @@ class OrderProcessor: class AppGroup(Group): settings = providers.Factory( + Settings, scope=Scope.APP, - creator=Settings, cache=True, ) order_processor = providers.Factory( + OrderProcessor, scope=Scope.REQUEST, - creator=OrderProcessor, ) @@ -107,8 +107,8 @@ def create_message_info(message: faststream.StreamMessage) -> dict[str, str]: class AppGroup(Group): # The message dependency is resolved by type annotation message_info = providers.Factory( + create_message_info, scope=Scope.REQUEST, - creator=create_message_info, ) ``` @@ -126,8 +126,8 @@ def create_message_info(message: faststream.StreamMessage) -> dict[str, str]: class AppGroup(Group): message_info = providers.Factory( + create_message_info, scope=Scope.REQUEST, - creator=create_message_info, kwargs={"message": modern_di_faststream.faststream_message_provider}, ) ``` diff --git a/docs/integrations/litestar.md b/docs/integrations/litestar.md index 5bc4d20..15a2e98 100644 --- a/docs/integrations/litestar.md +++ b/docs/integrations/litestar.md @@ -39,8 +39,8 @@ def create_singleton() -> datetime.datetime: class AppGroup(Group): singleton = providers.Factory( + create_singleton, scope=Scope.APP, - creator=create_singleton, cache=True ) @@ -77,7 +77,7 @@ class UserRepository: class AppGroup(Group): - user_repo = providers.Factory(scope=Scope.REQUEST, creator=UserRepository) + user_repo = providers.Factory(UserRepository, scope=Scope.REQUEST) ALL_GROUPS = [AppGroup] @@ -114,7 +114,7 @@ class MyService: class Dependencies(Group): - my_service = providers.Factory(scope=Scope.REQUEST, creator=MyService) + my_service = providers.Factory(MyService, scope=Scope.REQUEST) ALL_GROUPS = [Dependencies] @@ -168,8 +168,8 @@ def create_request_info(request: litestar.Request) -> dict[str, str]: class AppGroup(Group): # Factory automatically resolves the request dependency based on type annotation request_info = providers.Factory( + create_request_info, scope=Scope.REQUEST, - creator=create_request_info, ) ``` @@ -192,8 +192,8 @@ def create_request_info(request: litestar.Request) -> dict[str, str]: class AppGroup(Group): # Factory explicitly uses the request provider from the integration request_info = providers.Factory( + create_request_info, scope=Scope.REQUEST, - creator=create_request_info, kwargs={"request": modern_di_litestar.litestar_request_provider} ) ``` diff --git a/docs/integrations/starlette.md b/docs/integrations/starlette.md index 0f64e96..0d40098 100644 --- a/docs/integrations/starlette.md +++ b/docs/integrations/starlette.md @@ -41,7 +41,7 @@ class Settings: class AppGroup(Group): - settings = providers.Factory(scope=Scope.APP, creator=Settings) + settings = providers.Factory(Settings, scope=Scope.APP) @inject @@ -112,7 +112,7 @@ def create_request_info(request: Request) -> dict[str, str]: class AppGroup(Group): - request_info = providers.Factory(scope=Scope.REQUEST, creator=create_request_info) + request_info = providers.Factory(create_request_info, scope=Scope.REQUEST) ``` ### Explicit Usage (Provider-based Resolution) @@ -129,8 +129,8 @@ def create_request_info(request: Request) -> dict[str, str]: class AppGroup(Group): request_info = providers.Factory( + create_request_info, scope=Scope.REQUEST, - creator=create_request_info, kwargs={"request": modern_di_starlette.starlette_request_provider}, ) ``` diff --git a/docs/integrations/typer.md b/docs/integrations/typer.md index 8b97194..f8be6aa 100644 --- a/docs/integrations/typer.md +++ b/docs/integrations/typer.md @@ -48,13 +48,13 @@ class HealthReporter: class AppGroup(Group): settings = providers.Factory( + Settings, scope=Scope.APP, - creator=Settings, cache=True, ) health_reporter = providers.Factory( + HealthReporter, scope=Scope.REQUEST, - creator=HealthReporter, ) @@ -101,7 +101,7 @@ class Job: class AppGroup(Group): - job = providers.Factory(scope=Scope.ACTION, creator=Job, bound_type=None) + job = providers.Factory(Job, scope=Scope.ACTION, bound_type=None) @app.command() diff --git a/docs/integrations/writing-integrations.md b/docs/integrations/writing-integrations.md index d900e8a..4daeb6e 100644 --- a/docs/integrations/writing-integrations.md +++ b/docs/integrations/writing-integrations.md @@ -34,8 +34,8 @@ dispatches off them. ```python from modern_di import Scope, providers -myfw_request_provider = providers.ContextProvider(scope=Scope.REQUEST, context_type=myfw.Request) -myfw_websocket_provider = providers.ContextProvider(scope=Scope.SESSION, context_type=myfw.WebSocket) +myfw_request_provider = providers.ContextProvider(myfw.Request, scope=Scope.REQUEST) +myfw_websocket_provider = providers.ContextProvider(myfw.WebSocket, scope=Scope.SESSION) _CONNECTION_PROVIDERS = (myfw_request_provider, myfw_websocket_provider) ``` diff --git a/docs/introduction/comparison.md b/docs/introduction/comparison.md index f75c99a..f8790d6 100644 --- a/docs/introduction/comparison.md +++ b/docs/introduction/comparison.md @@ -132,11 +132,11 @@ lifetime dialect, so here is how the same six concepts translate: | Concept | dependency-injector | dishka | wireup | svcs | FastAPI `Depends` | modern-di | |---|---|---|---|---|---|---| -| Singleton (create once, share) | `providers.Singleton(...)` | `provide(Impl, scope=Scope.APP)` — cached by default within its scope | `@injectable` — default `lifetime="singleton"` | `registry.register_value(Type, value)` at startup | a dependency wrapped in `@lru_cache` | [`Factory(scope=Scope.APP, cache=True)`](../providers/factories.md#cached-factories) | +| Singleton (create once, share) | `providers.Singleton(...)` | `provide(Impl, scope=Scope.APP)` — cached by default within its scope | `@injectable` — default `lifetime="singleton"` | `registry.register_value(Type, value)` at startup | a dependency wrapped in `@lru_cache` | [`Factory(..., scope=Scope.APP, cache=True)`](../providers/factories.md#cached-factories) | | Transient (fresh instance every time) | `providers.Factory(...)` | `provide(Impl, cache=False)` | `@injectable(lifetime="transient")` | no dedicated provider — call the plain factory directly | `Depends(fn, use_cache=False)` | a plain [`Factory(...)`](../providers/factories.md) with no `cache` | -| Request-scoped | `providers.Resource` + the `Closing` wiring marker | `provide(Impl, scope=Scope.REQUEST)` | `@injectable(lifetime="scoped")` | one instance per `svcs.Container` (built per request) | bare `Depends(fn)` — computed once per request by default | [`Factory(scope=Scope.REQUEST, cache=True)`](../providers/scopes.md) | -| Runtime value (request object, etc.) | `providers.Configuration` / `.from_value()` | `from_context(provides=Type, scope=...)` declared, then `context={Type: value}` at scope entry | a typed constructor parameter resolved from the active scope's context | `registry.register_value(Type, value)`, or a per-container local factory | the framework injects `Request`/`WebSocket` directly by type | [`ContextProvider(context_type=...)`](../providers/context.md) + `context={...}` | -| Interface binding (concrete → abstract type) | `providers.AbstractFactory` — must be overridden with a concrete `Factory` before use | `alias(source=Impl, provides=Interface)` | `@injectable(as_type=Interface)` | `register_factory(Interface, factory)` — svcs keys by whatever type you register under | n/a — `Depends` is callable-keyed, not type-keyed | [`Alias(source_type=Impl, bound_type=Interface)`](../providers/alias.md) | +| Request-scoped | `providers.Resource` + the `Closing` wiring marker | `provide(Impl, scope=Scope.REQUEST)` | `@injectable(lifetime="scoped")` | one instance per `svcs.Container` (built per request) | bare `Depends(fn)` — computed once per request by default | [`Factory(..., scope=Scope.REQUEST, cache=True)`](../providers/scopes.md) | +| Runtime value (request object, etc.) | `providers.Configuration` / `.from_value()` | `from_context(provides=Type, scope=...)` declared, then `context={Type: value}` at scope entry | a typed constructor parameter resolved from the active scope's context | `registry.register_value(Type, value)`, or a per-container local factory | the framework injects `Request`/`WebSocket` directly by type | [`ContextProvider(...)`](../providers/context.md) + `context={...}` | +| Interface binding (concrete → abstract type) | `providers.AbstractFactory` — must be overridden with a concrete `Factory` before use | `alias(source=Impl, provides=Interface)` | `@injectable(as_type=Interface)` | `register_factory(Interface, factory)` — svcs keys by whatever type you register under | n/a — `Depends` is callable-keyed, not type-keyed | [`Alias(Impl, bound_type=Interface)`](../providers/alias.md) | | Test override | `provider.override(...)`, or `with provider.override(...):` | no dedicated API — build a separate container from mock providers | `with container.override.injectable(Target, new=fake):` | re-call `register_value()`/`register_factory()`; `container.close()` first if already cached | `app.dependency_overrides[dep] = fake` | [`container.override(provider, mock)`](../recipes/testing-overrides.md) | ## See also diff --git a/docs/introduction/for-fastapi-users.md b/docs/introduction/for-fastapi-users.md index c077e38..0951c3a 100644 --- a/docs/introduction/for-fastapi-users.md +++ b/docs/introduction/for-fastapi-users.md @@ -10,11 +10,11 @@ translates the `Depends` idioms you already know into their modern-di equivalent | FastAPI `Depends` | modern-di | Notes | |---|---|---| -| `Depends(fn)` | `Factory(creator=fn)` | Both auto-wire the callable's parameters; modern-di matches by type annotation instead of by the callable's own parameter defaults. | -| bare `Depends(fn)` (`use_cache=True`, the default) | `Factory(scope=Scope.REQUEST, creator=fn, cache=True)` | FastAPI memoizes a dependency for the rest of the *same request* once it's been called; the REQUEST-scoped cached `Factory` is the equivalent — one shared instance per request container. | -| `Depends(fn, use_cache=False)` | a bare `Factory(creator=fn)` — no `cache` | Without `cache`, a `Factory` builds a fresh instance on every resolve, matching `use_cache=False`. | +| `Depends(fn)` | `Factory(fn)` | Both auto-wire the callable's parameters; modern-di matches by type annotation instead of by the callable's own parameter defaults. | +| bare `Depends(fn)` (`use_cache=True`, the default) | `Factory(fn, scope=Scope.REQUEST, cache=True)` | FastAPI memoizes a dependency for the rest of the *same request* once it's been called; the REQUEST-scoped cached `Factory` is the equivalent — one shared instance per request container. | +| `Depends(fn, use_cache=False)` | a bare `Factory(fn)` — no `cache` | Without `cache`, a `Factory` builds a fresh instance on every resolve, matching `use_cache=False`. | | `yield`-based teardown (`def fn(): ...; yield x; ...cleanup...`) | `cache=CacheSettings(finalizer=cleanup_fn)` | modern-di has no generator-creator form (see [Design decisions](design-decisions.md)); teardown is a second, explicit object instead of code after `yield`. `finalizer` may be sync or async — see [Lifecycle](../providers/lifecycle.md). | -| `@lru_cache`-wrapped dependency (process-wide singleton) | `Factory(scope=Scope.APP, creator=fn, cache=True)`, optionally with a `finalizer` | `lru_cache` has no cleanup hook; the APP-scoped cached `Factory` adds one via `CacheSettings(finalizer=...)` if the singleton needs to release anything on shutdown. | +| `@lru_cache`-wrapped dependency (process-wide singleton) | `Factory(fn, scope=Scope.APP, cache=True)`, optionally with a `finalizer` | `lru_cache` has no cleanup hook; the APP-scoped cached `Factory` adds one via `CacheSettings(finalizer=...)` if the singleton needs to release anything on shutdown. | | `app.dependency_overrides[fn] = fake` | `container.override(provider, fake)` | modern-di overrides are keyed by **provider reference**, not by callable, and apply across the whole container tree — see [Testing with overrides](../recipes/testing-overrides.md). Reset with `container.reset_override(provider)`. | ## Two meanings of "scope" @@ -53,8 +53,8 @@ def close_session(session: Session) -> None: class Dependencies(Group): session = providers.Factory( + create_session, scope=Scope.REQUEST, - creator=create_session, cache=providers.CacheSettings(finalizer=close_session), ) ``` diff --git a/docs/introduction/resolving.md b/docs/introduction/resolving.md index da84d8c..c6970bf 100644 --- a/docs/introduction/resolving.md +++ b/docs/introduction/resolving.md @@ -30,11 +30,11 @@ class DatabaseConnection: class Dependencies(Group): db_config = providers.Factory( + DatabaseConfig, scope=Scope.APP, - creator=DatabaseConfig, kwargs={"host": "localhost", "port": 5432}, ) - db_connection = providers.Factory(scope=Scope.APP, creator=DatabaseConnection) + db_connection = providers.Factory(DatabaseConnection, scope=Scope.APP) container = Container(groups=[Dependencies], validate=True) diff --git a/docs/migration/from-dependency-injector.md b/docs/migration/from-dependency-injector.md index dcb5786..e79e1ac 100644 --- a/docs/migration/from-dependency-injector.md +++ b/docs/migration/from-dependency-injector.md @@ -61,20 +61,20 @@ Use this table as the index for the rest of the guide. Every provider class docu | `dependency-injector` | `modern-di` replacement | Where to look | |---|---|---| | `Factory` | `providers.Factory(...)` | [§4](#4-migrate-the-dependency-graph) | -| `Callable` | `providers.Factory(creator=the_callable)` — `Factory`'s creator can be any callable, not just a class | [§4](#4-migrate-the-dependency-graph) | +| `Callable` | `providers.Factory(the_callable)` — `Factory`'s creator can be any callable, not just a class | [§4](#4-migrate-the-dependency-graph) | | `Singleton` | `providers.Factory(..., cache=True)` | [§4](#4-migrate-the-dependency-graph) | | `ThreadSafeSingleton` | `providers.Factory(..., cache=True)` — `modern-di`'s cache is lock-guarded by default (`use_lock=True` on the container) | [§4](#4-migrate-the-dependency-graph) | | `ThreadLocalSingleton` | No direct equivalent — see [§11](#11-no-direct-equivalent) | [§11](#11-no-direct-equivalent) | | `Resource` (plain-function initializer — their docs' most common form; no shutdown step) | `providers.Factory(..., cache=True)` — same as `Singleton`; add a finalizer only when there is teardown | [§4](#4-migrate-the-dependency-graph) | | `Resource` (generator / context-manager initializer) | `providers.Factory(..., cache=CacheSettings(finalizer=...))` | [§4](#4-migrate-the-dependency-graph) | | `Resource` (async initializer) | Lifespan + `ContextProvider` (or sync creator + async finalizer) | [§4](#4-migrate-the-dependency-graph) | -| `ContextLocalResource` | `providers.Factory(scope=Scope.REQUEST, ..., cache=CacheSettings(finalizer=...))` resolved from a per-request child container | [§4](#4-migrate-the-dependency-graph) | +| `ContextLocalResource` | `providers.Factory(..., scope=Scope.REQUEST, cache=CacheSettings(finalizer=...))` resolved from a per-request child container | [§4](#4-migrate-the-dependency-graph) | | `Coroutine` | No direct equivalent — resolution is sync-only; do the `await` in the lifespan and inject the result, same as an async `Resource` | [§4](#4-migrate-the-dependency-graph) | | `Object` | `providers.Factory` with a creator that returns the value | [§4](#4-migrate-the-dependency-graph) | | `List` | `providers.Factory` with a creator that returns a list | [§4](#4-migrate-the-dependency-graph) | | `Dict` | `providers.Factory` with a creator that returns a dict | [§4](#4-migrate-the-dependency-graph) | -| `Dependency` | `providers.ContextProvider(context_type=...)` | [§4](#4-migrate-the-dependency-graph) | -| `AbstractFactory` | `providers.Alias(source_type=..., bound_type=...)` — pick the concrete implementation at declaration time instead of via `.override()` before first use | [§4](#4-migrate-the-dependency-graph) | +| `Dependency` | `providers.ContextProvider(...)` | [§4](#4-migrate-the-dependency-graph) | +| `AbstractFactory` | `providers.Alias(..., bound_type=...)` — pick the concrete implementation at declaration time instead of via `.override()` before first use | [§4](#4-migrate-the-dependency-graph) | | `Configuration` | A plain settings object registered as a provider — no config subsystem (`from_yaml`/`from_env`/etc.) | [§5](#5-configuration) | | `Selector` | No direct equivalent — see [§11](#11-no-direct-equivalent) | [§11](#11-no-direct-equivalent) | | `Aggregate` / `FactoryAggregate` | No direct equivalent — see [§11](#11-no-direct-equivalent) | [§11](#11-no-direct-equivalent) | @@ -93,7 +93,7 @@ Use this table as the index for the rest of the guide. Every provider class docu 2. Add an explicit `scope=` to each provider (defaults to `Scope.APP`). 3. Create the runtime container with `Container(groups=[MyGroup], validate=True)`. In `modern-di`, `Group` is a schema only — you cannot resolve from it directly, unlike a `DeclarativeContainer` instance. -**`Singleton` / `ThreadSafeSingleton`** → `providers.Factory(creator=SomeClass, cache=True)` — no separate thread-safe class, since `modern-di`'s cache is lock-guarded by default. See [Cached factories](../providers/factories.md#cached-factories). +**`Singleton` / `ThreadSafeSingleton`** → `providers.Factory(SomeClass, cache=True)` — no separate thread-safe class, since `modern-di`'s cache is lock-guarded by default. See [Cached factories](../providers/factories.md#cached-factories). **`Resource`** → cached `Factory`, with or without a `finalizer` depending on the initializer form. Their docs call the plain-function initializer "the most common way to specify resource initialization" — and a plain-function `Resource` has no shutdown step, so it maps to exactly what `Singleton` maps to: @@ -102,7 +102,7 @@ Use this table as the index for the rest of the guide. Every provider class docu thread_pool = providers.Resource(init_thread_pool, max_workers=4) # modern-di — same as the Singleton mapping -thread_pool = providers.Factory(creator=init_thread_pool, kwargs={"max_workers": 4}, cache=True) +thread_pool = providers.Factory(init_thread_pool, kwargs={"max_workers": 4}, cache=True) ``` For the generator or context-manager initializer forms (the ones with a shutdown step), split init and teardown into a plain creator function and a separate finalizer function: @@ -124,7 +124,7 @@ def close_resource(resource: SomeResource) -> None: ... # shutdown code thread_pool = providers.Factory( - creator=create_resource, + create_resource, cache=providers.CacheSettings(finalizer=close_resource), ) ``` @@ -137,8 +137,8 @@ db_session = providers.ContextLocalResource(AsyncSessionLocal) # modern-di — one instance per request container, finalizer on request end db_session = providers.Factory( + create_session, scope=Scope.REQUEST, - creator=create_session, cache=providers.CacheSettings(finalizer=close_session), ) ``` @@ -152,7 +152,7 @@ hashed = container.password_hasher("super secret") # "super secret" supplied at # modern-di — the value must be static (kwargs) or itself a resolvable dependency password_hasher = providers.Factory( - creator=passlib.hash.sha256_crypt.hash, + passlib.hash.sha256_crypt.hash, kwargs={"secret": "super secret", "salt_size": 16, "rounds": 10000}, ) ``` @@ -171,7 +171,7 @@ class ApiKey(str): ... def _api_key() -> ApiKey: return ApiKey("secret-token") -api_key = providers.Factory(creator=_api_key, cache=True) +api_key = providers.Factory(_api_key, cache=True) ``` If you only need the value passed into one downstream provider, skip the wrapper and put it directly in that provider's `kwargs`. @@ -189,7 +189,7 @@ modules = providers.List( def build_modules() -> list[Module]: return [Module("m1"), Module("m2")] -modules = providers.Factory(creator=build_modules) +modules = providers.Factory(build_modules) ``` **`Dependency`** → `ContextProvider`. Both are a typed placeholder filled in at runtime rather than constructed by a factory: @@ -200,7 +200,7 @@ database = providers.Dependency(instance_of=DbAdapter) # container = Container(database=providers.Singleton(SqliteDbAdapter)) # modern-di -database = providers.ContextProvider(scope=Scope.APP, context_type=DbAdapter) +database = providers.ContextProvider(DbAdapter, scope=Scope.APP) # container = Container(groups=[AppGroup], context={DbAdapter: SqliteDbAdapter()}) ``` @@ -212,8 +212,8 @@ cache_client_factory = providers.AbstractFactory(AbstractCacheClient) # container.cache_client_factory.override(providers.Factory(RedisCacheClient, host="localhost")) # modern-di -redis_cache_client = providers.Factory(creator=RedisCacheClient, cache=True) -cache_client = providers.Alias(source_type=RedisCacheClient, bound_type=AbstractCacheClient) +redis_cache_client = providers.Factory(RedisCacheClient, cache=True) +cache_client = providers.Alias(RedisCacheClient, bound_type=AbstractCacheClient) ``` ## 5. Configuration @@ -226,7 +226,7 @@ class Settings: self.database_url = os.environ["DATABASE_URL"] class AppGroup(Group): - settings = providers.Factory(creator=Settings, cache=True) + settings = providers.Factory(Settings, cache=True) ``` If a value needs to be supplied by the caller rather than computed (e.g. it comes from a CLI flag or a request header), use `ContextProvider` instead — see [§4](#4-migrate-the-dependency-graph)'s `Dependency` mapping. @@ -273,10 +273,10 @@ This also removes `dependency-injector`'s most-filed failure mode: an unwired fu ```python class AppGroup(Group): # one instance for the whole app's lifetime - db_pool = providers.Factory(scope=Scope.APP, creator=create_pool, cache=True) + db_pool = providers.Factory(create_pool, scope=Scope.APP, cache=True) # one instance per request; built by build_child_container(scope=Scope.REQUEST) - current_user = providers.Factory(scope=Scope.REQUEST, creator=UserFromRequest) + current_user = providers.Factory(UserFromRequest, scope=Scope.REQUEST) app_container = Container(scope=Scope.APP, groups=[AppGroup], validate=True) request_container = app_container.build_child_container(scope=Scope.REQUEST, context={...}) diff --git a/docs/migration/from-that-depends.md b/docs/migration/from-that-depends.md index 8d6a712..a098733 100644 --- a/docs/migration/from-that-depends.md +++ b/docs/migration/from-that-depends.md @@ -64,7 +64,7 @@ Use this table as the index for the rest of the guide. | `Singleton` | `providers.Factory(..., cache=True)` | [§4](#4-migrate-the-dependency-graph) | | `Resource` (sync gen / ctx mgr) | `providers.Factory(..., cache=CacheSettings(finalizer=...))` | [§4](#4-migrate-the-dependency-graph) | | `Resource` (async gen / ctx mgr) | Lifespan + `ContextProvider` (or sync creator + async finalizer) | [§6](#6-async-resources) | -| `ContextResource` | `providers.Factory(scope=Scope.REQUEST, ...)` | [§5](#5-context-resources-and-request-scope) | +| `ContextResource` | `providers.Factory(..., scope=Scope.REQUEST)` | [§5](#5-context-resources-and-request-scope) | | `AsyncFactory` | Lifespan-managed; expose via `ContextProvider` | [§6](#6-async-resources) | | `AsyncSingleton` | Lifespan-managed; expose via `ContextProvider` | [§6](#6-async-resources) | | `Object` | `providers.Factory` with a creator that returns the value | [§4](#4-migrate-the-dependency-graph) | @@ -74,12 +74,12 @@ Use this table as the index for the rest of the guide. | `AttrGetter` (`provider.attr`) | No direct equivalent — see [§9](#9-no-direct-equivalent) | | `ThreadLocalSingleton` | No direct equivalent — see [§9](#9-no-direct-equivalent) | | `State` | `ContextProvider` + `set_context` | [§5](#5-context-resources-and-request-scope) | -| `Provider.bind(Type)` | `providers.Alias(source_type=..., bound_type=...)` | [§4](#4-migrate-the-dependency-graph) | +| `Provider.bind(Type)` | `providers.Alias(..., bound_type=...)` | [§4](#4-migrate-the-dependency-graph) | | `@inject` + `Provide[T]()` (web) | `FromDI(T)` from the framework integration | [§8](#8-framework-integration-and-routes) | | `@inject` + `Provide[T]()` (non-web) | Explicit `container.resolve(T)` | [§9](#9-no-direct-equivalent) | | `container_context()` | `container.build_child_container(scope=..., context=...)` | [§5](#5-context-resources-and-request-scope) | | `DIContextMiddleware` | `setup_di(app, container)` / `ModernDIPlugin(container)` | [§8](#8-framework-integration-and-routes) | -| `fetch_context_item` / `_by_type` | `ContextProvider(context_type=T)` | [§5](#5-context-resources-and-request-scope) | +| `fetch_context_item` / `_by_type` | `ContextProvider(T)` | [§5](#5-context-resources-and-request-scope) | | `init_resources()` | Lazy initialization — no equivalent needed | [§7](#7-lifecycle-and-testing) | | `tear_down()` / `tear_down_sync()` | `await container.close_async()` / `container.close_sync()` | [§7](#7-lifecycle-and-testing) | | `container.override_providers_sync({...})` | `container.override(provider, mock)` | [§7](#7-lifecycle-and-testing) | @@ -121,24 +121,24 @@ When a provider is passed inside `kwargs={...}`, `modern-di` detects it and reso class Dependencies(Group): database_engine = providers.Factory( - creator=create_sa_engine, + create_sa_engine, cache=providers.CacheSettings(finalizer=close_sa_engine), ) session = providers.Factory( + create_session, scope=Scope.REQUEST, - creator=create_session, cache=providers.CacheSettings(finalizer=close_session), kwargs={"engine": database_engine}, ) decks_service = providers.Factory( + repositories.DecksService, scope=Scope.REQUEST, - creator=repositories.DecksService, kwargs={"session": session}, ) cards_service = providers.Factory( + repositories.CardsService, scope=Scope.REQUEST, - creator=repositories.CardsService, kwargs={"session": session}, ) @@ -157,7 +157,7 @@ some_singleton = providers.Singleton(SomeClass) # modern-di some_singleton = providers.Factory( - creator=SomeClass, + SomeClass, cache=True, ) ``` @@ -176,7 +176,7 @@ class ApiKey(str): ... def _api_key() -> ApiKey: return ApiKey("secret-token") -api_key = providers.Factory(creator=_api_key, cache=True) +api_key = providers.Factory(_api_key, cache=True) ``` If you only need the value passed into one downstream provider, skip the wrapper and put it directly in that provider's `kwargs`. @@ -191,7 +191,7 @@ some_list = providers.List(provider1, provider2) def build_list(a: SomeType1, b: SomeType2) -> list[object]: return [a, b] -some_list = providers.Factory(creator=build_list) +some_list = providers.Factory(build_list) ``` **`Provider.bind(Type)`** → `Alias`. Useful when you want an abstract type (`Protocol`, ABC) to resolve to a concrete registered provider: @@ -201,8 +201,8 @@ some_list = providers.Factory(creator=build_list) repo = providers.Factory(PostgresRepository).bind(Repository) # modern-di -repo = providers.Factory(creator=PostgresRepository, cache=True) -abstract_repo = providers.Alias(source_type=PostgresRepository, bound_type=Repository) +repo = providers.Factory(PostgresRepository, cache=True) +abstract_repo = providers.Alias(PostgresRepository, bound_type=Repository) ``` ## 5. Context resources and request scope @@ -221,11 +221,11 @@ class TenantId(str): ... class Dependencies(Group): - tenant = providers.ContextProvider(scope=Scope.REQUEST, context_type=TenantId) + tenant = providers.ContextProvider(TenantId, scope=Scope.REQUEST) repo = providers.Factory( + TenantScopedRepository, # signature: (tenant: TenantId, ...) scope=Scope.REQUEST, - creator=TenantScopedRepository, # signature: (tenant: TenantId, ...) ) @@ -261,7 +261,7 @@ async def close_engine(engine: sqlalchemy.ext.asyncio.AsyncEngine) -> None: engine = providers.Factory( - creator=create_engine, + create_engine, cache=providers.CacheSettings(finalizer=close_engine), ) ``` diff --git a/docs/migration/to-2.x.md b/docs/migration/to-2.x.md index 5ba8409..1d59a6b 100644 --- a/docs/migration/to-2.x.md +++ b/docs/migration/to-2.x.md @@ -18,16 +18,18 @@ modern-di 2.x merges the container classes, moves providers to keyword-only argu `with`/`async with container.build_child_container(...)` still works for automatic cleanup; `close_sync()`/`close_async()` are also available for manual lifecycle control. The framework integration packages were updated with matching new APIs. -2. **All provider constructors are keyword-only.** +2. **Provider constructor arguments became keyword-only.** ```python # Before (1.x) factory = providers.Factory(Scope.REQUEST, MyClass, arg1="value1") # After (2.x) - factory = providers.Factory(scope=Scope.REQUEST, creator=MyClass, kwargs={"arg1": "value1"}) + factory = providers.Factory(MyClass, scope=Scope.REQUEST, kwargs={"arg1": "value1"}) ``` + Since 2.27, the subject argument (`creator` / `context_type` / `source_type`) is accepted positionally again; all other parameters remain keyword-only. + 3. **`Singleton`, `Resource`, `Dict`, `List` removed** — all four map onto `Factory`: ```python @@ -36,10 +38,10 @@ modern-di 2.x merges the container classes, moves providers to keyword-only argu resource = providers.Resource(Scope.REQUEST, create_resource) # After (2.x) - singleton = providers.Factory(scope=Scope.APP, creator=create_singleton, cache=True) + singleton = providers.Factory(create_singleton, scope=Scope.APP, cache=True) resource = providers.Factory( + create_resource, scope=Scope.REQUEST, - creator=create_resource, cache=providers.CacheSettings(finalizer=lambda r: r.close()), ) ``` @@ -72,5 +74,5 @@ modern-di 2.x merges the container classes, moves providers to keyword-only argu service = providers.Factory(MyService, db_engine=database_engine.cast) # 2.x — MyService.__init__(self, db_engine: DBEngine); resolved by type - service = providers.Factory(scope=Scope.APP, creator=MyService) + service = providers.Factory(MyService, scope=Scope.APP) ``` diff --git a/docs/migration/to-3.x.md b/docs/migration/to-3.x.md index e042e34..dc5603e 100644 --- a/docs/migration/to-3.x.md +++ b/docs/migration/to-3.x.md @@ -62,14 +62,14 @@ from modern_di import Scope, providers # DeprecationWarning: The `scope` parameter of Alias is deprecated and ignored: # an alias's effective scope is derived from its source. It will be removed in # a future release. -alias = providers.Alias(source_type=DatabaseProtocol, scope=Scope.APP) +alias = providers.Alias(DatabaseProtocol, scope=Scope.APP) ``` **After (3.0):** ```python from modern_di import providers -alias = providers.Alias(source_type=DatabaseProtocol) +alias = providers.Alias(DatabaseProtocol) ``` ### 3. `Factory(cache_settings=)` removed @@ -83,8 +83,8 @@ works but warns; in 3.0 only `cache=` is accepted. # cache=True for defaults, or cache=CacheSettings(...) to tune). It will be # removed in a future release. factory = providers.Factory( + create_resource, scope=Scope.REQUEST, - creator=create_resource, cache_settings=providers.CacheSettings(finalizer=lambda resource: resource.close()), ) ``` @@ -92,8 +92,8 @@ factory = providers.Factory( **After (3.0):** ```python factory = providers.Factory( + create_resource, scope=Scope.REQUEST, - creator=create_resource, cache=providers.CacheSettings(finalizer=lambda resource: resource.close()), ) ``` diff --git a/docs/providers/alias.md b/docs/providers/alias.md index c8df54c..f987de7 100644 --- a/docs/providers/alias.md +++ b/docs/providers/alias.md @@ -6,6 +6,9 @@ Resolving the alias delegates straight back through the container, so overrides ## Parameters +`Alias(source_type, *, scope=UNSET, bound_type=UNSET)` — `scope` is deprecated and ignored (see +below). `source_type` may also be passed as a keyword (`source_type=`). + ### source_type The type whose registered provider should answer the call. At resolution time, the alias looks up `source_type` in the providers registry and delegates to that provider. If `source_type` is not registered, an `AliasSourceNotRegisteredError` is raised. @@ -41,11 +44,11 @@ class PostgresRepository: class Dependencies(Group): repo = providers.Factory( - creator=PostgresRepository, + PostgresRepository, cache=True, ) abstract_repo = providers.Alias( - source_type=PostgresRepository, + PostgresRepository, bound_type=Repository, ) diff --git a/docs/providers/container.md b/docs/providers/container.md index 36f7d83..6f8b428 100644 --- a/docs/providers/container.md +++ b/docs/providers/container.md @@ -19,7 +19,7 @@ def my_creator(di_container: Container) -> str: return f"Container scope: {di_container.scope.name}" class Dependencies(Group): - my_factory = providers.Factory(scope=Scope.APP, creator=my_creator) + my_factory = providers.Factory(my_creator, scope=Scope.APP) container = Container(groups=[Dependencies], validate=True) result = container.resolve(str) @@ -39,8 +39,8 @@ def another_creator(di_container: Container) -> str: class Dependencies(Group): another_factory = providers.Factory( + another_creator, scope=Scope.APP, - creator=another_creator, kwargs={"di_container": providers.container_provider} ) diff --git a/docs/providers/context.md b/docs/providers/context.md index 81032e6..d5d6695 100644 --- a/docs/providers/context.md +++ b/docs/providers/context.md @@ -10,6 +10,9 @@ container's context registry at resolve time. In integrations, some context objects (like `fastapi.Request`, `litestar.WebSocket`, etc.) are automatically provided — see [Framework Context Objects](#framework-context-objects) below. +`ContextProvider(context_type, *, scope=Scope.APP, bound_type=UNSET)` — `context_type` may also be +passed as a keyword (`context_type=`). + ## Basic Usage Declare a `ContextProvider` for your context type, supply the value when you build the child container, and any [`Factory`](factories.md) that takes that type as a parameter receives it automatically: @@ -33,12 +36,12 @@ def create_user_info(custom_context: CustomContext) -> dict[str, str]: class Dependencies(Group): # Manually defined ContextProvider for custom context - custom_context = providers.ContextProvider(scope=Scope.REQUEST, context_type=CustomContext) + custom_context = providers.ContextProvider(CustomContext, scope=Scope.REQUEST) # Factory uses the custom context user_info = providers.Factory( + create_user_info, scope=Scope.REQUEST, - creator=create_user_info, ) @@ -128,8 +131,8 @@ def create_request_info(request: fastapi.Request) -> dict[str, str]: class Dependencies(Group): # Factory uses the request from context (automatically provided by the integration) request_info = providers.Factory( + create_request_info, scope=Scope.REQUEST, - creator=create_request_info, ) diff --git a/docs/providers/factories.md b/docs/providers/factories.md index b3a2329..e60ede3 100644 --- a/docs/providers/factories.md +++ b/docs/providers/factories.md @@ -24,8 +24,8 @@ class IndependentFactory: class Dependencies(Group): independent_factory = providers.Factory( + IndependentFactory, scope=Scope.APP, - creator=IndependentFactory, kwargs={"dep1": "text", "dep2": 123} ) @@ -73,8 +73,8 @@ def generate_random_number() -> float: class Dependencies(Group): singleton = providers.Factory( + generate_random_number, scope=Scope.APP, - creator=generate_random_number, cache=True ) @@ -110,8 +110,8 @@ class Dependencies(Group): # Cache with cleanup — clear_cache=True (the default) ensures the closed # resource is evicted from cache so it cannot be returned again after close resource = providers.Factory( + create_resource, scope=Scope.APP, - creator=create_resource, cache=providers.CacheSettings( finalizer=lambda res: res.close(), # Cleanup function ) @@ -120,6 +120,9 @@ class Dependencies(Group): ## Parameters +`Factory(creator, *, scope=Scope.APP, bound_type=UNSET, kwargs=None, cache=None, cache_settings=UNSET, skip_creator_parsing=False)` +— `creator` may also be passed as a keyword (`creator=`). + When creating a Factory provider, you can configure several parameters: ### scope @@ -191,7 +194,7 @@ class Service: class Dependencies(Group): - service = providers.Factory(scope=Scope.APP, creator=Service) + service = providers.Factory(Service, scope=Scope.APP) container = Container(groups=[Dependencies], validate=True) @@ -237,10 +240,10 @@ def make_service(dep: object) -> object: class Dependencies(Group): - backend = providers.Factory(scope=Scope.APP, creator=Backend) + backend = providers.Factory(Backend, scope=Scope.APP) service = providers.Factory( + make_service, scope=Scope.APP, - creator=make_service, skip_creator_parsing=True, bound_type=None, kwargs={"dep": backend}, # provider object — resolved at resolve-time @@ -280,8 +283,8 @@ def flaky_creator() -> object: class Dependencies(Group): svc = providers.Factory( + flaky_creator, scope=Scope.APP, - creator=flaky_creator, cache=True, ) diff --git a/docs/providers/lifecycle.md b/docs/providers/lifecycle.md index 578c80b..a74d3c3 100644 --- a/docs/providers/lifecycle.md +++ b/docs/providers/lifecycle.md @@ -28,8 +28,8 @@ container.resolve(Settings) ```python session = providers.Factory( + create_session, scope=Scope.REQUEST, - creator=create_session, cache=providers.CacheSettings(finalizer=close_session), ) ``` diff --git a/docs/providers/scopes.md b/docs/providers/scopes.md index aa945df..c41bdcc 100644 --- a/docs/providers/scopes.md +++ b/docs/providers/scopes.md @@ -105,7 +105,7 @@ class TenantContext: class MyGroup(Group): - tenant_provider = providers.Factory(scope=MyScope.TENANT, creator=TenantContext) + tenant_provider = providers.Factory(TenantContext, scope=MyScope.TENANT) container = Container(groups=[MyGroup], validate=True) diff --git a/docs/recipes/async-lifespan.md b/docs/recipes/async-lifespan.md index 5e0072e..e3eea2c 100644 --- a/docs/recipes/async-lifespan.md +++ b/docs/recipes/async-lifespan.md @@ -4,7 +4,7 @@ ## Solution -Do the async construction in the framework's lifespan. Use `container.set_context(SomeType, instance)` to register the live object on the APP container, then declare a `ContextProvider(scope=Scope.APP, context_type=SomeType)` so downstream factories can depend on the type. +Do the async construction in the framework's lifespan. Use `container.set_context(SomeType, instance)` to register the live object on the APP container, then declare a `ContextProvider(SomeType, scope=Scope.APP)` so downstream factories can depend on the type. ```python import contextlib @@ -17,14 +17,14 @@ from modern_di import Container, Group, Scope, providers class Dependencies(Group): http_client = providers.ContextProvider( + aiohttp.ClientSession, scope=Scope.APP, - context_type=aiohttp.ClientSession, ) # Downstream factories declare `client: aiohttp.ClientSession` and get the live instance weather_api = providers.Factory( + WeatherApi, # signature: (client: aiohttp.ClientSession) scope=Scope.REQUEST, - creator=WeatherApi, # signature: (client: aiohttp.ClientSession) ) diff --git a/docs/recipes/good-and-bad-practices.md b/docs/recipes/good-and-bad-practices.md index 4287b4f..1a1df86 100644 --- a/docs/recipes/good-and-bad-practices.md +++ b/docs/recipes/good-and-bad-practices.md @@ -10,13 +10,13 @@ outlive — see [the scope dependency rule](../providers/scopes.md#the-scope-dep ```python class Dependencies(Group): - session = providers.Factory(scope=Scope.REQUEST, creator=Session) + session = providers.Factory(Session, scope=Scope.REQUEST) # ❌ forgot scope=Scope.REQUEST — defaults to Scope.APP, which cannot hold `session` - user_cache = providers.Factory(creator=UserCache) + user_cache = providers.Factory(UserCache) # ✅ matches the lifetime of what it consumes - user_cache = providers.Factory(scope=Scope.REQUEST, creator=UserCache) + user_cache = providers.Factory(UserCache, scope=Scope.REQUEST) ``` **Caught by:** `Container(groups=[...], validate=True)` raises `InvalidScopeDependencyError` for @@ -54,13 +54,13 @@ factory is built once, and a later `set_context` does not rebuild it. ```python class Dependencies(Group): - tenant_id = providers.ContextProvider(scope=Scope.REQUEST, context_type=str) + tenant_id = providers.ContextProvider(str, scope=Scope.REQUEST) # ❌ cached: built on first resolve and frozen from then on - tenant_config = providers.Factory(scope=Scope.REQUEST, creator=create_tenant_config, cache=True) + tenant_config = providers.Factory(create_tenant_config, scope=Scope.REQUEST, cache=True) # ✅ uncached: re-reads the live context on every resolve - tenant_config = providers.Factory(scope=Scope.REQUEST, creator=create_tenant_config) + tenant_config = providers.Factory(create_tenant_config, scope=Scope.REQUEST) ``` If a request container resolves `tenant_config` before the real tenant ID is known (e.g. during @@ -129,12 +129,12 @@ no idea what type the provider produces, so type-based resolution silently can't ```python # ❌ nothing else can resolve this provider by type — UserWarning at declaration time -providers.Factory(scope=Scope.APP, creator=opaque_creator, skip_creator_parsing=True) +providers.Factory(opaque_creator, scope=Scope.APP, skip_creator_parsing=True) # ✅ tell modern-di the type explicitly providers.Factory( + opaque_creator, scope=Scope.APP, - creator=opaque_creator, skip_creator_parsing=True, bound_type=MyClass, ) diff --git a/docs/recipes/multi-group.md b/docs/recipes/multi-group.md index f8ceac0..a7b1ac2 100644 --- a/docs/recipes/multi-group.md +++ b/docs/recipes/multi-group.md @@ -42,35 +42,35 @@ async def close_redis(client: aioredis.Redis) -> None: class Database(Group): engine = providers.Factory( + create_engine, scope=Scope.APP, - creator=create_engine, cache=providers.CacheSettings(finalizer=close_engine), ) session = providers.Factory( + create_session, scope=Scope.REQUEST, - creator=create_session, cache=providers.CacheSettings(finalizer=close_session), ) class Cache(Group): redis_client = providers.Factory( + create_redis, scope=Scope.APP, - creator=create_redis, cache=providers.CacheSettings(finalizer=close_redis), ) class Repositories(Group): # UserRepository signature: (session: AsyncSession) - users = providers.Factory(scope=Scope.REQUEST, creator=UserRepository) - orders = providers.Factory(scope=Scope.REQUEST, creator=OrderRepository) + users = providers.Factory(UserRepository, scope=Scope.REQUEST) + orders = providers.Factory(OrderRepository, scope=Scope.REQUEST) class UseCases(Group): # PlaceOrder signature: (users: UserRepository, orders: OrderRepository, cache: aioredis.Redis) - place_order = providers.Factory(scope=Scope.REQUEST, creator=PlaceOrder) - cancel_order = providers.Factory(scope=Scope.REQUEST, creator=CancelOrder) + place_order = providers.Factory(PlaceOrder, scope=Scope.REQUEST) + cancel_order = providers.Factory(CancelOrder, scope=Scope.REQUEST) ALL_GROUPS = [Database, Cache, Repositories, UseCases] diff --git a/docs/recipes/request-scoped-engine.md b/docs/recipes/request-scoped-engine.md index e0893d9..0b6119a 100644 --- a/docs/recipes/request-scoped-engine.md +++ b/docs/recipes/request-scoped-engine.md @@ -45,30 +45,30 @@ class ReplicaEngine(sa_async.AsyncEngine): ... class Dependencies(Group): primary = providers.Factory( + create_primary_engine, scope=Scope.APP, - creator=create_primary_engine, bound_type=PrimaryEngine, cache=providers.CacheSettings(finalizer=close_engine), ) replica = providers.Factory( + create_replica_engine, scope=Scope.APP, - creator=create_replica_engine, bound_type=ReplicaEngine, cache=providers.CacheSettings(finalizer=close_engine), ) # REQUEST-scope: picks per-request, cached for the rest of that request engine = providers.Factory( + choose_engine, scope=Scope.REQUEST, - creator=choose_engine, kwargs={"primary": primary, "replica": replica}, cache=True, ) # Sessions and repositories use the REQUEST-scoped engine session = providers.Factory( + create_session, scope=Scope.REQUEST, - creator=create_session, cache=providers.CacheSettings(finalizer=close_session), ) ``` diff --git a/docs/recipes/sqlalchemy.md b/docs/recipes/sqlalchemy.md index 360f3b3..e1f16cf 100644 --- a/docs/recipes/sqlalchemy.md +++ b/docs/recipes/sqlalchemy.md @@ -41,18 +41,18 @@ class UserRepository: class Dependencies(Group): engine = providers.Factory( + create_engine, scope=Scope.APP, - creator=create_engine, cache=providers.CacheSettings(finalizer=close_engine), ) session = providers.Factory( + create_session, scope=Scope.REQUEST, - creator=create_session, cache=providers.CacheSettings(finalizer=close_session), ) user_repository = providers.Factory( + UserRepository, scope=Scope.REQUEST, - creator=UserRepository, ) ``` diff --git a/docs/troubleshooting/alias-source-not-registered-error.md b/docs/troubleshooting/alias-source-not-registered-error.md index 7fbd9dd..a7441e7 100644 --- a/docs/troubleshooting/alias-source-not-registered-error.md +++ b/docs/troubleshooting/alias-source-not-registered-error.md @@ -6,7 +6,7 @@ Raised naming the `source_type` an `Alias` points at, saying no provider is regi **Cause** -`Alias(source_type=X)` was declared, but no provider's `bound_type` resolves to `X` — either the +`Alias(X)` was declared, but no provider's `bound_type` resolves to `X` — either the provider for `X` was never defined, its group wasn't passed to `Container(groups=[...])`, or it was declared with `bound_type=None` (making it unresolvable by type, which an alias also can't reach). This is checked eagerly during `validate()`, and again at resolve time if validation was skipped. @@ -21,9 +21,9 @@ from modern_di import Group, Scope, providers class Dependencies(Group): # The alias's source must resolve by type — no bound_type=None here. - impl = providers.Factory(scope=Scope.APP, creator=Implementation) + impl = providers.Factory(Implementation, scope=Scope.APP) - interface_alias = providers.Alias(source_type=Implementation) + interface_alias = providers.Alias(Implementation) ``` If the source provider lives in a different `Group`, make sure that group is also passed to diff --git a/docs/troubleshooting/argument-resolution-error.md b/docs/troubleshooting/argument-resolution-error.md index b9ac79a..57d1e2a 100644 --- a/docs/troubleshooting/argument-resolution-error.md +++ b/docs/troubleshooting/argument-resolution-error.md @@ -21,13 +21,13 @@ pass it explicitly via `kwargs`: ```python class Dependencies(Group): # missing: no provider for `Clock` anywhere - service = providers.Factory(scope=Scope.APP, creator=Service) # Service(clock: Clock) + service = providers.Factory(Service, scope=Scope.APP) # Service(clock: Clock) # fix option 1: register a provider - clock = providers.Factory(scope=Scope.APP, creator=SystemClock, bound_type=Clock) + clock = providers.Factory(SystemClock, scope=Scope.APP, bound_type=Clock) # fix option 2: pass explicitly - service2 = providers.Factory(scope=Scope.APP, creator=Service, kwargs={"clock": clock}) + service2 = providers.Factory(Service, scope=Scope.APP, kwargs={"clock": clock}) ``` Check `.suggestions` on the caught exception for a "did you mean" hint when a similarly-named type is diff --git a/docs/troubleshooting/context-not-set.md b/docs/troubleshooting/context-not-set.md index f51fa5f..7af53c2 100644 --- a/docs/troubleshooting/context-not-set.md +++ b/docs/troubleshooting/context-not-set.md @@ -1,6 +1,6 @@ # ContextProvider has no value -A `ContextProvider(context_type=SomeType)` resolves by looking up `SomeType` in the container's context registry. If no value was registered, the outcome depends on how the provider is consumed: resolving it directly returns `None`, while injecting it into a `Factory` parameter that has no value raises `ArgumentResolutionError` — **unless** that parameter has a default (the default is used; `None` is not injected) or is nullable `X | None` (then `None` is injected). +A `ContextProvider(SomeType)` resolves by looking up `SomeType` in the container's context registry. If no value was registered, the outcome depends on how the provider is consumed: resolving it directly returns `None`, while injecting it into a `Factory` parameter that has no value raises `ArgumentResolutionError` — **unless** that parameter has a default (the default is used; `None` is not injected) or is nullable `X | None` (then `None` is injected). ## Understanding the error @@ -41,9 +41,9 @@ request_container.set_context(TenantId, TenantId("acme")) ### 2. The `ContextProvider`'s scope doesn't match where you set the context -`ContextProvider(scope=Scope.APP, context_type=TenantId)` looks up the value on the APP container. If you `set_context` on the REQUEST child container, the APP-scope provider doesn't see it. +`ContextProvider(TenantId, scope=Scope.APP)` looks up the value on the APP container. If you `set_context` on the REQUEST child container, the APP-scope provider doesn't see it. -Fix: match the scope. If the value is per-request, declare `ContextProvider(scope=Scope.REQUEST, ...)` and `set_context` on the request container (or pass via `build_child_container(context=...)`). +Fix: match the scope. If the value is per-request, declare `ContextProvider(TenantId, scope=Scope.REQUEST)` and `set_context` on the request container (or pass via `build_child_container(context=...)`). ### 3. Framework integration didn't inject the expected request diff --git a/docs/troubleshooting/creator-call-error.md b/docs/troubleshooting/creator-call-error.md index e4797d8..61ef61b 100644 --- a/docs/troubleshooting/creator-call-error.md +++ b/docs/troubleshooting/creator-call-error.md @@ -27,13 +27,13 @@ def create_service(host: str, port: int) -> Service: ... class Dependencies(Group): # Wrong: skip_creator_parsing=True but kwargs misses `port` service = providers.Factory( - scope=Scope.APP, creator=create_service, skip_creator_parsing=True, + create_service, scope=Scope.APP, skip_creator_parsing=True, bound_type=Service, kwargs={"host": "localhost"}, ) # Right service = providers.Factory( - scope=Scope.APP, creator=create_service, skip_creator_parsing=True, + create_service, scope=Scope.APP, skip_creator_parsing=True, bound_type=Service, kwargs={"host": "localhost", "port": 5432}, ) ``` diff --git a/docs/troubleshooting/duplicate-type-error.md b/docs/troubleshooting/duplicate-type-error.md index 75b4770..3d44b18 100644 --- a/docs/troubleshooting/duplicate-type-error.md +++ b/docs/troubleshooting/duplicate-type-error.md @@ -46,29 +46,29 @@ class MyGroup(Group): # Step 1: Set bound_type=None on the secondary provider or for both providers # This provider can be resolved by type: container.resolve(DatabaseConfig) primary_db_config = providers.Factory( + DatabaseConfig, scope=Scope.APP, - creator=DatabaseConfig, kwargs={"connection_string": "postgresql://primary"} ) # This provider cannot be resolved by type # Must use: container.resolve_provider(MyGroup.secondary_db_config) secondary_db_config = providers.Factory( + DatabaseConfig, scope=Scope.APP, - creator=DatabaseConfig, bound_type=None, # <-- Step 1: Makes it unresolvable by type kwargs={"connection_string": "postgresql://secondary"} ) # Step 2: Explicitly pass dependencies via kwargs for second repository or for both primary_repository = providers.Factory( + Repository, # <-- Implicit dependency, no kwargs scope=Scope.APP, - creator=Repository, # <-- Implicit dependency, no kwargs ) secondary_repository = providers.Factory( + Repository, scope=Scope.APP, - creator=Repository, kwargs={"db_config": secondary_db_config} # <-- Step 2: Explicit dependency ) ``` diff --git a/docs/troubleshooting/group-instantiation-error.md b/docs/troubleshooting/group-instantiation-error.md index bed67da..bb9dbb2 100644 --- a/docs/troubleshooting/group-instantiation-error.md +++ b/docs/troubleshooting/group-instantiation-error.md @@ -17,7 +17,7 @@ Use the class itself, not an instance: ```python class Dependencies(Group): - service = providers.Factory(scope=Scope.APP, creator=Service) + service = providers.Factory(Service, scope=Scope.APP) # Wrong diff --git a/docs/troubleshooting/scope-chain.md b/docs/troubleshooting/scope-chain.md index 4451b2a..38ef330 100644 --- a/docs/troubleshooting/scope-chain.md +++ b/docs/troubleshooting/scope-chain.md @@ -28,18 +28,18 @@ Bump the depender's scope: ```python class Dependencies(Group): session = providers.Factory( + create_session, scope=Scope.REQUEST, - creator=create_session, cache=providers.CacheSettings(finalizer=close_session), ) # ❌ APP-scoped — fails validation - user_repository = providers.Factory(creator=UserRepository) + user_repository = providers.Factory(UserRepository) # ✅ REQUEST-scoped — matches session's lifetime user_repository = providers.Factory( + UserRepository, scope=Scope.REQUEST, - creator=UserRepository, ) ``` diff --git a/docs/troubleshooting/unknown-factory-kwarg-error.md b/docs/troubleshooting/unknown-factory-kwarg-error.md index 73fac39..da6f4e5 100644 --- a/docs/troubleshooting/unknown-factory-kwarg-error.md +++ b/docs/troubleshooting/unknown-factory-kwarg-error.md @@ -23,12 +23,12 @@ def create_service(connection_string: str) -> Service: ... class Dependencies(Group): # Wrong: typo — raises UnknownFactoryKwargError, suggests "connection_string" service = providers.Factory( - scope=Scope.APP, creator=create_service, kwargs={"conection_string": "..."} + create_service, scope=Scope.APP, kwargs={"conection_string": "..."} ) # Right service = providers.Factory( - scope=Scope.APP, creator=create_service, kwargs={"connection_string": "..."} + create_service, scope=Scope.APP, kwargs={"connection_string": "..."} ) ``` diff --git a/docs/troubleshooting/unsupported-creator-parameter-error.md b/docs/troubleshooting/unsupported-creator-parameter-error.md index d82c48a..aa2b637 100644 --- a/docs/troubleshooting/unsupported-creator-parameter-error.md +++ b/docs/troubleshooting/unsupported-creator-parameter-error.md @@ -25,11 +25,11 @@ class Dependencies(Group): # def create_thing(items: list[Item] = ()) -> Thing: ... # 2. Supply the value via kwargs at declaration time - thing = providers.Factory(scope=Scope.APP, creator=create_thing, kwargs={"items": []}) + thing = providers.Factory(create_thing, scope=Scope.APP, kwargs={"items": []}) # 3. Skip creator parsing entirely and supply every argument via kwargs thing2 = providers.Factory( - scope=Scope.APP, creator=create_thing, skip_creator_parsing=True, kwargs={"items": []} + create_thing, scope=Scope.APP, skip_creator_parsing=True, kwargs={"items": []} ) ``` From dd86d284978eff528eb83696733c3ebaca1a8f02 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Wed, 8 Jul 2026 09:48:23 +0300 Subject: [PATCH 5/5] docs: promote positional subject arguments to architecture/providers.md Co-Authored-By: Claude Opus 4.8 (1M context) --- architecture/providers.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/architecture/providers.md b/architecture/providers.md index 7815b9e..c0ed54c 100644 --- a/architecture/providers.md +++ b/architecture/providers.md @@ -15,8 +15,8 @@ attributes: from modern_di import providers, Group, Scope class AppProviders(Group): - db_pool = providers.Factory(scope=Scope.APP, creator=create_pool) - user_repo = providers.Factory(scope=Scope.REQUEST, creator=UserRepository) + db_pool = providers.Factory(create_pool, scope=Scope.APP) + user_repo = providers.Factory(UserRepository, scope=Scope.REQUEST) ``` `Group.get_named_providers()` walks the MRO and returns a `dict[str, AbstractProvider]` mapping each declared @@ -28,15 +28,18 @@ non-provider override mask the parent provider of the same name. `Group.get_prov ## `Factory` — the universal provider -`Factory` is the main building block. Every provider that calls a creator callable (a constructor or factory function passed as `creator=`) is a `Factory`. +`Factory` is the main building block. Every provider that calls a creator callable (a constructor or factory +function passed as its `creator` argument) is a `Factory`. Each registerable provider's subject argument — +`Factory.creator`, `ContextProvider.context_type`, `Alias.source_type` — is positional-or-keyword and leads its +`__init__`; every other parameter stays keyword-only. ### Signature ```python Factory( + creator: Callable[..., T], *, scope: IntEnum = Scope.APP, - creator: Callable[..., T], bound_type: type | None = UNSET, kwargs: dict[str, Any] | None = None, cache: bool | CacheSettings[T] | None = None, @@ -101,7 +104,7 @@ each time. being constructed by a factory: ```python -providers.ContextProvider(scope=Scope.REQUEST, context_type=HttpRequest) +providers.ContextProvider(HttpRequest, scope=Scope.REQUEST) ``` At resolution time it looks the value up in the container's `context_registry` for the matching scope. What @@ -129,7 +132,7 @@ and the two paths are independent: `Alias` delegates resolution to another registered provider, located by the source type: ```python -providers.Alias(source_type=ConcreteDatabase, bound_type=DatabaseProtocol) +providers.Alias(ConcreteDatabase, bound_type=DatabaseProtocol) ``` `Alias.resolve` calls `container.resolve_provider(source_provider)` — it holds no cache of its own — and also