| name | pydantree-extraction |
| description | Extract typed data from text with pydantree_sitter (Product A) — OutputModel declarations, captures (field/kind/record/optional/list/descendant), predicates and markers, schema binding and validate_with, bundles, community grammars, stubs, and the error surface. Use when consuming a grammar with pydantree in your own project. |
pydantree — typed extraction (pydantree_sitter, Product A)
Declare an OutputModel — the model IS the query — and get
schema-checked, typed rows over any tree-sitter grammar. Full reference:
../../docs/user-guide.md §2.
The model
from typing import Annotated
from pydantree_sitter import M, Matches, NodeKind, OutputModel, capture, source_meta
import tree_sitter_python
class Assignment(OutputModel):
__match__ = M("module", "expression_statement", "assignment")
name: Annotated[str, Matches(r"^[A-Z][A-Z_]*$")] = capture("left")
value: Annotated[int, NodeKind("integer")] = capture("right")
line: int = source_meta()
rows = Assignment.extract(source_text, language=tree_sitter_python)
The capture surface
| pattern | meaning |
|---|
= capture("f") | bind to CST field f (no-arg: attr name IS the field) |
= capture_kind("code_span") | bind to a CHILD BY NODE KIND (positional-children grammars like markdown) |
= source_meta() | anchor line (int) or byte span (Span) |
list[T] = capture("f") | field-mode LIST (repeated field on the anchor, merged) |
str | None = capture("f") | OPTIONAL capture — matches without the field materialize None |
Annotated[..., NodeKind(...)] | constrain the node kind (tuple = alternation) |
Annotated[str, Unescaped()] | decode string-literal escapes |
nested OutputModel field | materialize a nested node with the inner model |
M("module", ..., "call") | descendant "..." matches any depth |
Record mode (key/value documents)
class ServerSection(OutputModel):
__match__ = M("source_file", "section", record=True)
host: str
port: int
debug: bool = False
line: int = source_meta()
The record node is the anchor; attr names (or capture("key")) are the
record keys; the value shapes are DERIVED from the grammar's schema; a
predicate field that doesn't match filters the whole record.
Schemas: check BEFORE parsing
from pydantree_sitter import Language
lang = Language.load_bundle("dist/cfg-bundle")
ext = lang.extractor(ServerSection)
rows = ext.extract(text)
lang = Language.load(tree_sitter_rust.language(), schema="node-schema.json")
The compiled state lives on the Language INSTANCE keyed by (model, strict)
— no class-level caches, no global registry (D5); a model bound against a
second language re-checks. Record mode over a non-JSON grammar needs a
ValueMap (propose_value_map draft or a bundle value_map entry);
schema-less record mode is the documented JSON family + JSON_VALUE_MAP.
Other A surface
lang.parse(src) / lang.reparse(old_tree, new) — parse + incremental.
OutputModel.extract_tree(tree, ...) — parse once, extract many models.
OutputModel.compiled_source(...) — the derived .scm (diagnostics).
- Typed CST codegen (D7):
generate_typed_api(lang.schema, "mylang_api") —
REAL runtime classes (the .pyi fiction is deleted).
Errors
ExtractionError (one MatchFailure per failed match: pattern, span,
snippet, pydantic errors), SchemaCheckError / ShapeError /
QueryBuildError (at bind, before parsing), AmbiguousCaptureError,
BundleError, and WasmRuntimeUnavailableError for a .wasm bundle (the
seam raises unconditionally — see ../../docs/architecture.md §3.1).
Facts that matter
- Community grammars ship no schema — derive one from the grammar source
with
pydantree_sitter_grammar.pipeline.build_from_source_dir (B-side) or
bind none (schema-less path).
tree-sitter>=0.26 is the floor (0.26-only APIs are used).
- The light install (
pydantree-sitter) never
imports pydantree_sitter_grammar — import pydantree_sitter_grammar fails there by design.
- Run in your own project with
uv pip install pydantree-sitter.