Skip to content

Commit a5cf9ac

Browse files
refactor(parser): introduce icp function for creating InteractiveContextParser instances for clean handling of set_grammar
1 parent 1412315 commit a5cf9ac

7 files changed

Lines changed: 173 additions & 182 deletions

File tree

src/dylan/gui/parse_session.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@ def __init__(self) -> None:
120120
def set_grammar(self, path_str: str, *, repairing: bool) -> str:
121121
"""Load grammar from a filesystem directory *path_str*; return log text and set ``self.parser``.
122122
123-
Bundled grammar nicknames (e.g. ``\"ttr\"``) are only accepted by :func:`dynamicsyntax.set_grammar`;
123+
Bundled grammar nicknames (e.g. ``\"ttr\"``) are only accepted on a parser from
124+
:func:`dynamicsyntax.icp` via :meth:`InteractiveContextParser.set_grammar`;
124125
this GUI/session path expects a real directory from the file picker.
125126
"""
126127
p = Path(path_str.strip())

src/dylan/parser/interactive_context_parser.py

Lines changed: 99 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from dylan.formula.formula import Formula
2020
from dylan.formula.ttr_formula import TTRFormula
2121
from dylan.formula.ttr_record_type import TTRRecordType
22-
from dylan.nlp.types import Dialogue, WAIT_TOKEN, RELEASE_TURN_TOKEN, Utterance
22+
from dylan.nlp.types import DEFAULT_SPEAKER, Dialogue, WAIT_TOKEN, RELEASE_TURN_TOKEN, Utterance
2323
from dylan.parser.dag_parser import DAGParser
2424
from dylan.tree.label.labels import Requirement, TypeLabel
2525
from dylan.tree.node_address import NodeAddress
@@ -87,27 +87,108 @@ class InteractiveContextParser(DAGParser):
8787

