chirp shapes-codegen — suggest @shapedecorators and audit drift (#172).
Two jobs, both incremental and non-destructive by design:
-
Ingest + emit. Scan target Python modules for frozen dataclasses sitting near an explicit named-column
SELECTliteral, pair each dataclass to theSELECTwhose output columns are a subset of the dataclass fields, and emit a@shape("SELECT ...")decorator suggestion above each matched class (view-by-view, one suggestion per pair).--dry-runprints the unified diff and writes nothing. -
Day-one audit (
--audit). Load an app'ssurface_contractsregistry (set viaapp.set_contract_check_data("surface_contracts", {...})) and report every surface name with no backing Shape — REUSING the L2 registry-drift logic (_check_registry_drift()), never a second copy. Exit non-zero when drift is found (CI-friendly), 0 otherwise.
The SELECT parsing reuses_parse_select_columns()
(the single conservative SELECT parser); ingest only pairs a class to a SELECT
the parser can read (SELECT */ expressions / CTE / UNION are skipped), so the
emit step never suggests a decorator it cannot verify.
This command is intentionally best-effort and never overwrites source unless
explicitly asked: the default is a dry-run-style preview. Mirrors
_makemigrations.py — lazy imports live inside run_shapes_codegen().
cli._shapes_codegen
| Name | Type | Default | Description |
|---|---|---|---|
type
|
|
— | |
qualified_name
|
|
— | |
element_type
|
|
— | |
description
|
|
— | |
source_file
|
|
— | |
line_number
|
|
— | |
is_autodoc
|
|
— | |
autodoc_element
|
|
— | |
_autodoc_template
|
|
— | |
_autodoc_url_path
|
|
— | |
_autodoc_page_type
|
|
— | |
title
|
|
— | |
doc_content_hash
|
|
— |
Symbols on this page
Suggest@shapedecorators and/or audit surface-contract drift.
Resolves the requested mode fromargs:
--auditloads the app atargs.pathand reports surface-contract names…
Report surface-contract names with no backing Shape; return the exit code.
Loads the app atargs.path (an import string, e.g. myapp:app), reads
its …
Return the app'ssurface_contracts contract-check data (or {}).
The data is stored viaapp.set_contract_check_data("surface_contracts", {...}); it lives on the app's mutable state.…
Scanargs.path for dataclass/SELECT pairs and print @shapesuggestions.
--dry-runis the safe, write-nothing default (the only behavior in v1): every match prints a…
Return the.py files under root (or rootitself if it is one).
Skips dunder/cache and common virtual-env / build directories so a broad …
Pair frozen dataclasses to nearby explicit-column SELECTs; yield suggestions.
Returns a list of_Suggestion(class name, line number, the chosen SELECT, and the SELECT's…
Collect(sql, columns)for every explicit-column SELECT string in the module.
Walks string constants (assignments and bare expressions) and keeps only those the conservative…
Return the(sql, columns) whose columns best subset fields, else None.
A candidate qualifies only when EVERY parsed output column is a…
True when aClassDef carries @dataclass(frozen=True).
True when aClassDef already carries an @shape(...)decorator (skip it).
Return the annotated field names declared on a dataclassClassDef.
Render one suggestion as a unified-diff-style@shapeinsertion preview.
One dataclass/SELECT pairing the ingest step would annotate with@shape.
run_shapes_codegen
function
def run_shapes_codegen(args: argparse.Namespace) -> None
Suggest@shapedecorators and/or audit surface-contract drift.
Resolves the requested mode fromargs:
--auditloads the app atargs.pathand reports surface-contract names with no backing Shape; exits non-zero on drift, 0 when clean.- otherwise (the default) scans
args.pathfor frozen dataclasses paired with a nearby explicit-columnSELECTand prints a@shape(...)suggestion above each match (--dry-runis the safe default — nothing is written).
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
args
|
argparse.Namespace
|
— |
_run_audit
function
def _run_audit(args: argparse.Namespace) -> int
Report surface-contract names with no backing Shape; return the exit code.
Loads the app atargs.path (an import string, e.g. myapp:app), reads
itssurface_contractscontract-check data, and reuses the L2
_check_registry_drift() against the
liveshape_registry(). Returns 1when any drift is found
(so CI fails),0when clean. The drift detection — including the
closest-match suggestion — is exactly whatapp.check()runs; the audit is
a focused, exit-coded view of the same logic.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
args
|
argparse.Namespace
|
— |
_app_surface_contracts
function
def _app_surface_contracts(app: object) -> dict[str, str]
Return the app'ssurface_contracts contract-check data (or {}).
The data is stored viaapp.set_contract_check_data("surface_contracts", {...}); it lives on the app's mutable state. Only str -> strentries are
kept (the same shape the drift checker consumes), so a malformed registry
never crashes the audit.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
app
|
object
|
— |
_run_ingest
function
def _run_ingest(args: argparse.Namespace) -> None
Scanargs.path for dataclass/SELECT pairs and print @shapesuggestions.
--dry-runis the safe, write-nothing default (the only behavior in v1):
every match prints a unified-diff-style preview of the@shape("SELECT ...")
line that would be inserted above the dataclass. Nothing on disk is modified.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
args
|
argparse.Namespace
|
— |
_python_files
function
def _python_files(root: Path) -> list[Path]
Return the.py files under root (or rootitself if it is one).
Skips dunder/cache and common virtual-env / build directories so a broad
shapes-codegen .does not walk the whole environment.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
root
|
Path
|
— |
_suggest_shapes
function
def _suggest_shapes(source: str) -> list[_Suggestion]
Pair frozen dataclasses to nearby explicit-column SELECTs; yield suggestions.
Returns a list of_Suggestion(class name, line number, the chosen SELECT,
and the SELECT's parsed columns) for every frozen dataclass that:
- is NOT already
@shape-decorated (incremental — skip done work), and - sits near an explicit named-column
SELECTliteral (string constant / assignment in the module) whose parsed output columns are a SUBSET of the dataclass fields.
A dataclass with no matching SELECT yields nothing (best-effort, view-by-view).
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
source
|
str
|
— |
_collect_select_literals
function
def _collect_select_literals(tree: ast.Module, parse_columns: Callable[[str], tuple[str, ...] | None]) -> list[tuple[str, tuple[str, ...]]]
Collect(sql, columns)for every explicit-column SELECT string in the module.
Walks string constants (assignments and bare expressions) and keeps only those
the conservative SELECT parser can read —SELECT */ expressions / CTE /
UNION returnNone and are skipped, so a suggested @shapeis always one
shapecheckcan later verify.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
tree
|
ast.Module
|
— | |
parse_columns
|
Callable[[str], tuple[str, ...] | None]
|
— |
_best_select
function
def _best_select(fields: frozenset[str], selects: list[tuple[str, tuple[str, ...]]]) -> tuple[str, tuple[str, ...]] | None
Return the(sql, columns) whose columns best subset fields, else None.
A candidate qualifies only when EVERY parsed output column is a field of the dataclass (columns ⊆ fields) — the pairing the blueprint specifies. Among qualifying candidates, the one covering the most fields wins (the richest verified projection), ties broken by the SQL text for determinism.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
fields
|
frozenset[str]
|
— | |
selects
|
list[tuple[str, tuple[str, ...]]]
|
— |
_is_frozen_dataclass
function
def _is_frozen_dataclass(node: ast.ClassDef) -> bool
True when aClassDef carries @dataclass(frozen=True).
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
node
|
ast.ClassDef
|
— |
_already_shape
function
def _already_shape(node: ast.ClassDef) -> bool
True when aClassDef already carries an @shape(...)decorator (skip it).
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
node
|
ast.ClassDef
|
— |
_class_field_names
function
def _class_field_names(node: ast.ClassDef) -> frozenset[str]
Return the annotated field names declared on a dataclassClassDef.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
node
|
ast.ClassDef
|
— |
_format_suggestion
function
def _format_suggestion(path: Path, suggestion: _Suggestion) -> str
Render one suggestion as a unified-diff-style@shapeinsertion preview.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
path
|
Path
|
— | |
suggestion
|
_Suggestion
|
— |
_Suggestion
class
One dataclass/SELECT pairing the ingest step would annotate with@shape.
View source · /home/runner/work/chirp/chirp/site/../src/chirp/cli/_shapes_codegen.py:1