Skip to content

docs: rework the errors catalog and the scope, container and lifecycle troubleshooting pages - #664

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

lesnik512 merged 1 commit into
mainfrom
docs/rework-errors-a

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Reworks docs/providers/errors-and-exceptions.md and twelve troubleshooting pages: InvalidChildScopeError, MaxScopeReachedError, ScopeNotInitializedError, ScopeSkippedError, InvalidScopeTypeError, ContainerClosedError, ValidationFailedError, scope-chain (InvalidScopeDependencyError), ScopeEnumMismatchError, FinalizerError, AsyncFinalizerInSyncCloseError and ChildContainerRegistrationError. I checked every behavioural claim by running a script against this checkout. Every quoted error block was regenerated from a small myapp/providers.py module, so the module:line locations match what a real run prints.

What was wrong

  • InvalidChildScopeError said Container(...) raises it. Only build_child_container(scope=...) does (container.py; a root at Scope.REQUEST builds fine).
  • ScopeNotInitializedError said the dependency chain shows up only for a captive dependency. resolve() and resolve_provider() always draw it, even for a direct resolve. The page also said "see the scope dependency rule below", and nothing below it said that.
  • ScopeNotInitializedError and ScopeSkippedError said a chain line points at "where that provider was declared". It points at the creator's definition (code.co_firstlineno of the class or function), not at the Factory(...) line.
  • scope-chain and ScopeEnumMismatchError built their Broken and Works providers in one class body. The second binding silently replaced the first, so the broken provider was never registered and the examples demonstrated nothing.
  • scope-chain said a violation "reads identically" from validate() and from a resolve. Only the tree lines match. The headline and the caused by line differ.
  • ContainerClosedError quoted the message without the class prefix and the See: line. docs: quote errors the way Python prints them on troubleshooting pages #648 converted the other pages to that form.
  • ValidationFailedError showed Shallow and Deep without locations. Real output always has one.
  • errors-and-exceptions.md said ValidationFailedError's str() is one line. It is two: the summary, then the See: line. Its security note said CreatorCallError embeds the wrapped exception's text, as if creator exceptions were wrapped. Only argument-binding TypeErrors are wrapped, and exceptions from a creator body propagate unchanged.
  • In seven pages a <!-- raises --> block put the Works code after the raising line. Sybil stops at the first raise, so the Works code never ran. Each pair is now two blocks, and every Works block runs.

What was added

  • InvalidChildScopeError: .allowed_scopes and "Possible scopes are" list only the parent's own enum, so a STEP parent shows [] even though a higher-valued member of another enum is accepted.
  • MaxScopeReachedError: a child built from a one-member custom enum raises again on the next call without scope=. Using one full IntEnum in place of Scope lets every level derive its scope, but then every provider needs that enum's members. A Scope.APP provider raises ScopeSkippedError in an AppScope.APP container.
  • ScopeNotInitializedError: the captive case names container scope APP even when you resolve from a REQUEST child, and building the deeper container does not fix it.
  • ScopeSkippedError: a third cause, a provider scope from a different enum than the containers.
  • InvalidScopeTypeError: the message, the rejected kinds (int, str, plain Enum, IntFlag), and converting a config value with Scope(3) or Scope["REQUEST"].
  • ContainerClosedError: a finalizer that resolves from its own container gets the error as an entry inside a FinalizerError.
  • FinalizerError: finalizers run newest first; a finalizer that raised is not retried, its instance leaves the cache, and a reopen builds a new one; except* re-raises the remainder as a FinalizerError.
  • AsyncFinalizerInSyncCloseError: the printed group, the sync-finalizer-returning-an-awaitable case, a repeated close_sync() reporting it again, a reopen returning the kept instance, and a link to lifecycle.md#async-finalizers-and-close_sync.
  • ChildContainerRegistrationError: the message, that any root works whatever its scope, and the real reason to register at startup (add_providers is not coordinated with concurrent resolves). This replaces the advice against walking parent_container, which gave no reason.