8888
def __init__(
8989
self,
90-
resource_dir: str | Path,
90+
resource_dir: str | Path | None = None,
91+
*,
9192
repairing: bool = False,
9293
top_n: int | tuple[str, ...] = 3,
9394
participants: tuple[str, ...] = (DEFAULT_NAME,),
9495
) -> None:
95-
p = Path(resource_dir)
96+
"""Construct a parser with optional *resource_dir* (filesystem directory or bundled grammar id)."""
97+
participants_resolved = participants if participants else (DEFAULT_NAME,)
9698
if not isinstance(top_n, int):
97-
participants = top_n
99+
participants_resolved = top_n # type: ignore[assignment]
98100
top_n = 3
99-
super().__init__(Lexicon(p, top_n), Grammar(p), SpeechActInferenceGrammar(p))
101+
self._top_n = top_n
102+
self._participants = participants_resolved
103+
self._default_repairing = repairing
104+
105+
if resource_dir is None:
106+
self._init_shell_unloaded()
107+
return
108+
109+
p = Path(resource_dir) if isinstance(resource_dir, str) else resource_dir
110+
if p.is_dir():
111+
self._apply_resource_dir(p, repairing=repairing)
112+
return
113+
114+
self._init_shell_unloaded()
115+
self.set_grammar(resource_dir, repairing=repairing)
116+
117+
def _init_shell_unloaded(self) -> None:
118+
"""Initialise placeholder lexicon/grammar with no dialogue ``context`` until :meth:`set_grammar`."""
119+
DAGParser.__init__(
120+
self,
121+
Lexicon(None, self._top_n),
122+
Grammar(None),
123+
SpeechActInferenceGrammar(Path(".")),
124+
)
125+
self.context = None
126+
self.forced_restart = False
127+
self.forced_repair = False
128+
self.right_edge_indicators = []
129+
self.acks = ["uhu"]
130+
self.repairanda = ["uhh", "errm", "err", "er", "uh", "erm", "uhm", "um", "oh"]
131+
self.forced_repairanda = ["sorry", "oops", "wait", "erm"]
132+
self.restarters = ["yeah"]
133+
134+
def _apply_resource_dir(self, path: Path, *, repairing: bool) -> None:
135+
"""Load lexicon and grammars from *path* and wire a fresh :class:`Context`."""
136+
parts = self._participants if self._participants else (DEFAULT_NAME,)
137+
DAGParser.__init__(
138+
self,
139+
Lexicon(path, self._top_n),
140+
Grammar(path),
141+
SpeechActInferenceGrammar(path),
142+
)
100143
dag = WordLevelContextDAG()
101-
parts = participants if participants else (DEFAULT_NAME,)
102144
self.context = Context(dag, self.sa_grammar, *parts)
103145
self.context.set_repair_processing(repairing)
104146
self.forced_restart = False
105147
self.forced_repair = False
106-
self.right_edge_indicators: list[str] = []
107-
self.acks: list[str] = ["uhu"]
108-
self.repairanda: list[str] = ["uhh", "errm", "err", "er", "uh", "erm", "uhm", "um", "oh"]
109-
self.forced_repairanda: list[str] = ["sorry", "oops", "wait", "erm"]
110-
self.restarters: list[str] = ["yeah"]
148+
self.right_edge_indicators = []
149+
self.acks = ["uhu"]
150+
self.repairanda = ["uhh", "errm", "err", "er", "uh", "erm", "uhm", "um", "oh"]
151+
self.forced_repairanda = ["sorry", "oops", "wait", "erm"]
152+
self.restarters = ["yeah"]
153+
self.init()
154+
155+
def set_grammar(self, grammar: str | Path, *, repairing: bool | None = None) -> None:
156+
"""Load grammar files from a directory path or bundled grammar id/alias into this parser."""
157+
from dynamicsyntax._session import resolved_grammar_path
158+
159+
rep = self._default_repairing if repairing is None else repairing
160+
with resolved_grammar_path(grammar) as path:
161+
self._apply_resource_dir(path, repairing=rep)
162+
163+
def parse(
164+
self,
165+
sentence_or_goal: str | Formula | None = None,
166+
/,
167+
*,
168+
speaker: str = DEFAULT_SPEAKER,
169+
trace: bool = False,
170+
) -> object:
171+
"""Parse a surface string into a :class:`~dynamicsyntax.parse_result.ParseResult`, or run ``parse_goal``.
172+
173+
Pass a ``str`` for high-level sentence parsing (requires :meth:`set_grammar` first unless
174+
constructed with a grammar). Pass ``None`` or a :class:`~dylan.formula.formula.Formula`
175+
for the internal ``parse_goal`` path (Java ``DAGParser.parse``).
176+
"""
177+
if isinstance(sentence_or_goal, str):
178+
return self._parse_surface(sentence_or_goal, speaker=speaker, trace=trace)
179+
return self.parse_goal(sentence_or_goal)
180+
181+
def _parse_surface(self, sentence: str, *, speaker: str, trace: bool) -> object:
182+
"""Run :func:`~dynamicsyntax._parse._run_parse_core` for a non-goal surface string."""
183+
if self.context is None:
184+
raise ValueError("grammar not loaded; call set_grammar(...) first")
185+
from dynamicsyntax._parse import _run_parse_core
186+
from dynamicsyntax.parse_result import ParseResult
187+
188+
stripped = sentence.strip()
189+
if not stripped:
190+
return ParseResult(ok=False, semantics=None, tree=None, sentence="", parser=self)
191+
return _run_parse_core(self, stripped, speaker=speaker, trace=trace)
111192

