feat!: tighten Container's public surface (#565) - #592
Merged
Merged
Conversation
There was a problem hiding this comment.
Benchmark
Details
| Benchmark suite | Current: d026aae | Previous: 6b6bae8 | Ratio |
|---|---|---|---|
benchmarks/test_guard_by_type.py::test_g16_resolve_by_type |
4938407.685503681 iter/sec (stddev: 1.3564386618137858e-8) |
3260457.4363020025 iter/sec (stddev: 2.562881313164587e-8) |
0.66 |
benchmarks/test_guard_by_type.py::test_g17_resolve_by_type_large_registry |
4989555.114785454 iter/sec (stddev: 7.287019695830428e-9) |
3345379.823064965 iter/sec (stddev: 2.3037199812108472e-8) |
0.67 |
benchmarks/test_guard_cold.py::test_g8_cold_first_resolve |
28551.680136101604 iter/sec (stddev: 0.000043537523519490644) |
19972.199535381777 iter/sec (stddev: 0.00005420123255749911) |
0.70 |
benchmarks/test_guard_cold.py::test_g8b_cold_first_resolve_cached |
23045.661581024702 iter/sec (stddev: 0.00011389532155588124) |
15929.163443361573 iter/sec (stddev: 0.0001250899737435769) |
0.69 |
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[1] |
638.6083950434078 iter/sec (stddev: 0.000033232247666545824) |
345.1288957057148 iter/sec (stddev: 0.00003575619732955365) |
0.54 |
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[2] |
584.0407147946878 iter/sec (stddev: 0.0000541700039184801) |
323.1251382731525 iter/sec (stddev: 0.0001643208237976516) |
0.55 |
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[4] |
500.99792499226436 iter/sec (stddev: 0.00006881635503768241) |
303.40565343582915 iter/sec (stddev: 0.00021911545461739438) |
0.61 |
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[1] |
2553.5499643144235 iter/sec (stddev: 0.00013645812803904665) |
1925.224628389066 iter/sec (stddev: 0.00017411413888552155) |
0.75 |
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[2] |
1945.3061610381121 iter/sec (stddev: 0.0001591873289718167) |
1486.1759995513646 iter/sec (stddev: 0.00017960699173028347) |
0.76 |
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[4] |
1315.267058838599 iter/sec (stddev: 0.00016286785775797126) |
1030.401002703205 iter/sec (stddev: 0.00018547248997487608) |
0.78 |
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[1] |
2506.4285191970725 iter/sec (stddev: 0.00017419374628923525) |
1867.7688453618966 iter/sec (stddev: 0.00021269549818429573) |
0.75 |
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[2] |
1430.4492167217722 iter/sec (stddev: 0.0013177158723345868) |
1131.7276576227218 iter/sec (stddev: 0.0013758238078706761) |
0.79 |
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[4] |
1030.512442922103 iter/sec (stddev: 0.0002021134023463434) |
841.6653114104772 iter/sec (stddev: 0.0001919974716285053) |
0.82 |
benchmarks/test_guard_lifecycle.py::test_g6_build_child_container |
1216562.0691942126 iter/sec (stddev: 5.859939241050908e-8) |
934982.0276565943 iter/sec (stddev: 3.8517759045937046e-8) |
0.77 |
benchmarks/test_guard_lifecycle.py::test_g6b_build_child_container_auto_scope |
1108696.5547968317 iter/sec (stddev: 3.891923102247578e-8) |
851047.3701704049 iter/sec (stddev: 3.892838831244081e-8) |
0.77 |
benchmarks/test_guard_lifecycle.py::test_g7_request_lifecycle_batch |
3313.413880406987 iter/sec (stddev: 0.000017470981015691367) |
2457.019595826527 iter/sec (stddev: 0.00004081942717020917) |
0.74 |
benchmarks/test_guard_lifecycle.py::test_g7c_event_loop_floor_control |
75045.3021736796 iter/sec (stddev: 0.0000020771337586261562) |
58089.307373110685 iter/sec (stddev: 0.0000016792803452722592) |
0.77 |
benchmarks/test_guard_lifecycle.py::test_g13_teardown_at_scale |
57480.091208809106 iter/sec (stddev: 0.0000020683764558980826) |
42890.73815290032 iter/sec (stddev: 0.0000018687946096749885) |
0.75 |
benchmarks/test_guard_lifecycle.py::test_g13b_teardown_at_scale_async_no_finalizers |
764.4260206904556 iter/sec (stddev: 0.00003279946600047198) |
556.6901383417683 iter/sec (stddev: 0.000026268509821701806) |
0.73 |
benchmarks/test_guard_resolve.py::test_g1_transient_resolve |
2920995.6126003796 iter/sec (stddev: 2.8633016799226372e-8) |
1967768.3480172108 iter/sec (stddev: 2.8281562436705376e-8) |
0.67 |
benchmarks/test_guard_resolve.py::test_g2_cached_resolve |
4863925.601398329 iter/sec (stddev: 1.0609551872308175e-8) |
2813177.2372624027 iter/sec (stddev: 8.052988898495071e-9) |
0.58 |
benchmarks/test_guard_resolve.py::test_g3_deep_chain |
1026416.2594177481 iter/sec (stddev: 4.079585653740863e-8) |
698436.1647584069 iter/sec (stddev: 4.578476629415878e-8) |
0.68 |
benchmarks/test_guard_resolve.py::test_g4_wide_resolve |
605856.6526810711 iter/sec (stddev: 4.4790071820107344e-7) |
438601.83909269725 iter/sec (stddev: 4.755224725919662e-7) |
0.72 |
benchmarks/test_guard_resolve.py::test_g5_cross_scope |
2774163.8514061123 iter/sec (stddev: 2.7893142171725838e-8) |
1863248.9835879544 iter/sec (stddev: 2.7817473351082116e-8) |
0.67 |
benchmarks/test_guard_resolve.py::test_g9_context_resolve |
1987252.6683974515 iter/sec (stddev: 1.4952259786671056e-7) |
1389722.2396839291 iter/sec (stddev: 1.6220603917392167e-7) |
0.70 |
benchmarks/test_guard_resolve.py::test_g12_override_active_resolve |
994155.6077189809 iter/sec (stddev: 1.1438723919054846e-7) |
704292.2846812549 iter/sec (stddev: 4.5443606775615695e-8) |
0.71 |
benchmarks/test_guard_resolve.py::test_g18_alias_hop |
4873321.183489031 iter/sec (stddev: 1.0202478312999676e-8) |
2822972.3098096806 iter/sec (stddev: 8.430114761076153e-9) |
0.58 |
benchmarks/test_guard_validate.py::test_g10_validate_deep_chain |
37927.1751964395 iter/sec (stddev: 0.000017131175186896323) |
26247.90571255939 iter/sec (stddev: 0.000023063125486108578) |
0.69 |
benchmarks/test_guard_validate.py::test_g11_validate_wide |
21873.309238759535 iter/sec (stddev: 0.000022048712619985966) |
15589.023780817508 iter/sec (stddev: 0.00002714007872121336) |
0.71 |
This comment was automatically generated by workflow using github-action-benchmark.
lesnik512
force-pushed
the
feat/container-surface
branch
from
October 5, 2026 08:51
235342f to
d026aae
Compare
This was referenced Oct 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #565.
Summary
Container.find_provider(dependency_type), returning the registered provider orNone. It delegates to the providers registry lookup, so a child answers the same as its root, and it ignores overrides and the closed state._providers_registry,_cache_registry,_context_registry. Theoverrides_registryslot is gone; overrides are reached through_providers_registry.overrides, so there is one name for one object.closedis a read-only property backed by_closed.open(),close_sync()andclose_async()set the private field; assigningclosedraisesAttributeError.Container(scope=..., parent_container=p, groups=[G])registeredGinto the tree-wide registry. It now raisesChildContainerRegistrationError, the same erroradd_providersraises on a child.resolvecheckedclosedafter the registry lookup, so a closed container asked for an unregistered type raisedProviderNotRegisteredError. Bothresolveandresolve_providernow check the closed state first.build_child_container,find_container,close_sync,close_async,reset_overrideand the newclosedproperty.scopes.mdno longer names registry attributes,container.mdgains a "Looking up a provider" section, andlifecycle.md,errors-and-exceptions.mdand theChildContainerRegistrationErrortroubleshooting page cover the constructor case. Four migration entries into-4.x.md.Design decisions
ChildContainerRegistrationErrornow has two raise sites, and its old message ("Container.add_providers can only be called on a root container") was wrong for the constructor path. The message is now general and suggestsgroups=on the root oradd_providers. Any test that matched the old text needs updating; the migration entry says so.providers/alias.pyuses the newcontainer.find_provider.dependency_graph.collect_errorstakes the registry as an argument;Container.validatepasses it in._handle_recursion_errortakes the registry as an argument for the same reason.resolver_compiler.pyis SLF001-exempt and reads_closedand the registries directly, so the generated resolvers do not pay for a property call.noqa: SLF001added in two places:providers/factory.py(get_dependenciesanditer_validation_issues, one each).Factory.wiring_planneeds the registry itself (plan memo plus the suggester), and the provider hooks receive aContainer. Threading the registry through every hook (get_dependencies,redirect_target,iter_validation_issues, and thedependency_graphhelpers, about 58 call sites) was too large a change for this issue.container.py__init__, for the child readingparent_container._providers_registry. This is a same-class read next to the existing_lockand_scope_mapnoqa lines.resolve, the closed check sits inside thetry, as before, so aContainerClosedErrorfrom anAliastype still gets its redirect hops prepended.test_failed_group_registration_does_not_pollute_shared_registryused a child constructor withgroups=to provoke a duplicate. That path now raises before registering, so the test goes throughadd_providerson the root instead and still checks that a failed batch registers nothing.Hot-path timing
timeitofc.resolve(T)on a warm root (stmtr(tp)with a bound method, best of 21 x 1M,python -Sagainst anorigin/mainexport and this branch, 5 alternating runs, median):Both differences are within run-to-run noise (about 2%), so the closed-first order stays.
Downstream
modern-di-grpc reads the registry directly and must switch to
container.find_provider(ServicerContext)(tracked in #579):modern_di_grpc/main.py:45tests/test_inject.py:191Test plan
test_child_constructor_with_groups_raisestest_resolve_unregistered_type_on_closed_container_raises_closed(resolve_provideralready checked first;test_resolve_provider_unregistered_on_closed_container_raises_closedguards it)test_find_provider_returns_registered_provider_or_none,test_closed_is_read_only,test_registries_are_not_publicjust lint,just lint-cijust test-ci(578 passed, 100% coverage)pytest benchmarks/ --ignore=benchmarks/comparative)mkdocs build --strict