Repository: reagento/dishka
Pull request #362 (2025-02-09 18:01:58)

Refactoring (exceptions, ruff rules)
#351 

Changes exceptions module significantly. Thorough review required.


Patch:
diff --git a/.ruff.toml b/.ruff.toml
index d969678..8419a48 100644
--- a/.ruff.toml
+++ b/.ruff.toml
@@ -17,20 +17,25 @@ lint.ignore = [
     "PT001",
     "PT023",
     "SIM108",
-    "SIM114",
-    "TRY003",
-    "TC003",
-    "PLW2901",
     "RET505",
     "PLR0913",
-    "UP038",
-    "TC001",
     "SIM103",
     "ISC003",
+
+    # identitcal by code != identical by meaning
+    "SIM114",
+
+    # awful things, never use.
+    # It makes runtime work differently from typechecker
+    "TC001",
+    "TC003",
+
+    # only affects perfomance and brings nothing useful
+    "UP038",
 ]
 
 [lint.per-file-ignores]
-"tests/**" = ["TID252", "PLR2004", "S101"]
+"tests/**" = ["TID252", "PLR2004", "S101", "TRY003"]
 
 [lint.isort]
 no-lines-before = ["local-folder"]
diff --git a/src/dishka/__init__.py b/src/dishka/__init__.py
index eabac46..8a814ca 100644
--- a/src/dishka/__init__.py
+++ b/src/dishka/__init__.py
@@ -25,13 +25,6 @@ __all__ = [
 
 from .async_container import AsyncContainer, make_async_container
 from .container import Container, make_container
-from .dependency_source import (
-    alias,
-    decorate,
-    from_context,
-    provide,
-    provide_all,
-)
 from .entities.component import DEFAULT_COMPONENT, Component
 from .entities.depends_marker import FromDishka
 from .entities.key import DependencyKey, FromComponent
@@ -39,4 +32,11 @@ from .entities.provides_marker import AnyOf
 from .entities.scope import BaseScope, Scope, new_scope
 from .entities.validation_settigs import STRICT_VALIDATION, ValidationSettings
 from .entities.with_parents import WithParents
-from .provider import Provider
+from .provider import (
+    Provider,
+    alias,
+    decorate,
+    from_context,
+    provide,
+    provide_all,
+)
diff --git a/src/dishka/async_container.py b/src/dishka/async_container.py
index 0180e55..66376e6 100644
--- a/src/dishka/async_container.py
+++ b/src/dishka/async_container.py
@@ -16,8 +16,11 @@ from .context_proxy import ContextProxy
 from .dependency_source import Factory
 from .entities.validation_settigs import DEFAULT_VALIDATION, ValidationSettings
 from .exceptions import (
+    ChildScopeNotFoundError,
     ExitError,
+    NoChildScopesError,
     NoFactoryError,
+    NoNonSkippedScopesError,
 )
 from .provider import BaseProvider
 from .registry import Registry
@@ -55,7 +58,10 @@ class AsyncContainer:
         if context:
             for key, value in context.items():
                 if not isinstance(key, DependencyKey):
-                    key = DependencyKey(key, DEFAULT_COMPONENT)
+                    key = DependencyKey(  # noqa: PLW2901
+                        key,
+                        DEFAULT_COMPONENT,
+                    )
                 self._context[key] = value
         self._cache = {**self._context}
         self.parent_container = parent_container
@@ -97,7 +103,7 @@ class AsyncContainer:
         :return: async context manager for inner scope
         """
         if not self.child_registries:
-            raise ValueError("No child scopes found")
+            raise NoChildScopesError
 
         child = AsyncContainer(
             *self.child_registries,
@@ -108,7 +114,7 @@ class AsyncContainer:
         if scope is None:
             while child.registry.scope.skip:
                 if not child.child_registries:
-                    raise ValueError("No non-skipped scopes found.")
+                    raise NoNonSkippedScopesError
                 child = AsyncContainer(
                     *child.child_registries,
                     parent_container=child,
@@ -119,8 +125,7 @@ class AsyncContainer:
         else:
             while child.registry.scope is not scope:
                 if not child.child_registries:
-                    raise ValueError(f"Cannot find {scope} as a child of "
-                                     f"current {self.registry.scope}")
+                    raise ChildScopeNotFoundError(scope, self.registry.scope)
                 child = AsyncContainer(
                     *child.child_registries,
                     parent_container=child,
@@ -199,7 +204,7 @@ class AsyncContainer:
             except Exception as err:  # noqa: BLE001
                 errors.append(err)
         if errors:
-            raise ExitError("Cleanup context errors", errors)
+            raise ExitError("Cleanup context errors", errors)  # noqa: TRY003
 
 
 class AsyncContextWrapper:
diff --git a/src/dishka/container.py b/src/dishka/container.py
index 3492f5a..5bf0cbc 100644
--- a/src/dishka/container.py
+++ b/src/dishka/container.py
@@ -16,8 +16,11 @@ from .context_proxy import ContextProxy
 from .dependency_source import Factory
 from .entities.validation_settigs import DEFAULT_VALIDATION, ValidationSettings
 from .exceptions import (
+    ChildScopeNotFoundError,
     ExitError,
+    NoChildScopesError,
     NoFactoryError,
+    NoNonSkippedScopesError,
 )
 from .provider import BaseProvider
 from .registry import Registry
@@ -55,7 +58,10 @@ class Container:
         if context:
             for key, value in context.items():
                 if not isinstance(key, DependencyKey):
-                    key = DependencyKey(key, DEFAULT_COMPONENT)
+                    key = DependencyKey(  # noqa: PLW2901
+                        key,
+                        DEFAULT_COMPONENT,
+                    )
                 self._context[key] = value
         self._cache = {**self._context}
         self.parent_container = parent_container
@@ -97,7 +103,7 @@ class Container:
         :return: context manager for inner scope
         """
         if not self.child_registries:
-            raise ValueError("No child scopes found")
+            raise NoChildScopesError
         child = Container(
             *self.child_registries,
             parent_container=self,
@@ -107,7 +113,7 @@ class Container:
         if scope is None:
             while child.registry.scope.skip:
                 if not child.child_registries:
-                    raise ValueError("No non-skipped scopes found.")
+                    raise NoNonSkippedScopesError
                 child = Container(
                     *child.child_registries,
                     parent_container=child,
@@ -118,8 +124,7 @@ class Container:
         else:
             while child.registry.scope is not scope:
                 if not child.child_registries:
-                    raise ValueError(f"Cannot find {scope} as a child of "
-                                     f"current {self.registry.scope}")
+                    raise ChildScopeNotFoundError(scope, self.registry.scope)
                 child = Container(
                     *child.child_registries,
                     parent_container=child,
@@ -195,7 +200,7 @@ class Container:
                 errors.append(err)
 
         if errors:
-            raise ExitError("Cleanup context errors", errors)
+            raise ExitError("Cleanup context errors", errors)  # noqa: TRY003
 
 
 class ContextWrapper:
diff --git a/src/dishka/context_proxy.py b/src/dishka/context_proxy.py
index 4880b52..7443b9f 100644
--- a/src/dishka/context_proxy.py
+++ b/src/dishka/context_proxy.py
@@ -18,7 +18,9 @@ class ContextProxy(MutableMapping[DependencyKey, Any]):
         self._context[key] = value
 
     def __delitem__(self, key: DependencyKey) -> NoReturn:
-        raise RuntimeError("Cannot delete anything from context")
+        raise RuntimeError(  # noqa: TRY003
+            "Cannot delete anything from context",
+        )
 
     def __getitem__(self, key: DependencyKey) -> Any:
         return self._cache[key]
diff --git a/src/dishka/dependency_source/__init__.py b/src/dishka/dependency_source/__init__.py
index 18218e7..c6051ff 100644
--- a/src/dishka/dependency_source/__init__.py
+++ b/src/dishka/dependency_source/__init__.py
@@ -1,23 +1,20 @@
 __all__ = [
     "Alias",
+    "CompositeDependencySource",
     "ContextVariable",
     "Decorator",
     "DependencySource",
     "Factory",
-    "alias",
-    "context_var",
-    "decorate",
-    "from_context",
-    "provide",
-    "provide_all",
+    "context_stub",
+    "ensure_composite",
 ]
 
 from .alias import Alias
-from .composite import DependencySource
-from .context_var import ContextVariable
+from .composite import (
+    CompositeDependencySource,
+    DependencySource,
+    ensure_composite,
+)
+from .context_var import ContextVariable, context_stub
 from .decorator import Decorator
 from .factory import Factory
-from .make_alias import alias
-from .make_context_var import from_context
-from .make_decorator import decorate
-from .make_factory import provide, provide_all
diff --git a/src/dishka/dependency_source/type_match.py b/src/dishka/dependency_source/type_match.py
index 0911540..932613d 100644
--- a/src/dishka/dependency_source/type_match.py
+++ b/src/dishka/dependency_source/type_match.py
@@ -5,6 +5,7 @@ from collections.abc import Sequence
 from typing import Any, ForwardRef, TypeVar, get_args, get_origin
 
 from dishka._adaptix.type_tools.basic_utils import eval_forward_ref, is_generic
+from dishka.exception_base import DishkaError
 
 
 def eval_maybe_forward(t: Any, wrapper: Any) -> Any:
@@ -52,7 +53,7 @@ class _TypeMatcher:
     def _is_bound_broader(self, t1: TypeVar, t2: Any) -> bool:
         bound1 = eval_maybe_forward(t1.__bound__, t1)
         if get_origin(bound1):
-            raise ValueError(f"Generic bounds are not supported: {t1}")
+            raise UnsupportedGenericBoundsError(t1)
         if not isinstance(t2, TypeVar):
             if get_origin(t2):
                 t2 = get_origin(t2)
@@ -102,3 +103,11 @@ def get_typevar_replacement(t1: Any, t2: Any) -> dict[TypeVar, Any]:
     matcher = _TypeMatcher()
     matcher.is_broader_or_same_type(t1, t2)
     return matcher.type_var_subst
+
+
+class UnsupportedGenericBoundsError(TypeError, DishkaError):
+    def __init__(self, bounds: TypeVar) -> None:
+        self.bounds = bounds
+
+    def __str__(self) -> str:
+        return f"Generic bounds are not supported: {self.bounds}"
diff --git a/src/dishka/entities/with_parents.py b/src/dishka/entities/with_parents.py
index 5efbcb5..d157c20 100644
--- a/src/dishka/entities/with_parents.py
+++ b/src/dishka/entities/with_parents.py
@@ -21,6 +21,8 @@ from dishka.entities.provides_marker import ProvideMultiple
 
 __all__ = ["ParentsResolver", "WithParents"]
 
+from dishka.exception_base import DishkaError
+
 IGNORE_TYPES: Final = (
     type,
     object,
@@ -84,9 +86,7 @@ def create_type_vars_map(obj: TypeHint) -> dict[TypeHint, TypeHint]:
 class ParentsResolver:
     def get_parents(self, child_type: TypeHint) -> list[TypeHint]:
         if is_ignored_type(strip_alias(child_type)):
-            raise ValueError(
-                f"The starting class {child_type!r} is in ignored types",
-            )
+            raise StartingClassIgnoredError(child_type)
         if is_parametrized(child_type) or has_orig_bases(child_type):
             return self._get_parents_for_generic(child_type)
         return self._get_parents_for_mro(child_type)
@@ -176,3 +176,11 @@ else:
             if len(parents) > 1:
                 return ProvideMultiple[tuple(parents)]
             return parents[0]
+
+
+class StartingClassIgnoredError(ValueError, DishkaError):
+    def __init__(self, hint: TypeHint) -> None:
+        self.hint = hint
+
+    def __str__(self) -> str:
+        return f"The starting class {self.hint!r} is in ignored types"
diff --git a/src/dishka/exception_base.py b/src/dishka/exception_base.py
new file mode 100644
index 0000000..88d7f89
--- /dev/null
+++ b/src/dishka/exception_base.py
@@ -0,0 +1,2 @@
+class DishkaError(Exception):
+    pass
diff --git a/src/dishka/exceptions.py b/src/dishka/exceptions.py
index eef7036..9cf96c0 100644
--- a/src/dishka/exceptions.py
+++ b/src/dishka/exceptions.py
@@ -1,9 +1,13 @@
 from collections.abc import Sequence
 
-from dishka.entities.key import DependencyKey
-from .dependency_source import Factory
-from .text_rendering import get_name
-from .text_rendering.suggestion import render_suggestions_for_missing
+from dishka.dependency_source import Factory
+from dishka.entities.factory_type import FactoryData
+from dishka.exception_base import DishkaError
+from dishka.text_rendering import get_name
+from dishka.text_rendering.path import PathRenderer
+from dishka.text_rendering.suggestion import render_suggestions_for_missing
+from .entities.key import DependencyKey
+from .entities.scope import BaseScope
 
 try:
     from builtins import (  # type: ignore[attr-defined, unused-ignore]
@@ -15,36 +19,9 @@ except ImportError:
         ExceptionGroup,
     )
 
-from .text_rendering.path import PathRenderer
-
 _renderer = PathRenderer()
 
 
-class DishkaError(Exception):
-    pass
-
-
-class InvalidGraphError(DishkaError):
-    pass
-
-
-class UnknownScopeError(InvalidGraphError):
-    pass
-
-
-class CycleDependenciesError(InvalidGraphError):
-    def __init__(self, path: Sequence[Factory]) -> None:
-        self.path = path
-
-    def __str__(self) -> str:
-        if len(self.path) == 1:
-            hint = " Did you mean @decorate instead of @provide?"
-        else:
-            hint = ""
-        details = _renderer.render(self.path)
-        return f"Cycle dependencies detected.{hint}\n{details}"
-
-
 class ExitError(
     ExceptionGroup[Exception],  # type: ignore[misc, unused-ignore]
     DishkaError,
@@ -52,11 +29,15 @@ class ExitError(
     pass
 
 
+class NoContextValueError(DishkaError):
+    pass
+
+
 class UnsupportedFactoryError(DishkaError):
     pass
 
 
-class NoContextValueError(DishkaError):
+class InvalidGraphError(DishkaError):
     pass
 
 
@@ -102,6 +83,78 @@ class NoFactoryError(DishkaError):
             ) + suggestion
 
 
+class AliasedFactoryNotFoundError(ValueError, DishkaError):
+    def __init__(
+            self, dependency: DependencyKey, alias: FactoryData,
+    ) -> None:
+        self.dependency = dependency
+        self.alias_provider = alias.provides
+
+    def __str__(self) -> str:
+        return (
+            f"Factory for {self.dependency} "
+            f"aliased from {self.alias_provider} is not found"
+        )
+
+
+class NoChildScopesError(ValueError, DishkaError):
+    def __str__(self) -> str:
+        return "No child scopes found"
+
+
+class NoNonSkippedScopesError(ValueError, DishkaError):
+    def __str__(self) -> str:
+        return "No non-skipped scopes found."
+
+
+class ChildScopeNotFoundError(ValueError, DishkaError):
+    def __init__(
+            self,
+            assumed_child_scope: BaseScope | None,
+            current_scope: BaseScope | None,
+    ) -> None:
+        self.child_scope = assumed_child_scope
+        self.current_scope = current_scope
+
+    def __str__(self) -> str:
+        return (
+            f"Cannot find {self.child_scope} as a "
+            f"child of current {self.current_scope}"
+        )
+
+
+class UnknownScopeError(InvalidGraphError):
+    def __init__(
+            self,
+            scope: BaseScope | None,
+            expected: type[BaseScope],
+            extend_message: str = "",
+    ) -> None:
+        self.scope = scope
+        self.expected = expected
+        self.extend_message = extend_message
+
+    def __str__(self) -> str:
+        return " ".join((
+            f"Scope {self.scope} is unknown, "
+            f"expected one of {self.expected}",
+            self.extend_message,
+        ))
+
+
+class CycleDependenciesError(InvalidGraphError):
+    def __init__(self, path: Sequence[Factory]) -> None:
+        self.path = path
+
+    def __str__(self) -> str:
+        if len(self.path) == 1:
+            hint = " Did you mean @decorate instead of @provide?"
+        else:
+            hint = ""
+        details = _renderer.render(self.path)
+        return f"Cycle dependencies detected.{hint}\n{details}"
+
+
 class GraphMissingFactoryError(NoFactoryError, InvalidGraphError):
     pass
 
@@ -136,4 +189,3 @@ class NothingOverriddenError(InvalidGraphError):
             f" * Try removing override=True from {name}\n"
             f" * Check the order of providers\n"
         )
-
diff --git a/src/dishka/provider/__init__.py b/src/dishka/provider/__init__.py
new file mode 100644
index 0000000..2f95d96
--- /dev/null
+++ b/src/dishka/provider/__init__.py
@@ -0,0 +1,17 @@
+__all__ = [
+    "BaseProvider",
+    "Provider",
+    "ProviderWrapper",
+    "alias",
+    "decorate",
+    "from_context",
+    "provide",
+    "provide_all",
+]
+
+from .base_provider import BaseProvider, ProviderWrapper
+from .make_alias import alias
+from .make_context_var import from_context
+from .make_decorator import decorate
+from .make_factory import provide, provide_all
+from .provider import Provider
diff --git a/src/dishka/provider/base_provider.py b/src/dishka/provider/base_provider.py
new file mode 100644
index 0000000..ec2ea4f
--- /dev/null
+++ b/src/dishka/provider/base_provider.py
@@ -0,0 +1,25 @@
+from dishka.dependency_source import (
+    Alias,
+    ContextVariable,
+    Decorator,
+    Factory,
+)
+from dishka.entities.component import Component
+
+
+class BaseProvider:
+    def __init__(self, component: Component | None) -> None:
+        if component is not None:
+            self.component = component
+        self.factories: list[Factory] = []
+        self.aliases: list[Alias] = []
+        self.decorators: list[Decorator] = []
+        self.context_vars: list[ContextVariable] = []
+
+
+class ProviderWrapper(BaseProvider):
+    def __init__(self, component: Component, provider: BaseProvider) -> None:
+        super().__init__(component)
+        self.factories.extend(provider.factories)
+        self.aliases.extend(provider.aliases)
+        self.decorators.extend(provider.decorators)
diff --git a/src/dishka/provider/exceptions.py b/src/dishka/provider/exceptions.py
new file mode 100644
index 0000000..c1f9def
--- /dev/null
+++ b/src/dishka/provider/exceptions.py
@@ -0,0 +1,143 @@
+from collections.abc import Callable, Sequence
+from typing import Any
+
+from dishka.exception_base import DishkaError
+from dishka.text_rendering import get_name
+
+
+class NotAFactoryError(TypeError, DishkaError):
+    def __init__(self, attempted_factory_type: type) -> None:
+        self.type = attempted_factory_type
+
+    def __str__(self) -> str:
+        return f"Cannot use {self.type} as a factory"
+
+
+class UnsupportedGeneratorReturnTypeError(TypeError, DishkaError):
+    def __init__(
+            self,
+            name: str,
+            guess: str,
+            guessed_args: str,
+            *,
+            is_async: bool = False,
+    ) -> None:
+        self.name = name
+        self.guess = guess
+        self.guessed_args = guessed_args
+        self.is_async = is_async
+
+    def __str__(self) -> str:
+        gen_type = "async generator" if self.is_async else "generator"
+        return (
+            f"Unsupported return type `{self.name}` for {gen_type}. "
+            f"Did you mean {self.guess}[{self.guessed_args}]?"
+        )
+
+
+class MissingHintsError(ValueError, DishkaError):
+    def __init__(
+            self,
+            source: Any,
+            missing_hints: Sequence[str],
+            *,
+            append_init: bool = False,
+    ) -> None:
+        self.source = source
+        self.missing = missing_hints
+        self.append_init = append_init
+
+    def __str__(self) -> str:
+        name = get_name(self.source, include_module=True)
+        if self.append_init:
+            name += ".__init__"
+        missing = ", ".join(self.missing)
+        return (
+            f"Failed to analyze `{name}`.\n"
+            f"Some parameters do not have type hints: {missing}\n"
+        )
+
+
+class UndefinedTypeAnalysisError(NameError, DishkaError):
+    def __init__(
+            self,
+            source: Any,
+            type_name: str,
+    ) -> None:
+        self.source = source
+        self.type_name = type_name
+        self.message = self._construct_message()
+        super().__init__(self.message, name=type_name)
+
+    def _construct_message(self) -> str:
+        name = get_name(self.source, include_module=True)
+        return (
+            f"Failed to analyze `{name}`. \n"
+            f"Type '{self.type_name}' is not defined. \n\n"
+            f"If your are using `if TYPE_CHECKING` "
+            f"to import '{self.type_name}' "
+            f"then try removing it. \n"
+            f"Or, create a separate factory with all types imported."
+        )
+
+    def __str__(self) -> str:
+        return self.message
+
+
+class MissingReturnHintError(ValueError, DishkaError):
+    def __init__(self, source: Any) -> None:
+        self.source = source
+
+    def __str__(self) -> str:
+        name = get_name(self.source, include_module=True)
+        return (
+            f"Failed to analyze `{name}`. \n"
+            f"Missing return type hint."
+        )
+
+
+class NoScopeSetInProvideError(ValueError, DishkaError):
+    def __init__(
+            self,
+            provides_name: str,
+            src_name: str,
+            provider_name: str,
+    ) -> None:
+        self.provides_name = provides_name
+        self.src_name = src_name
+        self.provider_name = provider_name
+
+    def __str__(self) -> str:
+        return (
+            f"No scope is set for {self.provides_name}.\n"
+            f"Set in provide() call for {self.src_name} or "
+            f"within {self.provider_name}"
+        )
+
+
+class NoScopeSetInContextError(ValueError, DishkaError):
+    def __init__(
+            self,
+            provides_name: str,
+            provider_name: str,
+    ) -> None:
+        self.provides_name = provides_name
+        self.provider_name = provider_name
+
+    def __str__(self) -> str:
+        return (
+            f"No scope is set for {self.provides_name}.\n"
+            f"Set in from_context() call or within {self.provider_name}"
+        )
+
+
+class IndependentDecoratorError(ValueError, DishkaError):
+    def __init__(self, source: Callable[..., Any] | type) -> None:
+        self.source = source
+
+    def __str__(self) -> str:
+        name = get_name(self.source, include_module=True)
+        return (
+            f"Decorator {name} does not depends on provided type.\n"
+            f"Did you mean @provide instead of @decorate?"
+        )
diff --git a/src/dishka/dependency_source/make_alias.py b/src/dishka/provider/make_alias.py
similarity index 78%
rename from src/dishka/dependency_source/make_alias.py
rename to src/dishka/provider/make_alias.py
index 91821b7..d4a89b1 100644
--- a/src/dishka/dependency_source/make_alias.py
+++ b/src/dishka/provider/make_alias.py
@@ -1,9 +1,12 @@
 from typing import Any
 
+from dishka.dependency_source import (
+    Alias,
+    CompositeDependencySource,
+    ensure_composite,
+)
 from dishka.entities.component import Component
 from dishka.entities.key import DependencyKey
-from .alias import Alias
-from .composite import CompositeDependencySource, ensure_composite
 from .unpack_provides import unpack_alias
 
 
@@ -16,7 +19,9 @@ def alias(
         override: bool = False,
 ) -> CompositeDependencySource:
     if component is provides is None:
-        raise ValueError("Either component or provides must be set in alias")
+        raise ValueError(  # noqa: TRY003
+            "Either component or provides must be set in alias",
+        )
     if provides is None:
         provides = source
 
diff --git a/src/dishka/dependency_source/make_context_var.py b/src/dishka/provider/make_context_var.py
similarity index 86%
rename from src/dishka/dependency_source/make_context_var.py
rename to src/dishka/provider/make_context_var.py
index 13da74d..d9969bd 100644
--- a/src/dishka/dependency_source/make_context_var.py
+++ b/src/dishka/provider/make_context_var.py
@@ -1,10 +1,13 @@
 from typing import Any
 
+from dishka.dependency_source import (
+    CompositeDependencySource,
+    ContextVariable,
+    context_stub,
+)
 from dishka.entities.component import DEFAULT_COMPONENT
 from dishka.entities.key import DependencyKey
 from dishka.entities.scope import BaseScope
-from .composite import CompositeDependencySource
-from .context_var import ContextVariable, context_stub
 
 
 def from_context(
diff --git a/src/dishka/dependency_source/make_decorator.py b/src/dishka/provider/make_decorator.py
similarity index 83%
rename from src/dishka/dependency_source/make_decorator.py
rename to src/dishka/provider/make_decorator.py
index 33736d4..2f8ee4a 100644
--- a/src/dishka/dependency_source/make_decorator.py
+++ b/src/dishka/provider/make_decorator.py
@@ -1,9 +1,12 @@
 from collections.abc import Callable
 from typing import Any, overload
 
-from dishka.text_rendering import get_name
-from .composite import CompositeDependencySource, ensure_composite
-from .decorator import Decorator
+from dishka.dependency_source import (
+    CompositeDependencySource,
+    Decorator,
+    ensure_composite,
+)
+from .exceptions import IndependentDecoratorError
 from .make_factory import make_factory
 from .unpack_provides import unpack_decorator
 
@@ -29,11 +32,7 @@ def _decorate(
         decorator.provides not in decorator.factory.kw_dependencies.values()
         and decorator.provides not in decorator.factory.dependencies
     ):
-        name = get_name(source, include_module=True)
-        raise ValueError(
-            f"Decorator {name} does not depends on provided type.\n"
-            f"Did you mean @provide instead of @decorate?",
-        )
+        raise IndependentDecoratorError(source)
 
     composite.dependency_sources.extend(unpack_decorator(decorator))
     return composite
diff --git a/src/dishka/dependency_source/make_factory.py b/src/dishka/provider/make_factory.py
similarity index 87%
rename from src/dishka/dependency_source/make_factory.py
rename to src/dishka/provider/make_factory.py
index 79e16ed..22a0449 100644
--- a/src/dishka/dependency_source/make_factory.py
+++ b/src/dishka/provider/make_factory.py
@@ -45,6 +45,11 @@ from dishka._adaptix.type_tools.generic_resolver import (
     GenericResolver,
     MembersStorage,
 )
+from dishka.dependency_source import (
+    CompositeDependencySource,
+    Factory,
+    ensure_composite,
+)
 from dishka.entities.factory_type import FactoryType
 from dishka.entities.key import (
     dependency_key_to_hint,
@@ -54,8 +59,13 @@ from dishka.entities.key import (
 from dishka.entities.provides_marker import ProvideMultiple
 from dishka.entities.scope import BaseScope
 from dishka.text_rendering import get_name
-from .composite import CompositeDependencySource, ensure_composite
-from .factory import Factory
+from .exceptions import (
+    MissingHintsError,
+    MissingReturnHintError,
+    NotAFactoryError,
+    UndefinedTypeAnalysisError,
+    UnsupportedGeneratorReturnTypeError,
+)
 from .unpack_provides import unpack_factory
 
 _empty = signature(lambda a: 0).parameters["a"].annotation
@@ -130,10 +140,7 @@ def _async_generator_result(hint: Any) -> Any:
         args = name
         guess = "AsyncIterable"
 
-    raise TypeError(
-        f"Unsupported return type `{name}` for async generator. "
-        f"Did you mean {guess}[{args}]?",
-    )
+    raise UnsupportedGeneratorReturnTypeError(name, guess, args, is_async=True)
 
 
 def _generator_result(hint: Any) -> Any:
@@ -163,10 +170,7 @@ def _generator_result(hint: Any) -> Any:
         args = name
         guess = "Iterable"
 
-    raise TypeError(
-        f"Unsupported return type `{name}` for generator. "
-        f"Did you mean {guess}[{args}]?",
-    )
+    raise UnsupportedGeneratorReturnTypeError(name, guess, args)
 
 
 def _clean_result_hint(
@@ -209,27 +213,14 @@ def _make_factory_by_class(
         source = get_args(source)[0]
     init = strip_alias(source).__init__
     if missing_hints := _params_without_hints(init, skip_self=True):
-        name = get_name(source, include_module=True) + ".__init__"
-        missing = ", ".join(missing_hints)
-        raise ValueError(
-            f"Failed to analyze `{name}`. \n"
-            f"Some parameters do not have type hints: {missing}\n",
-        )
+        raise MissingHintsError(source, missing_hints, append_init=True)
     # we need to fix concrete generics and normal classes as well
     # as classes can be children of concrete generics
     res = GenericResolver(_get_init_members)
     try:
         hints = dict(res.get_resolved_members(source).members)
     except NameError as e:
-        name = get_name(source, include_module=True) + ".__init__"
-        raise NameError(
-            f"Failed to analyze `{name}`. \n"
-            f"Type '{e.name}' is not defined\n\n"
-            f"If your are using `if TYPE_CHECKING` to import '{e.name}' "
-            f"then try removing it. \n"
-            f"Or, create a separate factory with all types imported.",
-            name=e.name,
-        ) from e
+        raise UndefinedTypeAnalysisError(source, e.name) from e
 
     hints.pop("return", _empty)
     params = signature(init).parameters
@@ -283,12 +274,7 @@ def _make_factory_by_function(
     raw_source = unwrap(cast(Callable[..., Any], source))
     missing_hints = _params_without_hints(raw_source, skip_self=is_in_class)
     if missing_hints:
-        name = get_name(source, include_module=True)
-        missing = ", ".join(missing_hints)
-        raise ValueError(
-            f"Failed to analyze `{name}`. \n"
-            f"Some parameters do not have type hints: {missing}\n",
-        )
+        raise MissingHintsError(source, missing_hints)
 
     params = signature(raw_source).parameters
     factory_type = _guess_factory_type(raw_source)
@@ -296,15 +282,7 @@ def _make_factory_by_function(
     try:
         hints = get_type_hints(source, include_extras=True)
     except NameError as e:
-        name = get_name(source, include_module=True)
-        raise NameError(
-            f"Failed to analyze `{name}`. \n"
-            f"Type '{e.name}' is not defined. \n\n"
-            f"If your are using `if TYPE_CHECKING` to import '{e.name}' "
-            f"then try removing it. \n"
-            f"Or, create a separate factory with all types imported.",
-            name=e.name,
-        ) from e
+        raise UndefinedTypeAnalysisError(source, e.name) from e
     if is_in_class:
         self = next(iter(params.values()), None)
         if self and self.name not in hints:
@@ -324,9 +302,7 @@ def _make_factory_by_function(
 
     if not provides:
         if possible_dependency is _empty:
-            name = get_name(source, include_module=True)
-            raise ValueError(f"Failed to analyze `{name}`. \n"
-                             f"Missing return type hint.")
+            raise MissingReturnHintError(source)
         try:
             provides = _clean_result_hint(factory_type, possible_dependency)
         except TypeError as e:
@@ -354,25 +330,12 @@ def _make_factory_by_static_method(
         override: bool,
 ) -> Factory:
     if missing_hints := _params_without_hints(source, skip_self=False):
-        name = get_name(source, include_module=True)
-        missing = ", ".join(missing_hints)
-        raise ValueError(
-            f"Failed to analyze `{name}`. \n"
-            f"Some parameters do not have type hints: {missing}\n",
-        )
+        raise MissingHintsError(source, missing_hints)
     factory_type = _guess_factory_type(source.__wrapped__)
     try:
         hints = get_type_hints(source, include_extras=True)
     except NameError as e:
-        name = get_name(source, include_module=True)
-        raise NameError(
-            f"Failed to analyze `{name}`. \n"
-            f"Type '{e.name}' is not defined. \n\n"
-            f"If your are using `if TYPE_CHECKING` to import '{e.name}' "
-            f"then try removing it. \n"
-            f"Or, create a separate factory with all types imported.",
-            name=e.name,
-        ) from e
+        raise UndefinedTypeAnalysisError(source, e.name) from e
 
     possible_dependency = hints.pop("return", _empty)
 
@@ -386,9 +349,7 @@ def _make_factory_by_static_method(
 
     if not provides:
         if possible_dependency is _empty:
-            name = get_name(source, include_module=True)
-            raise ValueError(f"Failed to analyze `{name}`. \n"
-                             f"Missing return type hint.")
+            raise MissingReturnHintError(source)
         try:
             provides = _clean_result_hint(factory_type, possible_dependency)
         except TypeError as e:
@@ -514,7 +475,7 @@ def make_factory(
             override=override,
         )
     else:
-        raise TypeError(f"Cannot use {type(source)} as a factory")
+        raise NotAFactoryError(type(source))
 
 
 def _provide(
diff --git a/src/dishka/provider.py b/src/dishka/provider/provider.py
similarity index 80%
rename from src/dishka/provider.py
rename to src/dishka/provider/provider.py
index 73f6d6c..e4f38e2 100644
--- a/src/dishka/provider.py
+++ b/src/dishka/provider/provider.py
@@ -1,23 +1,26 @@
-from __future__ import annotations
-
 import inspect
 from collections.abc import Callable, Sequence
 from typing import Any, TypeGuard
 
-from dishka.entities.component import DEFAULT_COMPONENT, Component
-from dishka.entities.scope import BaseScope
-from .dependency_source import (
+from dishka.dependency_source import (
     Alias,
+    CompositeDependencySource,
     ContextVariable,
     Decorator,
     DependencySource,
     Factory,
-    alias,
-    from_context,
 )
-from .dependency_source.composite import CompositeDependencySource
-from .dependency_source.make_decorator import decorate_on_instance
-from .dependency_source.make_factory import (
+from dishka.entities.component import DEFAULT_COMPONENT, Component
+from dishka.entities.scope import BaseScope
+from .base_provider import BaseProvider, ProviderWrapper
+from .exceptions import (
+    NoScopeSetInContextError,
+    NoScopeSetInProvideError,
+)
+from .make_alias import alias
+from .make_context_var import from_context
+from .make_decorator import decorate_on_instance
+from .make_factory import (
     provide_all_on_instance,
     provide_on_instance,
 )
@@ -29,15 +32,6 @@ def is_dependency_source(
     return isinstance(attribute, CompositeDependencySource)
 
 
-class BaseProvider:
-    def __init__(self, component: Component | None) -> None:
-        if component is not None:
-            self.component = component
-        self.factories: list[Factory] = []
-        self.aliases: list[Alias] = []
-        self.decorators: list[Decorator] = []
-        self.context_vars: list[ContextVariable] = []
-
 
 class Provider(BaseProvider):
     """
@@ -110,22 +104,19 @@ class Provider(BaseProvider):
                 self.aliases.append(source)
             if isinstance(source, Factory):
                 if source.scope is None:
-                    src_name = self._source_name(source)
-                    provides_name = self._provides_name(source)
-                    raise ValueError(
-                        f"No scope is set for {provides_name}.\n"
-                        f"Set in provide() call for {src_name} or "
-                        f"within {self._name()}",
+                    raise NoScopeSetInProvideError(
+                        self._provides_name(source),
+                        self._source_name(source),
+                        self._name(),
                     )
                 self.factories.append(source)
             if isinstance(source, Decorator):
                 self.decorators.append(source)
             if isinstance(source, ContextVariable):
                 if source.scope is None:
-                    provides_name = self._provides_name(source)
-                    raise ValueError(
-                        f"No scope is set for {provides_name}.\n"
-                        f"Set in from_context() call or within {self._name()}",
+                    raise NoScopeSetInContextError(
+                        self._provides_name(source),
+                        self._name(),
                     )
                 self.context_vars.append(source)
 
@@ -224,11 +215,3 @@ class Provider(BaseProvider):
             sources=composite.dependency_sources,
         )
         return composite
-
-
-class ProviderWrapper(BaseProvider):
-    def __init__(self, component: Component, provider: Provider) -> None:
-        super().__init__(component)
-        self.factories.extend(provider.factories)
-        self.aliases.extend(provider.aliases)
-        self.decorators.extend(provider.decorators)
diff --git a/src/dishka/dependency_source/unpack_provides.py b/src/dishka/provider/unpack_provides.py
similarity index 94%
rename from src/dishka/dependency_source/unpack_provides.py
rename to src/dishka/provider/unpack_provides.py
index ed1b62b..2457e03 100644
--- a/src/dishka/dependency_source/unpack_provides.py
+++ b/src/dishka/provider/unpack_provides.py
@@ -1,12 +1,14 @@
 from collections.abc import Sequence
 from typing import get_args, get_origin
 
+from dishka.dependency_source import (
+    Alias,
+    Decorator,
+    DependencySource,
+    Factory,
+)
 from dishka.entities.key import DependencyKey
 from dishka.entities.provides_marker import ProvideMultiple
-from .alias import Alias
-from .composite import DependencySource
-from .decorator import Decorator
-from .factory import Factory
 
 
 def unpack_factory(factory: Factory) -> Sequence[DependencySource]:
diff --git a/src/dishka/registry_builder.py b/src/dishka/registry_builder.py
index 8522a68..27d802a 100644
--- a/src/dishka/registry_builder.py
+++ b/src/dishka/registry_builder.py
@@ -15,6 +15,7 @@ from .entities.key import DependencyKey
 from .entities.scope import BaseScope, InvalidScopes, Scope
 from .entities.validation_settigs import ValidationSettings
 from .exceptions import (
+    AliasedFactoryNotFoundError,
     CycleDependenciesError,
     GraphMissingFactoryError,
     ImplicitOverrideDetectedError,
@@ -145,10 +146,7 @@ class RegistryBuilder:
         for provider in self.providers:
             for factory in provider.factories:
                 if not isinstance(factory.scope, self.scopes):
-                    raise UnknownScopeError(
-                        f"Scope {factory.scope} is unknown, "
-                        f"expected one of {self.scopes}",
-                    )
+                    raise UnknownScopeError(factory.scope, self.scopes)
                 provides = factory.provides.with_component(provider.component)
                 self.dependency_scopes[provides] = factory.scope
             for context_var in provider.context_vars:
@@ -342,10 +340,7 @@ class RegistryBuilder:
         dependency = alias.dependencies[0]
         factory = registry.get_factory(dependency)
         if factory is None:
-            raise ValueError(
-                f"Factory for {dependency} "
-                f"aliased from {alias.provides} is not found",
-            )
+            raise AliasedFactoryNotFoundError(dependency, alias)
         return factory.source is decorator.factory.source
 
     def _decorate_factory(
@@ -356,7 +351,9 @@ class RegistryBuilder:
     ) -> None:
         provides = old_factory.provides
         if provides.component is None:
-            raise ValueError(f"Unexpected empty component for {provides}")
+            raise ValueError(  # noqa: TRY003
+                f"Unexpected empty component for {provides}",
+            )
         if (
             old_factory.type is FactoryType.ALIAS
                 and self._is_alias_decorated(decorator, registry, old_factory)
@@ -366,12 +363,12 @@ class RegistryBuilder:
         decorated_component = f"{DECORATED_COMPONENT_PREFIX}{depth}"
         self.decorator_depth[provides] += 1
         if old_factory is None:
-            raise InvalidGraphError(
+            raise InvalidGraphError(  # noqa: TRY003
                 "Cannot apply decorator because there is"
                 f"no factory for {provides}",
             )
         if old_factory.type is FactoryType.CONTEXT:
-            raise InvalidGraphError(
+            raise InvalidGraphError(  # noqa: TRY003
                 f"Cannot apply decorator to context data {provides}",
             )
         old_factory.provides = DependencyKey(
@@ -392,9 +389,11 @@ class RegistryBuilder:
     ) -> None:
         if context_var.scope is None:
             raise UnknownScopeError(
-                f"Scope {context_var.scope} is unknown, "
-                f"expected one of {self.scopes}. Define it"
-                f"explicitly in Provider or from_context",
+                context_var.scope,
+                self.scopes,
+                extend_message=(
+                    "Define it explicitly in Provider or from_context"
+                ),
             )
         registry = self.registries[context_var.scope]
         for component in self.components:


Test patch:
diff --git a/tests/unit/container/test_context_vars.py b/tests/unit/container/test_context_vars.py
index 74c66e6..d9603c6 100644
--- a/tests/unit/container/test_context_vars.py
+++ b/tests/unit/container/test_context_vars.py
@@ -6,11 +6,11 @@ from dishka import (
     Provider,
     Scope,
     decorate,
+    from_context,
     make_async_container,
     make_container,
     provide,
 )
-from dishka.dependency_source import from_context
 from dishka.exceptions import InvalidGraphError, NoContextValueError
 
 
diff --git a/tests/unit/container/test_dynamic.py b/tests/unit/container/test_dynamic.py
index 66c226b..ad4cc43 100644
--- a/tests/unit/container/test_dynamic.py
+++ b/tests/unit/container/test_dynamic.py
@@ -1,7 +1,13 @@
 from typing import NewType
 
-from dishka import Container, Provider, Scope, make_container, provide
-from dishka.dependency_source import from_context
+from dishka import (
+    Container,
+    Provider,
+    Scope,
+    from_context,
+    make_container,
+    provide,
+)
 
 Request = NewType("Request", int)
 
diff --git a/tests/unit/test_provider.py b/tests/unit/test_provider.py
index c5b0e67..6cc04e3 100644
--- a/tests/unit/test_provider.py
+++ b/tests/unit/test_provider.py
@@ -4,12 +4,12 @@ from typing import Any, Protocol
 import pytest
 
 from dishka import Provider, Scope, alias, decorate, make_container, provide
-from dishka.dependency_source.make_factory import make_factory, provide_all
 from dishka.entities.factory_type import FactoryType
 from dishka.entities.key import (
     hint_to_dependency_key,
     hints_to_dependency_keys,
 )
+from dishka.provider.make_factory import make_factory, provide_all
 from .sample_providers import (
     ClassA,
     async_func_a,
diff --git a/tests/unit/text_rendering/test_path.py b/tests/unit/text_rendering/test_path.py
index 353544d..9736a1b 100644
--- a/tests/unit/text_rendering/test_path.py
+++ b/tests/unit/text_rendering/test_path.py
@@ -2,7 +2,7 @@ import textwrap
 
 import pytest
 
-from dishka.dependency_source.make_factory import make_factory
+from dishka.provider.make_factory import make_factory
 from dishka.text_rendering.path import PathRenderer