pycg3 0.2.0__py3-none-any.whl
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.
- pycg3/__init__.py +35 -0
- pycg3/cli.py +111 -0
- pycg3/cohort.py +325 -0
- pycg3/context.py +267 -0
- pycg3/engine.py +75 -0
- pycg3/grammar.py +497 -0
- pycg3/pipeline.py +142 -0
- pycg3/rule.py +253 -0
- pycg3/set_def.py +179 -0
- pycg3/stream.py +235 -0
- pycg3/tag.py +140 -0
- pycg3-0.2.0.dist-info/METADATA +227 -0
- pycg3-0.2.0.dist-info/RECORD +17 -0
- pycg3-0.2.0.dist-info/WHEEL +5 -0
- pycg3-0.2.0.dist-info/entry_points.txt +3 -0
- pycg3-0.2.0.dist-info/licenses/LICENSE +674 -0
- pycg3-0.2.0.dist-info/top_level.txt +1 -0
pycg3/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""pycg3 - A pure Python implementation of VISL CG-3 (Constraint Grammar 3)."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.2.0"
|
|
4
|
+
|
|
5
|
+
from pycg3.tag import TagPattern, is_wordform, is_baseform
|
|
6
|
+
from pycg3.cohort import Cohort, Reading, SingleWindow
|
|
7
|
+
from pycg3.set_def import SetDefinition, CompositeTag
|
|
8
|
+
from pycg3.context import ContextualTest
|
|
9
|
+
from pycg3.rule import Rule
|
|
10
|
+
from pycg3.grammar import Grammar, GrammarParser
|
|
11
|
+
from pycg3.stream import parse_stream
|
|
12
|
+
from pycg3.engine import Disambiguator
|
|
13
|
+
from pycg3.pipeline import Cg, CgWord, analyses_to_cg, analysis_to_cg
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"__version__",
|
|
17
|
+
"TagPattern",
|
|
18
|
+
"is_wordform",
|
|
19
|
+
"is_baseform",
|
|
20
|
+
"Cohort",
|
|
21
|
+
"Reading",
|
|
22
|
+
"SingleWindow",
|
|
23
|
+
"SetDefinition",
|
|
24
|
+
"CompositeTag",
|
|
25
|
+
"ContextualTest",
|
|
26
|
+
"Rule",
|
|
27
|
+
"Grammar",
|
|
28
|
+
"GrammarParser",
|
|
29
|
+
"parse_stream",
|
|
30
|
+
"Disambiguator",
|
|
31
|
+
"Cg",
|
|
32
|
+
"CgWord",
|
|
33
|
+
"analyses_to_cg",
|
|
34
|
+
"analysis_to_cg",
|
|
35
|
+
]
|
pycg3/cli.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Command-line interface compatible with vislcg3."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pycg3 import __version__
|
|
6
|
+
from pycg3.grammar import GrammarParser
|
|
7
|
+
from pycg3.engine import Disambiguator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main(args=None):
|
|
11
|
+
parser = argparse.ArgumentParser(
|
|
12
|
+
prog="pycg3",
|
|
13
|
+
description="A pure Python implementation of VISL CG-3 (Constraint Grammar 3)"
|
|
14
|
+
)
|
|
15
|
+
parser.add_argument(
|
|
16
|
+
"-g", "--grammar",
|
|
17
|
+
required=True,
|
|
18
|
+
help="Grammar file to load and apply."
|
|
19
|
+
)
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"-i", "-I", "--input",
|
|
22
|
+
dest="input_file",
|
|
23
|
+
default=None,
|
|
24
|
+
help="Input file to read (default: stdin)."
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"-o", "-O", "--output",
|
|
28
|
+
dest="output_file",
|
|
29
|
+
default=None,
|
|
30
|
+
help="Output file to write (default: stdout)."
|
|
31
|
+
)
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"-e", "--show-rules",
|
|
34
|
+
action="store_true",
|
|
35
|
+
help="Mark modified/selected readings with <<<."
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"-t", "--trace",
|
|
39
|
+
action="store_true",
|
|
40
|
+
help="Provide trace output of rules that modified/selected readings."
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"-p", "--pipe-deleted",
|
|
44
|
+
action="store_true",
|
|
45
|
+
help="Output deleted readings prefixed with ;."
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"-s", "--sections",
|
|
49
|
+
type=int,
|
|
50
|
+
default=None,
|
|
51
|
+
help="Number of sections to execute."
|
|
52
|
+
)
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"-v", "--version",
|
|
55
|
+
action="version",
|
|
56
|
+
version=f"pycg3 {__version__} (compatible with VISL CG-3)",
|
|
57
|
+
help="Show version information and exit."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
parsed = parser.parse_args(args)
|
|
61
|
+
|
|
62
|
+
# Read grammar
|
|
63
|
+
try:
|
|
64
|
+
with open(parsed.grammar, "r", encoding="utf-8", errors="replace") as gf:
|
|
65
|
+
grammar_text = gf.read()
|
|
66
|
+
except Exception as e:
|
|
67
|
+
sys.stderr.write(f"Error opening grammar file '{parsed.grammar}': {e}\n")
|
|
68
|
+
sys.exit(1)
|
|
69
|
+
|
|
70
|
+
grammar = GrammarParser().parse(grammar_text)
|
|
71
|
+
engine = Disambiguator(grammar, trace=parsed.trace)
|
|
72
|
+
|
|
73
|
+
# Input stream
|
|
74
|
+
if parsed.input_file:
|
|
75
|
+
try:
|
|
76
|
+
in_stream = open(parsed.input_file, "r", encoding="utf-8", errors="replace")
|
|
77
|
+
except Exception as e:
|
|
78
|
+
sys.stderr.write(f"Error opening input file '{parsed.input_file}': {e}\n")
|
|
79
|
+
sys.exit(1)
|
|
80
|
+
else:
|
|
81
|
+
in_stream = sys.stdin
|
|
82
|
+
|
|
83
|
+
# Output stream
|
|
84
|
+
if parsed.output_file:
|
|
85
|
+
try:
|
|
86
|
+
out_stream = open(parsed.output_file, "w", encoding="utf-8")
|
|
87
|
+
except Exception as e:
|
|
88
|
+
sys.stderr.write(f"Error opening output file '{parsed.output_file}': {e}\n")
|
|
89
|
+
sys.exit(1)
|
|
90
|
+
else:
|
|
91
|
+
out_stream = sys.stdout
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
for win in engine.run_stream(in_stream, num_sections=parsed.sections):
|
|
95
|
+
formatted = win.to_text(
|
|
96
|
+
show_rules=parsed.show_rules,
|
|
97
|
+
trace=parsed.trace,
|
|
98
|
+
pipe_deleted=parsed.pipe_deleted
|
|
99
|
+
)
|
|
100
|
+
out_stream.write(formatted)
|
|
101
|
+
out_stream.write("\n")
|
|
102
|
+
out_stream.flush()
|
|
103
|
+
finally:
|
|
104
|
+
if parsed.input_file:
|
|
105
|
+
in_stream.close()
|
|
106
|
+
if parsed.output_file:
|
|
107
|
+
out_stream.close()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
main()
|
pycg3/cohort.py
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""Cohort, Reading, and SingleWindow data structures."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Set, Optional, Dict, Any
|
|
4
|
+
from pycg3.tag import is_wordform, is_baseform
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Reading:
|
|
8
|
+
"""Represents a single reading of a cohort."""
|
|
9
|
+
|
|
10
|
+
__slots__ = (
|
|
11
|
+
'baseform',
|
|
12
|
+
'tags',
|
|
13
|
+
'_tag_set',
|
|
14
|
+
'sub_readings',
|
|
15
|
+
'deleted',
|
|
16
|
+
'changed',
|
|
17
|
+
'trace',
|
|
18
|
+
'parent_cohort',
|
|
19
|
+
'mapping_tags',
|
|
20
|
+
'id',
|
|
21
|
+
'parent_id',
|
|
22
|
+
'rel_name',
|
|
23
|
+
'_match_cache',
|
|
24
|
+
'mapped'
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def __init__(self, baseform: str = "", tags: Optional[List[str]] = None, parent_cohort: Optional['Cohort'] = None):
|
|
28
|
+
self.baseform = baseform
|
|
29
|
+
self.tags: List[str] = list(tags) if tags is not None else []
|
|
30
|
+
self._tag_set: Set[str] = set(self.tags)
|
|
31
|
+
self.sub_readings: List['Reading'] = []
|
|
32
|
+
self.deleted = False
|
|
33
|
+
self.changed = False
|
|
34
|
+
self.trace: List[str] = []
|
|
35
|
+
self.parent_cohort: Optional['Cohort'] = parent_cohort
|
|
36
|
+
self.mapping_tags: Set[str] = set()
|
|
37
|
+
self.id: Optional[int] = None
|
|
38
|
+
self.parent_id: Optional[int] = None
|
|
39
|
+
self.rel_name: Optional[str] = None
|
|
40
|
+
self._match_cache: Optional[frozenset] = None
|
|
41
|
+
self.mapped = False
|
|
42
|
+
|
|
43
|
+
# Check for dependency / relation tags
|
|
44
|
+
for t in self.tags:
|
|
45
|
+
if t.startswith('#') and '->' in t:
|
|
46
|
+
# Dependency tag: e.g. #1->2 or #1->2:rel
|
|
47
|
+
parts = t[1:].split('->')
|
|
48
|
+
try:
|
|
49
|
+
self.id = int(parts[0])
|
|
50
|
+
if ':' in parts[1]:
|
|
51
|
+
pid, rname = parts[1].split(':', 1)
|
|
52
|
+
self.parent_id = int(pid)
|
|
53
|
+
self.rel_name = rname
|
|
54
|
+
else:
|
|
55
|
+
self.parent_id = int(parts[1])
|
|
56
|
+
except ValueError:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
def add_tag(self, tag: str, mapping_prefix: str = '@'):
|
|
60
|
+
"""Add a tag to the reading if not already present."""
|
|
61
|
+
if tag not in self._tag_set:
|
|
62
|
+
self._match_cache = None
|
|
63
|
+
self.tags.append(tag)
|
|
64
|
+
self._tag_set.add(tag)
|
|
65
|
+
if tag.startswith(mapping_prefix):
|
|
66
|
+
self.mapping_tags.add(tag)
|
|
67
|
+
self.changed = True
|
|
68
|
+
|
|
69
|
+
def remove_tag(self, tag: str):
|
|
70
|
+
"""Remove a tag from the reading."""
|
|
71
|
+
if tag in self._tag_set:
|
|
72
|
+
self._match_cache = None
|
|
73
|
+
self.tags = [t for t in self.tags if t != tag]
|
|
74
|
+
self._tag_set.discard(tag)
|
|
75
|
+
self.mapping_tags.discard(tag)
|
|
76
|
+
self.changed = True
|
|
77
|
+
|
|
78
|
+
def replace_tag(self, old_tag: str, new_tags: List[str], mapping_prefix: str = '@'):
|
|
79
|
+
"""Replace old_tag with one or more new_tags in-place."""
|
|
80
|
+
if old_tag not in self._tag_set:
|
|
81
|
+
return
|
|
82
|
+
self._match_cache = None
|
|
83
|
+
new_list = []
|
|
84
|
+
replaced = False
|
|
85
|
+
for t in self.tags:
|
|
86
|
+
if t == old_tag and not replaced:
|
|
87
|
+
new_list.extend(new_tags)
|
|
88
|
+
replaced = True
|
|
89
|
+
elif t != old_tag:
|
|
90
|
+
new_list.append(t)
|
|
91
|
+
self.tags = new_list
|
|
92
|
+
self._tag_set = set(self.tags)
|
|
93
|
+
self.mapping_tags = {t for t in self.tags if t.startswith(mapping_prefix)}
|
|
94
|
+
self.changed = True
|
|
95
|
+
|
|
96
|
+
def identity(self):
|
|
97
|
+
"""What makes two readings the same: base form and tag set (order does not matter), sub-readings in order."""
|
|
98
|
+
return (self.baseform, frozenset(self.tags), tuple(sub.identity() for sub in self.sub_readings))
|
|
99
|
+
|
|
100
|
+
def has_tag(self, tag: str) -> bool:
|
|
101
|
+
"""Check if reading contains exact tag."""
|
|
102
|
+
return tag in self._tag_set or tag == self.baseform
|
|
103
|
+
|
|
104
|
+
def match_set(self) -> frozenset:
|
|
105
|
+
"""Every tag a set can match on this reading, base form and word form included (cached)."""
|
|
106
|
+
if self._match_cache is None:
|
|
107
|
+
self._match_cache = frozenset(self.all_tags())
|
|
108
|
+
return self._match_cache
|
|
109
|
+
|
|
110
|
+
def all_tags(self) -> List[str]:
|
|
111
|
+
"""Return list of all tags including baseform and wordform."""
|
|
112
|
+
res = []
|
|
113
|
+
if self.baseform:
|
|
114
|
+
res.append(self.baseform)
|
|
115
|
+
res.extend(self.tags)
|
|
116
|
+
if self.parent_cohort and self.parent_cohort.wordform:
|
|
117
|
+
res.append(self.parent_cohort.wordform)
|
|
118
|
+
if self.parent_cohort and self.parent_cohort.is_end:
|
|
119
|
+
res.append('<<<')
|
|
120
|
+
return res
|
|
121
|
+
|
|
122
|
+
def to_text(self, indent: str = "\t", show_rules: bool = False, trace: bool = False) -> str:
|
|
123
|
+
"""Format reading as VISL CG-3 text line."""
|
|
124
|
+
parts = []
|
|
125
|
+
if self.baseform:
|
|
126
|
+
parts.append(self.baseform)
|
|
127
|
+
parts.extend(self.tags)
|
|
128
|
+
line = indent + " ".join(parts)
|
|
129
|
+
if show_rules and self.changed:
|
|
130
|
+
line += " <<<"
|
|
131
|
+
if trace and self.trace:
|
|
132
|
+
line += " " + " ".join(self.trace)
|
|
133
|
+
lines = [line]
|
|
134
|
+
for sub in self.sub_readings:
|
|
135
|
+
lines.append(sub.to_text(indent=indent + "\t", show_rules=show_rules, trace=trace))
|
|
136
|
+
return "\n".join(lines)
|
|
137
|
+
|
|
138
|
+
def clone(self, parent_cohort: Optional['Cohort'] = None) -> 'Reading':
|
|
139
|
+
"""Deep copy reading for cohort operations."""
|
|
140
|
+
nr = Reading(self.baseform, list(self.tags), parent_cohort=parent_cohort)
|
|
141
|
+
nr.deleted = self.deleted
|
|
142
|
+
nr.changed = self.changed
|
|
143
|
+
nr.trace = list(self.trace)
|
|
144
|
+
nr.mapping_tags = set(self.mapping_tags)
|
|
145
|
+
nr.id = self.id
|
|
146
|
+
nr.parent_id = self.parent_id
|
|
147
|
+
nr.rel_name = self.rel_name
|
|
148
|
+
nr.mapped = self.mapped
|
|
149
|
+
nr.sub_readings = [s.clone(parent_cohort=parent_cohort) for s in self.sub_readings]
|
|
150
|
+
return nr
|
|
151
|
+
|
|
152
|
+
def __repr__(self) -> str:
|
|
153
|
+
return f"Reading({self.baseform} {' '.join(self.tags)})"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class Cohort:
|
|
157
|
+
"""Represents a cohort (a wordform with its readings and untouchable tags)."""
|
|
158
|
+
|
|
159
|
+
__slots__ = (
|
|
160
|
+
'wordform',
|
|
161
|
+
'untouchable_tags',
|
|
162
|
+
'_untouchable_set',
|
|
163
|
+
'readings',
|
|
164
|
+
'deleted_readings',
|
|
165
|
+
'text_before',
|
|
166
|
+
'line_number',
|
|
167
|
+
'global_number',
|
|
168
|
+
'parent_window',
|
|
169
|
+
'is_end',
|
|
170
|
+
'_wread'
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def __init__(
|
|
174
|
+
self,
|
|
175
|
+
wordform: str = "",
|
|
176
|
+
untouchable_tags: Optional[List[str]] = None,
|
|
177
|
+
text_before: Optional[List[str]] = None,
|
|
178
|
+
line_number: int = 0,
|
|
179
|
+
global_number: int = 0,
|
|
180
|
+
parent_window: Optional['SingleWindow'] = None
|
|
181
|
+
):
|
|
182
|
+
self.wordform = wordform
|
|
183
|
+
self.untouchable_tags: List[str] = list(untouchable_tags) if untouchable_tags is not None else []
|
|
184
|
+
self._untouchable_set: Set[str] = set(self.untouchable_tags)
|
|
185
|
+
self.readings: List[Reading] = []
|
|
186
|
+
self.deleted_readings: List[Reading] = []
|
|
187
|
+
self.text_before: List[str] = list(text_before) if text_before is not None else []
|
|
188
|
+
self.line_number = line_number
|
|
189
|
+
self.global_number = global_number
|
|
190
|
+
self.parent_window: Optional['SingleWindow'] = parent_window
|
|
191
|
+
self._wread = None
|
|
192
|
+
self.is_end = False # the last cohort of a window: its readings answer to the tag <<<
|
|
193
|
+
|
|
194
|
+
def append_reading(self, reading: Reading):
|
|
195
|
+
"""Append a reading to this cohort."""
|
|
196
|
+
reading.parent_cohort = self
|
|
197
|
+
self.readings.append(reading)
|
|
198
|
+
|
|
199
|
+
def dedupe_readings(self, with_trace: bool = False) -> bool:
|
|
200
|
+
"""Drop later readings that have the same base form and the same set of tags as an earlier one
|
|
201
|
+
(vislcg3 does this silently, on input and after a rule has changed a reading). With -t the rules
|
|
202
|
+
that hit a reading are part of what it is, so readings with different traces stay apart."""
|
|
203
|
+
seen, keep = set(), []
|
|
204
|
+
for r in self.readings:
|
|
205
|
+
key = (r.identity(), tuple(r.trace) if with_trace else ())
|
|
206
|
+
if key in seen and not r.deleted:
|
|
207
|
+
continue
|
|
208
|
+
seen.add(key)
|
|
209
|
+
keep.append(r)
|
|
210
|
+
changed = len(keep) != len(self.readings)
|
|
211
|
+
self.readings = keep
|
|
212
|
+
return changed
|
|
213
|
+
|
|
214
|
+
def active_readings(self) -> List[Reading]:
|
|
215
|
+
"""Return active (non-deleted) readings."""
|
|
216
|
+
return [r for r in self.readings if not r.deleted]
|
|
217
|
+
|
|
218
|
+
def select_reading(self, reading: Reading, rule_name: str = ""):
|
|
219
|
+
"""Select this reading, removing all other non-matching readings."""
|
|
220
|
+
for r in self.readings:
|
|
221
|
+
if r is not reading and not r.deleted:
|
|
222
|
+
r.deleted = True
|
|
223
|
+
r.changed = True
|
|
224
|
+
if rule_name:
|
|
225
|
+
r.trace.append(rule_name)
|
|
226
|
+
self.deleted_readings.append(r)
|
|
227
|
+
self.readings = [reading]
|
|
228
|
+
reading.changed = True
|
|
229
|
+
if rule_name:
|
|
230
|
+
reading.trace.append(rule_name)
|
|
231
|
+
|
|
232
|
+
def remove_reading(self, reading: Reading, rule_name: str = ""):
|
|
233
|
+
"""Remove a reading."""
|
|
234
|
+
if not reading.deleted:
|
|
235
|
+
reading.deleted = True
|
|
236
|
+
reading.changed = True
|
|
237
|
+
if rule_name:
|
|
238
|
+
reading.trace.append(rule_name)
|
|
239
|
+
self.deleted_readings.append(reading)
|
|
240
|
+
self.readings = [r for r in self.readings if r is not reading]
|
|
241
|
+
|
|
242
|
+
def to_text(self, show_rules: bool = False, trace: bool = False, pipe_deleted: bool = False) -> str:
|
|
243
|
+
"""Format cohort into VISL CG-3 stream format."""
|
|
244
|
+
out = []
|
|
245
|
+
if self.text_before:
|
|
246
|
+
out.extend(self.text_before)
|
|
247
|
+
header = self.wordform
|
|
248
|
+
if self.untouchable_tags:
|
|
249
|
+
header += " " + " ".join(self.untouchable_tags)
|
|
250
|
+
out.append(header)
|
|
251
|
+
for r in self.readings:
|
|
252
|
+
out.append(r.to_text(show_rules=show_rules, trace=trace))
|
|
253
|
+
if pipe_deleted:
|
|
254
|
+
for r in self.deleted_readings:
|
|
255
|
+
# vislcg3 prints a deleted reading, and each of its sub-reading lines, with a leading ';'
|
|
256
|
+
out.extend(";" + ln for ln in r.to_text(show_rules=show_rules, trace=trace).split("\n"))
|
|
257
|
+
return "\n".join(out)
|
|
258
|
+
|
|
259
|
+
def clone(self, parent_window: Optional['SingleWindow'] = None) -> 'Cohort':
|
|
260
|
+
"""Deep copy cohort."""
|
|
261
|
+
nc = Cohort(
|
|
262
|
+
self.wordform,
|
|
263
|
+
list(self.untouchable_tags),
|
|
264
|
+
list(self.text_before),
|
|
265
|
+
self.line_number,
|
|
266
|
+
self.global_number,
|
|
267
|
+
parent_window=parent_window
|
|
268
|
+
)
|
|
269
|
+
for r in self.readings:
|
|
270
|
+
nc.append_reading(r.clone(parent_cohort=nc))
|
|
271
|
+
nc.deleted_readings = [r.clone(parent_cohort=nc) for r in self.deleted_readings]
|
|
272
|
+
return nc
|
|
273
|
+
|
|
274
|
+
def __repr__(self) -> str:
|
|
275
|
+
return f"Cohort({self.wordform}, {len(self.readings)} readings)"
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class SingleWindow:
|
|
279
|
+
"""Represents a window (sentence / clause) of cohorts."""
|
|
280
|
+
|
|
281
|
+
__slots__ = ('cohorts', 'text_after', 'variables', 'flush_after', 'begin')
|
|
282
|
+
|
|
283
|
+
def __init__(self, cohorts: Optional[List[Cohort]] = None):
|
|
284
|
+
self.cohorts: List[Cohort] = list(cohorts) if cohorts is not None else []
|
|
285
|
+
self.text_after: List[str] = []
|
|
286
|
+
self.variables: Dict[str, str] = {}
|
|
287
|
+
self.flush_after = False
|
|
288
|
+
# CG-3 puts a begin cohort (tag >>>) before every window that only contextual tests can reach
|
|
289
|
+
self.begin = self._edge('>>>')
|
|
290
|
+
for c in self.cohorts:
|
|
291
|
+
c.parent_window = self
|
|
292
|
+
|
|
293
|
+
def _edge(self, tag: str) -> Cohort:
|
|
294
|
+
c = Cohort(wordform="", parent_window=self)
|
|
295
|
+
c.append_reading(Reading(baseform="", tags=[tag]))
|
|
296
|
+
return c
|
|
297
|
+
|
|
298
|
+
def mark_end(self):
|
|
299
|
+
"""The last cohort of the window gets the tag <<< (for matching only), as vislcg3 does."""
|
|
300
|
+
for c in self.cohorts:
|
|
301
|
+
if c.is_end:
|
|
302
|
+
c.is_end = False
|
|
303
|
+
for r in c.readings:
|
|
304
|
+
r._match_cache = None
|
|
305
|
+
if self.cohorts:
|
|
306
|
+
last = self.cohorts[-1]
|
|
307
|
+
last.is_end = True
|
|
308
|
+
for r in last.readings:
|
|
309
|
+
r._match_cache = None
|
|
310
|
+
|
|
311
|
+
def virtual(self, vi: int) -> Cohort:
|
|
312
|
+
"""Cohort at virtual index vi: 0 is the begin cohort, 1..n the real cohorts."""
|
|
313
|
+
return self.begin if vi == 0 else self.cohorts[vi - 1]
|
|
314
|
+
|
|
315
|
+
def append_cohort(self, cohort: Cohort):
|
|
316
|
+
cohort.parent_window = self
|
|
317
|
+
self.cohorts.append(cohort)
|
|
318
|
+
|
|
319
|
+
def to_text(self, show_rules: bool = False, trace: bool = False, pipe_deleted: bool = False) -> str:
|
|
320
|
+
lines = []
|
|
321
|
+
for c in self.cohorts:
|
|
322
|
+
lines.append(c.to_text(show_rules=show_rules, trace=trace, pipe_deleted=pipe_deleted))
|
|
323
|
+
if self.text_after:
|
|
324
|
+
lines.extend(self.text_after)
|
|
325
|
+
return "\n".join(lines)
|