What problem does this solve?
Python class bodies are not extracted: class-level field/attribute assignments produce no nodes, type annotations are not projected as edges, and enums are not typed. The internal object model of every Python codebase is invisible to the graph.
Evidence from an indexed Django project (~82.6k nodes):
Field label total = 9 across the entire graph — and all 9 are Rust struct fields; zero Python. MATCH (c:Class)-[:DEFINES]->(f:Field) -> 0 rows.
- The core
Inventory model (base_api/observations/models/models_shared.py) has out_degree = 0 despite in_degree = 1235 — heavily referenced, but its own body (every CharField/ForeignKey/etc.) contributes nothing.
- Type annotations live only as string properties (
return_type/param_types): 219 methods carry return_type containing Inventory, but there is no edge to that Class node — "who returns/accepts X?" is not traversable.
- Enums:
Enum label = 2 (both Rust); 493 Python enums (Enum/TextChoices/IntegerChoices bases) are bare Class nodes with no member representation.
Functions, methods, decorators (DECORATES 11,773) and inheritance (INHERITS 5,712) are already well modeled — the missing layer is class fields + type-annotation edges + enum members.
Proposed solution
The tree-sitter Python grammar already exposes class-body assignments and typed_parameter/annotation nodes; the Rust path already emits Field. Extend the existing extractors (internal/cbm/extract_defs.c, extract_type_refs.c / extract_type_assigns.c):
Field nodes per class-level assignment / annotated attribute (plain assignment, @dataclass, attrs, Pydantic, Django x = models.CharField(...)), with Class -[:DEFINES]-> Field and parent_class (label + prop already exist in the schema).
- Generic symbol resolution (NOT framework-specific): when a field's RHS callable argument or its annotation resolves to a known
Class, emit Field -[:REFERENCES]-> Class — e.g. ForeignKey(Inventory), x: Inventory, list[Target]. This is plain AST symbol resolution; the model-relationship graph people want emerges as a side effect, without modeling ORM semantics.
- Type-annotation edges: project the already-parsed param/return annotation strings into
DEPENDS_ON/USAGE edges to their Class nodes (today they are dead string props).
- Enums: type enum classes as
Enum and emit their members as Fields.
Scope note: this deliberately does NOT introduce Django-ORM-semantic FK/M2M/O2O edge types (framework-specific, out of scope for a generic AST tool) — only generic field nodes and symbol/type resolution. Relationship traversal falls out of (2) generically.
Public OSS test beds
django/django (its own tests/ app models) and wagtail/wagtail — FK/M2M/O2O-heavy.
pydantic/pydantic, tiangolo/sqlmodel, tiangolo/fastapi — annotated-attribute models and typed params.
python/cpython Lib/enum.py + tests — enum members.
Alternatives considered
- Treat field assignments as generic
Variables: loses parent-class scoping and the type target, so no reference/type edges — the key value.
search_code grep for ForeignKey/type names: finds text, no graph edges; trace_path can't traverse.
- LSP go-to-def on attributes: intra-symbol resolution only; does not populate field/type graph structure.
Confirmations
What problem does this solve?
Python class bodies are not extracted: class-level field/attribute assignments produce no nodes, type annotations are not projected as edges, and enums are not typed. The internal object model of every Python codebase is invisible to the graph.
Evidence from an indexed Django project (~82.6k nodes):
Fieldlabel total = 9 across the entire graph — and all 9 are Rust struct fields; zero Python.MATCH (c:Class)-[:DEFINES]->(f:Field)-> 0 rows.Inventorymodel (base_api/observations/models/models_shared.py) has out_degree = 0 despite in_degree = 1235 — heavily referenced, but its own body (everyCharField/ForeignKey/etc.) contributes nothing.return_type/param_types): 219 methods carryreturn_typecontainingInventory, but there is no edge to thatClassnode — "who returns/acceptsX?" is not traversable.Enumlabel = 2 (both Rust); 493 Python enums (Enum/TextChoices/IntegerChoicesbases) are bareClassnodes with no member representation.Functions, methods, decorators (
DECORATES11,773) and inheritance (INHERITS5,712) are already well modeled — the missing layer is class fields + type-annotation edges + enum members.Proposed solution
The tree-sitter Python grammar already exposes class-body assignments and
typed_parameter/annotation nodes; the Rust path already emitsField. Extend the existing extractors (internal/cbm/extract_defs.c,extract_type_refs.c/extract_type_assigns.c):Fieldnodes per class-level assignment / annotated attribute (plain assignment,@dataclass,attrs, Pydantic, Djangox = models.CharField(...)), withClass -[:DEFINES]-> Fieldandparent_class(label + prop already exist in the schema).Class, emitField -[:REFERENCES]-> Class— e.g.ForeignKey(Inventory),x: Inventory,list[Target]. This is plain AST symbol resolution; the model-relationship graph people want emerges as a side effect, without modeling ORM semantics.DEPENDS_ON/USAGEedges to theirClassnodes (today they are dead string props).Enumand emit their members asFields.Scope note: this deliberately does NOT introduce Django-ORM-semantic FK/M2M/O2O edge types (framework-specific, out of scope for a generic AST tool) — only generic field nodes and symbol/type resolution. Relationship traversal falls out of (2) generically.
Public OSS test beds
django/django(its owntests/app models) andwagtail/wagtail— FK/M2M/O2O-heavy.pydantic/pydantic,tiangolo/sqlmodel,tiangolo/fastapi— annotated-attribute models and typed params.python/cpythonLib/enum.py+ tests — enum members.Alternatives considered
Variables: loses parent-class scoping and the type target, so no reference/type edges — the key value.search_codegrep forForeignKey/type names: finds text, no graph edges;trace_pathcan't traverse.Confirmations