tsemekwes.api

Python interface matching src/api/api.ts, calling the CLI out-of-process.

  1# Copyright © 2017-2026 Juancarlo Añez (apalala@gmail.com)
  2# SPDX-License-Identifier: Apache-2.0
  3"""Python interface matching src/api/api.ts, calling the CLI out-of-process."""
  4
  5from __future__ import annotations
  6
  7import json
  8import tempfile
  9from collections.abc import Generator
 10from contextlib import contextmanager
 11from pathlib import Path
 12
 13from . import bun
 14from .peg import Grammar
 15from .tree import Tree
 16
 17
 18def _build_grammar_args(
 19    path: str,
 20    *,
 21    trace: bool = False,
 22) -> list[str]:
 23    args = ["grammar", "-j", path]
 24    if trace:
 25        args.append("-t")
 26    return args
 27
 28
 29def _build_run_args(
 30    path: str,
 31    inputs: list[str],
 32    *,
 33    start: str | None = None,
 34    nproc: int | None = None,
 35    trace: bool = False,
 36) -> list[str]:
 37    args = ["run", "-j"]
 38    if start:
 39        args += ["-s", start]
 40    if nproc is not None:
 41        args += ["-n", str(nproc)]
 42    if trace:
 43        args.append("-t")
 44    args.append(path)
 45    return [*args, *inputs]
 46
 47
 48@contextmanager
 49def temp_path_from_text(
 50    text: str, suffix: str = ".ebnf", encoding: str = "utf-8"
 51) -> Generator:
 52    """Write text to a temp file and yield its path. Cleans up on exit."""
 53    with tempfile.NamedTemporaryFile(
 54        mode="w+",
 55        suffix=suffix,
 56        encoding=encoding,
 57        delete=True,
 58    ) as tmp:
 59        tmp.write(text)
 60        tmp.flush()
 61        yield tmp.name
 62
 63
 64def parse_jsonl(s: str) -> list[Tree]:
 65    """Parse JSON Lines: one JSON value per line."""
 66    result: list[Tree] = []
 67    for line in s.strip().splitlines():
 68        line = line.strip()
 69        if not line:
 70            continue
 71        result.append(json.loads(line))
 72    return result
 73
 74
 75def parse_grammar(
 76    path: str,
 77    *,
 78    trace: bool = False,
 79    output: str | None = None,
 80) -> Tree:
 81    """Parse a grammar file and return the parse tree."""
 82    result = bun.run(_build_grammar_args(path, trace=trace), output=output)
 83    return json.loads(result)
 84
 85
 86def compile(path: str, *, output: str | None = None) -> Grammar:
 87    """Compile a grammar file into a Grammar."""
 88    grammar = json.loads(bun.run(["grammar", "-j", path], output=output))
 89    return Grammar(grammar)
 90
 91
 92def parse_inputs(
 93    path: str,
 94    inputs: list[str],
 95    *,
 96    start: str | None = None,
 97    nproc: int | None = None,
 98    trace: bool = False,
 99    output: str | None = None,
100) -> list[Tree]:
101    """Parse each input file against the grammar, return one Tree per input (JSONL)."""
102    result = bun.run(
103        _build_run_args(path, inputs, start=start, nproc=nproc, trace=trace),
104        output=output,
105    )
106    trees = parse_jsonl(result)
107    if len(trees) != len(inputs):
108        raise ValueError(
109            f"parse_inputs: expected {len(inputs)} result(s), got {len(trees)}"
110        )
111    return trees
112
113
114def boot_grammar(*, output: str | None = None) -> Grammar:
115    """Get the bootstrapped TS'emekwes grammar."""
116    if output is not None:
117        result = bun.run(["boot", "--json"], output=output)
118    else:
119        with tempfile.NamedTemporaryFile(
120            mode="w+", suffix=".json", encoding="utf-8", delete=True
121        ) as tmp:
122            bun.run(["boot", "--json"], output=tmp.name)
123            result = Path(tmp.name).read_text()
124    return Grammar(json.loads(result))
125
126
127def boot_pretty(*, output: str | None = None) -> str:
128    """Get the bootstrapped grammar as a pretty-printed string."""
129    return bun.run(["boot", "--pretty"], output=output)
130
131
132def loads_grammar(json_str: str) -> Grammar:
133    """Deserialize a JSON string into a Grammar."""
134    return Grammar(json.loads(json_str))
135
136
137def grammar_pretty(path: str, *, output: str | None = None) -> str:
138    """Pretty-print a compiled grammar file."""
139    return bun.run(["grammar", "--pretty", path], output=output)
140
141
142def read_grammar(
143    path: str,
144) -> Grammar:
145    """Read a compiled grammar JSON file as a Grammar."""
146    text = Path(path).read_text()
147    return Grammar(json.loads(text))
@contextmanager
def temp_path_from_text(text: str, suffix: str = '.ebnf', encoding: str = 'utf-8') -> Generator:
49@contextmanager
50def temp_path_from_text(
51    text: str, suffix: str = ".ebnf", encoding: str = "utf-8"
52) -> Generator:
53    """Write text to a temp file and yield its path. Cleans up on exit."""
54    with tempfile.NamedTemporaryFile(
55        mode="w+",
56        suffix=suffix,
57        encoding=encoding,
58        delete=True,
59    ) as tmp:
60        tmp.write(text)
61        tmp.flush()
62        yield tmp.name

