Skip to content

feat: reject provider subclasses at definition, name both duplicate providers, summarize finalizer errors, document the providers (#618) - #631

Merged
lesnik512 merged 2 commits into
mainfrom
errors-docstrings-618
Oct 10, 2026
Merged

lesnik512 merged 2 commits into
mainfrom
errors-docstrings-618

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Part of #618 (items 1, 2, 3, 4). Item 6 is #630; the rest was dropped (see the issue comments).

Summary

  • Provider subclasses fail when the class is defined. AbstractProvider.__init_subclass__ raises TypeError for a subclass declared outside modern-di. Since refactor: dispatch the dependency graph by provider type, make the provider step a free function (#617) #629 the graph dispatches by exact type, so a Factory subclass got no validation at all and then failed at its first resolve with a bare "no compiled resolver" TypeError.
  • DuplicateProviderTypeError names both providers. New .first_provider and .second_provider, in registration order, and one line per provider in the message. The hint "pass dependencies via kwargs", which does not fix a duplicate, is gone:
    Provider is duplicated by type <class 'str'>.
      - Factory (myapp.ioc:12)
      - Factory (myapp.other:30)
    Set bound_type=None on one of them to make it resolvable by reference only.
    
  • FinalizerError is summarized like ValidationFailedError: Container.close_sync() found 2 finalizer error(s): ConnectionError (1), ValueError (1). Each user finalizer exception gets a note, raised by the finalizer of a cached Database, which shows in the traceback.
  • Docstrings on Factory, Alias, Group, AbstractProvider, CacheSettings, Group.get_providers and Group.get_named_providers, worded to match docs/providers/.

Design decisions

  • Item 1 keeps TypeError, the type the docs already promised, rather than adding a ModernDIError subclass that would need its own troubleshooting page. The check looks at the subclass's module, so modern-di's own provider types still subclass AbstractProvider. No integration, template or chat-app subclasses a provider.
  • The two new constructor keywords are optional, so code that builds DuplicateProviderTypeError itself keeps working. Like the other errors that hold providers, they degrade to their repr under pickle protocols 0 and 1.
  • The provider lines and the finalizer note are drawn in exceptions/, which owns every message.

Release notes

Two changes are visible to 4.0.0 users and belong in the next release notes:

  • A provider subclass now fails at import rather than at first resolve.
  • FinalizerError's message text changed. Code that matched on Errors during sync cleanup: [...] must update.

Test plan

  • New tests first, failing, then passing: subclass rejection (Factory and AbstractProvider), duplicate providers and message in both registration paths, finalizer notes on sync and async close, the new FinalizerError text
  • just lint-ci, full suite at 100% line and branch coverage, just test-race
  • Pickling census covers DuplicateProviderTypeError with providers

…roviders, summarize finalizer errors, document the providers (#618)

@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: 63852d9 Previous: 9fca05d Ratio
benchmarks/test_guard_by_type.py::test_g16_resolve_by_type 4594412.184010674 iter/sec (stddev: 1.9964050998746996e-8) 3757940.2458935827 iter/sec (stddev: 1.9871470784386957e-8) 0.82
benchmarks/test_guard_by_type.py::test_g17_resolve_by_type_large_registry 4619484.727881559 iter/sec (stddev: 2.0789972197940685e-8) 3772690.9914374542 iter/sec (stddev: 1.0168008591359035e-8) 0.82
benchmarks/test_guard_cold.py::test_g8_cold_first_resolve 23769.971117835496 iter/sec (stddev: 0.00004143298893311027) 22103.290349830208 iter/sec (stddev: 0.00004627586827240872) 0.93
benchmarks/test_guard_cold.py::test_g8b_cold_first_resolve_cached 19196.277187909112 iter/sec (stddev: 0.0001266698100708045) 17243.220046602797 iter/sec (stddev: 0.00012370532748143167) 0.90
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[1] 631.811480845798 iter/sec (stddev: 0.00008171350152958304) 386.02246310233477 iter/sec (stddev: 0.00008801829546247881) 0.61
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[2] 609.805180590712 iter/sec (stddev: 0.00006021891277541407) 424.92173710239894 iter/sec (stddev: 0.0001408626172301536) 0.70
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[4] 575.2256746545758 iter/sec (stddev: 0.00007367620559968257) 389.9142863525594 iter/sec (stddev: 0.00012773187148944017) 0.68
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[1] 13670.817202675702 iter/sec (stddev: 0.000008618731910652419) 12894.870484989313 iter/sec (stddev: 0.000014094940280855665) 0.94
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[2] 6433.65436016382 iter/sec (stddev: 0.00002399136611744103) 6245.309512340506 iter/sec (stddev: 0.00005076579950550514) 0.97
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[4] 3269.999617405953 iter/sec (stddev: 0.00004648281020558787) 3325.602416251845 iter/sec (stddev: 0.000045069836303194324) 1.02
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[1] 10537.838083518829 iter/sec (stddev: 0.000011248528052703273) 8974.439226584482 iter/sec (stddev: 0.000010548015828080406) 0.85
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[2] 4371.7414132522645 iter/sec (stddev: 0.000022152727287610355) 3863.139277112657 iter/sec (stddev: 0.00002147797550262497) 0.88
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[4] 1944.6191900853214 iter/sec (stddev: 0.00005502692811861615) 1861.32230570072 iter/sec (stddev: 0.00004399656028164962) 0.96
benchmarks/test_guard_concurrency.py::test_g15c_worker_pool_floor_control[1] 30453.158220655034 iter/sec (stddev: 0.0000047657974525078986) 39717.21344135618 iter/sec (stddev: 0.000005937724465444655) 1.30
benchmarks/test_guard_concurrency.py::test_g15c_worker_pool_floor_control[2] 12031.26444382543 iter/sec (stddev: 0.00001928212345602406) 12926.652236162534 iter/sec (stddev: 0.000016035591671154977) 1.07
benchmarks/test_guard_concurrency.py::test_g15c_worker_pool_floor_control[4] 5534.749764981986 iter/sec (stddev: 0.00004070953878807983) 5633.227857391772 iter/sec (stddev: 0.000035472654583660356) 1.02
benchmarks/test_guard_lifecycle.py::test_g6_build_child_container 1794560.3466207052 iter/sec (stddev: 5.498757978636024e-8) 1599934.6586693635 iter/sec (stddev: 3.5092442851353725e-8) 0.89
benchmarks/test_guard_lifecycle.py::test_g6b_build_child_container_auto_scope 1945468.7066985124 iter/sec (stddev: 5.955511754930611e-8) 1677839.6103617244 iter/sec (stddev: 2.0099612510064583e-8) 0.86
benchmarks/test_guard_lifecycle.py::test_g7_request_lifecycle_batch 3006.6190893370067 iter/sec (stddev: 0.00004960438085672508) 2985.18003500368 iter/sec (stddev: 0.00001349872220770278) 0.99
benchmarks/test_guard_lifecycle.py::test_g7c_event_loop_floor_control 71378.2709451807 iter/sec (stddev: 0.0000013613548308297734) 57811.65437798101 iter/sec (stddev: 0.0000023636264035798613) 0.81
benchmarks/test_guard_lifecycle.py::test_g7b_request_cycle_sync 408275.4660701501 iter/sec (stddev: 1.0429605637520283e-7) 387336.70804320497 iter/sec (stddev: 1.1612783600768163e-7) 0.95
benchmarks/test_guard_lifecycle.py::test_g13_teardown_at_scale 62918.14291435815 iter/sec (stddev: 0.0000017306462897363167) 56429.917854609484 iter/sec (stddev: 0.000001909351631550383) 0.90
benchmarks/test_guard_lifecycle.py::test_g13b_teardown_at_scale_async_no_finalizers 599.4359759215686 iter/sec (stddev: 0.001133913367716592) 518.2335247250417 iter/sec (stddev: 0.001294428485352388) 0.86
benchmarks/test_guard_resolve.py::test_g1_transient_resolve 2669292.8949794346 iter/sec (stddev: 2.637196736785289e-8) 2142403.7566680824 iter/sec (stddev: 2.84876267436551e-8) 0.80
benchmarks/test_guard_resolve.py::test_g2_cached_resolve 4849255.791989697 iter/sec (stddev: 1.970425746450663e-8) 3391886.6816813163 iter/sec (stddev: 2.4182600235528645e-8) 0.70
benchmarks/test_guard_resolve.py::test_g3_deep_chain 891212.0807359907 iter/sec (stddev: 5.453211822120974e-8) 683099.4434992333 iter/sec (stddev: 7.622996206571738e-8) 0.77
benchmarks/test_guard_resolve.py::test_g4_wide_resolve 511639.6877771057 iter/sec (stddev: 4.539774422504683e-7) 425099.6428249923 iter/sec (stddev: 3.902063625365541e-7) 0.83
benchmarks/test_guard_resolve.py::test_g5_cross_scope 2367801.7789955237 iter/sec (stddev: 2.755111064861269e-8) 1953645.5587753574 iter/sec (stddev: 4.0531940972630803e-8) 0.83
benchmarks/test_guard_resolve.py::test_g9_context_resolve 1748886.9427710746 iter/sec (stddev: 1.5587647694664483e-7) 1360392.5385450525 iter/sec (stddev: 1.7458080708057942e-7) 0.78
benchmarks/test_guard_resolve.py::test_g12_override_active_resolve 903808.6405718289 iter/sec (stddev: 4.148867316346122e-8) 715184.2155420394 iter/sec (stddev: 8.360877431979395e-8) 0.79
benchmarks/test_guard_resolve.py::test_g18_alias_hop 4752568.906068427 iter/sec (stddev: 1.6808273071045618e-8) 3396212.855866297 iter/sec (stddev: 3.6069980524154594e-8) 0.71
benchmarks/test_guard_validate.py::test_g10_validate_deep_chain 36060.35609320815 iter/sec (stddev: 0.000019079801164790778) 36792.48962257669 iter/sec (stddev: 0.000017542859660054885) 1.02
benchmarks/test_guard_validate.py::test_g11_validate_wide 21366.950669043326 iter/sec (stddev: 0.00002887897872424291) 20992.70455232314 iter/sec (stddev: 0.00002802513785042281) 0.98

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

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