From 020992703bed856cdee3dac6452414e6c27881ac Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Fri, 9 Oct 2026 17:19:01 +0300 Subject: [PATCH] fix: skip scope checks on Container injection in validate() (#612) container_provider resolves the current container whatever its scope, so validate() no longer checks scope on edges that end at it. Its display name in error chains is Container. --- modern_di/dependency_graph.py | 2 ++ modern_di/providers/abstract.py | 8 +++++++- modern_di/providers/container_provider.py | 8 ++++++++ tests/test_custom_scope.py | 22 ++++++++++++++++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/modern_di/dependency_graph.py b/modern_di/dependency_graph.py index ddf5ad00..a1394a2c 100644 --- a/modern_di/dependency_graph.py +++ b/modern_di/dependency_graph.py @@ -184,6 +184,8 @@ def collect_errors(registry: "ProvidersRegistry") -> list[Exception]: if type(event) is Edge: parent, name, dep = event dependency_chain = terminal_chain(dep, registry) + if dependency_chain[-1]._ignores_scope: # noqa: SLF001 + continue dependency_scope = dependency_chain[-1].scope parent_scope = effective_scope(parent, registry) if dependency_scope > parent_scope: diff --git a/modern_di/providers/abstract.py b/modern_di/providers/abstract.py index 055b5a01..793d36ed 100644 --- a/modern_di/providers/abstract.py +++ b/modern_di/providers/abstract.py @@ -18,6 +18,9 @@ class AbstractProvider(typing.Generic[types.T_co]): _takes_group_scope: typing.ClassVar[bool] = True """Whether a Group-level default scope applies. False when the effective scope is derived.""" + _ignores_scope: typing.ClassVar[bool] = False + """Whether resolution ignores this provider's scope.""" + def __init__( self, *, @@ -44,7 +47,10 @@ def provider_id(self) -> int: @property def scope(self) -> enum.IntEnum: - """The effective scope: the provider's own ``scope=``, else a Group default, else ``Scope.APP``.""" + """The declared scope: the provider's own ``scope=``, else a Group default, else ``Scope.APP``. + + A redirect such as ``Alias`` resolves at its source's scope instead. + """ if self._explicit_scope is not None: return self._explicit_scope if self._group_claim is not None: diff --git a/modern_di/providers/container_provider.py b/modern_di/providers/container_provider.py index 409650d9..f396aee1 100644 --- a/modern_di/providers/container_provider.py +++ b/modern_di/providers/container_provider.py @@ -8,9 +8,17 @@ class _ContainerProvider(AbstractProvider[typing.Any]): __slots__ = () _takes_group_scope = False + _ignores_scope = True def __init__(self) -> None: super().__init__(scope=Scope.APP, bound_type=None) + def __repr__(self) -> str: + return "container_provider" + + @property + def display_name(self) -> str: + return "Container" + container_provider = _ContainerProvider() diff --git a/tests/test_custom_scope.py b/tests/test_custom_scope.py index 86e37f87..1f9aa1f7 100644 --- a/tests/test_custom_scope.py +++ b/tests/test_custom_scope.py @@ -357,3 +357,25 @@ class DeeperGroup(Group): Container(groups=[DeeperGroup]).validate() (issue,) = exc.value.exceptions assert isinstance(issue, InvalidScopeDependencyError) + + +@dataclasses.dataclass(kw_only=True, slots=True) +class _NeedsContainer: + container: Container + + +@pytest.mark.parametrize("members", [{"APP": 1, "REQ": 2}, {"APP": 0, "REQ": 1}]) +def test_validate_accepts_container_injection_under_a_custom_scope_enum(members: dict[str, int]) -> None: + custom = enum.IntEnum("custom", members) + + class ContainerGroup(Group): + needs_container = providers.Factory(creator=_NeedsContainer, scope=custom.APP) + + container = Container(groups=[ContainerGroup], scope=custom.APP) + container.validate() + assert container.resolve(_NeedsContainer).container is container + + +def test_container_provider_displays_as_container() -> None: + assert providers.container_provider.display_name == "Container" + assert repr(providers.container_provider) == "container_provider"