diff --git a/docs/migration/to-4.x.md b/docs/migration/to-4.x.md index 3f7e0682..caf1b6cf 100644 --- a/docs/migration/to-4.x.md +++ b/docs/migration/to-4.x.md @@ -157,6 +157,30 @@ except* exceptions.AsyncFinalizerInSyncCloseError: ... ``` +### `Factory(cache=)` takes only a bool or a `CacheSettings` + +`cache=None` raises `TypeError`, and so does any other value that is not `True`, `False` or a +`CacheSettings`. Replace `cache=None` with `cache=False`, or drop the argument: an uncached +factory is still the default. + +### Provider internals are no longer public + +These methods were never documented and no integration calls them. In 4.0 they are private, so +code that calls them raises `AttributeError`: + +- `AbstractProvider.get_dependencies()`, `AbstractProvider.redirect_target()` and + `AbstractProvider.iter_validation_issues()`. Call `container.validate()` to check the graph. +- `Factory.wiring_plan()`, `Factory.can_call_positionally()` and `Factory.resolution_step()`. +- `Alias.find_source()`. Resolve the alias instead. +- `CacheSettings.coerce()`. Pass `True`, `False` or a `CacheSettings` to `Factory(cache=)`. + +### `AbstractProvider` is a plain class + +`AbstractProvider` no longer derives from `abc.ABC`. It never declared an abstract method, and the +provider set is closed. Only code that relied on `ABCMeta` is affected: +`AbstractProvider.register(...)` raises `AttributeError`. `isinstance(x, AbstractProvider)` works +as before for every provider, and type hints that name `AbstractProvider` need no change. + ### The 3.x deprecations are removed - `Container(validate=...)` raises `TypeError`, and `ValidateArgumentWarning` is gone with it. Drop diff --git a/docs/providers/factories.md b/docs/providers/factories.md index 3470d678..ca3f0ea8 100644 --- a/docs/providers/factories.md +++ b/docs/providers/factories.md @@ -112,7 +112,7 @@ class Dependencies(Group): ## Parameters -`Factory(creator, *, scope=Scope.APP, bound_type=UNSET, kwargs=None, cache=None, skip_creator_parsing=False)`. +`Factory(creator, *, scope=Scope.APP, bound_type=UNSET, kwargs=None, cache=False, skip_creator_parsing=False)`. The `creator` may also be passed as a keyword (`creator=`). When creating a Factory provider, you can configure several parameters: @@ -143,7 +143,7 @@ Use this to provide specific values for parameters or override automatically res ### cache -Enables caching for the provider. Pass `cache=True` to cache with default settings (no finalizer, cache cleared on close), or `cache=providers.CacheSettings(...)` to tune the finalizer and/or `clear_cache` behavior. Absent, `None`, or `False` means a fresh instance is created on every resolve. See [Lifecycle](lifecycle.md) for how caching, finalizers, and `close_async()` fit together. +Enables caching for the provider. Pass `cache=True` to cache with default settings (no finalizer, cache cleared on close), or `cache=providers.CacheSettings(...)` to tune the finalizer and/or `clear_cache` behavior. With `cache=False`, the default, a fresh instance is created on every resolve. Any other value, `None` included, raises `TypeError`. See [Lifecycle](lifecycle.md) for how caching, finalizers, and `close_async()` fit together. ### skip_creator_parsing diff --git a/modern_di/__init__.py b/modern_di/__init__.py index b95475da..9b7d0992 100644 --- a/modern_di/__init__.py +++ b/modern_di/__init__.py @@ -1,4 +1,4 @@ -from modern_di import exceptions, integrations +from modern_di import exceptions, integrations, providers from modern_di.container import Container from modern_di.group import Group from modern_di.scope import Scope diff --git a/modern_di/dependency_graph.py b/modern_di/dependency_graph.py index 8dfffb06..86e03675 100644 --- a/modern_di/dependency_graph.py +++ b/modern_di/dependency_graph.py @@ -51,14 +51,14 @@ class DependenciesError(NamedTuple): def terminal_chain( provider: "AbstractProvider[typing.Any]", container: "Container" ) -> "list[AbstractProvider[typing.Any]]": - """Follow ``redirect_target`` hops from ``provider``, ``provider`` first. + """Follow ``_redirect_target`` hops from ``provider``, ``provider`` first. A redirect cycle collapses the chain to the single provider the repeat was detected at, so ``effective_scope`` reports that provider's own scope; ``walk()`` reports the cycle itself. """ chain = [provider] seen: set[int] = set() - while (nxt := provider.redirect_target(container)) is not None: + while (nxt := provider._redirect_target(container)) is not None: # noqa: SLF001 if provider.provider_id in seen: return [provider] seen.add(provider.provider_id) @@ -174,7 +174,7 @@ def _enter( path.append(provider) yield NodeEntered(provider) try: - dependencies = provider.get_dependencies(container) + dependencies = provider._get_dependencies(container) # noqa: SLF001 except exceptions.ResolutionError as exc: yield DependenciesError(provider, exc) dependencies = {} @@ -187,7 +187,7 @@ def collect_errors(container: "Container") -> list[Exception]: for event in DependencyGraph().walk(container.providers_registry, container): match event: case NodeEntered(provider): - errors.extend(provider.iter_validation_issues(container)) + errors.extend(provider._iter_validation_issues(container)) # noqa: SLF001 case DependenciesError(_, error): errors.append(error) case Edge(parent, name, dep): diff --git a/modern_di/providers/abstract.py b/modern_di/providers/abstract.py index 436ef53e..50e2067a 100644 --- a/modern_di/providers/abstract.py +++ b/modern_di/providers/abstract.py @@ -1,4 +1,3 @@ -import abc import enum import itertools import typing @@ -13,7 +12,7 @@ _provider_id_counter = itertools.count() -class AbstractProvider(abc.ABC, typing.Generic[types.T_co]): +class AbstractProvider(typing.Generic[types.T_co]): __slots__ = ("_explicit_scope", "_group_claim", "_registered", "bound_type", "provider_id") _takes_group_scope: typing.ClassVar[bool] = True @@ -81,13 +80,13 @@ def definition_site(self) -> str | None: """``module:line`` of the provider's declaration when known; None by default (no creator).""" return None - def get_dependencies(self, container: "Container") -> dict[str, "AbstractProvider[typing.Any]"]: # noqa: ARG002 + def _get_dependencies(self, container: "Container") -> dict[str, "AbstractProvider[typing.Any]"]: # noqa: ARG002 return {} - def redirect_target(self, container: "Container") -> "AbstractProvider[typing.Any] | None": # noqa: ARG002 + def _redirect_target(self, container: "Container") -> "AbstractProvider[typing.Any] | None": # noqa: ARG002 """Return the provider this transparently forwards to, or None if resolution terminates here.""" return None - def iter_validation_issues(self, container: "Container") -> typing.Iterable[Exception]: # noqa: ARG002 + def _iter_validation_issues(self, container: "Container") -> typing.Iterable[Exception]: # noqa: ARG002 """Yield validation-time issues for this provider. Default: no issues.""" return iter(()) diff --git a/modern_di/providers/alias.py b/modern_di/providers/alias.py index 485c591d..485a5773 100644 --- a/modern_di/providers/alias.py +++ b/modern_di/providers/alias.py @@ -27,17 +27,17 @@ def __init__( def __repr__(self) -> str: return f"Alias(source_type={self._source_type!r}, bound_type={self.bound_type!r}, scope={self.scope!r})" - def find_source(self, container: "Container") -> "AbstractProvider[types.T_co]": + def _find_source(self, container: "Container") -> "AbstractProvider[types.T_co]": source = container.providers_registry.find_provider(self._source_type) if source is None: raise exceptions.AliasSourceNotRegisteredError(source_type=self._source_type) return source - def get_dependencies(self, container: "Container") -> dict[str, "AbstractProvider[typing.Any]"]: - return {"source": self.find_source(container)} + def _get_dependencies(self, container: "Container") -> dict[str, "AbstractProvider[typing.Any]"]: + return {"source": self._find_source(container)} - def redirect_target(self, container: "Container") -> "AbstractProvider[typing.Any] | None": + def _redirect_target(self, container: "Container") -> "AbstractProvider[typing.Any] | None": try: - return self.find_source(container) + return self._find_source(container) except exceptions.AliasSourceNotRegisteredError: return None diff --git a/modern_di/providers/factory.py b/modern_di/providers/factory.py index 8615057b..beba982b 100644 --- a/modern_di/providers/factory.py +++ b/modern_di/providers/factory.py @@ -25,11 +25,19 @@ def __post_init__(self) -> None: self.is_async_finalizer = bool(self.finalizer) and inspect.iscoroutinefunction(self.finalizer) @staticmethod - def coerce(cache: "bool | CacheSettings[types.T] | None") -> "CacheSettings[types.T] | None": - """Read a ``Factory``'s ``cache`` argument: ``True`` is the defaults, ``False`` and ``None`` are off.""" + def _coerce(cache: "bool | CacheSettings[types.T]") -> "CacheSettings[types.T] | None": + """Read a ``Factory``'s ``cache`` argument: ``True`` is the defaults, ``False`` is off.""" + if isinstance(cache, CacheSettings): + return cache if cache is True: return CacheSettings() - return cache or None + if cache is False: + return None + msg = ( + f"Factory cache= takes a bool or a CacheSettings; got {cache!r}. " + "Pass cache=False, or leave it out, for an uncached factory." + ) + raise TypeError(msg) class Factory(AbstractProvider[types.T_co]): @@ -49,7 +57,7 @@ def __init__( # noqa: PLR0913 scope: enum.IntEnum | types.UnsetType = types.UNSET, bound_type: type | types.UnsetType | None = types.UNSET, kwargs: dict[str, typing.Any] | None = None, - cache: bool | CacheSettings[types.T_co] | None = None, + cache: bool | CacheSettings[types.T_co] = False, skip_creator_parsing: bool = False, ) -> None: if skip_creator_parsing: @@ -83,7 +91,7 @@ def __init__( # noqa: PLR0913 bound_type=parsed.return_type.arg_type if isinstance(bound_type, types.UnsetType) else bound_type, ) self._creator = creator - self.cache_settings = CacheSettings.coerce(cache) + self.cache_settings = CacheSettings._coerce(cache) # noqa: SLF001 self._kwargs = kwargs self._cached_definition_site: str | types.UnsetType | None = types.UNSET @@ -145,7 +153,7 @@ def _compute_definition_site(self) -> str | None: return None return f"{module}:{lineno}" - def resolution_step(self) -> exceptions.ResolutionStep: + def _resolution_step(self) -> exceptions.ResolutionStep: return exceptions.ResolutionStep(scope=self.scope, name=self.display_name, location=self.definition_site) def _argument_resolution_error( @@ -161,11 +169,11 @@ def _argument_resolution_error( member_types=item.args, ) - def wiring_plan(self, registry: "ProvidersRegistry") -> WiringPlan: + def _wiring_plan(self, registry: "ProvidersRegistry") -> WiringPlan: """Return this factory's wiring plan, memoized on the tree-wide providers registry.""" return registry.plan_for(self, self._parsed_kwargs, self._kwargs) - def can_call_positionally(self, plan: WiringPlan) -> bool: + def _can_call_positionally(self, plan: WiringPlan) -> bool: """Whether this creator can be called positionally under `plan`. True when every parsed parameter is a positional-or-keyword provider dependency, in signature @@ -180,12 +188,12 @@ def can_call_positionally(self, plan: WiringPlan) -> bool: return False return not (names and self._has_positional_only_gap) - def get_dependencies(self, container: "Container") -> dict[str, "AbstractProvider[typing.Any]"]: + def _get_dependencies(self, container: "Container") -> dict[str, "AbstractProvider[typing.Any]"]: """Return parameter name → dependency provider: a pure registry lookup, no scope or cache touched.""" - return self.wiring_plan(container.providers_registry).edges + return self._wiring_plan(container.providers_registry).edges - def iter_validation_issues(self, container: "Container") -> typing.Iterable[Exception]: + def _iter_validation_issues(self, container: "Container") -> typing.Iterable[Exception]: """Yield ArgumentResolutionError for parameters with no provider, no default, no static kwarg.""" - plan = self.wiring_plan(container.providers_registry) + plan = self._wiring_plan(container.providers_registry) for name, item in plan.unwireable: yield self._argument_resolution_error(arg_name=name, item=item, registry=container.providers_registry) diff --git a/modern_di/resolver_compiler.py b/modern_di/resolver_compiler.py index 2401e18e..a92a8a69 100644 --- a/modern_di/resolver_compiler.py +++ b/modern_di/resolver_compiler.py @@ -184,10 +184,10 @@ def _code(arity: int, names: tuple[str, ...] | None, static: bool, cached: bool) def _compile_factory(f: "Factory[typing.Any]", registry: "ProvidersRegistry") -> "Resolver": - plan = f.wiring_plan(registry) + plan = f._wiring_plan(registry) if plan.unwireable: return _compile_unwireable_factory(f, plan) - positional = f.can_call_positionally(plan) + positional = f._can_call_positionally(plan) code, arg_lines = _code( len(plan.provider_kwargs), None if positional else tuple(plan.provider_kwargs), @@ -199,7 +199,7 @@ def _compile_factory(f: "Factory[typing.Any]", registry: "ProvidersRegistry") -> "pid": f.provider_id, "scope": f.scope, "creator": f._creator, - "resolution_step": f.resolution_step, + "resolution_step": f._resolution_step, "edges": plan.provider_kwargs, "arg_lines": arg_lines, "static": plan.static_kwargs, @@ -229,7 +229,7 @@ def resolve(_: "Container") -> typing.Any: def _compile_unwireable_factory(f: "Factory[typing.Any]", plan: "WiringPlan") -> "Resolver": """Compile a resolver that always raises for the factory's first unwireable parameter, freshly built per call.""" scope = f.scope - resolution_step = f.resolution_step + resolution_step = f._resolution_step build_error = f._argument_resolution_error arg_name, item = plan.unwireable[0] diff --git a/tests/providers/test_alias.py b/tests/providers/test_alias.py index 0cd34ab7..b1825d6a 100644 --- a/tests/providers/test_alias.py +++ b/tests/providers/test_alias.py @@ -424,14 +424,14 @@ def test_redirect_target_default_none() -> None: class X: ... factory = providers.Factory(scope=Scope.APP, creator=X) - assert factory.redirect_target(None) is None # ty: ignore[invalid-argument-type] + assert factory._redirect_target(None) is None # ty: ignore[invalid-argument-type] def test_alias_redirect_target_returns_source() -> None: container = Container(groups=[MyGroup]) source = container.providers_registry.find_provider(PostgresRepository) assert source is not None - target = MyGroup.abstract_repo.redirect_target(container) + target = MyGroup.abstract_repo._redirect_target(container) assert target is not None assert target.provider_id == source.provider_id @@ -441,7 +441,7 @@ class G(Group): abstract = providers.Alias(source_type=PostgresRepository, bound_type=AbstractRepository) container = Container(groups=[G]) - assert G.abstract.redirect_target(container) is None + assert G.abstract._redirect_target(container) is None # A genuine scope inversion through an alias must still raise, even measured against a custom diff --git a/tests/providers/test_factory.py b/tests/providers/test_factory.py index d474626b..54551d1c 100644 --- a/tests/providers/test_factory.py +++ b/tests/providers/test_factory.py @@ -770,10 +770,9 @@ class G(Group): assert G.f.cache_settings is None -@pytest.mark.parametrize("cache_value", [False, None]) -def test_cache_falsy_disables_caching(cache_value: bool | None) -> None: +def test_cache_false_disables_caching() -> None: class G(Group): - f = providers.Factory(creator=SimpleCreator, kwargs={"dep1": "x"}, cache=cache_value) + f = providers.Factory(creator=SimpleCreator, kwargs={"dep1": "x"}, cache=False) container = Container(groups=[G]) container.open() @@ -1226,12 +1225,19 @@ def _body_raises() -> None: @pytest.mark.parametrize( ("cache", "expected"), - [(True, providers.CacheSettings()), (False, None), (None, None)], + [(True, providers.CacheSettings()), (False, None)], ) -def test_cache_settings_coerce(cache: bool | None, expected: providers.CacheSettings[object] | None) -> None: - assert providers.CacheSettings.coerce(cache) == expected +def test_cache_settings_coerce(cache: bool, expected: providers.CacheSettings[object] | None) -> None: + assert providers.CacheSettings._coerce(cache) == expected def test_cache_settings_coerce_returns_an_instance_unchanged() -> None: settings: providers.CacheSettings[object] = providers.CacheSettings(clear_cache=False) - assert providers.CacheSettings.coerce(settings) is settings + assert providers.CacheSettings._coerce(settings) is settings + + +@pytest.mark.parametrize("cache", [None, 1, "yes"]) +def test_factory_cache_rejects_anything_but_bool_or_cache_settings(cache: object) -> None: + with pytest.raises(TypeError, match=r"cache= takes a bool or a CacheSettings") as exc_info: + providers.Factory(SimpleCreator, cache=cache) # ty: ignore[invalid-argument-type] + assert repr(cache) in str(exc_info.value) diff --git a/tests/registries/test_providers_registry.py b/tests/registries/test_providers_registry.py index 904c37b5..7110b332 100644 --- a/tests/registries/test_providers_registry.py +++ b/tests/registries/test_providers_registry.py @@ -292,7 +292,7 @@ def hold_open( return plan monkeypatch.setattr(pr_mod.WiringPlan, "build", staticmethod(hold_open)) - worker = threading.Thread(target=lambda: svc.wiring_plan(registry)) + worker = threading.Thread(target=lambda: svc._wiring_plan(registry)) worker.start() try: assert built.wait(5), "plan build never reached the publication window" diff --git a/tests/test_container.py b/tests/test_container.py index 58f798cb..74e80bb9 100644 --- a/tests/test_container.py +++ b/tests/test_container.py @@ -201,10 +201,10 @@ class Top: class _CountingFactory(providers.Factory[Bottom]): __slots__ = () - def get_dependencies(self, container: Container) -> dict[str, AbstractProvider[typing.Any]]: + def _get_dependencies(self, container: Container) -> dict[str, AbstractProvider[typing.Any]]: nonlocal call_count call_count += 1 - return super().get_dependencies(container) + return super()._get_dependencies(container) bottom_provider = _CountingFactory(creator=Bottom) @@ -971,6 +971,11 @@ class _UnknownProvider(AbstractProvider[object]): container.resolve_provider(provider) +def test_abstract_provider_is_a_plain_class() -> None: + """`resolve_dependency` runs `isinstance(x, AbstractProvider)` per call; ABCMeta would make it Python-level.""" + assert type(AbstractProvider) is type + + # --- validate() is the only trigger: construction, open(), resolve() and add_providers() never ---- # --- walk the graph on their own. ------------------------------------------------------------- diff --git a/tests/test_packaging.py b/tests/test_packaging.py index 78fb91ed..3162d706 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -53,3 +53,12 @@ def test_modern_di_imports_without_typing_extensions() -> None: ) assert result.returncode == 0, result.stderr assert "OK REQUEST" in result.stdout + + +def test_package_init_imports_every_name_it_exports() -> None: + """Every name in `modern_di.__all__` is bound by an import in `__init__` itself.""" + tree = ast.parse((_PKG_ROOT / "__init__.py").read_text(encoding="utf-8")) + imported = { + alias.asname or alias.name for node in tree.body if isinstance(node, ast.ImportFrom) for alias in node.names + } + assert set(modern_di.__all__) <= imported diff --git a/tests/test_resolver_compiler.py b/tests/test_resolver_compiler.py index e3ec763e..041addcc 100644 --- a/tests/test_resolver_compiler.py +++ b/tests/test_resolver_compiler.py @@ -2,7 +2,7 @@ The differential-harness suite in ``tests/providers/test_factory.py`` characterizes each compiled path black-box through ``resolve_provider``. These pin what it leaves unguarded: the -argument-ordering invariant the positional path depends on, ``Factory.can_call_positionally``'s full +argument-ordering invariant the positional path depends on, ``Factory._can_call_positionally``'s full contract, the per-node frame budget, and the contracts of the generated source (shape sharing, source lines in tracebacks, non-identifier kwarg names, overrides compiled as constants). """ @@ -79,7 +79,7 @@ def _make(a: _A, b: _B, c: _C) -> _Ordered: def _plan(registry: ProvidersRegistry, owner: "providers.Factory[object]") -> WiringPlan: """Build ``owner``'s wiring plan the way production does (via the registry memo).""" - return owner.wiring_plan(registry) + return owner._wiring_plan(registry) @dataclasses.dataclass(slots=True) @@ -175,7 +175,7 @@ class G(Group): container = Container(groups=[G]) container.open() plan = _plan(container.providers_registry, G.ordered) - assert G.ordered.can_call_positionally(plan) # self-guard: positional path selected + assert G.ordered._can_call_positionally(plan) # self-guard: positional path selected result = container.resolve(_Ordered) assert isinstance(result.a, _A) @@ -202,7 +202,7 @@ def test_positional_path_binds_args_in_signature_order_at_every_arity(arity: int container = Container(scope=Scope.APP, groups=[group]) container.open() plan = _plan(container.providers_registry, members["bag"]) - assert members["bag"].can_call_positionally(plan) # self-guard: positional path selected + assert members["bag"]._can_call_positionally(plan) # self-guard: positional path selected bag = container.resolve_provider(members["bag"]) assert len(bag.values) == arity @@ -343,7 +343,7 @@ def test_arity_rung_prepends_its_step_to_a_dependency_error(arity: int) -> None: # --------------------------------------------------------------------------- -# Factory.can_call_positionally — the full predicate contract, called directly +# Factory._can_call_positionally — the full predicate contract, called directly # --------------------------------------------------------------------------- @@ -358,7 +358,7 @@ def test_can_call_positionally_accepts_ordered_provider_signature() -> None: owner = providers.Factory(creator=_make, scope=Scope.APP) registry.add_providers(owner) - assert owner.can_call_positionally(_plan(registry, owner)) is True + assert owner._can_call_positionally(_plan(registry, owner)) is True def test_can_call_positionally_rejects_static_kwarg() -> None: @@ -366,7 +366,7 @@ def test_can_call_positionally_rejects_static_kwarg() -> None: A wrong `True` silently binds arguments to the wrong parameters -- a correctness bug, not a slow path. Every negative case must keep `creator(**kwargs)`; widening the predicate to admit one of - them trades correctness for speed. The other reject-case tests for `Factory.can_call_positionally` + them trades correctness for speed. The other reject-case tests for `Factory._can_call_positionally` below share this rationale rather than repeating it. """ @@ -379,7 +379,7 @@ def creator(dep: _A, req: _Req) -> _Ordered: owner = providers.Factory(creator=creator, scope=Scope.APP, kwargs={"req": _Req()}) registry.add_providers(owner) - assert owner.can_call_positionally(_plan(registry, owner)) is False + assert owner._can_call_positionally(_plan(registry, owner)) is False def test_can_call_positionally_accepts_a_context_provider_dependency() -> None: @@ -394,7 +394,7 @@ def creator(dep: _A, req: _Req) -> _Ordered: owner = providers.Factory(creator=creator, scope=Scope.APP) registry.add_providers(owner) - assert owner.can_call_positionally(_plan(registry, owner)) is True + assert owner._can_call_positionally(_plan(registry, owner)) is True def test_can_call_positionally_rejects_defaulted_omitted_param() -> None: @@ -414,7 +414,7 @@ def creator(dep: _A, opt: int = 5) -> _Ordered: owner = providers.Factory(creator=creator, scope=Scope.APP) registry.add_providers(owner) - assert owner.can_call_positionally(_plan(registry, owner)) is False + assert owner._can_call_positionally(_plan(registry, owner)) is False def test_can_call_positionally_rejects_kwargs_overlay_reorder() -> None: @@ -437,7 +437,7 @@ def creator(a: _A, b: _B) -> _Ordered: plan = _plan(registry, owner) assert tuple(plan.provider_kwargs) == ("b", "a") # overlay put `a` last - assert owner.can_call_positionally(plan) is False + assert owner._can_call_positionally(plan) is False def test_can_call_positionally_rejects_keyword_only_param() -> None: @@ -456,7 +456,7 @@ def creator(*, dep: _A) -> _Ordered: owner = providers.Factory(creator=creator, scope=Scope.APP) registry.add_providers(owner) - assert owner.can_call_positionally(_plan(registry, owner)) is False + assert owner._can_call_positionally(_plan(registry, owner)) is False def test_can_call_positionally_rejects_positional_only_param() -> None: @@ -477,7 +477,7 @@ def creator(prefix: str = "P", /, dep: _A = None) -> _Ordered: # ty: ignore[inv owner = providers.Factory(creator=creator, scope=Scope.APP) registry.add_providers(owner) - assert owner.can_call_positionally(_plan(registry, owner)) is False + assert owner._can_call_positionally(_plan(registry, owner)) is False def test_first_resolve_does_not_reintrospect_creator(monkeypatch: pytest.MonkeyPatch) -> None: