Skip to content

docs: rework the pytest and writing an integration pages - #662

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

lesnik512 merged 1 commit into
mainfrom
docs/rework-tooling

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Reworks docs/integrations/pytest.md and docs/integrations/writing-integrations.md, the same way the Quickstart, Introduction and Providers pages were reworked. Every behavioural claim was checked against the source of modern-di-pytest and the integration repos, and where possible by running a script.

What was wrong

pytest page:

  • It said the testing-with-overrides recipe was a worked example of a session di_container fixture plus a request child fixture. The recipe has neither.
  • It called modern-di-pytest "the plugin". The package has no pytest11 entry point; you import expose and modern_di_fixture yourself.
  • The override example asserted list_users() == [], which also passes with the real repo. The hidden FakeRepo now returns ["fake"] and the examples assert that.
  • Overrides used imperative override()/reset_override() and named OverridesRegistry, which is not public. The page now leads with with di_container.override(...), the spelling the recipe and the package README use.
  • The See-also gloss mixed pytest's session scope up with Scope.SESSION.

writing-integrations page:

  • "A Typer command skips the kit entirely": Typer uses from_di, parse_markers and resolve_markers. It skips only bind and classify_connection.
  • The decorator-path section described Typer's mechanics as universal (signature rewriting, a context parameter inserted, the decorator building the child). Only Typer, Celery and aiogram set __signature__. Starlette, aiohttp, Flask, arq and gRPC build the child in middleware, a hook or an interceptor.
  • The quoted missing-container RuntimeError matched none of the three integrations it cited (aiohttp, Starlette, aiogram).
  • The README instruction Usage example: [examples/](./examples) used a relative link; every repo uses an absolute GitHub URL, because PyPI does not rewrite relative links.
  • The native-DI list left out taskiq, and the scaffolding required Annotated[T, FromDI(...)] without mentioning that FastMCP refuses that form at startup.
  • The checklist required a _CONNECTION_PROVIDERS tuple. FastStream and FastMCP register their single provider directly.

How it was verified

  • I ran a real pytest suite in a scratchpad copying the page's conftest.py and test modules (modern-di checkout plus modern-di-pytest on PYTHONPATH): 8 passed. It covers expose in conftest with di_container redefined in a test module, the targeted container_fixture= override, and an override that resets for later modules.
  • Probes: expose() with no groups raises TypeError, and a duplicate name raises ValueError with nothing installed. A REQUEST-scoped provider resolved from the root fixture fails setup with ScopeNotInitializedError. A session-scoped fixture on a function-scoped container gives pytest ScopeMismatch. A sync with Container(...) holding an async finalizer raises FinalizerError (AsyncFinalizerInSyncCloseError) on exit. A failing session fixture errors every test that requests it.
  • The cache caveat runs both ways. An override of a dependency does not reach a cached provider that was already resolved, and a cached provider first resolved under an override keeps the fake after the with block ends. Overriding the cached provider itself works, and the original comes back after the block.
  • For the integration claims I read every modern-di-*/modern_di_*/main.py: __signature__, functools.wraps, the lifecycle hooks, the RuntimeError messages, the connection providers, and test_public_surface.py (9 repos have it).

What was added

  • pytest: an ## API table; notes on the async-finalizer fixture, on why to use container_fixture=, and that expose installs onto the calling module (module=) and overwrites an existing attribute; the pytest_scope limit; and the cache caveat. A hidden block at the end now calls the test functions against a real container, so CI checks their assertions under test-docs-integrations (before, they were only defined).
  • writing-integrations: setup_di now wires a composed lifespan, and a hidden block runs two lifespan cycles. fetch_di_container raises the missing-container RuntimeError, now stated as a rule for both paths. The decorator path has a runnable example (middleware stores the child and deletes it in finally; @inject uses parse_markers/resolve_markers; there is a raises: RuntimeError block). There is a "Signature rewriting" subsection saying when rewriting is needed, including that aiogram cannot use functools.wraps.

Structure changes

  • writing-integrations: the 4-column FastAPI/FastStream/Litestar/Typer table and the Starlette, aiohttp and pytest paragraphs become one table with a row per integration (path, who builds the child, connection providers, root lifecycle). The Typer-centric "What changes vs. the native path" table is gone. The repeated open() no-op and validate-ordering explanations are now stated once, in Lifecycle rules. Repo scaffolding points at the Starlette page as the model instead of restating its shape. Down from 532 to 481 lines; most of what remains is the runnable examples and the table.
  • pytest: the "without the package" paragraph is shorter and drops the false recipe claim.