112193
@classmethod
113194
def from_loaded(
@@ -125,6 +206,9 @@ def from_loaded(
125206
parts = participants if participants else (DEFAULT_NAME,)
126207
obj.context = Context(dag, obj.sa_grammar, *parts)
127208
obj.context.set_repair_processing(False)
209+
obj._top_n = lexicon.top_n
210+
obj._participants = parts
211+
obj._default_repairing = False
128212
obj.forced_restart = False
129213
obj.forced_repair = False
130214
obj.right_edge_indicators = []
@@ -136,10 +220,14 @@ def from_loaded(
136220

137221
def get_name(self) -> str:
138222
"""Return this parser's context name."""
223+
if self.context is None:
224+
return DEFAULT_NAME
139225
return self.context.get_name()
140226

141227
def repair_initiated(self) -> bool:
142228
"""Return whether local repair has been initiated."""
229+
if self.context is None:
230+
return False
143231
return self.context.repair_initiated()
144232

145233
def _adjust_once(self, goal: Formula | None) -> bool:

src/dynamicsyntax/__init__.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,42 @@
1-
"""Public facade for the *dynamicsyntax* distribution (high-level :func:`parse`)."""
1+
"""Public facade for the *dynamicsyntax* distribution (high-level :func:`parse` and :func:`icp`)."""
22

33
from __future__ import annotations
44

55
from importlib.metadata import PackageNotFoundError, version
6+
from pathlib import Path
67

78
from dylan.action.lexicon import NotebookMultilineText
89
from dylan.formula.latex.build_result import LaTeXBuildResult
910
from dylan.formula.manim.models import ManimBuildResult
1011
from dylan.formula.ttr_record_type import TTRRecordType
11-
from dylan.parser.interactive_context_parser import InteractiveContextParser
12+
from dylan.parser.interactive_context_parser import DEFAULT_NAME, InteractiveContextParser
1213

1314
from dynamicsyntax._manim import to_manim
1415
from dynamicsyntax._parse import parse
15-
from dynamicsyntax._session import get_datasets, get_grammars, set_grammar
16+
from dynamicsyntax._session import get_datasets, get_grammars
1617
from dynamicsyntax.parse_result import ParseResult
1718

1819
try:
1920
__version__: str = version("dynamicsyntax")
2021
except PackageNotFoundError: # pragma: no cover - editable checkout without metadata
2122
__version__ = "0.0.0"
2223

24+
25+
def icp(
26+
grammar: str | Path | None = None,
27+
*,
28+
repairing: bool = False,
29+
top_n: int | tuple[str, ...] = 3,
30+
participants: tuple[str, ...] = (DEFAULT_NAME,),
31+
) -> InteractiveContextParser:
32+
"""Return an :class:`~dylan.parser.interactive_context_parser.InteractiveContextParser` (short alias ``icp``).
33+
34+
With no *grammar*, returns an unloaded parser; call :meth:`~dylan.parser.interactive_context_parser.InteractiveContextParser.set_grammar`
35+
before :meth:`~dylan.parser.interactive_context_parser.InteractiveContextParser.parse`.
36+
"""
37+
return InteractiveContextParser(grammar, repairing=repairing, top_n=top_n, participants=participants)
38+
39+
2340
__all__ = [
2441
"InteractiveContextParser",
2542
"LaTeXBuildResult",
@@ -30,7 +47,7 @@
3047
"__version__",
3148
"get_datasets",
3249
"get_grammars",
33-
"set_grammar",
50+
"icp",
3451
"parse",
3552
"to_manim",
3653
]

src/dynamicsyntax/_parse.py

Lines changed: 20 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from dylan.parser.interactive_context_parser import InteractiveContextParser
1313
from dylan.tree.tree import Tree
1414

15-
from dynamicsyntax._session import resolved_grammar_path, session_parser
15+
from dynamicsyntax._session import resolved_grammar_path
1616
from dynamicsyntax.parse_trace import ParseActionStep
1717
from dynamicsyntax.parse_result import ParseResult
1818

@@ -142,30 +142,16 @@ def _parse_at_path(grammar_path: Path, sentence: str, *, speaker: str, trace: bo
142142
return _parse_one(parser, sentence, speaker=speaker, trace=trace)
143143

144144

145-
@overload
146-
def parse(
147-
sentence: str,
148-
/,
149-
*,
150-
speaker: str = ...,
151-
trace: bool = ...,
152-
) -> ParseResult: ...
153-
154-
155-
@overload
156-
def parse(
157-
sentences: list[str],
158-
/,
159-
*,
160-
speaker: str = ...,
161-
trace: bool = ...,
162-
) -> list[ParseResult]: ...
145+
_GRAMMAR_REQUIRED_MSG = (
146+
"grammar is required for dynamicsyntax.parse(...); use parser.parse(...) after "
147+
"dynamicsyntax.icp().set_grammar(...), or pass grammar= to parse(...)"
148+
)
163149

164150

165151
@overload
166152
def parse(
167153
sentence: str,
168-
grammar: str | Path | None,
154+
grammar: str | Path,
169155
/,
170156
*,
171157
speaker: str = ...,
@@ -176,7 +162,7 @@ def parse(
176162
@overload
177163
def parse(
178164
sentences: list[str],
179-
grammar: str | Path | None,
165+
grammar: str | Path,
180166
/,
181167
*,
182168
speaker: str = ...,
@@ -197,8 +183,9 @@ def parse(
197183
:param sentence_or_sentences: A single whitespace-tokenised surface string, or a list of
198184
such strings (lowercased by the tokenizer). An empty list returns ``[]`` without using
199185
a grammar. Per-item blank or whitespace-only strings yield a failed result for that slot.
200-
:param grammar: Bundled id or alias (e.g. ``\"ttr\"``), a grammar directory path, or
201-
``None`` to use the parser from :func:`~dynamicsyntax.set_grammar`.
186+
:param grammar: Bundled id or alias (e.g. ``\"ttr\"``) or a grammar directory path. Required
187+
for non-empty input; for grammar-bound parsing on a long-lived object use
188+
:func:`dynamicsyntax.icp` and :meth:`~dylan.parser.interactive_context_parser.InteractiveContextParser.parse`.
202189
:param speaker: Dialogue participant id passed to the parser (default matches ``dylan``).
203190
:param trace: If ``True``, record one DS tree after ``new_sentence`` and after each word
204191
(for :meth:`~dynamicsyntax.parse_result.ParseResult.to_latex` ``incremental``).
@@ -207,8 +194,7 @@ def parse(
207194
Each result may include ``parser`` (the
208195
:class:`~dylan.parser.interactive_context_parser.InteractiveContextParser` used), except when
209196
the facade returns early for whitespace-only single-string input without a parse.
210-
:raises ValueError: If *grammar* is omitted or ``None`` but no session grammar was set with
211-
:func:`~dynamicsyntax.set_grammar` (non-empty sentence, or any list item non-blank after strip).
197+
:raises ValueError: If *grammar* is omitted or ``None`` while any non-blank input would require parsing.
212198
:raises FileNotFoundError: If *grammar* is unknown or not a directory.
213199
214200
Packaged grammars: ``dynamicsyntax/grammars/`` in the library, and the project
@@ -220,34 +206,24 @@ def parse(
220206
sentences = sentence_or_sentences
221207
if not sentences:
222208
return []
223-
if grammar is not None:
224-
with resolved_grammar_path(grammar) as grammar_path:
225-
parser = InteractiveContextParser(grammar_path)
226-
return [_parse_one(parser, s, speaker=speaker, trace=trace) for s in sentences]
227-
parser = session_parser()
228-
if parser is None:
209+
if grammar is None:
229210
if any(s.strip() for s in sentences):
230-
raise ValueError(
231-
"no grammar set; call dynamicsyntax.set_grammar(...) first or pass grammar= to parse(...)",
232-
)
211+
raise ValueError(_GRAMMAR_REQUIRED_MSG)
233212
return [
234213
ParseResult(ok=False, semantics=None, tree=None, sentence="", parser=None)
235214
for _ in sentences
236215
]
237-
return [_parse_one(parser, s, speaker=speaker, trace=trace) for s in sentences]
216+
with resolved_grammar_path(grammar) as grammar_path:
217+
parser = InteractiveContextParser(grammar_path)
218+
return [_parse_one(parser, s, speaker=speaker, trace=trace) for s in sentences]
238219

239220
sentence = sentence_or_sentences
240221
stripped = sentence.strip()
241222
if not stripped:
242223
return ParseResult(ok=False, semantics=None, tree=None, sentence="", parser=None)
243224

244-
if grammar is not None:
245-
with resolved_grammar_path(grammar) as grammar_path:
246-
return _parse_at_path(grammar_path, stripped, speaker=speaker, trace=trace)
225+
if grammar is None:
226+
raise ValueError(_GRAMMAR_REQUIRED_MSG)
247227

248-
parser = session_parser()
249-
if parser is None:
250-
raise ValueError(
251-
"no grammar set; call dynamicsyntax.set_grammar(...) first or pass grammar= to parse(...)",
252-
)
253-
return _run_parse_core(parser, stripped, speaker=speaker, trace=trace)
228+
with resolved_grammar_path(grammar) as grammar_path:
229+
return _parse_at_path(grammar_path, stripped, speaker=speaker, trace=trace)

0 commit comments

Comments
 (0)