Structure

  • Every troubleshooting page now follows Symptom (where it is raised, the full printed error in a text fence, the attributes), Cause, Fix, See also. The two "Escape hatches" sections and scope-chain's "How to detect" section were folded into Fix.
  • scope-chain.md's H1 is now # InvalidScopeDependencyError, as docs/agents/docs-style.md requires. The file name and slug did not change.
  • errors-and-exceptions.md is now a catalog: the hierarchy, a "Catching errors" section with runnable examples, one table per category (Error, Raised by, Inspect, with a link to the page), the dependency_path paragraph, pickling, and a rewritten security note. Cause and fix text now lives only on the troubleshooting pages. The dropped details already exist elsewhere: the threading caveat in circular-dependency.md, the post-add_providers validate advice in lifecycle.md#validation, and the closed-container rules in container-closed-error.md.

Anchors kept

  • scope-chain.md#when-the-dependency-is-reached-through-an-alias, linked from providers/alias.md.
  • Every page path. They are bound by tests/test_docs_slug_census.py and by the See: URLs.
  • The ## Hierarchy, ## Pickling and copying and ## Security note headings on errors-and-exceptions.md. Nothing links to an anchor there, but they stay anyway.

Checks

  • just test-docs -q -p no:cacheprovider --no-cov: 167 passed, 21 skipped (the skips are pages outside this batch). All 85 examples in the batch pages pass.
  • tests/test_docs_slug_census.py: 3 passed.
  • just docs-build: no WARNING lines. Anchor validation is on.
  • just lint-ci: passes.
  • lychee --offline over docs and README.md: 0 errors.

Library bugs found (not fixed here)

  1. InvalidChildScopeError lists no allowed scopes even when another enum's higher member would be accepted. _scope_algebra.deeper_members looks only at type(parent_scope), while build_child_container accepts any IntEnum with a higher value.

    import enum
    from modern_di import Container, Scope
    
    class MyScope(enum.IntEnum):
        SUBSTEP = Scope.STEP + 1
    
    step = Container(scope=Scope.STEP)
    step.build_child_container(scope=MyScope.SUBSTEP)  # accepted
    step.build_child_container(scope=Scope.STEP)  # "... Possible scopes are []."
  2. The ScopeSkippedError message contradicts itself across enums. A Scope.APP provider resolved in a root built at AppScope.APP (a custom enum with APP = 1) prints No APP-scope container exists in this chain, which starts at APP. Build the root container at scope APP. The message uses .name only, so the two scopes look the same. The page now explains this case.

  3. An Alias's repr reports scope=APP when its source is REQUEST-scoped, so .dependency_provider looks wrong on an InvalidScopeDependencyError reached through an alias:

    class D(Group):
        session = providers.Factory(Session, scope=Scope.REQUEST)
        repo = providers.Alias(Session, bound_type=Repository)
        user_cache = providers.Factory(UserCache)  # takes Repository
    # validate() -> e.exceptions[0].dependency_provider
    # Alias(source_type=<class 'Session'>, bound_type=<class 'Repository'>, scope=<Scope.APP: 1>)

    This only affects the repr. The pages do not show it.

Out-of-batch changes needed

  • mkdocs.yml nav "Scope chain violation", docs/providers/alias.md:115 "[Troubleshooting: Scope chain]" and docs/recipes/good-and-bad-practices.md:36 "[Scope chain violation]" should become InvalidScopeDependencyError to match the new H1.
  • docs/troubleshooting/creator-call-error.md should pick up a fact this PR cut from the catalog: a ResolutionError raised by container.resolve() inside a creator body gains the creator's step in its dependency_path.
  • docs/troubleshooting/context-not-set.md could link migration/to-4.x.md#a-missing-context-value-for-a-required-parameter-raises-contextvaluenotseterror, which the catalog no longer links. The catalog also no longer links migration/to-3.x.md.
  • docs/providers/lifecycle.md should be checked against the new FinalizerError and AsyncFinalizerInSyncCloseError facts: a failed finalizer is not retried, and a reopen returns the kept async instance.
  • docs/troubleshooting/unsupported-creator-parameter-error.md still has an "Escape hatches" section.

