Repository: reagento/dishka

Issue:
Unify validation settings

Discussion:

Patch:
diff --git a/docs/provider/provide.rst b/docs/provider/provide.rst
index 743bd80..3b8cd41 100644
--- a/docs/provider/provide.rst
+++ b/docs/provider/provide.rst
@@ -155,7 +155,7 @@ WithParents generates only one factory and many aliases and is equivalent to ``A
         c = provide(C, scope=Scope.APP, recursive=True)
 
 
-* Do you want to override the factory? To do this, specify the parameter ``override=True``. If this is not done, error ``FactoryIsNotOverriddenError`` will be thrown
+* Do you want to override the factory? To do this, specify the parameter ``override=True``. This can be checked when passing proper ``validation_settings`` when creating container.
 
 .. code-block:: python
     from dishka import WithParents, provide, Provider, Scope
diff --git a/src/dishka/__init__.py b/src/dishka/__init__.py
index 31bef40..aca426f 100644
--- a/src/dishka/__init__.py
+++ b/src/dishka/__init__.py
@@ -19,6 +19,8 @@ __all__ = [
     "provide",
     "provide_all",
     "new_scope",
+    "ValidationSettings",
+    "STRICT_VALIDATION",
 ]
 
 from .async_container import AsyncContainer, make_async_container
@@ -35,5 +37,6 @@ from .entities.depends_marker import FromDishka
 from .entities.key import DependencyKey, FromComponent
 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
diff --git a/src/dishka/async_container.py b/src/dishka/async_container.py
index 484cc25..769a4e0 100644
--- a/src/dishka/async_container.py
+++ b/src/dishka/async_container.py
@@ -13,6 +13,7 @@ from dishka.entities.scope import BaseScope, Scope
 from .container_objects import Exit
 from .context_proxy import ContextProxy
 from .dependency_source import Factory
