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