@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: 94eb244 Previous: 395e8fb Ratio
benchmarks/test_guard_by_type.py::test_g16_resolve_by_type 5791007.966065862 iter/sec (stddev: 1.8643690258731708e-8) 6139935.138982263 iter/sec (stddev: 1.8605923146541302e-8) 1.06
benchmarks/test_guard_by_type.py::test_g17_resolve_by_type_large_registry 5904501.42064673 iter/sec (stddev: 1.7013081044220067e-8) 6205832.315667191 iter/sec (stddev: 1.3805592208773412e-8) 1.05
benchmarks/test_guard_cold.py::test_g8_cold_first_resolve 26963.305164746904 iter/sec (stddev: 0.00003579260576571272) 32024.886577255867 iter/sec (stddev: 0.000030238944431934) 1.19
benchmarks/test_guard_cold.py::test_g8b_cold_first_resolve_cached 21950.16802884705 iter/sec (stddev: 0.00013897720713502474) 24960.133332489408 iter/sec (stddev: 0.00011886986138363566) 1.14
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[1] 749.3157421922463 iter/sec (stddev: 0.00007192406478698934) 775.0388204206938 iter/sec (stddev: 0.00004473754687217169) 1.03
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[2] 695.4277934590127 iter/sec (stddev: 0.0001044457032864375) 764.2436132277375 iter/sec (stddev: 0.0001144571715558005) 1.10
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[4] 629.4568306994992 iter/sec (stddev: 0.00009424951577115076) 680.0345534095648 iter/sec (stddev: 0.0000803400475442327) 1.08
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[1] 10446.409917088471 iter/sec (stddev: 0.000011783454554986201) 10419.935559673875 iter/sec (stddev: 0.00001230746171334532) 1.00
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[2] 7872.2248685374625 iter/sec (stddev: 0.000015077355303843093) 5634.100392630293 iter/sec (stddev: 0.00003435893765767916) 0.72
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[4] 2971.418666696458 iter/sec (stddev: 0.00006208300262172865) 2968.9836953557647 iter/sec (stddev: 0.000061919224501018) 1.00
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[1] 12289.087259800075 iter/sec (stddev: 0.000018729178871096452) 12389.480670083722 iter/sec (stddev: 0.000019500905757513246) 1.01
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[2] 5077.343598138035 iter/sec (stddev: 0.000014520016425001374) 4843.535831204409 iter/sec (stddev: 0.00003981695650002605) 0.95
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[4] 1838.032682550074 iter/sec (stddev: 0.0001668885758254382) 1868.0290700812566 iter/sec (stddev: 0.00007765838735564648) 1.02
benchmarks/test_guard_concurrency.py::test_g15c_worker_pool_floor_control[1] 18871.218089798083 iter/sec (stddev: 0.00000439590192695075) 24896.9783783031 iter/sec (stddev: 0.000017881784016521047) 1.32
benchmarks/test_guard_concurrency.py::test_g15c_worker_pool_floor_control[2] 12781.43113684052 iter/sec (stddev: 0.0000075203522892032244) 11645.85197075752 iter/sec (stddev: 0.000015482573556939863) 0.91
benchmarks/test_guard_concurrency.py::test_g15c_worker_pool_floor_control[4] 5095.359872308934 iter/sec (stddev: 0.00004830947915217749) 5052.235696391264 iter/sec (stddev: 0.000048475453192008314) 0.99
benchmarks/test_guard_lifecycle.py::test_g6_build_child_container 2424069.3603774505 iter/sec (stddev: 2.733076405774691e-8) 2335541.2567186337 iter/sec (stddev: 3.860763352243666e-8) 0.96
benchmarks/test_guard_lifecycle.py::test_g6b_build_child_container_auto_scope 2465853.173488663 iter/sec (stddev: 4.603663361447113e-8) 2505907.6773532387 iter/sec (stddev: 3.905850495221895e-8) 1.02
benchmarks/test_guard_lifecycle.py::test_g7_request_lifecycle_batch 3634.938704985746 iter/sec (stddev: 0.000014816109673592282) 3317.2697581338502 iter/sec (stddev: 0.000052891745120707064) 0.91
benchmarks/test_guard_lifecycle.py::test_g7c_event_loop_floor_control 87589.81205554071 iter/sec (stddev: 0.0000012952220151239049) 83100.58991551855 iter/sec (stddev: 0.0000014527703096612033) 0.95
benchmarks/test_guard_lifecycle.py::test_g7b_request_cycle_sync 542774.6891790937 iter/sec (stddev: 7.268976616449304e-8) 468518.9908558271 iter/sec (stddev: 8.515899938405845e-8) 0.86
benchmarks/test_guard_lifecycle.py::test_g13_teardown_at_scale 77107.5785474624 iter/sec (stddev: 0.0000014563390785576194) 78785.45603292529 iter/sec (stddev: 0.0000017671089125152717) 1.02
benchmarks/test_guard_lifecycle.py::test_g13b_teardown_at_scale_async_no_finalizers 699.017103213025 iter/sec (stddev: 0.0011576839780805014) 586.9668417253587 iter/sec (stddev: 0.0013708543619692236) 0.84
benchmarks/test_guard_resolve.py::test_g1_transient_resolve 3107493.884649706 iter/sec (stddev: 2.3370309230035162e-8) 3091773.339921797 iter/sec (stddev: 3.1905368666696455e-8) 0.99
benchmarks/test_guard_resolve.py::test_g2_cached_resolve 5500852.632156926 iter/sec (stddev: 1.532794690592437e-8) 5585002.481125255 iter/sec (stddev: 1.7326975696337195e-8) 1.02
benchmarks/test_guard_resolve.py::test_g3_deep_chain 1027810.4700035306 iter/sec (stddev: 5.5623204055786324e-8) 1053634.7223852123 iter/sec (stddev: 4.376268176758002e-8) 1.03
benchmarks/test_guard_resolve.py::test_g4_wide_resolve 546661.1209718777 iter/sec (stddev: 3.7076261829182327e-7) 602475.4027370019 iter/sec (stddev: 3.669391702377282e-7) 1.10
benchmarks/test_guard_resolve.py::test_g5_cross_scope 2638970.495376011 iter/sec (stddev: 4.40573711860842e-8) 2722743.1964451196 iter/sec (stddev: 2.2608711961763022e-8) 1.03
benchmarks/test_guard_resolve.py::test_g9_context_resolve 2028689.6272748925 iter/sec (stddev: 1.5075277710791751e-7) 2095884.4060996436 iter/sec (stddev: 1.4539795764318154e-7) 1.03
benchmarks/test_guard_resolve.py::test_g12_override_active_resolve 1026564.143709598 iter/sec (stddev: 4.653118499678925e-8) 1056924.0255533508 iter/sec (stddev: 4.539064934432262e-8) 1.03
benchmarks/test_guard_resolve.py::test_g18_alias_hop 5375044.827865133 iter/sec (stddev: 1.4534574274358616e-8) 5564689.374851157 iter/sec (stddev: 1.595292015837515e-8) 1.04
benchmarks/test_guard_validate.py::test_g10_validate_deep_chain 42495.4224286077 iter/sec (stddev: 0.00001626905220012278) 23951.926949647954 iter/sec (stddev: 0.000031960404140896095) 0.56
benchmarks/test_guard_validate.py::test_g11_validate_wide 25412.20330255422 iter/sec (stddev: 0.000023258162801566658) 21724.253571453744 iter/sec (stddev: 0.00003130808206721572) 0.85

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

@lesnik512
lesnik512 merged commit 3a903a3 into main Oct 10, 2026
12 checks passed
@lesnik512
lesnik512 deleted the docs/rework-errors-a 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