Anchors kept

#the-contract, #lifecycle-rules, #how-the-existing-integrations-realize-the-contract, #frameworks-without-native-di-the-decorator-path, and the numbered contract subsection headings. No inbound links target headings in pytest.md. Both page paths are unchanged.

Checks

  • just test-docs -q -p no:cacheprovider --no-cov: 158 passed, 21 skipped
  • pytest docs/integrations/pytest.md docs/integrations/writing-integrations.md --docs-integrations with modern-di-pytest on PYTHONPATH: 22 passed
  • just docs-build: no warnings
  • just lint-ci: passed
  • lychee offline: 0 errors

Library bugs

None found. Not rebuilding cached dependents under an override is current behaviour, and the page now documents it.

Out-of-batch changes worth making

  • docs/recipes/testing-overrides.md Pattern 3 should state the cache caveat and show with container.override(...) in a fixture, so it matches the pytest page.
  • docs/integrations/litestar.md comments say "after the plugin is installed". The plugin's on_app_init (which calls add_providers) runs at Litestar(...) construction, so check that validate() comes after that call in those examples.
  • mkdocs.yml llms entry for integrations/pytest.md still reads "with and without modern-di-pytest", which is fine, but the nav says "Pytest" while the H1 is "Usage with pytest".

Fix claims that did not match modern-di-pytest or the integration
repos, add the missing pitfalls and the pytest API table, and replace
the Typer-centric decorator-path description with a runnable example
and a per-integration table.