+from .entities.validation_settigs import DEFAULT_VALIDATION, ValidationSettings
 from .exceptions import (
     ExitError,
     NoFactoryError,
@@ -196,16 +197,14 @@ def make_async_container(
         lock_factory: Callable[[], Lock] | None = Lock,
         skip_validation: bool = False,
         start_scope: BaseScope | None = None,
-        skip_override: bool = False,
-        skip_cant_override: bool = False,
+        validation_settings: ValidationSettings = DEFAULT_VALIDATION,
 ) -> AsyncContainer:
     registries = RegistryBuilder(
         scopes=scopes,
         container_type=AsyncContainer,
         providers=providers,
         skip_validation=skip_validation,
-        skip_override=skip_override,
-        skip_cant_override=skip_cant_override,
+        validation_settings=validation_settings,
     ).build()
     container = AsyncContainer(
         *registries,
diff --git a/src/dishka/container.py b/src/dishka/container.py
index 8fdc4f7..c15c4d8 100644
--- a/src/dishka/container.py
+++ b/src/dishka/container.py
@@ -13,6 +13,7 @@ from dishka.entities.scope import BaseScope, Scope
 from .container_objects import Exit
 from .context_proxy import ContextProxy
 from .dependency_source import Factory
+from .entities.validation_settigs import DEFAULT_VALIDATION, ValidationSettings
 from .exceptions import (
     ExitError,
     NoFactoryError,
@@ -194,16 +195,14 @@ def make_container(
         lock_factory: Callable[[], Lock] | None = None,
         skip_validation: bool = False,
         start_scope: BaseScope | None = None,
-        skip_override: bool = False,
-        skip_cant_override: bool = False,
+        validation_settings: ValidationSettings = DEFAULT_VALIDATION,
 ) -> Container:
     registries = RegistryBuilder(
         scopes=scopes,
         container_type=Container,
         providers=providers,
         skip_validation=skip_validation,
-        skip_override=skip_override,
-        skip_cant_override=skip_cant_override,
+        validation_settings=validation_settings,
     ).build()
     container = Container(
         *registries,
diff --git a/src/dishka/entities/validation_settigs.py b/src/dishka/entities/validation_settigs.py
new file mode 100644
index 0000000..557d099
--- /dev/null
+++ b/src/dishka/entities/validation_settigs.py
@@ -0,0 +1,16 @@
+from dataclasses import dataclass
+
+
+@dataclass
+class ValidationSettings:
+    # check if no factory found to override when set override=True
+    nothing_overridden: bool = False
+    # check if factory is overridden when set override=False
+    implicit_override: bool = False
+
+
+DEFAULT_VALIDATION = ValidationSettings()
+STRICT_VALIDATION = ValidationSettings(
+    nothing_overridden=True,
+    implicit_override=True,
+)
diff --git a/src/dishka/exceptions.py b/src/dishka/exceptions.py
index f8092d4..23af56c 100644
--- a/src/dishka/exceptions.py
+++ b/src/dishka/exceptions.py
@@ -101,33 +101,34 @@ class GraphMissingFactoryError(NoFactoryError, InvalidGraphError):
     pass
 
 
-class FactoryNotOverrideError(InvalidGraphError):
-    def __init__(self, factory: Factory) -> None:
-        self.factory = factory
+class ImplicitOverrideDetectedError(InvalidGraphError):
+    def __init__(self, new: Factory, existing: Factory) -> None:
+        self.new = new
+        self.existing = existing
 
     def __str__(self) -> str:
-        name = get_name(self.factory.source, include_module=False)
+        new_name = get_name(self.new.source, include_module=False)
+        existing_name = get_name(self.existing.source, include_module=False)
         return (
-            f"Factory {self.factory.provides} was found that "
-            "implicitly override factory.\n"
+            f"Detected multiple factories for {self.new.provides} "
+            f"while `override` flag is not set.\n"
             "Hint:\n"
-            f" * Specify the parameter override=True for {name}.\n"
-            " * Add skip_override=True to make_container"
-            " or make_async_container."
+            f" * Try specifying `override=True` for {new_name}\n"
+            f" * Try removing factory {existing_name} or {new_name}\n"
         )
 
 
-class CantOverrideFactoryError(InvalidGraphError):
+class NothingOverriddenError(InvalidGraphError):
     def __init__(self, factory: Factory) -> None:
         self.factory = factory
 
     def __str__(self) -> str:
         name = get_name(self.factory.source, include_module=False)
         return (
-            f"Can't override factory for {self.factory.provides}.\n"
+            f"Overriding factory found for {self.factory.provides}, "
+            "but there is nothing to override.\n"
             "Hint:\n"
-            f" * Remove override=True from {name}.\n"
-            " * Add skip_cant_override=True to make_container"
-            " or make_async_container."
+            f" * Try removing override=True from {name}\n"
+            f" * Check the order of providers\n"
         )
 
diff --git a/src/dishka/registry.py b/src/dishka/registry.py
index 16a9b1b..c8b405c 100644
--- a/src/dishka/registry.py
+++ b/src/dishka/registry.py
@@ -15,13 +15,14 @@ from .entities.component import DEFAULT_COMPONENT, Component
 from .entities.factory_type import FactoryType
 from .entities.key import DependencyKey
 from .entities.scope import BaseScope, InvalidScopes, Scope
+from .entities.validation_settigs import ValidationSettings
 from .exceptions import (
-    CantOverrideFactoryError,
     CycleDependenciesError,
-    FactoryNotOverrideError,
     GraphMissingFactoryError,
+    ImplicitOverrideDetectedError,
     InvalidGraphError,
     NoFactoryError,
+    NothingOverriddenError,
     UnknownScopeError,
 )
 from .factory_compiler import compile_factory
@@ -260,8 +261,7 @@ class RegistryBuilder:
             providers: Sequence[BaseProvider],
             container_type: type,
             skip_validation: bool,
-            skip_override: bool,
-            skip_cant_override: bool,
+            validation_settings: ValidationSettings,
     ) -> None:
         self.scopes = scopes
         self.providers = providers
@@ -273,9 +273,8 @@ class RegistryBuilder:
         self.container_type = container_type
         self.decorator_depth: dict[DependencyKey, int] = defaultdict(int)
         self.skip_validation = skip_validation
-        self.skip_override = skip_override
-        self.skip_cant_override = skip_cant_override
-        self.override_factories: dict[DependencyKey, Factory] = {}
+        self.validation_settings = validation_settings
+        self.processed_factories: dict[DependencyKey, Factory] = {}
 
     def _collect_components(self) -> None:
         for provider in self.providers:
@@ -326,17 +325,23 @@ class RegistryBuilder:
         factory = factory.with_component(provider.component)
         provides = factory.provides
         if (
-            not self.skip_cant_override
+            self.validation_settings.nothing_overridden
             and factory.override
-            and provides not in self.override_factories
+            and provides not in self.processed_factories
         ):
-            raise CantOverrideFactoryError(factory)
-        if not self.skip_override and provides in self.override_factories:
-            if not factory.override:
-                raise FactoryNotOverrideError(factory)
-        else:
-            self.override_factories[provides] = factory
+            raise NothingOverriddenError(factory)
+
+        if (
+            self.validation_settings.implicit_override
+            and not factory.override
+            and provides in self.processed_factories
+        ):
+            raise ImplicitOverrideDetectedError(
+                factory,
+                self.processed_factories[provides],
+            )
 
+        self.processed_factories[provides] = factory
         registry = self.registries[cast(Scope, factory.scope)]
         registry.add_factory(factory)
 


Test patch:
diff --git a/tests/unit/container/test_override.py b/tests/unit/container/test_override.py
index 65df8a3..57f98c0 100644
--- a/tests/unit/container/test_override.py
+++ b/tests/unit/container/test_override.py
@@ -1,10 +1,17 @@
 import pytest
 
 from dishka import Provider, Scope, make_container, provide, provide_all
-from dishka.exceptions import CantOverrideFactoryError, FactoryNotOverrideError
+from dishka.entities.validation_settigs import (
+    STRICT_VALIDATION,
+    ValidationSettings,
+)
+from dishka.exceptions import (
+    ImplicitOverrideDetectedError,
+    NothingOverriddenError,
+)
 
 
-def test_not_override_provide() -> None:
+def test_no_override_provide() -> None:
     class TestProvider(Provider):
         scope = Scope.APP
         provides = (
@@ -12,11 +19,15 @@ def test_not_override_provide() -> None:
             + provide(int, provides=int)
         )
 
-    with pytest.raises(FactoryNotOverrideError):
-        make_container(TestProvider())
+    with pytest.raises(ImplicitOverrideDetectedError) as e:
+        make_container(
+            TestProvider(),
+            validation_settings=STRICT_VALIDATION,
+        )
+    assert str(e.value)
 
 
-def test_skip_override_provide() -> None:
+def test_skip_no_override_provide() -> None:
     class TestProvider(Provider):
         scope = Scope.APP
         provides = (
@@ -26,11 +37,11 @@ def test_skip_override_provide() -> None:
 
     make_container(
         TestProvider(),
-        skip_override=True,
+        validation_settings=ValidationSettings(implicit_override=False),
     )
 
 
-def test_override_provide() -> None:
+def test_override_provide_ok() -> None:
     class TestProvider(Provider):
         scope = Scope.APP
         provides = (
@@ -38,7 +49,10 @@ def test_override_provide() -> None:
             + provide(int, provides=int, override=True)
         )
 
-    make_container(TestProvider())
+    make_container(
+        TestProvider(),
+        validation_settings=STRICT_VALIDATION,
+    )
 
 
 def test_not_override_provide_all() -> None:
@@ -49,8 +63,11 @@ def test_not_override_provide_all() -> None:
             + provide_all(int, str)
         )
 
-    with pytest.raises(FactoryNotOverrideError):
-        make_container(TestProvider())
+    with pytest.raises(ImplicitOverrideDetectedError):
+        make_container(
+            TestProvider(),
+            validation_settings=STRICT_VALIDATION,
+        )
 
 
 def test_override_provide_all() -> None:
@@ -61,7 +78,10 @@ def test_override_provide_all() -> None:
             + provide_all(int, str, override=True)
         )
 
-    make_container(TestProvider())
+    make_container(
+        TestProvider(),
+        validation_settings=STRICT_VALIDATION,
+    )
 
 
 def test_cant_override() -> None:
@@ -69,8 +89,12 @@ def test_cant_override() -> None:
         scope = Scope.APP
         provides = provide(int, provides=int, override=True)
 
-    with pytest.raises(CantOverrideFactoryError):
-        make_container(TestProvider())
+    with pytest.raises(NothingOverriddenError) as e:
+        make_container(
+            TestProvider(),
+            validation_settings=STRICT_VALIDATION,
+        )
+    assert str(e.value)
 
 
 def test_skip_cant_override() -> None:
@@ -80,6 +104,6 @@ def test_skip_cant_override() -> None:
 
     make_container(
         TestProvider(),
-        skip_cant_override=True,
+        validation_settings=ValidationSettings(nothing_overridden=False),
     )
 
diff --git a/tests/unit/container/test_recursive.py b/tests/unit/container/test_recursive.py
index aa2d918..0fd055c 100644
--- a/tests/unit/container/test_recursive.py
+++ b/tests/unit/container/test_recursive.py
@@ -1,6 +1,14 @@
 from typing import Protocol
 
-from dishka import AnyOf, Provider, Scope, make_container, provide, provide_all
+from dishka import (
+    AnyOf,
+    Provider,
+    Scope,
+    ValidationSettings,
+    make_container,
+    provide,
+    provide_all,
+)
 
 
 class A1:
@@ -61,7 +69,12 @@ def test_provide_all_class():
     class MyProvider(Provider):
         x = provide_all(B, C, scope=Scope.APP, recursive=True)
 
-    container = make_container(MyProvider(), skip_override=True)
+    container = make_container(
+        MyProvider(),
+        validation_settings=ValidationSettings(
+            implicit_override=False,
+        ),
+    )
     b = container.get(B)
     assert isinstance(b, B)
     assert isinstance(b.a1, A1)
@@ -75,7 +88,12 @@ def test_provide_all_class():
 def test_provide_all_instance():
     provider = Provider(scope=Scope.APP)
     provider.provide_all(B, C, recursive=True)
-    container = make_container(provider, skip_override=True)
+    container = make_container(
+        provider,
+        validation_settings=ValidationSettings(
+            implicit_override=False,
+        ),
+    )
     b = container.get(B)
     assert isinstance(b, B)
     assert isinstance(b.a1, A1)