|
1 | 1 | import argparse |
2 | 2 | import contextlib |
| 3 | +import csv |
3 | 4 | import json |
4 | 5 | import sys |
5 | 6 | import time |
@@ -252,7 +253,7 @@ class CommandCompleter(Completer): |
252 | 253 | # Commands whose argument is a filesystem path. The completer |
253 | 254 | # delegates to ``PathCompleter`` once the user has typed past the |
254 | 255 | # command name. |
255 | | - PATH_ARG_COMMANDS = frozenset({"load", "save"}) |
| 256 | + PATH_ARG_COMMANDS = frozenset({"load", "save", "count-csv"}) |
256 | 257 |
|
257 | 258 | def __init__(self, commands: dict) -> None: |
258 | 259 | self.commands = commands |
@@ -365,6 +366,10 @@ def __init__(self, parser_name: str | None, settings: dict[str, Any]) -> None: |
365 | 366 | "help": "Count pattern matches across loaded edges (most common first)", |
366 | 367 | "handler": self.cmd_count, |
367 | 368 | }, |
| 369 | + "count-csv": { |
| 370 | + "help": "Like /count, but write counts to a .csv file instead", |
| 371 | + "handler": self.cmd_count_csv, |
| 372 | + }, |
368 | 373 | "types": { |
369 | 374 | "help": ( |
370 | 375 | "Count atom types across loaded edges " |
@@ -1006,6 +1011,79 @@ def cmd_count(self, args: list) -> bool: |
1006 | 1011 | ) |
1007 | 1012 | return False |
1008 | 1013 |
|
| 1014 | + def cmd_count_csv(self, args: list) -> bool: |
| 1015 | + if len(args) < 2: |
| 1016 | + self.console.print( |
| 1017 | + "[red]Error:[/red] /count-csv requires a file path and a pattern: " |
| 1018 | + "[cyan]/count-csv <path> <pattern>[/cyan]" |
| 1019 | + ) |
| 1020 | + return False |
| 1021 | + if not self.edges: |
| 1022 | + self.console.print("[yellow]No edges loaded.[/yellow]") |
| 1023 | + self.console.print( |
| 1024 | + "[dim]Use[/dim] [cyan]/load <path>[/cyan] [dim]first.[/dim]" |
| 1025 | + ) |
| 1026 | + return False |
| 1027 | + |
| 1028 | + csv_path = Path(args[0]).expanduser() |
| 1029 | + pattern_text = " ".join(args[1:]) |
| 1030 | + try: |
| 1031 | + pattern = hedge(pattern_text) |
| 1032 | + except Exception as e: |
| 1033 | + self.console.print(f"[red]Error:[/red] failed to parse pattern: {e}") |
| 1034 | + return False |
| 1035 | + if pattern is None: |
| 1036 | + self.console.print( |
| 1037 | + f"[red]Error:[/red] could not parse pattern: " |
| 1038 | + f"[cyan]{pattern_text}[/cyan]" |
| 1039 | + ) |
| 1040 | + return False |
| 1041 | + |
| 1042 | + recursive = bool(self.settings.get("search_recursive", True)) |
| 1043 | + |
| 1044 | + counter: Counter[Any] = Counter() |
| 1045 | + for top_edge in self.edges: |
| 1046 | + candidates: Iterable[Hyperedge] = ( |
| 1047 | + self._iter_subedges_ordered(top_edge) if recursive else [top_edge] |
| 1048 | + ) |
| 1049 | + for sub in candidates: |
| 1050 | + bindings_list = sub.match(pattern) |
| 1051 | + for b in bindings_list: |
| 1052 | + key: Any = tuple(sorted(b.items())) if b else sub |
| 1053 | + counter[key] += 1 |
| 1054 | + |
| 1055 | + if not counter: |
| 1056 | + self.console.print( |
| 1057 | + f"[yellow]No matches[/yellow] for [cyan]{pattern_text}[/cyan]" |
| 1058 | + ) |
| 1059 | + return False |
| 1060 | + |
| 1061 | + items = counter.most_common() |
| 1062 | + try: |
| 1063 | + with open(csv_path, "w", newline="") as f: |
| 1064 | + writer = csv.writer(f) |
| 1065 | + first_key = items[0][0] |
| 1066 | + if isinstance(first_key, tuple): |
| 1067 | + var_names = [var for var, _ in first_key] |
| 1068 | + writer.writerow(["count", *var_names]) |
| 1069 | + for key, count in items: |
| 1070 | + writer.writerow([count, *(str(val) for _, val in key)]) |
| 1071 | + else: |
| 1072 | + writer.writerow(["count", "edge"]) |
| 1073 | + for key, count in items: |
| 1074 | + writer.writerow([count, str(key)]) |
| 1075 | + except OSError as e: |
| 1076 | + self.console.print(f"[red]Error:[/red] failed to write {csv_path}: {e}") |
| 1077 | + return False |
| 1078 | + |
| 1079 | + self.console.print( |
| 1080 | + f"[green]✓[/green] Wrote [cyan]{len(items)}[/cyan] row(s) " |
| 1081 | + f"([green]{sum(counter.values())}[/green] total match(es), " |
| 1082 | + f"{'recursive' if recursive else 'top-level only'}) " |
| 1083 | + f"to [cyan]{csv_path}[/cyan]" |
| 1084 | + ) |
| 1085 | + return False |
| 1086 | + |
1009 | 1087 | def cmd_types(self, args: list) -> bool: |
1010 | 1088 | if not self.edges: |
1011 | 1089 | self.console.print("[yellow]No edges loaded.[/yellow]") |
|
0 commit comments