yaffle 0.0.1__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.
- yaffle/__init__.py +77 -0
- yaffle/_harness.py +288 -0
- yaffle/api.py +212 -0
- yaffle/arrays.py +242 -0
- yaffle/async_view.py +288 -0
- yaffle/checksum.py +20 -0
- yaffle/codec.py +183 -0
- yaffle/commit.py +212 -0
- yaffle/errors.py +114 -0
- yaffle/externs.py +139 -0
- yaffle/ints.py +99 -0
- yaffle/jsonc.py +306 -0
- yaffle/layout.py +495 -0
- yaffle/prim.py +105 -0
- yaffle/reader.py +608 -0
- yaffle/store.py +656 -0
- yaffle/streams.py +187 -0
- yaffle/strings.py +98 -0
- yaffle/values.py +168 -0
- yaffle/view.py +1070 -0
- yaffle/writer.py +980 -0
- yaffle-0.0.1.dist-info/METADATA +26 -0
- yaffle-0.0.1.dist-info/RECORD +24 -0
- yaffle-0.0.1.dist-info/WHEEL +4 -0
yaffle/__init__.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""yaffle: the runtime of the Python code the yaffle compiler generates.
|
|
2
|
+
|
|
3
|
+
Generated modules import it as ``_rt``. Pure Python 3.12+, no dependencies. Besides what
|
|
4
|
+
generated code calls, it offers sources (:func:`file_source`), errors (:class:`YaffleError`),
|
|
5
|
+
checksums and canonical JSON helpers.
|
|
6
|
+
|
|
7
|
+
Modules: ``errors``, ``prim`` (primitive encodings), ``ints`` (number semantics), ``strings``,
|
|
8
|
+
``checksum``, ``arrays`` (array builtins and equality), ``jsonc`` (canonical JSON), ``values``
|
|
9
|
+
(checks of values), ``externs`` (host code), ``codec`` (``@via`` and codec reuse), ``streams``
|
|
10
|
+
(streams), ``reader``, ``writer`` and ``layout`` (parse and serialize), ``api`` (root API
|
|
11
|
+
glue and sources), and for views ``store`` (bytes read on demand), ``view``, ``commit`` and
|
|
12
|
+
``async_view``.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from . import (
|
|
18
|
+
api,
|
|
19
|
+
arrays,
|
|
20
|
+
async_view,
|
|
21
|
+
checksum,
|
|
22
|
+
codec,
|
|
23
|
+
commit,
|
|
24
|
+
errors,
|
|
25
|
+
externs,
|
|
26
|
+
ints,
|
|
27
|
+
jsonc,
|
|
28
|
+
layout,
|
|
29
|
+
prim,
|
|
30
|
+
reader,
|
|
31
|
+
streams,
|
|
32
|
+
strings,
|
|
33
|
+
values,
|
|
34
|
+
view,
|
|
35
|
+
writer,
|
|
36
|
+
)
|
|
37
|
+
from .api import *
|
|
38
|
+
from .arrays import *
|
|
39
|
+
from .async_view import *
|
|
40
|
+
from .checksum import *
|
|
41
|
+
from .codec import *
|
|
42
|
+
from .commit import *
|
|
43
|
+
from .errors import *
|
|
44
|
+
from .externs import *
|
|
45
|
+
from .ints import *
|
|
46
|
+
from .jsonc import *
|
|
47
|
+
from .layout import *
|
|
48
|
+
from .prim import *
|
|
49
|
+
from .reader import *
|
|
50
|
+
from .streams import *
|
|
51
|
+
from .strings import *
|
|
52
|
+
from .values import *
|
|
53
|
+
from .view import *
|
|
54
|
+
from .writer import *
|
|
55
|
+
|
|
56
|
+
__version__ = "0.0.1"
|
|
57
|
+
|
|
58
|
+
__all__ = [
|
|
59
|
+
*api.__all__,
|
|
60
|
+
*arrays.__all__,
|
|
61
|
+
*async_view.__all__,
|
|
62
|
+
*checksum.__all__,
|
|
63
|
+
*codec.__all__,
|
|
64
|
+
*commit.__all__,
|
|
65
|
+
*errors.__all__,
|
|
66
|
+
*externs.__all__,
|
|
67
|
+
*ints.__all__,
|
|
68
|
+
*jsonc.__all__,
|
|
69
|
+
*layout.__all__,
|
|
70
|
+
*prim.__all__,
|
|
71
|
+
*reader.__all__,
|
|
72
|
+
*streams.__all__,
|
|
73
|
+
*strings.__all__,
|
|
74
|
+
*values.__all__,
|
|
75
|
+
*view.__all__,
|
|
76
|
+
*writer.__all__,
|
|
77
|
+
]
|
yaffle/_harness.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""Conformance harness: runs a batch of conformance cases through generated code.
|
|
2
|
+
|
|
3
|
+
Usage: ``python -m yaffle._harness <spec.json> <results-dir>``
|
|
4
|
+
|
|
5
|
+
The spec (written by the Python backend's conformance adapter) lists the cases of one case
|
|
6
|
+
directory: which generated module and class to use, the input bytes (hex or a file), the JSON,
|
|
7
|
+
and what is expected. One result file per case is written to the results directory
|
|
8
|
+
(``<index>.json``), with the actual canonical JSON, output bytes and error, and the harness's own
|
|
9
|
+
verdict (the runner re-checks them). See docs/CONFORMANCE.md.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import importlib
|
|
16
|
+
import json
|
|
17
|
+
import math
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import sys
|
|
21
|
+
import traceback
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from .api import file_source
|
|
25
|
+
from .commit import apply_patches
|
|
26
|
+
from .errors import YaffleError
|
|
27
|
+
|
|
28
|
+
_NOT_IMPLEMENTED = re.compile(r"not implemented|not supported", re.IGNORECASE)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _Failed(Exception):
|
|
32
|
+
"""Ends a case with a message (not an error the case could expect)."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _type_name(v: Any) -> str:
|
|
36
|
+
if v is None:
|
|
37
|
+
return "null"
|
|
38
|
+
if isinstance(v, bool):
|
|
39
|
+
return "boolean"
|
|
40
|
+
if isinstance(v, (int, float)):
|
|
41
|
+
return "number"
|
|
42
|
+
if isinstance(v, str):
|
|
43
|
+
return "string"
|
|
44
|
+
if isinstance(v, list):
|
|
45
|
+
return "array"
|
|
46
|
+
return "object"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _show(v: Any) -> str:
|
|
50
|
+
s = json.dumps(v)
|
|
51
|
+
return s if len(s) <= 120 else s[:120] + "…"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def json_diff(actual: Any, expected: Any, path: str = "") -> str | None:
|
|
55
|
+
"""The first difference between two canonical JSON values (key order significant), or None."""
|
|
56
|
+
where = path or "/"
|
|
57
|
+
ta, te = _type_name(actual), _type_name(expected)
|
|
58
|
+
if ta != te:
|
|
59
|
+
return f"{where}: expected {_show(expected)}, got {_show(actual)}"
|
|
60
|
+
if ta == "array":
|
|
61
|
+
if len(actual) != len(expected):
|
|
62
|
+
return f"{where}: expected {len(expected)} elements, got {len(actual)}"
|
|
63
|
+
for i, (a, e) in enumerate(zip(actual, expected)):
|
|
64
|
+
d = json_diff(a, e, f"{path}/{i}")
|
|
65
|
+
if d:
|
|
66
|
+
return d
|
|
67
|
+
return None
|
|
68
|
+
if ta == "object":
|
|
69
|
+
for k in expected:
|
|
70
|
+
if k not in actual:
|
|
71
|
+
return f"{path}/{k}: missing"
|
|
72
|
+
for k in actual:
|
|
73
|
+
if k not in expected:
|
|
74
|
+
return f"{path}/{k}: unexpected"
|
|
75
|
+
for k in expected:
|
|
76
|
+
d = json_diff(actual[k], expected[k], f"{path}/{k}")
|
|
77
|
+
if d:
|
|
78
|
+
return d
|
|
79
|
+
if list(actual) != list(expected):
|
|
80
|
+
return f"{where}: fields are not in declaration order"
|
|
81
|
+
return None
|
|
82
|
+
if ta == "number":
|
|
83
|
+
a, e = float(actual), float(expected)
|
|
84
|
+
if a != e or (a == 0 and math.copysign(1.0, a) != math.copysign(1.0, e)):
|
|
85
|
+
return f"{where}: expected {_show(expected)}, got {_show(actual)}"
|
|
86
|
+
return None
|
|
87
|
+
if actual != expected:
|
|
88
|
+
return f"{where}: expected {_show(expected)}, got {_show(actual)}"
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _load_bytes(spec: Any) -> bytes | None:
|
|
93
|
+
if spec is None:
|
|
94
|
+
return None
|
|
95
|
+
if isinstance(spec, str):
|
|
96
|
+
return bytes.fromhex(re.sub(r"[\s_]+", "", spec))
|
|
97
|
+
with open(spec["file"], "rb") as f:
|
|
98
|
+
return f.read()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _options(cls: Any, c: dict[str, Any]) -> dict[str, Any]:
|
|
102
|
+
options: dict[str, Any] = cls.options_from_json(c.get("args") or {})
|
|
103
|
+
if c.get("strict"):
|
|
104
|
+
options["strict"] = True
|
|
105
|
+
return options
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class _Case:
|
|
109
|
+
"""One case: its spec, its generated class and the result being built."""
|
|
110
|
+
|
|
111
|
+
def __init__(self, c: dict[str, Any], cls: Any, max_hex: int) -> None:
|
|
112
|
+
self.c = c
|
|
113
|
+
self.cls = cls
|
|
114
|
+
self.max_hex = max_hex
|
|
115
|
+
self.is_async = bool(c.get("async"))
|
|
116
|
+
self.error: Any = c.get("error")
|
|
117
|
+
self.result: dict[str, Any] = {"name": c["name"], "ok": False}
|
|
118
|
+
self.phase = "serialize" if c.get("input") is not None else "parse"
|
|
119
|
+
|
|
120
|
+
def set_bytes(self, out: bytes) -> None:
|
|
121
|
+
if len(out) <= self.max_hex:
|
|
122
|
+
self.result["bytes"] = bytes(out).hex()
|
|
123
|
+
|
|
124
|
+
def expect_no_error(self, phase: str | None, what: str) -> None:
|
|
125
|
+
"""Fails the case if it expects an error (in ``phase``, if given): ``what`` succeeded."""
|
|
126
|
+
if self.error and (phase is None or self.error.get("phase") == phase):
|
|
127
|
+
raise _Failed(f"expected {self.error['code']}, but {what} succeeded")
|
|
128
|
+
|
|
129
|
+
def check_json(self, actual: Any, mode: str, what: str) -> None:
|
|
130
|
+
"""Records the JSON read; fails if a parse error was expected or the JSON differs."""
|
|
131
|
+
self.result["json"] = actual
|
|
132
|
+
self.expect_no_error("parse", what)
|
|
133
|
+
expected = self.c.get("json")
|
|
134
|
+
if expected is not None:
|
|
135
|
+
d = json_diff(actual, expected)
|
|
136
|
+
if d:
|
|
137
|
+
raise _Failed(f"{mode}: {d}")
|
|
138
|
+
|
|
139
|
+
async def parse(self, data: bytes, options: dict[str, Any]) -> Any:
|
|
140
|
+
if self.is_async:
|
|
141
|
+
return await self.cls.parse_async(data, **options)
|
|
142
|
+
return self.cls.parse(data, **options)
|
|
143
|
+
|
|
144
|
+
async def serialize(self, value: Any, options: dict[str, Any]) -> bytes:
|
|
145
|
+
write_options = {k: v for k, v in options.items() if k != "strict"}
|
|
146
|
+
if self.is_async:
|
|
147
|
+
out: bytes = await self.cls.serialize_async(value, **write_options)
|
|
148
|
+
else:
|
|
149
|
+
out = self.cls.serialize(value, **write_options)
|
|
150
|
+
self.set_bytes(out)
|
|
151
|
+
return out
|
|
152
|
+
|
|
153
|
+
async def run(self) -> None:
|
|
154
|
+
c = self.c
|
|
155
|
+
options = _options(self.cls, c)
|
|
156
|
+
data = _load_bytes(c.get("bytes"))
|
|
157
|
+
if c.get("input") is not None:
|
|
158
|
+
out = await self.serialize(self.cls.from_json(c["input"]), options)
|
|
159
|
+
self.expect_no_error(None, "serialize")
|
|
160
|
+
if data is None:
|
|
161
|
+
raise _Failed("serialize case without bytes")
|
|
162
|
+
if out != data:
|
|
163
|
+
raise _Failed(f"serialize: expected {data.hex()}, got {self.result.get('bytes', '(large)')}")
|
|
164
|
+
return
|
|
165
|
+
if data is None:
|
|
166
|
+
raise _Failed("parse case without bytes")
|
|
167
|
+
value = await self.parse(data, options)
|
|
168
|
+
self.check_json(value.to_json(), "parse", "parse")
|
|
169
|
+
# Cases expecting a serialize error round-trip the JSON.
|
|
170
|
+
rt = "json" if (self.error and self.error.get("phase") == "serialize") else c.get("roundtrip", True)
|
|
171
|
+
if rt is False:
|
|
172
|
+
return
|
|
173
|
+
self.phase = "serialize"
|
|
174
|
+
source = value if rt == "value" else self.cls.from_json(c["json"] if c.get("json") is not None else self.result["json"])
|
|
175
|
+
out = await self.serialize(source, options)
|
|
176
|
+
self.expect_no_error("serialize", "serialize")
|
|
177
|
+
if out != data:
|
|
178
|
+
i = 0
|
|
179
|
+
n = min(len(out), len(data))
|
|
180
|
+
while i < n and out[i] == data[i]:
|
|
181
|
+
i += 1
|
|
182
|
+
raise _Failed(f"roundtrip: bytes differ at offset 0x{i:x} (expected {len(data)} bytes, got {len(out)})")
|
|
183
|
+
|
|
184
|
+
async def run_view(self) -> None:
|
|
185
|
+
"""View mode (docs/CONFORMANCE.md §7): read every field through the lazy view, then commit
|
|
186
|
+
with no edits (which must reproduce the input)."""
|
|
187
|
+
c = self.c
|
|
188
|
+
options = _options(self.cls, c)
|
|
189
|
+
spec = c.get("bytes")
|
|
190
|
+
if spec is None:
|
|
191
|
+
raise _Failed("view case without bytes")
|
|
192
|
+
# A file is viewed through a memory-mapped source.
|
|
193
|
+
src: Any = _load_bytes(spec) if isinstance(spec, str) else file_source(spec["file"])
|
|
194
|
+
try:
|
|
195
|
+
if self.is_async:
|
|
196
|
+
v = self.cls.view_async(src, **options)
|
|
197
|
+
actual = await v._to_json()
|
|
198
|
+
else:
|
|
199
|
+
v = self.cls.view(src, **options)
|
|
200
|
+
actual = self.cls.to_json(v) # reads every field through the view
|
|
201
|
+
self.check_json(actual, "view", "reading the view")
|
|
202
|
+
if c.get("roundtrip", True) is False:
|
|
203
|
+
return
|
|
204
|
+
self.phase = "serialize"
|
|
205
|
+
orig = _load_bytes(spec)
|
|
206
|
+
assert orig is not None
|
|
207
|
+
if self.is_async:
|
|
208
|
+
out = apply_patches(orig, await v._commit())
|
|
209
|
+
else:
|
|
210
|
+
out = v._commit()
|
|
211
|
+
self.set_bytes(out)
|
|
212
|
+
if orig != bytes(out):
|
|
213
|
+
raise _Failed(f"commit with no edits changed the bytes ({len(orig)} → {len(out)} bytes)")
|
|
214
|
+
finally:
|
|
215
|
+
close = getattr(src, "close", None)
|
|
216
|
+
if callable(close):
|
|
217
|
+
close()
|
|
218
|
+
|
|
219
|
+
def failed(self, e: BaseException) -> None:
|
|
220
|
+
"""Records that the case raised ``e`` in the current phase."""
|
|
221
|
+
result = self.result
|
|
222
|
+
if not isinstance(e, YaffleError):
|
|
223
|
+
msg = str(e)
|
|
224
|
+
if _NOT_IMPLEMENTED.search(msg):
|
|
225
|
+
result["skipped"] = msg
|
|
226
|
+
else:
|
|
227
|
+
result["message"] = f"unexpected exception: {type(e).__name__}: {msg}\n{traceback.format_exc()}"
|
|
228
|
+
return
|
|
229
|
+
message = e.describe()
|
|
230
|
+
err = {"phase": self.phase, "code": e.code, "path": list(e.path), "offset": e.offset, "message": message}
|
|
231
|
+
result["error"] = err
|
|
232
|
+
expected = self.error
|
|
233
|
+
if not expected:
|
|
234
|
+
if _NOT_IMPLEMENTED.search(message):
|
|
235
|
+
result["skipped"] = message
|
|
236
|
+
else:
|
|
237
|
+
result["message"] = f"unexpected {e.code} error in {self.phase}: {message}"
|
|
238
|
+
elif expected.get("phase") != self.phase:
|
|
239
|
+
result["message"] = f"expected a {expected['phase']} error, got {e.code} in {self.phase}: {message}"
|
|
240
|
+
elif e.code != expected.get("code"):
|
|
241
|
+
result["message"] = f"expected {expected['code']}, got {e.code}: {message}"
|
|
242
|
+
elif expected.get("path") is not None and [str(x) for x in e.path] != [str(x) for x in expected["path"]]:
|
|
243
|
+
result["message"] = f"expected path {expected['path']}, got {list(e.path)} ({message})"
|
|
244
|
+
else:
|
|
245
|
+
result["ok"] = True
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
async def run_case(c: dict[str, Any], max_hex: int, mode: str = "parse") -> dict[str, Any]:
|
|
249
|
+
try:
|
|
250
|
+
cls = getattr(importlib.import_module(c["module"]), c["cls"])
|
|
251
|
+
except Exception as e: # pragma: no cover - generated code that doesn't import
|
|
252
|
+
return {"name": c["name"], "ok": False, "message": f"cannot import generated code: {e}\n{traceback.format_exc()}"}
|
|
253
|
+
case = _Case(c, cls, max_hex)
|
|
254
|
+
try:
|
|
255
|
+
await (case.run_view() if mode == "view" else case.run())
|
|
256
|
+
case.result["ok"] = True
|
|
257
|
+
except _Failed as f:
|
|
258
|
+
case.result["message"] = str(f)
|
|
259
|
+
except (KeyboardInterrupt, SystemExit):
|
|
260
|
+
raise
|
|
261
|
+
except BaseException as e: # noqa: BLE001 - every failure is reported
|
|
262
|
+
case.failed(e)
|
|
263
|
+
return case.result
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
async def main(spec_path: str, out_dir: str) -> None:
|
|
267
|
+
with open(spec_path, encoding="utf-8") as f:
|
|
268
|
+
spec = json.load(f)
|
|
269
|
+
# Real files nest deeply.
|
|
270
|
+
sys.setrecursionlimit(max(sys.getrecursionlimit(), 200_000))
|
|
271
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
272
|
+
max_hex = int(spec.get("maxHexBytes", 1 << 20))
|
|
273
|
+
mode = str(spec.get("mode", "parse"))
|
|
274
|
+
for i, c in enumerate(spec["cases"]):
|
|
275
|
+
res = await run_case(c, max_hex, mode)
|
|
276
|
+
tmp = os.path.join(out_dir, f"{i}.json.tmp")
|
|
277
|
+
with open(tmp, "w", encoding="utf-8") as f:
|
|
278
|
+
try:
|
|
279
|
+
json.dump(res, f, allow_nan=False)
|
|
280
|
+
except (TypeError, ValueError) as e:
|
|
281
|
+
f.seek(0)
|
|
282
|
+
f.truncate()
|
|
283
|
+
json.dump({"name": c["name"], "ok": False, "message": f"result is not JSON: {e}"}, f)
|
|
284
|
+
os.replace(tmp, os.path.join(out_dir, f"{i}.json"))
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
if __name__ == "__main__":
|
|
288
|
+
asyncio.run(main(sys.argv[1], sys.argv[2]))
|
yaffle/api.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""What the generated root APIs (``X.parse``, ``X.serialize``, …) run on, and byte sources.
|
|
2
|
+
|
|
3
|
+
A source is a bytes-like object, or an object with a ``size`` and ``read(offset, length)``. If
|
|
4
|
+
``read`` returns an awaitable, the source is async: use ``parse_async`` (or an async view).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import inspect
|
|
10
|
+
import mmap
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any, Awaitable, Callable, Generic, Literal, Protocol, Sequence, TypeVar, Union
|
|
15
|
+
|
|
16
|
+
from .errors import YaffleError, fail
|
|
17
|
+
from .reader import Reader
|
|
18
|
+
from .writer import Region, Writer
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"AsyncSource", "FileSource", "SafeResult", "Source", "SyncSource", "endian_option",
|
|
22
|
+
"file_source", "parse_with", "parse_with_async", "read_all", "read_all_async", "safe",
|
|
23
|
+
"safe_async", "serialize_with", "serialize_with_async",
|
|
24
|
+
] # fmt: skip
|
|
25
|
+
|
|
26
|
+
T = TypeVar("T")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SyncSource(Protocol):
|
|
30
|
+
@property
|
|
31
|
+
def size(self) -> int: ...
|
|
32
|
+
|
|
33
|
+
def read(self, offset: int, length: int) -> bytes: ...
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AsyncSource(Protocol):
|
|
37
|
+
@property
|
|
38
|
+
def size(self) -> int: ...
|
|
39
|
+
|
|
40
|
+
def read(self, offset: int, length: int) -> Awaitable[bytes]: ...
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
Source = Union[bytes, bytearray, memoryview, SyncSource, AsyncSource]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class SafeResult(Generic[T]):
|
|
48
|
+
"""The result of ``safe_parse``: ``ok`` with a ``value``, or not ``ok`` with an ``error``."""
|
|
49
|
+
|
|
50
|
+
ok: bool
|
|
51
|
+
value: T | None = None
|
|
52
|
+
error: YaffleError | None = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def ensure_recursion(limit: int = 100_000) -> None:
|
|
56
|
+
"""Raises the interpreter's recursion limit for deeply nested data (never lowers it)."""
|
|
57
|
+
if sys.getrecursionlimit() < limit:
|
|
58
|
+
sys.setrecursionlimit(limit)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def reject_path(source: Any) -> None:
|
|
62
|
+
if isinstance(source, (str, os.PathLike)):
|
|
63
|
+
fail("INPUT", "pass bytes or a source, not a path (use file_source(path))")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def read_all(source: Any) -> bytes:
|
|
67
|
+
"""Reads a whole sync source into memory."""
|
|
68
|
+
if isinstance(source, (bytes, bytearray, memoryview)):
|
|
69
|
+
return bytes(source)
|
|
70
|
+
reject_path(source)
|
|
71
|
+
out = source.read(0, source.size)
|
|
72
|
+
if inspect.isawaitable(out):
|
|
73
|
+
if inspect.iscoroutine(out):
|
|
74
|
+
out.close()
|
|
75
|
+
fail("INPUT", "this source is async; use parse_async or an async view")
|
|
76
|
+
return bytes(out)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def read_all_async(source: Any) -> bytes:
|
|
80
|
+
"""Reads a whole source (sync or async) into memory."""
|
|
81
|
+
if isinstance(source, (bytes, bytearray, memoryview)):
|
|
82
|
+
return bytes(source)
|
|
83
|
+
out = source.read(0, source.size)
|
|
84
|
+
if inspect.isawaitable(out):
|
|
85
|
+
out = await out
|
|
86
|
+
return bytes(out)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def parse_with(source: Any, strict: bool, read: Callable[[Reader], T]) -> T:
|
|
90
|
+
"""Runs a generated root reader over a sync source."""
|
|
91
|
+
ensure_recursion()
|
|
92
|
+
return read(Reader(read_all(source), strict))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def parse_with_async(source: Any, strict: bool, read: Callable[[Reader], Any]) -> Any:
|
|
96
|
+
"""Runs a generated root reader (sync or async) over any source."""
|
|
97
|
+
ensure_recursion()
|
|
98
|
+
out = read(Reader(await read_all_async(source), strict))
|
|
99
|
+
return await out if inspect.isawaitable(out) else out
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def serialize_with(layout: Sequence[Any] | None, write: Callable[[Writer], Any]) -> bytes:
|
|
103
|
+
"""Runs a generated root writer in a fresh root region and returns the bytes."""
|
|
104
|
+
ensure_recursion()
|
|
105
|
+
region = Region(layout)
|
|
106
|
+
region.attach_root(write(Writer(region)))
|
|
107
|
+
return bytes(region.finish())
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
async def serialize_with_async(
|
|
111
|
+
layout: Sequence[Any] | None, write: Callable[[Writer], Awaitable[Any]]
|
|
112
|
+
) -> bytes:
|
|
113
|
+
ensure_recursion()
|
|
114
|
+
region = Region(layout)
|
|
115
|
+
region.attach_root(await write(Writer(region)))
|
|
116
|
+
return bytes(region.finish())
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def safe(fn: Callable[[], T]) -> SafeResult[T]:
|
|
120
|
+
"""Runs ``fn``; a raised :class:`YaffleError` becomes ``SafeResult(ok=False, error=…)``."""
|
|
121
|
+
try:
|
|
122
|
+
return SafeResult(True, fn())
|
|
123
|
+
except YaffleError as e:
|
|
124
|
+
return SafeResult(False, None, e)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
async def safe_async(fn: Callable[[], Awaitable[T]]) -> SafeResult[T]:
|
|
128
|
+
try:
|
|
129
|
+
return SafeResult(True, await fn())
|
|
130
|
+
except YaffleError as e:
|
|
131
|
+
return SafeResult(False, None, e)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def endian_option(v: Any) -> Literal["le", "be"]:
|
|
135
|
+
if v == "le":
|
|
136
|
+
return "le"
|
|
137
|
+
if v == "be":
|
|
138
|
+
return "be"
|
|
139
|
+
fail("INPUT", f'endian must be "le" or "be", got {v!r}')
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class FileSource:
|
|
143
|
+
"""A sync random-access source over a file (``size``, ``read(offset, length)``).
|
|
144
|
+
|
|
145
|
+
``buffer()`` memory-maps the file, so views read only the pages they touch. With
|
|
146
|
+
``writable=True`` the source also has ``write`` and ``truncate``, and a view's ``_commit``
|
|
147
|
+
writes the edits back to the file.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
def __init__(self, path: str | os.PathLike[str], writable: bool = False) -> None:
|
|
151
|
+
self._f = open(path, "r+b" if writable else "rb") # noqa: SIM115
|
|
152
|
+
self._f.seek(0, os.SEEK_END)
|
|
153
|
+
self.size = self._f.tell()
|
|
154
|
+
self.writable = writable
|
|
155
|
+
self._map: mmap.mmap | None = None
|
|
156
|
+
if not writable:
|
|
157
|
+
# Views write commits back to sources that have `write`.
|
|
158
|
+
self.write = None # type: ignore[assignment, method-assign]
|
|
159
|
+
|
|
160
|
+
def read(self, offset: int, length: int) -> bytes:
|
|
161
|
+
self._f.seek(offset)
|
|
162
|
+
data = self._f.read(length)
|
|
163
|
+
if len(data) != length:
|
|
164
|
+
fail("EOF", "short read from file", offset)
|
|
165
|
+
return data
|
|
166
|
+
|
|
167
|
+
def buffer(self) -> Any:
|
|
168
|
+
"""The file's bytes as a read-only memory map (bytes for an empty file)."""
|
|
169
|
+
if self.size == 0:
|
|
170
|
+
return b""
|
|
171
|
+
if self._map is None:
|
|
172
|
+
self._map = mmap.mmap(self._f.fileno(), 0, access=mmap.ACCESS_READ)
|
|
173
|
+
return self._map
|
|
174
|
+
|
|
175
|
+
def copy_buffer(self) -> Any:
|
|
176
|
+
"""A private copy-on-write map of the file: a view's in-place patches copy only the pages
|
|
177
|
+
they write, and the file is not changed."""
|
|
178
|
+
if self.size == 0:
|
|
179
|
+
return bytearray()
|
|
180
|
+
return mmap.mmap(self._f.fileno(), 0, access=mmap.ACCESS_COPY)
|
|
181
|
+
|
|
182
|
+
def write(self, offset: int, data: bytes) -> None:
|
|
183
|
+
self._drop_map()
|
|
184
|
+
self._f.seek(offset)
|
|
185
|
+
self._f.write(data)
|
|
186
|
+
self._f.flush()
|
|
187
|
+
self.size = max(self.size, offset + len(data))
|
|
188
|
+
|
|
189
|
+
def truncate(self, n: int) -> None:
|
|
190
|
+
self._drop_map()
|
|
191
|
+
self._f.truncate(n)
|
|
192
|
+
self.size = n
|
|
193
|
+
|
|
194
|
+
def _drop_map(self) -> None:
|
|
195
|
+
if self._map is not None:
|
|
196
|
+
self._map.close()
|
|
197
|
+
self._map = None
|
|
198
|
+
|
|
199
|
+
def close(self) -> None:
|
|
200
|
+
self._drop_map()
|
|
201
|
+
self._f.close()
|
|
202
|
+
|
|
203
|
+
def __enter__(self) -> FileSource:
|
|
204
|
+
return self
|
|
205
|
+
|
|
206
|
+
def __exit__(self, *exc: Any) -> None:
|
|
207
|
+
self.close()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def file_source(path: str | os.PathLike[str], writable: bool = False) -> FileSource:
|
|
211
|
+
"""A source over a file (memory-mapped for views; ``writable`` for committing views back)."""
|
|
212
|
+
return FileSource(path, writable)
|