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,468 @@
|
|
|
1
|
+
"""A plot card's folded criteria rows (D152; the owner's R2): under an
|
|
2
|
+
array family's plot, the elements whose criterion is one and the same read
|
|
3
|
+
as ONE row — the metric, the family with the folded indices, a count by
|
|
4
|
+
verdict — whose chevron unfolds it to its members' own rows and whose click
|
|
5
|
+
opens an editor that changes every member at once. Which rows fold is
|
|
6
|
+
`authoring.family_rows.fold_rows`' rule; this draws them and knows none of
|
|
7
|
+
it.
|
|
8
|
+
|
|
9
|
+
The fold is DERIVED from the tree at every paint, and nothing remembers it:
|
|
10
|
+
a member whose criterion is edited is its own row at the next paint, and
|
|
11
|
+
one edited back folds again. So nothing here is rebuilt for it, and no row
|
|
12
|
+
is moved. The criteria's rows stand where the card drew them, flat and in
|
|
13
|
+
tree order; a fold's row is put in before its first member's, and a member
|
|
14
|
+
is that same row with two attributes — ``data-member`` (its indent) and
|
|
15
|
+
``hidden`` (while its fold is closed). Attributes, never classes: a server
|
|
16
|
+
repaint of a row's classes writes its whole class attribute (the U5 spike),
|
|
17
|
+
and both survive every repaint. Where a row is SHOWN is its CSS ``order``
|
|
18
|
+
in the column: a fold's row, then its members as one block, then — a
|
|
19
|
+
sibling of the fold, never between two of its members — the row of an
|
|
20
|
+
element with a criterion of its own. An editor being typed in is therefore
|
|
21
|
+
never re-parented, moved or redrawn by a row joining or leaving a fold (a
|
|
22
|
+
row moved in the document loses the focus inside it); the cost is that Tab
|
|
23
|
+
goes through the rows in tree order, not in the order shown.
|
|
24
|
+
|
|
25
|
+
Which folds are open is the browser tab's (`TabState.family_unfolded`),
|
|
26
|
+
kept as their members' paths so it follows the members — a fold whose first
|
|
27
|
+
member left it is still the fold that was opened."""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
from functools import partial
|
|
33
|
+
from typing import TYPE_CHECKING, Any
|
|
34
|
+
|
|
35
|
+
from nicegui import ui
|
|
36
|
+
|
|
37
|
+
from ...reporting.tree_rows import criteria_rows
|
|
38
|
+
from ..capabilities import Placement
|
|
39
|
+
from ..family_rows import Fold, criterion_of, fold_rows, is_fold, leaf_of, tally
|
|
40
|
+
from ..session import Selection
|
|
41
|
+
from ..spec_document import json_equal
|
|
42
|
+
from .actions import InlineHost, RowTarget
|
|
43
|
+
from .tree_card import _set_prop, leaf_status
|
|
44
|
+
from .widgets import MiddleCut, Pill
|
|
45
|
+
|
|
46
|
+
if TYPE_CHECKING:
|
|
47
|
+
from .plot_card import PlotCard
|
|
48
|
+
|
|
49
|
+
#: A folded row's editor hint (the controller's D3): fields only. A handle
|
|
50
|
+
#: on the plot belongs to ONE criterion's drawing, and twenty of them at
|
|
51
|
+
#: one place could not be told apart or dragged as one.
|
|
52
|
+
HINT = "These fields change all {n} criteria. Unfold to drag one on the plot."
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class _NoControls:
|
|
56
|
+
"""A folded row's place in `PlotCard.rows`' ``host``: it has no node
|
|
57
|
+
controls — `+`, wrap and `−` act on ONE node, and are its members'."""
|
|
58
|
+
|
|
59
|
+
def build(self) -> None:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
def follow(self) -> None:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
def clear(self) -> None:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class FoldRow:
|
|
70
|
+
"""One folded row. It stands in `PlotCard.rows` under its key
|
|
71
|
+
(`family_rows.fold_key`) beside the criteria's rows, so the page opens,
|
|
72
|
+
reveals, closes and scrolls to it exactly as it does theirs; what a
|
|
73
|
+
criterion's row has and this has not (`taker`, `host`) is here as
|
|
74
|
+
nothing."""
|
|
75
|
+
|
|
76
|
+
def __init__(self, folding: Folding, fold: Fold, crumb_ch: int) -> None:
|
|
77
|
+
card = folding.card
|
|
78
|
+
index, key = card.index, fold.key
|
|
79
|
+
self.folding, self.key, self.fold = folding, key, fold
|
|
80
|
+
self.taker: str | None = None
|
|
81
|
+
self.host = _NoControls()
|
|
82
|
+
#: What the open editor's fields were built from (`stamp`).
|
|
83
|
+
self._edited: str | None = None
|
|
84
|
+
self.row = ui.row().classes("dstf-crit dstf-fold items-center gap-3 w-full")
|
|
85
|
+
# The row IS the way in, as a criterion's is: its editor opens below
|
|
86
|
+
# it, and a second click closes it.
|
|
87
|
+
self.row.on("click", partial(card.page.row_click, index, key))
|
|
88
|
+
with self.row.mark(f"family:{index}:{key}"):
|
|
89
|
+
# In the swatch's place, so every row's name starts in one
|
|
90
|
+
# column. A button: Tab reaches it, Enter and Space toggle it.
|
|
91
|
+
self.chevron = (
|
|
92
|
+
ui.element("button")
|
|
93
|
+
.classes("dstf-chev dstf-fold-chev")
|
|
94
|
+
.props('type="button"')
|
|
95
|
+
.mark(f"family-chev:{index}:{key}")
|
|
96
|
+
.on(
|
|
97
|
+
"click",
|
|
98
|
+
partial(folding.toggle, key),
|
|
99
|
+
js_handler="(e) => { e.stopPropagation(); emit(); }",
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
with self.chevron:
|
|
103
|
+
ui.icon("expand_more").props('aria-hidden="true"')
|
|
104
|
+
if fold.crumbs:
|
|
105
|
+
ui.label(fold.crumbs).classes("dstf-crumb dstf-why").style(
|
|
106
|
+
f"min-width: {crumb_ch}ch"
|
|
107
|
+
)
|
|
108
|
+
self.pill = Pill("queued", f"family-pill:{index}:{key}")
|
|
109
|
+
self.name = MiddleCut().classes(
|
|
110
|
+
"dstf-crit-name text-body2 text-weight-medium"
|
|
111
|
+
)
|
|
112
|
+
self.count = (
|
|
113
|
+
ui.label()
|
|
114
|
+
.classes("dstf-mono text-xs")
|
|
115
|
+
.mark(f"family-count:{index}:{key}")
|
|
116
|
+
)
|
|
117
|
+
self.slot = ui.column().classes("w-full gap-0")
|
|
118
|
+
self.target = RowTarget(
|
|
119
|
+
card.page, f"family:{index}:{key}", self.row, leaf_of(key), index
|
|
120
|
+
)
|
|
121
|
+
self.target.slot = self.slot
|
|
122
|
+
|
|
123
|
+
def paint(self, fold: Fold, shut: bool) -> None:
|
|
124
|
+
from .plot_card import worst # plot_card imports this module
|
|
125
|
+
|
|
126
|
+
self.fold = fold
|
|
127
|
+
statuses = [leaf_status(leaf) for leaf in fold.leaves]
|
|
128
|
+
self.pill.set(worst(statuses))
|
|
129
|
+
self.name.set_text(fold.name)
|
|
130
|
+
# A family with nothing scored because of a run says the word its
|
|
131
|
+
# members' rows say (D151), not a count of verdicts it has none of.
|
|
132
|
+
self.count.set_text(str(fold.leaves[0].get("unscored") or tally(statuses)))
|
|
133
|
+
if _set_prop(self.row, "data-folded", "1" if shut else None):
|
|
134
|
+
self.row.update()
|
|
135
|
+
moved = _set_prop(self.chevron, "aria-expanded", "false" if shut else "true")
|
|
136
|
+
moved |= _set_prop(
|
|
137
|
+
self.chevron, "aria-label", f"{'Unfold' if shut else 'Fold'} {fold.name}"
|
|
138
|
+
)
|
|
139
|
+
if moved:
|
|
140
|
+
self.chevron.update()
|
|
141
|
+
card = self.folding.card
|
|
142
|
+
if card.open_path == self.key and card.hint is not None:
|
|
143
|
+
card.hint.set_text(self._hint())
|
|
144
|
+
|
|
145
|
+
# -- its editor ---------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
def _hint(self) -> str:
|
|
148
|
+
from .plot_card import STAGED # plot_card imports this module
|
|
149
|
+
|
|
150
|
+
return HINT.format(n=len(self.fold.paths)) + STAGED
|
|
151
|
+
|
|
152
|
+
def stamp(self, view: dict | None) -> str | None:
|
|
153
|
+
"""The criterion its members share, as ``view`` shows it — what
|
|
154
|
+
the open editor's fields say. ``None`` when they no longer share
|
|
155
|
+
one, or are no longer all there: the editor has nothing to show."""
|
|
156
|
+
wanted = set(self.fold.paths)
|
|
157
|
+
rows = criteria_rows(view, self.folding.card.subject)
|
|
158
|
+
leaves = [row["leaf"] for row in rows if str(row["leaf"]["path"]) in wanted]
|
|
159
|
+
if len(leaves) != len(wanted):
|
|
160
|
+
return None
|
|
161
|
+
criteria = [criterion_of(leaf) for leaf in leaves]
|
|
162
|
+
if not all(json_equal(criteria[0], other) for other in criteria[1:]):
|
|
163
|
+
return None
|
|
164
|
+
return json.dumps(criteria[0], sort_keys=True, default=str)
|
|
165
|
+
|
|
166
|
+
def open_editor(self, view: dict | None) -> None:
|
|
167
|
+
"""Its editor, below it, in the card's own editor fields — so
|
|
168
|
+
`PlotCard.close_editor`, the page's one-editor rule and its scroll
|
|
169
|
+
find it as they find a criterion's: the hint line and Done, then
|
|
170
|
+
every capability placed at FAMILY_CRITERION. No figure: nothing of
|
|
171
|
+
a fold is dragged."""
|
|
172
|
+
card, index, key = self.folding.card, self.folding.card.index, self.key
|
|
173
|
+
model = card.page.model_id
|
|
174
|
+
card.open_path, card._edited = key, None
|
|
175
|
+
self.row.classes(add="dstf-crit-open")
|
|
176
|
+
with self.slot:
|
|
177
|
+
card.editor_box = ui.column().classes("dstf-editor w-full gap-2")
|
|
178
|
+
with card.editor_box:
|
|
179
|
+
with ui.row().classes("items-center no-wrap gap-3 w-full"):
|
|
180
|
+
card.hint = (
|
|
181
|
+
ui.label(self._hint())
|
|
182
|
+
.classes("dstf-why")
|
|
183
|
+
.mark(f"editor-hint:{index}:{key}")
|
|
184
|
+
)
|
|
185
|
+
ui.space()
|
|
186
|
+
ui.button("Done", on_click=card.page.close_editor).props(
|
|
187
|
+
"flat dense no-caps"
|
|
188
|
+
).mark(f"editor-done:{index}:{key}")
|
|
189
|
+
# Where the host says a refusal: above the fields it is about.
|
|
190
|
+
self.target.slot = ui.column().classes("w-full gap-0")
|
|
191
|
+
card.editor = InlineHost(
|
|
192
|
+
card.page.shell,
|
|
193
|
+
Placement.FAMILY_CRITERION,
|
|
194
|
+
# Read at every build: the members are the fold's as it is.
|
|
195
|
+
lambda: Selection(
|
|
196
|
+
tests=frozenset({model}),
|
|
197
|
+
variable=self.fold.variables[0],
|
|
198
|
+
leaf_path=self.fold.paths[0],
|
|
199
|
+
leaf_paths=self.fold.paths,
|
|
200
|
+
),
|
|
201
|
+
figure=None,
|
|
202
|
+
marker=f"editor:{index}:{key}",
|
|
203
|
+
on_applied=self._applied,
|
|
204
|
+
row=self.target,
|
|
205
|
+
apply=card.page.apply,
|
|
206
|
+
)
|
|
207
|
+
card.editor.build()
|
|
208
|
+
self._edited = self.stamp(view)
|
|
209
|
+
|
|
210
|
+
def _applied(self, name: str) -> None:
|
|
211
|
+
"""A panel's own submit: recorded from a FRESH tree view, so its
|
|
212
|
+
edit is not read as one staged elsewhere (`PlotCard._applied`'s
|
|
213
|
+
rule). No panel is rebuilt: neither shows what the other edits,
|
|
214
|
+
and an Indices edit redraws the whole card."""
|
|
215
|
+
self._edited = self.stamp(self.folding.card._view())
|
|
216
|
+
|
|
217
|
+
def follow(self, view: dict | None) -> bool:
|
|
218
|
+
"""The open editor after a paint: False when its members no longer
|
|
219
|
+
share one criterion — a Discard, a Save, another tab's edit took
|
|
220
|
+
them apart — and it has to close; rebuilt when the criterion they
|
|
221
|
+
share moved by an edit staged ELSEWHERE, whose fields would stage
|
|
222
|
+
the undone value straight back."""
|
|
223
|
+
stamp = self.stamp(view)
|
|
224
|
+
if stamp is None:
|
|
225
|
+
return False
|
|
226
|
+
editor = self.folding.card.editor
|
|
227
|
+
if stamp != self._edited and editor is not None:
|
|
228
|
+
editor.build()
|
|
229
|
+
self._edited = stamp
|
|
230
|
+
return True
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _show_at(element: ui.element, order: int) -> None:
|
|
234
|
+
"""Where ``element`` is shown in its column (its CSS ``order``), sent
|
|
235
|
+
only when it moved."""
|
|
236
|
+
if element.style.get("order") != str(order):
|
|
237
|
+
element.style(f"order: {order}")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class Folding:
|
|
241
|
+
"""One card's folds: `PlotCard` builds it after its criteria rows,
|
|
242
|
+
calls `paint` wherever it paints them, and `released` when its editor
|
|
243
|
+
closes."""
|
|
244
|
+
|
|
245
|
+
def __init__(self, card: PlotCard, column: ui.column, crumb_ch: int) -> None:
|
|
246
|
+
self.card, self.column, self.crumb_ch = card, column, crumb_ch
|
|
247
|
+
#: Each member's fold, by the member's path, as last painted.
|
|
248
|
+
self.fold_of: dict[str, Fold] = {}
|
|
249
|
+
#: Whether the last paint held an open fold's members together
|
|
250
|
+
#: (`fold_rows`' ``pinned``): what `released` has to undo.
|
|
251
|
+
self.held = False
|
|
252
|
+
|
|
253
|
+
@property
|
|
254
|
+
def rows(self) -> dict[str, Any]:
|
|
255
|
+
"""`PlotCard.rows` as it IS: the criteria's rows by path, and each
|
|
256
|
+
`FoldRow` by its key among them (its docstring) — the card's own
|
|
257
|
+
annotation names the criteria's rows alone."""
|
|
258
|
+
return self.card.rows
|
|
259
|
+
|
|
260
|
+
# -- the tab's record ---------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def unfolded(self) -> set[str]:
|
|
264
|
+
"""The member paths of this test's unfolded folds, the tab's."""
|
|
265
|
+
page = self.card.page
|
|
266
|
+
return set(page.state.family_unfolded.get(page.model_id, []))
|
|
267
|
+
|
|
268
|
+
def _keep(self, unfolded: set[str]) -> None:
|
|
269
|
+
page = self.card.page
|
|
270
|
+
if unfolded:
|
|
271
|
+
page.state.family_unfolded[page.model_id] = sorted(unfolded)
|
|
272
|
+
else:
|
|
273
|
+
page.state.family_unfolded.pop(page.model_id, None)
|
|
274
|
+
|
|
275
|
+
# -- what is open on this card ------------------------------------------------
|
|
276
|
+
|
|
277
|
+
def _open(self) -> str | None:
|
|
278
|
+
"""What the page's one editor is open on, when it is on this card:
|
|
279
|
+
a fold's key or a criterion's path."""
|
|
280
|
+
is_open = self.card.page.open
|
|
281
|
+
if is_open is None or is_open[0] != self.card.index:
|
|
282
|
+
return None
|
|
283
|
+
return is_open[1]
|
|
284
|
+
|
|
285
|
+
def _under(self) -> set[str]:
|
|
286
|
+
"""The criteria rows something is open under — the page's editor or
|
|
287
|
+
its form row. A fold such a row is a member of is shown unfolded,
|
|
288
|
+
or what is open under it is open under nothing: a criterion typed
|
|
289
|
+
to its fold's value while that fold is shut joins it, and its
|
|
290
|
+
editor would go on staging edits from a row nobody can see."""
|
|
291
|
+
under = set()
|
|
292
|
+
opened = self._open()
|
|
293
|
+
if opened is not None and not is_fold(opened):
|
|
294
|
+
under.add(opened)
|
|
295
|
+
form = self.card.page.form
|
|
296
|
+
if form is not None:
|
|
297
|
+
under |= {
|
|
298
|
+
path
|
|
299
|
+
for path, row in self.rows.items()
|
|
300
|
+
if row.target is form[0] and not is_fold(path)
|
|
301
|
+
}
|
|
302
|
+
return under
|
|
303
|
+
|
|
304
|
+
# -- painting -----------------------------------------------------------------
|
|
305
|
+
|
|
306
|
+
def paint(self, view: dict | None) -> None:
|
|
307
|
+
"""The folds as the tree has them now, into the rows standing: a
|
|
308
|
+
fold's row comes, goes or is repainted, every criterion's row is a
|
|
309
|
+
member of one or its own, and each is shown in its place."""
|
|
310
|
+
card, rows = self.card, self.rows
|
|
311
|
+
if len(card.subject) < 2:
|
|
312
|
+
# One variable is no family: nothing on this card can fold, and
|
|
313
|
+
# most cards are this one — asked on every sync. It is also
|
|
314
|
+
# what keeps a UnitTests element out of every fold: each is a
|
|
315
|
+
# plot of its own (D140, `derive_plots`).
|
|
316
|
+
return
|
|
317
|
+
criteria = criteria_rows(view, card.subject)
|
|
318
|
+
standing = {path for path in rows if not is_fold(path)}
|
|
319
|
+
if {str(row["leaf"]["path"]) for row in criteria} != standing:
|
|
320
|
+
# The rows standing are another tree's — a leaf came or went,
|
|
321
|
+
# and the rebuild that draws the new ones follows. Reached when
|
|
322
|
+
# an editor closes on the way into that rebuild (`released`).
|
|
323
|
+
return
|
|
324
|
+
opened = self._open()
|
|
325
|
+
pinned: tuple[str, ...] = ()
|
|
326
|
+
if opened is not None and is_fold(opened) and opened in rows:
|
|
327
|
+
if rows[opened].follow(view):
|
|
328
|
+
# The open editor's members stay one fold whatever they
|
|
329
|
+
# equal (`fold_rows`): a value on its way through another
|
|
330
|
+
# group's must not merge the two under the typing hand.
|
|
331
|
+
pinned = rows[opened].fold.paths
|
|
332
|
+
else:
|
|
333
|
+
self.held = False # the close below has nothing to release
|
|
334
|
+
card.page.drop_editor()
|
|
335
|
+
self.held = bool(pinned)
|
|
336
|
+
entries = fold_rows(criteria, pinned=pinned)
|
|
337
|
+
folds = {entry.key: entry for entry in entries if isinstance(entry, Fold)}
|
|
338
|
+
for key in [key for key in rows if is_fold(key) and key not in folds]:
|
|
339
|
+
gone = rows.pop(key)
|
|
340
|
+
gone.slot.delete()
|
|
341
|
+
gone.row.delete()
|
|
342
|
+
self.fold_of = {path: fold for fold in folds.values() for path in fold.paths}
|
|
343
|
+
# A fold holding something open is unfolded, and stays so.
|
|
344
|
+
unfolded = self.unfolded
|
|
345
|
+
for path in self._under():
|
|
346
|
+
fold = self.fold_of.get(path)
|
|
347
|
+
if fold is not None and not unfolded & set(fold.paths):
|
|
348
|
+
unfolded |= set(fold.paths)
|
|
349
|
+
self._keep(unfolded)
|
|
350
|
+
for key, fold in folds.items():
|
|
351
|
+
row = rows.get(key)
|
|
352
|
+
if row is None:
|
|
353
|
+
row = self._add(fold)
|
|
354
|
+
row.paint(fold, not unfolded & set(fold.paths))
|
|
355
|
+
for path, row in rows.items():
|
|
356
|
+
if is_fold(path):
|
|
357
|
+
continue
|
|
358
|
+
fold = self.fold_of.get(path)
|
|
359
|
+
shut = fold is not None and not unfolded & set(fold.paths)
|
|
360
|
+
moved = _set_prop(row.row, "data-member", "1" if fold is not None else None)
|
|
361
|
+
moved |= _set_prop(row.row, "hidden", True if shut else None)
|
|
362
|
+
if moved:
|
|
363
|
+
row.row.update()
|
|
364
|
+
if _set_prop(row.slot, "hidden", True if shut else None):
|
|
365
|
+
row.slot.update()
|
|
366
|
+
self._lay_out(entries)
|
|
367
|
+
|
|
368
|
+
def _lay_out(self, entries: list[dict | Fold]) -> None:
|
|
369
|
+
"""Where each row is shown: `fold_rows`' order — a fold where its
|
|
370
|
+
first member's row was, a criterion's own row where it was — with
|
|
371
|
+
each fold's members as one block right under it, and every row's
|
|
372
|
+
slot (its editor's place) right under the row."""
|
|
373
|
+
shown: list[Any] = []
|
|
374
|
+
for entry in entries:
|
|
375
|
+
if isinstance(entry, Fold):
|
|
376
|
+
shown.append(self.rows[entry.key])
|
|
377
|
+
shown += [self.rows[path] for path in entry.paths]
|
|
378
|
+
else:
|
|
379
|
+
shown.append(self.rows[str(entry["leaf"]["path"])])
|
|
380
|
+
for k, row in enumerate(shown):
|
|
381
|
+
_show_at(row.row, 2 * k)
|
|
382
|
+
_show_at(row.slot, 2 * k + 1)
|
|
383
|
+
|
|
384
|
+
def _add(self, fold: Fold) -> FoldRow:
|
|
385
|
+
"""A fold's row and its slot, put in before its first member's row
|
|
386
|
+
— in the document too, so Tab reaches its chevron before them."""
|
|
387
|
+
with self.column:
|
|
388
|
+
row = FoldRow(self, fold, self.crumb_ch)
|
|
389
|
+
first = self.rows[fold.paths[0]].row
|
|
390
|
+
at = self.column.default_slot.children.index(first)
|
|
391
|
+
row.row.move(target_index=at)
|
|
392
|
+
row.slot.move(target_index=at + 1)
|
|
393
|
+
self.rows[fold.key] = row
|
|
394
|
+
return row
|
|
395
|
+
|
|
396
|
+
def released(self) -> None:
|
|
397
|
+
"""An editor on this card closed (`PlotCard.close_editor`'s last
|
|
398
|
+
word): if it was a fold's, its members are no longer held together,
|
|
399
|
+
and the rows are repainted from the tree — at once, since no event
|
|
400
|
+
follows a close. Left as they were, a fold typed to its neighbour's
|
|
401
|
+
value read as two rows of one criterion, and its Indices then
|
|
402
|
+
staged a second leaf on an element that had one (the executing
|
|
403
|
+
review's H1)."""
|
|
404
|
+
if self.held:
|
|
405
|
+
self.held = False
|
|
406
|
+
self.paint(self.card._view())
|
|
407
|
+
|
|
408
|
+
# -- folding ------------------------------------------------------------------
|
|
409
|
+
|
|
410
|
+
def toggle(self, key: str, *_: Any) -> None:
|
|
411
|
+
"""A chevron's click. Folding a row shut closes what is open under
|
|
412
|
+
its members — an editor nobody can see is no editor, and a form
|
|
413
|
+
nobody can see no form (the tree card's rule)."""
|
|
414
|
+
row = self.rows.get(key)
|
|
415
|
+
if row is None:
|
|
416
|
+
return
|
|
417
|
+
page, paths = self.card.page, set(row.fold.paths)
|
|
418
|
+
unfolded = self.unfolded
|
|
419
|
+
if unfolded & paths:
|
|
420
|
+
unfolded -= paths
|
|
421
|
+
if self._open() in paths:
|
|
422
|
+
page.close_editor()
|
|
423
|
+
for path in paths:
|
|
424
|
+
page.drop_row(self.rows[path].target)
|
|
425
|
+
else:
|
|
426
|
+
unfolded |= paths
|
|
427
|
+
if self._open() == key:
|
|
428
|
+
# The row its open editor is on: what the plot's line said
|
|
429
|
+
# of a FOLDED row is no longer so.
|
|
430
|
+
self.card.fold_unfolded()
|
|
431
|
+
self._keep(unfolded)
|
|
432
|
+
self.paint(self.card._view())
|
|
433
|
+
|
|
434
|
+
def unfold_to(self, path: str, view: dict | None) -> None:
|
|
435
|
+
"""Show ``path``'s own row: its fold, if it is in a closed one, is
|
|
436
|
+
unfolded — a criterion asked for by name (a jump, a reveal, the
|
|
437
|
+
tree's ⤓) is shown, not the row that folds it away."""
|
|
438
|
+
fold = self.fold_of.get(path)
|
|
439
|
+
unfolded = self.unfolded
|
|
440
|
+
if fold is not None and not unfolded & set(fold.paths):
|
|
441
|
+
self._keep(unfolded | set(fold.paths))
|
|
442
|
+
self.paint(view)
|
|
443
|
+
|
|
444
|
+
def opens(self, path: str, view: dict | None) -> bool:
|
|
445
|
+
"""`PlotCard.open_editor`'s first question: a fold's key opens that
|
|
446
|
+
fold's editor here (True); a criterion's path has its own row shown
|
|
447
|
+
first, and its editor is the card's to open."""
|
|
448
|
+
row = self.rows.get(path)
|
|
449
|
+
if isinstance(row, FoldRow):
|
|
450
|
+
row.open_editor(view)
|
|
451
|
+
return True
|
|
452
|
+
self.unfold_to(path, view)
|
|
453
|
+
return False
|
|
454
|
+
|
|
455
|
+
@staticmethod
|
|
456
|
+
def rekey(state: Any, model_id: str, paths: dict[str, str]) -> None:
|
|
457
|
+
"""An edit of the tree went through (`TestPage.edited`): each
|
|
458
|
+
unfolded member is recorded where the edit put it; one it removed
|
|
459
|
+
is dropped."""
|
|
460
|
+
now = sorted(
|
|
461
|
+
paths[path]
|
|
462
|
+
for path in state.family_unfolded.get(model_id, ())
|
|
463
|
+
if path in paths
|
|
464
|
+
)
|
|
465
|
+
if now:
|
|
466
|
+
state.family_unfolded[model_id] = now
|
|
467
|
+
else:
|
|
468
|
+
state.family_unfolded.pop(model_id, None)
|