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
29 changes: 18 additions & 11 deletions modern_di/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import typing_extensions

from modern_di import types
from modern_di import errors, types
from modern_di.group import Group
from modern_di.providers.abstract import AbstractProvider
from modern_di.providers.container_provider import container_provider
Expand Down Expand Up @@ -56,38 +56,45 @@ def build_child_container(
self, context: dict[type[typing.Any], typing.Any] | None = None, scope: Scope | None = None
) -> "typing_extensions.Self":
if scope and scope <= self.scope:
msg = "Scope of child container must be more than current scope"
raise RuntimeError(msg)
raise RuntimeError(
errors.CONTAINER_SCOPE_IS_LOWER_ERROR.format(
parent_scope=self.scope.name,
child_scope=scope.name,
allowed_scopes=[x.name for x in Scope if x > self.scope],
)
)

if not scope:
try:
scope = self.scope.__class__(self.scope.value + 1)
except ValueError as exc:
msg = f"Max scope is reached, {self.scope.name}"
raise RuntimeError(msg) from exc
raise RuntimeError(
errors.CONTAINER_MAX_SCOPE_REACHED_ERROR.format(parent_scope=self.scope.name)
) from exc

return self.__class__(scope=scope, parent_container=self, context=context)

def find_container(self, scope: Scope) -> "typing_extensions.Self":
container = self
if container.scope < scope:
msg = f"Scope {scope.name} is not initialized"
raise RuntimeError(msg)
raise RuntimeError(
errors.CONTAINER_NOT_INITIALIZED_SCOPE_ERROR.format(
provider_scope=scope.name, container_scope=self.scope.name
)
)

while container.scope > scope and container.parent_container:
container = container.parent_container

if container.scope != scope:
msg = f"Scope {scope.name} is skipped"
raise RuntimeError(msg)
raise RuntimeError(errors.CONTAINER_SCOPE_IS_SKIPPED_ERROR.format(provider_scope=scope.name))

return container

def resolve(self, dependency_type: type[types.T]) -> types.T:
provider = self.providers_registry.find_provider(dependency_type)
if not provider:
msg = f"Provider is not found, {dependency_type=}"
raise RuntimeError(msg)
raise RuntimeError(errors.CONTAINER_MISSING_PROVIDER_ERROR.format(provider_type=dependency_type))

return self.resolve_provider(provider)

Expand Down
14 changes: 14 additions & 0 deletions modern_di/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
CONTAINER_SCOPE_IS_LOWER_ERROR = (
"Scope of child container cannot be {child_scope} if parent scope is {parent_scope}. "
"Possible scopes are {allowed_scopes}."
)
CONTAINER_MAX_SCOPE_REACHED_ERROR = "Max scope of {parent_scope} is reached."
CONTAINER_NOT_INITIALIZED_SCOPE_ERROR = (
"Provider of scope {provider_scope} cannot be resolved in container of scope {container_scope}."
)
CONTAINER_SCOPE_IS_SKIPPED_ERROR = "Provider of scope {provider_scope} is skipped in the chain of containers."
CONTAINER_MISSING_PROVIDER_ERROR = "Provider of type {provider_type} is not registered in providers registry."
FACTORY_ARGUMENT_RESOLUTION_ERROR = (
"Argument {arg_name} of type {arg_type} cannot be resolved. Trying to build dependency {bound_type}."
)
PROVIDER_DUPLICATE_TYPE_ERROR = "Provider is duplicated by type {provider_type}"
9 changes: 6 additions & 3 deletions modern_di/providers/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import inspect
import typing

from modern_di import types
from modern_di import errors, types
from modern_di.providers.abstract import AbstractProvider
from modern_di.scope import Scope
from modern_di.types_parser import SignatureItem, parse_creator
Expand Down Expand Up @@ -66,8 +66,11 @@ def _compile_kwargs(self, container: "Container") -> dict[str, typing.Any]:
continue

