comber 1.0.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.
- comber/__init__.py +8 -0
- comber/combinator.py +352 -0
- comber/extras.py +121 -0
- comber/parser.py +335 -0
- comber/py.typed +0 -0
- comber-1.0.0.dist-info/LICENSE.txt +504 -0
- comber-1.0.0.dist-info/METADATA +910 -0
- comber-1.0.0.dist-info/RECORD +10 -0
- comber-1.0.0.dist-info/WHEEL +5 -0
- comber-1.0.0.dist-info/top_level.txt +1 -0
comber/parser.py
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base parser definitions.
|
|
3
|
+
"""
|
|
4
|
+
from typing import Optional, Callable, Any
|
|
5
|
+
from abc import abstractmethod
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Expect:
|
|
9
|
+
""" Internal state of expect calculation """
|
|
10
|
+
def __init__(self) -> None:
|
|
11
|
+
self._recurseStack:list[list] = [[]]
|
|
12
|
+
|
|
13
|
+
def pushParser(self, parser:Any) -> None:
|
|
14
|
+
"""
|
|
15
|
+
Push the current parser.
|
|
16
|
+
"""
|
|
17
|
+
self._recurseStack[-1].append(parser)
|
|
18
|
+
|
|
19
|
+
def popParser(self) -> None:
|
|
20
|
+
"""
|
|
21
|
+
Pop the last parser.
|
|
22
|
+
"""
|
|
23
|
+
self._recurseStack[-1].pop()
|
|
24
|
+
|
|
25
|
+
def inRecursion(self, parser:Any) -> bool:
|
|
26
|
+
"""
|
|
27
|
+
See if we're already trying to parse a given parser.
|
|
28
|
+
"""
|
|
29
|
+
return not parser.recurse and parser in self._recurseStack[-1]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class State:
|
|
33
|
+
"""
|
|
34
|
+
Internal parse state.
|
|
35
|
+
"""
|
|
36
|
+
def __init__(self,
|
|
37
|
+
text:str,
|
|
38
|
+
whitespace:str|None,
|
|
39
|
+
|
|
40
|
+
line:int = 1,
|
|
41
|
+
char:int = 1,
|
|
42
|
+
tree:list[list]|None = None,
|
|
43
|
+
recurseStack:list[set[int]]|None = None,
|
|
44
|
+
) -> None:
|
|
45
|
+
self.text = text
|
|
46
|
+
""" Unparsed input """
|
|
47
|
+
self.line = line
|
|
48
|
+
""" Current line offset into the input text (starts at 1) """
|
|
49
|
+
self.char = char
|
|
50
|
+
""" Current character offset into the current line (starts at 1) """
|
|
51
|
+
self.eof = False
|
|
52
|
+
""" True if we have it the end of the text """
|
|
53
|
+
self._tree:list[list] = tree if tree is not None else [[]]
|
|
54
|
+
self._recurseStack:list[set[int]] = recurseStack if recurseStack is not None else [set()]
|
|
55
|
+
self._whitespace = whitespace
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def tree(self) -> list:
|
|
59
|
+
"""
|
|
60
|
+
The current parse tree
|
|
61
|
+
"""
|
|
62
|
+
return self._tree[0]
|
|
63
|
+
|
|
64
|
+
def eatWhite(self) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Consume the leading whitespace, if whitespace was defined.
|
|
67
|
+
"""
|
|
68
|
+
if self._whitespace:
|
|
69
|
+
text = self.text.lstrip(self._whitespace)
|
|
70
|
+
eaten = self.text[0:len(self.text) - len(text)]
|
|
71
|
+
lines = eaten.count('\n')
|
|
72
|
+
self.text = text
|
|
73
|
+
self.line += lines
|
|
74
|
+
self.char = \
|
|
75
|
+
len(eaten) - (eaten.rfind('\n') + 1) \
|
|
76
|
+
if lines \
|
|
77
|
+
else self.char + len(eaten)
|
|
78
|
+
self.eof = not self.text
|
|
79
|
+
|
|
80
|
+
def consume(self, length:int) -> None:
|
|
81
|
+
"""
|
|
82
|
+
Consume a number of characters in the stream.
|
|
83
|
+
"""
|
|
84
|
+
eaten = text = self.text[0:length]
|
|
85
|
+
self.text = self.text[length:]
|
|
86
|
+
|
|
87
|
+
if self._whitespace:
|
|
88
|
+
stripped = self.text.lstrip(self._whitespace)
|
|
89
|
+
eaten = text + self.text[0:len(self.text) - len(stripped)]
|
|
90
|
+
self.text = stripped
|
|
91
|
+
|
|
92
|
+
lines = eaten.count('\n')
|
|
93
|
+
|
|
94
|
+
self.line += lines
|
|
95
|
+
self.char = \
|
|
96
|
+
length - (eaten.rfind('\n') + 1) \
|
|
97
|
+
if lines \
|
|
98
|
+
else self.char + length
|
|
99
|
+
|
|
100
|
+
self._tree[-1].append(text)
|
|
101
|
+
self.eof = not self.text
|
|
102
|
+
|
|
103
|
+
def pushLeaf(self, value:Any) -> None:
|
|
104
|
+
"""
|
|
105
|
+
Push a value onto the current stack branch.
|
|
106
|
+
"""
|
|
107
|
+
self._tree[-1].append(value)
|
|
108
|
+
|
|
109
|
+
def pushBranch(self) -> None:
|
|
110
|
+
"""
|
|
111
|
+
Push a new stack branch.
|
|
112
|
+
"""
|
|
113
|
+
self._tree.append([])
|
|
114
|
+
|
|
115
|
+
def popBranch(self) -> list:
|
|
116
|
+
"""
|
|
117
|
+
Pop a stack branch off the tree stack
|
|
118
|
+
"""
|
|
119
|
+
return self._tree.pop()
|
|
120
|
+
|
|
121
|
+
def pushState(self) -> 'State':
|
|
122
|
+
"""
|
|
123
|
+
Extend this state.
|
|
124
|
+
"""
|
|
125
|
+
stack = list(self._recurseStack)
|
|
126
|
+
stack[-1] = set(stack[-1])
|
|
127
|
+
tree = list(self._tree)
|
|
128
|
+
tree.append([])
|
|
129
|
+
state = State(
|
|
130
|
+
self.text,
|
|
131
|
+
self._whitespace,
|
|
132
|
+
self.line,
|
|
133
|
+
self.char,
|
|
134
|
+
tree,
|
|
135
|
+
stack
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return state
|
|
139
|
+
|
|
140
|
+
def popState(self) -> 'State':
|
|
141
|
+
"""
|
|
142
|
+
Collapse an extended state.
|
|
143
|
+
"""
|
|
144
|
+
popped = self.popBranch()
|
|
145
|
+
self._tree[-1] += popped
|
|
146
|
+
return self
|
|
147
|
+
|
|
148
|
+
def pushParser(self, parser:'Parser') -> None:
|
|
149
|
+
"""
|
|
150
|
+
Push the current parser.
|
|
151
|
+
"""
|
|
152
|
+
self._recurseStack[-1].add(id(parser))
|
|
153
|
+
|
|
154
|
+
def popParser(self, parser:'Parser') -> None:
|
|
155
|
+
"""
|
|
156
|
+
Pop the last parser.
|
|
157
|
+
"""
|
|
158
|
+
self._recurseStack[-1].remove(id(parser))
|
|
159
|
+
|
|
160
|
+
def shiftParser(self) -> None:
|
|
161
|
+
"""
|
|
162
|
+
Create a new parser stack because we're looking for the element in a sequence
|
|
163
|
+
"""
|
|
164
|
+
self._recurseStack.append(set())
|
|
165
|
+
|
|
166
|
+
def unshiftParser(self) -> None:
|
|
167
|
+
"""
|
|
168
|
+
Toss out the current parser stack.
|
|
169
|
+
"""
|
|
170
|
+
self._recurseStack.pop()
|
|
171
|
+
|
|
172
|
+
def inRecursion(self, parser:Any) -> bool:
|
|
173
|
+
"""
|
|
174
|
+
See if we're already trying to parse a given parser.
|
|
175
|
+
"""
|
|
176
|
+
return not parser.recurse and id(parser) in self._recurseStack[-1]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class ParseError(Exception):
|
|
180
|
+
"""
|
|
181
|
+
When a string cannot be parsed, this exception is thrown.
|
|
182
|
+
"""
|
|
183
|
+
def __init__(self, state:State, parser:'Parser') -> None:
|
|
184
|
+
super().__init__('Unexpected text')
|
|
185
|
+
self.line = state.line
|
|
186
|
+
""" The input line the error occurred at. """
|
|
187
|
+
self.char = state.char
|
|
188
|
+
""" The character offset into the line the error occurred at. """
|
|
189
|
+
self.text = state.text
|
|
190
|
+
""" The unparsed input text. """
|
|
191
|
+
self.parser = parser
|
|
192
|
+
""" The parser that failed. """
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def expected(self) -> list[str]:
|
|
196
|
+
""" The possible next tokens """
|
|
197
|
+
return list(set(self.parser.expectCore()))
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def message(self) -> str:
|
|
201
|
+
""" A lazy version of the exception message. """
|
|
202
|
+
return str(self.line)+":"+str(self.char)+": " \
|
|
203
|
+
+'Unexpected text: ' \
|
|
204
|
+
+self.text[0:10] \
|
|
205
|
+
+'. Expected one of: ' \
|
|
206
|
+
+', '.join(self.expected)
|
|
207
|
+
|
|
208
|
+
def __str__(self) -> str:
|
|
209
|
+
return self.message
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class EndOfInputError(ParseError):
|
|
213
|
+
"""
|
|
214
|
+
When we reach the end of input before completing a full parse.
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def message(self) -> str:
|
|
219
|
+
return 'Unexpected end of input. Expected one of: ' \
|
|
220
|
+
+', '.join(self.expected)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
Emitter = Callable[[list[Any]], Any]
|
|
224
|
+
"""
|
|
225
|
+
Type of emitter functions.
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class Parser:
|
|
230
|
+
"""
|
|
231
|
+
Base parser.
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
recurse = False
|
|
235
|
+
""" If True, the parser class is allowed to recurse without any checks. """
|
|
236
|
+
compound = False
|
|
237
|
+
""" If True, the parser class is a compound class, and so may fail partway through. """
|
|
238
|
+
|
|
239
|
+
def __init__(self) -> None:
|
|
240
|
+
self.name:Optional[str] = None
|
|
241
|
+
""" Friendly name of this sub-parser """
|
|
242
|
+
self.emit:Optional[Emitter] = None
|
|
243
|
+
""" Internalizer function; if not provided, the result will be the parsed string """
|
|
244
|
+
self.whitespace:str|None = ' \t\n'
|
|
245
|
+
""" Default whitespace """
|
|
246
|
+
|
|
247
|
+
def __call__(self, text:str, whitespace:str|None=None) -> State:
|
|
248
|
+
"""
|
|
249
|
+
Parse a string.
|
|
250
|
+
"""
|
|
251
|
+
state = State(text, whitespace if whitespace is not None else self.whitespace)
|
|
252
|
+
state.eatWhite()
|
|
253
|
+
return self.parseCore(state)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def parseCore(self, state:State) -> State:
|
|
257
|
+
"""
|
|
258
|
+
Internal parse function, for calling by subparsers.
|
|
259
|
+
"""
|
|
260
|
+
if self.emit:
|
|
261
|
+
state.pushBranch()
|
|
262
|
+
|
|
263
|
+
if not self.recurse:
|
|
264
|
+
state.pushParser(self)
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
newState = self.recognize(state)
|
|
268
|
+
except ParseError:
|
|
269
|
+
# Nothing that Can recurse will actually throw
|
|
270
|
+
#if not self.recurse:
|
|
271
|
+
state.popParser(self)
|
|
272
|
+
raise
|
|
273
|
+
|
|
274
|
+
if newState is None:
|
|
275
|
+
if state.eof:
|
|
276
|
+
raise EndOfInputError(state, self)
|
|
277
|
+
else:
|
|
278
|
+
raise ParseError(state, self)
|
|
279
|
+
|
|
280
|
+
if not self.recurse:
|
|
281
|
+
newState.popParser(self)
|
|
282
|
+
|
|
283
|
+
if self.emit is not None:
|
|
284
|
+
value = self.emit(newState.popBranch())
|
|
285
|
+
newState.pushLeaf(value)
|
|
286
|
+
|
|
287
|
+
return newState
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def expectCore(self, state:Expect|None = None) -> list[str]:
|
|
291
|
+
"""
|
|
292
|
+
If this parser has a name, then a list containing only its name, otherwise the value returned by expect
|
|
293
|
+
"""
|
|
294
|
+
state = state or Expect()
|
|
295
|
+
if state.inRecursion(self):
|
|
296
|
+
expecting = []
|
|
297
|
+
else:
|
|
298
|
+
state.pushParser(self)
|
|
299
|
+
expecting = [self.name] if self.name else self.expect(state)
|
|
300
|
+
state.popParser()
|
|
301
|
+
return expecting
|
|
302
|
+
|
|
303
|
+
def __repr__(self) -> str:
|
|
304
|
+
"""
|
|
305
|
+
A string representation of the combinator
|
|
306
|
+
"""
|
|
307
|
+
if self.name:
|
|
308
|
+
return f"@{self.name}"
|
|
309
|
+
return self.repr()
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def analyze(self) -> None:
|
|
313
|
+
"""
|
|
314
|
+
Analyze the grammar to improve performance.
|
|
315
|
+
"""
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@abstractmethod
|
|
319
|
+
def expect(self, state:Expect) -> list[str]:
|
|
320
|
+
"""
|
|
321
|
+
Strings representing what's expected by this parser.
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
@abstractmethod
|
|
326
|
+
def recognize(self, state:State) -> Optional[State]:
|
|
327
|
+
"""
|
|
328
|
+
Core parse function of a parser.
|
|
329
|
+
"""
|
|
330
|
+
|
|
331
|
+
@abstractmethod
|
|
332
|
+
def repr(self) -> str:
|
|
333
|
+
"""
|
|
334
|
+
The specific combinator string represenation.
|
|
335
|
+
"""
|
comber/py.typed
ADDED
|
File without changes
|