Skip to content

docs: rework the providers: context, container, alias, advanced api pages - #661

Merged
lesnik512 merged 1 commit into
mainfrom
docs/rework-providers
Oct 10, 2026
Merged

lesnik512 merged 1 commit into
mainfrom
docs/rework-providers

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Reworks four provider pages (context, container, alias, advanced API) the same way the Quickstart, Introduction and Providers pages were done. I checked every behavioural claim with a script against the checkout, and every example now asserts its result and runs under Sybil.

What was wrong

  • context.md
    • The signature said scope=Scope.APP. The default is unset, so the provider takes its group's default scope and falls back to APP only when the group sets none. With class G(Group, scope=Scope.REQUEST), G.cp.scope is REQUEST.
    • The page said every framework integration registers ContextProviders. arq, Celery and Typer register none (grep of the sibling repos, and their own pages say so). gRPC registers its provider when DIInterceptor is constructed.
    • The basic example's factory returned dict[str, str], so it registered under bare dict, and the result was a comment instead of an assert.
    • Both FastAPI blocks were never run by any recipe. fastapi is on conftest's uninstalled list, and test-docs-integrations covers only docs/integrations.
  • container.md
    • The page said a creator with a Container parameter gets the container that is resolving it. It actually gets the container at the factory's own scope: an APP factory resolved from a REQUEST child receives the APP container. Only resolve(Container) returns the calling container.
    • Both examples registered a factory under str, and their results were comments.
  • alias.md: the page said validate() raises AliasSourceNotRegisteredError and CircularDependencyError. It raises ValidationFailedError, and those errors sit in .exceptions. Only resolve() raises them directly.
  • advanced-api.md: the page said AbstractProvider appears in kwargs=, but that parameter is typed dict[str, Any]. The signatures that do name it are resolve_provider, resolve_dependency, override, reset_override, find_provider and add_providers. The modern_di.providers bullet also left out AbstractProvider, which is in its __all__.

What was added

  • context.md
    • What bound_type does: it defaults to context_type, and None makes the provider reference-only. Values are still keyed by context_type.
    • Container(context=...) on the root.
    • A context= entry with no declared provider registers nothing, so resolving it raises ProviderNotRegisteredError.
    • Propagation in the child-to-parent direction: an APP provider ignores a child's value, and an ACTION child's value does not shadow a REQUEST provider.
    • The full per-integration link list, now including Flask, aiogram, taskiq, FastMCP and gRPC.
  • container.md
    • The constructor, a table of methods that links to the page covering each one, and the corrected rule for which container you get.
    • add_providers raises DuplicateProviderTypeError on a duplicate type and clears the validated state.
    • container_provider is always APP-scoped and ignores a group default.
  • alias.md
    • An alias takes no scope= and ignores a group default; it resolves at its source's scope.
    • bound_type is not checked against the source.
    • Aliases chain, can go in kwargs, and can be reference-only.
    • The with container.override(...) form restores the prior state.
    • A runnable ValidationFailedError example.
  • advanced-api.md
    • A non-provider attribute in a subclass removes the inherited provider.
    • Passing a group together with its subclass, or one group twice, raises DuplicateProviderTypeError.

Structure

  • context.md
    • A shorter intro.
    • Runnable stand-ins (a local Request class, with add_providers standing in for setup_di()) replace the FastAPI blocks. The integration pages keep the real FastAPI code and run it under test-docs-integrations.
    • The four paragraphs on validate() ordering are now two sentences that link to writing-integrations.md#lifecycle-rules and lifecycle.md#validation.
    • The caveats under Optional parameters are now a list.
  • container.md: now a Container reference. "Resolving a provider or type" became a table row.
  • alias.md: the example comes first. "Sharing the source's cache" is folded into the paragraph after it, and the page gets a See also section.
  • advanced-api.md: the RLock bullet moved out of Container navigation into its own section, and the page gets a See also section.

Anchors kept

All of the following still resolve: context.md#framework-context-objects, #context-propagation, #optional-parameters, #when-no-value-is-set, #optional-context-default, #basic-usage; container.md#registering-providers-after-construction, #looking-up-a-provider; advanced-api.md#what-is-public, #container-navigation. "Which container you get" is now an h3 with the same slug, and nothing linked to it.

Checks

  • just test-docs: 163 passed, 19 skipped
  • just docs-build: no WARNING lines
  • just lint-ci: clean
  • lychee offline over docs and README.md: 0 errors

