Skip to content

fix: wire an Annotated union member read from a creator's signature - #724

Merged
lesnik512 merged 1 commit into
mainfrom
fix/annotated-union-member
Oct 18, 2026
Merged

lesnik512 merged 1 commit into
mainfrom
fix/annotated-union-member

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Closes #720.

A creator parameter whose type comes from inspect.signature(creator), not from its annotations, could be annotated Annotated[X, ...] | None. It got None even when a provider for X was registered. Pydantic models and msgspec Structs take this path, and Annotated[X, Field(...)] | None = None is a common pydantic field.

SignatureItem.from_type stripped Annotated only when it was the whole annotation. A union member Annotated[X, ...] went straight to typing.get_origin, which returns typing.Annotated. Nothing is registered for that, so the optional fell back to None, and validate() passed because an optional with no match is allowed. A two-member union of annotated types failed validate() instead.

Each union member is now unwrapped from Annotated before its origin is taken. This matches what get_type_hints already does on the annotations path. Python flattens nested Annotated, so one unwrap covers it.

Tests, all through a creator typed only on __signature__:

  • Annotated[X, ...] | None, Optional[Annotated[X, ...]] and nested Annotated all inject the registered X. These failed on main with None.
  • With X unregistered, None is still injected and validate() passes. This test passes on main and pins the fallback.
  • A union of two annotated members resolves the first registered one. This failed validate() on main.

The factories page already describes this behavior, so it needs no change.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Benchmark

