Skip to content

Commit 617b744

Browse files
committed
repl /save command
1 parent c2a1956 commit 617b744

2 files changed

Lines changed: 49 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
- readers provide source information.
1212
- `hyperbase.parsers.badness` module for parser-agnostic combined structural + token-matching validation.
1313
- built-in REPL setting `check_badness` to render a badness panel after each parse, available regardless of the active parser plugin.
14-
- more REPL commands: /load, /search, /count, /types and /transform.
14+
- more REPL commands: /load, /save, /search, /count, /types and /transform.
1515

1616
### Changed
1717

src/hyperbase/cli/repl.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ class CommandCompleter(Completer):
251251
# Commands whose argument is a filesystem path. The completer
252252
# delegates to ``PathCompleter`` once the user has typed past the
253253
# command name.
254-
PATH_ARG_COMMANDS = frozenset({"load"})
254+
PATH_ARG_COMMANDS = frozenset({"load", "save"})
255255

256256
def __init__(self, commands: dict) -> None:
257257
self.commands = commands
@@ -348,6 +348,10 @@ def __init__(self, parser_name: str | None, settings: dict[str, Any]) -> None:
348348
"help": "Load hyperedges from a .jsonl parse-results file",
349349
"handler": self.cmd_load,
350350
},
351+
"save": {
352+
"help": "Save in-memory edges to a .jsonl file",
353+
"handler": self.cmd_save,
354+
},
351355
"edges": {
352356
"help": "Show in-memory edges (count and source file)",
353357
"handler": self.cmd_edges,
@@ -754,6 +758,49 @@ def cmd_load(self, args: list) -> bool:
754758
)
755759
return False
756760

761+
def cmd_save(self, args: list) -> bool:
762+
if len(args) < 1:
763+
self.console.print(
764+
"[red]Error:[/red] /save requires a file path: "
765+
"[cyan]/save <path>[/cyan]"
766+
)
767+
return False
768+
if not self.edges:
769+
self.console.print("[yellow]No edges loaded.[/yellow]")
770+
return False
771+
772+
path = Path(" ".join(args)).expanduser()
773+
with_metadata = 0
774+
try:
775+
with open(path, "w") as f:
776+
for edge in self.edges:
777+
d: dict[str, Any] = {"edge": str(edge)}
778+
if edge.tokens is not None and edge.text is not None:
779+
d["text"] = edge.text
780+
d["tokens"] = list(edge.tokens)
781+
d["tok_pos"] = str(self._build_tok_pos_tree(edge))
782+
with_metadata += 1
783+
f.write(json.dumps(d, ensure_ascii=False) + "\n")
784+
except OSError as e:
785+
self.console.print(f"[red]Error:[/red] failed to write {path}: {e}")
786+
return False
787+
788+
self.console.print(
789+
f"[green]✓[/green] Saved [cyan]{len(self.edges)}[/cyan] edge(s) "
790+
f"to [cyan]{path}[/cyan] "
791+
f"[dim]({with_metadata} with source-position metadata)[/dim]"
792+
)
793+
return False
794+
795+
def _build_tok_pos_tree(self, edge: Hyperedge) -> Hyperedge:
796+
"""Build a tok_pos mirror tree where each atom is replaced by its
797+
``tok_pos`` (or ``-1`` for synthetic atoms with no position)."""
798+
if edge.atom:
799+
atom = cast(Atom, edge)
800+
pos = atom.tok_pos if atom.tok_pos is not None else -1
801+
return Atom(str(pos))
802+
return Hyperedge(tuple(self._build_tok_pos_tree(sub) for sub in edge))
803+
757804
def cmd_edges(self, args: list) -> bool:
758805
if not self.edges:
759806
self.console.print("[yellow]No edges loaded.[/yellow]")

0 commit comments

Comments
 (0)