diff --git a/modern_di/container.py b/modern_di/container.py index 9c38f58..e1f8d3e 100644 --- a/modern_di/container.py +++ b/modern_di/container.py @@ -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 @@ -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) diff --git a/modern_di/errors.py b/modern_di/errors.py new file mode 100644 index 0000000..99a6151 --- /dev/null +++ b/modern_di/errors.py @@ -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}" diff --git a/modern_di/providers/factory.py b/modern_di/providers/factory.py index 566d173..8e118c0 100644 --- a/modern_di/providers/factory.py +++ b/modern_di/providers/factory.py @@ -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 @@ -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) diff --git a/modern_di/registries/providers_registry.py b/modern_di/registries/providers_registry.py index 3b5c57f..e414cb4 100644 --- a/modern_di/registries/providers_registry.py +++ b/modern_di/registries/providers_registry.py @@ -1,6 +1,6 @@ import typing -from modern_di import types +from modern_di import errors, types from modern_di.providers.abstract import AbstractProvider @@ -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 diff --git a/tests/providers/test_factory.py b/tests/providers/test_factory.py index 784fb72..4eab86d 100644 --- a/tests/providers/test_factory.py +++ b/tests/providers/test_factory.py @@ -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= 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) @@ -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) diff --git a/tests/test_container.py b/tests/test_container.py index e76ee89..c9b31b9 100644 --- a/tests/test_container.py +++ b/tests/test_container.py @@ -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) @@ -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 is not registered in providers registry."): assert app_container.resolve(str) is None