Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/providers/factories.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions modern_di/types_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
56 changes: 56 additions & 0 deletions tests/test_types_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Loading