dynamic-systems-testing-framework 1.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.
- dstf/__init__.py +41 -0
- dstf/__main__.py +10 -0
- dstf/authoring/__init__.py +13 -0
- dstf/authoring/builtin/__init__.py +32 -0
- dstf/authoring/builtin/accept.py +205 -0
- dstf/authoring/builtin/edit_criteria.py +479 -0
- dstf/authoring/builtin/edit_family.py +228 -0
- dstf/authoring/builtin/edit_on_plot.py +1354 -0
- dstf/authoring/builtin/edit_tree.py +1399 -0
- dstf/authoring/builtin/enqueue.py +144 -0
- dstf/authoring/capabilities.py +463 -0
- dstf/authoring/errors.py +49 -0
- dstf/authoring/family_rows.py +526 -0
- dstf/authoring/forms.py +631 -0
- dstf/authoring/layout.py +104 -0
- dstf/authoring/live_rows.py +337 -0
- dstf/authoring/live_runs.py +385 -0
- dstf/authoring/needle.py +127 -0
- dstf/authoring/picker_rows.py +92 -0
- dstf/authoring/queue_rows.py +354 -0
- dstf/authoring/session.py +3016 -0
- dstf/authoring/shell/__init__.py +9 -0
- dstf/authoring/shell/actions.py +702 -0
- dstf/authoring/shell/app.py +53 -0
- dstf/authoring/shell/drawer_controls.py +190 -0
- dstf/authoring/shell/family_fold.py +468 -0
- dstf/authoring/shell/figure_hook.py +249 -0
- dstf/authoring/shell/grips.js +1000 -0
- dstf/authoring/shell/header.py +783 -0
- dstf/authoring/shell/live_view.py +93 -0
- dstf/authoring/shell/picker.py +267 -0
- dstf/authoring/shell/placing.py +122 -0
- dstf/authoring/shell/plot_card.py +1171 -0
- dstf/authoring/shell/queue_panel.py +433 -0
- dstf/authoring/shell/rail.py +593 -0
- dstf/authoring/shell/shell.py +528 -0
- dstf/authoring/shell/stats_card.py +501 -0
- dstf/authoring/shell/strip.py +204 -0
- dstf/authoring/shell/style.py +721 -0
- dstf/authoring/shell/suite.py +277 -0
- dstf/authoring/shell/suite_grid.js +414 -0
- dstf/authoring/shell/suite_grid.py +373 -0
- dstf/authoring/shell/tab_theme.py +110 -0
- dstf/authoring/shell/tabstate.py +146 -0
- dstf/authoring/shell/test_list.py +179 -0
- dstf/authoring/shell/test_page.py +1404 -0
- dstf/authoring/shell/tree_card.py +1171 -0
- dstf/authoring/shell/variables_drawer.py +589 -0
- dstf/authoring/shell/widgets.py +188 -0
- dstf/authoring/situation.py +287 -0
- dstf/authoring/spec_document.py +215 -0
- dstf/authoring/spec_preview.py +83 -0
- dstf/authoring/suite_rows.py +99 -0
- dstf/authoring/tree_move.py +516 -0
- dstf/authoring/tree_pointer.py +33 -0
- dstf/authoring/tree_text.py +61 -0
- dstf/authoring/typed_number.js +64 -0
- dstf/authoring/typed_number.py +76 -0
- dstf/authoring/unscorable.py +141 -0
- dstf/authoring/variable_rows.py +216 -0
- dstf/backends/__init__.py +283 -0
- dstf/backends/base.py +1117 -0
- dstf/backends/common/__init__.py +1 -0
- dstf/backends/common/json_driver.py +484 -0
- dstf/backends/common/live_mat.py +369 -0
- dstf/backends/common/mat_reader.py +632 -0
- dstf/backends/common/proc_kill.py +238 -0
- dstf/backends/common/proc_output.py +20 -0
- dstf/backends/common/result_files.py +109 -0
- dstf/backends/common/result_variables.py +329 -0
- dstf/backends/common/statistics.py +39 -0
- dstf/backends/cross_backend.py +193 -0
- dstf/backends/dymola/__init__.py +5 -0
- dstf/backends/dymola/backend.py +776 -0
- dstf/backends/dymola/interface_loader.py +241 -0
- dstf/backends/dymola/log_parser.py +396 -0
- dstf/backends/dymola/persistent_backend.py +949 -0
- dstf/backends/fmpy/__init__.py +11 -0
- dstf/backends/fmpy/backend.py +443 -0
- dstf/backends/julia/__init__.py +27 -0
- dstf/backends/julia/backend.py +156 -0
- dstf/backends/julia/persistent_backend.py +541 -0
- dstf/backends/julia/run_persistent.jl +167 -0
- dstf/backends/julia/run_test.jl +146 -0
- dstf/backends/openmodelica/__init__.py +21 -0
- dstf/backends/openmodelica/backend.py +368 -0
- dstf/backends/openmodelica/init_xml.py +76 -0
- dstf/backends/openmodelica/log_parser.py +119 -0
- dstf/backends/openmodelica/mos_generator.py +228 -0
- dstf/backends/openmodelica/persistent_backend.py +1045 -0
- dstf/backends/openmodelica/session_loader.py +63 -0
- dstf/backends/persistent.py +1811 -0
- dstf/backends/pool.py +602 -0
- dstf/backends/progress.py +395 -0
- dstf/backends/python/__init__.py +11 -0
- dstf/backends/python/backend.py +141 -0
- dstf/backends/python/run_test.py +141 -0
- dstf/backends/run_metadata.py +103 -0
- dstf/backends/types.py +310 -0
- dstf/cli/__init__.py +12 -0
- dstf/cli/author_cmd.py +61 -0
- dstf/cli/baseline_cmds.py +341 -0
- dstf/cli/common.py +231 -0
- dstf/cli/diag_cmds.py +350 -0
- dstf/cli/main.py +561 -0
- dstf/cli/review.py +398 -0
- dstf/cli/run_cmds.py +589 -0
- dstf/cli/spec_cmds.py +123 -0
- dstf/comparison/__init__.py +13 -0
- dstf/comparison/algorithms.py +2127 -0
- dstf/comparison/builtins.py +291 -0
- dstf/comparison/comparator.py +1189 -0
- dstf/comparison/geometry.py +164 -0
- dstf/comparison/handles.py +1194 -0
- dstf/comparison/metric_tree.py +332 -0
- dstf/comparison/mode_schema.py +176 -0
- dstf/comparison/modes.py +788 -0
- dstf/comparison/registry.py +230 -0
- dstf/comparison/seeds.py +281 -0
- dstf/comparison/sides.py +158 -0
- dstf/comparison/snap.py +62 -0
- dstf/comparison/strict_keys.py +246 -0
- dstf/comparison/summaries.py +354 -0
- dstf/comparison/tree_eval.py +868 -0
- dstf/comparison/tree_spec.py +678 -0
- dstf/comparison/types.py +194 -0
- dstf/comparison/validator.py +232 -0
- dstf/config.py +815 -0
- dstf/discovery/__init__.py +0 -0
- dstf/discovery/json_recognizer.py +702 -0
- dstf/discovery/library_state.py +248 -0
- dstf/discovery/mo_parser.py +794 -0
- dstf/discovery/patch_apply.py +308 -0
- dstf/discovery/patch_envelope.py +63 -0
- dstf/discovery/plots.py +318 -0
- dstf/discovery/recognizer.py +103 -0
- dstf/discovery/scan_cache.py +154 -0
- dstf/discovery/spec_parser.py +538 -0
- dstf/discovery/spec_shape.py +122 -0
- dstf/discovery/spec_update.py +493 -0
- dstf/discovery/test_registry.py +488 -0
- dstf/discovery/unit_tests.py +95 -0
- dstf/figures/__init__.py +14 -0
- dstf/figures/assemble.py +48 -0
- dstf/figures/decimate.py +150 -0
- dstf/figures/families.py +289 -0
- dstf/figures/palette.py +170 -0
- dstf/figures/plotly.py +1413 -0
- dstf/figures/view.py +233 -0
- dstf/pipeline.py +435 -0
- dstf/plugins.py +104 -0
- dstf/reporting/__init__.py +0 -0
- dstf/reporting/console_report.py +187 -0
- dstf/reporting/context_builder.py +979 -0
- dstf/reporting/dashboard_render.py +534 -0
- dstf/reporting/junit_report.py +192 -0
- dstf/reporting/overlay_loader.py +708 -0
- dstf/reporting/report_suite.py +1059 -0
- dstf/reporting/rows.py +458 -0
- dstf/reporting/schema_export.py +413 -0
- dstf/reporting/stats_rows.py +321 -0
- dstf/reporting/status.py +388 -0
- dstf/reporting/templates/axes.js +61 -0
- dstf/reporting/templates/dashboard.html +284 -0
- dstf/reporting/templates/dashboard.js +604 -0
- dstf/reporting/templates/filters.js +100 -0
- dstf/reporting/templates/interactive.html +420 -0
- dstf/reporting/templates/report.js +116 -0
- dstf/reporting/templates/vendor/plotly.min.js +8 -0
- dstf/reporting/theme.py +324 -0
- dstf/reporting/tree_rows.py +73 -0
- dstf/reporting/verdicts.py +124 -0
- dstf/storage/__init__.py +0 -0
- dstf/storage/accept.py +216 -0
- dstf/storage/baseline.py +43 -0
- dstf/storage/batch_manifest.py +204 -0
- dstf/storage/ref_index.py +295 -0
- dstf/storage/reference_store.py +661 -0
- dstf/storage/roles.py +576 -0
- dstf/storage/run_stats.py +85 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/METADATA +266 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/RECORD +185 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/WHEEL +4 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/entry_points.txt +2 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""A plot's event hook (two-tier design §4.6 "Panel", amended in sub-project 6
|
|
2
|
+
and in 3b; D131): ``panel.figure.on(kind, handler)``. One hook per plot card,
|
|
3
|
+
created with the card and attached to its ``ui.plotly`` when the plot is
|
|
4
|
+
drawn; a panel hosted under that card subscribes through ``owned(owner)``, so
|
|
5
|
+
closing or rebuilding it takes back exactly its handlers."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from nicegui import ui
|
|
14
|
+
|
|
15
|
+
# The index is bounded so a hostile key cannot reach `int()`'s digit limit;
|
|
16
|
+
# `[0-9]`, not `\d` (which matches any Unicode digit, and `int()` accepts
|
|
17
|
+
# those), and `\Z`, not `$` (which also matches before a trailing newline).
|
|
18
|
+
_SHAPE_KEY = re.compile(r"^shapes\[([0-9]{1,9})\]\.(.+)\Z")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class FigureHook:
|
|
22
|
+
"""``panel.figure.on(kind, handler)``: handlers get a primitive id and a
|
|
23
|
+
payload. Wired to one plot's Plotly events — the card's, so a drag on one
|
|
24
|
+
plot of a stacked page reaches that card's subscribers and no other. The
|
|
25
|
+
payload contract (the spike's, over the payloads plotly.js actually
|
|
26
|
+
sends):
|
|
27
|
+
|
|
28
|
+
- ``plotly_click`` / ``plotly_selected``: the id of the first point whose
|
|
29
|
+
trace ``meta.kind`` is ``"handle"``, else of the first point —
|
|
30
|
+
``customdata``, then ``meta.id``, then ``curveNumber`` — with the event
|
|
31
|
+
payload. Under the figure's ``hovermode: "x unified"`` a click lists
|
|
32
|
+
every trace at that x, so a handle is not necessarily ``points[0]``.
|
|
33
|
+
- ``plotly_relayout`` with keys matching ``shapes[N].<key>``: dispatched
|
|
34
|
+
ONCE PER SHAPE INDEX, ascending. The payload never names the shape;
|
|
35
|
+
the hook resolves the id from the rendered figure's
|
|
36
|
+
``layout.shapes[N].name`` (``""`` when that lookup fails) and hands the
|
|
37
|
+
handler that shape's keys with the ``shapes[N].`` prefix stripped —
|
|
38
|
+
``{"y0": 6.0, "y1": 6.0}`` for a line, ``{"xanchor": 4.0, "yanchor":
|
|
39
|
+
2.99, "path": "M …"}`` for a ring (a pixel path). Positions arrive in
|
|
40
|
+
data coordinates. The hook does not interpret them: the capability does,
|
|
41
|
+
from the handle's primitive type (a Marker's ring moves only by
|
|
42
|
+
``xanchor``/``yanchor``). ``on_shape_moved`` is then called once, in a
|
|
43
|
+
``finally``: a raising handler still gets its snap-back. A relayout
|
|
44
|
+
mixing shape keys with others (a zoom) is a shape relayout; the other
|
|
45
|
+
keys are dropped.
|
|
46
|
+
- Any other relayout (a zoom): dispatched once with id ``""`` and the raw
|
|
47
|
+
payload (``{}`` when it is not a dict); ``on_shape_moved`` is not called.
|
|
48
|
+
- ``dstf_move`` (D149; `move`): a handle HELD on the plot. The page
|
|
49
|
+
sends ``{"seq": n, "update": {...}}``, ``update`` shaped as the
|
|
50
|
+
relayout its release would be, and it is dispatched as one — once per
|
|
51
|
+
shape index, the id resolved the same way — but ``on_shape_moved`` is
|
|
52
|
+
not called: nothing moved. ``{"seq": n, "end": True}`` (a drag that
|
|
53
|
+
ended with no release) is dispatched once with id ``""``. Every move
|
|
54
|
+
and every release carries a sequence number, the page's, rising: a
|
|
55
|
+
move numbered at or under the last release or end heard is DROPPED —
|
|
56
|
+
a preview that arrives after its drag was let go must not paint over
|
|
57
|
+
what the release staged.
|
|
58
|
+
- ``dstf_click`` / anything else a caller hands `emit`: dispatched once
|
|
59
|
+
with id ``""`` and the payload as given.
|
|
60
|
+
|
|
61
|
+
Handler semantics: each shape index is dispatched in turn, ascending,
|
|
62
|
+
from its own snapshot of the handlers, taken when that shape's dispatch
|
|
63
|
+
starts. A handler registered since the snapshot (a panel rebuilt by the
|
|
64
|
+
act it triggered) is not called for that shape; one removed since the
|
|
65
|
+
snapshot (``clear``) is skipped, checked by identity against the current
|
|
66
|
+
list, so a rebuilt panel's predecessor never runs. A handler registered
|
|
67
|
+
twice is called twice. A handler that raises ENDS the relayout: later
|
|
68
|
+
handlers and later shapes are not dispatched, the exception propagates,
|
|
69
|
+
and ``on_shape_moved`` still runs. A malformed payload never makes
|
|
70
|
+
dispatch raise."""
|
|
71
|
+
|
|
72
|
+
def __init__(self, on_shape_moved: Callable[[], None] | None = None) -> None:
|
|
73
|
+
self._on_shape_moved = on_shape_moved
|
|
74
|
+
self._handlers: dict[str, list[tuple[str, Callable[[str, dict], None]]]] = {}
|
|
75
|
+
self._plot: Any = None
|
|
76
|
+
self.dispatching = False # true while handlers run
|
|
77
|
+
#: The sequence number of the last release or drag end heard: a
|
|
78
|
+
#: `move` numbered at or under it is of a drag already over.
|
|
79
|
+
self.released: float = -1
|
|
80
|
+
|
|
81
|
+
def clear(self, owner: str | None = None) -> None:
|
|
82
|
+
"""Remove one owner's handlers, or every handler. A host rebuilds its
|
|
83
|
+
panels whenever what they show can have moved, and the card itself is
|
|
84
|
+
dropped on a rebuild of the page; a subscriber that re-registers in
|
|
85
|
+
`panel()` must not accumulate."""
|
|
86
|
+
if owner is None:
|
|
87
|
+
self._handlers = {}
|
|
88
|
+
return
|
|
89
|
+
# `_call` skips a handler removed mid-dispatch by IDENTITY against
|
|
90
|
+
# the current list, so the entries left here must be the SAME tuple
|
|
91
|
+
# objects — rebuilding them (`(o, h)` anew) would make every
|
|
92
|
+
# surviving handler look removed and skip it.
|
|
93
|
+
self._handlers = {
|
|
94
|
+
kind: [entry for entry in entries if entry[0] != owner]
|
|
95
|
+
for kind, entries in self._handlers.items()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
def on(
|
|
99
|
+
self, kind: str, handler: Callable[[str, dict], None], owner: str = ""
|
|
100
|
+
) -> None:
|
|
101
|
+
self._handlers.setdefault(kind, []).append((owner, handler))
|
|
102
|
+
|
|
103
|
+
def owned(self, owner: str) -> _OwnedFigureHook:
|
|
104
|
+
"""The view of this hook a capability's panel holds: its `on`
|
|
105
|
+
registers under ``owner``, so `clear(owner)` removes exactly those."""
|
|
106
|
+
return _OwnedFigureHook(self, owner)
|
|
107
|
+
|
|
108
|
+
def attach(self, plot: ui.plotly, relayout_js: str | None = None) -> None:
|
|
109
|
+
"""Wire ``plot``'s events to this hook. ``relayout_js``: a
|
|
110
|
+
JavaScript expression over ``e``, the relayout's payload in the
|
|
111
|
+
page, whose value is sent in its place — what the page alone can
|
|
112
|
+
say of a release (the card holds a dragged grip to the VISIBLE
|
|
113
|
+
range, which no server knows). The hook still interprets
|
|
114
|
+
nothing."""
|
|
115
|
+
self._plot = plot
|
|
116
|
+
for event in ("plotly_click", "plotly_relayout", "plotly_selected"):
|
|
117
|
+
|
|
118
|
+
def heard(e: Any, kind: str = event) -> None:
|
|
119
|
+
self._dispatch(kind, e.args)
|
|
120
|
+
|
|
121
|
+
if event == "plotly_relayout" and relayout_js is not None:
|
|
122
|
+
plot.on(event, heard, js_handler=f"(e) => emit({relayout_js})")
|
|
123
|
+
else:
|
|
124
|
+
plot.on(event, heard)
|
|
125
|
+
|
|
126
|
+
def emit(self, kind: str, payload: dict, primitive: str = "") -> None:
|
|
127
|
+
"""One event of ``kind`` to its subscribers, as a dispatch: what
|
|
128
|
+
the card hears from the page beside Plotly's own events (a click),
|
|
129
|
+
and what one panel says to the others (a preview)."""
|
|
130
|
+
self._call(kind, primitive, payload if isinstance(payload, dict) else {})
|
|
131
|
+
|
|
132
|
+
def move(self, payload: Any) -> None:
|
|
133
|
+
"""A held handle's would-be position, from the page (`dstf_move`
|
|
134
|
+
above). Dropped where its drag is already over."""
|
|
135
|
+
data = payload if isinstance(payload, dict) else {}
|
|
136
|
+
seq = data.get("seq")
|
|
137
|
+
if not isinstance(seq, (int, float)) or isinstance(seq, bool):
|
|
138
|
+
return
|
|
139
|
+
if seq <= self.released:
|
|
140
|
+
return
|
|
141
|
+
if data.get("end"):
|
|
142
|
+
self.released = seq
|
|
143
|
+
self._call("dstf_move", "", {"end": True})
|
|
144
|
+
return
|
|
145
|
+
for i, shape in sorted(_by_shape(data.get("update")).items()):
|
|
146
|
+
self._call("dstf_move", self._shape_name(i), shape)
|
|
147
|
+
|
|
148
|
+
def _shape_name(self, index: int) -> str:
|
|
149
|
+
# A known race: the index is resolved against the figure the SERVER
|
|
150
|
+
# last sent. A drag released after an in-place `update_figure` that
|
|
151
|
+
# changed the shape list, but resolved before the browser has applied
|
|
152
|
+
# that update and the relayout has flown back, can resolve to the
|
|
153
|
+
# wrong name. The window runs from `update_figure` through both
|
|
154
|
+
# websocket legs. The snap-back redraw corrects only the picture: once
|
|
155
|
+
# edits are staged from drags, a wrongly resolved handle's edit stays
|
|
156
|
+
# staged.
|
|
157
|
+
figure = getattr(self._plot, "figure", None)
|
|
158
|
+
layout = figure.get("layout") if isinstance(figure, dict) else None
|
|
159
|
+
shapes = layout.get("shapes") if isinstance(layout, dict) else None
|
|
160
|
+
if not isinstance(shapes, list) or index >= len(shapes):
|
|
161
|
+
return ""
|
|
162
|
+
shape = shapes[index]
|
|
163
|
+
name = shape.get("name") if isinstance(shape, dict) else None
|
|
164
|
+
return name if isinstance(name, str) else ""
|
|
165
|
+
|
|
166
|
+
def _dispatch(self, kind: str, payload: Any) -> None:
|
|
167
|
+
data = payload if isinstance(payload, dict) else {}
|
|
168
|
+
if kind == "plotly_relayout":
|
|
169
|
+
by_shape = _by_shape(data)
|
|
170
|
+
if not by_shape:
|
|
171
|
+
self._call(kind, "", data)
|
|
172
|
+
return
|
|
173
|
+
# The release's number, before any handler runs: whatever
|
|
174
|
+
# move of this drag is still on its way is late from here on.
|
|
175
|
+
for shape in by_shape.values():
|
|
176
|
+
if _is_seq(shape.get("seq")):
|
|
177
|
+
self.released = max(self.released, shape["seq"])
|
|
178
|
+
try:
|
|
179
|
+
for i in sorted(by_shape):
|
|
180
|
+
self._call(kind, self._shape_name(i), by_shape[i])
|
|
181
|
+
finally:
|
|
182
|
+
if self._on_shape_moved is not None:
|
|
183
|
+
self._on_shape_moved()
|
|
184
|
+
return
|
|
185
|
+
points = data.get("points")
|
|
186
|
+
points = (
|
|
187
|
+
[p for p in points if isinstance(p, dict)]
|
|
188
|
+
if isinstance(points, list)
|
|
189
|
+
else []
|
|
190
|
+
)
|
|
191
|
+
chosen = next(
|
|
192
|
+
(p for p in points if _meta(p).get("kind") == "handle"),
|
|
193
|
+
points[0] if points else {},
|
|
194
|
+
)
|
|
195
|
+
# The figure layer stamps every trace's primitive id in `meta.id`
|
|
196
|
+
# (figures/plotly.py); marker groups carry per-point ids in
|
|
197
|
+
# `customdata`. The trace index is the last resort.
|
|
198
|
+
primitive = chosen.get("customdata")
|
|
199
|
+
if primitive is None:
|
|
200
|
+
primitive = _meta(chosen).get("id")
|
|
201
|
+
if primitive is None:
|
|
202
|
+
primitive = chosen.get("curveNumber")
|
|
203
|
+
self._call(kind, "" if primitive is None else str(primitive), data)
|
|
204
|
+
|
|
205
|
+
def _call(self, kind: str, primitive: str, payload: dict) -> None:
|
|
206
|
+
"""One event: the snapshot is taken here, and each entry is checked
|
|
207
|
+
by identity against the current list just before it is called."""
|
|
208
|
+
snapshot = list(self._handlers.get(kind, []))
|
|
209
|
+
was, self.dispatching = self.dispatching, True
|
|
210
|
+
try:
|
|
211
|
+
for entry in snapshot:
|
|
212
|
+
if not any(e is entry for e in self._handlers.get(kind, [])):
|
|
213
|
+
continue # removed since the snapshot
|
|
214
|
+
entry[1](primitive, payload)
|
|
215
|
+
finally:
|
|
216
|
+
self.dispatching = was
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _is_seq(value: object) -> bool:
|
|
220
|
+
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _by_shape(update: object) -> dict[int, dict]:
|
|
224
|
+
"""A relayout's ``shapes[N].<key>`` entries, grouped by shape index
|
|
225
|
+
with the prefix stripped; every other key is dropped."""
|
|
226
|
+
by_shape: dict[int, dict] = {}
|
|
227
|
+
if not isinstance(update, dict):
|
|
228
|
+
return by_shape
|
|
229
|
+
for key, value in update.items():
|
|
230
|
+
match = _SHAPE_KEY.match(key) if isinstance(key, str) else None
|
|
231
|
+
if match is not None:
|
|
232
|
+
by_shape.setdefault(int(match.group(1)), {})[match.group(2)] = value
|
|
233
|
+
return by_shape
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _meta(point: dict) -> dict:
|
|
237
|
+
trace = point.get("data")
|
|
238
|
+
meta = trace.get("meta") if isinstance(trace, dict) else None
|
|
239
|
+
return meta if isinstance(meta, dict) else {}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class _OwnedFigureHook:
|
|
243
|
+
"""`FigureHook.owned(owner)`: satisfies the capability-side protocol."""
|
|
244
|
+
|
|
245
|
+
def __init__(self, hook: FigureHook, owner: str) -> None:
|
|
246
|
+
self._hook, self._owner = hook, owner
|
|
247
|
+
|
|
248
|
+
def on(self, kind: str, handler: Callable[[str, dict], None]) -> None:
|
|
249
|
+
self._hook.on(kind, handler, owner=self._owner)
|