Skip to content

feat(python): class-field nodes, type-annotation edges, and enum members #451

Description

@ilyabrykau-orca

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):

  1. 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).
  2. 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.
  3. 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).
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestparsing/qualityGraph extraction bugs, false positives, missing edgespriority/normalStandard review queue; useful PR with ordinary maintainer urgency.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions