structile 6.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.
- structile/__init__.py +544 -0
- structile/__main__.py +57 -0
- structile/_dom_lite.py +128 -0
- structile/_log.py +33 -0
- structile/_paths.py +148 -0
- structile/_plugins.py +230 -0
- structile/config.py +272 -0
- structile/convert.py +247 -0
- structile/dom_lite.js +70 -0
- structile/interpreters.py +430 -0
- structile/render.py +535 -0
- structile/serialize.py +88 -0
- structile/static/structile.prod.html +188 -0
- structile/static/widget.js +185 -0
- structile/widget.py +406 -0
- structile-6.0.0.dist-info/METADATA +471 -0
- structile-6.0.0.dist-info/RECORD +20 -0
- structile-6.0.0.dist-info/WHEEL +5 -0
- structile-6.0.0.dist-info/licenses/LICENSE +201 -0
- structile-6.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
"""Interpreter registry + selection — markup (XML, HTML included) and any
|
|
2
|
+
other text format alike — see README.md's "Custom formats via an
|
|
3
|
+
interpreter" section and docs/advanced.md's "Interpreter selection" section
|
|
4
|
+
for the full picture.
|
|
5
|
+
|
|
6
|
+
An interpreter is caller-supplied JS: `interpretXML(xmlDocument)` (optionally
|
|
7
|
+
`serializeXML(value)`) for markup, given a DOM document; or `interpretText(text)`
|
|
8
|
+
(optionally `serializeText(value)`) for any other format, given the raw text
|
|
9
|
+
directly — see interpreters/generic_xml.js / interpreters/html.js for the
|
|
10
|
+
former and interpreters/generic_ini.js for the latter, and their header
|
|
11
|
+
comments for the "fail loudly on the wrong shape" contract this module's
|
|
12
|
+
selection depends on. `xmlDocument` is a REAL browser DOM when open()/
|
|
13
|
+
diff() resolve this client-side (structile.html's own DOMParser);
|
|
14
|
+
when this module runs an interpreter itself (`run_interpreter_source`, used
|
|
15
|
+
by `convert()` and whenever more than one candidate needs disambiguating —
|
|
16
|
+
see `select_interpreter` below), it's a much smaller DOM-lite object built
|
|
17
|
+
from a Python-parsed tree instead (see dom_lite.py/dom_lite.js) — no Node.js,
|
|
18
|
+
no jsdom, just an embedded JS engine (`mini-racer`).
|
|
19
|
+
|
|
20
|
+
This module is generic across every extension AND across which contract an
|
|
21
|
+
interpreter uses, on purpose: HTML isn't treated as a different *kind* of
|
|
22
|
+
thing from XML anywhere below, only as a different DOMParser *mode*
|
|
23
|
+
(`MARKUP_FORMATS`, the one place that distinction exists at all) — the
|
|
24
|
+
registry, candidate-list resolution, and try-in-order selection never
|
|
25
|
+
branch on "is this XML or HTML", let alone "is this markup at all". Which
|
|
26
|
+
of the two JS contracts actually applies is decided client-side
|
|
27
|
+
(structile.html's `tryInterpreterCandidates`/`structuredFormat`),
|
|
28
|
+
never here.
|
|
29
|
+
"""
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
import pathlib
|
|
34
|
+
from dataclasses import dataclass
|
|
35
|
+
from typing import Any, Dict, List, Optional, Sequence, Union
|
|
36
|
+
|
|
37
|
+
from ._dom_lite import build_document_payload
|
|
38
|
+
from ._log import logger
|
|
39
|
+
from ._paths import InterpreterSource, read_interpreter
|
|
40
|
+
|
|
41
|
+
# A single candidate: a path, a bare str (a path if it names an existing
|
|
42
|
+
# file, else literal JS source text — see read_interpreter), or an explicit
|
|
43
|
+
# InterpreterSource (source text supplied directly, e.g. from an installed
|
|
44
|
+
# plugin package — see _paths.py's InterpreterSource docstring).
|
|
45
|
+
InterpreterCandidate = Union[str, pathlib.Path, InterpreterSource]
|
|
46
|
+
# A single candidate, or a list of candidates tried in order for ONE format.
|
|
47
|
+
InterpreterCandidateOrList = Union[InterpreterCandidate, Sequence[InterpreterCandidate]]
|
|
48
|
+
# The above, OR a dict keyed by file extension (".xml", ".html", ...) — each
|
|
49
|
+
# entry tried only for a file/format matching THAT extension, never mixed
|
|
50
|
+
# with another entry's candidates (see resolve_candidates /
|
|
51
|
+
# select_interpreter_any_format).
|
|
52
|
+
InterpreterSpec = Union[InterpreterCandidateOrList, Dict[str, InterpreterCandidateOrList]]
|
|
53
|
+
|
|
54
|
+
# The only xml/html-specific knowledge in this whole module: which DOMParser
|
|
55
|
+
# mode a markup extension implies. A file extension not listed here can
|
|
56
|
+
# still be used for ANY format — markup (pass format="xml"/"html"
|
|
57
|
+
# explicitly) or otherwise (open() infers a format straight from the
|
|
58
|
+
# extension once an interpreter is attached to it, e.g. ".ini" -> "ini") —
|
|
59
|
+
# this dict only grows the set of extensions a format is inferred from via
|
|
60
|
+
# the markup/DOM path specifically; it's not a gate on what's usable at all.
|
|
61
|
+
MARKUP_FORMATS: Dict[str, str] = {".xml": "xml", ".html": "html", ".htm": "html"}
|
|
62
|
+
# The reverse: which extension to use as a registry lookup key when only a
|
|
63
|
+
# format (not a real file/extension) is known — e.g. structile.open(xml_text,
|
|
64
|
+
# format="xml", ...) with no path to infer an extension from.
|
|
65
|
+
CANONICAL_EXT_FOR_FORMAT: Dict[str, str] = {"xml": ".xml", "html": ".html"}
|
|
66
|
+
|
|
67
|
+
_DOM_LITE_JS_PATH = pathlib.Path(__file__).resolve().parent / "dom_lite.js"
|
|
68
|
+
_SET_MARK = "__structile_set__"
|
|
69
|
+
|
|
70
|
+
_registry: Dict[str, InterpreterSpec] = {}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _normalize_ext(ext: str) -> str:
|
|
74
|
+
ext = ext.lower()
|
|
75
|
+
return ext if ext.startswith(".") else "." + ext
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ---- registry: set up once at import time --------------------------------
|
|
79
|
+
def register_interpreter(ext: str, interpreter: Optional[InterpreterSpec]) -> None:
|
|
80
|
+
"""Register one or more interpreters for a file extension, once, at
|
|
81
|
+
import time — so later `structile.open(path)` / `convert(path, ...)` calls
|
|
82
|
+
don't need `interpreter=` at all:
|
|
83
|
+
|
|
84
|
+
structile.register_interpreter(".xml", "interpreters/generic_xml.js")
|
|
85
|
+
structile.register_interpreter(".html", ["interpreters/html.js", "interpreters/generic_xml.js"])
|
|
86
|
+
|
|
87
|
+
`interpreter` is a single path/raw JS source/`InterpreterSource`, or a
|
|
88
|
+
list/tuple of candidates tried in order — the first one that
|
|
89
|
+
successfully parses the data wins (see `select_interpreter`). `None`
|
|
90
|
+
unregisters `ext` (same as `unregister_interpreter`).
|
|
91
|
+
|
|
92
|
+
A direct call here always wins over a plugin's own registration for the
|
|
93
|
+
same `ext` (see `_plugins.py`'s precedence rule) — calling this
|
|
94
|
+
"forgets" any plugin ownership of `ext` recorded so far, so a later
|
|
95
|
+
`load_plugins(force=True)` can't silently re-overwrite what was just set
|
|
96
|
+
directly.
|
|
97
|
+
"""
|
|
98
|
+
ext = _normalize_ext(ext)
|
|
99
|
+
from . import _plugins as _plugins_mod
|
|
100
|
+
|
|
101
|
+
_plugins_mod._plugin_owner_by_ext.pop(ext, None)
|
|
102
|
+
if interpreter is None:
|
|
103
|
+
_registry.pop(ext, None)
|
|
104
|
+
logger.debug("structile: unregistered interpreter(s) for %r", ext)
|
|
105
|
+
return
|
|
106
|
+
_registry[ext] = interpreter
|
|
107
|
+
logger.debug("structile: registered interpreter(s) for %r: %r", ext, interpreter)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def unregister_interpreter(ext: str) -> None:
|
|
111
|
+
"""Remove any interpreter(s) registered for `ext`."""
|
|
112
|
+
register_interpreter(ext, None)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def get_registered_interpreter(ext: str) -> Optional[InterpreterSpec]:
|
|
116
|
+
"""The raw spec registered for `ext` — a single candidate, a list, or
|
|
117
|
+
`None` if nothing's registered. See `resolve_candidates` for the
|
|
118
|
+
normalized list form callers actually use.
|
|
119
|
+
|
|
120
|
+
Triggers entry-point plugin discovery first (lazy, at most once per
|
|
121
|
+
process — see `_plugins.load_plugins`): every renderer's resolution
|
|
122
|
+
path (`open()`/`diff()`'s markup-mode detection, `resolve_candidates`'s
|
|
123
|
+
own registry fallback below, `convert()`'s format inference) reaches the
|
|
124
|
+
registry through this one function, so a plugin's registration becomes
|
|
125
|
+
visible here with no import of the plugin package and no explicit
|
|
126
|
+
registration call needed.
|
|
127
|
+
"""
|
|
128
|
+
from . import _plugins as _plugins_mod
|
|
129
|
+
|
|
130
|
+
_plugins_mod.load_plugins()
|
|
131
|
+
return _registry.get(_normalize_ext(ext))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def resolve_candidates(explicit: Optional[InterpreterSpec], ext: Optional[str]) -> List[InterpreterCandidate]:
|
|
135
|
+
"""Normalize `interpreter=` (explicit, wins if given) or the registry
|
|
136
|
+
entry for `ext` (fallback — `ext` may be `None` when there's no file/
|
|
137
|
+
extension to key off of, e.g. a literal markup string) into a plain
|
|
138
|
+
list of candidates, in the order they should be tried. Empty means
|
|
139
|
+
"nothing available for this format".
|
|
140
|
+
|
|
141
|
+
If `explicit` is a `dict` (keyed by file extension — e.g.
|
|
142
|
+
`{".xml": [...], ".html": [...]}`), only the entry for THIS `ext` is
|
|
143
|
+
used — candidates registered for a different extension are never tried
|
|
144
|
+
against a file/format they weren't meant for. A `dict` with no entry
|
|
145
|
+
for `ext` (or given with `ext=None`) resolves to nothing, same as no
|
|
146
|
+
interpreter at all — see `select_interpreter_any_format` for the
|
|
147
|
+
"format itself is unknown, try every entry" case this deliberately
|
|
148
|
+
doesn't cover.
|
|
149
|
+
"""
|
|
150
|
+
if isinstance(explicit, dict):
|
|
151
|
+
normalized = {_normalize_ext(k): v for k, v in explicit.items()}
|
|
152
|
+
spec = normalized.get(_normalize_ext(ext)) if ext else None
|
|
153
|
+
else:
|
|
154
|
+
spec = explicit if explicit is not None else (get_registered_interpreter(ext) if ext else None)
|
|
155
|
+
if spec is None:
|
|
156
|
+
return []
|
|
157
|
+
if isinstance(spec, (str, pathlib.Path, InterpreterSource)):
|
|
158
|
+
return [spec]
|
|
159
|
+
return list(spec)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# ---- running one candidate via an embedded JS engine (parse or serialize) -
|
|
163
|
+
def _mark_sets(value: Any) -> Any:
|
|
164
|
+
"""JSON has no Set — mark one the same reversible way dom_lite.js's
|
|
165
|
+
__structileMarkSets/__structileUnmarkSets do on its side, so `value instanceof Set`
|
|
166
|
+
still works inside the interpreter."""
|
|
167
|
+
if isinstance(value, (set, frozenset)):
|
|
168
|
+
try:
|
|
169
|
+
items = sorted(value)
|
|
170
|
+
except TypeError:
|
|
171
|
+
items = list(value)
|
|
172
|
+
return {_SET_MARK: [_mark_sets(v) for v in items]}
|
|
173
|
+
if isinstance(value, (list, tuple)):
|
|
174
|
+
return [_mark_sets(v) for v in value]
|
|
175
|
+
if isinstance(value, dict):
|
|
176
|
+
return {k: _mark_sets(v) for k, v in value.items()}
|
|
177
|
+
return value
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _unmark_sets(value: Any) -> Any:
|
|
181
|
+
if isinstance(value, dict) and list(value.keys()) == [_SET_MARK] and isinstance(value[_SET_MARK], list):
|
|
182
|
+
return {_unmark_sets(v) for v in value[_SET_MARK]}
|
|
183
|
+
if isinstance(value, list):
|
|
184
|
+
return [_unmark_sets(v) for v in value]
|
|
185
|
+
if isinstance(value, dict):
|
|
186
|
+
return {k: _unmark_sets(v) for k, v in value.items()}
|
|
187
|
+
return value
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _mini_racer_available() -> bool:
|
|
191
|
+
try:
|
|
192
|
+
import py_mini_racer # noqa: F401
|
|
193
|
+
except ImportError:
|
|
194
|
+
return False
|
|
195
|
+
return True
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def run_interpreter_source(
|
|
199
|
+
op: str,
|
|
200
|
+
interpreter_source: str,
|
|
201
|
+
fmt: str,
|
|
202
|
+
*,
|
|
203
|
+
text: Optional[str] = None,
|
|
204
|
+
value: Any = None,
|
|
205
|
+
) -> Any:
|
|
206
|
+
"""Run one already-resolved interpreter script, as actual JS, inside an
|
|
207
|
+
embedded V8 engine (the `mini-racer` package — see dom_lite.js/
|
|
208
|
+
_dom_lite.py) — the only way to either get a real parsed/serialized
|
|
209
|
+
result out of an interpreter in Python (used by `convert()`), or to
|
|
210
|
+
test whether a *candidate* interpreter accepts a given document at all
|
|
211
|
+
(used by `select_interpreter` below). No Node.js, no jsdom, no
|
|
212
|
+
subprocess: for `fmt` `"xml"`/`"html"` the DOM handed to `interpretXML`
|
|
213
|
+
is built directly from a Python-parsed tree (stdlib `ElementTree`/
|
|
214
|
+
`html.parser` — see dom_lite.py) rather than a real browser DOM; see
|
|
215
|
+
dom_lite.js's header comment for exactly what subset of the DOM this
|
|
216
|
+
provides. Raises `RuntimeError` if the `mini-racer` package is missing,
|
|
217
|
+
if `text` isn't well-formed enough to parse as `fmt` (xml only — HTML5
|
|
218
|
+
parsing never rejects anything, matching a real browser's own lenient
|
|
219
|
+
behavior), or if the interpreter itself fails (throws, or has no
|
|
220
|
+
matching function for `op`)."""
|
|
221
|
+
if not _mini_racer_available():
|
|
222
|
+
raise RuntimeError(
|
|
223
|
+
"structile: this needs the 'mini-racer' package (run `pip install "
|
|
224
|
+
"mini-racer`) — an optional capability, not a hard dependency of "
|
|
225
|
+
'structile itself. See README.md\'s "Custom formats via an '
|
|
226
|
+
'interpreter" section.'
|
|
227
|
+
)
|
|
228
|
+
from py_mini_racer import JSEvalException, MiniRacer
|
|
229
|
+
|
|
230
|
+
is_markup = fmt in ("xml", "html")
|
|
231
|
+
doc_payload = build_document_payload(text, fmt) if is_markup and op == "parse" else None
|
|
232
|
+
marked_value_json = json.dumps(_mark_sets(value)) if op == "serialize" else None
|
|
233
|
+
dom_lite_js = _DOM_LITE_JS_PATH.read_text(encoding="utf-8")
|
|
234
|
+
|
|
235
|
+
# Mirrors buildInterpreterFns's function-extraction in
|
|
236
|
+
# structile.html, and (before it) convert_runner.js's `new
|
|
237
|
+
# Function(...)` call — same execution shape regardless of which of the
|
|
238
|
+
# three actually drives an interpreter script. The whole thing is one
|
|
239
|
+
# try/catch so an interpreter's own thrown Error (the "fail loudly on
|
|
240
|
+
# the wrong shape" contract every bundled interpreter documents) comes
|
|
241
|
+
# back as a clean {ok:false, error} instead of a noisy JS stack trace.
|
|
242
|
+
driver = f"""
|
|
243
|
+
(function(){{
|
|
244
|
+
try {{
|
|
245
|
+
var __structileFns = {{
|
|
246
|
+
interpretXML: (typeof interpretXML === "function") ? interpretXML : null,
|
|
247
|
+
serializeXML: (typeof serializeXML === "function") ? serializeXML : null,
|
|
248
|
+
interpretText: (typeof interpretText === "function") ? interpretText : null,
|
|
249
|
+
serializeText: (typeof serializeText === "function") ? serializeText : null,
|
|
250
|
+
}};
|
|
251
|
+
var __structileResult;
|
|
252
|
+
if ({json.dumps(op)} === "parse") {{
|
|
253
|
+
if ({json.dumps(is_markup)}) {{
|
|
254
|
+
if (!__structileFns.interpretXML) throw new Error("Interpreter script must define function interpretXML(xmlDocument)");
|
|
255
|
+
__structileResult = __structileFns.interpretXML(__structileBuildDocument({json.dumps(doc_payload)}));
|
|
256
|
+
}} else {{
|
|
257
|
+
if (!__structileFns.interpretText) throw new Error("Interpreter script must define function interpretText(text)");
|
|
258
|
+
__structileResult = __structileFns.interpretText({json.dumps(text)});
|
|
259
|
+
}}
|
|
260
|
+
return JSON.stringify({{ ok: true, value: __structileMarkSets(__structileResult) }});
|
|
261
|
+
}} else {{
|
|
262
|
+
var __structileValue = __structileUnmarkSets(JSON.parse({json.dumps(marked_value_json)}));
|
|
263
|
+
if ({json.dumps(is_markup)}) {{
|
|
264
|
+
if (!__structileFns.serializeXML) throw new Error("Interpreter script has no serializeXML — can't serialize to this format");
|
|
265
|
+
__structileResult = __structileFns.serializeXML(__structileValue);
|
|
266
|
+
}} else {{
|
|
267
|
+
if (!__structileFns.serializeText) throw new Error("Interpreter script has no serializeText — can't serialize to this format");
|
|
268
|
+
__structileResult = __structileFns.serializeText(__structileValue);
|
|
269
|
+
}}
|
|
270
|
+
return JSON.stringify({{ ok: true, value: __structileResult }});
|
|
271
|
+
}}
|
|
272
|
+
}} catch (e) {{
|
|
273
|
+
return JSON.stringify({{ ok: false, error: String((e && e.message) || e) }});
|
|
274
|
+
}}
|
|
275
|
+
}})()
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
logger.debug("structile: running interpreter via mini-racer (%s, format=%r)", op, fmt)
|
|
279
|
+
mr = MiniRacer() # fresh isolate per call — candidates never share JS state
|
|
280
|
+
try:
|
|
281
|
+
raw = mr.eval(dom_lite_js + "\n" + interpreter_source + "\n" + driver)
|
|
282
|
+
except JSEvalException as e:
|
|
283
|
+
raise RuntimeError(f"structile: interpreter script threw while loading: {e}") from e
|
|
284
|
+
outcome = json.loads(raw)
|
|
285
|
+
if not outcome["ok"]:
|
|
286
|
+
raise RuntimeError(outcome["error"])
|
|
287
|
+
return _unmark_sets(outcome["value"]) if op == "parse" else outcome["value"]
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@dataclass
|
|
291
|
+
class SelectedInterpreter:
|
|
292
|
+
candidate: InterpreterCandidate
|
|
293
|
+
source: str
|
|
294
|
+
result: Any # the parsed/serialized value if it was actually run to select it, else None
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def select_interpreter(
|
|
298
|
+
candidates: Sequence[InterpreterCandidate],
|
|
299
|
+
fmt: str,
|
|
300
|
+
*,
|
|
301
|
+
op: str = "parse",
|
|
302
|
+
text: Optional[str] = None,
|
|
303
|
+
value: Any = None,
|
|
304
|
+
run: bool = True,
|
|
305
|
+
) -> SelectedInterpreter:
|
|
306
|
+
"""Pick the interpreter that actually handles this data out of
|
|
307
|
+
`candidates`, tried in order — the first one that doesn't raise wins
|
|
308
|
+
(see interpreters/generic_xml.js's header comment for why a *correct*
|
|
309
|
+
interpreter is expected to throw on the wrong shape rather than return
|
|
310
|
+
something meaningless).
|
|
311
|
+
|
|
312
|
+
A single candidate is returned untested when `run=False` — the
|
|
313
|
+
`open()` `widget`/`browser`/`file` renderers only ever forward
|
|
314
|
+
the interpreter source to the viewer, which does the real parse
|
|
315
|
+
client-side, so with nothing to disambiguate there's no reason to run
|
|
316
|
+
anything here either. `convert()` always needs the actual parsed/
|
|
317
|
+
serialized Python value back, so it passes `run=True` unconditionally —
|
|
318
|
+
even a single candidate has to actually run there. Whenever there's
|
|
319
|
+
more than one candidate, disambiguating them always requires actually
|
|
320
|
+
running each in turn (via mini-racer), regardless of `run`.
|
|
321
|
+
|
|
322
|
+
Raises `ValueError` if `candidates` is empty, or `RuntimeError` (after
|
|
323
|
+
trying every candidate, and naming how each one failed) if none of them
|
|
324
|
+
accept the data.
|
|
325
|
+
"""
|
|
326
|
+
if not candidates:
|
|
327
|
+
raise ValueError(
|
|
328
|
+
f"structile: no interpreter registered or provided for {fmt!r} data — pass interpreter=, "
|
|
329
|
+
"or call register_interpreter(ext, ...) once at import time"
|
|
330
|
+
)
|
|
331
|
+
if len(candidates) == 1 and not run:
|
|
332
|
+
source = read_interpreter(candidates[0])
|
|
333
|
+
logger.debug("structile: single interpreter candidate for %r, using it without testing", fmt)
|
|
334
|
+
return SelectedInterpreter(candidate=candidates[0], source=source, result=None)
|
|
335
|
+
|
|
336
|
+
# From here on every candidate actually has to run (run=True, or more
|
|
337
|
+
# than one candidate needs disambiguating) — checked once up front so a
|
|
338
|
+
# missing 'mini-racer' package surfaces as ONE clear error, not this same
|
|
339
|
+
# message repeated (and then buried in an aggregate "none of N worked")
|
|
340
|
+
# once per candidate as run_interpreter_source's own identical check
|
|
341
|
+
# would give.
|
|
342
|
+
if not _mini_racer_available():
|
|
343
|
+
reason = (
|
|
344
|
+
f"choosing between {len(candidates)} interpreter candidates"
|
|
345
|
+
if len(candidates) > 1
|
|
346
|
+
else "running this interpreter"
|
|
347
|
+
)
|
|
348
|
+
raise RuntimeError(
|
|
349
|
+
f"structile: {reason} for {fmt!r} data needs the 'mini-racer' package "
|
|
350
|
+
"(run `pip install mini-racer`) — an optional capability, not a hard "
|
|
351
|
+
'dependency of structile itself. See README.md\'s "Custom formats via '
|
|
352
|
+
'an interpreter" section.'
|
|
353
|
+
)
|
|
354
|
+
if len(candidates) > 1:
|
|
355
|
+
logger.debug("structile: %d interpreter candidates for %r, trying each in order", len(candidates), fmt)
|
|
356
|
+
|
|
357
|
+
errors = []
|
|
358
|
+
for i, candidate in enumerate(candidates):
|
|
359
|
+
source = read_interpreter(candidate)
|
|
360
|
+
try:
|
|
361
|
+
result = run_interpreter_source(op, source, fmt, text=text, value=value)
|
|
362
|
+
except RuntimeError as e:
|
|
363
|
+
logger.warning("structile: interpreter candidate %r failed (%s), trying the next one", candidate, e)
|
|
364
|
+
errors.append(f"{candidate}: {e}")
|
|
365
|
+
continue
|
|
366
|
+
if len(candidates) > 1:
|
|
367
|
+
logger.info("structile: selected interpreter candidate %r (%d of %d)", candidate, i + 1, len(candidates))
|
|
368
|
+
return SelectedInterpreter(candidate=candidate, source=source, result=result)
|
|
369
|
+
|
|
370
|
+
logger.error("structile: no interpreter candidate could handle this %r data (tried %d)", fmt, len(candidates))
|
|
371
|
+
raise RuntimeError(
|
|
372
|
+
f"structile: none of {len(candidates)} interpreter candidate(s) could handle this {fmt} data:\n"
|
|
373
|
+
+ "\n".join(f" - {e}" for e in errors)
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def select_interpreter_any_format(
|
|
378
|
+
interpreter_dict: Dict[str, InterpreterCandidateOrList],
|
|
379
|
+
*,
|
|
380
|
+
op: str = "parse",
|
|
381
|
+
text: Optional[str] = None,
|
|
382
|
+
value: Any = None,
|
|
383
|
+
) -> tuple[str, SelectedInterpreter]:
|
|
384
|
+
"""Like `select_interpreter`, but for when the *format* itself isn't
|
|
385
|
+
known ahead of time either — no `format=`, no path to infer an
|
|
386
|
+
extension from. Tries every `(ext, candidates)` entry in
|
|
387
|
+
`interpreter_dict` in turn (each under its own format — `MARKUP_FORMATS`'
|
|
388
|
+
DOM mode for a markup extension, or the extension itself for anything
|
|
389
|
+
else, e.g. `.ini` -> `"ini"`), stopping at the first candidate — from
|
|
390
|
+
any entry — that doesn't raise. Returns `(fmt, SelectedInterpreter)`
|
|
391
|
+
since the winning format wasn't known ahead of time.
|
|
392
|
+
|
|
393
|
+
Always actually runs candidates (needs the 'mini-racer' package) — unlike
|
|
394
|
+
`select_interpreter`'s `run=False` escape hatch (a single, *known*-
|
|
395
|
+
format candidate can be forwarded to a browser untested, since there's
|
|
396
|
+
nothing to disambiguate), determining which of several *formats* even
|
|
397
|
+
applies is inherently a "parse it to find out" question, so there's no
|
|
398
|
+
untested shortcut here.
|
|
399
|
+
|
|
400
|
+
Raises `ValueError` if `interpreter_dict` is empty, `RuntimeError`
|
|
401
|
+
(naming every attempt, across every extension) if nothing in it can
|
|
402
|
+
handle the data.
|
|
403
|
+
"""
|
|
404
|
+
if not interpreter_dict:
|
|
405
|
+
raise ValueError(
|
|
406
|
+
"structile: interpreter dict is empty — nothing to try (pass at least one "
|
|
407
|
+
'{"ext": interpreter} entry)'
|
|
408
|
+
)
|
|
409
|
+
errors: List[str] = []
|
|
410
|
+
tried_any = False
|
|
411
|
+
for ext, spec in interpreter_dict.items():
|
|
412
|
+
ext_norm = _normalize_ext(ext)
|
|
413
|
+
fmt = MARKUP_FORMATS.get(ext_norm) or ext_norm.lstrip(".")
|
|
414
|
+
candidates = resolve_candidates(spec, ext_norm)
|
|
415
|
+
if not candidates:
|
|
416
|
+
continue
|
|
417
|
+
tried_any = True
|
|
418
|
+
try:
|
|
419
|
+
selected = select_interpreter(candidates, fmt, op=op, text=text, value=value, run=True)
|
|
420
|
+
except (ValueError, RuntimeError) as e:
|
|
421
|
+
errors.append(f"{ext} ({fmt}): {e}")
|
|
422
|
+
continue
|
|
423
|
+
return fmt, selected
|
|
424
|
+
if not tried_any and not errors:
|
|
425
|
+
raise ValueError(f"structile: no usable interpreter entries in {interpreter_dict!r}")
|
|
426
|
+
logger.error("structile: no interpreter (across %d extension(s)) could handle this data", len(interpreter_dict))
|
|
427
|
+
raise RuntimeError(
|
|
428
|
+
f"structile: none of the provided interpreters (across {len(interpreter_dict)} extension(s)) "
|
|
429
|
+
"could handle this data:\n" + "\n".join(f" - {e}" for e in errors)
|
|
430
|
+
)
|