Repository navigation
docs: rework the recipes pages - #668
Merged
Merged
Conversation
Fix wrong claims (repository caching, MissingGreenlet, REQUEST-scoped resolves in tests), make every example define the names it uses, add a plugin-free pytest fixture example and a close_sync() footgun entry, and replace duplicated material with links.
There was a problem hiding this comment.
Benchmark
Details
| Benchmark suite | Current: 1862d63 | Previous: 395e8fb | Ratio |
|---|---|---|---|
benchmarks/test_guard_by_type.py::test_g16_resolve_by_type |
5106119.720224288 iter/sec (stddev: 6.296233292123535e-9) |
6139935.138982263 iter/sec (stddev: 1.8605923146541302e-8) |
1.20 |
benchmarks/test_guard_by_type.py::test_g17_resolve_by_type_large_registry |
5131152.77813225 iter/sec (stddev: 5.853743537589257e-9) |
6205832.315667191 iter/sec (stddev: 1.3805592208773412e-8) |
1.21 |
benchmarks/test_guard_cold.py::test_g8_cold_first_resolve |
32853.52097388092 iter/sec (stddev: 0.00003595800534247451) |
32024.886577255867 iter/sec (stddev: 0.000030238944431934) |
0.97 |
benchmarks/test_guard_cold.py::test_g8b_cold_first_resolve_cached |
26408.761566731722 iter/sec (stddev: 0.0000872307582729815) |
24960.133332489408 iter/sec (stddev: 0.00011886986138363566) |
0.95 |
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[1] |
672.796407629877 iter/sec (stddev: 0.000010186054227702802) |
775.0388204206938 iter/sec (stddev: 0.00004473754687217169) |
1.15 |
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[2] |
656.8745913210425 iter/sec (stddev: 0.000021451490487672628) |
764.2436132277375 iter/sec (stddev: 0.0001144571715558005) |
1.16 |
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[4] |
602.9617038417476 iter/sec (stddev: 0.00013036483297890395) |
680.0345534095648 iter/sec (stddev: 0.0000803400475442327) |
1.13 |
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[1] |
18178.141239615226 iter/sec (stddev: 0.0000068683166077245195) |
10419.935559673875 iter/sec (stddev: 0.00001230746171334532) |
0.57 |
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[2] |
9008.07303501487 iter/sec (stddev: 0.00001845086365991487) |
5634.100392630293 iter/sec (stddev: 0.00003435893765767916) |
0.63 |
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[4] |
4828.8970859345145 iter/sec (stddev: 0.00002614187195790505) |
2968.9836953557647 iter/sec (stddev: 0.000061919224501018) |
0.61 |
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[1] |
11701.217599477875 iter/sec (stddev: 0.000012976400599150017) |
12389.480670083722 iter/sec (stddev: 0.000019500905757513246) |
1.06 |
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[2] |
4992.131360953484 iter/sec (stddev: 0.000017798551032894882) |
4843.535831204409 iter/sec (stddev: 0.00003981695650002605) |
0.97 |
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[4] |
2613.8710031125665 iter/sec (stddev: 0.0000303227064160822) |
1868.0290700812566 iter/sec (stddev: 0.00007765838735564648) |
0.71 |
benchmarks/test_guard_concurrency.py::test_g15c_worker_pool_floor_control[1] |
47116.00943139982 iter/sec (stddev: 0.0000022436179366084376) |
24896.9783783031 iter/sec (stddev: 0.000017881784016521047) |
0.53 |
benchmarks/test_guard_concurrency.py::test_g15c_worker_pool_floor_control[2] |
22334.145241504535 iter/sec (stddev: 0.00001048913178792468) |
11645.85197075752 iter/sec (stddev: 0.000015482573556939863) |
0.52 |
benchmarks/test_guard_concurrency.py::test_g15c_worker_pool_floor_control[4] |
9296.029503110984 iter/sec (stddev: 0.00002518460475862088) |
5052.235696391264 iter/sec (stddev: 0.000048475453192008314) |
0.54 |
benchmarks/test_guard_lifecycle.py::test_g6_build_child_container |
1460151.2139914741 iter/sec (stddev: 6.395147792935572e-8) |
2335541.2567186337 iter/sec (stddev: 3.860763352243666e-8) |
1.60 |
benchmarks/test_guard_lifecycle.py::test_g6b_build_child_container_auto_scope |
2065030.2816037873 iter/sec (stddev: 9.896972467487716e-8) |
2505907.6773532387 iter/sec (stddev: 3.905850495221895e-8) |
1.21 |
benchmarks/test_guard_lifecycle.py::test_g7_request_lifecycle_batch |
4084.566832929413 iter/sec (stddev: 0.000006274625746576131) |
3317.2697581338502 iter/sec (stddev: 0.000052891745120707064) |
0.81 |
benchmarks/test_guard_lifecycle.py::test_g7c_event_loop_floor_control |
75969.73130830468 iter/sec (stddev: 0.0000010307878458857364) |
83100.58991551855 iter/sec (stddev: 0.0000014527703096612033) |
1.09 |
benchmarks/test_guard_lifecycle.py::test_g7b_request_cycle_sync |
529785.0871159357 iter/sec (stddev: 6.042547934333303e-8) |
468518.9908558271 iter/sec (stddev: 8.515899938405845e-8) |
0.88 |
benchmarks/test_guard_lifecycle.py::test_g13_teardown_at_scale |
80609.93499638163 iter/sec (stddev: 9.819504660899682e-7) |
78785.45603292529 iter/sec (stddev: 0.0000017671089125152717) |
0.98 |
benchmarks/test_guard_lifecycle.py::test_g13b_teardown_at_scale_async_no_finalizers |
746.3209639461878 iter/sec (stddev: 0.0008341959588484839) |
586.9668417253587 iter/sec (stddev: 0.0013708543619692236) |
0.79 |
benchmarks/test_guard_resolve.py::test_g1_transient_resolve |
2903032.7547638807 iter/sec (stddev: 2.386215239817019e-8) |
3091773.339921797 iter/sec (stddev: 3.1905368666696455e-8) |
1.07 |
benchmarks/test_guard_resolve.py::test_g2_cached_resolve |
4959172.3736368995 iter/sec (stddev: 7.873293603091878e-9) |
5585002.481125255 iter/sec (stddev: 1.7326975696337195e-8) |
1.13 |
benchmarks/test_guard_resolve.py::test_g3_deep_chain |
990783.2634611722 iter/sec (stddev: 3.678123749372453e-8) |
1053634.7223852123 iter/sec (stddev: 4.376268176758002e-8) |
1.06 |
benchmarks/test_guard_resolve.py::test_g4_wide_resolve |
592246.7380470355 iter/sec (stddev: 3.4297735603837854e-7) |
602475.4027370019 iter/sec (stddev: 3.669391702377282e-7) |
1.02 |
benchmarks/test_guard_resolve.py::test_g5_cross_scope |
2586299.8138301414 iter/sec (stddev: 2.6619536755748323e-8) |
2722743.1964451196 iter/sec (stddev: 2.2608711961763022e-8) |
1.05 |
benchmarks/test_guard_resolve.py::test_g9_context_resolve |
2022590.720278535 iter/sec (stddev: 1.30360407966388e-7) |
2095884.4060996436 iter/sec (stddev: 1.4539795764318154e-7) |
1.04 |
benchmarks/test_guard_resolve.py::test_g12_override_active_resolve |
1024537.047565218 iter/sec (stddev: 3.11438360703835e-8) |
1056924.0255533508 iter/sec (stddev: 4.539064934432262e-8) |
1.03 |
benchmarks/test_guard_resolve.py::test_g18_alias_hop |
5015794.486054756 iter/sec (stddev: 7.453645023982295e-9) |
5564689.374851157 iter/sec (stddev: 1.595292015837515e-8) |
1.11 |
benchmarks/test_guard_validate.py::test_g10_validate_deep_chain |
44542.9252225048 iter/sec (stddev: 0.0001782201399104728) |
23951.926949647954 iter/sec (stddev: 0.000031960404140896095) |
0.54 |
benchmarks/test_guard_validate.py::test_g11_validate_wide |
29102.106311425898 iter/sec (stddev: 0.00002195938065199246) |
21724.253571453744 iter/sec (stddev: 0.00003130808206721572) |
0.75 |
This comment was automatically generated by workflow using github-action-benchmark.
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.
Reworks the seven recipe pages the same way as the Quickstart, Introduction and Providers pages. Every behavioural claim was checked by a script run against this checkout, with FastAPI, Litestar, SQLAlchemy 2.1.4, aiohttp and pydantic-ai-slim 2.55.0 installed. The transactional-test claims were checked against a local Postgres 17 container.
What was wrong
expire_on_commit=Truecauses "session is closed" errors. On aiosqlite, reading an attribute after commit raises aStatementErrorwrappingsqlalchemy.exc.MissingGreenlet. The page never showed a handler, and nothing told the reader to callvalidate()aftersetup_di.setup_di. One pitfall said to drop theasync with containerthe example used. Another said wrapping both "closes it twice". The second close actually runs no finalizers: an APP finalizer ran exactly once with both wrappers. A third pitfall described requests arriving afteryield, which cannot happen because that code runs at shutdown.AsyncSession,PlaceOrder) from the root container, which raisesScopeNotInitializedError. Its fixtures were typed as their yield values. Pattern 3 used adi_containerfixture the page never defined.integrations/pytest.mdpromised this page had a plugin-free worked example, and it had none.PrimaryEngine/ReplicaEnginesubclasses as bound types, but the creators return plainAsyncEngine, so the annotations were false.ProviderNotRegisteredError. Section 5 referred toDependencies.clockandfake_clock, which existed nowhere on the page.save_notebody was....What changed
AsyncEngine.dispose/AsyncSession.close. Added aFromDIroute andvalidate()aftersetup_di. Pitfalls are down to three:MissingGreenlet, async finalizers underclose_sync()or a container nobody closes (Flask, gRPC, Typer, Celery), and the scope rule as a link. The multi-database variation now usesbound_type=Nonepluskwargs.setup_di, aFromDIroute andvalidate(). The integration closes the container inside your lifespan, so APP finalizers still see the open session (checked with a finalizer that recordssession.closed). One short block covers the no-integration case. The two asyncpg paragraphs are merged into one.scope=Scope.REQUEST. A second block shows that both repositories share the request's session. New pitfall: assigning one provider object to a second group raisesDuplicateProviderTypeError. The Litestar section is shortened to the snippet plus a link.AsyncIterator, notes that pytest-asyncio needsasyncio_mode = "auto"for plain@pytest.fixture(strict mode failed), and recommendsjoin_transaction_mode="create_savepoint". On Postgres 17, the default mode lost the test's earlier commit onsession.rollback()and the teardown emittedSAWarning: transaction already deassociated from connection. Withcreate_savepoint, commits and rollbacks inside a test worked and the next test saw an empty table. The modern-di-pytest pattern is replaced by links into pytest.md.bound_type=Noneon primary and replica,setup_di,validate()and a route. A hidden test checks that GET goes to the replica and POST to the primary. Added a pitfall:kwargsresolve both engines, so the first request creates both.validate()beforesetup_direportsArgumentResolutionError, and an APP-scoped choice reportsInvalidScopeDependencyError.Container": the hidden dependency passesvalidate()and fails at resolve. Section 5 is now self-contained and useswith container.override(...). Section 6 is corrected. New section 7 covers an async finalizer underclose_sync(), with araises: FinalizerErrorblock.pydantic-ai-slim[openai]extra. The second example definesSessionwith an async finalizer and givessave_notea real body. Run with a FunctionModel issuing two concurrent tool calls, each call got its ownSessionand each was finalized.Anchors kept
good-and-bad-practices.md#1-captive-dependency-a-wide-scoped-provider-holding-a-narrow-scoped-one(linked from scopes.md)testing-overrides.md#pitfallsChecks
just test-docs: 158 passed, 23 skippedjust test-docs-integrations: 255 passed, 1 skippedjust docs-build: no warningsjust lint-ci: cleanLibrary behaviour worth a look
Assigning one provider object to two groups, or listing one group twice, raises
DuplicateProviderTypeError. Its hint, "Set bound_type=None on one of them", cannot be followed for a single object:The same thing happens when
modern_di_fastapi.setup_diis called twice on one container. The docs describe the current behaviour.Needed outside this PR
providers/scopes.mdsays "Sharing the same provider instance with the same default scope across multiple groups is allowed." That holds when the classes are created, butContainer(groups=[...])with both groups raisesDuplicateProviderTypeError.troubleshooting/duplicate-type-error.md: the Fix example registersprimary_repositoryandsecondary_repository, both bound toRepository, so the example would itself raiseDuplicateProviderTypeErrorin a container.providers/lifecycle.mdSee also says async-lifespan.md shows "sync creator + async finalizer is the most common shape". That pattern is in sqlalchemy.md.mkdocs.ymlnav label and the "Multi-Group organization" link text in celery.md, flask.md and group-instantiation-error.md do not match the H1 of multi-group.md.