Skip to content

fix(admin): union/TypeVar-safe bean introspection — admin bean-graph 500 (v26.06.99)#129

Merged
ancongui merged 1 commit into
mainfrom
fix/admin-bean-graph-union-safe
Jun 12, 2026
Merged

fix(admin): union/TypeVar-safe bean introspection — admin bean-graph 500 (v26.06.99)#129
ancongui merged 1 commit into
mainfrom
fix/admin-bean-graph-union-safe

Conversation

@ancongui

Copy link
Copy Markdown
Contributor

Summary

Fixes the admin dashboard #bean-graph 500 (and /beans, /beans/{name}) that occurs whenever a bean's registration key is not a plain class.

Root cause: the idiomatic @bean def make_foo(self) -> Foo | None. typing.get_type_hints preserves Foo | None as a types.UnionType, and ApplicationContext._process_configurations registered that union object directly as a _registrations key. A types.UnionType (and TypeVar) has no __name__/__qualname__/__module__, so BeansProvider — which derived bean names/types straight from the key — raised AttributeError: 'types.UnionType' object has no attribute '__name__'.

⚠️ This only became visible after the v26.06.95 union-safe startup fix: before, a union bean crashed startup (you never reached the dashboard); after, the app boots and the bean-graph endpoint crashes instead. None of the v26.06.95–98 commits touched beans_provider.py, so pulling alone did not fix it.

Fix (defense in depth, 3 layers)

Layer Change
Source ApplicationContext no longer registers a non-class return hint as a _registrations key. The concrete type(result) registration already backs single-bean resolution, so nothing is lost.
Provider BeansProvider derives names/types via union/TypeVar-safe _key_name/_key_qualname helpers; condition values coerced JSON-safe; a raising on_class check() is reported not-passed; a raising descriptor in the autowired scan is skipped.
Backstop Admin API handlers convert any unexpected error into a logged, structured JSON 500 instead of a raw Starlette 500.

Also covers three additional reproduced /beans/{name} detail 500s (generic-alias conditions, raising on_class check, raising descriptor), and realigns the stale pyfly.__version__ (26.06.9426.06.99) that fed the dashboard framework_version, CLI banner, SBOMs, and admin asset cache-busting.

Why it slipped through

The prior admin tests (test_beans_provider_enhanced.py) use a mocked container whose keys are always vanilla classes — real type-hint resolution was never exercised. New regression tests drive the real ApplicationContext/Container:

  • tests/context/test_bean_method_union_return.py (the exact @bean -> Foo | None scenario, end to end through the admin provider)
  • tests/admin/test_beans_provider_realcontext.py (union/TypeVar keys, generic-alias conditions, raising check, raising descriptor)

Verification

  • ruff check ✓ · ruff format --check ✓ · mypy src/pyfly --strict ✓ (changed files)
  • Affected suites green: tests/admin tests/context tests/container tests/cli tests/observability807 passed
  • New regression tests fail on main, pass here.

🤖 Generated with Claude Code

…500 (v26.06.99)

The admin dashboard's #bean-graph view (and /beans, /beans/{name}) 500'd whenever
a bean's registration key was not a plain class. The idiomatic
`@bean def make_foo(self) -> Foo | None` is the common trigger: typing.get_type_hints
preserves `Foo | None` as a types.UnionType, which the configuration processor
registered directly as a _registrations key. UnionType/TypeVar have no
__name__/__qualname__/__module__, so BeansProvider — deriving names/types straight
from the key — raised AttributeError. This only surfaced after the v26.06.95 startup
fix made union beans bootable.

Fixed at three layers:
- Source: ApplicationContext no longer registers a non-class return hint as a
  _registrations key (the concrete type(result) registration already backs
  single-bean resolution, so nothing is lost).
- Provider: BeansProvider derives names/types via union/TypeVar-safe _key_name /
  _key_qualname helpers; condition values are coerced JSON-safe; a raising on_class
  check() is reported not-passed; a raising descriptor in the autowired-field scan
  is skipped.
- Backstop: admin API handlers convert any unexpected error into a logged, structured
  JSON 500 instead of a raw Starlette 500.

Also realigns pyfly.__version__ (had drifted to 26.06.94) so framework_version, the
CLI banner, SBOMs, and admin asset cache-busting report the shipped version.

Regression tests drive the REAL ApplicationContext/Container (not the mocked
container the prior admin tests used, which is why these 500s shipped):
tests/context/test_bean_method_union_return.py,
tests/admin/test_beans_provider_realcontext.py.

Bumps 26.6.98 -> 26.6.99; updates CHANGELOG + README badge.
@ancongui ancongui merged commit d18b3de into main Jun 12, 2026
6 checks passed
@ancongui ancongui deleted the fix/admin-bean-graph-union-safe branch June 12, 2026 16:58
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.

1 participant