Library bugs found (not fixed here)

  1. Registering the same provider object twice gives a misleading DuplicateProviderTypeError.

    from modern_di import Container, Group, providers
    class A: ...
    class Base(Group):
        a = providers.Factory(A)
    class Child(Base): ...
    Container(groups=[Base, Child])  # also Container(groups=[Base, Base])

    The error lists the same Factory twice and suggests bound_type=None on one of them, which cannot help because both entries are one object. The docs describe the current behaviour.

  2. Alias.scope and its repr report APP whatever the source's scope is. For an alias of a REQUEST factory, G.a.scope is Scope.APP and the repr shows scope=<Scope.APP: 1>, yet resolving it from APP raises ScopeNotInitializedError. The AbstractProvider.scope docstring acknowledges this. Low severity.

Follow-ups outside this PR

  • The mkdocs.yml llmstxt description for providers/container.md is now stale. A possible replacement: "The Container API: building containers, a map of its methods, and injecting the container itself".
  • troubleshooting/context-not-set.md cause 1 repeats the propagation warning almost word for word, and its code comment still says "registry". It could become a link to context.md#context-propagation.
  • troubleshooting/duplicate-type-error.md and recipes/multi-group.md don't mention the group-and-subclass case.

@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: ea3e336 Previous: 395e8fb Ratio
benchmarks/test_guard_by_type.py::test_g16_resolve_by_type 5193249.108505582 iter/sec (stddev: 4.832700798314597e-9) 6139935.138982263 iter/sec (stddev: 1.8605923146541302e-8) 1.18
benchmarks/test_guard_by_type.py::test_g17_resolve_by_type_large_registry 5148485.408253094 iter/sec (stddev: 1.0361519279891035e-8) 6205832.315667191 iter/sec (stddev: 1.3805592208773412e-8) 1.21
benchmarks/test_guard_cold.py::test_g8_cold_first_resolve 33004.76952906015 iter/sec (stddev: 0.000035626795976427125) 32024.886577255867 iter/sec (stddev: 0.000030238944431934) 0.97
benchmarks/test_guard_cold.py::test_g8b_cold_first_resolve_cached 26259.227630119076 iter/sec (stddev: 0.00011349031197513305) 24960.133332489408 iter/sec (stddev: 0.00011886986138363566) 0.95
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[1] 664.069314290154 iter/sec (stddev: 0.000010501312195968394) 775.0388204206938 iter/sec (stddev: 0.00004473754687217169) 1.17
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[2] 648.20108657088 iter/sec (stddev: 0.000027028471386647455) 764.2436132277375 iter/sec (stddev: 0.0001144571715558005) 1.18
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[4] 610.7716553740476 iter/sec (stddev: 0.00003652858922399275) 680.0345534095648 iter/sec (stddev: 0.0000803400475442327) 1.11
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[1] 18939.710797303036 iter/sec (stddev: 0.000006178882028073279) 10419.935559673875 iter/sec (stddev: 0.00001230746171334532) 0.55
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[2] 9099.930431059862 iter/sec (stddev: 0.000014584247599831315) 5634.100392630293 iter/sec (stddev: 0.00003435893765767916) 0.62
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[4] 4958.980757545409 iter/sec (stddev: 0.000030113170236992703) 2968.9836953557647 iter/sec (stddev: 0.000061919224501018) 0.60
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[1] 13780.172261725564 iter/sec (stddev: 0.000005907434769069808) 12389.480670083722 iter/sec (stddev: 0.000019500905757513246) 0.90
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[2] 5118.570840453977 iter/sec (stddev: 0.00004911548898671997) 4843.535831204409 iter/sec (stddev: 0.00003981695650002605) 0.95
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[4] 2689.32874354108 iter/sec (stddev: 0.0000664608000410762) 1868.0290700812566 iter/sec (stddev: 0.00007765838735564648) 0.69
benchmarks/test_guard_concurrency.py::test_g15c_worker_pool_floor_control[1] 52820.52816992293 iter/sec (stddev: 0.0000027049146471823743) 24896.9783783031 iter/sec (stddev: 0.000017881784016521047) 0.47
benchmarks/test_guard_concurrency.py::test_g15c_worker_pool_floor_control[2] 20498.489091280735 iter/sec (stddev: 0.000010474826103190854) 11645.85197075752 iter/sec (stddev: 0.000015482573556939863) 0.57
benchmarks/test_guard_concurrency.py::test_g15c_worker_pool_floor_control[4] 8182.27490151447 iter/sec (stddev: 0.00003549769115035338) 5052.235696391264 iter/sec (stddev: 0.000048475453192008314) 0.62
benchmarks/test_guard_lifecycle.py::test_g6_build_child_container 2191179.89836369 iter/sec (stddev: 2.664977325949408e-8) 2335541.2567186337 iter/sec (stddev: 3.860763352243666e-8) 1.07
benchmarks/test_guard_lifecycle.py::test_g6b_build_child_container_auto_scope 2359945.216195241 iter/sec (stddev: 1.8543654286122994e-8) 2505907.6773532387 iter/sec (stddev: 3.905850495221895e-8) 1.06
benchmarks/test_guard_lifecycle.py::test_g7_request_lifecycle_batch 4327.2244601093535 iter/sec (stddev: 0.000005836165471820058) 3317.2697581338502 iter/sec (stddev: 0.000052891745120707064) 0.77
benchmarks/test_guard_lifecycle.py::test_g7c_event_loop_floor_control 75973.05414011616 iter/sec (stddev: 0.0000012935977206743153) 83100.58991551855 iter/sec (stddev: 0.0000014527703096612033) 1.09
benchmarks/test_guard_lifecycle.py::test_g7b_request_cycle_sync 522667.911308588 iter/sec (stddev: 5.936322345202273e-8) 468518.9908558271 iter/sec (stddev: 8.515899938405845e-8) 0.90
benchmarks/test_guard_lifecycle.py::test_g13_teardown_at_scale 82931.07378505087 iter/sec (stddev: 9.841393149813524e-7) 78785.45603292529 iter/sec (stddev: 0.0000017671089125152717) 0.95
benchmarks/test_guard_lifecycle.py::test_g13b_teardown_at_scale_async_no_finalizers 746.3619648998294 iter/sec (stddev: 0.0010258310266080847) 586.9668417253587 iter/sec (stddev: 0.0013708543619692236) 0.79
benchmarks/test_guard_resolve.py::test_g1_transient_resolve 3048979.6475114 iter/sec (stddev: 2.5260577130181994e-8) 3091773.339921797 iter/sec (stddev: 3.1905368666696455e-8) 1.01
benchmarks/test_guard_resolve.py::test_g2_cached_resolve 4987641.870338065 iter/sec (stddev: 5.2076781642349365e-9) 5585002.481125255 iter/sec (stddev: 1.7326975696337195e-8) 1.12
benchmarks/test_guard_resolve.py::test_g3_deep_chain 1013994.3386634365 iter/sec (stddev: 3.0204017517021995e-8) 1053634.7223852123 iter/sec (stddev: 4.376268176758002e-8) 1.04
benchmarks/test_guard_resolve.py::test_g4_wide_resolve 607734.7864826723 iter/sec (stddev: 3.41578665705509e-7) 602475.4027370019 iter/sec (stddev: 3.669391702377282e-7) 0.99
benchmarks/test_guard_resolve.py::test_g5_cross_scope 2730082.1058545928 iter/sec (stddev: 1.4888265276725142e-8) 2722743.1964451196 iter/sec (stddev: 2.2608711961763022e-8) 1.00
benchmarks/test_guard_resolve.py::test_g9_context_resolve 2050711.3225539043 iter/sec (stddev: 1.285977942500552e-7) 2095884.4060996436 iter/sec (stddev: 1.4539795764318154e-7) 1.02
benchmarks/test_guard_resolve.py::test_g12_override_active_resolve 1055069.3444233427 iter/sec (stddev: 3.65894966499322e-8) 1056924.0255533508 iter/sec (stddev: 4.539064934432262e-8) 1.00
benchmarks/test_guard_resolve.py::test_g18_alias_hop 4977322.324047423 iter/sec (stddev: 6.584826655971779e-9) 5564689.374851157 iter/sec (stddev: 1.595292015837515e-8) 1.12
benchmarks/test_guard_validate.py::test_g10_validate_deep_chain 55211.49693481896 iter/sec (stddev: 0.000015206195340001708) 23951.926949647954 iter/sec (stddev: 0.000031960404140896095) 0.43
benchmarks/test_guard_validate.py::test_g11_validate_wide 31184.045419594546 iter/sec (stddev: 0.000021337896687603556) 21724.253571453744 iter/sec (stddev: 0.00003130808206721572) 0.70

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

@lesnik512
lesnik512 merged commit a67829c into main Oct 10, 2026
12 checks passed
@lesnik512
lesnik512 deleted the docs/rework-providers branch October 10, 2026 19:39
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