Skip to content

Removed compatibility factory for types when Optional[T] factory is defined - #140

Open
ranjanprasad96 wants to merge 2 commits into
maldoinc:masterfrom
ranjanprasad96:remove_backward_compatibility
Open

Removed compatibility factory for types when Optional[T] factory is defined#140
ranjanprasad96 wants to merge 2 commits into
maldoinc:masterfrom
ranjanprasad96:remove_backward_compatibility

Conversation

@ranjanprasad96

Copy link
Copy Markdown
Contributor

Summary

Fixes #138 — factories returning Optional[T] registered with a qualifier failed at container creation with a spurious self-dependency error:

WireupError: Parameter 'raw_type_instance' of <class 'AuthContext'>
has an unknown dependency on <class 'AuthContext'>.

Root cause

To keep container.get(T) working when a factory returned Optional[T], Wireup registered an alias factory (compat_fn) under the raw type T. That alias
depended on the real Optional[T] factory — but it built that dependency from the type alone, dropping the qualifier.

So for a qualified factory, the alias looked up (Optional[T], qualifier=None) while the real factory was registered under (Optional[T], qualifier). The lookup
missed, and registry validation reported it as a spurious self-dependency on T.

The alias was also too broad: living under the raw type T, it leaked into injected resolution as well as container.get — but this compatibility should apply
to container.get only.

Fix

Remove the compatibility alias factory entirely. Optional[T] now registers a single factory under its normalized T | None key (qualifier preserved, like any other registration), so the qualifier can never be dropped.

The container.get(T) convenience is handled instead as a fallback on unknown types, scoped to get alone:

# base_container.py — only reached when the requested type isn't registered directly
if (optional_klass := self._optional_compat_klass(klass, qualifier)) is not None:
    return self._synchronous_get(optional_klass, qualifier)

_optional_compat_klass returns T | None (carrying the original qualifier) when that registration exists, else None. Injected dependencies never hit this paththey
resolve by their actual annotated type, which narrows the compatibility surface to container.get as intended.

Behavior changes

  • Injected parameters annotated as a bare T (where only Optional[T] was registered) no longer resolve — use T | None. This is the intended narrowing.
  • Optional[T] and a plain T factory are now distinct registration keys and can coexist (previously raised DuplicateServiceRegistrationError, purely as a side effect
    of the alias).
  • container.get(T) for an Optional[T] registration resolves silently (no deprecation warning).

Tests

  • test_getting_qualified_optional_service_via_plain_type_keeps_qualifier — the Cannot use qualifiers on factory with Optional Types #138 regression: registers an Optional[T] factory with a qualifier and asserts
    container.get(T, qualifier=...) resolves the correctly qualified instance. Verified it fails without the fix and passes with it.
  • test_getting_optional_service_via_plain_type_resolves_silently (+ async variant) — container.get(T) resolves T | None without emitting any warning.
  • test_registering_optional_and_plain_type_are_distinct — Optional[T] and plain T coexist and each resolves independently.

All unit tests, mypy --strict, and ruff pass.

@ranjanprasad96

Copy link
Copy Markdown
Contributor Author

@maldoinc Any updates on this PR?

@maldoinc maldoinc left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, just a few minor items


return await res if compiled_factory.is_async else res # type:ignore[no-any-return]

if (optional_klass := self._optional_compat_klass(klass, qualifier)) is not None:

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is deprecated behavior so whenever this branch is hit the deprecation warning should be raised.


return compiled_factory.factory(self) # type:ignore[no-any-return]

if (optional_klass := self._optional_compat_klass(klass, qualifier)) is not None:

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is deprecated behavior so whenever this branch is hit the deprecation warning should be raised.

Comment thread wireup/ioc/registry.py
Comment on lines 53 to 54
is_optional_type: bool
raw_type: type

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are now unused and can be removed.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's leave the existing tests in tact to keep asserting the behavior, we can add any new ones as necessary

Comment thread wireup/ioc/registry.py
def _register_mapping_collections(self) -> None:
for klass, qualifiers in dict(self.impls).items():
# Only real registration keys should contribute to collection members.
# Synthetic aliases like raw optional-compat or Mapping[Hashable, T] keys must not participate here.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment is stale, can be updated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cannot use qualifiers on factory with Optional Types

2 participants