flow: structured dtypes, and the widgets that read them
A series, record or list message declares its shape instead of riding DType.JSON, so a widget binds a shape rather than some JSON and a wrong binding is refused before anything runs. A list declares its item type, which is what keeps list[float] expressible for a pipeline. On top of that: an agenda over a list, a notification over a record, and a dashboard alert channel that publishes engine faults as one — so a panel can show what went wrong without a flow wiring it by hand. Also: only None means a node published nothing, a falsy value of the wrong shape is now the named error it always should have been; and the gauge's readout says its size is viewBox geometry rather than type scale. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -187,3 +187,64 @@ def test_publishing_nothing_does_nothing():
|
||||
pipeline.publish({})
|
||||
|
||||
assert pipeline.values() == {}
|
||||
|
||||
|
||||
def query_chart(**config) -> dict:
|
||||
return {
|
||||
"source": "query",
|
||||
"request": "heating.chart_req",
|
||||
"request_dtype": "record",
|
||||
"message": "heating.chart_series",
|
||||
"dtype": "series",
|
||||
"refresh_s": 30,
|
||||
**config,
|
||||
}
|
||||
|
||||
|
||||
def test_a_widget_refuses_a_dtype_it_cannot_carry():
|
||||
WidgetDef(id="s", type="switch", config={"message": "a.b", "dtype": "bool"})
|
||||
# A document written before the editor recorded types binds anything.
|
||||
WidgetDef(id="s", type="switch", config={"message": "a.b"})
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
WidgetDef(id="s", type="switch", config={"message": "a.b", "dtype": "float"})
|
||||
|
||||
|
||||
def test_the_structured_widgets_bind_their_shapes():
|
||||
WidgetDef(id="a", type="agenda", config={"message": "a.b", "dtype": "list"})
|
||||
WidgetDef(id="n", type="notification", config={"message": "a.b", "dtype": "record"})
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
WidgetDef(id="a", type="agenda", config={"message": "a.b", "dtype": "json"})
|
||||
with pytest.raises(ValueError):
|
||||
WidgetDef(
|
||||
id="n", type="notification", config={"message": "a.b", "dtype": "list"}
|
||||
)
|
||||
|
||||
|
||||
def test_a_querying_chart_asks_with_a_record_and_draws_a_series():
|
||||
widget = WidgetDef(id="c", type="chart", config=query_chart())
|
||||
|
||||
# It publishes its request and reads the answer, so the canvas draws both.
|
||||
assert widget.target == "heating.chart_req"
|
||||
assert widget.messages == ["heating.chart_series"]
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
WidgetDef(id="c", type="chart", config=query_chart(dtype="float"))
|
||||
with pytest.raises(ValueError):
|
||||
WidgetDef(id="c", type="chart", config=query_chart(request_dtype="json"))
|
||||
|
||||
|
||||
def test_a_querying_chart_keeps_no_ring():
|
||||
"""The answer carries its own past; a ring would store it twice."""
|
||||
widget = WidgetDef(
|
||||
id="c", type="chart", config=query_chart(history={"points": 900})
|
||||
)
|
||||
|
||||
assert widget.history_points == 0
|
||||
|
||||
|
||||
def test_a_dashboard_is_cut_into_a_sane_number_of_columns():
|
||||
for columns in (0, 49):
|
||||
with pytest.raises(ValueError):
|
||||
DashboardDef(name="house", columns=columns)
|
||||
|
||||
Reference in New Issue
Block a user