anyplotlib 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.
- anyplotlib/__init__.py +53 -0
- anyplotlib/_base_plot.py +229 -0
- anyplotlib/_electron.py +74 -0
- anyplotlib/_repr_utils.py +345 -0
- anyplotlib/_utils.py +194 -0
- anyplotlib/axes/__init__.py +6 -0
- anyplotlib/axes/_axes.py +510 -0
- anyplotlib/axes/_inset_axes.py +126 -0
- anyplotlib/callbacks.py +328 -0
- anyplotlib/embed.py +194 -0
- anyplotlib/figure/__init__.py +7 -0
- anyplotlib/figure/_figure.py +633 -0
- anyplotlib/figure/_gridspec.py +99 -0
- anyplotlib/figure/_subplots.py +100 -0
- anyplotlib/figure_esm.js +6011 -0
- anyplotlib/markers.py +704 -0
- anyplotlib/plot1d/__init__.py +6 -0
- anyplotlib/plot1d/_plot1d.py +1376 -0
- anyplotlib/plot1d/_plotbar.py +436 -0
- anyplotlib/plot2d/__init__.py +5 -0
- anyplotlib/plot2d/_plot2d.py +726 -0
- anyplotlib/plot2d/_plotmesh.py +116 -0
- anyplotlib/plot3d/__init__.py +4 -0
- anyplotlib/plot3d/_plot3d.py +524 -0
- anyplotlib/plotxy/__init__.py +4 -0
- anyplotlib/plotxy/_plotxy.py +273 -0
- anyplotlib/sphinx_anywidget/__init__.py +177 -0
- anyplotlib/sphinx_anywidget/_directive.py +245 -0
- anyplotlib/sphinx_anywidget/_repr_utils.py +298 -0
- anyplotlib/sphinx_anywidget/_scraper.py +390 -0
- anyplotlib/sphinx_anywidget/_wheel_builder.py +84 -0
- anyplotlib/sphinx_anywidget/static/anywidget_bridge.js +1099 -0
- anyplotlib/sphinx_anywidget/static/anywidget_overlay.css +100 -0
- anyplotlib/widgets/__init__.py +18 -0
- anyplotlib/widgets/_base.py +218 -0
- anyplotlib/widgets/_widgets1d.py +109 -0
- anyplotlib/widgets/_widgets2d.py +141 -0
- anyplotlib/widgets/_widgets3d.py +50 -0
- anyplotlib-0.1.0.dist-info/METADATA +160 -0
- anyplotlib-0.1.0.dist-info/RECORD +42 -0
- anyplotlib-0.1.0.dist-info/WHEEL +4 -0
- anyplotlib-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1099 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* anywidget_bridge.js
|
|
3
|
+
*
|
|
4
|
+
* Generic Pyodide bridge for anywidget-based interactive documentation.
|
|
5
|
+
*
|
|
6
|
+
* Architecture
|
|
7
|
+
* ────────────
|
|
8
|
+
* Parent page (this script)
|
|
9
|
+
* ├─ Per-figure ⚡ badge (in .awi-badge div, rendered by _scraper.py)
|
|
10
|
+
* ├─ Pyodide WASM runtime (loaded once from CDN on first ⚡ click)
|
|
11
|
+
* ├─ Package wheel at _static/wheels/{pkg}-0.0.0-py3-none-any.whl
|
|
12
|
+
* ├─ <script type="text/x-python" data-fig-id="…"> — example source
|
|
13
|
+
* └─ <iframe data-awi-fig="…" src="…_static/viewer_widgets/….html">
|
|
14
|
+
* anywidget ESM renderer + postMessage listener
|
|
15
|
+
*
|
|
16
|
+
* Data flow after activation
|
|
17
|
+
* ──────────────────────────
|
|
18
|
+
* Python setattr(widget, key, val) [any sync=True traitlet]
|
|
19
|
+
* → traitlets fires observe() on monkey-patched AnyWidget.__init__ observer
|
|
20
|
+
* → observer calls js.window._anywidgetPush(fig_id, key, val_str)
|
|
21
|
+
* → iframe.contentWindow.postMessage({type:'awi_state', key, value})
|
|
22
|
+
* → model.set(key, val) + save_changes() → JS re-renders
|
|
23
|
+
*
|
|
24
|
+
* user interaction → iframe save_changes() forwards awi_event to parent
|
|
25
|
+
* → window 'message' listener → pyodide dispatch
|
|
26
|
+
* → if hasattr(widget, '_dispatch_event'): widget._dispatch_event(data)
|
|
27
|
+
* → Python on_release / on_change callbacks
|
|
28
|
+
*
|
|
29
|
+
* Monkey-patch (installed in Pyodide after anywidget stub is ready)
|
|
30
|
+
* ─────────────────────────────────────────────────────────────────
|
|
31
|
+
* _aw.AnyWidget.__init__ is wrapped to add a traitlets.observe(names=All)
|
|
32
|
+
* observer to every new widget instance. The observer fires for every trait
|
|
33
|
+
* change; it filters to sync=True traits and calls _anywidgetPush when
|
|
34
|
+
* _anywidget_fig_id is set on the instance. Dynamic traits added via
|
|
35
|
+
* add_traits() are automatically covered by the All sentinel.
|
|
36
|
+
*
|
|
37
|
+
* Wheel URL resolution
|
|
38
|
+
* ────────────────────
|
|
39
|
+
* The wheel lives at _static/wheels/{pkg}-0.0.0-py3-none-any.whl relative
|
|
40
|
+
* to the docs root. _DOCS_ROOT is derived from document.currentScript.src
|
|
41
|
+
* so the URL is always version-correct for every deployed copy of the docs.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
(function () {
|
|
45
|
+
'use strict';
|
|
46
|
+
|
|
47
|
+
const PYODIDE_VERSION = '0.27.5';
|
|
48
|
+
const PYODIDE_CDN = `https://cdn.jsdelivr.net/pyodide/v${PYODIDE_VERSION}/full/`;
|
|
49
|
+
|
|
50
|
+
// Derives the docs root from this script's own URL so wheel references are
|
|
51
|
+
// always version-specific (dev vs v0.1.0, etc.)
|
|
52
|
+
const _SCRIPT_SRC = (document.currentScript || {}).src || '';
|
|
53
|
+
const _DOCS_ROOT = _SCRIPT_SRC.replace(/_static\/anywidget_bridge\.js.*$/, '') || './';
|
|
54
|
+
|
|
55
|
+
// One shared Pyodide instance for the whole page, resolved once on first boot.
|
|
56
|
+
let _pyodidePromise = null;
|
|
57
|
+
|
|
58
|
+
// ── helpers ──────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
/** True when the page has at least one interactive anywidget figure. */
|
|
61
|
+
function _hasInteractiveFigures() {
|
|
62
|
+
return document.querySelector('button.awi-activate-btn') !== null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Deliver a state-update message into the iframe for figId.
|
|
66
|
+
*
|
|
67
|
+
* rawValue is always a JSON-encoded string (Python serialises every trait
|
|
68
|
+
* with json.dumps so numeric/boolean/object traits are not type-erased when
|
|
69
|
+
* the iframe receives them).
|
|
70
|
+
*/
|
|
71
|
+
function _postToIframe(figId, key, rawValue) {
|
|
72
|
+
const iframe = document.querySelector(`iframe[data-awi-fig="${figId}"]`);
|
|
73
|
+
if (iframe && iframe.contentWindow) {
|
|
74
|
+
// JSON.parse recovers the real JS type (number, bool, array, object …).
|
|
75
|
+
// Plain Python strings are also JSON-encoded (quoted), so they round-trip
|
|
76
|
+
// correctly too.
|
|
77
|
+
let value = rawValue;
|
|
78
|
+
try { value = JSON.parse(rawValue); } catch (_) {}
|
|
79
|
+
iframe.contentWindow.postMessage({ type: 'awi_state', key, value }, '*');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── badge state management ────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
function _allActivateBtns() {
|
|
86
|
+
return Array.from(document.querySelectorAll('button.awi-activate-btn'));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function _setBadgesLoading() {
|
|
90
|
+
for (const btn of _allActivateBtns()) {
|
|
91
|
+
btn.textContent = '⏳';
|
|
92
|
+
btn.dataset.state = 'loading';
|
|
93
|
+
btn.title = 'Loading Pyodide…';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function _setBadgesActive() {
|
|
98
|
+
for (const btn of _allActivateBtns()) {
|
|
99
|
+
btn.textContent = '✓';
|
|
100
|
+
btn.dataset.state = 'active';
|
|
101
|
+
btn.title = 'Python active';
|
|
102
|
+
const wrap = btn.closest('.awi-fig-wrap');
|
|
103
|
+
if (wrap) wrap.dataset.awiLive = 'true';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function _setBadgesError(err) {
|
|
108
|
+
const msg = String(err.message || err);
|
|
109
|
+
const lines = msg.split('\n').map(l => l.trim()).filter(Boolean);
|
|
110
|
+
const summary = lines.length > 1 ? lines[lines.length - 1] : (lines[0] || msg);
|
|
111
|
+
for (const btn of _allActivateBtns()) {
|
|
112
|
+
btn.textContent = '❌';
|
|
113
|
+
btn.dataset.state = 'error';
|
|
114
|
+
btn.title = `Boot failed: ${summary}\n\n${msg}`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Pyodide bootstrap ─────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
function _boot() {
|
|
121
|
+
if (_pyodidePromise) return _pyodidePromise;
|
|
122
|
+
_pyodidePromise = _doBoot().catch(err => {
|
|
123
|
+
_pyodidePromise = null; // allow retry
|
|
124
|
+
throw err;
|
|
125
|
+
});
|
|
126
|
+
return _pyodidePromise;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function _doBoot() {
|
|
130
|
+
// ── file:// guard ─────────────────────────────────────────────────────
|
|
131
|
+
if (window.location.protocol === 'file:') {
|
|
132
|
+
throw new Error(
|
|
133
|
+
'Serve docs over HTTP — run: python -m http.server -d build/html 8080'
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function _step(label, promise) {
|
|
138
|
+
return promise.catch(e => {
|
|
139
|
+
throw new Error('[' + label + '] ' + (e.message || String(e)));
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 1. Load pyodide.js from CDN
|
|
144
|
+
if (typeof loadPyodide === 'undefined') {
|
|
145
|
+
await _step('load pyodide.js', new Promise((resolve, reject) => {
|
|
146
|
+
const s = document.createElement('script');
|
|
147
|
+
s.src = PYODIDE_CDN + 'pyodide.js';
|
|
148
|
+
s.onload = resolve;
|
|
149
|
+
s.onerror = () => reject(new Error('Failed to load pyodide.js from CDN'));
|
|
150
|
+
document.head.appendChild(s);
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
console.info('[sphinx_anywidget] pyodide.js ready');
|
|
154
|
+
|
|
155
|
+
// 2. Initialise Pyodide
|
|
156
|
+
const pyodide = await _step('init pyodide',
|
|
157
|
+
loadPyodide({ indexURL: PYODIDE_CDN }));
|
|
158
|
+
console.info('[sphinx_anywidget] Pyodide initialised');
|
|
159
|
+
|
|
160
|
+
// 3. Load micropip + numpy
|
|
161
|
+
await _step('load micropip+numpy',
|
|
162
|
+
pyodide.loadPackage(['micropip', 'numpy']));
|
|
163
|
+
console.info('[sphinx_anywidget] micropip + numpy loaded');
|
|
164
|
+
|
|
165
|
+
// 4. Install pure-Python deps
|
|
166
|
+
await _step('install traitlets/colorcet',
|
|
167
|
+
pyodide.runPythonAsync(`
|
|
168
|
+
import micropip
|
|
169
|
+
await micropip.install(['traitlets', 'colorcet', 'toolz'])
|
|
170
|
+
`));
|
|
171
|
+
console.info('[sphinx_anywidget] traitlets + colorcet installed');
|
|
172
|
+
|
|
173
|
+
// 5. Stub anywidget in sys.modules
|
|
174
|
+
await _step('stub anywidget',
|
|
175
|
+
pyodide.runPythonAsync(`
|
|
176
|
+
import sys, traitlets as _tr
|
|
177
|
+
|
|
178
|
+
class _AnyWidget(_tr.HasTraits):
|
|
179
|
+
_esm = _tr.Unicode("").tag(sync=False)
|
|
180
|
+
_css = _tr.Unicode("").tag(sync=False)
|
|
181
|
+
def __init__(self, **kw):
|
|
182
|
+
super().__init__(**kw)
|
|
183
|
+
|
|
184
|
+
class _AnyWidgetMod:
|
|
185
|
+
AnyWidget = _AnyWidget
|
|
186
|
+
|
|
187
|
+
sys.modules['anywidget'] = _AnyWidgetMod()
|
|
188
|
+
_AWI_REGISTRY = {} # fig_id → widget instance
|
|
189
|
+
|
|
190
|
+
# Pre-compiled interaction-event dispatcher. The JS message handler calls
|
|
191
|
+
# this proxy DIRECTLY per frame instead of pyodide.runPythonAsync(code-string)
|
|
192
|
+
# — recompiling a code string every event costs ~1.2 ms in WASM (vs ~0.01 ms
|
|
193
|
+
# to call a ready function), which is the dominant per-frame cost of the
|
|
194
|
+
# Pyodide interaction path on a drag (30-60 events/sec).
|
|
195
|
+
def _awi_dispatch(fig_id, data):
|
|
196
|
+
_w = _AWI_REGISTRY.get(fig_id)
|
|
197
|
+
if _w is not None and hasattr(_w, '_dispatch_event'):
|
|
198
|
+
_w._dispatch_event(data)
|
|
199
|
+
`));
|
|
200
|
+
console.info('[sphinx_anywidget] anywidget stub installed');
|
|
201
|
+
|
|
202
|
+
// 5.5 Stub traits in sys.modules using traitlets as the backend.
|
|
203
|
+
// traits (from Enthought) has a C extension (ctraits) so it can't be
|
|
204
|
+
// installed via micropip in Pyodide. HyperSpy uses traits for HasTraits,
|
|
205
|
+
// trait types (CBool, Str, Enum, …) and Undefined — all of which have
|
|
206
|
+
// close equivalents in traitlets (the IPython fork of traits). We
|
|
207
|
+
// install shim modules for traits, traits.api, traits.trait_errors, and
|
|
208
|
+
// traits.trait_numeric before the HyperSpy wheel is installed so that
|
|
209
|
+
// micropip's dep-resolution never even sees traits as a requirement.
|
|
210
|
+
await _step('stub traits',
|
|
211
|
+
pyodide.runPythonAsync(`
|
|
212
|
+
import sys
|
|
213
|
+
import traitlets as _tr
|
|
214
|
+
|
|
215
|
+
# ── Patch traits-only methods onto traitlets.HasTraits ────────────────
|
|
216
|
+
# traits.HasTraits has trait_get() / trait_set() / trait_names() etc.
|
|
217
|
+
# that traitlets dropped or renamed. Patch them in so HyperSpy modules
|
|
218
|
+
# that call e.g. obj.trait_get() work transparently.
|
|
219
|
+
#
|
|
220
|
+
# Also patch __init__ to auto-register _{name}_changed methods as
|
|
221
|
+
# traitlets observers (traits magic that traitlets doesn't replicate).
|
|
222
|
+
_orig_HasTraits_init = _tr.HasTraits.__init__
|
|
223
|
+
|
|
224
|
+
def _patched_HasTraits_init(self, *args, **kwargs):
|
|
225
|
+
# Call the original traitlets init first so that the object is fully
|
|
226
|
+
# initialised before we wire any trait-change observers. traits wires
|
|
227
|
+
# _{name}_changed observers only after __init__ completes; traitlets
|
|
228
|
+
# would fire them mid-init, so we defer registration to after init.
|
|
229
|
+
_orig_HasTraits_init(self, *args, **kwargs)
|
|
230
|
+
# Now register _{name}_changed auto-observers.
|
|
231
|
+
cls = type(self)
|
|
232
|
+
import inspect as _inspect
|
|
233
|
+
for attr_name in dir(cls):
|
|
234
|
+
if not attr_name.endswith('_changed'):
|
|
235
|
+
continue
|
|
236
|
+
inner = attr_name[1:-8]
|
|
237
|
+
if not inner:
|
|
238
|
+
continue
|
|
239
|
+
if inner in cls.class_traits():
|
|
240
|
+
method = getattr(cls, attr_name)
|
|
241
|
+
if callable(method) and not isinstance(method, property):
|
|
242
|
+
try:
|
|
243
|
+
sig = _inspect.signature(method)
|
|
244
|
+
# Exclude 'self' from count
|
|
245
|
+
nparams = len([p for p in sig.parameters.values()
|
|
246
|
+
if p.name != 'self'
|
|
247
|
+
and p.default is _inspect.Parameter.empty
|
|
248
|
+
and p.kind not in (_inspect.Parameter.VAR_POSITIONAL,
|
|
249
|
+
_inspect.Parameter.VAR_KEYWORD)])
|
|
250
|
+
except (ValueError, TypeError):
|
|
251
|
+
nparams = 3
|
|
252
|
+
def _make_bridge(m, np):
|
|
253
|
+
def _bridge(change):
|
|
254
|
+
if np == 0:
|
|
255
|
+
m(self)
|
|
256
|
+
elif np == 1:
|
|
257
|
+
m(self, change['new'])
|
|
258
|
+
elif np == 2:
|
|
259
|
+
m(self, change['name'], change['new'])
|
|
260
|
+
else:
|
|
261
|
+
m(self, change['name'], change['old'], change['new'])
|
|
262
|
+
return _bridge
|
|
263
|
+
self.observe(_make_bridge(method, nparams), names=[inner])
|
|
264
|
+
|
|
265
|
+
_tr.HasTraits.__init__ = _patched_HasTraits_init
|
|
266
|
+
|
|
267
|
+
def _trait_get(self, *names):
|
|
268
|
+
"""Return a dict of trait name → current value (traits API)."""
|
|
269
|
+
if names:
|
|
270
|
+
return {n: getattr(self, n) for n in names if n in self.traits()}
|
|
271
|
+
return {n: getattr(self, n) for n in self.traits()}
|
|
272
|
+
|
|
273
|
+
def _trait_set(self, trait_change_notify=True, **kw):
|
|
274
|
+
"""Set multiple traits at once (traits API). Silently skip coercion
|
|
275
|
+
failures — traits does auto-coerce (CFloat accepts '7.5') but traitlets
|
|
276
|
+
does not; skip rather than crash for preference loading."""
|
|
277
|
+
for k, v in kw.items():
|
|
278
|
+
if k not in self.traits():
|
|
279
|
+
continue
|
|
280
|
+
# Try the value as-is; if validation fails try common coercions.
|
|
281
|
+
for candidate in (v, _coerce_trait_value(self.traits()[k], v)):
|
|
282
|
+
try:
|
|
283
|
+
setattr(self, k, candidate)
|
|
284
|
+
break
|
|
285
|
+
except Exception:
|
|
286
|
+
pass
|
|
287
|
+
|
|
288
|
+
def _coerce_trait_value(trait, v):
|
|
289
|
+
"""Best-effort coercion of a string config value to the trait's type."""
|
|
290
|
+
import traitlets as _tr2
|
|
291
|
+
if isinstance(v, str):
|
|
292
|
+
if isinstance(trait, _tr2.Bool):
|
|
293
|
+
return v.lower() not in ('false', '0', 'no', '')
|
|
294
|
+
if isinstance(trait, (_tr2.Int,)):
|
|
295
|
+
try: return int(v)
|
|
296
|
+
except: pass
|
|
297
|
+
if isinstance(trait, (_tr2.Float,)):
|
|
298
|
+
try: return float(v)
|
|
299
|
+
except: pass
|
|
300
|
+
return v
|
|
301
|
+
|
|
302
|
+
def _trait_names(self):
|
|
303
|
+
return list(self.traits().keys())
|
|
304
|
+
|
|
305
|
+
def _get(self, *names):
|
|
306
|
+
return _trait_get(self, *names)
|
|
307
|
+
|
|
308
|
+
def _on_trait_change(self, handler, trait_name, remove=False):
|
|
309
|
+
"""Register or remove a traits-style observer.
|
|
310
|
+
|
|
311
|
+
Handles dotted paths like "_axes.slice" (traits list-item observation):
|
|
312
|
+
watches each item in the list trait for changes to the child attribute,
|
|
313
|
+
and re-wires when the list itself changes.
|
|
314
|
+
"""
|
|
315
|
+
import inspect
|
|
316
|
+
try:
|
|
317
|
+
sig = inspect.signature(handler)
|
|
318
|
+
nparams = len([p for p in sig.parameters.values()
|
|
319
|
+
if p.default is inspect.Parameter.empty
|
|
320
|
+
and p.kind not in (inspect.Parameter.VAR_POSITIONAL,
|
|
321
|
+
inspect.Parameter.VAR_KEYWORD)])
|
|
322
|
+
except (ValueError, TypeError):
|
|
323
|
+
nparams = 3
|
|
324
|
+
|
|
325
|
+
def _call_handler(change):
|
|
326
|
+
if nparams == 0:
|
|
327
|
+
handler()
|
|
328
|
+
elif nparams == 1:
|
|
329
|
+
handler(change['new'])
|
|
330
|
+
elif nparams == 2:
|
|
331
|
+
handler(change['name'], change['new'])
|
|
332
|
+
else:
|
|
333
|
+
handler(change['name'], change['old'], change['new'])
|
|
334
|
+
|
|
335
|
+
if not hasattr(self, '_oti_bridges'):
|
|
336
|
+
self._oti_bridges = {}
|
|
337
|
+
if not hasattr(self, '_oti_item_bridges'):
|
|
338
|
+
self._oti_item_bridges = {}
|
|
339
|
+
|
|
340
|
+
if '.' in trait_name:
|
|
341
|
+
# dotted path: "list_trait.child_attr"
|
|
342
|
+
list_trait, child_attr = trait_name.split('.', 1)
|
|
343
|
+
|
|
344
|
+
key = (id(handler), trait_name)
|
|
345
|
+
if remove:
|
|
346
|
+
# unobserve child bridges already registered
|
|
347
|
+
for item_bridge, item in self._oti_item_bridges.pop(key, []):
|
|
348
|
+
try:
|
|
349
|
+
item.unobserve(item_bridge, names=[child_attr])
|
|
350
|
+
except Exception:
|
|
351
|
+
pass
|
|
352
|
+
list_bridge = self._oti_bridges.pop(key, None)
|
|
353
|
+
if list_bridge:
|
|
354
|
+
try:
|
|
355
|
+
self.unobserve(list_bridge, names=[list_trait])
|
|
356
|
+
except Exception:
|
|
357
|
+
pass
|
|
358
|
+
return
|
|
359
|
+
|
|
360
|
+
# Track per-item bridges so we can remove them
|
|
361
|
+
self._oti_item_bridges[key] = []
|
|
362
|
+
|
|
363
|
+
def _observe_item(item):
|
|
364
|
+
def _item_bridge(change):
|
|
365
|
+
_call_handler(change)
|
|
366
|
+
_item_bridge.__name__ = getattr(handler, '__name__', 'handler') + '_item'
|
|
367
|
+
item.observe(_item_bridge, names=[child_attr])
|
|
368
|
+
self._oti_item_bridges[key].append((_item_bridge, item))
|
|
369
|
+
|
|
370
|
+
# Observe existing items now
|
|
371
|
+
existing = getattr(self, list_trait, None)
|
|
372
|
+
if existing:
|
|
373
|
+
for item in existing:
|
|
374
|
+
if hasattr(item, 'observe'):
|
|
375
|
+
_observe_item(item)
|
|
376
|
+
|
|
377
|
+
# Watch the list for new items being added
|
|
378
|
+
def _list_changed(change):
|
|
379
|
+
new_list = change.get('new', []) or []
|
|
380
|
+
old_list = change.get('old', []) or []
|
|
381
|
+
added = [x for x in new_list if x not in old_list]
|
|
382
|
+
for item in added:
|
|
383
|
+
if hasattr(item, 'observe'):
|
|
384
|
+
_observe_item(item)
|
|
385
|
+
# Also call handler when list changes (slice assignment etc.)
|
|
386
|
+
_call_handler(change)
|
|
387
|
+
|
|
388
|
+
_list_changed.__name__ = getattr(handler, '__name__', 'handler') + '_list'
|
|
389
|
+
self.observe(_list_changed, names=[list_trait])
|
|
390
|
+
self._oti_bridges[key] = _list_changed
|
|
391
|
+
else:
|
|
392
|
+
# simple trait name
|
|
393
|
+
def _bridge(change):
|
|
394
|
+
_call_handler(change)
|
|
395
|
+
_bridge.__name__ = getattr(handler, '__name__', 'handler')
|
|
396
|
+
key = (id(handler), trait_name)
|
|
397
|
+
if not remove:
|
|
398
|
+
self.observe(_bridge, names=[trait_name])
|
|
399
|
+
self._oti_bridges[key] = _bridge
|
|
400
|
+
else:
|
|
401
|
+
bridge = self._oti_bridges.pop(key, None)
|
|
402
|
+
if bridge:
|
|
403
|
+
try:
|
|
404
|
+
self.unobserve(bridge, names=[trait_name])
|
|
405
|
+
except Exception:
|
|
406
|
+
pass
|
|
407
|
+
|
|
408
|
+
if not hasattr(_tr.HasTraits, 'trait_get'):
|
|
409
|
+
_tr.HasTraits.trait_get = _trait_get
|
|
410
|
+
if not hasattr(_tr.HasTraits, 'trait_set'):
|
|
411
|
+
_tr.HasTraits.trait_set = _trait_set
|
|
412
|
+
if not hasattr(_tr.HasTraits, 'trait_names'):
|
|
413
|
+
_tr.HasTraits.trait_names = _trait_names
|
|
414
|
+
if not hasattr(_tr.HasTraits, 'get'):
|
|
415
|
+
_tr.HasTraits.get = _get
|
|
416
|
+
def _add_trait(self, name, trait_class_or_inst):
|
|
417
|
+
"""traits API: add_trait(name, TraitClass) — add a dynamic trait."""
|
|
418
|
+
if isinstance(trait_class_or_inst, type):
|
|
419
|
+
inst = trait_class_or_inst()
|
|
420
|
+
else:
|
|
421
|
+
inst = trait_class_or_inst
|
|
422
|
+
self.add_traits(**{name: inst})
|
|
423
|
+
|
|
424
|
+
if not hasattr(_tr.HasTraits, 'on_trait_change'):
|
|
425
|
+
_tr.HasTraits.on_trait_change = _on_trait_change
|
|
426
|
+
if not hasattr(_tr.HasTraits, 'add_trait'):
|
|
427
|
+
_tr.HasTraits.add_trait = _add_trait
|
|
428
|
+
|
|
429
|
+
# ── Undefined sentinel (traitlets already has one) ──────────────────
|
|
430
|
+
Undefined = _tr.Undefined
|
|
431
|
+
|
|
432
|
+
# ── TraitError ────────────────────────────────────────────────────────
|
|
433
|
+
TraitError = _tr.TraitError
|
|
434
|
+
|
|
435
|
+
# ── Trait descriptor factory ─────────────────────────────────────────
|
|
436
|
+
# traits.Property needs a callable that returns a descriptor. We use a
|
|
437
|
+
# simple traitlets.Any with optional validator/observer hooks.
|
|
438
|
+
class _Property(_tr.Any):
|
|
439
|
+
def __init__(self, *args, fget=None, fset=None, depends_on=None, **kw):
|
|
440
|
+
super().__init__(None, allow_none=True, **kw)
|
|
441
|
+
|
|
442
|
+
class _Event(_tr.Any):
|
|
443
|
+
"""Fires-and-forgets; observers receive the new value."""
|
|
444
|
+
def __init__(self, *args, **kw):
|
|
445
|
+
super().__init__(None, allow_none=True, **kw)
|
|
446
|
+
|
|
447
|
+
class _Button(_tr.Any):
|
|
448
|
+
def __init__(self, *args, **kw):
|
|
449
|
+
super().__init__(None, allow_none=True, **kw)
|
|
450
|
+
|
|
451
|
+
# ── Array placeholder ─────────────────────────────────────────────────
|
|
452
|
+
class _Array(_tr.Any):
|
|
453
|
+
def __init__(self, *args, **kw):
|
|
454
|
+
super().__init__(None, allow_none=True, **kw)
|
|
455
|
+
|
|
456
|
+
# ── Str: traits.Str accepts Undefined as a valid value (used as
|
|
457
|
+
# "not set" sentinel in HyperSpy axes). Use Any so any value works. ──
|
|
458
|
+
class _TraitsStr(_tr.Any):
|
|
459
|
+
def __init__(self, default_value='', **kw):
|
|
460
|
+
kw.setdefault('allow_none', True)
|
|
461
|
+
super().__init__(default_value, **kw)
|
|
462
|
+
|
|
463
|
+
# ── List: traits.List(SomeClass) means typed list; traitlets.List needs
|
|
464
|
+
# an Instance trait, not a class, as the first arg. ──────────────────
|
|
465
|
+
class _TraitsList(_tr.List):
|
|
466
|
+
def __init__(self, trait_or_type=None, value=None, **kw):
|
|
467
|
+
if isinstance(trait_or_type, type):
|
|
468
|
+
# traits API: List(SomeClass) — element type
|
|
469
|
+
trait = _tr.Instance(trait_or_type)
|
|
470
|
+
else:
|
|
471
|
+
trait = trait_or_type
|
|
472
|
+
super().__init__(trait=trait, **kw)
|
|
473
|
+
|
|
474
|
+
# ── Enum: traitlets.UseEnum needs an actual enum.Enum class;
|
|
475
|
+
# traits.Enum takes *values* instead. Bridge with a custom class. ──
|
|
476
|
+
class _TraitsEnum(_tr.TraitType):
|
|
477
|
+
def __init__(self, values, **kw):
|
|
478
|
+
self._allowed = list(values)
|
|
479
|
+
super().__init__(self._allowed[0] if self._allowed else None, **kw)
|
|
480
|
+
def validate(self, obj, value):
|
|
481
|
+
if value in self._allowed:
|
|
482
|
+
return value
|
|
483
|
+
raise _tr.TraitError(
|
|
484
|
+
f"The value {value!r} is not in {self._allowed!r}"
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
# ── Range ─────────────────────────────────────────────────────────────
|
|
488
|
+
class _Range(_tr.Any):
|
|
489
|
+
def __init__(self, low=None, high=None, value=None, **kw):
|
|
490
|
+
super().__init__(value if value is not None else low, allow_none=True, **kw)
|
|
491
|
+
|
|
492
|
+
# ── Instance: traits.Instance(SomeClass) allows None by default;
|
|
493
|
+
# traitlets.Instance doesn't. Wrap to force allow_none=True. ─────────
|
|
494
|
+
class _Instance(_tr.Instance):
|
|
495
|
+
def __init__(self, klass=None, args=None, **kw):
|
|
496
|
+
kw.setdefault('allow_none', True)
|
|
497
|
+
super().__init__(klass, args=args, **kw)
|
|
498
|
+
|
|
499
|
+
# ── WeakRef stub ──────────────────────────────────────────────────────
|
|
500
|
+
class _WeakRef(_tr.Any):
|
|
501
|
+
def __init__(self, *a, **kw):
|
|
502
|
+
super().__init__(None, allow_none=True)
|
|
503
|
+
|
|
504
|
+
# ── Delegate / DelegatesTo / PrototypedFrom stubs ─────────────────────
|
|
505
|
+
class _Delegate(_tr.Any):
|
|
506
|
+
def __init__(self, *a, **kw):
|
|
507
|
+
super().__init__(None, allow_none=True)
|
|
508
|
+
|
|
509
|
+
# ── Type stub ─────────────────────────────────────────────────────────
|
|
510
|
+
class _Type(_tr.Type):
|
|
511
|
+
pass
|
|
512
|
+
|
|
513
|
+
# ── Build traits.api namespace ─────────────────────────────────────────
|
|
514
|
+
import types as _types
|
|
515
|
+
|
|
516
|
+
_traits_api = _types.ModuleType('traits.api')
|
|
517
|
+
|
|
518
|
+
# Map the most-used traits names onto traitlets equivalents or our shims.
|
|
519
|
+
_traits_api.HasTraits = _tr.HasTraits
|
|
520
|
+
_traits_api.HasStrictTraits = _tr.HasTraits
|
|
521
|
+
_traits_api.HasPrivateTraits = _tr.HasTraits
|
|
522
|
+
_traits_api.HasRequiredTraits= _tr.HasTraits
|
|
523
|
+
_traits_api.Interface = _tr.HasTraits
|
|
524
|
+
_traits_api.Undefined = Undefined
|
|
525
|
+
_traits_api.TraitError = TraitError
|
|
526
|
+
_traits_api.Bool = _tr.Bool
|
|
527
|
+
_traits_api.CBool = _tr.Bool
|
|
528
|
+
_traits_api.BaseBool = _tr.Bool
|
|
529
|
+
_traits_api.BaseCBool = _tr.Bool
|
|
530
|
+
_traits_api.Int = _tr.Int
|
|
531
|
+
_traits_api.CInt = _tr.Int
|
|
532
|
+
_traits_api.BaseInt = _tr.Int
|
|
533
|
+
_traits_api.BaseCInt = _tr.Int
|
|
534
|
+
_traits_api.Float = _tr.Float
|
|
535
|
+
_traits_api.CFloat = _tr.Float
|
|
536
|
+
_traits_api.BaseFloat = _tr.Float
|
|
537
|
+
_traits_api.BaseCFloat = _tr.Float
|
|
538
|
+
_traits_api.Complex = _tr.Complex
|
|
539
|
+
_traits_api.CComplex = _tr.Complex
|
|
540
|
+
_traits_api.BaseComplex = _tr.Complex
|
|
541
|
+
_traits_api.BaseCComplex = _tr.Complex
|
|
542
|
+
_traits_api.Str = _TraitsStr
|
|
543
|
+
_traits_api.CStr = _TraitsStr
|
|
544
|
+
_traits_api.String = _TraitsStr
|
|
545
|
+
_traits_api.BaseStr = _TraitsStr
|
|
546
|
+
_traits_api.BaseCStr = _TraitsStr
|
|
547
|
+
_traits_api.Bytes = _tr.Bytes
|
|
548
|
+
_traits_api.CBytes = _tr.Bytes
|
|
549
|
+
_traits_api.BaseBytes = _tr.Bytes
|
|
550
|
+
_traits_api.Any = _tr.Any
|
|
551
|
+
_traits_api.List = _TraitsList
|
|
552
|
+
_traits_api.CList = _TraitsList
|
|
553
|
+
_traits_api.Set = _tr.Set
|
|
554
|
+
_traits_api.CSet = _tr.Set
|
|
555
|
+
_traits_api.Dict = _tr.Dict
|
|
556
|
+
_traits_api.Tuple = _tr.Tuple
|
|
557
|
+
_traits_api.Instance = _Instance
|
|
558
|
+
_traits_api.BaseInstance = _Instance
|
|
559
|
+
_traits_api.AdaptedTo = _Instance
|
|
560
|
+
_traits_api.AdaptsTo = _Instance
|
|
561
|
+
_traits_api.Type = _tr.Type
|
|
562
|
+
_traits_api.Subclass = _tr.Type
|
|
563
|
+
_traits_api.Enum = _TraitsEnum
|
|
564
|
+
_traits_api.BaseEnum = _TraitsEnum
|
|
565
|
+
_traits_api.Range = _Range
|
|
566
|
+
_traits_api.BaseRange = _Range
|
|
567
|
+
_traits_api.Property = _Property
|
|
568
|
+
_traits_api.Event = _Event
|
|
569
|
+
_traits_api.Button = _Button
|
|
570
|
+
_traits_api.ToolbarButton = _Button
|
|
571
|
+
_traits_api.WeakRef = _WeakRef
|
|
572
|
+
_traits_api.Delegate = _Delegate
|
|
573
|
+
_traits_api.DelegatesTo = _Delegate
|
|
574
|
+
_traits_api.PrototypedFrom = _Delegate
|
|
575
|
+
_traits_api.Callable = _tr.Callable
|
|
576
|
+
_traits_api.BaseCallable = _tr.Callable
|
|
577
|
+
_traits_api.Either = _tr.Union
|
|
578
|
+
_traits_api.Union = _tr.Union
|
|
579
|
+
_traits_api.ReadOnly = _tr.Any
|
|
580
|
+
_traits_api.Disallow = _tr.Any
|
|
581
|
+
_traits_api.Constant = _tr.Any
|
|
582
|
+
_traits_api.PrefixList = _tr.List
|
|
583
|
+
_traits_api.PrefixMap = _tr.Dict
|
|
584
|
+
_traits_api.Map = _tr.Dict
|
|
585
|
+
_traits_api.Expression = _tr.Any
|
|
586
|
+
_traits_api.PythonValue = _tr.Any
|
|
587
|
+
_traits_api.File = _tr.Unicode
|
|
588
|
+
_traits_api.Directory = _tr.Unicode
|
|
589
|
+
_traits_api.BaseFile = _tr.Unicode
|
|
590
|
+
_traits_api.BaseDirectory = _tr.Unicode
|
|
591
|
+
_traits_api.HTML = _tr.Unicode
|
|
592
|
+
_traits_api.Password = _tr.Unicode
|
|
593
|
+
_traits_api.Regex = _tr.Unicode
|
|
594
|
+
_traits_api.Code = _tr.Unicode
|
|
595
|
+
_traits_api.Title = _tr.Unicode
|
|
596
|
+
_traits_api.UUID = _tr.Unicode
|
|
597
|
+
_traits_api.Date = _tr.Any
|
|
598
|
+
_traits_api.Datetime = _tr.Any
|
|
599
|
+
_traits_api.Time = _tr.Any
|
|
600
|
+
_traits_api.Supports = _tr.Any
|
|
601
|
+
_traits_api.Python = _tr.Any
|
|
602
|
+
_traits_api.Module = _tr.Any
|
|
603
|
+
_traits_api.Self = _tr.Any
|
|
604
|
+
_traits_api.This = _tr.Any
|
|
605
|
+
_traits_api.self = _tr.Any
|
|
606
|
+
_traits_api.ValidatedTuple = _tr.Tuple
|
|
607
|
+
_traits_api.Array = _Array
|
|
608
|
+
|
|
609
|
+
# observers/decorators
|
|
610
|
+
_traits_api.observe = _tr.observe
|
|
611
|
+
_traits_api.on_trait_change = lambda *a, **kw: (lambda f: f)
|
|
612
|
+
_traits_api.cached_property = property
|
|
613
|
+
_traits_api.property_depends_on = lambda *a, **kw: (lambda f: f)
|
|
614
|
+
_traits_api.provides = lambda *a, **kw: (lambda cls: cls)
|
|
615
|
+
_traits_api.isinterface = lambda x: False
|
|
616
|
+
|
|
617
|
+
# meta classes
|
|
618
|
+
_traits_api.MetaHasTraits = type(_tr.HasTraits)
|
|
619
|
+
_traits_api.ABCHasTraits = _tr.HasTraits
|
|
620
|
+
_traits_api.ABCHasStrictTraits= _tr.HasTraits
|
|
621
|
+
_traits_api.ABCMetaHasTraits = type(_tr.HasTraits)
|
|
622
|
+
_traits_api.AbstractViewElement = _tr.HasTraits
|
|
623
|
+
|
|
624
|
+
# misc
|
|
625
|
+
_traits_api.Trait = _tr.Any
|
|
626
|
+
_traits_api.Default = lambda *a, **kw: None
|
|
627
|
+
_traits_api.Vetoable = _tr.HasTraits
|
|
628
|
+
_traits_api.VetoableEvent = _Event
|
|
629
|
+
|
|
630
|
+
# ── traits.trait_errors module ────────────────────────────────────────
|
|
631
|
+
_te = _types.ModuleType('traits.trait_errors')
|
|
632
|
+
_te.TraitError = TraitError
|
|
633
|
+
_te.TraitNotificationError = TraitError
|
|
634
|
+
_te.DelegationError = TraitError
|
|
635
|
+
|
|
636
|
+
# ── traits.trait_numeric module ───────────────────────────────────────
|
|
637
|
+
_tn = _types.ModuleType('traits.trait_numeric')
|
|
638
|
+
_tn.Array = _Array
|
|
639
|
+
|
|
640
|
+
# ── traits.etsconfig module ────────────────────────────────────────────
|
|
641
|
+
_etc = _types.ModuleType('traits.etsconfig')
|
|
642
|
+
_etc_api = _types.ModuleType('traits.etsconfig.api')
|
|
643
|
+
class _ETSConfig:
|
|
644
|
+
toolkit = ''
|
|
645
|
+
_etc_api.ETSConfig = _ETSConfig()
|
|
646
|
+
_etc.api = _etc_api
|
|
647
|
+
|
|
648
|
+
# ── traits.constants ──────────────────────────────────────────────────
|
|
649
|
+
_tc = _types.ModuleType('traits.constants')
|
|
650
|
+
_tc.ComparisonMode = None
|
|
651
|
+
_tc.DefaultValue = None
|
|
652
|
+
_tc.TraitKind = None
|
|
653
|
+
_tc.ValidateTrait = None
|
|
654
|
+
_tc.NO_COMPARE = None
|
|
655
|
+
_tc.OBJECT_IDENTITY_COMPARE = None
|
|
656
|
+
_tc.RICH_COMPARE = None
|
|
657
|
+
|
|
658
|
+
# ── Register all in sys.modules ───────────────────────────────────────
|
|
659
|
+
_traits_root = _types.ModuleType('traits')
|
|
660
|
+
_traits_root.api = _traits_api
|
|
661
|
+
_traits_root.trait_errors= _te
|
|
662
|
+
_traits_root.trait_numeric= _tn
|
|
663
|
+
_traits_root.etsconfig = _etc
|
|
664
|
+
_traits_root.constants = _tc
|
|
665
|
+
_traits_root.__version__ = '9999.0.0'
|
|
666
|
+
|
|
667
|
+
sys.modules['traits'] = _traits_root
|
|
668
|
+
sys.modules['traits.api'] = _traits_api
|
|
669
|
+
sys.modules['traits.trait_errors'] = _te
|
|
670
|
+
sys.modules['traits.trait_numeric'] = _tn
|
|
671
|
+
sys.modules['traits.etsconfig'] = _etc
|
|
672
|
+
sys.modules['traits.etsconfig.api'] = _etc_api
|
|
673
|
+
sys.modules['traits.constants'] = _tc
|
|
674
|
+
print('[sphinx_anywidget] traits shim installed')
|
|
675
|
+
`));
|
|
676
|
+
console.info('[sphinx_anywidget] traits shim installed');
|
|
677
|
+
|
|
678
|
+
// 5.6 Stub rosettasciio (rsciio) in sys.modules.
|
|
679
|
+
// rosettasciio has one Bruker C extension but otherwise is pure Python.
|
|
680
|
+
// HyperSpy imports rsciio.utils.{path,rgb} and rsciio.IO_PLUGINS at
|
|
681
|
+
// module-level. Provide minimal stubs so HyperSpy starts up cleanly
|
|
682
|
+
// without needing the real package installed.
|
|
683
|
+
await _step('stub rsciio',
|
|
684
|
+
pyodide.runPythonAsync(`
|
|
685
|
+
import sys, types as _types
|
|
686
|
+
import numpy as _np
|
|
687
|
+
|
|
688
|
+
# ── rsciio.utils.path ─────────────────────────────────────────────────
|
|
689
|
+
from pathlib import Path as _Path
|
|
690
|
+
|
|
691
|
+
def _append2pathname(filename, to_append):
|
|
692
|
+
p = _Path(filename)
|
|
693
|
+
return _Path(p.parent, p.stem + to_append + p.suffix)
|
|
694
|
+
|
|
695
|
+
def _incremental_filename(filename, i=1):
|
|
696
|
+
filename = _Path(filename)
|
|
697
|
+
if filename.is_file():
|
|
698
|
+
nf = _append2pathname(filename, f"-{i}")
|
|
699
|
+
return _incremental_filename(filename, i+1) if nf.is_file() else nf
|
|
700
|
+
return filename
|
|
701
|
+
|
|
702
|
+
def _ensure_directory(path):
|
|
703
|
+
p = _Path(path)
|
|
704
|
+
p = p.parent if p.is_file() else p
|
|
705
|
+
try: p.mkdir(parents=True, exist_ok=False)
|
|
706
|
+
except FileExistsError: pass
|
|
707
|
+
|
|
708
|
+
def _overwrite(filename): return True # no interactive prompts in Pyodide
|
|
709
|
+
|
|
710
|
+
_rp = _types.ModuleType('rsciio.utils.path')
|
|
711
|
+
_rp.append2pathname = _append2pathname
|
|
712
|
+
_rp.incremental_filename = _incremental_filename
|
|
713
|
+
_rp.ensure_directory = _ensure_directory
|
|
714
|
+
_rp.overwrite = _overwrite
|
|
715
|
+
|
|
716
|
+
# ── rsciio.utils.rgb ──────────────────────────────────────────────────
|
|
717
|
+
_RGBA8 = _np.dtype({"names": ["R","G","B","A"], "formats":["u1","u1","u1","u1"]})
|
|
718
|
+
_RGB8 = _np.dtype({"names": ["R","G","B"], "formats":["u1","u1","u1"]})
|
|
719
|
+
_RGBA16 = _np.dtype({"names": ["R","G","B","A"], "formats":["u2","u2","u2","u2"]})
|
|
720
|
+
_RGB16 = _np.dtype({"names": ["R","G","B"], "formats":["u2","u2","u2"]})
|
|
721
|
+
RGB_DTYPES = {"rgb8": _RGB8, "rgb16": _RGB16, "rgba8": _RGBA8, "rgba16": _RGBA16}
|
|
722
|
+
|
|
723
|
+
def _is_rgba(a): return a.dtype in (_RGBA8, _RGBA16)
|
|
724
|
+
def _is_rgb(a): return a.dtype in (_RGB8, _RGB16)
|
|
725
|
+
def _is_rgbx(a): return _is_rgb(a) or _is_rgba(a)
|
|
726
|
+
|
|
727
|
+
def _rgbx2regular_array(data, plot_friendly=False, show_progressbar=True):
|
|
728
|
+
dt = data.dtype
|
|
729
|
+
names = list(dt.names)
|
|
730
|
+
arr = _np.stack([data[n] for n in names], axis=-1)
|
|
731
|
+
if plot_friendly and arr.dtype != _np.uint8:
|
|
732
|
+
arr = arr.astype(float) / _np.iinfo(arr.dtype).max
|
|
733
|
+
return arr
|
|
734
|
+
|
|
735
|
+
def _regular_array2rgbx(data, **kw):
|
|
736
|
+
n = data.shape[-1]
|
|
737
|
+
names = ["R","G","B","A"][:n]
|
|
738
|
+
dt = _np.dtype({"names": names, "formats": [data.dtype]*n})
|
|
739
|
+
out = _np.empty(data.shape[:-1], dtype=dt)
|
|
740
|
+
for i, nm in enumerate(names):
|
|
741
|
+
out[nm] = data[...,i]
|
|
742
|
+
return out
|
|
743
|
+
|
|
744
|
+
_rrgb = _types.ModuleType('rsciio.utils.rgb')
|
|
745
|
+
_rrgb.RGB_DTYPES = RGB_DTYPES
|
|
746
|
+
_rrgb.is_rgba = _is_rgba
|
|
747
|
+
_rrgb.is_rgb = _is_rgb
|
|
748
|
+
_rrgb.is_rgbx = _is_rgbx
|
|
749
|
+
_rrgb.rgbx2regular_array = _rgbx2regular_array
|
|
750
|
+
_rrgb.regular_array2rgbx = _regular_array2rgbx
|
|
751
|
+
|
|
752
|
+
# ── rsciio.utils (parent) ─────────────────────────────────────────────
|
|
753
|
+
_ru = _types.ModuleType('rsciio.utils')
|
|
754
|
+
_ru.path = _rp
|
|
755
|
+
_ru.rgb = _rrgb
|
|
756
|
+
|
|
757
|
+
# ── rsciio root ───────────────────────────────────────────────────────
|
|
758
|
+
_rsciio = _types.ModuleType('rsciio')
|
|
759
|
+
_rsciio.utils = _ru
|
|
760
|
+
_rsciio.IO_PLUGINS = []
|
|
761
|
+
_rsciio.__version__ = '9999.0.0'
|
|
762
|
+
|
|
763
|
+
sys.modules['rsciio'] = _rsciio
|
|
764
|
+
sys.modules['rsciio.utils'] = _ru
|
|
765
|
+
sys.modules['rsciio.utils.path'] = _rp
|
|
766
|
+
sys.modules['rsciio.utils.rgb'] = _rrgb
|
|
767
|
+
print('[sphinx_anywidget] rsciio shim installed')
|
|
768
|
+
|
|
769
|
+
# ── natsort stub ──────────────────────────────────────────────────────
|
|
770
|
+
_natsort = _types.ModuleType('natsort')
|
|
771
|
+
_natsort.natsorted = sorted
|
|
772
|
+
_natsort.natsort_keygen = lambda *a, **kw: (lambda x: x)
|
|
773
|
+
_natsort.ns = type('ns', (), {'DEFAULT': 0, 'REAL': 1, 'LOCALE': 2})()
|
|
774
|
+
sys.modules['natsort'] = _natsort
|
|
775
|
+
|
|
776
|
+
# ── prettytable stub ──────────────────────────────────────────────────
|
|
777
|
+
_pt = _types.ModuleType('prettytable')
|
|
778
|
+
class _PrettyTable:
|
|
779
|
+
def __init__(self, *a, **kw): self._rows = []
|
|
780
|
+
def add_row(self, row): self._rows.append(row)
|
|
781
|
+
def __str__(self): return str(self._rows)
|
|
782
|
+
_pt.PrettyTable = _PrettyTable
|
|
783
|
+
sys.modules['prettytable'] = _pt
|
|
784
|
+
|
|
785
|
+
print('[sphinx_anywidget] misc stubs installed')
|
|
786
|
+
`));
|
|
787
|
+
console.info('[sphinx_anywidget] rsciio shim installed');
|
|
788
|
+
|
|
789
|
+
// 6. Install package wheel via micropip.
|
|
790
|
+
// anywidget is already stubbed in sys.modules (step 5). Register it as
|
|
791
|
+
// a mock package so micropip does not try to download it from PyPI when
|
|
792
|
+
// it resolves the wheel's declared deps. numpy/traitlets/colorcet are
|
|
793
|
+
// properly installed above so micropip will find them and skip them.
|
|
794
|
+
// We do NOT pass deps=False — that triggers a micropip 0.7.x internal
|
|
795
|
+
// bug ("attempted to install wheel before downloading it").
|
|
796
|
+
|
|
797
|
+
// Collect all _PYODIDE_MOCK_PACKAGES declared by any example on this page
|
|
798
|
+
// so they can be registered as mock packages BEFORE the wheel install.
|
|
799
|
+
// (Dep resolution happens during install, not during exec.)
|
|
800
|
+
const _globalMockPkgs = [];
|
|
801
|
+
for (const s of document.querySelectorAll(
|
|
802
|
+
'script[type="text/x-python"][data-pyodide-mock-packages]')) {
|
|
803
|
+
try {
|
|
804
|
+
const pkgs = JSON.parse(s.dataset.pyodideMockPackages || '[]');
|
|
805
|
+
for (const p of pkgs) if (!_globalMockPkgs.includes(p)) _globalMockPkgs.push(p);
|
|
806
|
+
} catch (_) {}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const wheelUrl = _DOCS_ROOT + '_static/wheels/';
|
|
810
|
+
// Discover wheel name: try the configured package name from the <script>
|
|
811
|
+
// data-package attribute injected by _build_pyodide_wheel, else fall back
|
|
812
|
+
// to scanning data-src-file scripts for a clue.
|
|
813
|
+
const pkgName = _inferPackageName();
|
|
814
|
+
const fullWheelUrl = wheelUrl + pkgName + '-0.0.0-py3-none-any.whl';
|
|
815
|
+
console.info('[sphinx_anywidget] installing wheel from', fullWheelUrl);
|
|
816
|
+
const _mockList = JSON.stringify(['anywidget', ..._globalMockPkgs]);
|
|
817
|
+
await _step('install wheel', pyodide.runPythonAsync(`
|
|
818
|
+
import micropip
|
|
819
|
+
# Register anywidget + any page-level mock packages so micropip skips them
|
|
820
|
+
# during dep resolution. Our sys.modules stub takes priority at import time.
|
|
821
|
+
for _pkg in ${_mockList}:
|
|
822
|
+
try:
|
|
823
|
+
micropip.add_mock_package(_pkg, "9999.0.0")
|
|
824
|
+
except Exception:
|
|
825
|
+
pass
|
|
826
|
+
del _pkg
|
|
827
|
+
await micropip.install(${JSON.stringify(fullWheelUrl)})
|
|
828
|
+
`));
|
|
829
|
+
console.info('[sphinx_anywidget] package wheel installed');
|
|
830
|
+
|
|
831
|
+
// 6.5 Patch BaseDataAxis._update_slice to notify AxesManager
|
|
832
|
+
// When traits observes "_axes.slice" (dotted path), it calls
|
|
833
|
+
// AxesManager._on_slice_changed → _update_attributes() each time any
|
|
834
|
+
// axis's slice changes. Our traitlets shim handles dotted paths by
|
|
835
|
+
// registering item-level observers, but those are set up during
|
|
836
|
+
// AxesManager.__init__ *before* the wheel is installed. Patching
|
|
837
|
+
// _update_slice directly is the most reliable alternative: after each
|
|
838
|
+
// slice assignment the axes_manager (if already set) re-classifies axes.
|
|
839
|
+
await pyodide.runPythonAsync(`
|
|
840
|
+
from hyperspy.axes import BaseDataAxis as _BDA, AxesManager as _AM
|
|
841
|
+
_orig_update_slice = _BDA._update_slice
|
|
842
|
+
|
|
843
|
+
def _patched_update_slice(self, value):
|
|
844
|
+
_orig_update_slice(self, value)
|
|
845
|
+
am = getattr(self, 'axes_manager', None)
|
|
846
|
+
if am is not None and hasattr(am, '_update_attributes'):
|
|
847
|
+
am._update_attributes()
|
|
848
|
+
|
|
849
|
+
_BDA._update_slice = _patched_update_slice
|
|
850
|
+
print('[sphinx_anywidget] BaseDataAxis._update_slice patched for traitlets compat')
|
|
851
|
+
`).catch(e => console.warn('[DIAG] _update_slice patch failed:', e.message));
|
|
852
|
+
|
|
853
|
+
// 7. Install generic anywidget monkey-patch
|
|
854
|
+
// Wraps AnyWidget.__init__ so that every new widget instance automatically
|
|
855
|
+
// gets a traitlets.observe(names=All) callback. The callback pushes any
|
|
856
|
+
// sync=True trait change to the matching iframe via window._anywidgetPush.
|
|
857
|
+
// This works for dynamically-added traits too because traitlets.All
|
|
858
|
+
// matches all trait names, including ones added after __init__ via add_traits().
|
|
859
|
+
window._anywidgetPush = (figId, key, value) =>
|
|
860
|
+
_postToIframe(String(figId), String(key), String(value));
|
|
861
|
+
|
|
862
|
+
await _step('install monkey-patch',
|
|
863
|
+
pyodide.runPythonAsync(`
|
|
864
|
+
import anywidget as _aw
|
|
865
|
+
import traitlets as _tr
|
|
866
|
+
import js
|
|
867
|
+
|
|
868
|
+
_orig_init = _aw.AnyWidget.__init__
|
|
869
|
+
|
|
870
|
+
def _patched_init(self, *args, **kw):
|
|
871
|
+
_orig_init(self, *args, **kw)
|
|
872
|
+
|
|
873
|
+
# Capture self in a closure so each widget has its own push callback.
|
|
874
|
+
_self = self
|
|
875
|
+
|
|
876
|
+
def _push_cb(change):
|
|
877
|
+
fid = getattr(_self, '_anywidget_fig_id', None)
|
|
878
|
+
if fid is None:
|
|
879
|
+
return
|
|
880
|
+
tname = change['name']
|
|
881
|
+
# Only push traits tagged sync=True
|
|
882
|
+
# instance_traits() covers dynamically-added traits from add_traits()
|
|
883
|
+
t = _self.traits().get(tname)
|
|
884
|
+
if t is None or not t.metadata.get('sync'):
|
|
885
|
+
return
|
|
886
|
+
import json as _j
|
|
887
|
+
val = change['new']
|
|
888
|
+
# Always JSON-encode so the JS bridge can JSON.parse to recover the
|
|
889
|
+
# correct type — strings, numbers, bools and objects all round-trip.
|
|
890
|
+
val_str = _j.dumps(val, default=str)
|
|
891
|
+
js.window._anywidgetPush(fid, tname, val_str)
|
|
892
|
+
|
|
893
|
+
self.observe(_push_cb, names=_tr.All)
|
|
894
|
+
|
|
895
|
+
_aw.AnyWidget.__init__ = _patched_init
|
|
896
|
+
print('[sphinx_anywidget] anywidget monkey-patch installed')
|
|
897
|
+
`));
|
|
898
|
+
console.info('[sphinx_anywidget] monkey-patch installed');
|
|
899
|
+
|
|
900
|
+
// 8. Collect text/x-python script blocks, group by src-file so each
|
|
901
|
+
// example source runs exactly once even with multiple figures.
|
|
902
|
+
const srcGroups = new Map(); // srcFile → { src, pairs, packages, mockPackages }
|
|
903
|
+
|
|
904
|
+
for (const script of document.querySelectorAll(
|
|
905
|
+
'script[type="text/x-python"][data-fig-id]')) {
|
|
906
|
+
const srcFile = script.dataset.srcFile || '__default__';
|
|
907
|
+
const figId = script.dataset.figId;
|
|
908
|
+
const figIndex = parseInt(script.dataset.figIndex || '0', 10);
|
|
909
|
+
let src = '';
|
|
910
|
+
try { src = JSON.parse(script.dataset.src || 'null') || ''; } catch (_) {}
|
|
911
|
+
let packages = [];
|
|
912
|
+
try { packages = JSON.parse(script.dataset.pyodidePackages || 'null') || []; } catch (_) {}
|
|
913
|
+
let mockPackages = [];
|
|
914
|
+
try { mockPackages = JSON.parse(script.dataset.pyodideMockPackages || 'null') || []; } catch (_) {}
|
|
915
|
+
|
|
916
|
+
if (!srcGroups.has(srcFile)) srcGroups.set(srcFile, { src, pairs: [], packages, mockPackages });
|
|
917
|
+
const grp = srcGroups.get(srcFile);
|
|
918
|
+
grp.pairs.push({ figId, figIndex });
|
|
919
|
+
// Merge any packages declared by any script tag for this source file.
|
|
920
|
+
for (const p of packages) if (!grp.packages.includes(p)) grp.packages.push(p);
|
|
921
|
+
for (const p of mockPackages) if (!grp.mockPackages.includes(p)) grp.mockPackages.push(p);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
for (const g of srcGroups.values())
|
|
925
|
+
g.pairs.sort((a, b) => a.figIndex - b.figIndex);
|
|
926
|
+
|
|
927
|
+
// 9. Run each example source once, assign _anywidget_fig_id in creation
|
|
928
|
+
// order, then push current state into the matching iframes.
|
|
929
|
+
const _execErrors = [];
|
|
930
|
+
for (const [srcFile, { src, pairs, packages, mockPackages }] of srcGroups) {
|
|
931
|
+
const figIdList = JSON.stringify(pairs.map(p => p.figId));
|
|
932
|
+
console.info(`[sphinx_anywidget] running: ${srcFile} (${pairs.length} figure(s))`);
|
|
933
|
+
const _srcFileRepr = JSON.stringify(srcFile);
|
|
934
|
+
|
|
935
|
+
// Load any extra packages declared by this example (e.g. scipy).
|
|
936
|
+
if (packages.length > 0) {
|
|
937
|
+
console.info(`[sphinx_anywidget] loading extra packages for ${srcFile}:`, packages);
|
|
938
|
+
await _step(`load packages for ${srcFile}`,
|
|
939
|
+
pyodide.loadPackage(packages));
|
|
940
|
+
console.info(`[sphinx_anywidget] extra packages loaded for ${srcFile}`);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
try {
|
|
944
|
+
await pyodide.runPythonAsync(`
|
|
945
|
+
import anywidget as _aw
|
|
946
|
+
import traitlets as _tr
|
|
947
|
+
|
|
948
|
+
_SRC_FILE = ${_srcFileRepr}
|
|
949
|
+
_CREATED_WIDS = []
|
|
950
|
+
_SEEN_IDS = set()
|
|
951
|
+
_orig_init2 = _aw.AnyWidget.__init__
|
|
952
|
+
|
|
953
|
+
def _tracked_init(self, *a, **kw):
|
|
954
|
+
_orig_init2(self, *a, **kw)
|
|
955
|
+
if id(self) not in _SEEN_IDS:
|
|
956
|
+
_SEEN_IDS.add(id(self))
|
|
957
|
+
_CREATED_WIDS.append(self)
|
|
958
|
+
|
|
959
|
+
_aw.AnyWidget.__init__ = _tracked_init
|
|
960
|
+
_exec_error = None
|
|
961
|
+
try:
|
|
962
|
+
exec(${JSON.stringify(src)}, {"__name__": "__main__"})
|
|
963
|
+
except Exception as _e:
|
|
964
|
+
import traceback as _tb
|
|
965
|
+
_exec_error = _tb.format_exc()
|
|
966
|
+
print("[sphinx_anywidget] exec error in " + _SRC_FILE + ":\\n" + _exec_error)
|
|
967
|
+
finally:
|
|
968
|
+
_aw.AnyWidget.__init__ = _orig_init2
|
|
969
|
+
|
|
970
|
+
_fig_ids = ${figIdList}
|
|
971
|
+
_wired = 0
|
|
972
|
+
for _i, _fid in enumerate(_fig_ids):
|
|
973
|
+
if _i < len(_CREATED_WIDS):
|
|
974
|
+
_w = _CREATED_WIDS[_i]
|
|
975
|
+
_w._anywidget_fig_id = _fid
|
|
976
|
+
_AWI_REGISTRY[_fid] = _w
|
|
977
|
+
# Push current state into the iframe immediately
|
|
978
|
+
for _tname, _trait in _w.traits().items():
|
|
979
|
+
if _tname.startswith('_') or not _trait.metadata.get('sync'):
|
|
980
|
+
continue
|
|
981
|
+
import json as _jj
|
|
982
|
+
_val = getattr(_w, _tname, None)
|
|
983
|
+
if _val is None:
|
|
984
|
+
continue
|
|
985
|
+
# Always JSON-encode (matching the live observer above) so the
|
|
986
|
+
# JS bridge can JSON.parse to recover the correct type.
|
|
987
|
+
_vs = _jj.dumps(_val, default=str)
|
|
988
|
+
import js as _js
|
|
989
|
+
_js.window._anywidgetPush(_fid, _tname, _vs)
|
|
990
|
+
_wired += 1
|
|
991
|
+
|
|
992
|
+
print("[sphinx_anywidget] wired " + str(_wired) + "/" + str(len(_fig_ids))
|
|
993
|
+
+ " widgets for " + _SRC_FILE)
|
|
994
|
+
if _exec_error:
|
|
995
|
+
raise RuntimeError("exec failed: " + _exec_error)
|
|
996
|
+
`);
|
|
997
|
+
} catch (err) {
|
|
998
|
+
const msg = String(err.message || err);
|
|
999
|
+
console.warn(`[sphinx_anywidget] Pyodide failed for ${srcFile}:`, msg);
|
|
1000
|
+
_execErrors.push(`${srcFile}: ${msg.split('\n').filter(Boolean).pop()}`);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// 10. Route awi_event messages from iframes → Pyodide callbacks.
|
|
1005
|
+
// Call the pre-compiled _awi_dispatch proxy DIRECTLY (no runPythonAsync
|
|
1006
|
+
// code-string recompile per frame — that was ~1.2 ms/event in WASM, the
|
|
1007
|
+
// dominant per-frame cost of the Pyodide interaction path; the proxy is
|
|
1008
|
+
// ~50x faster). Synchronous call: _dispatch_event itself is sync, and
|
|
1009
|
+
// skipping the async wrapper removes a microtask hop per event.
|
|
1010
|
+
// The proxy is fetched lazily + cached (robust to any boot-step ordering),
|
|
1011
|
+
// with a one-shot runPythonAsync fallback if it isn't available.
|
|
1012
|
+
let _awiDispatch = null;
|
|
1013
|
+
window.addEventListener('message', (e) => {
|
|
1014
|
+
if (!e.data || e.data.type !== 'awi_event') return;
|
|
1015
|
+
const { figId, data } = e.data;
|
|
1016
|
+
try {
|
|
1017
|
+
if (!_awiDispatch) {
|
|
1018
|
+
try { _awiDispatch = pyodide.globals.get('_awi_dispatch'); } catch (_) {}
|
|
1019
|
+
}
|
|
1020
|
+
if (_awiDispatch) {
|
|
1021
|
+
_awiDispatch(figId, data);
|
|
1022
|
+
} else {
|
|
1023
|
+
// Fallback (should not happen): recompiled dispatch.
|
|
1024
|
+
pyodide.runPythonAsync(
|
|
1025
|
+
`_awi_dispatch(${JSON.stringify(figId)}, ${JSON.stringify(data)})`);
|
|
1026
|
+
}
|
|
1027
|
+
} catch (err) {
|
|
1028
|
+
console.warn('[sphinx_anywidget] event dispatch error:', err);
|
|
1029
|
+
}
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
if (_execErrors.length > 0) {
|
|
1033
|
+
console.warn('[sphinx_anywidget] some examples failed:', _execErrors);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
return pyodide;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// ── package name inference ────────────────────────────────────────────────
|
|
1040
|
+
|
|
1041
|
+
function _inferPackageName() {
|
|
1042
|
+
// 0. Authoritative: set by anywidget_config.js (written at build time)
|
|
1043
|
+
if (window._anywidgetPackage) return window._anywidgetPackage;
|
|
1044
|
+
// 1. Check for a <meta name="anywidget:package"> tag (set by the extension)
|
|
1045
|
+
const meta = document.querySelector('meta[name="anywidget:package"]');
|
|
1046
|
+
if (meta) return meta.getAttribute('content');
|
|
1047
|
+
// 2. Scan script data-src-file stems for a known module name
|
|
1048
|
+
// (heuristic: first segment before "_" often indicates the package)
|
|
1049
|
+
const script = document.querySelector('script[type="text/x-python"][data-src-file]');
|
|
1050
|
+
if (script) {
|
|
1051
|
+
const stem = script.dataset.srcFile || '';
|
|
1052
|
+
const pkg = stem.split('_')[0];
|
|
1053
|
+
if (pkg) return pkg;
|
|
1054
|
+
}
|
|
1055
|
+
// 3. Fallback: derive from the docs root URL last segment
|
|
1056
|
+
try {
|
|
1057
|
+
const parts = new URL(_DOCS_ROOT).pathname.replace(/\/$/, '').split('/');
|
|
1058
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
1059
|
+
const s = parts[i];
|
|
1060
|
+
if (s && s !== 'dev' && !/^v\d/.test(s)) return s;
|
|
1061
|
+
}
|
|
1062
|
+
} catch (_) {}
|
|
1063
|
+
return 'anyplotlib';
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// ── per-badge click handler ───────────────────────────────────────────────
|
|
1067
|
+
|
|
1068
|
+
function _attachBadgeHandlers() {
|
|
1069
|
+
// Use event delegation so badges added by SG after DOMContentLoaded also work.
|
|
1070
|
+
document.addEventListener('click', function (e) {
|
|
1071
|
+
const btn = e.target.closest('button.awi-activate-btn');
|
|
1072
|
+
if (!btn || btn.dataset.state) return; // already loading/active/error
|
|
1073
|
+
|
|
1074
|
+
// Disable ALL activate buttons and show ⏳ on all of them
|
|
1075
|
+
_setBadgesLoading();
|
|
1076
|
+
|
|
1077
|
+
_boot()
|
|
1078
|
+
.then(() => _setBadgesActive())
|
|
1079
|
+
.catch(err => {
|
|
1080
|
+
console.error('[sphinx_anywidget] boot failed:', err);
|
|
1081
|
+
_setBadgesError(err);
|
|
1082
|
+
});
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// ── init ──────────────────────────────────────────────────────────────────
|
|
1087
|
+
|
|
1088
|
+
function _init() {
|
|
1089
|
+
if (!_hasInteractiveFigures()) return;
|
|
1090
|
+
_attachBadgeHandlers();
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
if (document.readyState === 'loading') {
|
|
1094
|
+
document.addEventListener('DOMContentLoaded', _init);
|
|
1095
|
+
} else {
|
|
1096
|
+
_init();
|
|
1097
|
+
}
|
|
1098
|
+
})();
|
|
1099
|
+
|