From 407060de38656d404bb8e8a014f1d473862980da Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sun, 18 Oct 2026 12:59:22 +0300 Subject: [PATCH] fix: wire a parameterized generic class creator from its origin --- docs/providers/factories.md | 5 ++++ modern_di/types_parser.py | 7 +++++ tests/test_types_parser.py | 56 +++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+) diff --git a/docs/providers/factories.md b/docs/providers/factories.md index 8736bd54..f98c934d 100644 --- a/docs/providers/factories.md +++ b/docs/providers/factories.md @@ -68,6 +68,11 @@ there, the one on `inspect.signature(creator)` is used, if it is a type rather t Pydantic models and msgspec `Struct`s keep their field types only on the signature, so this is what wires their fields. +A parameterized generic class such as `Repo[int]` works as a creator. Its parameters come from +`Repo`'s constructor, calling `Repo[int]` builds the instance, and the bound type is `Repo`. Two +parameterizations of one class therefore share a bound type. To register both, set `bound_type=` on +them, for example `bound_type=None` to resolve each by reference. Before modern-di 4.2.1, such a creator was wired with no parameters and no bound type. + ### scope The provider's lifetime. Without `scope=`, the provider takes its group's default scope, or diff --git a/modern_di/types_parser.py b/modern_di/types_parser.py index 6926d404..47d0a361 100644 --- a/modern_di/types_parser.py +++ b/modern_di/types_parser.py @@ -149,7 +149,14 @@ def _signature(creator: typing.Callable[..., typing.Any]) -> inspect.Signature: return inspect.signature(creator) +def _generic_origin(creator: typing.Callable[..., typing.Any]) -> typing.Callable[..., typing.Any]: + """Return the class behind a parameterized generic class such as ``Repo[int]``, else ``creator``.""" + origin = typing.get_origin(creator) + return origin if isinstance(origin, type) and not isinstance(creator, type) else creator + + def parse_creator(creator: typing.Callable[..., typing.Any]) -> ParsedCreator: + creator = _generic_origin(creator) try: sig = _signature(creator) except (ValueError, TypeError): diff --git a/tests/test_types_parser.py b/tests/test_types_parser.py index b022f43e..b7a6d063 100644 --- a/tests/test_types_parser.py +++ b/tests/test_types_parser.py @@ -669,3 +669,59 @@ def test_class_signature_matches_inspect_signature(creator: typing.Callable[..., except ValueError as exc: actual = (ValueError, str(exc)) assert actual == expected + + +class _Repo(typing.Generic[types.T]): + def __init__(self, dep: _Dep) -> None: + self.dep = dep + + +def test_generic_alias_creator_wires_the_origin_class() -> None: + class Dependencies(Group): + dep = providers.Factory(_Dep) + repo = providers.Factory(_Repo[int], cache=True) + + container = Container(groups=[Dependencies]) + container.validate() + repo = container.resolve(_Repo) + + assert Dependencies.repo.bound_type is _Repo + assert isinstance(repo.dep, _Dep) + assert repo.__orig_class__ == _Repo[int] # ty: ignore[unresolved-attribute] + assert container.resolve_provider(Dependencies.repo) is repo + + +def test_generic_alias_creator_dependencies_are_validated() -> None: + class Dependencies(Group): + repo = providers.Factory(_Repo[int]) + + with pytest.raises(exceptions.ValidationFailedError): + Container(groups=[Dependencies]).validate() + + +def test_generic_alias_creator_rejects_an_unknown_kwarg() -> None: + with pytest.raises(exceptions.UnknownFactoryKwargError): + providers.Factory(_Repo[int], kwargs={"nope": 1}) + + +def test_two_parameterizations_of_one_generic_collide_on_the_origin() -> None: + class Dependencies(Group): + dep = providers.Factory(_Dep) + ints = providers.Factory(_Repo[int]) + strs = providers.Factory(_Repo[str]) + + with pytest.raises(exceptions.DuplicateProviderTypeError): + Container(groups=[Dependencies]) + + +def test_explicit_bound_types_register_parameterizations_side_by_side() -> None: + class Dependencies(Group): + dep = providers.Factory(_Dep) + ints = providers.Factory(_Repo[int], bound_type=None) + strs = providers.Factory(_Repo[str], bound_type=None) + + container = Container(groups=[Dependencies]) + container.validate() + + assert container.resolve_provider(Dependencies.ints).__orig_class__ == _Repo[int] # ty: ignore[unresolved-attribute] + assert container.resolve_provider(Dependencies.strs).__orig_class__ == _Repo[str] # ty: ignore[unresolved-attribute]