Details
Benchmark suite Current: 1178533 Previous: b7a1775 Ratio
benchmarks/test_guard_by_type.py::test_g16_resolve_by_type 3385536.2713156226 iter/sec (stddev: 1.3033071089100934e-8) 6829021.182046608 iter/sec (stddev: 5.484705719035411e-9) 2.02
benchmarks/test_guard_by_type.py::test_g17_resolve_by_type_large_registry 3305448.6784667904 iter/sec (stddev: 1.2518534825217117e-8) 6697985.105419868 iter/sec (stddev: 4.150738470799008e-9) 2.03
benchmarks/test_guard_cold.py::test_g8_cold_first_resolve 16002.655854211207 iter/sec (stddev: 0.00005047315121380543) 30757.49282217767 iter/sec (stddev: 0.00003212381064992869) 1.92
benchmarks/test_guard_cold.py::test_g8b_cold_first_resolve_cached 12483.88825055001 iter/sec (stddev: 0.000123581761207184) 24483.236846288204 iter/sec (stddev: 0.00015154400300924624) 1.96
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[1] 350.6094934436215 iter/sec (stddev: 0.000038427368305138276) 857.6505269844425 iter/sec (stddev: 0.000031478572667331435) 2.45
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[2] 453.7881587221414 iter/sec (stddev: 0.00005564690813429717) 852.2634799214209 iter/sec (stddev: 0.000029566400203879867) 1.88
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[4] 432.8482285275235 iter/sec (stddev: 0.00006211261921572117) 767.6217950190414 iter/sec (stddev: 0.0002570266758932922) 1.77
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[1] 13075.627248648618 iter/sec (stddev: 0.000007944220456896726) 8156.588001364661 iter/sec (stddev: 0.0001306104049620297) 0.62
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[2] 6597.867404297306 iter/sec (stddev: 0.000023080236901366023) 6062.480530611141 iter/sec (stddev: 0.00018017904286592358) 0.92
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[4] 3399.9442862489254 iter/sec (stddev: 0.00004202491894215667) 3415.1846852251756 iter/sec (stddev: 0.0002481508400191217) 1.00
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[1] 10283.454863771989 iter/sec (stddev: 0.000008943187866682982) 19060.870891138744 iter/sec (stddev: 0.000010501867428911193) 1.85
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[2] 4334.510188975243 iter/sec (stddev: 0.00003468323214262874) 7946.913031583689 iter/sec (stddev: 0.000014106057259191144) 1.83
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[4] 2024.325065983362 iter/sec (stddev: 0.0000436808663882196) 3882.0758637220474 iter/sec (stddev: 0.000042286664071694886) 1.92
benchmarks/test_guard_concurrency.py::test_g15c_worker_pool_floor_control[1] 39805.60270500575 iter/sec (stddev: 0.000005607503613407588) 57980.00559382983 iter/sec (stddev: 0.0000016583671333003195) 1.46
benchmarks/test_guard_concurrency.py::test_g15c_worker_pool_floor_control[2] 13428.343338509014 iter/sec (stddev: 0.000010180740359973868) 26583.077655991503 iter/sec (stddev: 0.000028387436691761624) 1.98
benchmarks/test_guard_concurrency.py::test_g15c_worker_pool_floor_control[4] 5735.79110707796 iter/sec (stddev: 0.00003260191154261586) 10858.506065052692 iter/sec (stddev: 0.000019239849675808495) 1.89
benchmarks/test_guard_lifecycle.py::test_g6_build_child_container 1561473.2093762013 iter/sec (stddev: 2.7975609396474993e-8) 3109200.162117778 iter/sec (stddev: 1.6655170160438705e-8) 1.99
benchmarks/test_guard_lifecycle.py::test_g6b_build_child_container_auto_scope 1626779.3254744145 iter/sec (stddev: 2.0641096972401086e-8) 3355865.1714153057 iter/sec (stddev: 4.5860760432145475e-8) 2.06
benchmarks/test_guard_lifecycle.py::test_g7_request_lifecycle_batch 3153.741249252071 iter/sec (stddev: 0.000009841311664348434) 5518.268723840956 iter/sec (stddev: 0.000053690988201758585) 1.75
benchmarks/test_guard_lifecycle.py::test_g7c_event_loop_floor_control 57723.34486993463 iter/sec (stddev: 0.0000019298552632940542) 111291.94431021478 iter/sec (stddev: 7.415236228873871e-7) 1.93
benchmarks/test_guard_lifecycle.py::test_g7b_request_cycle_sync 404204.34769033175 iter/sec (stddev: 7.400993492163958e-8) 811531.4726922724 iter/sec (stddev: 7.030419208342152e-8) 2.01
benchmarks/test_guard_lifecycle.py::test_g13_teardown_at_scale 64754.149112613726 iter/sec (stddev: 0.0000015930664356883794) 128880.30989858645 iter/sec (stddev: 7.515728812940163e-7) 1.99
benchmarks/test_guard_lifecycle.py::test_g13b_teardown_at_scale_async_no_finalizers 586.6940343364163 iter/sec (stddev: 0.0010313962527597317) 1106.9222569933033 iter/sec (stddev: 0.0012045400255381931) 1.89
benchmarks/test_guard_resolve.py::test_g1_transient_resolve 2116495.846926867 iter/sec (stddev: 3.1813554193268004e-8) 3927978.9350432907 iter/sec (stddev: 3.170569802722682e-8) 1.86
benchmarks/test_guard_resolve.py::test_g2_cached_resolve 3310891.2296672002 iter/sec (stddev: 1.9697386109674e-8) 6651633.973879918 iter/sec (stddev: 4.456018484664371e-9) 2.01
benchmarks/test_guard_resolve.py::test_g3_deep_chain 785659.8998096765 iter/sec (stddev: 4.8752463865235617e-8) 1540870.274285195 iter/sec (stddev: 2.8074047812986162e-8) 1.96
benchmarks/test_guard_resolve.py::test_g4_wide_resolve 474886.04337642185 iter/sec (stddev: 3.864550967233848e-7) 866746.707836144 iter/sec (stddev: 3.635108057427795e-7) 1.83
benchmarks/test_guard_resolve.py::test_g5_cross_scope 1904304.599612334 iter/sec (stddev: 4.9132082598027375e-8) 3642649.193107508 iter/sec (stddev: 1.6992111188829588e-8) 1.91
benchmarks/test_guard_resolve.py::test_g9_context_resolve 1443567.8298585857 iter/sec (stddev: 1.4439310431670355e-7) 2725232.6428932473 iter/sec (stddev: 1.4077741255402713e-7) 1.89
benchmarks/test_guard_resolve.py::test_g12_override_active_resolve 715788.4610316678 iter/sec (stddev: 2.1219275962841917e-7) 1524730.519123093 iter/sec (stddev: 2.5341878937965144e-8) 2.13
benchmarks/test_guard_resolve.py::test_g18_alias_hop 3025511.4453129694 iter/sec (stddev: 4.3584784886757416e-8) 6463835.3904325925 iter/sec (stddev: 4.452053658239917e-9) 2.14
benchmarks/test_guard_validate.py::test_g10_validate_deep_chain 35629.428292562654 iter/sec (stddev: 0.000018333937152494847) 53458.71373762307 iter/sec (stddev: 0.00025732724667223246) 1.50
benchmarks/test_guard_validate.py::test_g11_validate_wide 21270.493677539318 iter/sec (stddev: 0.000022865684276875775) 41550.27511260399 iter/sec (stddev: 0.000015081427680084859) 1.95

This comment was automatically generated by workflow using github-action-benchmark.

@lesnik512
lesnik512 merged commit ddb853f into main Oct 18, 2026
12 checks passed
@lesnik512
lesnik512 deleted the fix/annotated-union-member branch October 18, 2026 10:02
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.

Annotated[X, ...] | None from a creator's signature injects None when X is registered

1 participant