Skip to content

fix!: wire NamedTuple, __new__-only classes, NewType and type aliases (#571) - #591

Merged
lesnik512 merged 1 commit into
mainfrom
fix/types-parser-forms
Oct 5, 2026
Merged

lesnik512 merged 1 commit into
mainfrom
fix/types-parser-forms

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Closes #571.

Summary

  • Classes now take their parameter hints from the same __new__ or __init__ that inspect.signature reads (first Python-level one in the MRO). NamedTuple subclasses and classes that define only __new__ wire their parameters, including string forward references in a NamedTuple.
  • NewType objects and PEP 695 type X = ... aliases are bound types of their own. A parameter annotated with one resolves from the provider whose bound_type is that object, and a creator returning one gets it as its inferred bound type.
  • Factory(creator) with a return annotation that is a union of several types (-> A | B) emits a UserWarning. The audit suspicion reproduced: the provider got bound_type=None and was silently skipped at registration.
  • Cleanup from the issue: get_origin(...) is Annotated replaces the __metadata__ probe, the always-true hasattr(creator, "__init__") and the unreachable elif non_none_members branch are gone, and the narrating comments are removed.
  • Docs: support matrix row and bound_type notes in docs/providers/factories.md, and a migration entry.

Design decisions

  • An alias or NewType is matched as the object itself. type DepAlias = Dep with only a Dep provider still fails, now with Argument dep of type DepAlias cannot be resolved instead of the "no usable type annotation" message. Following __value__ / __supertype__ would make a UserId parameter silently receive any int.
  • The union-return case warns the same way the existing skip_creator_parsing and unresolvable-hints cases do (UserWarning, stacklevel=2). It does not raise, because a provider resolved only through resolve_provider is valid. An explicit bound_type (including None) silences it.
  • Class hints are resolved with the defining class's module as localns. The __new__ that NamedTuple generates has a private globals dict, so forward references failed without it. For ordinary classes the dict is the method's own globals, so nothing changes.
  • Marked !: a creator returning a NewType or alias now registers under it, which can collide with an explicit bound_type= provider for the same object (DuplicateProviderTypeError). The migration entry covers it.
  • Cold path, Factory(...) construction on 3.14, best of 7 x 20k: class 21.0 to 20.7 us, kw-only dataclass 18.8 to 18.7 us, function 10.1 to 9.7 us. from_type now calls get_origin once instead of up to three times, which pays for the new checks.

Open questions

  • The alias tests use typing.TypeAliasType("_UserIdAlias", int), which builds the same object a type statement creates, rather than exec. That way the test module parses on 3.11 and the 100% line gate holds there (the cases are skipped below 3.12). A real type statement was checked by hand with the audit repro on 3.14.
  • A metaclass __call__ still falls back to __init__ hints, as before. Not in scope.

Test plan

  • New tests failed before the fix: NamedTuple, NamedTuple forward ref, __new__-only class and its subclass, NewType and alias parameters, return annotations and unregistered-form messages, union-return warning
  • just lint, just lint-ci
  • just test-ci at 100% on 3.14, plus full runs with coverage on 3.11 and 3.12 (100%)
  • mkdocs build --strict
  • Audit repros r14_new.py and r6_types.py behave as expected

@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: 3a59da2 Previous: fd1e0f4 Ratio
benchmarks/test_guard_by_type.py::test_g16_resolve_by_type 4724052.727771523 iter/sec (stddev: 1.6672884772236973e-8) 2785475.74950628 iter/sec (stddev: 1.4364414722432417e-8) 0.59
benchmarks/test_guard_by_type.py::test_g17_resolve_by_type_large_registry 4814937.167576123 iter/sec (stddev: 1.2427847137729109e-8) 3177510.2434177175 iter/sec (stddev: 1.3790078078729603e-8) 0.66
benchmarks/test_guard_cold.py::test_g8_cold_first_resolve 19913.619514589802 iter/sec (stddev: 0.000051805072200300686) 20283.841161522876 iter/sec (stddev: 0.00005290189514080043) 1.02
benchmarks/test_guard_cold.py::test_g8b_cold_first_resolve_cached 16215.438899778686 iter/sec (stddev: 0.00014750013451195716) 16408.469239894028 iter/sec (stddev: 0.00011783911748240829) 1.01
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[1] 610.7083376522121 iter/sec (stddev: 0.00008396524384613371) 334.71483392209666 iter/sec (stddev: 0.00005097644447706185) 0.55
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[2] 566.5134028845748 iter/sec (stddev: 0.00007030649248953457) 321.9356643442428 iter/sec (stddev: 0.00007526403499159544) 0.57
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[4] 456.4306894048939 iter/sec (stddev: 0.00022797773425180885) 299.1314862237464 iter/sec (stddev: 0.00015135528795282046) 0.66
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[1] 1924.7169731749043 iter/sec (stddev: 0.00016526011182520665) 1909.8701404471688 iter/sec (stddev: 0.00016488825446358974) 0.99
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[2] 1584.3671130749701 iter/sec (stddev: 0.0001563089025173073) 1283.774447504292 iter/sec (stddev: 0.00020133764649106264) 0.81
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[4] 1030.300582061424 iter/sec (stddev: 0.0001742468122274287) 1019.194134918245 iter/sec (stddev: 0.0001933798585599239) 0.99
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[1] 2023.998892142404 iter/sec (stddev: 0.00014894724581217364) 1851.8014417012332 iter/sec (stddev: 0.00021655536210606418) 0.91
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[2] 1164.3173636868946 iter/sec (stddev: 0.00141403205240231) 1159.6223666571705 iter/sec (stddev: 0.0011509626487236457) 1.00
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[4] 899.4696367282685 iter/sec (stddev: 0.00018004215200217988) 836.5240451725125 iter/sec (stddev: 0.00018625872855068212) 0.93
benchmarks/test_guard_lifecycle.py::test_g6_build_child_container 1098996.9591819819 iter/sec (stddev: 4.142723037160206e-8) 929162.4870813187 iter/sec (stddev: 4.500367366727941e-8) 0.85
benchmarks/test_guard_lifecycle.py::test_g6b_build_child_container_auto_scope 1004133.0114842409 iter/sec (stddev: 3.065130172890026e-8) 839247.9155338999 iter/sec (stddev: 3.051896837754992e-8) 0.84
benchmarks/test_guard_lifecycle.py::test_g7_request_lifecycle_batch 2454.1119221555155 iter/sec (stddev: 0.000012116565935991959) 2368.9196838669773 iter/sec (stddev: 0.000051192486509853176) 0.97
benchmarks/test_guard_lifecycle.py::test_g7c_event_loop_floor_control 68432.14444749721 iter/sec (stddev: 0.0000014469874666476483) 57986.44256206956 iter/sec (stddev: 0.000002010699586755405) 0.85
benchmarks/test_guard_lifecycle.py::test_g13_teardown_at_scale 49072.74269448379 iter/sec (stddev: 0.000001743380894833483) 42505.97817077367 iter/sec (stddev: 0.0000019620757051777622) 0.87
benchmarks/test_guard_lifecycle.py::test_g13b_teardown_at_scale_async_no_finalizers 624.1824424049817 iter/sec (stddev: 0.00003779352751747016) 507.9171923025423 iter/sec (stddev: 0.000038635885906492634) 0.81
benchmarks/test_guard_resolve.py::test_g1_transient_resolve 2687444.5541494004 iter/sec (stddev: 3.4472906267940917e-8) 1957502.809752955 iter/sec (stddev: 6.993608542046132e-8) 0.73
benchmarks/test_guard_resolve.py::test_g2_cached_resolve 4529713.380001874 iter/sec (stddev: 1.4453993529718734e-8) 2768727.1931305034 iter/sec (stddev: 2.5771136906708655e-8) 0.61
benchmarks/test_guard_resolve.py::test_g3_deep_chain 945998.1228497069 iter/sec (stddev: 5.0084619117323515e-8) 674111.0207255966 iter/sec (stddev: 5.616840649357383e-8) 0.71
benchmarks/test_guard_resolve.py::test_g4_wide_resolve 548668.0944790678 iter/sec (stddev: 4.690930481205634e-7) 452150.1582536596 iter/sec (stddev: 4.7140486270837895e-7) 0.82
benchmarks/test_guard_resolve.py::test_g5_cross_scope 2508935.573913691 iter/sec (stddev: 2.464170040456481e-8) 1857756.1621631144 iter/sec (stddev: 2.2144501895465633e-8) 0.74
benchmarks/test_guard_resolve.py::test_g9_context_resolve 1755474.204768919 iter/sec (stddev: 1.630212247387252e-7) 1353873.3979482 iter/sec (stddev: 1.7011337438267227e-7) 0.77
benchmarks/test_guard_resolve.py::test_g12_override_active_resolve 920333.7480310067 iter/sec (stddev: 1.0834852314583847e-7) 682268.9057128795 iter/sec (stddev: 4.921611630457953e-8) 0.74
benchmarks/test_guard_resolve.py::test_g18_alias_hop 4542242.148113701 iter/sec (stddev: 2.894784200121989e-8) 2878165.4711447074 iter/sec (stddev: 1.2323641519737934e-8) 0.63
benchmarks/test_guard_validate.py::test_g10_validate_deep_chain 24248.49292894262 iter/sec (stddev: 0.00002150227371703452) 25353.007458676224 iter/sec (stddev: 0.000024898450622046718) 1.05
benchmarks/test_guard_validate.py::test_g11_validate_wide 15202.868214048249 iter/sec (stddev: 0.00002619249812960554) 15835.792042389787 iter/sec (stddev: 0.000027956843376048667) 1.04

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

@lesnik512
lesnik512 force-pushed the fix/types-parser-forms branch from 7efff66 to 3a59da2 Compare October 5, 2026 08:50
@lesnik512
lesnik512 merged commit 6b6bae8 into main Oct 5, 2026
9 checks passed
@lesnik512
lesnik512 deleted the fix/types-parser-forms branch October 5, 2026 08:50
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.

types_parser: NamedTuple, __new__-only classes, NewType and type aliases cannot be wired

1 participant