if (not self._kwargs or k not in self._kwargs) and v.default == types.UNSET:
msg = f"Argument {k} cannot be resolved, type={v.arg_type}, factory={self._creator}"
raise RuntimeError(msg)
raise RuntimeError(
errors.FACTORY_ARGUMENT_RESOLUTION_ERROR.format(
arg_name=k, arg_type=v.arg_type, bound_type=self.bound_type or self._creator
)
)

if self._kwargs:
result.update(self._kwargs)
Expand Down
5 changes: 2 additions & 3 deletions modern_di/registries/providers_registry.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import typing

from modern_di import types
from modern_di import errors, types
from modern_di.providers.abstract import AbstractProvider


Expand All @@ -20,7 +20,6 @@ def add_providers(self, *args: AbstractProvider[typing.Any]) -> None:
continue

if provider_type in self._providers:
msg = f"Provider is duplicated by type {provider_type}"
raise RuntimeError(msg)
raise RuntimeError(errors.PROVIDER_DUPLICATE_TYPE_ERROR.format(provider_type=provider_type))

self._providers[provider_type] = provider
6 changes: 3 additions & 3 deletions tests/providers/test_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def test_app_factory_skip_creator_parsing() -> None:

def test_app_factory_unresolvable() -> None:
app_container = Container(groups=[MyGroup])
with pytest.raises(RuntimeError, match="Argument dep1 cannot be resolved, type=<class 'str'"):
with pytest.raises(RuntimeError, match="Argument dep1 of type <class 'str'> cannot be resolved"):
app_container.resolve_provider(MyGroup.app_factory_unresolvable)


Expand All @@ -70,7 +70,7 @@ def test_func_with_union_factory() -> None:

def test_func_with_broken_annotation() -> None:
app_container = Container(groups=[MyGroup])
with pytest.raises(RuntimeError, match="Argument dep1 cannot be resolved, type=None"):
with pytest.raises(RuntimeError, match="Argument dep1 of type None cannot be resolved"):
app_container.resolve_provider(MyGroup.func_with_broken_annotation)


Expand Down Expand Up @@ -150,7 +150,7 @@ def test_factory_overridden_request_scope() -> None:

def test_factory_scope_is_not_initialized() -> None:
app_container = Container(groups=[MyGroup])
with pytest.raises(RuntimeError, match="Scope REQUEST is not initialize"):
with pytest.raises(RuntimeError, match=r"Provider of scope REQUEST cannot be resolved in container of scope APP."):
app_container.resolve_provider(MyGroup.request_factory)


Expand Down
8 changes: 4 additions & 4 deletions tests/test_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def test_container_prevent_copy() -> None:
def test_container_scope_skipped() -> None:
app_factory = providers.Factory(creator=lambda: "test")
container = Container(scope=Scope.REQUEST)
with pytest.raises(RuntimeError, match="Scope APP is skipped"):
with pytest.raises(RuntimeError, match=r"Provider of scope APP is skipped in the chain of containers."):
container.resolve_provider(app_factory)


Expand All @@ -28,17 +28,17 @@ def test_container_build_child() -> None:

def test_container_scope_limit_reached() -> None:
step_container = Container(scope=Scope.STEP)
with pytest.raises(RuntimeError, match="Max scope is reached, STEP"):
with pytest.raises(RuntimeError, match=r"Max scope of STEP is reached."):
step_container.build_child_container()


def test_container_build_child_wrong_scope() -> None:
app_container = Container()
with pytest.raises(RuntimeError, match="Scope of child container must be more than current scope"):
with pytest.raises(RuntimeError, match="Scope of child container cannot be"):
app_container.build_child_container(scope=Scope.APP)


def test_container_resolve_missing_provider() -> None:
app_container = Container()
with pytest.raises(RuntimeError, match="Provider is not found"):
with pytest.raises(RuntimeError, match=r"Provider of type <class 'str'> is not registered in providers registry."):
assert app_container.resolve(str) is None
Loading