Skip to content

feat!: tighten the provider surface (#566) - #589

Merged
lesnik512 merged 1 commit into
mainfrom
feat/provider-surface
Oct 5, 2026
Merged

lesnik512 merged 1 commit into
mainfrom
feat/provider-surface

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Closes #566.

Summary

  • Internal provider methods are now underscore-prefixed: AbstractProvider.get_dependencies, redirect_target, iter_validation_issues; Factory.wiring_plan, can_call_positionally, resolution_step; Alias.find_source; CacheSettings.coerce.
  • Factory(cache=) accepts bool | CacheSettings only. The default is now False. cache=None, or any other value, raises TypeError naming the value and the fix.
  • AbstractProvider is a plain typing.Generic class; abc.ABC is gone.
  • modern_di/__init__.py imports providers itself, so modern_di.providers no longer depends on container.py importing it first.
  • docs/providers/factories.md and docs/migration/to-4.x.md updated, with one migration entry per removed name.

Design decisions

  • Rule used for each method: underscore it when its call sites outside its own class and outside resolver_compiler.py (exempt from SLF001) need at most one # noqa: SLF001. Counts:
    • get_dependencies, redirect_target, iter_validation_issues: one site each, all in dependency_graph.py (lines with a noqa).
    • wiring_plan, can_call_positionally, resolution_step: only resolver_compiler.py.
    • find_source: only Alias itself.
    • CacheSettings.coerce: one site, Factory.__init__.
    • mark_registered: two sites, so it stays public (see Open questions).
  • Public API kept: constructors, bound_type, scope, provider_id, display_name, definition_site, cache_settings, ContextProvider.context_type / default. Integrations only use AbstractProvider as a type (modern-di-pytest, -fastapi, -faststream, -litestar, -taskiq); none call any renamed method.
  • cache= rejects every value that is not a bool or CacheSettings, not just None. Before, cache=1 was silently stored as cache_settings. The error is a plain TypeError, like Python's own error for a bad keyword, so it has no docs_slug or troubleshooting page.
  • New tests: Factory(cache=None|1|"yes") raises; type(AbstractProvider) is type; every name in modern_di.__all__ is bound by an import in __init__ (AST check, which failed before the change).

Timings (Python 3.14.7, best of 7 x 2M calls, lambda overhead included)

before after
isinstance(factory, AbstractProvider) 102.8 ns 23.4 ns
isinstance(SomeClass, AbstractProvider) 105.3 ns 29.0 ns
Container.resolve_dependency(cached_factory) 251.7 ns 163.2 ns

Open questions

  • AbstractProvider.mark_registered stays public. Underscoring it would need two # noqa: SLF001, at modern_di/registries/providers_registry.py:145 and :165 (both in ProvidersRegistry). Should it be private anyway, or should providers_registry.py join the SLF001 exemption?

Test plan

  • just lint and just lint-ci clean
  • just test-ci: 575 passed, 100% line coverage
  • mkdocs build --strict
  • New tests fail on main, pass here

@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: ff75f76 Previous: b217d41 Ratio
benchmarks/test_guard_by_type.py::test_g16_resolve_by_type 3085529.3301762324 iter/sec (stddev: 2.130845158709925e-8) 3051290.1739530643 iter/sec (stddev: 1.899336899848768e-8) 0.99
benchmarks/test_guard_by_type.py::test_g17_resolve_by_type_large_registry 3097385.0698529356 iter/sec (stddev: 2.5331446362372795e-8) 3098533.3928946564 iter/sec (stddev: 1.612550876831143e-8) 1.00
benchmarks/test_guard_cold.py::test_g8_cold_first_resolve 20216.977278792583 iter/sec (stddev: 0.00006168690542843038) 19342.161015171005 iter/sec (stddev: 0.00005195636868147634) 0.96
benchmarks/test_guard_cold.py::test_g8b_cold_first_resolve_cached 16391.372285013345 iter/sec (stddev: 0.00016604891809457185) 15557.072043312739 iter/sec (stddev: 0.00012133655653996784) 0.95
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[1] 345.0137135095071 iter/sec (stddev: 0.00004906592870982926) 336.9942257637145 iter/sec (stddev: 0.00004241326422012053) 0.98
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[2] 330.54735455227495 iter/sec (stddev: 0.00006537614475280445) 324.65693933586215 iter/sec (stddev: 0.000032423015870784556) 0.98
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[4] 324.64274120639305 iter/sec (stddev: 0.0000635695184015499) 307.00338088231155 iter/sec (stddev: 0.00004861079528143017) 0.95
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[1] 1910.2172730281966 iter/sec (stddev: 0.00017228315982151856) 1720.6321677132655 iter/sec (stddev: 0.0001616368065884017) 0.90
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[2] 1470.2521853616379 iter/sec (stddev: 0.00017825413620022683) 1366.9873934254185 iter/sec (stddev: 0.0002060694942949656) 0.93
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[4] 1019.9230493526372 iter/sec (stddev: 0.00020853974535277775) 951.1811204375468 iter/sec (stddev: 0.0002041261068180413) 0.93
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[1] 1879.6957443009046 iter/sec (stddev: 0.00021099943696166516) 1772.3003457138411 iter/sec (stddev: 0.00018834625443126438) 0.94
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[2] 1298.5410750355734 iter/sec (stddev: 0.00019526047371561336) 1258.922957418992 iter/sec (stddev: 0.0001848620837711849) 0.97
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[4] 742.4152353868143 iter/sec (stddev: 0.0016055646704167862) 711.6081277463601 iter/sec (stddev: 0.0013285930653377468) 0.96
benchmarks/test_guard_lifecycle.py::test_g6_build_child_container 811452.3022552484 iter/sec (stddev: 3.5675749009103553e-7) 934598.7426451171 iter/sec (stddev: 9.666444604239457e-8) 1.15
benchmarks/test_guard_lifecycle.py::test_g6b_build_child_container_auto_scope 822023.3706422871 iter/sec (stddev: 2.0585459882145103e-7) 864093.1222795943 iter/sec (stddev: 4.6211436587660205e-8) 1.05
benchmarks/test_guard_lifecycle.py::test_g7_request_lifecycle_batch 2465.6858198341574 iter/sec (stddev: 0.000014504964028720355) 2454.641585404389 iter/sec (stddev: 0.000019664538636602372) 1.00
benchmarks/test_guard_lifecycle.py::test_g7c_event_loop_floor_control 56380.55131830674 iter/sec (stddev: 0.0000026768609331507602) 56319.68717451932 iter/sec (stddev: 0.0000019232483710720968) 1.00
benchmarks/test_guard_lifecycle.py::test_g13_teardown_at_scale 42594.422005397035 iter/sec (stddev: 0.0000026906114611533046) 42261.349899341985 iter/sec (stddev: 0.0000021383037041670694) 0.99
benchmarks/test_guard_lifecycle.py::test_g13b_teardown_at_scale_async_no_finalizers 539.372050148053 iter/sec (stddev: 0.00003292677896071759) 558.1719296431494 iter/sec (stddev: 0.00002707394828739131) 1.03
benchmarks/test_guard_resolve.py::test_g1_transient_resolve 2014331.3632315653 iter/sec (stddev: 2.6340150261360775e-8) 2017593.8216257344 iter/sec (stddev: 2.369375232643272e-8) 1.00
benchmarks/test_guard_resolve.py::test_g2_cached_resolve 2878107.816790032 iter/sec (stddev: 8.52047336737628e-9) 2750958.076187929 iter/sec (stddev: 1.5809560276093075e-8) 0.96
benchmarks/test_guard_resolve.py::test_g3_deep_chain 697430.8287190774 iter/sec (stddev: 5.276837847892658e-8) 679984.6996619223 iter/sec (stddev: 4.0089485670710627e-8) 0.97
benchmarks/test_guard_resolve.py::test_g4_wide_resolve 435944.58106957027 iter/sec (stddev: 5.241114692500745e-7) 453519.71663432627 iter/sec (stddev: 4.868584238541877e-7) 1.04
benchmarks/test_guard_resolve.py::test_g5_cross_scope 1865139.1550462772 iter/sec (stddev: 2.6149272761389316e-8) 1850823.0170494418 iter/sec (stddev: 5.328416278809819e-8) 0.99
benchmarks/test_guard_resolve.py::test_g9_context_resolve 1334236.5670705687 iter/sec (stddev: 1.788862946434627e-7) 1324481.0319031694 iter/sec (stddev: 1.5210061635352964e-7) 0.99
benchmarks/test_guard_resolve.py::test_g12_override_active_resolve 671262.4200752133 iter/sec (stddev: 2.2511773521939864e-7) 693200.0060204576 iter/sec (stddev: 5.332537180219424e-8) 1.03
benchmarks/test_guard_resolve.py::test_g18_alias_hop 2861188.168426083 iter/sec (stddev: 8.911754541475025e-9) 2913071.244886299 iter/sec (stddev: 1.1246686611147843e-8) 1.02
benchmarks/test_guard_validate.py::test_g10_validate_deep_chain 26198.973078367526 iter/sec (stddev: 0.000021141571691406735) 26007.765589254235 iter/sec (stddev: 0.00002273436297771503) 0.99
benchmarks/test_guard_validate.py::test_g11_validate_wide 15630.522947399479 iter/sec (stddev: 0.000025578226035471673) 15614.049915556076 iter/sec (stddev: 0.000026879403282835526) 1.00

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

Make internal provider methods private, narrow Factory(cache=) to bool | CacheSettings, drop abc.ABC from AbstractProvider, and import providers in modern_di/__init__.
@lesnik512
lesnik512 force-pushed the feat/provider-surface branch from 2aaedfb to ff75f76 Compare October 5, 2026 08:47
@lesnik512
lesnik512 merged commit dc429b5 into main Oct 5, 2026
9 checks passed
@lesnik512
lesnik512 deleted the feat/provider-surface branch October 5, 2026 08:48
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.

4.0: tighten the provider surface

1 participant