ymlplus 0.1.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.
- yamlplus/__init__.py +16 -0
- yamlplus/__main__.py +8 -0
- yamlplus/_identifiers.py +72 -0
- yamlplus/_io.py +262 -0
- yamlplus/_numbers.py +40 -0
- yamlplus/_pyyaml.py +53 -0
- yamlplus/_syntax.py +511 -0
- yamlplus/_validation.py +197 -0
- yamlplus/_xid.py +2151 -0
- yamlplus/_yaml_surface.py +346 -0
- yamlplus/api.py +130 -0
- yamlplus/errors.py +14 -0
- ymlplus-0.1.0.dist-info/METADATA +92 -0
- ymlplus-0.1.0.dist-info/RECORD +18 -0
- ymlplus-0.1.0.dist-info/WHEEL +5 -0
- ymlplus-0.1.0.dist-info/entry_points.txt +2 -0
- ymlplus-0.1.0.dist-info/licenses/LICENSE +674 -0
- ymlplus-0.1.0.dist-info/top_level.txt +1 -0
yamlplus/_syntax.py
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
"""Literal-profile syntax recognition, without constructing a document value."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import base64
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import math
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
from .errors import ValidationError
|
|
10
|
+
from ._identifiers import (KEYWORDS, PREFIXES, continuation, fold, key_error,
|
|
11
|
+
reserved_fold, start)
|
|
12
|
+
from ._numbers import WARN_DIGITS, int_from_decimal
|
|
13
|
+
|
|
14
|
+
_C32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
|
15
|
+
_C32_VALUES = {ch: index for index, ch in enumerate(_C32)} | {"O": 0, "I": 1, "L": 1}
|
|
16
|
+
_RADIXES = {"b": (2, "01"), "q": (4, "0-3"), "o": (8, "0-7"),
|
|
17
|
+
"x": (16, "0-9a-fA-F"), "z": (32, "0-9A-HJ-NP-TV-Za-hj-np-tv-zOoIiLl")}
|
|
18
|
+
_STREAMS = {
|
|
19
|
+
"bin": (1, "01", False, {}, ""),
|
|
20
|
+
"dna": (2, "TCAG", True, {}, ""),
|
|
21
|
+
"oct": (3, "01234567", False, {}, ""),
|
|
22
|
+
"hex": (4, "0123456789ABCDEF", True, {}, ""),
|
|
23
|
+
"c32": (5, _C32, True, {"O": "0", "I": "1", "L": "1"}, "-"),
|
|
24
|
+
}
|
|
25
|
+
_ESCAPES = {"\n": "", "\\": "\\", "'": "'", '"': '"', "a": "\a", "b": "\b",
|
|
26
|
+
"f": "\f", "n": "\n", "r": "\r", "t": "\t", "v": "\v"}
|
|
27
|
+
_PUNCTUATION = {":": "COLON", "=": "EQUAL", ",": "COMMA",
|
|
28
|
+
"[": "LBRACKET", "]": "RBRACKET", "{": "LBRACE", "}": "RBRACE"}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _digit(character):
|
|
32
|
+
return bool(character) and "0" <= character <= "9"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class Token:
|
|
37
|
+
kind: str
|
|
38
|
+
value: object
|
|
39
|
+
line: int
|
|
40
|
+
col: int
|
|
41
|
+
prefix: str = ""
|
|
42
|
+
lexeme: str = ""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class _Lexer:
|
|
46
|
+
def __init__(self, text, max_digits, source):
|
|
47
|
+
self.text, self.max_digits, self.source = text, max_digits, source
|
|
48
|
+
self.pos, self.line, self.col = 0, 1, 0
|
|
49
|
+
self.indents, self.brackets, self.based = [0], [], False
|
|
50
|
+
self.tokens, self.warnings = [], []
|
|
51
|
+
|
|
52
|
+
def peek(self, offset=0):
|
|
53
|
+
return self.text[self.pos + offset:self.pos + offset + 1]
|
|
54
|
+
|
|
55
|
+
def advance(self, count=1):
|
|
56
|
+
text = self.text[self.pos:self.pos + count]
|
|
57
|
+
for ch in text:
|
|
58
|
+
if ch == "\n":
|
|
59
|
+
self.line, self.col = self.line + 1, 0
|
|
60
|
+
else:
|
|
61
|
+
self.col += 1
|
|
62
|
+
self.pos += count
|
|
63
|
+
return text
|
|
64
|
+
|
|
65
|
+
def fail(self, message, line=None, col=None):
|
|
66
|
+
raise ValidationError(message, self.line if line is None else line,
|
|
67
|
+
self.col if col is None else col, self.source)
|
|
68
|
+
|
|
69
|
+
def emit(self, kind, value=None, *, line=None, col=None, prefix="", lexeme=""):
|
|
70
|
+
self.tokens.append(Token(kind, value, self.line if line is None else line,
|
|
71
|
+
self.col if col is None else col, prefix, lexeme))
|
|
72
|
+
|
|
73
|
+
def tokenize(self):
|
|
74
|
+
line_start = True
|
|
75
|
+
while self.pos < len(self.text):
|
|
76
|
+
if line_start and not self.brackets:
|
|
77
|
+
if self.line_start():
|
|
78
|
+
continue
|
|
79
|
+
line_start = False
|
|
80
|
+
ch = self.peek()
|
|
81
|
+
if ch == "\n":
|
|
82
|
+
if not self.brackets:
|
|
83
|
+
self.emit("NEWLINE")
|
|
84
|
+
line_start = True
|
|
85
|
+
self.advance()
|
|
86
|
+
elif ch in (" ", "\t"):
|
|
87
|
+
self.advance()
|
|
88
|
+
elif ch == "#":
|
|
89
|
+
while self.peek() and self.peek() != "\n":
|
|
90
|
+
self.advance()
|
|
91
|
+
else:
|
|
92
|
+
self.token()
|
|
93
|
+
if self.brackets:
|
|
94
|
+
self.fail("unclosed bracket at end of document")
|
|
95
|
+
if self.tokens and self.tokens[-1].kind not in ("NEWLINE", "DEDENT"):
|
|
96
|
+
self.emit("NEWLINE")
|
|
97
|
+
while len(self.indents) > 1:
|
|
98
|
+
self.indents.pop()
|
|
99
|
+
self.emit("DEDENT")
|
|
100
|
+
self.emit("EOF")
|
|
101
|
+
for index, token in enumerate(self.tokens[:-1]):
|
|
102
|
+
if token.kind == "STRING" and self.tokens[index + 1].kind == "COLON":
|
|
103
|
+
token.kind = "QKEY"
|
|
104
|
+
return self.tokens
|
|
105
|
+
|
|
106
|
+
def line_start(self):
|
|
107
|
+
width = 0
|
|
108
|
+
while self.peek() in (" ", "\t"):
|
|
109
|
+
if self.peek() == "\t":
|
|
110
|
+
self.fail("indentation must use spaces, not tabs")
|
|
111
|
+
self.advance()
|
|
112
|
+
width += 1
|
|
113
|
+
if self.peek() in ("", "\n"):
|
|
114
|
+
if self.peek():
|
|
115
|
+
self.advance()
|
|
116
|
+
return True
|
|
117
|
+
if self.peek() == "#":
|
|
118
|
+
while self.peek() and self.peek() != "\n":
|
|
119
|
+
self.advance()
|
|
120
|
+
if self.peek():
|
|
121
|
+
self.advance()
|
|
122
|
+
return True
|
|
123
|
+
if not self.based:
|
|
124
|
+
self.indents, self.based = [width], True
|
|
125
|
+
if width < self.indents[0]:
|
|
126
|
+
self.fail("indentation falls left of the document's base column")
|
|
127
|
+
if width > self.indents[-1]:
|
|
128
|
+
self.indents.append(width)
|
|
129
|
+
self.emit("INDENT")
|
|
130
|
+
else:
|
|
131
|
+
while width < self.indents[-1]:
|
|
132
|
+
self.indents.pop()
|
|
133
|
+
self.emit("DEDENT")
|
|
134
|
+
if width != self.indents[-1]:
|
|
135
|
+
self.fail("indentation matches no open block")
|
|
136
|
+
while self.peek() == "-" and self.peek(1) in ("", " ", "\n"):
|
|
137
|
+
column = self.col
|
|
138
|
+
self.emit("DASH")
|
|
139
|
+
self.advance()
|
|
140
|
+
if self.peek() == " ":
|
|
141
|
+
self.advance()
|
|
142
|
+
self.indents.append(column + 2)
|
|
143
|
+
self.emit("INDENT")
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
def token(self):
|
|
147
|
+
ch, line, col = self.peek(), self.line, self.col
|
|
148
|
+
if ch in _PUNCTUATION:
|
|
149
|
+
if ch in "[{":
|
|
150
|
+
self.brackets.append(ch)
|
|
151
|
+
elif ch in "]}":
|
|
152
|
+
if not self.brackets or self.brackets[-1] != {"}": "{", "]": "["}[ch]:
|
|
153
|
+
self.fail("unmatched closing bracket")
|
|
154
|
+
self.brackets.pop()
|
|
155
|
+
self.emit(_PUNCTUATION[ch], ch, lexeme=ch)
|
|
156
|
+
self.advance()
|
|
157
|
+
elif ch in ("'", '"'):
|
|
158
|
+
self.string("", line, col)
|
|
159
|
+
elif (_digit(ch) or ch in ("+", "-", ".") and _digit(self.peek(1))
|
|
160
|
+
or ch in ("+", "-") and self.peek(1) == "." and _digit(self.peek(2))):
|
|
161
|
+
self.number(line, col)
|
|
162
|
+
elif start(ch):
|
|
163
|
+
begin = self.pos
|
|
164
|
+
self.advance()
|
|
165
|
+
while continuation(self.peek()):
|
|
166
|
+
self.advance()
|
|
167
|
+
name = self.text[begin:self.pos]
|
|
168
|
+
reserved = reserved_fold(name)
|
|
169
|
+
if reserved is not None:
|
|
170
|
+
self.fail(f"reserved spelling {reserved!r} must be written exactly", line, col)
|
|
171
|
+
if self.peek() in ("'", '"'):
|
|
172
|
+
if name not in PREFIXES:
|
|
173
|
+
self.fail(f"unsupported literal prefix {name!r}", line, col)
|
|
174
|
+
self.string(name, line, col)
|
|
175
|
+
elif name in KEYWORDS:
|
|
176
|
+
self.emit("KEYWORD", KEYWORDS[name], line=line, col=col, lexeme=name)
|
|
177
|
+
else:
|
|
178
|
+
self.emit("NAME", fold(name), line=line, col=col, lexeme=name)
|
|
179
|
+
else:
|
|
180
|
+
self.fail(f"unexpected character {ch!r}; only literal profile syntax is allowed")
|
|
181
|
+
|
|
182
|
+
def number(self, line, col):
|
|
183
|
+
begin = self.pos
|
|
184
|
+
if self.peek() in ("+", "-"):
|
|
185
|
+
self.advance()
|
|
186
|
+
radix = self.peek() == "0" and self.peek(1).lower() in _RADIXES
|
|
187
|
+
floating = False
|
|
188
|
+
if radix:
|
|
189
|
+
self.advance(2)
|
|
190
|
+
while self.peek() and (self.peek().isascii() and self.peek().isalnum() or self.peek() == "_"):
|
|
191
|
+
self.advance()
|
|
192
|
+
else:
|
|
193
|
+
while _digit(self.peek()) or self.peek() == "_":
|
|
194
|
+
self.advance()
|
|
195
|
+
if self.peek() == ".":
|
|
196
|
+
floating = True
|
|
197
|
+
self.advance()
|
|
198
|
+
while _digit(self.peek()) or self.peek() == "_":
|
|
199
|
+
self.advance()
|
|
200
|
+
if self.peek() in ("e", "E") and (_digit(self.peek(1)) or
|
|
201
|
+
self.peek(1) in ("+", "-") and _digit(self.peek(2))):
|
|
202
|
+
floating = True
|
|
203
|
+
self.advance()
|
|
204
|
+
if self.peek() in ("+", "-"):
|
|
205
|
+
self.advance()
|
|
206
|
+
while _digit(self.peek()) or self.peek() == "_":
|
|
207
|
+
self.advance()
|
|
208
|
+
if continuation(self.peek()) or self.peek().isalnum():
|
|
209
|
+
self.fail("a number cannot be followed by name characters", line, col)
|
|
210
|
+
spelling = self.text[begin:self.pos]
|
|
211
|
+
sign = spelling[:1] if spelling[:1] in ("+", "-") else ""
|
|
212
|
+
body = spelling[len(sign):]
|
|
213
|
+
if radix:
|
|
214
|
+
base, digits = _RADIXES[body[1].lower()]
|
|
215
|
+
rest = body[2:]
|
|
216
|
+
if not re.fullmatch(rf"_?[{digits}]+(?:_[{digits}]+)*", rest):
|
|
217
|
+
self.fail("malformed radix integer", line, col)
|
|
218
|
+
rest = rest.replace("_", "")
|
|
219
|
+
if base == 32:
|
|
220
|
+
value = 0
|
|
221
|
+
for ch in rest.upper():
|
|
222
|
+
value = value * 32 + _C32_VALUES[ch]
|
|
223
|
+
else:
|
|
224
|
+
value = int(rest, base)
|
|
225
|
+
elif not floating:
|
|
226
|
+
if not re.fullmatch(r"0(?:_?0)*|[1-9](?:_?[0-9])*", body):
|
|
227
|
+
self.fail("malformed decimal integer", line, col)
|
|
228
|
+
digits = body.replace("_", "")
|
|
229
|
+
if len(digits) > self.max_digits:
|
|
230
|
+
self.fail("decimal integer exceeds max_digits", line, col)
|
|
231
|
+
if len(digits) > WARN_DIGITS:
|
|
232
|
+
where = f"{self.source}:{line}:{col}" if self.source else f"line {line}, column {col}"
|
|
233
|
+
self.warnings.append(f"warning: {where}: integer has {len(digits)} decimal digits; conversion is costly")
|
|
234
|
+
value = int_from_decimal(digits)
|
|
235
|
+
else:
|
|
236
|
+
try:
|
|
237
|
+
value = ast.literal_eval(body)
|
|
238
|
+
except (SyntaxError, ValueError):
|
|
239
|
+
self.fail("malformed floating-point literal", line, col)
|
|
240
|
+
if not isinstance(value, float) or not math.isfinite(value):
|
|
241
|
+
self.fail("a float must be finite", line, col)
|
|
242
|
+
if sign == "-":
|
|
243
|
+
value = -value
|
|
244
|
+
self.emit("FLOAT" if floating else "INT", value, line=line, col=col, lexeme=spelling)
|
|
245
|
+
|
|
246
|
+
def string(self, prefix, line, col):
|
|
247
|
+
begin = self.pos - len(prefix)
|
|
248
|
+
quote = self.peek()
|
|
249
|
+
triple = self.peek(1) == quote and self.peek(2) == quote
|
|
250
|
+
self.advance(3 if triple else 1)
|
|
251
|
+
body, positions = self.string_body(quote, triple, line, col)
|
|
252
|
+
is_bytes = prefix not in ("", "r")
|
|
253
|
+
if prefix == "b64":
|
|
254
|
+
for index, ch in enumerate(body):
|
|
255
|
+
if ch not in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/= \t\n":
|
|
256
|
+
self.fail("invalid base64 symbol", *positions[index])
|
|
257
|
+
compact = "".join(ch for ch in body if ch not in " \t\n")
|
|
258
|
+
if len(compact) % 4:
|
|
259
|
+
self.fail("base64 requires padding to a multiple of four symbols", line, col)
|
|
260
|
+
try:
|
|
261
|
+
value = base64.b64decode(compact, validate=True)
|
|
262
|
+
except ValueError:
|
|
263
|
+
self.fail("invalid base64 payload", line, col)
|
|
264
|
+
if base64.b64encode(value).decode("ascii") != compact:
|
|
265
|
+
self.fail("non-canonical base64 padding or spare bits", line, col)
|
|
266
|
+
elif prefix in _STREAMS:
|
|
267
|
+
value = self.stream(prefix, body, positions, line, col)
|
|
268
|
+
elif prefix in ("r", "rb"):
|
|
269
|
+
if is_bytes and any(ord(ch) > 127 for ch in body):
|
|
270
|
+
self.fail("raw bytes require ASCII source characters", line, col)
|
|
271
|
+
value = body.encode("ascii") if is_bytes else body
|
|
272
|
+
else:
|
|
273
|
+
value = self.escapes(body, positions, is_bytes, line, col)
|
|
274
|
+
self.emit("BYTES" if is_bytes else "STRING", value, line=line, col=col,
|
|
275
|
+
prefix=prefix, lexeme=self.text[begin:self.pos])
|
|
276
|
+
|
|
277
|
+
def string_body(self, quote, triple, line, col):
|
|
278
|
+
out, positions = [], []
|
|
279
|
+
closer, anchor, pristine = quote * (3 if triple else 1), None, True
|
|
280
|
+
while True:
|
|
281
|
+
if not self.peek():
|
|
282
|
+
self.fail("unterminated literal", line, col)
|
|
283
|
+
if self.text.startswith(closer, self.pos):
|
|
284
|
+
if triple and anchor is not None and self.col < anchor:
|
|
285
|
+
self.fail("closing quote outdents past the literal's indentation")
|
|
286
|
+
self.advance(len(closer))
|
|
287
|
+
return "".join(out), positions
|
|
288
|
+
if self.peek() == "\\" and self.peek(1) not in ("", "\n"):
|
|
289
|
+
for _ in range(2):
|
|
290
|
+
positions.append((self.line, self.col))
|
|
291
|
+
out.append(self.advance())
|
|
292
|
+
pristine = False
|
|
293
|
+
elif self.peek() == "\n":
|
|
294
|
+
if not triple:
|
|
295
|
+
self.fail("a single-line literal cannot contain a raw newline", line, col)
|
|
296
|
+
here = self.line, self.col
|
|
297
|
+
self.advance()
|
|
298
|
+
if not pristine:
|
|
299
|
+
out.append("\n")
|
|
300
|
+
positions.append(here)
|
|
301
|
+
pristine = False
|
|
302
|
+
width = 0
|
|
303
|
+
while self.peek() == " ":
|
|
304
|
+
self.advance()
|
|
305
|
+
width += 1
|
|
306
|
+
if self.peek() in ("", "\n") or self.text.startswith(closer, self.pos):
|
|
307
|
+
continue
|
|
308
|
+
if anchor is None:
|
|
309
|
+
anchor = width
|
|
310
|
+
elif width < anchor:
|
|
311
|
+
self.fail("literal content outdents past its indentation anchor")
|
|
312
|
+
else:
|
|
313
|
+
out.append(" " * (width - anchor))
|
|
314
|
+
positions.extend((self.line, anchor + index) for index in range(width - anchor))
|
|
315
|
+
else:
|
|
316
|
+
positions.append((self.line, self.col))
|
|
317
|
+
out.append(self.advance())
|
|
318
|
+
pristine = False
|
|
319
|
+
|
|
320
|
+
def escapes(self, body, positions, is_bytes, line, col):
|
|
321
|
+
out, index = [], 0
|
|
322
|
+
|
|
323
|
+
def emit(point, at, literal=False):
|
|
324
|
+
if is_bytes and (point > 255 or literal and point > 127):
|
|
325
|
+
self.fail("bytes require ASCII source and escape values within 0..255", *positions[at])
|
|
326
|
+
out.append(point)
|
|
327
|
+
|
|
328
|
+
while index < len(body):
|
|
329
|
+
if body[index] != "\\":
|
|
330
|
+
emit(ord(body[index]), index, literal=True)
|
|
331
|
+
index += 1
|
|
332
|
+
continue
|
|
333
|
+
begin = index
|
|
334
|
+
index += 1
|
|
335
|
+
if index >= len(body):
|
|
336
|
+
self.fail("dangling backslash", line, col)
|
|
337
|
+
escape = body[index]
|
|
338
|
+
if escape in _ESCAPES:
|
|
339
|
+
for ch in _ESCAPES[escape]:
|
|
340
|
+
emit(ord(ch), begin)
|
|
341
|
+
index += 1
|
|
342
|
+
elif escape == "x":
|
|
343
|
+
digits = body[index + 1:index + 3]
|
|
344
|
+
if len(digits) != 2 or any(ch not in "0123456789abcdefABCDEF" for ch in digits):
|
|
345
|
+
self.fail("incomplete hexadecimal escape", *positions[begin])
|
|
346
|
+
emit(int(digits, 16), begin)
|
|
347
|
+
index += 3
|
|
348
|
+
elif escape in "01234567":
|
|
349
|
+
end = index
|
|
350
|
+
while end < len(body) and end - index < 3 and body[end] in "01234567":
|
|
351
|
+
end += 1
|
|
352
|
+
emit(int(body[index:end], 8), begin)
|
|
353
|
+
index = end
|
|
354
|
+
elif escape in ("u", "U") and not is_bytes:
|
|
355
|
+
width = 4 if escape == "u" else 8
|
|
356
|
+
digits = body[index + 1:index + width + 1]
|
|
357
|
+
if len(digits) != width or any(ch not in "0123456789abcdefABCDEF" for ch in digits):
|
|
358
|
+
self.fail("incomplete Unicode escape", *positions[begin])
|
|
359
|
+
point = int(digits, 16)
|
|
360
|
+
if point > 0x10FFFF or 0xD800 <= point <= 0xDFFF:
|
|
361
|
+
self.fail("an escape must name a Unicode scalar value", *positions[begin])
|
|
362
|
+
emit(point, begin)
|
|
363
|
+
index += width + 1
|
|
364
|
+
else:
|
|
365
|
+
self.fail(f"unknown escape \\{escape}", *positions[begin])
|
|
366
|
+
return bytes(out) if is_bytes else "".join(chr(point) for point in out)
|
|
367
|
+
|
|
368
|
+
def stream(self, prefix, body, positions, line, col):
|
|
369
|
+
bits, alphabet, folds, aliases, ignored = _STREAMS[prefix]
|
|
370
|
+
symbols = []
|
|
371
|
+
for index, ch in enumerate(body):
|
|
372
|
+
if ch in " \t\n" or ch in ignored:
|
|
373
|
+
continue
|
|
374
|
+
key = ch.upper() if folds else ch
|
|
375
|
+
key = aliases.get(key, key)
|
|
376
|
+
value = alphabet.find(key)
|
|
377
|
+
if value < 0:
|
|
378
|
+
self.fail(f"invalid {prefix} symbol", *positions[index])
|
|
379
|
+
symbols.append(value)
|
|
380
|
+
total, number = len(symbols) * bits, 0
|
|
381
|
+
for value in symbols:
|
|
382
|
+
number = number << bits | value
|
|
383
|
+
if 8 % bits == 0:
|
|
384
|
+
count = -(-total // 8)
|
|
385
|
+
return (number << (8 * count - total)).to_bytes(count, "big")
|
|
386
|
+
count = total // 8
|
|
387
|
+
if -(-8 * count // bits) != len(symbols):
|
|
388
|
+
self.fail("stream symbol count does not encode a whole byte payload", line, col)
|
|
389
|
+
spare = total - 8 * count
|
|
390
|
+
if number & ((1 << spare) - 1):
|
|
391
|
+
self.fail("stream padding bits must be zero", line, col)
|
|
392
|
+
return (number >> spare).to_bytes(count, "big")
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
class _Parser:
|
|
396
|
+
def __init__(self, tokens, source):
|
|
397
|
+
self.tokens, self.source, self.index = tokens, source, 0
|
|
398
|
+
|
|
399
|
+
def peek(self, offset=0):
|
|
400
|
+
return self.tokens[min(self.index + offset, len(self.tokens) - 1)]
|
|
401
|
+
|
|
402
|
+
def take(self, kind):
|
|
403
|
+
if self.peek().kind == kind:
|
|
404
|
+
token = self.peek()
|
|
405
|
+
if kind != "EOF":
|
|
406
|
+
self.index += 1
|
|
407
|
+
return token
|
|
408
|
+
return None
|
|
409
|
+
|
|
410
|
+
def fail(self, message, token=None):
|
|
411
|
+
token = token or self.peek()
|
|
412
|
+
raise ValidationError(message, token.line, token.col, self.source)
|
|
413
|
+
|
|
414
|
+
def expect(self, kind):
|
|
415
|
+
token = self.take(kind)
|
|
416
|
+
if token is None:
|
|
417
|
+
self.fail(f"expected {kind.lower()}, found {self.peek().kind.lower()}")
|
|
418
|
+
return token
|
|
419
|
+
|
|
420
|
+
def at_key(self):
|
|
421
|
+
return self.peek().kind in ("NAME", "QKEY") and self.peek(1).kind == "COLON"
|
|
422
|
+
|
|
423
|
+
def key(self, seen):
|
|
424
|
+
token = self.peek()
|
|
425
|
+
if token.kind not in ("NAME", "QKEY"):
|
|
426
|
+
self.fail("a mapping key must be a name or a quoted string")
|
|
427
|
+
self.index += 1
|
|
428
|
+
if token.kind == "QKEY" and token.prefix:
|
|
429
|
+
self.fail("a quoted key cannot have a literal prefix", token)
|
|
430
|
+
why = key_error(token.value)
|
|
431
|
+
if why:
|
|
432
|
+
self.fail(why, token)
|
|
433
|
+
if token.value in seen:
|
|
434
|
+
self.fail("duplicate key", token)
|
|
435
|
+
seen.add(token.value)
|
|
436
|
+
self.expect("COLON")
|
|
437
|
+
self.take("EQUAL")
|
|
438
|
+
|
|
439
|
+
def value(self, depth=0):
|
|
440
|
+
if depth > 128:
|
|
441
|
+
self.fail("source nesting exceeds this implementation's ceiling of 128")
|
|
442
|
+
if self.at_key():
|
|
443
|
+
seen = set()
|
|
444
|
+
while self.at_key():
|
|
445
|
+
self.key(seen)
|
|
446
|
+
if self.take("NEWLINE"):
|
|
447
|
+
self.expect("INDENT")
|
|
448
|
+
self.value(depth + 1)
|
|
449
|
+
self.expect("DEDENT")
|
|
450
|
+
else:
|
|
451
|
+
self.flow(depth + 1)
|
|
452
|
+
self.expect("NEWLINE")
|
|
453
|
+
elif self.peek().kind == "DASH":
|
|
454
|
+
while self.take("DASH"):
|
|
455
|
+
self.expect("INDENT")
|
|
456
|
+
deeper = False
|
|
457
|
+
if self.take("NEWLINE"):
|
|
458
|
+
deeper = self.take("INDENT") is not None
|
|
459
|
+
self.value(depth + 1)
|
|
460
|
+
if deeper:
|
|
461
|
+
self.expect("DEDENT")
|
|
462
|
+
self.expect("DEDENT")
|
|
463
|
+
else:
|
|
464
|
+
self.flow(depth + 1)
|
|
465
|
+
self.expect("NEWLINE")
|
|
466
|
+
|
|
467
|
+
def flow(self, depth):
|
|
468
|
+
if depth > 128:
|
|
469
|
+
self.fail("source nesting exceeds this implementation's ceiling of 128")
|
|
470
|
+
if self.take("LBRACKET"):
|
|
471
|
+
if self.take("RBRACKET"):
|
|
472
|
+
return
|
|
473
|
+
while True:
|
|
474
|
+
self.flow(depth + 1)
|
|
475
|
+
if self.take("RBRACKET"):
|
|
476
|
+
return
|
|
477
|
+
self.expect("COMMA")
|
|
478
|
+
if self.take("RBRACKET"):
|
|
479
|
+
return
|
|
480
|
+
elif self.take("LBRACE"):
|
|
481
|
+
seen = set()
|
|
482
|
+
if self.take("RBRACE"):
|
|
483
|
+
return
|
|
484
|
+
while True:
|
|
485
|
+
self.key(seen)
|
|
486
|
+
self.flow(depth + 1)
|
|
487
|
+
if self.take("RBRACE"):
|
|
488
|
+
return
|
|
489
|
+
self.expect("COMMA")
|
|
490
|
+
if self.take("RBRACE"):
|
|
491
|
+
return
|
|
492
|
+
elif self.peek().kind in ("STRING", "BYTES", "INT", "FLOAT", "KEYWORD"):
|
|
493
|
+
self.index += 1
|
|
494
|
+
else:
|
|
495
|
+
self.fail("expected a literal, list, or mapping; bare text and computation are not profile values")
|
|
496
|
+
|
|
497
|
+
def parse(self):
|
|
498
|
+
if self.peek().kind == "EOF":
|
|
499
|
+
self.fail("a document must contain one value")
|
|
500
|
+
self.value()
|
|
501
|
+
self.expect("EOF")
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def validate(text, max_digits, source=None):
|
|
505
|
+
lexer = _Lexer(text, max_digits, source)
|
|
506
|
+
tokens = lexer.tokenize()
|
|
507
|
+
try:
|
|
508
|
+
_Parser(tokens, source).parse()
|
|
509
|
+
except RecursionError:
|
|
510
|
+
raise ValidationError("source nesting exceeds the parser's stack capacity", source=source) from None
|
|
511
|
+
return tokens, lexer.warnings
|