Skip to content

Latest commit

 

History

History
104 lines (76 loc) · 5.65 KB

AGENTS.md

File metadata and controls

104 lines (76 loc) · 5.65 KB

AGENTS.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

modern-di is a zero-dependency Python dependency injection framework; CONTEXT.md opens with what it does and owns the vocabulary. Read it before naming a concept in code, a test name, or an issue title. Every framework integration (ls docs/integrations/) lives in a separate repository and ships as a separate PyPI package, modern-di-pytest included.

Commands

just (task runner) and uv (package manager). The justfile is the source of truth: just --list, or read it. Every recipe carries its intent as a comment. The one thing it does not say: just docs-build runs mkdocs --strict over the site only. Local links in every Markdown file, root and docs/agents/ included, are checked by the links job in CI (lychee, offline), which has no just recipe.

Run just install before just lint, in every checkout and worktree. uv.lock is gitignored and CI's just install runs uv lock --upgrade before it syncs, so CI always lints with the newest ruff and ty; a plain uv sync keeps whatever the checkout resolved last time, and a stale ty passes code that CI rejects.

Architecture

  • Scope: IntEnum, APP=1 → SESSION=2 → REQUEST=3 → ACTION=4 → STEP=5. A provider resolves only from a container of the same or deeper (higher-int) scope; otherwise a clear error is raised.
  • Container: the central object. A child (build_child_container) shares the parent's providers/overrides registries; cache and context are per-container. container.validate() (cycle + transitive-scope checks) is the only thing that validates.

Key files

Every module under modern_di/ is named for what it does; read it. What a single-file read will not tell you:

  • resolver_compiler.py is the single resolve path. A Factory resolver is generated from a source template per resolver shape and exec'd with the factory's constants as globals; an Alias compiles to its source's resolver, and the other provider types compile to closures. A new provider type must add a branch here or compile_resolver raises, and branches in dependency_graph (redirect_target, dependencies_of, collect_errors), which fall through silently. Nothing in the template calls a helper on the hot path: the per-node frame budget is the point, and test_resolve_costs_exactly_one_resolver_frame_per_node says why. Overrides are compiled in: an override change drops the compiled resolvers.
  • exceptions/ owns every message and every glyph. A raise site passes structured facts, never formatting; the class renders its own f-string and sets a docs_slug (its page under docs/troubleshooting/, enforced by tests/test_docs_slug_census.py). Add a message, a glyph, or a class here, never at the raise site. Submodules split by family; __init__ re-exports every public name.
  • registries/: providers_registry (type → provider, plus the shared plan/resolver memos) and overrides_registry are shared tree-wide.
  • cache.py: CacheItem and the functions over a container's own cache items and creation order (fetch, count, close). Cache and context are per-container and live in the container's slots; the context is a plain dict.
  • dependency_graph.py walks WiringPlan.provider_kwargs, so what validate() traverses is exactly what resolve() follows. Explicit-stack, never recursive: a caller runs it inside a RecursionError handler near CPython's stack limit.
  • types.py: UNSET is load-bearing on the resolve path: the miss marker for both the override lookup and the cache slot, separating "not passed" from "explicitly None".

Testing patterns

A test declares a Group subclass with providers as class attributes → Container(groups=[...]), then container.resolve(SomeType) or resolve_provider(provider), with override/reset_override for mocks. Scope chains come from build_child_container.

Workflow

Every link in README.md must be absolute: https://github.com/modern-python/<repo>/blob/main/<path>, or .../tree/main/<path> for a directory. Never a relative path: README.md is also the PyPI long description, and PyPI does not rewrite relative links, so a relative one 404s on the package page.

Code Style

  • Design principle: conservative feature set; resolution is sync-only (async resolution was removed in 2.x), though finalizers may still be sync or async (close_sync/close_async); no global state
  • Docstrings: public API documents the contract; internal helpers get a one-line contract, plus at most one or two lines for a genuinely non-obvious constraint. Never narrate implementation or justify code to a reviewer; cross-file rationale lives in an invariant test's docstring
  • ruff (select = ["ALL"]) and ty are configured in pyproject.toml and run by just lint

Agent skills

Issue tracker

GitHub issues on modern-python/modern-di, via gh. See docs/agents/issue-tracker.md.

Triage labels

The five canonical roles, each label string equal to its name. See docs/agents/triage-labels.md.

Domain docs

Single-context: CONTEXT.md and docs/adr/ at the repo root. See docs/agents/domain.md.

Prose

Docs pages, PR and issue bodies, and commit messages run through the humanizer skill when it is installed (npx skills add blader/humanizer). Repo conventions it does not cover (heading and nav casing, colons in place of dashes in gloss bullets, contrast markers in code samples) are in docs/agents/docs-style.md.

Cutting a release

Maintainers only. See docs/agents/release.md.