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/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""YamlPlus validation and profile-conformant YAML I/O through PyYAML.
|
|
2
|
+
|
|
3
|
+
Reading uses native safe YAML semantics. Output is validated against the
|
|
4
|
+
literal profile before return. Identifier syntax is pinned to Unicode 14.0.0.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ._xid import UNICODE_VERSION
|
|
8
|
+
from .errors import ValidationError
|
|
9
|
+
from .api import check, dump, dumps, is_yamlplus, load, loads, normalize
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.0"
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"load", "loads", "dump", "dumps", "check", "is_yamlplus", "normalize",
|
|
15
|
+
"ValidationError", "UNICODE_VERSION",
|
|
16
|
+
]
|
yamlplus/__main__.py
ADDED
yamlplus/_identifiers.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Pinned profile identifiers and exact string-key validation."""
|
|
2
|
+
|
|
3
|
+
from bisect import bisect_right
|
|
4
|
+
from unicodedata import decomposition, normalize
|
|
5
|
+
|
|
6
|
+
from ._xid import XID_START, XID_CONTINUE
|
|
7
|
+
|
|
8
|
+
_START = tuple(lo for lo, _ in XID_START)
|
|
9
|
+
_CONTINUE = tuple(lo for lo, _ in XID_CONTINUE)
|
|
10
|
+
KEYWORDS = {"True": True, "False": False, "None": None}
|
|
11
|
+
PREFIXES = frozenset({"b", "rb", "r", "bin", "dna", "oct", "hex", "c32", "b64"})
|
|
12
|
+
_RESERVED_PREFIXES = PREFIXES | {"y", "yb", "yt", "ry"}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _in(character, lows, ranges):
|
|
16
|
+
if not character:
|
|
17
|
+
return False
|
|
18
|
+
point = ord(character)
|
|
19
|
+
index = bisect_right(lows, point) - 1
|
|
20
|
+
return index >= 0 and point <= ranges[index][1]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def start(character):
|
|
24
|
+
return character == "_" or _in(character, _START, XID_START)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def continuation(character):
|
|
28
|
+
return _in(character, _CONTINUE, XID_CONTINUE)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def identifier(value):
|
|
32
|
+
return bool(value) and start(value[0]) and all(continuation(ch) for ch in value[1:])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def fold(value):
|
|
36
|
+
out = []
|
|
37
|
+
for ch in value:
|
|
38
|
+
mapped = decomposition(ch)
|
|
39
|
+
if mapped.startswith(("<wide>", "<narrow>")):
|
|
40
|
+
out.extend(chr(int(part, 16)) for part in mapped.split()[1:])
|
|
41
|
+
else:
|
|
42
|
+
out.append(ch)
|
|
43
|
+
return normalize("NFC", "".join(out))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def dunder(value):
|
|
47
|
+
return value.startswith("__") and value.endswith("__")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def reserved_fold(value):
|
|
51
|
+
folded = fold(value)
|
|
52
|
+
return (folded if folded != value and
|
|
53
|
+
(folded in KEYWORDS or folded in _RESERVED_PREFIXES or dunder(folded)) else None)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def key_error(value):
|
|
57
|
+
if not isinstance(value, str):
|
|
58
|
+
return "a key must be a string"
|
|
59
|
+
if not value:
|
|
60
|
+
return "a key must be nonempty"
|
|
61
|
+
if value.startswith("@"):
|
|
62
|
+
return "keys beginning with '@' are reserved"
|
|
63
|
+
if dunder(value):
|
|
64
|
+
return "keys beginning and ending with '__' are reserved"
|
|
65
|
+
if any(0xD800 <= ord(ch) <= 0xDFFF for ch in value):
|
|
66
|
+
return "a key cannot contain a lone surrogate"
|
|
67
|
+
return ""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def plain_key(value):
|
|
71
|
+
return (identifier(value) and value not in KEYWORDS and not dunder(value)
|
|
72
|
+
and reserved_fold(value) is None and fold(value) == value)
|
yamlplus/_io.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""Safe PyYAML input and profile-conformant, bounded host-value output."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import math
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
from yaml.events import ScalarEvent
|
|
9
|
+
|
|
10
|
+
from .errors import ValidationError
|
|
11
|
+
from ._identifiers import key_error, plain_key
|
|
12
|
+
from ._numbers import WARN_DIGITS, decimal_digits, decimal_text
|
|
13
|
+
from ._pyyaml import Loader
|
|
14
|
+
from ._validation import (Notices, analyze, callback_option, digit_option,
|
|
15
|
+
implicit_type, key_warning, send)
|
|
16
|
+
|
|
17
|
+
MAX_DEPTH = 128
|
|
18
|
+
MAX_SIZE = 4 * 1024 * 1024
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def read(text, source=None):
|
|
22
|
+
try:
|
|
23
|
+
return yaml.load(text, Loader=Loader)
|
|
24
|
+
except yaml.YAMLError as error:
|
|
25
|
+
mark = getattr(error, "problem_mark", None)
|
|
26
|
+
raise ValidationError(getattr(error, "problem", None) or "YAML value construction failed",
|
|
27
|
+
mark.line + 1 if mark else 1, mark.column if mark else 0, source) from None
|
|
28
|
+
except RecursionError:
|
|
29
|
+
raise ValidationError("YAML construction exceeds the runtime's stack capacity", source=source) from None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _Dumper(yaml.SafeDumper):
|
|
33
|
+
ESCAPE_REPLACEMENTS = {
|
|
34
|
+
key: value for key, value in yaml.SafeDumper.ESCAPE_REPLACEMENTS.items()
|
|
35
|
+
if value not in ("e", "N", "_", "L", "P")
|
|
36
|
+
} | {"\0": "x00"}
|
|
37
|
+
|
|
38
|
+
def ignore_aliases(self, data):
|
|
39
|
+
return True
|
|
40
|
+
|
|
41
|
+
def increase_indent(self, flow=False, indentless=False):
|
|
42
|
+
return super().increase_indent(flow, indentless=False)
|
|
43
|
+
|
|
44
|
+
def expect_document_end(self):
|
|
45
|
+
super().expect_document_end()
|
|
46
|
+
self.open_ended = False
|
|
47
|
+
|
|
48
|
+
def check_simple_key(self):
|
|
49
|
+
ordinary = super().check_simple_key()
|
|
50
|
+
return ordinary or isinstance(self.event, ScalarEvent) and self.event.tag == "tag:yaml.org,2002:str"
|
|
51
|
+
|
|
52
|
+
def represent_mapping(self, tag, mapping, flow_style=None):
|
|
53
|
+
node = super().represent_mapping(tag, mapping, flow_style)
|
|
54
|
+
for key, _ in node.value:
|
|
55
|
+
key.style = None if plain_key(key.value) and not implicit_type(key.value) else '"'
|
|
56
|
+
return node
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _text(dumper, value):
|
|
60
|
+
return dumper.represent_scalar("tag:yaml.org,2002:str", value, style='"')
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _none(dumper, value):
|
|
64
|
+
return dumper.represent_scalar("tag:yaml.org,2002:str", "None")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _bytes(dumper, value):
|
|
68
|
+
literal = 'b64"' + base64.b64encode(value).decode("ascii") + '"'
|
|
69
|
+
return dumper.represent_scalar("tag:yaml.org,2002:str", literal)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _bool(dumper, value):
|
|
73
|
+
return dumper.represent_scalar("tag:yaml.org,2002:bool", "True" if value else "False")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _int(dumper, value):
|
|
77
|
+
return dumper.represent_scalar("tag:yaml.org,2002:int", decimal_text(value))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
for _type, _represent in ((str, _text), (type(None), _none), (bytes, _bytes),
|
|
81
|
+
(bool, _bool), (int, _int)):
|
|
82
|
+
_Dumper.add_representer(_type, _represent)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class _Path:
|
|
86
|
+
def __init__(self, parent=None, step="root"):
|
|
87
|
+
self.parent, self.step = parent, step
|
|
88
|
+
|
|
89
|
+
def child(self, step):
|
|
90
|
+
return _Path(self, step)
|
|
91
|
+
|
|
92
|
+
def __str__(self):
|
|
93
|
+
out, node = [], self
|
|
94
|
+
while node is not None:
|
|
95
|
+
out.append(node.step)
|
|
96
|
+
node = node.parent
|
|
97
|
+
return "".join(reversed(out))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _key_step(key, index):
|
|
101
|
+
if not isinstance(key, str):
|
|
102
|
+
return f"[entry {index} key]"
|
|
103
|
+
text = repr(str.__getitem__(key, slice(0, 64)))
|
|
104
|
+
return f"[{text}{'...' if str.__len__(key) > 64 else ''}]"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class _Budget:
|
|
108
|
+
def __init__(self, maximum):
|
|
109
|
+
self.maximum, self.used = maximum, 0
|
|
110
|
+
|
|
111
|
+
def spend(self, amount, path):
|
|
112
|
+
if amount > self.maximum - self.used:
|
|
113
|
+
raise ValidationError(f"serialized UTF-8 exceeds max_size of {self.maximum} bytes", path=str(path))
|
|
114
|
+
self.used += amount
|
|
115
|
+
|
|
116
|
+
def text(self, value, path):
|
|
117
|
+
if len(value) > self.maximum - self.used:
|
|
118
|
+
self.spend(len(value), path)
|
|
119
|
+
for start in range(0, len(value), 4096):
|
|
120
|
+
try:
|
|
121
|
+
size = len(value[start:start + 4096].encode("utf-8"))
|
|
122
|
+
except UnicodeEncodeError:
|
|
123
|
+
raise ValidationError("text cannot contain a lone surrogate", path=str(path)) from None
|
|
124
|
+
self.spend(size, path)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class _Output:
|
|
128
|
+
def __init__(self, maximum):
|
|
129
|
+
self.budget, self.parts = _Budget(maximum), []
|
|
130
|
+
|
|
131
|
+
def write(self, text):
|
|
132
|
+
self.budget.text(text, "root")
|
|
133
|
+
self.parts.append(text)
|
|
134
|
+
return len(text)
|
|
135
|
+
|
|
136
|
+
def flush(self):
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def options(max_digits, max_depth, max_size, warn, info):
|
|
141
|
+
max_digits = digit_option(max_digits)
|
|
142
|
+
for name, value in (("max_depth", max_depth), ("max_size", max_size)):
|
|
143
|
+
if not isinstance(value, int) or isinstance(value, bool):
|
|
144
|
+
raise TypeError(f"{name} must be an integer")
|
|
145
|
+
if value < 1:
|
|
146
|
+
raise ValueError(f"{name} must be positive")
|
|
147
|
+
callback_option(warn, "warn")
|
|
148
|
+
callback_option(info, "info")
|
|
149
|
+
return max_digits, int.__int__(max_depth), int.__int__(max_size)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _prepare_host(value, max_digits, max_depth, max_size):
|
|
153
|
+
budget, notices, warnings = _Budget(max_size), Notices(), []
|
|
154
|
+
active, root = {}, []
|
|
155
|
+
|
|
156
|
+
def children(original, out, path):
|
|
157
|
+
if isinstance(out, list):
|
|
158
|
+
for index, child in enumerate(list.__iter__(original)):
|
|
159
|
+
yield child, path.child(f"[{index}]"), out.append
|
|
160
|
+
else:
|
|
161
|
+
for index, (key, child) in enumerate(dict.items(original)):
|
|
162
|
+
child_path = path.child(_key_step(key, index))
|
|
163
|
+
if not isinstance(key, str):
|
|
164
|
+
raise TypeError(f"at {child_path}: a key must be a string")
|
|
165
|
+
key = str.__str__(key)
|
|
166
|
+
why = key_error(key)
|
|
167
|
+
if why:
|
|
168
|
+
raise ValidationError(why, path=str(child_path))
|
|
169
|
+
if len(key) > 1024:
|
|
170
|
+
raise ValidationError("an implicit key's source spelling must fit within 1,024 characters", path=str(child_path))
|
|
171
|
+
budget.text(key, child_path)
|
|
172
|
+
kind = implicit_type(key)
|
|
173
|
+
budget.spend(2 if plain_key(key) and not kind else 4, child_path)
|
|
174
|
+
if kind:
|
|
175
|
+
warnings.append(key_warning(key, kind, f"at {child_path}", writing=True))
|
|
176
|
+
|
|
177
|
+
def put(item, key=key, out=out):
|
|
178
|
+
out[key] = item
|
|
179
|
+
|
|
180
|
+
yield child, child_path, put
|
|
181
|
+
|
|
182
|
+
pending = [("value", value, _Path(), 0, root.append)]
|
|
183
|
+
while pending:
|
|
184
|
+
task = pending.pop()
|
|
185
|
+
if task[0] == "children":
|
|
186
|
+
_, original, path, depth, iterator = task
|
|
187
|
+
try:
|
|
188
|
+
child, child_path, put = next(iterator)
|
|
189
|
+
except StopIteration:
|
|
190
|
+
del active[id(original)]
|
|
191
|
+
continue
|
|
192
|
+
pending.append(task)
|
|
193
|
+
pending.append(("value", child, child_path, depth, put))
|
|
194
|
+
continue
|
|
195
|
+
_, original, path, depth, put = task
|
|
196
|
+
if original is None:
|
|
197
|
+
budget.spend(4, path)
|
|
198
|
+
notices.add("None", f"at {path}")
|
|
199
|
+
put(None)
|
|
200
|
+
elif isinstance(original, bool):
|
|
201
|
+
budget.spend(4 if original else 5, path)
|
|
202
|
+
put(original)
|
|
203
|
+
elif isinstance(original, int):
|
|
204
|
+
bits = int.bit_length(original)
|
|
205
|
+
if bits > (max_digits * 3322 + 999) // 1000:
|
|
206
|
+
raise ValidationError("integer exceeds max_digits", path=str(path))
|
|
207
|
+
value = int.__int__(original)
|
|
208
|
+
count = decimal_digits(value)
|
|
209
|
+
if count > max_digits:
|
|
210
|
+
raise ValidationError("integer exceeds max_digits", path=str(path))
|
|
211
|
+
budget.spend(count + (value < 0), path)
|
|
212
|
+
if count > WARN_DIGITS:
|
|
213
|
+
warnings.append(f"warning: at {path}: integer has {count} decimal digits; conversion is costly")
|
|
214
|
+
put(value)
|
|
215
|
+
elif isinstance(original, float):
|
|
216
|
+
value = float.__float__(original)
|
|
217
|
+
if not math.isfinite(value):
|
|
218
|
+
raise ValidationError("a float must be finite", path=str(path))
|
|
219
|
+
budget.spend(len(repr(value)), path)
|
|
220
|
+
put(value)
|
|
221
|
+
elif isinstance(original, str):
|
|
222
|
+
if str.__len__(original) > budget.maximum - budget.used:
|
|
223
|
+
budget.spend(str.__len__(original), path)
|
|
224
|
+
value = str.__str__(original)
|
|
225
|
+
budget.text(value, path)
|
|
226
|
+
budget.spend(2, path)
|
|
227
|
+
put(value)
|
|
228
|
+
elif isinstance(original, bytes):
|
|
229
|
+
budget.spend(4 * ((bytes.__len__(original) + 2) // 3) + 5, path)
|
|
230
|
+
notices.add("bytes", f"at {path}")
|
|
231
|
+
put(bytes.__bytes__(original))
|
|
232
|
+
elif isinstance(original, (dict, list)):
|
|
233
|
+
if id(original) in active:
|
|
234
|
+
raise ValidationError(f"container cycle reaches ancestor at {active[id(original)]}", path=str(path))
|
|
235
|
+
depth += 1
|
|
236
|
+
if depth > max_depth:
|
|
237
|
+
raise ValidationError(f"container depth exceeds max_depth of {max_depth}", path=str(path))
|
|
238
|
+
empty = not (dict.__len__(original) if isinstance(original, dict) else list.__len__(original))
|
|
239
|
+
budget.spend(2 if empty else 1, path)
|
|
240
|
+
out = {} if isinstance(original, dict) else []
|
|
241
|
+
put(out)
|
|
242
|
+
active[id(original)] = path
|
|
243
|
+
pending.append(("children", original, path, depth, iter(children(original, out, path))))
|
|
244
|
+
else:
|
|
245
|
+
raise TypeError(f"at {path}: {type(original).__name__} is not supported profile data")
|
|
246
|
+
return root[0], warnings, list(notices.messages())
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def write(value, max_digits, max_depth, max_size, warn, info):
|
|
250
|
+
value, warnings, notices = _prepare_host(value, max_digits, max_depth, max_size)
|
|
251
|
+
output = _Output(max_size)
|
|
252
|
+
try:
|
|
253
|
+
yaml.dump(value, stream=output, Dumper=_Dumper, sort_keys=False,
|
|
254
|
+
default_flow_style=False, allow_unicode=True, width=sys.maxsize,
|
|
255
|
+
explicit_start=False, explicit_end=False)
|
|
256
|
+
except RecursionError:
|
|
257
|
+
raise ValidationError("YAML emission exceeds the runtime's stack capacity", path="root") from None
|
|
258
|
+
text = "".join(output.parts)
|
|
259
|
+
analyze(text, max_digits)
|
|
260
|
+
send(warn, warnings)
|
|
261
|
+
send(info, notices)
|
|
262
|
+
return text
|
yamlplus/_numbers.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Bounded decimal conversion independent of Python's process-wide digit cap."""
|
|
2
|
+
|
|
3
|
+
WARN_DIGITS = 4300
|
|
4
|
+
REFUSE_DIGITS = 4 * WARN_DIGITS
|
|
5
|
+
_CHUNK = 500
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def int_from_decimal(digits):
|
|
9
|
+
if len(digits) <= _CHUNK:
|
|
10
|
+
return int(digits)
|
|
11
|
+
half = len(digits) // 2
|
|
12
|
+
return (int_from_decimal(digits[:-half]) * 10 ** half
|
|
13
|
+
+ int_from_decimal(digits[-half:]))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def decimal_text(value):
|
|
17
|
+
if value < 0:
|
|
18
|
+
return "-" + decimal_text(-value)
|
|
19
|
+
if value < 10 ** _CHUNK:
|
|
20
|
+
return str(value)
|
|
21
|
+
half = max(_CHUNK, (value.bit_length() * 30103 // 100000) // 2)
|
|
22
|
+
high, low = divmod(value, 10 ** half)
|
|
23
|
+
return decimal_text(high) + decimal_text(low).rjust(half, "0")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def decimal_digits(value):
|
|
27
|
+
value = abs(value)
|
|
28
|
+
if value < 10 ** _CHUNK:
|
|
29
|
+
return len(str(value))
|
|
30
|
+
estimate = value.bit_length() * 30103 // 100000
|
|
31
|
+
return estimate + (value >= 10 ** estimate)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def float_text(value):
|
|
35
|
+
text = repr(value)
|
|
36
|
+
if "e" in text:
|
|
37
|
+
mantissa, exponent = text.split("e")
|
|
38
|
+
if "." not in mantissa:
|
|
39
|
+
text = f"{mantissa}.0e{exponent}"
|
|
40
|
+
return text
|
yamlplus/_pyyaml.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Private safe loader with profile-compatible whitespace and decimal handling."""
|
|
2
|
+
|
|
3
|
+
import yaml
|
|
4
|
+
|
|
5
|
+
from ._numbers import int_from_decimal
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Loader(yaml.SafeLoader):
|
|
9
|
+
def scan_to_next_token(self):
|
|
10
|
+
# Profile validation has already excluded indentation tabs.
|
|
11
|
+
while True:
|
|
12
|
+
super().scan_to_next_token()
|
|
13
|
+
if self.peek() != "\t":
|
|
14
|
+
return
|
|
15
|
+
while self.peek() == "\t":
|
|
16
|
+
self.forward()
|
|
17
|
+
|
|
18
|
+
def scan_plain_spaces(self, indent, start_mark):
|
|
19
|
+
length = 0
|
|
20
|
+
while self.peek(length) in " \t":
|
|
21
|
+
length += 1
|
|
22
|
+
whitespace = self.prefix(length)
|
|
23
|
+
self.forward(length)
|
|
24
|
+
if self.peek() not in "\r\n\x85
":
|
|
25
|
+
return [whitespace] if whitespace else []
|
|
26
|
+
line_break = self.scan_line_break()
|
|
27
|
+
self.allow_simple_key = True
|
|
28
|
+
breaks = []
|
|
29
|
+
while True:
|
|
30
|
+
if (self.prefix(3) in ("---", "...") and
|
|
31
|
+
self.peek(3) in "\0 \t\r\n\x85
"):
|
|
32
|
+
return None
|
|
33
|
+
if self.peek() in " \t":
|
|
34
|
+
self.forward()
|
|
35
|
+
elif self.peek() in "\r\n\x85
":
|
|
36
|
+
breaks.append(self.scan_line_break())
|
|
37
|
+
else:
|
|
38
|
+
break
|
|
39
|
+
if line_break != "\n":
|
|
40
|
+
return [line_break, *breaks]
|
|
41
|
+
return breaks or [" "]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _integer(loader, node):
|
|
45
|
+
text = loader.construct_scalar(node).replace("_", "")
|
|
46
|
+
unsigned = text.lstrip("+-")
|
|
47
|
+
if unsigned.isascii() and unsigned.isdecimal() and (len(unsigned) == 1 or unsigned[0] != "0"):
|
|
48
|
+
value = int_from_decimal(unsigned)
|
|
49
|
+
return -value if text.startswith("-") else value
|
|
50
|
+
return yaml.SafeLoader.construct_yaml_int(loader, node)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
Loader.add_constructor("tag:yaml.org,2002:int", _integer)
|