docs: remove docs/adr; move the decisions that survive to the docs and the code - #480
Conversation
Every record is rewritten to Decision + Why, without the "revisit trigger" block: 1002 -> 388 lines. 0017 (exec declined) is removed as superseded by 0030; 0031 is folded into 0025, which already recorded the alias-bind measurement and now carries the one reason that still holds (the alias step in runtime error chains). Facts the template resolver made stale are corrected (0012, 0013, 0014, 0015, 0025, 0026, 0030). A one-line comment at the alias compiler states why it is not bound; the pyproject SLF001 note and the advanced-api page stop describing closures.
There was a problem hiding this comment.
Benchmark
Details
| Benchmark suite | Current: 429cd6e | Previous: 84b15b0 | Ratio |
|---|---|---|---|
benchmarks/test_guard_by_type.py::test_g16_resolve_by_type |
4338966.106904256 iter/sec (stddev: 8.165667392528595e-9) |
4111715.812015289 iter/sec (stddev: 1.737522845623885e-8) |
0.95 |
benchmarks/test_guard_by_type.py::test_g17_resolve_by_type_large_registry |
4265751.543565375 iter/sec (stddev: 6.4252700573881205e-9) |
3966758.2485261136 iter/sec (stddev: 1.572822708461717e-8) |
0.93 |
benchmarks/test_guard_cold.py::test_g8_cold_first_resolve |
18074.51223666573 iter/sec (stddev: 0.00005323540731865018) |
16004.72330958737 iter/sec (stddev: 0.0000625162597678295) |
0.89 |
benchmarks/test_guard_cold.py::test_g8b_cold_first_resolve_cached |
15420.585774739387 iter/sec (stddev: 0.00011270960281413739) |
13497.312687765632 iter/sec (stddev: 0.00014530411370138543) |
0.88 |
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[1] |
536.5746096746725 iter/sec (stddev: 0.00003367265748370334) |
540.6150637293446 iter/sec (stddev: 0.0001824312511385608) |
1.01 |
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[2] |
506.7095707515858 iter/sec (stddev: 0.00002060449382869758) |
502.80389045688327 iter/sec (stddev: 0.00014883161929520402) |
0.99 |
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[4] |
454.80965191731065 iter/sec (stddev: 0.000027382167923633477) |
448.1418084368608 iter/sec (stddev: 0.0001298107075192004) |
0.99 |
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[1] |
1859.9524903021604 iter/sec (stddev: 0.00018083629433317185) |
1676.2273084921449 iter/sec (stddev: 0.0001944100925791444) |
0.90 |
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[2] |
1501.2566456439479 iter/sec (stddev: 0.0001693835070540942) |
1333.8874005180764 iter/sec (stddev: 0.00022992267475147003) |
0.89 |
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[4] |
1078.2573485073071 iter/sec (stddev: 0.00017585993886011446) |
962.9503798168239 iter/sec (stddev: 0.00024922809084355847) |
0.89 |
benchmarks/test_guard_lifecycle.py::test_g6_build_child_container |
694736.5353276125 iter/sec (stddev: 3.997407207754713e-8) |
753265.49537359 iter/sec (stddev: 4.763568530631424e-8) |
1.08 |
benchmarks/test_guard_lifecycle.py::test_g6b_build_child_container_auto_scope |
635560.9979366362 iter/sec (stddev: 4.594295766454308e-8) |
679431.5203257735 iter/sec (stddev: 6.040131584214356e-8) |
1.07 |
benchmarks/test_guard_lifecycle.py::test_g7_request_lifecycle_batch |
2406.236028312242 iter/sec (stddev: 0.000008002465432848277) |
2399.581927113117 iter/sec (stddev: 0.00001753933625086649) |
1.00 |
benchmarks/test_guard_lifecycle.py::test_g7c_event_loop_floor_control |
58914.26903023947 iter/sec (stddev: 0.0000014744809224113097) |
61074.751422245674 iter/sec (stddev: 0.000002462942606397808) |
1.04 |
benchmarks/test_guard_lifecycle.py::test_g13_teardown_at_scale |
48680.79524037967 iter/sec (stddev: 0.0000016124226115277146) |
50503.96237302482 iter/sec (stddev: 0.000002069539854247738) |
1.04 |
benchmarks/test_guard_resolve.py::test_g1_transient_resolve |
2441030.2026165635 iter/sec (stddev: 3.349203708521621e-8) |
2447111.1974663865 iter/sec (stddev: 2.9348890650137755e-8) |
1.00 |
benchmarks/test_guard_resolve.py::test_g2_cached_resolve |
4227781.181279056 iter/sec (stddev: 7.228763982627601e-9) |
4081617.3261722233 iter/sec (stddev: 6.695477561497057e-8) |
0.97 |
benchmarks/test_guard_resolve.py::test_g3_deep_chain |
854535.1880785434 iter/sec (stddev: 6.3127058367329e-8) |
847432.8863865049 iter/sec (stddev: 4.402585884241757e-8) |
0.99 |
benchmarks/test_guard_resolve.py::test_g4_wide_resolve |
500536.8132184154 iter/sec (stddev: 3.4553865635892693e-7) |
524426.3503369563 iter/sec (stddev: 3.5818326365445204e-7) |
1.05 |
benchmarks/test_guard_resolve.py::test_g5_cross_scope |
1958921.6097631322 iter/sec (stddev: 2.485203461348961e-8) |
1959504.0083899717 iter/sec (stddev: 3.0232594743763504e-8) |
1.00 |
benchmarks/test_guard_resolve.py::test_g9_context_resolve |
1132717.7012759626 iter/sec (stddev: 1.4908350639100557e-7) |
1096218.4055127536 iter/sec (stddev: 1.7265430223555424e-7) |
0.97 |
benchmarks/test_guard_resolve.py::test_g12_override_active_resolve |
805629.0266822255 iter/sec (stddev: 5.236424805127281e-8) |
840875.5313768031 iter/sec (stddev: 3.84640730267e-8) |
1.04 |
benchmarks/test_guard_resolve.py::test_g18_alias_hop |
2687340.8456300353 iter/sec (stddev: 6.231051141156874e-8) |
2859243.2572375205 iter/sec (stddev: 1.3894440264295111e-8) |
1.06 |
benchmarks/test_guard_validate.py::test_g10_validate_deep_chain |
26831.73718333745 iter/sec (stddev: 0.00003917646951414279) |
23875.75800457809 iter/sec (stddev: 0.00003180494080777584) |
0.89 |
benchmarks/test_guard_validate.py::test_g11_validate_wide |
15233.539452188917 iter/sec (stddev: 0.0002434609194822762) |
14249.073539463352 iter/sec (stddev: 0.00026633961521020633) |
0.94 |
This comment was automatically generated by workflow using github-action-benchmark.
| `AsyncFinalizerInSyncCloseError`; `CacheRegistry.close_sync` preserves the items that raised it so | ||
| a later `close_async` can finish them, where the async path clears the creation order outright. A | ||
| unified method would carry both behaviours as conditional branches, adding complexity rather than | ||
| concentrating it. The area has been stable since the finalizer fixes shipped in 2.15.0. |
There was a problem hiding this comment.
challenge this decision against https://github.com/mattpocock/skills convention, seems not earnd its place as ADR
| **Why:** every alternative expresses caching twice. A `Singleton` class says "cached" in the class | ||
| name and again in a still-required `cache_settings` for finalizers, and the two can drift. A | ||
| `cached=True` flag beside `cache_settings=` needs a both-passed conflict rule and has no path to a | ||
| finalizer. Overloading `cache_settings=True` works but reads wrong. One argument, one model. |
| container is a real object with its own cache and context, and "enter scope" vocabulary would hide | ||
| the model the docs teach. A second spelling of the most-written call after `resolve()` also runs | ||
| against the conservative feature set. If discoverability turns out to be a problem, a vocabulary | ||
| table mapping `enter_scope` / `CreateScope` to `build_child_container` is the cheaper fix. |
| for uncached factories, rejecting async generators at declaration, extracting `bound_type` from | ||
| `Iterator[T]`. One explicit spelling also keeps the property that async finalizers work under sync | ||
| resolution, which the generator form cannot express. A `Factory` subclass in userland can wrap a | ||
| generator creator and register the continuation through `CacheSettings(finalizer=)`. |
There was a problem hiding this comment.
same. I think those are design decisions which should be here https://modern-di.modern-python.org/introduction/design-decisions/
Also revise this page https://modern-di.modern-python.org/introduction/design-decisions/
| overriding `T` is ambiguous between the collection and one contributor; validation can no longer | ||
| tell an empty collection from a wiring mistake. That is permanent structural cost for demand | ||
| inferred from other ecosystems. The workaround costs one provider: a `Factory` that takes the | ||
| individual dependencies and returns the list. |
There was a problem hiding this comment.
seems like can be dropped at all
| dependencies' resolvers by reference and an overridden alias compiles to its override before its | ||
| source is touched. Recovering the alias step at render time from the static graph, the way | ||
| `validate()` already draws it, would keep the diagnostic and take the win; that is an error-rendering | ||
| change, and the one worth making if the hop ever shows up in a real profile. |
There was a problem hiding this comment.
challenge this, maybe dropping will help us to get performance and readability in next major release?
| [ADR-0024](0024-scope-map-inline-declined.md). Together with | ||
| [ADR-0023](0023-debug-resolution-tracing-declined.md) this means modern-di offers no per-node | ||
| observation of resolution; a request for one should get a designed seam with a contract, not a | ||
| re-blessing of subclass overrides. |
| same contract against the lower-level API for no entrypoint that is not already reachable, at the | ||
| cost of a repository, a release cadence and a compatibility matrix. Vendoring Click support inside | ||
| the typer adapter would tie its surface to which of the two APIs the user built against. A | ||
| community-maintained adapter outside the org remains possible. |
| `Alias.scope` still returns `APP`, since ordering depends on it: this is a rendering rule. The walk | ||
| moved into the graph module rather than being copied, because `Alias` cannot import it and | ||
| [ADR-0007](0007-unify-graph-traversal.md) keeps one traversal. The rule assumes a redirect owns no | ||
| lifetime of its own, which is true of `Alias` and of nothing else. |
| namespace mismatch is a test failure. Coverage cannot see generated lines. Kwarg names enter the | ||
| source only as `repr` string keys. Generated source is registered in `linecache` and the resolvers | ||
| carry a `__qualname__`, so tracebacks and profilers read normally. Overriding drops and recompiles | ||
| lazily and is not coordinated with concurrent resolves. The free-threaded CI job (3.14t) passes. |
…d the code Judged against the ADR test (hard to reverse, surprising without context, a real trade-off), none of the records earned its place: most described code as it is, several duplicated the design-decisions page, and the rest were user-facing stances that belong on that page. The directory is gone. What moved: explicit-only validation, the closed provider set and the cache= argument become sections of design-decisions.md; multibinding, generator creators, an enter_scope alias and resolution tracing join its Non-goals (each with the measured or structural reason); "error text is not the contract" goes on the errors page; "find_container is the one supported override, resolve/resolve_provider are not hooks" on the advanced-api page; the decorator-free rule into the integration manual; the Click answer on the typer page; the mkdocs-redirects constraint into docs/requirements.txt; the rejected resolver designs into the performance page; two one-line comments at _navigate and _compile_alias. The lazy-allocation numbers are on #478 and the alias-binding question is #481. Agent files (AGENTS.md, docs/agents/domain.md, issue-tracker.md) point the triage prior-rejection check at the Non-goals section instead of docs/adr.
|
Third commit, per review: |
Brings every module under `modern_di/` (except `exceptions.py`, out of scope) to the AGENTS.md rule: public API docstrings state the contract, internal helpers get a one-line contract, and rationale lives in one place rather than a copy at each site it applies to. Removed: the comments that cited an invariant test to justify an inlined lookup. The frame-budget rationale they repeated already lives in `test_resolve_costs_exactly_one_resolver_frame_per_node`, which is unchanged. Kept, as the two comments a reader would otherwise read as a bug: the scope map holds ancestors only (a self-entry is a reference cycle), and the `_next_deeper` memo is keyed by enum type as well as member (IntEnum members hash by value). Rationale that was recorded nowhere else, moved rather than deleted: - Container `__init__` "inlined, not a helper" keeps a one-line pointer, now at the benchmark that measures it (`test_g6_build_child_container`). The spec expected the invariant test it cited to be its home, but that test measures the resolve path; nothing measures child-container construction. - `_handle_recursion_error`'s reason for being a separate call (the coverage tracer re-arms on the fresh boundary) moved into its docstring. - `find_context`'s `in`/`[]` over `.get(key, UNSET)` keeps one line: `.get` skips a dict subclass's `__contains__`/`__getitem__`, and no test covers it. - "a root seeds `container_provider`" was promoted from a comment into the `__init__` docstring, where it is a contract: `resolve(Container)` works. Test prose: the three provider test modules whose commentary described the per-resolver "override front-guard" and the compiled closures now speak of the compiled constant resolver and the generated template. Two modules beyond the three the spec named are also touched, both for dangling references #480 created: the arity comment in `test_resolver_compiler.py` described a 0/1/ generic closure split the template replaced, and five citations of deleted `docs/adr/2026-*.md` files (four in `test_container.py`, one in `test_context_provider.py`) now name the design-decisions section that absorbed the decision, "## 6. Validation is explicit". ADR-0030's free-threading sentence needed no correction: #480 deleted that record, and `docs/introduction/design-decisions.md` already states the fact. No signature, public attribute, exception class, `__all__` or docs slug moves. Verified two ways: every module's AST with docstrings stripped is identical before and after, for `modern_di/` and for `tests/`; and a dump of the exported names, signatures, MROs and `docs_slug` values is identical against main. Prose share of `modern_di/`: 25.4% -> 18.6% of non-blank lines; excluding the out-of-scope `exceptions.py`, 24.7% -> 15.7%. The package figure stays above the 12-15% the spec targets: `exceptions.py` is 158 prose lines that this change may not touch, 6.6 points of the package total on its own. Closes #472
What
docs/adr/is removed. Judged against the three-part test (hard to reverse, surprising without context, a real trade-off), none of the 30 records earned its place: most described code as it is, several duplicated the design-decisions page verbatim, and the rest were user-facing stances that belong on that page where users and the triage check can read them.Where each surviving decision went
cache=notSingleton(0002)enter_scope(0003), no tracing (0023, with the +37% number)find_containeris the one supported override;resolve/resolve_providerare not hooks (0024, 0026)docs/requirements.txt_compile_aliasand_navigatemodern-di-grpc, separate repoDropped with nothing to move (they described code, or were reversible without surprise): 0001, 0007, 0008, 0011, 0012, 0014, 0015, 0018 (verbatim duplicate of the Non-goals entry), 0019, 0020 (its rows are already the lifecycle rules), 0029 (its three invariant tests carry the rationale).
Agent files
docs/agents/issue-tracker.mdmapped the triage skill's.out-of-scope/corpus (the prior-rejection check reads it; a rejection writes to it) ontodocs/adr/. It now maps onto the design-decisions Non-goals section, both for reading and writing.docs/agents/domain.mdandAGENTS.mdstop referring to ADRs;mkdocs.ymldrops the/adr/exclusion; thepyproject.tomlper-file-ignore comment loses its ADR path.Gates
Lint,
ty, 540 tests, strict docs build all pass.