@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: 05fc0d9 Previous: 395e8fb Ratio
benchmarks/test_guard_by_type.py::test_g16_resolve_by_type 3633742.320721053 iter/sec (stddev: 2.6743465652381463e-8) 6139935.138982263 iter/sec (stddev: 1.8605923146541302e-8) 1.69
benchmarks/test_guard_by_type.py::test_g17_resolve_by_type_large_registry 3838055.107031964 iter/sec (stddev: 1.6233043622956427e-8) 6205832.315667191 iter/sec (stddev: 1.3805592208773412e-8) 1.62
benchmarks/test_guard_cold.py::test_g8_cold_first_resolve 20301.625817705626 iter/sec (stddev: 0.00005174488444367482) 32024.886577255867 iter/sec (stddev: 0.000030238944431934) 1.58
benchmarks/test_guard_cold.py::test_g8b_cold_first_resolve_cached 17505.930338839313 iter/sec (stddev: 0.00010637886984754731) 24960.133332489408 iter/sec (stddev: 0.00011886986138363566) 1.43
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[1] 426.19304004599456 iter/sec (stddev: 0.00018565260184396466) 775.0388204206938 iter/sec (stddev: 0.00004473754687217169) 1.82
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[2] 348.0587875985199 iter/sec (stddev: 0.0001624017628154257) 764.2436132277375 iter/sec (stddev: 0.0001144571715558005) 2.20
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[4] 389.39332429524495 iter/sec (stddev: 0.00008958136993440571) 680.0345534095648 iter/sec (stddev: 0.0000803400475442327) 1.75
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[1] 13538.281861134541 iter/sec (stddev: 0.000007517052360933997) 10419.935559673875 iter/sec (stddev: 0.00001230746171334532) 0.77
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[2] 6264.403887426599 iter/sec (stddev: 0.000021871396525719067) 5634.100392630293 iter/sec (stddev: 0.00003435893765767916) 0.90
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[4] 3327.9523117689682 iter/sec (stddev: 0.00004360213784618463) 2968.9836953557647 iter/sec (stddev: 0.000061919224501018) 0.89
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[1] 9387.254033458072 iter/sec (stddev: 0.00000981457678899777) 12389.480670083722 iter/sec (stddev: 0.000019500905757513246) 1.32
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[2] 3875.2893145614207 iter/sec (stddev: 0.000022364554432668438) 4843.535831204409 iter/sec (stddev: 0.00003981695650002605) 1.25
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[4] 1863.2880699227146 iter/sec (stddev: 0.0000622393980248703) 1868.0290700812566 iter/sec (stddev: 0.00007765838735564648) 1.00
benchmarks/test_guard_concurrency.py::test_g15c_worker_pool_floor_control[1] 40816.36817860615 iter/sec (stddev: 0.000004978127254251522) 24896.9783783031 iter/sec (stddev: 0.000017881784016521047) 0.61
benchmarks/test_guard_concurrency.py::test_g15c_worker_pool_floor_control[2] 14344.47540453622 iter/sec (stddev: 0.00001656036007116838) 11645.85197075752 iter/sec (stddev: 0.000015482573556939863) 0.81
benchmarks/test_guard_concurrency.py::test_g15c_worker_pool_floor_control[4] 5891.683750898387 iter/sec (stddev: 0.000033022475186569756) 5052.235696391264 iter/sec (stddev: 0.000048475453192008314) 0.86
benchmarks/test_guard_lifecycle.py::test_g6_build_child_container 1532094.565939957 iter/sec (stddev: 3.34624016698962e-8) 2335541.2567186337 iter/sec (stddev: 3.860763352243666e-8) 1.52
benchmarks/test_guard_lifecycle.py::test_g6b_build_child_container_auto_scope 1604427.5783349432 iter/sec (stddev: 2.483855743404064e-8) 2505907.6773532387 iter/sec (stddev: 3.905850495221895e-8) 1.56
benchmarks/test_guard_lifecycle.py::test_g7_request_lifecycle_batch 2969.0386384341928 iter/sec (stddev: 0.000010729315640809973) 3317.2697581338502 iter/sec (stddev: 0.000052891745120707064) 1.12
benchmarks/test_guard_lifecycle.py::test_g7c_event_loop_floor_control 56326.94485432178 iter/sec (stddev: 0.0000020215509302001716) 83100.58991551855 iter/sec (stddev: 0.0000014527703096612033) 1.48
benchmarks/test_guard_lifecycle.py::test_g7b_request_cycle_sync 383348.71935430914 iter/sec (stddev: 8.924633557404827e-8) 468518.9908558271 iter/sec (stddev: 8.515899938405845e-8) 1.22
benchmarks/test_guard_lifecycle.py::test_g13_teardown_at_scale 57875.43762256828 iter/sec (stddev: 0.0000018041285325877292) 78785.45603292529 iter/sec (stddev: 0.0000017671089125152717) 1.36
benchmarks/test_guard_lifecycle.py::test_g13b_teardown_at_scale_async_no_finalizers 532.350770309692 iter/sec (stddev: 0.0008816421868152388) 586.9668417253587 iter/sec (stddev: 0.0013708543619692236) 1.10
benchmarks/test_guard_resolve.py::test_g1_transient_resolve 2202379.3956498425 iter/sec (stddev: 2.9977649897825856e-8) 3091773.339921797 iter/sec (stddev: 3.1905368666696455e-8) 1.40
benchmarks/test_guard_resolve.py::test_g2_cached_resolve 3668664.156468773 iter/sec (stddev: 1.898877575634496e-8) 5585002.481125255 iter/sec (stddev: 1.7326975696337195e-8) 1.52
benchmarks/test_guard_resolve.py::test_g3_deep_chain 675727.5195003218 iter/sec (stddev: 4.5018222594902464e-8) 1053634.7223852123 iter/sec (stddev: 4.376268176758002e-8) 1.56
benchmarks/test_guard_resolve.py::test_g4_wide_resolve 432046.58913873957 iter/sec (stddev: 3.8583316343481493e-7) 602475.4027370019 iter/sec (stddev: 3.669391702377282e-7) 1.39
benchmarks/test_guard_resolve.py::test_g5_cross_scope 1885822.3217778828 iter/sec (stddev: 3.698647839336208e-8) 2722743.1964451196 iter/sec (stddev: 2.2608711961763022e-8) 1.44
benchmarks/test_guard_resolve.py::test_g9_context_resolve 1359065.748305402 iter/sec (stddev: 1.3262306748693935e-7) 2095884.4060996436 iter/sec (stddev: 1.4539795764318154e-7) 1.54
benchmarks/test_guard_resolve.py::test_g12_override_active_resolve 689875.6471561858 iter/sec (stddev: 5.2023927702619564e-8) 1056924.0255533508 iter/sec (stddev: 4.539064934432262e-8) 1.53
benchmarks/test_guard_resolve.py::test_g18_alias_hop 3778890.753487168 iter/sec (stddev: 1.4162362727118295e-8) 5564689.374851157 iter/sec (stddev: 1.595292015837515e-8) 1.47
benchmarks/test_guard_validate.py::test_g10_validate_deep_chain 36130.10581175993 iter/sec (stddev: 0.000019095847874020287) 23951.926949647954 iter/sec (stddev: 0.000031960404140896095) 0.66
benchmarks/test_guard_validate.py::test_g11_validate_wide 20993.1722856074 iter/sec (stddev: 0.00002697652799672106) 21724.253571453744 iter/sec (stddev: 0.00003130808206721572) 1.03

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

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