Write text to a temp file and yield its path. Cleans up on exit.

def parse_jsonl(s: str) -> list[Tree]:
65def parse_jsonl(s: str) -> list[Tree]:
66    """Parse JSON Lines: one JSON value per line."""
67    result: list[Tree] = []
68    for line in s.strip().splitlines():
69        line = line.strip()
70        if not line:
71            continue
72        result.append(json.loads(line))
73    return result

Parse JSON Lines: one JSON value per line.

def parse_grammar(path: str, *, trace: bool = False, output: str | None = None) -> Tree:
76def parse_grammar(
77    path: str,
78    *,
79    trace: bool = False,
80    output: str | None = None,
81) -> Tree:
82    """Parse a grammar file and return the parse tree."""
83    result = bun.run(_build_grammar_args(path, trace=trace), output=output)
84    return json.loads(result)

Parse a grammar file and return the parse tree.

def compile(path: str, *, output: str | None = None) -> tsemekwes.Grammar:
87def compile(path: str, *, output: str | None = None) -> Grammar:
88    """Compile a grammar file into a Grammar."""
89    grammar = json.loads(bun.run(["grammar", "-j", path], output=output))
90    return Grammar(grammar)

Compile a grammar file into a Grammar.

def parse_inputs( path: str, inputs: list[str], *, start: str | None = None, nproc: int | None = None, trace: bool = False, output: str | None = None) -> list[Tree]:
 93def parse_inputs(
 94    path: str,
 95    inputs: list[str],
 96    *,
 97    start: str | None = None,
 98    nproc: int | None = None,
 99    trace: bool = False,
100    output: str | None = None,
101) -> list[Tree]:
102    """Parse each input file against the grammar, return one Tree per input (JSONL)."""
103    result = bun.run(
104        _build_run_args(path, inputs, start=start, nproc=nproc, trace=trace),
105        output=output,
106    )
107    trees = parse_jsonl(result)
108    if len(trees) != len(inputs):
109        raise ValueError(
110            f"parse_inputs: expected {len(inputs)} result(s), got {len(trees)}"
111        )
112    return trees

Parse each input file against the grammar, return one Tree per input (JSONL).

def boot_grammar(*, output: str | None = None) -> tsemekwes.Grammar:
115def boot_grammar(*, output: str | None = None) -> Grammar:
116    """Get the bootstrapped TS'emekwes grammar."""
117    if output is not None:
118        result = bun.run(["boot", "--json"], output=output)
119    else:
120        with tempfile.NamedTemporaryFile(
121            mode="w+", suffix=".json", encoding="utf-8", delete=True
122        ) as tmp:
123            bun.run(["boot", "--json"], output=tmp.name)
124            result = Path(tmp.name).read_text()
125    return Grammar(json.loads(result))

Get the bootstrapped TS'emekwes grammar.

def boot_pretty(*, output: str | None = None) -> str:
128def boot_pretty(*, output: str | None = None) -> str:
129    """Get the bootstrapped grammar as a pretty-printed string."""
130    return bun.run(["boot", "--pretty"], output=output)

Get the bootstrapped grammar as a pretty-printed string.

def loads_grammar(json_str: str) -> tsemekwes.Grammar:
133def loads_grammar(json_str: str) -> Grammar:
134    """Deserialize a JSON string into a Grammar."""
135    return Grammar(json.loads(json_str))

Deserialize a JSON string into a Grammar.

def grammar_pretty(path: str, *, output: str | None = None) -> str:
138def grammar_pretty(path: str, *, output: str | None = None) -> str:
139    """Pretty-print a compiled grammar file."""
140    return bun.run(["grammar", "--pretty", path], output=output)

Pretty-print a compiled grammar file.

def read_grammar(path: str) -> tsemekwes.Grammar:
143def read_grammar(
144    path: str,
145) -> Grammar:
146    """Read a compiled grammar JSON file as a Grammar."""
147    text = Path(path).read_text()
148    return Grammar(json.loads(text))

Read a compiled grammar JSON file as a Grammar.