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,144 @@
|
|
|
1
|
+
"""Queue (D150; brief §3.6): SUITE and TEST placements. The selected tests
|
|
2
|
+
are appended to the Session's simulation queue — one per tool process,
|
|
3
|
+
first in, first out — as an ``Enqueue``.
|
|
4
|
+
|
|
5
|
+
The capability starts nothing itself: the Session owns the queue and the
|
|
6
|
+
pool it runs on, reads each result back as its test ends and rescores that
|
|
7
|
+
test alone. A test already queued or running is not queued twice, and the
|
|
8
|
+
caption says how many of the selection that leaves out.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
from ...backends.pool import ItemState
|
|
16
|
+
from ..capabilities import Emphasis, Enqueue, Panel, Placement, plural
|
|
17
|
+
from ..situation import Situation
|
|
18
|
+
from ..spec_document import edit_counts
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from ..session import SessionView
|
|
22
|
+
|
|
23
|
+
# NO module-level `nicegui` import: `import dstf.authoring` runs
|
|
24
|
+
# register_builtins(), and the Session must import without the extra.
|
|
25
|
+
|
|
26
|
+
#: The states of a test the queue already holds.
|
|
27
|
+
_HELD = (ItemState.WAITING, ItemState.RUNNING)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _models(s: SessionView) -> tuple[list[str], list[str]]:
|
|
31
|
+
"""The selected models in project order — the order the queue takes —
|
|
32
|
+
as ``(to queue, already queued or running)``."""
|
|
33
|
+
project = s.project
|
|
34
|
+
if project is None:
|
|
35
|
+
return [], []
|
|
36
|
+
selection = s.selection.tests
|
|
37
|
+
new: list[str] = []
|
|
38
|
+
held: list[str] = []
|
|
39
|
+
for test in project.tests:
|
|
40
|
+
if test.model_id in selection:
|
|
41
|
+
(held if s.queue_state(test.model_id) in _HELD else new).append(
|
|
42
|
+
test.model_id
|
|
43
|
+
)
|
|
44
|
+
return new, held
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class EnqueueTests:
|
|
48
|
+
"""The selected tests appended to the simulation queue. Named for what
|
|
49
|
+
it does, not "queue": that word is the shell's for the queue itself,
|
|
50
|
+
and the shell names no capability (D129)."""
|
|
51
|
+
|
|
52
|
+
name = "enqueue"
|
|
53
|
+
label = "Queue"
|
|
54
|
+
placements = frozenset({Placement.SUITE, Placement.TEST})
|
|
55
|
+
#: drawn as this icon where a bar draws icons (the Suite toolbar)
|
|
56
|
+
icon = "playlist_add"
|
|
57
|
+
#: the Test page's strip: the next action when there is no result to
|
|
58
|
+
#: read, a criterion names a variable the last run did not record, or
|
|
59
|
+
#: its last run was cancelled or could not start; in the ⋮ while edits
|
|
60
|
+
#: are pending (their next action is Save) and while the queue holds
|
|
61
|
+
#: the test (the strip's own Remove or Cancel is the action then)
|
|
62
|
+
emphasis = {
|
|
63
|
+
Situation.SIM_FAIL: Emphasis.NEXT,
|
|
64
|
+
Situation.NO_RESULT: Emphasis.NEXT,
|
|
65
|
+
Situation.CANCELLED: Emphasis.NEXT,
|
|
66
|
+
Situation.START_FAILED: Emphasis.NEXT,
|
|
67
|
+
Situation.EDITS: Emphasis.MENU,
|
|
68
|
+
Situation.EDITS_NEW: Emphasis.NEXT,
|
|
69
|
+
Situation.NEW_VARIABLES: Emphasis.NEXT,
|
|
70
|
+
Situation.QUEUED: Emphasis.MENU,
|
|
71
|
+
Situation.RUNNING: Emphasis.MENU,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
def available(self, s: SessionView) -> bool | str:
|
|
75
|
+
if s.project is None:
|
|
76
|
+
return "no project open"
|
|
77
|
+
if not s.selection.tests:
|
|
78
|
+
return "Queue needs a selection"
|
|
79
|
+
refusal = s.queue_refusal
|
|
80
|
+
if refusal is not None:
|
|
81
|
+
# The backend cannot run one test at a time, or cannot be built
|
|
82
|
+
# here: its own sentence, from the first attempt on.
|
|
83
|
+
return refusal
|
|
84
|
+
new, held = _models(s)
|
|
85
|
+
if not new and held:
|
|
86
|
+
if len(held) == 1:
|
|
87
|
+
doing = (
|
|
88
|
+
"running"
|
|
89
|
+
if s.queue_state(held[0]) is ItemState.RUNNING
|
|
90
|
+
else "queued"
|
|
91
|
+
)
|
|
92
|
+
return f"{held[0]} is already {doing}"
|
|
93
|
+
return f"all {len(held)} selected tests are already queued or running"
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
def caption(self, s: SessionView) -> str:
|
|
97
|
+
"""The count, how many of the selection the queue already holds
|
|
98
|
+
("Queue 3 (2 already queued)") and the selection's unsaved edits
|
|
99
|
+
(brief §3.6: a run uses the spec with pending edits applied, and
|
|
100
|
+
the button says so) — as they stand when each test STARTS, which is
|
|
101
|
+
when the queue takes a test's settings. One test whose last run in
|
|
102
|
+
the queue was cancelled or failed reads "Queue again". No backend:
|
|
103
|
+
the header shows it (owner ruling, 2026-09-26); the panel names
|
|
104
|
+
it."""
|
|
105
|
+
new, held = _models(s)
|
|
106
|
+
again = (ItemState.CANCELLED, ItemState.FAILED)
|
|
107
|
+
if len(new) == 1 and not held and s.queue_state(new[0]) in again:
|
|
108
|
+
return "Queue again"
|
|
109
|
+
notes = []
|
|
110
|
+
if held:
|
|
111
|
+
notes.append(f"{len(held)} already queued")
|
|
112
|
+
models = set(new)
|
|
113
|
+
counts = edit_counts(s.pending)
|
|
114
|
+
edits = sum(counts[model_id] for model_id in models)
|
|
115
|
+
if edits:
|
|
116
|
+
notes.append(
|
|
117
|
+
f"with {plural(edits, 'unsaved edit')}, as they stand when each starts"
|
|
118
|
+
)
|
|
119
|
+
return f"Queue {len(new)}" + (f" ({'; '.join(notes)})" if notes else "")
|
|
120
|
+
|
|
121
|
+
def panel(self, s: SessionView, ui_panel: Panel) -> None:
|
|
122
|
+
from nicegui import ui
|
|
123
|
+
|
|
124
|
+
project = s.project
|
|
125
|
+
assert project is not None # `available` greys the button without one
|
|
126
|
+
ui.label(f"runs through {project.config.backend_name}").classes("text-xs").mark(
|
|
127
|
+
"queue-backend"
|
|
128
|
+
)
|
|
129
|
+
new, held = _models(s)
|
|
130
|
+
for model_id in new:
|
|
131
|
+
ui.label(model_id).classes("text-xs")
|
|
132
|
+
for model_id in held:
|
|
133
|
+
ui.label(f"{model_id} · already in the queue").classes(
|
|
134
|
+
"text-xs text-secondary"
|
|
135
|
+
)
|
|
136
|
+
ui.button(self.caption(s), on_click=lambda: ui_panel.submit()).props(
|
|
137
|
+
"dense no-caps"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def act(self, s: SessionView, **inputs: Any) -> Enqueue:
|
|
141
|
+
if s.project is None:
|
|
142
|
+
raise ValueError("no project open")
|
|
143
|
+
new, _held = _models(s)
|
|
144
|
+
return Enqueue(models=tuple(new))
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
"""The capability contract (two-tier design, §4.6).
|
|
2
|
+
|
|
3
|
+
A capability is a plug-in the authoring shell draws at the placements it
|
|
4
|
+
names: it reads a :class:`SessionView`, builds its controls in a
|
|
5
|
+
:class:`Panel` the shell provides, and returns an :class:`Outcome`. **The Session applies outcomes;
|
|
6
|
+
a capability applies nothing.** It holds a view only, so the enforcement
|
|
7
|
+
is structural. Registration goes through the single ``dstf.plugins``
|
|
8
|
+
entry-point group: a plug-in module calls :func:`register_capability` on
|
|
9
|
+
import exactly as it calls ``register_metric``.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from collections.abc import Callable, Hashable, Sequence
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from ..storage.reference_store import ReferenceStore
|
|
22
|
+
from .session import SessionView
|
|
23
|
+
from .spec_document import PatchOp
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Placement(Enum):
|
|
27
|
+
"""Where a capability attaches (brief §3.2; D129): the Suite page's
|
|
28
|
+
batch bar, the Test page's status strip, a plot card's criteria header,
|
|
29
|
+
the open criterion's editor, a tree node's place in the tree, a row of
|
|
30
|
+
the Statistics card and a statistics criterion's editor there. A capability
|
|
31
|
+
names the frozenset it attaches at; the shell draws it at each and knows
|
|
32
|
+
no capability by name. The tab strip this replaces listed one tab per
|
|
33
|
+
``Scope``; the scopes' selection rules (TESTS needs a selection, LEAF a
|
|
34
|
+
leaf) are now what ``available`` answers for the page's selection.
|
|
35
|
+
|
|
36
|
+
SUITE and TEST are a button carrying the capability's ``caption``, its
|
|
37
|
+
panel in a dialog; VARIABLE, CRITERION, NODE and STATISTIC are inline —
|
|
38
|
+
the panel is built straight into the plot's criteria header (for the
|
|
39
|
+
plot's first tracked variable, its rows opening under the header), the
|
|
40
|
+
criterion's editor, beside a tree node as that node's controls, or at
|
|
41
|
+
the end of a Statistics row no criterion reads yet (the selection's
|
|
42
|
+
``quantity`` is the row's), with no button and no heading.
|
|
43
|
+
STATISTIC_CRITERION is the editor of a criterion scored through a
|
|
44
|
+
statistic, under its Statistics row: CRITERION's with no plot, so a
|
|
45
|
+
capability that needs one is not offered there. TREE_CRITERION is the
|
|
46
|
+
editor of any criterion opened under its row in the criteria tree
|
|
47
|
+
(U5): CRITERION's with no plot, like STATISTIC_CRITERION — the plot's
|
|
48
|
+
own editor is a link away. FAMILY_CRITERION is the editor of a folded
|
|
49
|
+
criteria row (D152): one criterion on several elements of an array,
|
|
50
|
+
every one of them named by the selection's ``leaf_paths`` (``leaf_path``
|
|
51
|
+
the first) — CRITERION's with no plot again, since nothing is dragged
|
|
52
|
+
for twenty criteria at once, so a capability that edits ONE leaf is not
|
|
53
|
+
offered there. At VARIABLE and
|
|
54
|
+
CRITERION ``Panel.figure`` is the plot's hook, subscribed for as long as
|
|
55
|
+
the panel is drawn; elsewhere a subscription raises."""
|
|
56
|
+
|
|
57
|
+
SUITE = "suite"
|
|
58
|
+
TEST = "test"
|
|
59
|
+
VARIABLE = "variable"
|
|
60
|
+
CRITERION = "criterion"
|
|
61
|
+
NODE = "node"
|
|
62
|
+
STATISTIC = "statistic"
|
|
63
|
+
STATISTIC_CRITERION = "statistic-criterion"
|
|
64
|
+
TREE_CRITERION = "tree-criterion"
|
|
65
|
+
FAMILY_CRITERION = "family-criterion"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Emphasis(Enum):
|
|
69
|
+
"""How a bar that is told the page's situation draws a capability
|
|
70
|
+
(``Capability.emphasis``): NEXT is the one next action, drawn filled;
|
|
71
|
+
MENU goes into the bar's ⋮ menu. A situation a capability does not
|
|
72
|
+
name draws it quiet."""
|
|
73
|
+
|
|
74
|
+
NEXT = "next"
|
|
75
|
+
MENU = "menu"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def plural(count: int, noun: str, many: str | None = None) -> str:
|
|
79
|
+
"""``"2 tests"``, ``"1 test"``: the count and its noun for a caption. A
|
|
80
|
+
regular plural adds ``s``; ``many`` spells one that does not
|
|
81
|
+
(``plural(2, "new entry", "new entries")``)."""
|
|
82
|
+
if count == 1:
|
|
83
|
+
return f"{count} {noun}"
|
|
84
|
+
return f"{count} {many}" if many is not None else f"{count} {noun}s"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True, kw_only=True)
|
|
88
|
+
class Patch:
|
|
89
|
+
"""Ops to stage; the Session rescores their models."""
|
|
90
|
+
|
|
91
|
+
ops: list[PatchOp]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(frozen=True, kw_only=True)
|
|
95
|
+
class StoreWrite:
|
|
96
|
+
"""A write against the reference store; results reload afterwards.
|
|
97
|
+
``models`` names what it touches: a queued or running one refuses it.
|
|
98
|
+
|
|
99
|
+
``run`` returns the one line the queue panel shows — what the write did,
|
|
100
|
+
including what it refused and why — or ``None`` when there is nothing
|
|
101
|
+
to say. A partial write has no other way to report its skips, and
|
|
102
|
+
raising after some baselines were stored would mislead."""
|
|
103
|
+
|
|
104
|
+
run: Callable[[ReferenceStore], str | None]
|
|
105
|
+
models: frozenset[str]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass(frozen=True, kw_only=True)
|
|
109
|
+
class Enqueue:
|
|
110
|
+
"""Tests for the simulation queue (D150), in the order to run them. The
|
|
111
|
+
Session appends them — a test already queued or running is skipped —
|
|
112
|
+
and each result is read back and rescored as its test ends. A queued
|
|
113
|
+
test runs with the spec as it stands when it STARTS, pending edits
|
|
114
|
+
applied."""
|
|
115
|
+
|
|
116
|
+
models: tuple[str, ...]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass(frozen=True, kw_only=True)
|
|
120
|
+
class Artifact:
|
|
121
|
+
"""Something written outside the spec and the store; shown as a link."""
|
|
122
|
+
|
|
123
|
+
paths: list[Path]
|
|
124
|
+
label: str
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass(frozen=True, kw_only=True)
|
|
128
|
+
class OpenProject:
|
|
129
|
+
config_path: Path
|
|
130
|
+
work_dir: Path | None = None # the run's work dir, when not the default
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
Outcome = Patch | StoreWrite | Enqueue | Artifact | OpenProject | None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class FigureHook(Protocol):
|
|
137
|
+
"""ONE plot card's figure, as a capability may subscribe to it. There is
|
|
138
|
+
a hook per card — the plot a VARIABLE panel (its criteria header) or a
|
|
139
|
+
CRITERION editor sits under — not one figure for the page.
|
|
140
|
+
|
|
141
|
+
``on(kind, handler)`` calls ``handler(primitive_id, payload)``:
|
|
142
|
+
|
|
143
|
+
- ``plotly_click`` / ``plotly_selected``: the id of the first point whose
|
|
144
|
+
trace ``meta.kind`` is ``"handle"``, else of the first point
|
|
145
|
+
(``customdata``, then ``meta.id``, then ``curveNumber``), with the event
|
|
146
|
+
payload — under x-unified hover a click lists every trace at that x.
|
|
147
|
+
- ``plotly_relayout`` moving shapes: once PER SHAPE, the payload being
|
|
148
|
+
that shape's keys without their ``shapes[N].`` prefix
|
|
149
|
+
(``{"y0": 6.0, "y1": 6.0}`` for a line; ``{"xanchor": 4.0, "yanchor":
|
|
150
|
+
2.99, "path": "M …"}`` for a ring), in data coordinates.
|
|
151
|
+
Plotly's payload never names the shape; the shell resolves the id from
|
|
152
|
+
the rendered figure's ``layout.shapes[N].name``, ``""`` when it cannot.
|
|
153
|
+
The shell does not interpret positions: the capability does, from the
|
|
154
|
+
handle's primitive type (a Marker's ring moves only by
|
|
155
|
+
``xanchor``/``yanchor``).
|
|
156
|
+
A relayout mixing shape keys with others is a shape relayout.
|
|
157
|
+
- Any other relayout (a zoom): once, with id ``""`` and the raw payload.
|
|
158
|
+
- ``dstf_move`` (D149): a handle HELD on the plot, at most about 15
|
|
159
|
+
times a second — once per shape, with the payload its release would
|
|
160
|
+
carry (so a capability reads both with one function) plus ``drag``,
|
|
161
|
+
the number of the drag it is of; or once with id ``""`` and
|
|
162
|
+
``{"end": True}`` for a drag that ended without a release. A move is
|
|
163
|
+
a preview: nothing is to be staged from it. One that arrives after
|
|
164
|
+
its drag's release is dropped by the shell.
|
|
165
|
+
- ``dstf_click`` (D149): a click on the plot that is no drag, no
|
|
166
|
+
double-click and on no draggable shape — id ``""`` and ``{"xaxis",
|
|
167
|
+
"yaxis", "x", "y", "xrange", "yrange", "ctrl", "shift", "alt",
|
|
168
|
+
"meta"}``: the axes of the panel under the pointer, the position in
|
|
169
|
+
them and what they showed.
|
|
170
|
+
- A release's and a move's per-shape payload also carries ``visible``,
|
|
171
|
+
``{"x": span, "y": span}``: what the shape's axes showed at the
|
|
172
|
+
press, which a placed value is snapped by.
|
|
173
|
+
|
|
174
|
+
Handler semantics: each shape index is dispatched in turn, ascending,
|
|
175
|
+
from its own snapshot of the handlers, taken when that shape's dispatch
|
|
176
|
+
starts. A handler registered since the snapshot is not called for that
|
|
177
|
+
shape; one removed since the snapshot (its panel rebuilt) is skipped,
|
|
178
|
+
checked by identity. A handler registered twice is called twice. A
|
|
179
|
+
handler that raises ENDS the relayout: later handlers and later shapes
|
|
180
|
+
are not dispatched, the exception propagates, and the shell still
|
|
181
|
+
re-sends the figure so the plot snaps back."""
|
|
182
|
+
|
|
183
|
+
def on(self, kind: str, handler: Callable[[str, dict], None]) -> None: ...
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class Panel(Protocol):
|
|
187
|
+
"""A container the capability owns entirely (the shell's
|
|
188
|
+
``ShellPanel``). ``figure`` is the plot the panel is drawn beside, as a
|
|
189
|
+
capability subscribes to it — what Edit on plot reads its drags from.
|
|
190
|
+
|
|
191
|
+
``kept`` is a dict of the panel's own that OUTLIVES the panel: what its
|
|
192
|
+
controls show that the spec does not hold — a required cell cleared to
|
|
193
|
+
be retyped, a tolerance with one side typed — and that a rebuild from
|
|
194
|
+
the spec alone would lose. A panel is rebuilt by edits it did not make
|
|
195
|
+
(a drag on the plot, another panel's submit), and is never told: it
|
|
196
|
+
writes there as its controls change and reads it back when it is
|
|
197
|
+
built. The host hands the same dict to every build of this capability's
|
|
198
|
+
panel for the same node of the same test, re-keys it when a tree edit
|
|
199
|
+
moves the node, and drops it — the panel is then rebuilt on an empty
|
|
200
|
+
one — with the node, and when an edit that is not this tab's lands on
|
|
201
|
+
the test: a Discard, a Save, another tab's. The record is the browser
|
|
202
|
+
tab's: it outlives the page. The criterion editor under a plot keeps
|
|
203
|
+
one; every other host — a dialog, a row, the criterion editors under a
|
|
204
|
+
tree row and under a Statistics row — hands a fresh dict to every
|
|
205
|
+
build (`tests/test_capability_conformance.py` holds both)."""
|
|
206
|
+
|
|
207
|
+
figure: FigureHook
|
|
208
|
+
kept: dict[Any, Any]
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def can_place(self) -> bool:
|
|
212
|
+
"""Whether this panel is beside a plot whose clicks it may take
|
|
213
|
+
(D149): the criterion editor under a plot, and no other host."""
|
|
214
|
+
...
|
|
215
|
+
|
|
216
|
+
def place_button(self, pointer: Hashable, label: str) -> None:
|
|
217
|
+
"""Draw a ⌖ for what this panel calls ``pointer`` — into the
|
|
218
|
+
current slot; a number field's ``append`` slot is the place — that
|
|
219
|
+
ARMS it (D149): the next plain click on the plot is handed to
|
|
220
|
+
`on_place` with that ``pointer``. ``label`` is what the hint line
|
|
221
|
+
says is being set. Draw one only where a click can set the thing in
|
|
222
|
+
the state it is in. The builtins name a field by its keys into the
|
|
223
|
+
leaf (``("tolerance", "upper")``) and a table's row by its table
|
|
224
|
+
and index (``("points", 3)``). One is armed on a page at a time;
|
|
225
|
+
pressing ⌖ again, Esc, closing the editor, `disarm` and any edit of
|
|
226
|
+
the test disarm. Draws nothing where `can_place` is false."""
|
|
227
|
+
...
|
|
228
|
+
|
|
229
|
+
def disarm(self) -> None:
|
|
230
|
+
"""Nothing of this editor is armed any more: for a panel to call
|
|
231
|
+
when what a ⌖ named has moved under it without an edit saying so —
|
|
232
|
+
a table's row added or removed, its fields swapped for another
|
|
233
|
+
mode's. A no-op where `can_place` is false."""
|
|
234
|
+
...
|
|
235
|
+
|
|
236
|
+
def on_place(self, handler: Callable[[Hashable, dict], bool]) -> None:
|
|
237
|
+
"""``handler(pointer, click)`` is called when the plot is clicked
|
|
238
|
+
while a field THIS panel drew a ⌖ for is armed — ``click`` is the
|
|
239
|
+
``dstf_click`` payload with ``label``, the armed ⌖'s, beside it. It
|
|
240
|
+
returns whether the click went through
|
|
241
|
+
(`submit`'s answer): then the field is disarmed; refused, it stays
|
|
242
|
+
armed, and the refusal is said under the plot. Registers nothing
|
|
243
|
+
where the panel is beside no plot."""
|
|
244
|
+
...
|
|
245
|
+
|
|
246
|
+
def preview(self, leaf: dict | None, revision: int = 0) -> None:
|
|
247
|
+
"""Hand every panel of this editor the leaf a handle held on the
|
|
248
|
+
plot WOULD write (D149), or None when it is over — each shows it in
|
|
249
|
+
its own fields (`on_preview`), and nothing is staged. ``revision``
|
|
250
|
+
is the drag's number, the same for every move of one drag. A no-op
|
|
251
|
+
where the panel is beside no plot."""
|
|
252
|
+
...
|
|
253
|
+
|
|
254
|
+
def on_preview(self, handler: Callable[[dict | None, int], None]) -> None:
|
|
255
|
+
"""``handler(leaf, revision)`` is called with what any panel of
|
|
256
|
+
this editor handed `preview`. Registers nothing where the panel is
|
|
257
|
+
beside no plot."""
|
|
258
|
+
...
|
|
259
|
+
|
|
260
|
+
def submit(self, **inputs: Any) -> bool:
|
|
261
|
+
"""Hand the capability's inputs to the shell, which calls ``act`` and
|
|
262
|
+
applies the outcome; an exception shows with the panel — above an
|
|
263
|
+
inline one, in the dialog of a placement that is a button. Returns
|
|
264
|
+
whether it went through: ``False`` when ``act`` or the apply
|
|
265
|
+
raised. A form row closes on ``True`` and only then — refused, it
|
|
266
|
+
stays open with what was typed, and the refusal shows under the
|
|
267
|
+
node's row, above the form."""
|
|
268
|
+
...
|
|
269
|
+
|
|
270
|
+
def row(self, title: str, build: Callable[[Callable[[], None]], None]) -> None:
|
|
271
|
+
"""Open a form row below the row this panel is drawn in, titled
|
|
272
|
+
``title``. ``build(close)`` draws its content — fields, a sentence,
|
|
273
|
+
its buttons; ``close`` takes the row away — after a ``submit`` that
|
|
274
|
+
went through, never before it (a refused act keeps the row and
|
|
275
|
+
what was typed in it). ONE thing is open at a time on a page: this
|
|
276
|
+
closes any other row and the criterion editor. While it is open,
|
|
277
|
+
``build`` is called AGAIN, into an emptied row, when an edit staged
|
|
278
|
+
elsewhere moves the node the panel is for — a form never shows a
|
|
279
|
+
value the node no longer has — and it closes with the host, when
|
|
280
|
+
its row is hidden, and when its node is gone. In a dialog (a
|
|
281
|
+
placement that is a button) the form is drawn in the dialog, below
|
|
282
|
+
the panel. A panel with no row to open one under — the criterion
|
|
283
|
+
editor — raises."""
|
|
284
|
+
...
|
|
285
|
+
|
|
286
|
+
def retitle(self, title: str) -> None:
|
|
287
|
+
"""Say the title of the form row this panel opened again — a form
|
|
288
|
+
whose subject follows a choice made in it (the add form's `Add a
|
|
289
|
+
criterion for pipe.T[1:20]` follows its Indices). The row keeps it
|
|
290
|
+
when the page builds it again. A no-op while no row of this
|
|
291
|
+
panel's is open, and where a form is drawn in a dialog."""
|
|
292
|
+
...
|
|
293
|
+
|
|
294
|
+
def lead(self, build: Callable[[], None]) -> None:
|
|
295
|
+
"""Draw ``build()`` in the node's lead place — beside its status,
|
|
296
|
+
where its kind is read (a combinator's kind dropdown). The place is
|
|
297
|
+
emptied first, and with the panel; while it holds something the
|
|
298
|
+
row's text leaves the kind out. A row without one (a leaf's, a
|
|
299
|
+
criteria row's) raises, like ``row`` where there is no row."""
|
|
300
|
+
...
|
|
301
|
+
|
|
302
|
+
def reveal(self, leaf_path: str) -> None:
|
|
303
|
+
"""Show that criterion's editor once the page has drawn it — after
|
|
304
|
+
an add, the new criterion. A no-op where no page reveals criteria:
|
|
305
|
+
a dialog, the criterion editor."""
|
|
306
|
+
...
|
|
307
|
+
|
|
308
|
+
@property
|
|
309
|
+
def can_move(self) -> bool:
|
|
310
|
+
"""Whether the host offers moving the node this panel is drawn for
|
|
311
|
+
(U4, V2): a tree card's row, and nowhere else — a plot's criteria
|
|
312
|
+
row hosts the same NODE placement and moves nothing."""
|
|
313
|
+
...
|
|
314
|
+
|
|
315
|
+
def movable(
|
|
316
|
+
self,
|
|
317
|
+
on_drop: Callable[[str, str], None],
|
|
318
|
+
zones: Callable[[], dict[str, dict[str, str | None]]] | None = None,
|
|
319
|
+
) -> bool:
|
|
320
|
+
"""Take part in the host's drag (U4) where it offers moving
|
|
321
|
+
(`can_move`); elsewhere register nothing and return False.
|
|
322
|
+
``on_drop(src_path, zone)`` is called when a node is dropped on
|
|
323
|
+
this panel's row — ``zone`` is ``before`` (the row's top half) or
|
|
324
|
+
``onto`` (its bottom half; the root's whole row). ``zones`` makes
|
|
325
|
+
the row draggable: called once at the start of a drag from it, it
|
|
326
|
+
answers ``{row path: {zone: reason}}`` — ``None`` allowed, ``""``
|
|
327
|
+
where the node already is, else the sentence refusing it — which
|
|
328
|
+
the page draws over every row while dragging, with no round trip
|
|
329
|
+
to the server."""
|
|
330
|
+
...
|
|
331
|
+
|
|
332
|
+
def on_click(self, handler: Callable[[], None]) -> bool:
|
|
333
|
+
"""What a click on the row this panel is drawn in does (U5, Q6):
|
|
334
|
+
``handler()``, called when the row itself is clicked — or, from the
|
|
335
|
+
keyboard, Enter or Space on it — never a control inside it. Offered
|
|
336
|
+
by the tree card's GROUP rows (a combinator's) and nowhere else
|
|
337
|
+
(`can_move`'s rule): a criterion's row — a plot's criteria row or
|
|
338
|
+
the tree's — has a click of its own, its editor. Elsewhere it
|
|
339
|
+
registers nothing and returns False. One per row; a later call
|
|
340
|
+
replaces it, and it goes with the panel."""
|
|
341
|
+
...
|
|
342
|
+
|
|
343
|
+
def moved(self, node_path: str, text: str, paths: dict[str, str]) -> None:
|
|
344
|
+
"""A move just went through (U4, V4): every open editor and form row
|
|
345
|
+
on the page closes NOW — two criteria on one variable trading
|
|
346
|
+
places move no shape, and an editor left open would show the other
|
|
347
|
+
one — and once the tree is redrawn ``node_path``'s row is brought
|
|
348
|
+
into view with ``text`` as one line under it, until the next form,
|
|
349
|
+
editor or edit. ``paths`` is every node the move kept, its path
|
|
350
|
+
before -> after (`Moved.paths`): what the page keys by a node's
|
|
351
|
+
path follows it, as for `edited`. A no-op where no page shows the
|
|
352
|
+
tree."""
|
|
353
|
+
...
|
|
354
|
+
|
|
355
|
+
def edited(self, paths: dict[str, str]) -> None:
|
|
356
|
+
"""Any other edit of the tree just went through: ``paths`` is every
|
|
357
|
+
node it kept, its path before -> after (`tree_move.path_map`) — an
|
|
358
|
+
add or a remove shifts the siblings after it, a wrap pushes its node
|
|
359
|
+
one level down, an unwrap lifts its children. What the page keys by
|
|
360
|
+
a node's path follows it (the tree card's folded groups, U5 R1);
|
|
361
|
+
a node not in it is gone. A no-op where no page shows the tree."""
|
|
362
|
+
...
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
@runtime_checkable
|
|
366
|
+
class Capability(Protocol):
|
|
367
|
+
"""A plug-in the shell draws at its placements. It MAY also declare
|
|
368
|
+
``icon`` (a Material icon name): a bar that draws icons (the Suite
|
|
369
|
+
toolbar) draws it as that icon, its caption the tooltip; one without
|
|
370
|
+
keeps its text button. Not a protocol member — a runtime-checkable
|
|
371
|
+
protocol requires every member, and most capabilities have no icon —
|
|
372
|
+
so the shell reads it with ``getattr``.
|
|
373
|
+
|
|
374
|
+
It MAY also declare ``emphasis``, read the same way: a mapping from a
|
|
375
|
+
:class:`~dstf.authoring.situation.Situation` to :attr:`Emphasis.NEXT`
|
|
376
|
+
— drawn filled, the page's one next action — or :attr:`Emphasis.MENU`
|
|
377
|
+
— drawn in the bar's ⋮ menu; a situation it does not name draws it
|
|
378
|
+
quiet. Only a bar the page tells its situation reads it (the Test
|
|
379
|
+
page's status strip); every other bar draws today's outlined button."""
|
|
380
|
+
|
|
381
|
+
name: str
|
|
382
|
+
label: str
|
|
383
|
+
placements: frozenset[Placement]
|
|
384
|
+
|
|
385
|
+
def available(self, s: SessionView) -> bool | str:
|
|
386
|
+
"""``True``, or ``False`` / the sentence saying why it is unavailable
|
|
387
|
+
and what would enable it ("Accept needs a selection", "FakeLib.A is
|
|
388
|
+
already queued"). The greyed button carries it where the
|
|
389
|
+
placement draws one; at CRITERION, NODE, STATISTIC and
|
|
390
|
+
STATISTIC_CRITERION it is shown in the panel's place."""
|
|
391
|
+
...
|
|
392
|
+
|
|
393
|
+
def caption(self, s: SessionView) -> str:
|
|
394
|
+
"""The button's label: its object, its effect and a count
|
|
395
|
+
("Accept 2 results as baselines"). At zero it still says what it
|
|
396
|
+
would do; ``available`` then carries the sentence. The inline
|
|
397
|
+
placements (CRITERION, NODE, STATISTIC, STATISTIC_CRITERION) draw
|
|
398
|
+
no button and never read it."""
|
|
399
|
+
...
|
|
400
|
+
|
|
401
|
+
def panel(self, s: SessionView, ui: Panel) -> None: ...
|
|
402
|
+
|
|
403
|
+
def act(self, s: SessionView, **inputs: Any) -> Outcome | Sequence[Outcome]:
|
|
404
|
+
"""What this capability's inputs DO, as an outcome the Session
|
|
405
|
+
applies — a Patch, a StoreWrite, an Enqueue, an Artifact, an OpenProject,
|
|
406
|
+
or a sequence of them.
|
|
407
|
+
|
|
408
|
+
Returning ``None`` means nothing happened: the host applies
|
|
409
|
+
nothing, leaves a dialog open (there is nothing to close over) and
|
|
410
|
+
rebuilds no panel. It is a real answer — every plot sends
|
|
411
|
+
``{"autosize": true}`` at mount, and a subscribing panel answers
|
|
412
|
+
that with None — but it is a silent one, so a capability that
|
|
413
|
+
returns None on purpose says why in its own panel."""
|
|
414
|
+
...
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
_CAPABILITIES: dict[str, Capability] = {}
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def register_capability(cap: Capability) -> Capability:
|
|
421
|
+
"""Register ``cap`` under its name; returns it. Loud on a duplicate —
|
|
422
|
+
two capabilities sharing a name would make the shell's button order
|
|
423
|
+
depend on import order — and on a shape the shell cannot draw."""
|
|
424
|
+
if not isinstance(cap, Capability):
|
|
425
|
+
missing = [
|
|
426
|
+
a
|
|
427
|
+
for a in (
|
|
428
|
+
"name",
|
|
429
|
+
"label",
|
|
430
|
+
"placements",
|
|
431
|
+
"available",
|
|
432
|
+
"caption",
|
|
433
|
+
"panel",
|
|
434
|
+
"act",
|
|
435
|
+
)
|
|
436
|
+
if not hasattr(cap, a)
|
|
437
|
+
]
|
|
438
|
+
raise TypeError(
|
|
439
|
+
f"{cap!r} is not a Capability: missing {', '.join(missing) or 'a method'}"
|
|
440
|
+
)
|
|
441
|
+
placements = cap.placements
|
|
442
|
+
if (
|
|
443
|
+
not isinstance(placements, frozenset)
|
|
444
|
+
or not placements
|
|
445
|
+
or not all(isinstance(p, Placement) for p in placements)
|
|
446
|
+
):
|
|
447
|
+
raise TypeError(
|
|
448
|
+
f"capability {cap.name!r}: placements must be a non-empty frozenset of "
|
|
449
|
+
f"Placement, got {placements!r}"
|
|
450
|
+
)
|
|
451
|
+
if cap.name in _CAPABILITIES:
|
|
452
|
+
raise ValueError(f"capability {cap.name!r} is already registered")
|
|
453
|
+
_CAPABILITIES[cap.name] = cap
|
|
454
|
+
return cap
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def registered_capabilities() -> tuple[Capability, ...]:
|
|
458
|
+
"""Every registered capability, in registration order."""
|
|
459
|
+
return tuple(_CAPABILITIES.values())
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _reset_for_tests() -> None:
|
|
463
|
+
_CAPABILITIES.clear()
|
dstf/authoring/errors.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""What the tool says when an action fails.
|
|
2
|
+
|
|
3
|
+
The errors a person can cause — a patch the validator refuses, a drag a
|
|
4
|
+
criterion cannot take, a move the tree refuses, a value out of range, a
|
|
5
|
+
locked spec, a test the queue holds — read as their plain reason: the location is
|
|
6
|
+
already on the screen, beside the row or the plot it came from. The
|
|
7
|
+
validator's refusal of a tree of warnings read "PatchError: validator
|
|
8
|
+
rejected the patched tree: metrics: tree has no leaf targeting 'primary'
|
|
9
|
+
..."; it now reads the sentence alone.
|
|
10
|
+
Anything else is a bug and keeps its class name, so it can be reported.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from ..backends.pool import Refused as PoolRefused
|
|
16
|
+
from ..comparison.handles import HandleEditError
|
|
17
|
+
from ..config import ConfigError
|
|
18
|
+
from ..discovery.patch_apply import PatchError
|
|
19
|
+
from .session import QueueError, StaleSpecError
|
|
20
|
+
from .tree_move import MoveRefused
|
|
21
|
+
from .unscorable import Refused as EditorRefused
|
|
22
|
+
|
|
23
|
+
#: What a person's own edit raises. Shown as its reason and never logged.
|
|
24
|
+
USER_ERRORS: tuple[type[BaseException], ...] = (
|
|
25
|
+
PatchError,
|
|
26
|
+
HandleEditError,
|
|
27
|
+
ConfigError,
|
|
28
|
+
# The simulation queue's refusals: the Session's and the pool's own.
|
|
29
|
+
QueueError,
|
|
30
|
+
PoolRefused,
|
|
31
|
+
# A move the tree refuses — a Move to… menu or a drag map gone stale
|
|
32
|
+
# under an edit staged since offers one — is said, not a bug.
|
|
33
|
+
MoveRefused,
|
|
34
|
+
# A value an editor will not stage: typed on the way to a good one.
|
|
35
|
+
EditorRefused,
|
|
36
|
+
# The spec file changed on disk under the tool: a write from the
|
|
37
|
+
# loaded one is refused, with the way out in the sentence.
|
|
38
|
+
StaleSpecError,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def user_text(e: BaseException) -> str:
|
|
43
|
+
"""``e`` as the tool shows it: its ``reason`` (else its text) for a
|
|
44
|
+
person's own error or a ValueError, the class name first for anything
|
|
45
|
+
else. A ValueError also covers a JSONDecodeError or a numpy shape error
|
|
46
|
+
from a bug, so a caller that shows one also logs it (see `USER_ERRORS`)."""
|
|
47
|
+
if isinstance(e, (*USER_ERRORS, ValueError)):
|
|
48
|
+
return str(getattr(e, "reason", None) or e)
|
|
49
|
+
return f"{type(e).__name__}: {e}"
|