quai 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quai-0.3.0/.gitignore +4 -0
- quai-0.3.0/PKG-INFO +69 -0
- quai-0.3.0/README.md +49 -0
- quai-0.3.0/pyproject.toml +33 -0
- quai-0.3.0/quai/__init__.py +14 -0
- quai-0.3.0/quai/cli.py +190 -0
- quai-0.3.0/quai/session.py +89 -0
quai-0.3.0/.gitignore
ADDED
quai-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: quai
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Interactive Quarb — a session REPL where every line becomes a reusable query (&1, &2, …)
|
|
5
|
+
Project-URL: Homepage, https://quarb.org/
|
|
6
|
+
Project-URL: Playground, https://demo.quarb.org/quai/
|
|
7
|
+
Project-URL: Cookbook, https://quarb.org/cookbooks/quai.html
|
|
8
|
+
Project-URL: Repository, https://gitlab.com/quarb/engine
|
|
9
|
+
Author: Bojan Đuričković
|
|
10
|
+
License: MIT OR Apache-2.0
|
|
11
|
+
Keywords: interactive,json,quarb,query,repl
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Utilities
|
|
17
|
+
Requires-Python: >=3.8
|
|
18
|
+
Requires-Dist: quarb>=0.3
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# quai
|
|
22
|
+
|
|
23
|
+
Interactive [Quarb][quarb] — a session REPL where every line becomes a
|
|
24
|
+
reusable query.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install quai
|
|
28
|
+
quai data.json
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`quai` opens a session over one or more sources and holds it. Each
|
|
32
|
+
line you run is labelled `&1`, `&2`, … — and every label is a
|
|
33
|
+
reusable *macro*: not the printed value, but the *query* that produced
|
|
34
|
+
it, ready to continue through the pipe. It is the notebook loop, cell
|
|
35
|
+
by cell, except a cell is a path into your data.
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
&1 /teams/*/members/*
|
|
39
|
+
/teams/0/members/0 ⋮ (every member, across all teams, one path)
|
|
40
|
+
&2 &1 | [/langs/*:: = 'Go'] | /name::
|
|
41
|
+
ada
|
|
42
|
+
eu
|
|
43
|
+
&3 &1 @| count
|
|
44
|
+
6
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- `&N` re-runs line N; continue with a pipe (`&N | /key::`,
|
|
48
|
+
`&N | [pred]`, `&N @| count`).
|
|
49
|
+
- `&N#` replays line N's output frozen, as it was when it ran.
|
|
50
|
+
- `def &x: … ;` adds a named fragment; `:history`, `:reset`, `:help`,
|
|
51
|
+
`:quit` are the commands.
|
|
52
|
+
|
|
53
|
+
The engine rides in on the [`quarb`][quarb] dependency, so `quai`
|
|
54
|
+
reaches every local source `quarb` does — JSON, YAML, TOML, CSV, XML,
|
|
55
|
+
HTML, Markdown, SQLite, kaiv, the filesystem, git, archives, XLSX, and
|
|
56
|
+
source code — and several at once mount under one root for
|
|
57
|
+
cross-source `<=>` joins.
|
|
58
|
+
|
|
59
|
+
Try it in the browser at [demo.quarb.org/quai][playground] (no
|
|
60
|
+
install), and read the [graph cookbook][cookbook] for the recipes.
|
|
61
|
+
|
|
62
|
+
[quarb]: https://quarb.org/
|
|
63
|
+
[playground]: https://demo.quarb.org/quai/
|
|
64
|
+
[cookbook]: https://quarb.org/cookbooks/quai.html
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
Licensed under either of Apache License, Version 2.0 or the MIT license
|
|
69
|
+
at your option.
|
quai-0.3.0/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# quai
|
|
2
|
+
|
|
3
|
+
Interactive [Quarb][quarb] — a session REPL where every line becomes a
|
|
4
|
+
reusable query.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install quai
|
|
8
|
+
quai data.json
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`quai` opens a session over one or more sources and holds it. Each
|
|
12
|
+
line you run is labelled `&1`, `&2`, … — and every label is a
|
|
13
|
+
reusable *macro*: not the printed value, but the *query* that produced
|
|
14
|
+
it, ready to continue through the pipe. It is the notebook loop, cell
|
|
15
|
+
by cell, except a cell is a path into your data.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
&1 /teams/*/members/*
|
|
19
|
+
/teams/0/members/0 ⋮ (every member, across all teams, one path)
|
|
20
|
+
&2 &1 | [/langs/*:: = 'Go'] | /name::
|
|
21
|
+
ada
|
|
22
|
+
eu
|
|
23
|
+
&3 &1 @| count
|
|
24
|
+
6
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- `&N` re-runs line N; continue with a pipe (`&N | /key::`,
|
|
28
|
+
`&N | [pred]`, `&N @| count`).
|
|
29
|
+
- `&N#` replays line N's output frozen, as it was when it ran.
|
|
30
|
+
- `def &x: … ;` adds a named fragment; `:history`, `:reset`, `:help`,
|
|
31
|
+
`:quit` are the commands.
|
|
32
|
+
|
|
33
|
+
The engine rides in on the [`quarb`][quarb] dependency, so `quai`
|
|
34
|
+
reaches every local source `quarb` does — JSON, YAML, TOML, CSV, XML,
|
|
35
|
+
HTML, Markdown, SQLite, kaiv, the filesystem, git, archives, XLSX, and
|
|
36
|
+
source code — and several at once mount under one root for
|
|
37
|
+
cross-source `<=>` joins.
|
|
38
|
+
|
|
39
|
+
Try it in the browser at [demo.quarb.org/quai][playground] (no
|
|
40
|
+
install), and read the [graph cookbook][cookbook] for the recipes.
|
|
41
|
+
|
|
42
|
+
[quarb]: https://quarb.org/
|
|
43
|
+
[playground]: https://demo.quarb.org/quai/
|
|
44
|
+
[cookbook]: https://quarb.org/cookbooks/quai.html
|
|
45
|
+
|
|
46
|
+
## License
|
|
47
|
+
|
|
48
|
+
Licensed under either of Apache License, Version 2.0 or the MIT license
|
|
49
|
+
at your option.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "quai"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Interactive Quarb — a session REPL where every line becomes a reusable query (&1, &2, …)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT OR Apache-2.0" }
|
|
12
|
+
authors = [{ name = "Bojan Đuričković" }]
|
|
13
|
+
keywords = ["query", "repl", "interactive", "quarb", "json"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"License :: OSI Approved :: Apache Software License",
|
|
18
|
+
"Topic :: Utilities",
|
|
19
|
+
"Environment :: Console",
|
|
20
|
+
]
|
|
21
|
+
dependencies = ["quarb>=0.3"]
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
quai = "quai.cli:main"
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://quarb.org/"
|
|
28
|
+
Playground = "https://demo.quarb.org/quai/"
|
|
29
|
+
Cookbook = "https://quarb.org/cookbooks/quai.html"
|
|
30
|
+
Repository = "https://gitlab.com/quarb/engine"
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.wheel]
|
|
33
|
+
packages = ["quai"]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Interactive Quarb — the ``quai`` session REPL.
|
|
2
|
+
|
|
3
|
+
A thin, pure-Python front end over the :mod:`quarb` engine: it opens a
|
|
4
|
+
source, holds a session, and labels each accepted line ``&1``, ``&2``,
|
|
5
|
+
… — every label a reusable macro (the *query*, not the printed value),
|
|
6
|
+
ready to continue through the pipe. ``pip install quai`` gives you the
|
|
7
|
+
``quai`` command; the engine itself rides in on the ``quarb``
|
|
8
|
+
dependency.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .session import Session
|
|
12
|
+
|
|
13
|
+
__version__ = "0.3.0"
|
|
14
|
+
__all__ = ["Session"]
|
quai-0.3.0/quai/cli.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""The ``quai`` command: an interactive Quarb REPL.
|
|
2
|
+
|
|
3
|
+
Built on :func:`input`, with the :mod:`readline` module imported so
|
|
4
|
+
backspace, arrow keys, and input-line history work out of the box. The
|
|
5
|
+
color codes in the prompt are wrapped in readline's zero-width markers
|
|
6
|
+
(``\\001``/``\\002``) so cursor math near the prompt stays correct.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
import readline # noqa: F401 — enables line editing for input()
|
|
18
|
+
except ImportError: # pragma: no cover — e.g. Windows without pyreadline
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
import quarb
|
|
22
|
+
|
|
23
|
+
from .session import Session
|
|
24
|
+
|
|
25
|
+
_CYAN = "\001\x1b[36m\002"
|
|
26
|
+
_RESET = "\001\x1b[0m\002"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def main() -> None:
|
|
30
|
+
ap = argparse.ArgumentParser(
|
|
31
|
+
prog="quai",
|
|
32
|
+
description="Interactive Quarb — each line becomes a reusable "
|
|
33
|
+
"query (&1, &2, ...).",
|
|
34
|
+
)
|
|
35
|
+
ap.add_argument(
|
|
36
|
+
"paths",
|
|
37
|
+
nargs="*",
|
|
38
|
+
help="source(s): a document (.json/.yaml/.toml/.csv/.xml/.html/"
|
|
39
|
+
".md), a SQLite/kaiv/xlsx file, a directory, or git:PATH. "
|
|
40
|
+
"Several mount as one root, so a single query — including a "
|
|
41
|
+
"<=> join — spans them all.",
|
|
42
|
+
)
|
|
43
|
+
args = ap.parse_args()
|
|
44
|
+
if not args.paths:
|
|
45
|
+
ap.error("quai needs at least one source (a document, a directory, or git:PATH)")
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
doc = (
|
|
49
|
+
quarb.open(args.paths[0])
|
|
50
|
+
if len(args.paths) == 1
|
|
51
|
+
else quarb.mount(args.paths)
|
|
52
|
+
)
|
|
53
|
+
except Exception as e: # noqa: BLE001 — surface the engine's message
|
|
54
|
+
sys.exit(f"quai: {e}")
|
|
55
|
+
|
|
56
|
+
session = Session(doc)
|
|
57
|
+
color = sys.stdout.isatty() and not os.environ.get("NO_COLOR")
|
|
58
|
+
print(
|
|
59
|
+
f"quai - interactive Quarb over {', '.join(args.paths)}. "
|
|
60
|
+
":help for commands, :quit (or Ctrl-D) to leave."
|
|
61
|
+
)
|
|
62
|
+
_repl(session, color)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _repl(session: Session, color: bool) -> None:
|
|
66
|
+
while True:
|
|
67
|
+
prompt = (
|
|
68
|
+
f"{_CYAN}&{session.line_no}{_RESET} " if color else f"&{session.line_no} "
|
|
69
|
+
)
|
|
70
|
+
try:
|
|
71
|
+
line = input(prompt)
|
|
72
|
+
except EOFError:
|
|
73
|
+
print()
|
|
74
|
+
break
|
|
75
|
+
except KeyboardInterrupt:
|
|
76
|
+
print()
|
|
77
|
+
continue
|
|
78
|
+
line = line.strip()
|
|
79
|
+
if not line:
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
# A ':' command (a query cannot start with a lone ':').
|
|
83
|
+
if line.startswith(":") and not line.startswith("::"):
|
|
84
|
+
if _command(session, line):
|
|
85
|
+
break
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
# A definition extends the macro table but is not itself run.
|
|
89
|
+
if (
|
|
90
|
+
line.startswith("def ")
|
|
91
|
+
or line == "def"
|
|
92
|
+
or line.startswith("macro ")
|
|
93
|
+
or line == "macro"
|
|
94
|
+
):
|
|
95
|
+
try:
|
|
96
|
+
session.add_def(line)
|
|
97
|
+
except Exception as e: # noqa: BLE001
|
|
98
|
+
print(f"error: {e}", file=sys.stderr)
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
# Capture references (&N#, &N!) are resolved here, not by the
|
|
102
|
+
# engine (its lexer has no '#', and '!' marks data-aware macros).
|
|
103
|
+
try:
|
|
104
|
+
kind, arg = _prepare(line)
|
|
105
|
+
except ValueError as e:
|
|
106
|
+
print(f"error: {e}", file=sys.stderr)
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
if kind == "frozen":
|
|
110
|
+
snap = session.frozen(arg)
|
|
111
|
+
if snap is None:
|
|
112
|
+
print(
|
|
113
|
+
f"error: &{arg}# has no captured result (line {arg} hasn't run)",
|
|
114
|
+
file=sys.stderr,
|
|
115
|
+
)
|
|
116
|
+
else:
|
|
117
|
+
for v in snap:
|
|
118
|
+
print(_render(v))
|
|
119
|
+
session.record_frozen(snap)
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
values = session.eval(arg)
|
|
124
|
+
except Exception as e: # noqa: BLE001
|
|
125
|
+
print(f"error: {e}", file=sys.stderr)
|
|
126
|
+
continue
|
|
127
|
+
for v in values:
|
|
128
|
+
print(_render(v))
|
|
129
|
+
n = session.line_no
|
|
130
|
+
if not session.commit(arg, values):
|
|
131
|
+
print(
|
|
132
|
+
f"note: &{n} is not referenceable (its shape can't be a macro body)",
|
|
133
|
+
file=sys.stderr,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _prepare(line: str):
|
|
138
|
+
n = _numeric_ref(line, "#")
|
|
139
|
+
if n is not None:
|
|
140
|
+
return ("frozen", n)
|
|
141
|
+
n = _numeric_ref(line, "!")
|
|
142
|
+
if n is not None:
|
|
143
|
+
# Live re-run; for a fixed in-process document this equals &N.
|
|
144
|
+
return ("eval", f"&{n}")
|
|
145
|
+
if "#" in line:
|
|
146
|
+
raise ValueError(
|
|
147
|
+
"'#' is the frozen-history suffix, valid only as a standalone '&N#'"
|
|
148
|
+
)
|
|
149
|
+
return ("eval", line)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _numeric_ref(line: str, suffix: str):
|
|
153
|
+
if line.endswith(suffix) and line.startswith("&"):
|
|
154
|
+
body = line[1:-1]
|
|
155
|
+
if body.isdigit():
|
|
156
|
+
return int(body)
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _render(v) -> str:
|
|
161
|
+
if isinstance(v, dict):
|
|
162
|
+
return json.dumps(v, ensure_ascii=False)
|
|
163
|
+
return str(v)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _command(session: Session, line: str) -> bool:
|
|
167
|
+
if line in (":q", ":quit"):
|
|
168
|
+
return True
|
|
169
|
+
if line in (":help", ":?"):
|
|
170
|
+
print(
|
|
171
|
+
" <query> run a query; its result is labelled &N and reusable\n"
|
|
172
|
+
" &N re-run line N (a macro); continue with a pipe: &N | /key::\n"
|
|
173
|
+
" &N# replay line N's frozen output (as it was when it ran)\n"
|
|
174
|
+
" def &x: ... ; add a named fragment to the session\n"
|
|
175
|
+
" :history show the macro table (&1, &2, ...)\n"
|
|
176
|
+
" :reset clear the history and restart numbering\n"
|
|
177
|
+
" :quit leave (also Ctrl-D)"
|
|
178
|
+
)
|
|
179
|
+
elif line == ":history":
|
|
180
|
+
h = session.history().strip()
|
|
181
|
+
print(h if h else "(no history yet)")
|
|
182
|
+
elif line == ":reset":
|
|
183
|
+
session.reset()
|
|
184
|
+
else:
|
|
185
|
+
print(f"unknown command '{line}' (:help lists them)")
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
if __name__ == "__main__":
|
|
190
|
+
main()
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""The interactive session: a macro table where each accepted line
|
|
2
|
+
becomes ``def &N: <line> ;``, evaluated through the :mod:`quarb`
|
|
3
|
+
engine.
|
|
4
|
+
|
|
5
|
+
History is the language's own reuse mechanism, not a bolted-on cell
|
|
6
|
+
store: line 3 is the fragment ``&3``, continued through the pipe
|
|
7
|
+
(``&3 | /name::``, ``&3 | [pred]``, ``&3 @| count``). A standalone
|
|
8
|
+
``&N#`` replays a line's captured output (the frozen footprint); in
|
|
9
|
+
this pure-Python front end the source is materialized once, so
|
|
10
|
+
``&N``, ``&N!`` and ``&N#`` coincide for a fixed document.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import quarb
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Session:
|
|
19
|
+
def __init__(self, doc: "quarb.Document"):
|
|
20
|
+
self.doc = doc
|
|
21
|
+
# The macro table as definition text, prepended to each query
|
|
22
|
+
# so ``&N`` history resolves inline.
|
|
23
|
+
self.defs_text = ""
|
|
24
|
+
# Each line's output, captured at commit — what ``&N#`` replays.
|
|
25
|
+
self.snapshots: dict[int, list] = {}
|
|
26
|
+
# The next line's number — the ``&N`` a fresh line will claim.
|
|
27
|
+
self.line_no = 1
|
|
28
|
+
|
|
29
|
+
def _combined(self, line: str) -> str:
|
|
30
|
+
if not self.defs_text:
|
|
31
|
+
return line
|
|
32
|
+
return f"{self.defs_text}\n{line}"
|
|
33
|
+
|
|
34
|
+
def eval(self, line: str) -> list:
|
|
35
|
+
"""Run a line against the source with the current macro table
|
|
36
|
+
prepended. Node results come back as locator strings, value
|
|
37
|
+
results as typed Python values (dicts for records)."""
|
|
38
|
+
return self.doc.values(self._combined(line))
|
|
39
|
+
|
|
40
|
+
def commit(self, line: str, snapshot: list) -> bool:
|
|
41
|
+
"""Register an accepted line as ``&N`` and capture its output as
|
|
42
|
+
the frozen footprint for ``&N#``. Returns whether the line's
|
|
43
|
+
shape can be a macro body (so ``&N`` will resolve); either way
|
|
44
|
+
the line number advances so labels track what the user saw."""
|
|
45
|
+
self.snapshots[self.line_no] = list(snapshot)
|
|
46
|
+
# A space before the ``;`` terminator: a line ending in a ``::``
|
|
47
|
+
# projection would otherwise lex ``::;`` as the metadata sigil.
|
|
48
|
+
candidate = f"{self.defs_text}def &{self.line_no}: {line} ;\n"
|
|
49
|
+
referenceable = self._defs_ok(candidate)
|
|
50
|
+
if referenceable:
|
|
51
|
+
self.defs_text = candidate
|
|
52
|
+
self.line_no += 1
|
|
53
|
+
return referenceable
|
|
54
|
+
|
|
55
|
+
def _defs_ok(self, candidate: str) -> bool:
|
|
56
|
+
# Validate by running the candidate table plus a reference to
|
|
57
|
+
# the new line; a parse error means it cannot be a macro body.
|
|
58
|
+
try:
|
|
59
|
+
self.doc.values(f"{candidate}&{self.line_no}")
|
|
60
|
+
return True
|
|
61
|
+
except Exception:
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
def frozen(self, n: int):
|
|
65
|
+
"""The frozen output of line ``n``, if captured."""
|
|
66
|
+
return self.snapshots.get(n)
|
|
67
|
+
|
|
68
|
+
def record_frozen(self, snapshot: list) -> None:
|
|
69
|
+
"""Record a frozen-recall line: it takes the next number and
|
|
70
|
+
keeps its own snapshot, but is not a referenceable macro body."""
|
|
71
|
+
self.snapshots[self.line_no] = list(snapshot)
|
|
72
|
+
self.line_no += 1
|
|
73
|
+
|
|
74
|
+
def add_def(self, line: str) -> None:
|
|
75
|
+
"""Add a ``def``/``macro`` line to the table (validated first)."""
|
|
76
|
+
candidate = f"{self.defs_text}{line}\n"
|
|
77
|
+
# Parse the whole table by running it with a trivial trailing
|
|
78
|
+
# query (``/`` — the root — always parses); a malformed
|
|
79
|
+
# definition raises here, before it can poison later lines.
|
|
80
|
+
self.doc.values(f"{candidate}/")
|
|
81
|
+
self.defs_text = candidate
|
|
82
|
+
|
|
83
|
+
def history(self) -> str:
|
|
84
|
+
return self.defs_text
|
|
85
|
+
|
|
86
|
+
def reset(self) -> None:
|
|
87
|
+
self.defs_text = ""
|
|
88
|
+
self.snapshots = {}
|
|
89
|
+
self.line_no = 1
|