From 4955d32b004f78281cc843d487954951701af9b1 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Tue, 5 Nov 2024 20:35:02 +0100 Subject: [PATCH 01/10] Introduced new callback I/O types AppInput, AppOutput, and State --- dashipy/dashipy/__init__.py | 7 ++- dashipy/dashipy/callback.py | 69 +++++++--------------------- dashipy/dashipy/contribution.py | 5 +- dashipy/dashipy/inputoutput.py | 68 +++++++++++++++++++++++++++ dashipy/my_extension/my_panel_2.py | 8 ++-- dashipy/my_extension/my_panel_3.py | 6 +-- dashipy/tests/contribs/panel_test.py | 5 +- dashipy/tests/lib/callback_test.py | 23 +++++----- 8 files changed, 113 insertions(+), 78 deletions(-) create mode 100644 dashipy/dashipy/inputoutput.py diff --git a/dashipy/dashipy/__init__.py b/dashipy/dashipy/__init__.py index 8109e2c3..c2a8b77a 100644 --- a/dashipy/dashipy/__init__.py +++ b/dashipy/dashipy/__init__.py @@ -1,6 +1,9 @@ from .callback import Callback -from .callback import Input -from .callback import Output +from .inputoutput import AppInput +from .inputoutput import AppOutput +from .inputoutput import Input +from .inputoutput import Output +from .inputoutput import State from .component import Component from .container import Container from .extension import Contribution diff --git a/dashipy/dashipy/callback.py b/dashipy/dashipy/callback.py index e8a103c8..f275e3a5 100644 --- a/dashipy/dashipy/callback.py +++ b/dashipy/dashipy/callback.py @@ -1,51 +1,14 @@ import inspect import types -from abc import ABC -from typing import Callable, Any, Literal +from typing import Any, Callable - -ComponentKind = Literal["Component"] -AppStateKind = Literal["AppState"] -StateKind = Literal["State"] -InputOutputKind = ComponentKind | StateKind | AppStateKind - - -class InputOutput(ABC): - # noinspection PyShadowingBuiltins - def __init__( - self, - id: str | None = None, - property: str | None = None, - kind: InputOutputKind | None = None, - ): - kind = "Component" if kind is None else kind - assert kind in ("AppState", "State", "Component") - if kind == "Component": - assert id is not None and isinstance(id, str) and id != "" - property = "value" if property is None else property - assert isinstance(property, str) and property != "" - else: - assert id is None or (isinstance(id, str) and id != "") - assert property is None or (isinstance(property, str) and property != "") - - self.kind = kind - self.id = id - self.property = property - - def to_dict(self) -> dict[str, Any]: - return { - k: v - for k, v in self.__dict__.items() - if not k.startswith("_") and v is not None - } - - -class Input(InputOutput): - """Callback input.""" - - -class Output(InputOutput): - """Callback output.""" +from dashipy.inputoutput import ( + AppInput, + AppOutput, + Input, + Output, + State, +) class Callback: @@ -81,18 +44,18 @@ def from_decorator( f" context parameter" ) - inputs: list[Input] = [] - outputs: list[Output] = [] + inputs: list[Input | State | AppInput] = [] + outputs: list[Output | AppOutput] = [] for arg in decorator_args: - if isinstance(arg, Input): + if isinstance(arg, (Input, State, AppInput)): inputs.append(arg) - elif outputs_allowed and isinstance(arg, Output): + elif outputs_allowed and isinstance(arg, (Output, AppOutput)): outputs.append(arg) elif outputs_allowed: raise TypeError( f"arguments for decorator {decorator_name!r}" - f" must be of type Input or Output," - f" but got {arg.__class__.__name__!r}" + f" must be of type Input, State, Output, AppInput," + f" or AppOutput, but got {arg.__class__.__name__!r}" ) else: raise TypeError( @@ -118,8 +81,8 @@ def from_decorator( def __init__( self, function: Callable, - inputs: list[Input], - outputs: list[Output], + inputs: list[Input | State | AppInput], + outputs: list[Output | AppOutput], signature: inspect.Signature | None = None, ): """Private constructor. diff --git a/dashipy/dashipy/contribution.py b/dashipy/dashipy/contribution.py index 79a797bd..8a7c84bc 100644 --- a/dashipy/dashipy/contribution.py +++ b/dashipy/dashipy/contribution.py @@ -1,7 +1,8 @@ from typing import Any, Callable from abc import ABC -from .callback import Callback, Output, Input +from .callback import Callback +from .inputoutput import InputOutput class Contribution(ABC): @@ -36,7 +37,7 @@ def decorator(function: Callable) -> Callable: return decorator - def callback(self, *args: Input | Output) -> Callable[[Callable], Callable]: + def callback(self, *args: InputOutput) -> Callable[[Callable], Callable]: """Decorator.""" def decorator(function: Callable) -> Callable: diff --git a/dashipy/dashipy/inputoutput.py b/dashipy/dashipy/inputoutput.py new file mode 100644 index 00000000..bab0c073 --- /dev/null +++ b/dashipy/dashipy/inputoutput.py @@ -0,0 +1,68 @@ +from abc import ABC +from typing import Any + + +class InputOutput(ABC): + # noinspection PyShadowingBuiltins + def __init__( + self, + id: str | None = None, + property: str | None = None, + ): + self.id = id + self.property = property + + def to_dict(self) -> dict[str, Any]: + d = {"type": self.__class__.__name__} + d.update({ + k: v + for k, v in self.__dict__.items() + if not k.startswith("_") and v is not None + }) + return d + + +class Input(InputOutput): + """An input value read from component state. + A component state change may trigger callback invocation. + """ + + # noinspection PyShadowingBuiltins + def __init__(self, id: str, property: str = "value"): + super().__init__(id, property) + + +class State(InputOutput): + """An input value read from component state. + Does not trigger callback invocation. + """ + + # noinspection PyShadowingBuiltins + def __init__(self, id: str, property: str = "value"): + super().__init__(id, property) + + +class Output(InputOutput): + """Callback output.""" + + # noinspection PyShadowingBuiltins + def __init__(self, id: str, property: str = "value"): + super().__init__(id, property) + + +class AppInput(InputOutput): + """An input value read from application state. + An application state change may trigger callback invocation. + """ + + # noinspection PyShadowingBuiltins + def __init__(self, property: str): + super().__init__(property=property) + + +class AppOutput(InputOutput): + """An output written to application state.""" + + # noinspection PyShadowingBuiltins + def __init__(self, property: str): + super().__init__(property=property) diff --git a/dashipy/my_extension/my_panel_2.py b/dashipy/my_extension/my_panel_2.py index 7ddffced..5d1312db 100644 --- a/dashipy/my_extension/my_panel_2.py +++ b/dashipy/my_extension/my_panel_2.py @@ -1,7 +1,7 @@ import altair as alt import pandas as pd -from dashipy import Component, Input, Output +from dashipy import Component, AppInput, Input, Output from dashipy.components import Plot, Box, Dropdown from dashipy.demo.contribs import Panel from dashipy.demo.context import Context @@ -10,7 +10,7 @@ panel = Panel(__name__, title="Panel B") -@panel.layout(Input(kind="AppState", property="selectedDatasetId")) +@panel.layout(AppInput("selectedDatasetId")) def render_panel( ctx: Context, selected_dataset_id: str = "", @@ -51,7 +51,7 @@ def render_panel( @panel.callback( - Input(kind="AppState", property="selectedDatasetId"), + AppInput("selectedDatasetId"), Input("selected_variable_name"), Output("plot", "chart"), ) @@ -89,7 +89,7 @@ def make_figure( @panel.callback( - Input(kind="AppState", property="selectedDatasetId"), + AppInput("selectedDatasetId"), Output("selected_variable_name", "options"), Output("selected_variable_name", "value"), ) diff --git a/dashipy/my_extension/my_panel_3.py b/dashipy/my_extension/my_panel_3.py index 40d29a11..b449b8b4 100644 --- a/dashipy/my_extension/my_panel_3.py +++ b/dashipy/my_extension/my_panel_3.py @@ -1,4 +1,4 @@ -from dashipy import Component, Input, Output +from dashipy import Component, AppInput, Input, Output from dashipy.components import Box, Dropdown, Checkbox, Typography from dashipy.demo.contribs import Panel from dashipy.demo.context import Context @@ -11,7 +11,7 @@ @panel.layout( - Input(kind="AppState", property="selectedDatasetId"), + AppInput("selectedDatasetId"), ) def render_panel( ctx: Context, @@ -53,7 +53,7 @@ def render_panel( # noinspection PyUnusedLocal @panel.callback( - Input(kind="AppState", property="selectedDatasetId"), + AppInput(property="selectedDatasetId"), Input("opaque"), Input("color"), Output("info_text", "text"), diff --git a/dashipy/tests/contribs/panel_test.py b/dashipy/tests/contribs/panel_test.py index 021eb2e1..99317ad9 100644 --- a/dashipy/tests/contribs/panel_test.py +++ b/dashipy/tests/contribs/panel_test.py @@ -4,10 +4,12 @@ from dashipy.callback import Input, Callback, Output +# noinspection PyUnusedLocal def my_callback_1(ctx): pass +# noinspection PyUnusedLocal def my_callback_2(ctx, a: int = 0, b: str = "", c: bool = False) -> str: return f"{a}-{b}-{c}" @@ -39,10 +41,7 @@ def test_callback(self): self.assertEqual(1, len(callback.outputs)) self.assertEqual("a", callback.inputs[0].id) self.assertEqual("value", callback.inputs[0].property) - self.assertEqual("Component", callback.inputs[0].kind) self.assertEqual("b", callback.inputs[1].id) self.assertEqual("value", callback.inputs[1].property) - self.assertEqual("Component", callback.inputs[1].kind) self.assertEqual("c", callback.inputs[2].id) self.assertEqual("value", callback.inputs[2].property) - self.assertEqual("Component", callback.inputs[2].kind) diff --git a/dashipy/tests/lib/callback_test.py b/dashipy/tests/lib/callback_test.py index 3d749e84..25d662a3 100644 --- a/dashipy/tests/lib/callback_test.py +++ b/dashipy/tests/lib/callback_test.py @@ -5,6 +5,7 @@ from dashipy.callback import Input, Callback, Output +# noinspection PyUnusedLocal def my_callback( ctx, a: int, @@ -59,10 +60,10 @@ def test_to_dict_with_no_outputs(self): "returnType": {"type": "string"}, }, "inputs": [ - {"id": "a", "kind": "Component", "property": "value"}, - {"id": "b", "kind": "Component", "property": "value"}, - {"id": "c", "kind": "Component", "property": "value"}, - {"id": "d", "kind": "Component", "property": "value"}, + {"type": "Input", "id": "a", "property": "value"}, + {"type": "Input", "id": "b", "property": "value"}, + {"type": "Input", "id": "c", "property": "value"}, + {"type": "Input", "id": "d", "property": "value"}, ], }, d, @@ -73,8 +74,8 @@ def test_to_dict_with_two_outputs(self): my_callback_2, [Input("n")], [ - Output(id="select", property="options"), - Output(id="select", property="value"), + Output("select", "options"), + Output("select", "value"), ], ) d = callback.to_dict() @@ -92,10 +93,10 @@ def test_to_dict_with_two_outputs(self): "type": "array", }, }, - "inputs": [{"id": "n", "kind": "Component", "property": "value"}], + "inputs": [{"type": "Input", "id": "n", "property": "value"}], "outputs": [ - {"id": "select", "kind": "Component", "property": "options"}, - {"id": "select", "kind": "Component", "property": "value"}, + {"type": "Output", "id": "select", "property": "options"}, + {"type": "Output", "id": "select", "property": "value"}, ], }, d, @@ -141,7 +142,7 @@ def test_decorator_args(self): with pytest.raises( TypeError, - match="arguments for decorator 'test' must be of" - " type Input or Output, but got 'int'", + match=("arguments for decorator 'test' must be of type Input," + " State, Output, AppInput, or AppOutput, but got 'int'"), ): Callback.from_decorator("test", (13,), my_callback, outputs_allowed=True) From a28ac0336db501a69a55c3a9de06a13cef3d924d Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Tue, 5 Nov 2024 21:11:32 +0100 Subject: [PATCH 02/10] Introduced new callback I/O types AppInput, AppOutput, and State --- .../src/lib/actions/handleComponentChange.ts | 4 +-- .../src/lib/actions/handleHostStoreChange.ts | 2 +- .../helpers/applyStateChangeRequests.test.ts | 8 ++--- .../helpers/applyStateChangeRequests.ts | 36 ++++++++++--------- .../actions/helpers/getInputValues.test.ts | 14 ++++---- .../src/lib/actions/helpers/getInputValues.ts | 11 +++--- dashi/src/lib/types/model/callback.ts | 23 ++++++++---- 7 files changed, 56 insertions(+), 42 deletions(-) diff --git a/dashi/src/lib/actions/handleComponentChange.ts b/dashi/src/lib/actions/handleComponentChange.ts index bb073b56..8948732e 100644 --- a/dashi/src/lib/actions/handleComponentChange.ts +++ b/dashi/src/lib/actions/handleComponentChange.ts @@ -17,7 +17,7 @@ export function handleComponentChange( contribIndex, stateChanges: [ { - kind: "Component", + type: "Output", id: changeEvent.componentId, property: changeEvent.propertyName, value: changeEvent.propertyValue, @@ -57,7 +57,7 @@ function getCallbackRequests( const inputs = callback.inputs; const inputIndex = inputs.findIndex( (input) => - (!input.kind || input.kind === "Component") && + (!input.type || input.type === "Input") && input.id === changeEvent.componentId && input.property === changeEvent.propertyName, ); diff --git a/dashi/src/lib/actions/handleHostStoreChange.ts b/dashi/src/lib/actions/handleHostStoreChange.ts index 29e7a437..f84b38da 100644 --- a/dashi/src/lib/actions/handleHostStoreChange.ts +++ b/dashi/src/lib/actions/handleHostStoreChange.ts @@ -68,7 +68,7 @@ function _getHostStorePropertyRefs(): PropertyRef[] { (contribution.callbacks || []).forEach( (callback, callbackIndex) => (callback.inputs || []).forEach((input, inputIndex) => { - if (input.kind === "AppState") { + if (input.type === "AppInput") { propertyRefs.push({ contribPoint, contribIndex, diff --git a/dashi/src/lib/actions/helpers/applyStateChangeRequests.test.ts b/dashi/src/lib/actions/helpers/applyStateChangeRequests.test.ts index ee6c822d..18e18650 100644 --- a/dashi/src/lib/actions/helpers/applyStateChangeRequests.test.ts +++ b/dashi/src/lib/actions/helpers/applyStateChangeRequests.test.ts @@ -46,7 +46,7 @@ describe("Test that applyContributionChangeRequests()", () => { contribIndex: 0, stateChanges: [ { - kind: "Component", + type: "Output", id: "dd1", property: "value", value: 14, @@ -59,7 +59,7 @@ describe("Test that applyContributionChangeRequests()", () => { contribIndex: 0, stateChanges: [ { - kind: "Component", + type: "Output", id: "dd1", property: "value", value: 13, @@ -88,7 +88,7 @@ describe("Test that applyContributionChangeRequests()", () => { describe("Test that applyComponentStateChange()", () => { it("changes state if values are different", () => { const newState = applyComponentStateChange(componentTree, { - kind: "Component", + type: "Output", id: "cb1", property: "value", value: false, @@ -99,7 +99,7 @@ describe("Test that applyComponentStateChange()", () => { it("doesn't change the state if value stays the same", () => { const newState = applyComponentStateChange(componentTree, { - kind: "Component", + type: "Output", id: "cb1", property: "value", value: true, diff --git a/dashi/src/lib/actions/helpers/applyStateChangeRequests.ts b/dashi/src/lib/actions/helpers/applyStateChangeRequests.ts index f0b94884..0ea7ced1 100644 --- a/dashi/src/lib/actions/helpers/applyStateChangeRequests.ts +++ b/dashi/src/lib/actions/helpers/applyStateChangeRequests.ts @@ -37,8 +37,9 @@ function applyHostStateChanges(stateChangeRequests: StateChangeRequest[]) { stateChangeRequests.forEach((stateChangeRequest) => { hostState = applyStateChanges( hostState, - stateChangeRequest.stateChanges, - "AppState", + stateChangeRequest.stateChanges.filter( + (stateChange) => stateChange.type === "AppOutput", + ), ); }); if (hostState !== hostStateOld) { @@ -53,13 +54,9 @@ function applyComponentStateChanges( ) { let component = componentOld; if (component) { - stateChanges - .filter( - (stateChange) => !stateChange.kind || stateChange.kind === "Component", - ) - .forEach((stateChange) => { - component = applyComponentStateChange(component!, stateChange); - }); + stateChanges.forEach((stateChange) => { + component = applyComponentStateChange(component!, stateChange); + }); } return component; } @@ -74,12 +71,19 @@ export function applyContributionChangeRequests( const contribution = contributionsRecord[contribPoint][contribIndex]; const state = applyStateChanges( contribution.state, - stateChanges, - "State", + stateChanges.filter( + (stateChange) => + (!stateChange.type || stateChange.type === "Output") && + !stateChange.id, + ), ); const component = applyComponentStateChanges( contribution.component, - stateChanges, + stateChanges.filter( + (stateChange) => + (!stateChange.type || stateChange.type === "Output") && + stateChange.id, + ), ); if ( state !== contribution.state || @@ -139,14 +143,12 @@ export function applyComponentStateChange( export function applyStateChanges( state: S | undefined, stateChanges: StateChange[], - kind: "State" | "AppState", ): S | undefined { stateChanges.forEach((stateChange) => { if ( - stateChange.kind === kind && - (!state || - (state as unknown as Record)[stateChange.property] !== - stateChange.value) + !state || + (state as unknown as Record)[stateChange.property] !== + stateChange.value ) { state = { ...state, [stateChange.property]: stateChange.value } as S; } diff --git a/dashi/src/lib/actions/helpers/getInputValues.test.ts b/dashi/src/lib/actions/helpers/getInputValues.test.ts index ac063ed2..80823d08 100644 --- a/dashi/src/lib/actions/helpers/getInputValues.test.ts +++ b/dashi/src/lib/actions/helpers/getInputValues.test.ts @@ -27,7 +27,7 @@ describe("Test that getComponentStateValue()", () => { it("works on 1st level", () => { expect( getComponentStateValue(componentState, { - kind: "Component", + type: "Input", id: "b1", property: "value", }), @@ -37,7 +37,7 @@ describe("Test that getComponentStateValue()", () => { it("works on 2nd level", () => { expect( getComponentStateValue(componentState, { - kind: "Component", + type: "Input", id: "p1", property: "chart", }), @@ -47,7 +47,7 @@ describe("Test that getComponentStateValue()", () => { it("works on 3rd level", () => { expect( getComponentStateValue(componentState, { - kind: "Component", + type: "Input", id: "cb1", property: "value", }), @@ -55,7 +55,7 @@ describe("Test that getComponentStateValue()", () => { expect( getComponentStateValue(componentState, { - kind: "Component", + type: "Input", id: "dd1", property: "value", }), @@ -67,21 +67,21 @@ describe("Test that getInputValueFromState()", () => { it("works with input.id and input.property", () => { const state = { x: { y: 26 } }; expect( - getInputValueFromState({ kind: "State", id: "x", property: "y" }, state), + getInputValueFromState({ type: "State", id: "x", property: "y" }, state), ).toEqual(26); }); it("works with arrays indexes", () => { const state = { x: [4, 5, 6] }; expect( - getInputValueFromState({ kind: "State", id: "x", property: "1" }, state), + getInputValueFromState({ type: "State", id: "x", property: "1" }, state), ).toEqual(5); }); it("works without input.id", () => { const state = { x: [4, 5, 6] }; expect( - getInputValueFromState({ kind: "State", property: "x" }, state), + getInputValueFromState({ type: "State", property: "x" }, state), ).toEqual([4, 5, 6]); }); }); diff --git a/dashi/src/lib/actions/helpers/getInputValues.ts b/dashi/src/lib/actions/helpers/getInputValues.ts index 80257593..c8de73e7 100644 --- a/dashi/src/lib/actions/helpers/getInputValues.ts +++ b/dashi/src/lib/actions/helpers/getInputValues.ts @@ -22,13 +22,14 @@ export function getInputValue( hostState?: S | undefined, ): unknown { let inputValue: unknown = undefined; - const inputKind = input.kind || "Component"; - if (inputKind === "Component" && contributionState.component) { + const inputKind = input.type || "Input"; + if ( + (inputKind === "Input" || inputKind === "State") && + contributionState.component + ) { // Return value of a property of some component in the tree inputValue = getComponentStateValue(contributionState.component, input); - } else if (inputKind === "State" && contributionState.state) { - inputValue = getInputValueFromState(input, contributionState.state); - } else if (inputKind === "AppState" && hostState) { + } else if (inputKind === "AppInput" && hostState) { inputValue = getInputValueFromState(input, hostState); } else { console.warn(`input of unknown kind:`, input); diff --git a/dashi/src/lib/types/model/callback.ts b/dashi/src/lib/types/model/callback.ts index cd4fae19..b82f8ad3 100644 --- a/dashi/src/lib/types/model/callback.ts +++ b/dashi/src/lib/types/model/callback.ts @@ -30,14 +30,21 @@ export interface CbParameter { default?: unknown; } -export type InputOutputKind = "AppState" | "State" | "Component"; +export type InputType = "AppInput" | "Input" | "State"; + +export type OutputType = "AppOutput" | "Output"; + +export type InputOutputType = InputType | OutputType; export interface InputOutput { - /** The kind of input or output. */ - kind: InputOutputKind; + /** + * The type of input or output. + */ + type: InputOutputType; + /** * The identifier of a subcomponent. - * `id` is not needed if kind == "AppState" | "State". + * `id` is not needed if type == "AppInput" | "AppOutput". */ id?: string; @@ -52,9 +59,13 @@ export interface InputOutput { property: string; } -export interface Input extends InputOutput {} +export interface Input extends InputOutput { + type: InputType; +} -export interface Output extends InputOutput {} +export interface Output extends InputOutput { + type: OutputType; +} /** * A reference to a specific contribution. From f1c9b34d267d93dedf7ea84b5eded853423fe497 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Wed, 6 Nov 2024 14:53:39 +0100 Subject: [PATCH 03/10] Back to just Input, State, Output --- dashipy/dashipy/__init__.py | 2 - dashipy/dashipy/callback.py | 14 ++-- dashipy/dashipy/inputoutput.py | 106 ++++++++++++++++++-------- dashipy/dashipy/util/__init__.py | 0 dashipy/dashipy/util/assertions.py | 15 ++++ dashipy/my_extension/my_panel_2.py | 8 +- dashipy/my_extension/my_panel_3.py | 6 +- dashipy/tests/components/box_test.py | 3 - dashipy/tests/components/plot_test.py | 17 +---- dashipy/tests/inputoutput_test.py | 71 +++++++++++++++++ dashipy/tests/lib/callback_test.py | 57 +++++++++++--- 11 files changed, 222 insertions(+), 77 deletions(-) create mode 100644 dashipy/dashipy/util/__init__.py create mode 100644 dashipy/dashipy/util/assertions.py create mode 100644 dashipy/tests/inputoutput_test.py diff --git a/dashipy/dashipy/__init__.py b/dashipy/dashipy/__init__.py index c2a8b77a..ace04397 100644 --- a/dashipy/dashipy/__init__.py +++ b/dashipy/dashipy/__init__.py @@ -1,6 +1,4 @@ from .callback import Callback -from .inputoutput import AppInput -from .inputoutput import AppOutput from .inputoutput import Input from .inputoutput import Output from .inputoutput import State diff --git a/dashipy/dashipy/callback.py b/dashipy/dashipy/callback.py index f275e3a5..1f7fdc2a 100644 --- a/dashipy/dashipy/callback.py +++ b/dashipy/dashipy/callback.py @@ -3,8 +3,6 @@ from typing import Any, Callable from dashipy.inputoutput import ( - AppInput, - AppOutput, Input, Output, State, @@ -44,12 +42,12 @@ def from_decorator( f" context parameter" ) - inputs: list[Input | State | AppInput] = [] - outputs: list[Output | AppOutput] = [] + inputs: list[Input | State] = [] + outputs: list[Output] = [] for arg in decorator_args: - if isinstance(arg, (Input, State, AppInput)): + if isinstance(arg, (Input, State)): inputs.append(arg) - elif outputs_allowed and isinstance(arg, (Output, AppOutput)): + elif outputs_allowed and isinstance(arg, Output): outputs.append(arg) elif outputs_allowed: raise TypeError( @@ -81,8 +79,8 @@ def from_decorator( def __init__( self, function: Callable, - inputs: list[Input | State | AppInput], - outputs: list[Output | AppOutput], + inputs: list[Input | State], + outputs: list[Output], signature: inspect.Signature | None = None, ): """Private constructor. diff --git a/dashipy/dashipy/inputoutput.py b/dashipy/dashipy/inputoutput.py index bab0c073..dd9f718a 100644 --- a/dashipy/dashipy/inputoutput.py +++ b/dashipy/dashipy/inputoutput.py @@ -1,5 +1,44 @@ from abc import ABC -from typing import Any +from typing import Any, Literal + +from .util.assertions import assert_is_one_of +from .util.assertions import assert_is_instance_of + + +Source = Literal["component"] | Literal["container"] | Literal["app"] +Target = Literal["component"] | Literal["container"] | Literal["app"] +NoneType = type(None) + + +# noinspection PyShadowingBuiltins +def _validate_input( + source: str | None, id: str | None, property: str | None +) -> tuple[str, str | None, str | None]: + return _validate_kind("source", source, id, property) + + +# noinspection PyShadowingBuiltins +def _validate_output( + target: str | None, id: str | None, property: str | None +) -> tuple[str, str | None, str | None]: + return _validate_kind("target", target, id, property) + + +# noinspection PyShadowingBuiltins +def _validate_kind( + kind_name: str, kind: str | None, id: str | None, property: str | None +) -> tuple[str, str | None, str | None]: + assert_is_one_of(kind_name, kind, ("component", "container", "app", None)) + if not kind or kind == "component": + assert_is_instance_of("id", id, (str, NoneType)) + assert_is_instance_of("property", id, (str, NoneType)) + kind = kind or "component" + if property is None and id is not None: + property = "value" + else: + assert_is_instance_of("id", id, NoneType) + assert_is_instance_of("property", property, str) + return kind, id, property class InputOutput(ABC): @@ -13,12 +52,14 @@ def __init__( self.property = property def to_dict(self) -> dict[str, Any]: - d = {"type": self.__class__.__name__} - d.update({ - k: v - for k, v in self.__dict__.items() - if not k.startswith("_") and v is not None - }) + d = {"class": self.__class__.__name__} + d.update( + { + k: v + for k, v in self.__dict__.items() + if not k.startswith("_") and v is not None + } + ) return d @@ -28,41 +69,42 @@ class Input(InputOutput): """ # noinspection PyShadowingBuiltins - def __init__(self, id: str, property: str = "value"): - super().__init__(id, property) + def __init__( + self, + id: str | None = None, + property: str | None = None, + source: Source | None = None, + ): + source, id, property = _validate_input(source, id, property) + super().__init__(id=id, property=property) + self.source = source -class State(InputOutput): +class State(Input): """An input value read from component state. Does not trigger callback invocation. """ # noinspection PyShadowingBuiltins - def __init__(self, id: str, property: str = "value"): - super().__init__(id, property) + def __init__( + self, + id: str | None = None, + property: str | None = None, + source: Source | None = None, + ): + super().__init__(id=id, property=property, source=source) class Output(InputOutput): """Callback output.""" # noinspection PyShadowingBuiltins - def __init__(self, id: str, property: str = "value"): - super().__init__(id, property) - - -class AppInput(InputOutput): - """An input value read from application state. - An application state change may trigger callback invocation. - """ - - # noinspection PyShadowingBuiltins - def __init__(self, property: str): - super().__init__(property=property) - - -class AppOutput(InputOutput): - """An output written to application state.""" - - # noinspection PyShadowingBuiltins - def __init__(self, property: str): - super().__init__(property=property) + def __init__( + self, + id: str | None = None, + property: str | None = None, + target: Target | None = None, + ): + target, id, property = _validate_output(target, id, property) + super().__init__(id=id, property=property) + self.target = target diff --git a/dashipy/dashipy/util/__init__.py b/dashipy/dashipy/util/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dashipy/dashipy/util/assertions.py b/dashipy/dashipy/util/assertions.py new file mode 100644 index 00000000..ce543f4d --- /dev/null +++ b/dashipy/dashipy/util/assertions.py @@ -0,0 +1,15 @@ +from typing import Any, Container, Type + + +def assert_is_one_of(name: str, value: Any, value_set: Container): + if value not in value_set: + raise ValueError( + f"value of {name!r} must be one of {value_set!r}, but was {value!r}" + ) + + +def assert_is_instance_of(name: str, value: Any, type_set: Type | tuple[Type, ...]): + if not isinstance(value, type_set): + raise ValueError( + f"value of {name!r} must be an instance of {type_set!r}, but was {value!r}" + ) diff --git a/dashipy/my_extension/my_panel_2.py b/dashipy/my_extension/my_panel_2.py index 5d1312db..2208c813 100644 --- a/dashipy/my_extension/my_panel_2.py +++ b/dashipy/my_extension/my_panel_2.py @@ -1,7 +1,7 @@ import altair as alt import pandas as pd -from dashipy import Component, AppInput, Input, Output +from dashipy import Component, Input, Output from dashipy.components import Plot, Box, Dropdown from dashipy.demo.contribs import Panel from dashipy.demo.context import Context @@ -10,7 +10,7 @@ panel = Panel(__name__, title="Panel B") -@panel.layout(AppInput("selectedDatasetId")) +@panel.layout(Input(property="selectedDatasetId", source="app")) def render_panel( ctx: Context, selected_dataset_id: str = "", @@ -51,7 +51,7 @@ def render_panel( @panel.callback( - AppInput("selectedDatasetId"), + Input(property="selectedDatasetId", source="app"), Input("selected_variable_name"), Output("plot", "chart"), ) @@ -89,7 +89,7 @@ def make_figure( @panel.callback( - AppInput("selectedDatasetId"), + Input(property="selectedDatasetId", source="app"), Output("selected_variable_name", "options"), Output("selected_variable_name", "value"), ) diff --git a/dashipy/my_extension/my_panel_3.py b/dashipy/my_extension/my_panel_3.py index b449b8b4..be95b0c8 100644 --- a/dashipy/my_extension/my_panel_3.py +++ b/dashipy/my_extension/my_panel_3.py @@ -1,4 +1,4 @@ -from dashipy import Component, AppInput, Input, Output +from dashipy import Component, Input, Output from dashipy.components import Box, Dropdown, Checkbox, Typography from dashipy.demo.contribs import Panel from dashipy.demo.context import Context @@ -11,7 +11,7 @@ @panel.layout( - AppInput("selectedDatasetId"), + Input(property="selectedDatasetId", source="app"), ) def render_panel( ctx: Context, @@ -53,7 +53,7 @@ def render_panel( # noinspection PyUnusedLocal @panel.callback( - AppInput(property="selectedDatasetId"), + Input("selectedDatasetId", source="app"), Input("opaque"), Input("color"), Output("info_text", "text"), diff --git a/dashipy/tests/components/box_test.py b/dashipy/tests/components/box_test.py index 8613fd01..170fdca9 100644 --- a/dashipy/tests/components/box_test.py +++ b/dashipy/tests/components/box_test.py @@ -1,9 +1,6 @@ import json import unittest -import plotly.graph_objects as go -from plotly.graph_objs import Layout - from dashipy.components import Box diff --git a/dashipy/tests/components/plot_test.py b/dashipy/tests/components/plot_test.py index 082c2b95..8dd96e15 100644 --- a/dashipy/tests/components/plot_test.py +++ b/dashipy/tests/components/plot_test.py @@ -1,22 +1,7 @@ -import json import unittest -import plotly.graph_objects as go -from plotly.graph_objs import Layout - -from dashipy.components import Plot - class PlotTest(unittest.TestCase): def test_is_json_serializable(self): - figure = go.Figure(layout=Layout(title="Bar Chart", autosize=True)) - figure.add_trace(go.Bar(x=["A", "B", "C"], y=[0.2, 0.3, 0.4])) - plot = Plot(figure=figure) - - d = plot.to_dict() - self.assertIsInstance(d, dict) - self.assertIsInstance(d.get("figure"), dict) - json_text = json.dumps(d) - self.assertEqual("{", json_text[0]) - self.assertEqual("}", json_text[-1]) + pass diff --git a/dashipy/tests/inputoutput_test.py b/dashipy/tests/inputoutput_test.py new file mode 100644 index 00000000..b4589263 --- /dev/null +++ b/dashipy/tests/inputoutput_test.py @@ -0,0 +1,71 @@ +import unittest +from typing import Type + +import pytest + +from dashipy.inputoutput import Input, State, Output + + +def make_base(): + class Base(unittest.TestCase): + kind_attr: str + cls: Type[Input] | Type[State] | Type[Output] + + def kind(self, obj): + return getattr(obj, self.kind_attr) + + def test_no_args(self): + obj = self.cls() + self.assertEqual("component", self.kind(obj)) + self.assertEqual(None, obj.id) + self.assertEqual(None, obj.property) + + def test_id_given(self): + obj = self.cls("dataset_select") + self.assertEqual("component", self.kind(obj)) + self.assertEqual("dataset_select", obj.id) + self.assertEqual("value", obj.property) + + def test_app(self): + obj = self.cls(property="datasetId", **{self.kind_attr: "app"}) + self.assertEqual("app", self.kind(obj)) + self.assertEqual(None, obj.id) + self.assertEqual("datasetId", obj.property) + + def test_app_no_prop(self): + with pytest.raises( + ValueError, + match=( + "value of 'property' must be an instance" + " of , but was None" + ), + ): + self.cls(**{self.kind_attr: "app"}) + + def test_wrong_kind(self): + with pytest.raises( + ValueError, + match=( + f"value of '{self.kind_attr}' must be one of" + f" \\('component', 'container', 'app', None\\)," + f" but was 'host'" + ), + ): + self.cls(**{self.kind_attr: "host"}) + + return Base + + +class InputTest(make_base(), unittest.TestCase): + cls = Input + kind_attr = "source" + + +class StateTest(make_base(), unittest.TestCase): + cls = State + kind_attr = "source" + + +class OutputTest(make_base(), unittest.TestCase): + cls = Output + kind_attr = "target" diff --git a/dashipy/tests/lib/callback_test.py b/dashipy/tests/lib/callback_test.py index 25d662a3..85c37b5b 100644 --- a/dashipy/tests/lib/callback_test.py +++ b/dashipy/tests/lib/callback_test.py @@ -60,10 +60,30 @@ def test_to_dict_with_no_outputs(self): "returnType": {"type": "string"}, }, "inputs": [ - {"type": "Input", "id": "a", "property": "value"}, - {"type": "Input", "id": "b", "property": "value"}, - {"type": "Input", "id": "c", "property": "value"}, - {"type": "Input", "id": "d", "property": "value"}, + { + "class": "Input", + "source": "component", + "id": "a", + "property": "value", + }, + { + "class": "Input", + "source": "component", + "id": "b", + "property": "value", + }, + { + "class": "Input", + "source": "component", + "id": "c", + "property": "value", + }, + { + "class": "Input", + "source": "component", + "id": "d", + "property": "value", + }, ], }, d, @@ -93,10 +113,27 @@ def test_to_dict_with_two_outputs(self): "type": "array", }, }, - "inputs": [{"type": "Input", "id": "n", "property": "value"}], + "inputs": [ + { + "class": "Input", + "source": "component", + "id": "n", + "property": "value", + } + ], "outputs": [ - {"type": "Output", "id": "select", "property": "options"}, - {"type": "Output", "id": "select", "property": "value"}, + { + "class": "Output", + "target": "component", + "id": "select", + "property": "options", + }, + { + "class": "Output", + "target": "component", + "id": "select", + "property": "value", + }, ], }, d, @@ -142,7 +179,9 @@ def test_decorator_args(self): with pytest.raises( TypeError, - match=("arguments for decorator 'test' must be of type Input," - " State, Output, AppInput, or AppOutput, but got 'int'"), + match=( + "arguments for decorator 'test' must be of type Input," + " State, Output, AppInput, or AppOutput, but got 'int'" + ), ): Callback.from_decorator("test", (13,), my_callback, outputs_allowed=True) From 7ca52c531144454100363932145c23e10f60ef4f Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Wed, 6 Nov 2024 16:06:35 +0100 Subject: [PATCH 04/10] Renamed InputOutput into Channel with common property "link" --- dashipy/dashipy/__init__.py | 6 +- dashipy/dashipy/callback.py | 2 +- dashipy/dashipy/channel.py | 121 ++++++++++++++++++ dashipy/dashipy/contribution.py | 4 +- dashipy/dashipy/inputoutput.py | 110 ---------------- dashipy/dashipy/util/assertions.py | 7 +- dashipy/my_extension/my_panel_3.py | 12 +- dashipy/tests/{lib => }/callback_test.py | 21 +-- dashipy/tests/channel_test.py | 104 +++++++++++++++ dashipy/tests/{lib => }/component_test.py | 0 dashipy/tests/{lib => }/container_test.py | 0 dashipy/tests/{contribs => demo}/__init__.py | 0 .../tests/{lib => demo/contribs}/__init__.py | 0 .../tests/{ => demo}/contribs/panel_test.py | 0 dashipy/tests/inputoutput_test.py | 71 ---------- 15 files changed, 252 insertions(+), 206 deletions(-) create mode 100644 dashipy/dashipy/channel.py delete mode 100644 dashipy/dashipy/inputoutput.py rename dashipy/tests/{lib => }/callback_test.py (89%) create mode 100644 dashipy/tests/channel_test.py rename dashipy/tests/{lib => }/component_test.py (100%) rename dashipy/tests/{lib => }/container_test.py (100%) rename dashipy/tests/{contribs => demo}/__init__.py (100%) rename dashipy/tests/{lib => demo/contribs}/__init__.py (100%) rename dashipy/tests/{ => demo}/contribs/panel_test.py (100%) delete mode 100644 dashipy/tests/inputoutput_test.py diff --git a/dashipy/dashipy/__init__.py b/dashipy/dashipy/__init__.py index ace04397..8623ca89 100644 --- a/dashipy/dashipy/__init__.py +++ b/dashipy/dashipy/__init__.py @@ -1,7 +1,7 @@ from .callback import Callback -from .inputoutput import Input -from .inputoutput import Output -from .inputoutput import State +from .channel import Input +from .channel import Output +from .channel import State from .component import Component from .container import Container from .extension import Contribution diff --git a/dashipy/dashipy/callback.py b/dashipy/dashipy/callback.py index 1f7fdc2a..de56f1e5 100644 --- a/dashipy/dashipy/callback.py +++ b/dashipy/dashipy/callback.py @@ -2,7 +2,7 @@ import types from typing import Any, Callable -from dashipy.inputoutput import ( +from dashipy.channel import ( Input, Output, State, diff --git a/dashipy/dashipy/channel.py b/dashipy/dashipy/channel.py new file mode 100644 index 00000000..cc5edf33 --- /dev/null +++ b/dashipy/dashipy/channel.py @@ -0,0 +1,121 @@ +from abc import ABC +from typing import Any, Literal + +from .util.assertions import assert_is_instance_of +from .util.assertions import assert_is_none +from .util.assertions import assert_is_one_of + + +Link = Literal["component"] | Literal["container"] | Literal["app"] + + +class Channel(ABC): + """Base class for `Input`, `State`, and `Output`. + Instances are used as argument passed to + the `layout` and `callback` decorators. + """ + + # noinspection PyShadowingBuiltins + def __init__( + self, + link: Link | None = None, + id: str | None = None, + property: str | None = None, + ): + self.link = link + self.id = id + self.property = property + + def to_dict(self) -> dict[str, Any]: + d = { + k: v + for k, v in self.__dict__.items() + if not k.startswith("_") and v is not None + } + if self.no_trigger: + d |= dict(noTrigger=True) + return d + + @property + def no_trigger(self): + return isinstance(self, State) + + +class Input(Channel): + """An input value read from component state. + A component state change may trigger callback invocation. + """ + + # noinspection PyShadowingBuiltins + def __init__( + self, + id: str | None = None, + property: str | None = None, + source: Link | None = None, + ): + link, id, property = _validate_input_params(source, id, property) + super().__init__(link=link, id=id, property=property) + + +class State(Input): + """An input value read from component state. + Does not trigger callback invocation. + """ + + # noinspection PyShadowingBuiltins + def __init__( + self, + id: str | None = None, + property: str | None = None, + source: Link | None = None, + ): + super().__init__(id=id, property=property, source=source) + + +class Output(Channel): + """Callback output.""" + + # noinspection PyShadowingBuiltins + def __init__( + self, + id: str | None = None, + property: str | None = None, + target: Link | None = None, + ): + target, id, property = _validate_output_params(target, id, property) + super().__init__(link=target, id=id, property=property) + + +NoneType = type(None) + + +# noinspection PyShadowingBuiltins +def _validate_input_params( + source: Link | None, id: str | None, property: str | None +) -> tuple[Link, str | None, str | None]: + return _validate_params("source", source, id, property) + + +# noinspection PyShadowingBuiltins +def _validate_output_params( + target: Link | None, id: str | None, property: str | None +) -> tuple[Link, str | None, str | None]: + return _validate_params("target", target, id, property) + + +# noinspection PyShadowingBuiltins +def _validate_params( + link_name: str, link: Link | None, id: str | None, property: str | None +) -> tuple[Link, str | None, str | None]: + assert_is_one_of(link_name, link, ("component", "container", "app", None)) + if not link or link == "component": + assert_is_instance_of("id", id, (str, NoneType)) + assert_is_instance_of("property", id, (str, NoneType)) + link = link or "component" + if property is None and id is not None: + property = "value" + else: + assert_is_none("id", id) + assert_is_instance_of("property", property, str) + # noinspection PyTypeChecker + return link, id, property diff --git a/dashipy/dashipy/contribution.py b/dashipy/dashipy/contribution.py index 8a7c84bc..43e253ec 100644 --- a/dashipy/dashipy/contribution.py +++ b/dashipy/dashipy/contribution.py @@ -2,7 +2,7 @@ from abc import ABC from .callback import Callback -from .inputoutput import InputOutput +from .channel import Channel class Contribution(ABC): @@ -37,7 +37,7 @@ def decorator(function: Callable) -> Callable: return decorator - def callback(self, *args: InputOutput) -> Callable[[Callable], Callable]: + def callback(self, *args: Channel) -> Callable[[Callable], Callable]: """Decorator.""" def decorator(function: Callable) -> Callable: diff --git a/dashipy/dashipy/inputoutput.py b/dashipy/dashipy/inputoutput.py deleted file mode 100644 index dd9f718a..00000000 --- a/dashipy/dashipy/inputoutput.py +++ /dev/null @@ -1,110 +0,0 @@ -from abc import ABC -from typing import Any, Literal - -from .util.assertions import assert_is_one_of -from .util.assertions import assert_is_instance_of - - -Source = Literal["component"] | Literal["container"] | Literal["app"] -Target = Literal["component"] | Literal["container"] | Literal["app"] -NoneType = type(None) - - -# noinspection PyShadowingBuiltins -def _validate_input( - source: str | None, id: str | None, property: str | None -) -> tuple[str, str | None, str | None]: - return _validate_kind("source", source, id, property) - - -# noinspection PyShadowingBuiltins -def _validate_output( - target: str | None, id: str | None, property: str | None -) -> tuple[str, str | None, str | None]: - return _validate_kind("target", target, id, property) - - -# noinspection PyShadowingBuiltins -def _validate_kind( - kind_name: str, kind: str | None, id: str | None, property: str | None -) -> tuple[str, str | None, str | None]: - assert_is_one_of(kind_name, kind, ("component", "container", "app", None)) - if not kind or kind == "component": - assert_is_instance_of("id", id, (str, NoneType)) - assert_is_instance_of("property", id, (str, NoneType)) - kind = kind or "component" - if property is None and id is not None: - property = "value" - else: - assert_is_instance_of("id", id, NoneType) - assert_is_instance_of("property", property, str) - return kind, id, property - - -class InputOutput(ABC): - # noinspection PyShadowingBuiltins - def __init__( - self, - id: str | None = None, - property: str | None = None, - ): - self.id = id - self.property = property - - def to_dict(self) -> dict[str, Any]: - d = {"class": self.__class__.__name__} - d.update( - { - k: v - for k, v in self.__dict__.items() - if not k.startswith("_") and v is not None - } - ) - return d - - -class Input(InputOutput): - """An input value read from component state. - A component state change may trigger callback invocation. - """ - - # noinspection PyShadowingBuiltins - def __init__( - self, - id: str | None = None, - property: str | None = None, - source: Source | None = None, - ): - source, id, property = _validate_input(source, id, property) - super().__init__(id=id, property=property) - self.source = source - - -class State(Input): - """An input value read from component state. - Does not trigger callback invocation. - """ - - # noinspection PyShadowingBuiltins - def __init__( - self, - id: str | None = None, - property: str | None = None, - source: Source | None = None, - ): - super().__init__(id=id, property=property, source=source) - - -class Output(InputOutput): - """Callback output.""" - - # noinspection PyShadowingBuiltins - def __init__( - self, - id: str | None = None, - property: str | None = None, - target: Target | None = None, - ): - target, id, property = _validate_output(target, id, property) - super().__init__(id=id, property=property) - self.target = target diff --git a/dashipy/dashipy/util/assertions.py b/dashipy/dashipy/util/assertions.py index ce543f4d..51d3f434 100644 --- a/dashipy/dashipy/util/assertions.py +++ b/dashipy/dashipy/util/assertions.py @@ -10,6 +10,11 @@ def assert_is_one_of(name: str, value: Any, value_set: Container): def assert_is_instance_of(name: str, value: Any, type_set: Type | tuple[Type, ...]): if not isinstance(value, type_set): - raise ValueError( + raise TypeError( f"value of {name!r} must be an instance of {type_set!r}, but was {value!r}" ) + + +def assert_is_none(name: str, value: Any): + if value is not None: + raise TypeError(f"value of {name!r} must be None, but was {value!r}") diff --git a/dashipy/my_extension/my_panel_3.py b/dashipy/my_extension/my_panel_3.py index be95b0c8..708762ef 100644 --- a/dashipy/my_extension/my_panel_3.py +++ b/dashipy/my_extension/my_panel_3.py @@ -1,4 +1,4 @@ -from dashipy import Component, Input, Output +from dashipy import Component, Input, State, Output from dashipy.components import Box, Dropdown, Checkbox, Typography from dashipy.demo.contribs import Panel from dashipy.demo.context import Context @@ -11,7 +11,7 @@ @panel.layout( - Input(property="selectedDatasetId", source="app"), + Input(source="app", property="selectedDatasetId"), ) def render_panel( ctx: Context, @@ -53,9 +53,10 @@ def render_panel( # noinspection PyUnusedLocal @panel.callback( - Input("selectedDatasetId", source="app"), + Input(source="app", property="selectedDatasetId"), Input("opaque"), Input("color"), + State("info_text", "text"), Output("info_text", "text"), ) def update_info_text( @@ -63,11 +64,14 @@ def update_info_text( dataset_id: str = "", opaque: bool = False, color: int = 0, + info_text: str = "" ) -> str: opaque = opaque or False color = color if color is not None else 0 return ( f"The dataset is {dataset_id}," f" the color is {COLORS[color][0]} and" - f" it {'is' if opaque else 'is not'} opaque" + f" it {'is' if opaque else 'is not'} opaque." + f" The length of the last info text" + f" was {len(info_text or "")}." ) diff --git a/dashipy/tests/lib/callback_test.py b/dashipy/tests/callback_test.py similarity index 89% rename from dashipy/tests/lib/callback_test.py rename to dashipy/tests/callback_test.py index 85c37b5b..981f3dc1 100644 --- a/dashipy/tests/lib/callback_test.py +++ b/dashipy/tests/callback_test.py @@ -61,26 +61,22 @@ def test_to_dict_with_no_outputs(self): }, "inputs": [ { - "class": "Input", - "source": "component", + "link": "component", "id": "a", "property": "value", }, { - "class": "Input", - "source": "component", + "link": "component", "id": "b", "property": "value", }, { - "class": "Input", - "source": "component", + "link": "component", "id": "c", "property": "value", }, { - "class": "Input", - "source": "component", + "link": "component", "id": "d", "property": "value", }, @@ -115,22 +111,19 @@ def test_to_dict_with_two_outputs(self): }, "inputs": [ { - "class": "Input", - "source": "component", + "link": "component", "id": "n", "property": "value", } ], "outputs": [ { - "class": "Output", - "target": "component", + "link": "component", "id": "select", "property": "options", }, { - "class": "Output", - "target": "component", + "link": "component", "id": "select", "property": "value", }, diff --git a/dashipy/tests/channel_test.py b/dashipy/tests/channel_test.py new file mode 100644 index 00000000..bda2922f --- /dev/null +++ b/dashipy/tests/channel_test.py @@ -0,0 +1,104 @@ +import unittest +from typing import Type + +import pytest + +from dashipy.channel import Channel, Input, State, Output + + +def make_base(): + class ChannelTest(unittest.TestCase): + channel_cls: Type[Channel] + link_name: str + + def test_no_args(self): + obj = self.channel_cls() + self.assertEqual("component", obj.link) + self.assertEqual(None, obj.id) + self.assertEqual(None, obj.property) + + def test_id_given(self): + obj = self.channel_cls("dataset_select") + self.assertEqual("component", obj.link) + self.assertEqual("dataset_select", obj.id) + self.assertEqual("value", obj.property) + + def test_app(self): + obj = self.channel_cls(property="datasetId", **{self.link_name: "app"}) + self.assertEqual("app", obj.link) + self.assertEqual(None, obj.id) + self.assertEqual("datasetId", obj.property) + + def test_container_with_id(self): + with pytest.raises( + TypeError, + match="value of 'id' must be None, but was 'test_id'", + ): + self.channel_cls("test_id", **{self.link_name: "container"}) + + def test_app_no_prop(self): + with pytest.raises( + TypeError, + match=( + "value of 'property' must be an instance" + " of , but was None" + ), + ): + self.channel_cls(**{self.link_name: "app"}) + + def test_wrong_link(self): + with pytest.raises( + ValueError, + match=( + f"value of '{self.link_name}' must be one of" + f" \\('component', 'container', 'app', None\\)," + f" but was 'host'" + ), + ): + self.channel_cls(**{self.link_name: "host"}) + + def test_no_trigger(self): + obj = self.channel_cls() + if isinstance(obj, State): + self.assertTrue(obj.no_trigger) + else: + self.assertFalse(obj.no_trigger) + + def test_to_dict(self): + obj = self.channel_cls("test_id") + if isinstance(obj, State): + self.assertEqual( + { + "link": "component", + "id": "test_id", + "property": "value", + "noTrigger": True, + }, + obj.to_dict(), + ) + else: + self.assertEqual( + { + "link": "component", + "id": "test_id", + "property": "value", + }, + obj.to_dict(), + ) + + return ChannelTest + + +class InputTest(make_base(), unittest.TestCase): + channel_cls = Input + link_name = "source" + + +class StateTest(make_base(), unittest.TestCase): + channel_cls = State + link_name = "source" + + +class OutputTest(make_base(), unittest.TestCase): + channel_cls = Output + link_name = "target" diff --git a/dashipy/tests/lib/component_test.py b/dashipy/tests/component_test.py similarity index 100% rename from dashipy/tests/lib/component_test.py rename to dashipy/tests/component_test.py diff --git a/dashipy/tests/lib/container_test.py b/dashipy/tests/container_test.py similarity index 100% rename from dashipy/tests/lib/container_test.py rename to dashipy/tests/container_test.py diff --git a/dashipy/tests/contribs/__init__.py b/dashipy/tests/demo/__init__.py similarity index 100% rename from dashipy/tests/contribs/__init__.py rename to dashipy/tests/demo/__init__.py diff --git a/dashipy/tests/lib/__init__.py b/dashipy/tests/demo/contribs/__init__.py similarity index 100% rename from dashipy/tests/lib/__init__.py rename to dashipy/tests/demo/contribs/__init__.py diff --git a/dashipy/tests/contribs/panel_test.py b/dashipy/tests/demo/contribs/panel_test.py similarity index 100% rename from dashipy/tests/contribs/panel_test.py rename to dashipy/tests/demo/contribs/panel_test.py diff --git a/dashipy/tests/inputoutput_test.py b/dashipy/tests/inputoutput_test.py deleted file mode 100644 index b4589263..00000000 --- a/dashipy/tests/inputoutput_test.py +++ /dev/null @@ -1,71 +0,0 @@ -import unittest -from typing import Type - -import pytest - -from dashipy.inputoutput import Input, State, Output - - -def make_base(): - class Base(unittest.TestCase): - kind_attr: str - cls: Type[Input] | Type[State] | Type[Output] - - def kind(self, obj): - return getattr(obj, self.kind_attr) - - def test_no_args(self): - obj = self.cls() - self.assertEqual("component", self.kind(obj)) - self.assertEqual(None, obj.id) - self.assertEqual(None, obj.property) - - def test_id_given(self): - obj = self.cls("dataset_select") - self.assertEqual("component", self.kind(obj)) - self.assertEqual("dataset_select", obj.id) - self.assertEqual("value", obj.property) - - def test_app(self): - obj = self.cls(property="datasetId", **{self.kind_attr: "app"}) - self.assertEqual("app", self.kind(obj)) - self.assertEqual(None, obj.id) - self.assertEqual("datasetId", obj.property) - - def test_app_no_prop(self): - with pytest.raises( - ValueError, - match=( - "value of 'property' must be an instance" - " of , but was None" - ), - ): - self.cls(**{self.kind_attr: "app"}) - - def test_wrong_kind(self): - with pytest.raises( - ValueError, - match=( - f"value of '{self.kind_attr}' must be one of" - f" \\('component', 'container', 'app', None\\)," - f" but was 'host'" - ), - ): - self.cls(**{self.kind_attr: "host"}) - - return Base - - -class InputTest(make_base(), unittest.TestCase): - cls = Input - kind_attr = "source" - - -class StateTest(make_base(), unittest.TestCase): - cls = State - kind_attr = "source" - - -class OutputTest(make_base(), unittest.TestCase): - cls = Output - kind_attr = "target" From 75dadf82500e4590e3dc266d700a344d2472079e Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Wed, 6 Nov 2024 16:35:14 +0100 Subject: [PATCH 05/10] Renamed InputOutput into Channel with common property "link" --- .../src/lib/actions/handleComponentChange.ts | 5 ++- .../src/lib/actions/handleHostStoreChange.ts | 9 +++-- .../helpers/applyStateChangeRequests.test.ts | 8 ++-- .../helpers/applyStateChangeRequests.ts | 8 ++-- .../actions/helpers/getInputValues.test.ts | 20 ++++++---- .../src/lib/actions/helpers/getInputValues.ts | 15 ++++--- dashi/src/lib/components/Component.tsx | 2 +- dashi/src/lib/types/model/callback.ts | 39 +------------------ dashi/src/lib/types/model/channel.ts | 33 ++++++++++++++++ 9 files changed, 72 insertions(+), 67 deletions(-) create mode 100644 dashi/src/lib/types/model/channel.ts diff --git a/dashi/src/lib/actions/handleComponentChange.ts b/dashi/src/lib/actions/handleComponentChange.ts index 8948732e..f215e378 100644 --- a/dashi/src/lib/actions/handleComponentChange.ts +++ b/dashi/src/lib/actions/handleComponentChange.ts @@ -17,7 +17,7 @@ export function handleComponentChange( contribIndex, stateChanges: [ { - type: "Output", + link: "component", id: changeEvent.componentId, property: changeEvent.propertyName, value: changeEvent.propertyValue, @@ -57,7 +57,8 @@ function getCallbackRequests( const inputs = callback.inputs; const inputIndex = inputs.findIndex( (input) => - (!input.type || input.type === "Input") && + !input.noTrigger && + (!input.link || input.link === "component") && input.id === changeEvent.componentId && input.property === changeEvent.propertyName, ); diff --git a/dashi/src/lib/actions/handleHostStoreChange.ts b/dashi/src/lib/actions/handleHostStoreChange.ts index f84b38da..110f3ae1 100644 --- a/dashi/src/lib/actions/handleHostStoreChange.ts +++ b/dashi/src/lib/actions/handleHostStoreChange.ts @@ -5,9 +5,9 @@ import type { CallbackRef, CallbackRequest, ContribRef, - Input, InputRef, } from "@/lib/types/model/callback"; +import type { Input } from "@/lib/types/model/channel"; import { getInputValues } from "@/lib/actions/helpers/getInputValues"; import { getValue, type PropertyPath } from "@/lib/utils/getValue"; import { invokeCallbacks } from "@/lib/actions/helpers/invokeCallbacks"; @@ -59,6 +59,9 @@ function getCallbackRequests( const getHostStorePropertyRefs = memoizeOne(_getHostStorePropertyRefs); +/** + * Get the static list of host state property references for all contributions. + */ function _getHostStorePropertyRefs(): PropertyRef[] { const { contributionsRecord } = store.getState(); const propertyRefs: PropertyRef[] = []; @@ -68,13 +71,13 @@ function _getHostStorePropertyRefs(): PropertyRef[] { (contribution.callbacks || []).forEach( (callback, callbackIndex) => (callback.inputs || []).forEach((input, inputIndex) => { - if (input.type === "AppInput") { + if (!input.noTrigger && input.link === "app") { propertyRefs.push({ contribPoint, contribIndex, callbackIndex, inputIndex, - propertyPath: input.property.split("."), + propertyPath: input.property!.split("."), }); } }), diff --git a/dashi/src/lib/actions/helpers/applyStateChangeRequests.test.ts b/dashi/src/lib/actions/helpers/applyStateChangeRequests.test.ts index 18e18650..a29aa32e 100644 --- a/dashi/src/lib/actions/helpers/applyStateChangeRequests.test.ts +++ b/dashi/src/lib/actions/helpers/applyStateChangeRequests.test.ts @@ -46,7 +46,7 @@ describe("Test that applyContributionChangeRequests()", () => { contribIndex: 0, stateChanges: [ { - type: "Output", + link: "component", id: "dd1", property: "value", value: 14, @@ -59,7 +59,7 @@ describe("Test that applyContributionChangeRequests()", () => { contribIndex: 0, stateChanges: [ { - type: "Output", + link: "component", id: "dd1", property: "value", value: 13, @@ -88,7 +88,7 @@ describe("Test that applyContributionChangeRequests()", () => { describe("Test that applyComponentStateChange()", () => { it("changes state if values are different", () => { const newState = applyComponentStateChange(componentTree, { - type: "Output", + link: "component", id: "cb1", property: "value", value: false, @@ -99,7 +99,7 @@ describe("Test that applyComponentStateChange()", () => { it("doesn't change the state if value stays the same", () => { const newState = applyComponentStateChange(componentTree, { - type: "Output", + link: "component", id: "cb1", property: "value", value: true, diff --git a/dashi/src/lib/actions/helpers/applyStateChangeRequests.ts b/dashi/src/lib/actions/helpers/applyStateChangeRequests.ts index 0ea7ced1..abee3098 100644 --- a/dashi/src/lib/actions/helpers/applyStateChangeRequests.ts +++ b/dashi/src/lib/actions/helpers/applyStateChangeRequests.ts @@ -38,7 +38,7 @@ function applyHostStateChanges(stateChangeRequests: StateChangeRequest[]) { hostState = applyStateChanges( hostState, stateChangeRequest.stateChanges.filter( - (stateChange) => stateChange.type === "AppOutput", + (stateChange) => stateChange.link === "app", ), ); }); @@ -72,16 +72,14 @@ export function applyContributionChangeRequests( const state = applyStateChanges( contribution.state, stateChanges.filter( - (stateChange) => - (!stateChange.type || stateChange.type === "Output") && - !stateChange.id, + (stateChange) => stateChange.link === "container" && !stateChange.id, ), ); const component = applyComponentStateChanges( contribution.component, stateChanges.filter( (stateChange) => - (!stateChange.type || stateChange.type === "Output") && + (!stateChange.link || stateChange.link === "component") && stateChange.id, ), ); diff --git a/dashi/src/lib/actions/helpers/getInputValues.test.ts b/dashi/src/lib/actions/helpers/getInputValues.test.ts index 80823d08..980f33a8 100644 --- a/dashi/src/lib/actions/helpers/getInputValues.test.ts +++ b/dashi/src/lib/actions/helpers/getInputValues.test.ts @@ -27,7 +27,7 @@ describe("Test that getComponentStateValue()", () => { it("works on 1st level", () => { expect( getComponentStateValue(componentState, { - type: "Input", + link: "component", id: "b1", property: "value", }), @@ -37,7 +37,7 @@ describe("Test that getComponentStateValue()", () => { it("works on 2nd level", () => { expect( getComponentStateValue(componentState, { - type: "Input", + link: "component", id: "p1", property: "chart", }), @@ -47,7 +47,7 @@ describe("Test that getComponentStateValue()", () => { it("works on 3rd level", () => { expect( getComponentStateValue(componentState, { - type: "Input", + link: "component", id: "cb1", property: "value", }), @@ -55,7 +55,7 @@ describe("Test that getComponentStateValue()", () => { expect( getComponentStateValue(componentState, { - type: "Input", + link: "component", id: "dd1", property: "value", }), @@ -67,21 +67,27 @@ describe("Test that getInputValueFromState()", () => { it("works with input.id and input.property", () => { const state = { x: { y: 26 } }; expect( - getInputValueFromState({ type: "State", id: "x", property: "y" }, state), + getInputValueFromState( + { link: "component", id: "x", property: "y" }, + state, + ), ).toEqual(26); }); it("works with arrays indexes", () => { const state = { x: [4, 5, 6] }; expect( - getInputValueFromState({ type: "State", id: "x", property: "1" }, state), + getInputValueFromState( + { link: "component", id: "x", property: "1" }, + state, + ), ).toEqual(5); }); it("works without input.id", () => { const state = { x: [4, 5, 6] }; expect( - getInputValueFromState({ type: "State", property: "x" }, state), + getInputValueFromState({ link: "container", property: "x" }, state), ).toEqual([4, 5, 6]); }); }); diff --git a/dashi/src/lib/actions/helpers/getInputValues.ts b/dashi/src/lib/actions/helpers/getInputValues.ts index c8de73e7..aaf436ca 100644 --- a/dashi/src/lib/actions/helpers/getInputValues.ts +++ b/dashi/src/lib/actions/helpers/getInputValues.ts @@ -1,4 +1,4 @@ -import type { Input } from "@/lib/types/model/callback"; +import type { Input } from "@/lib/types/model/channel"; import type { ContributionState } from "@/lib/types/state/contribution"; import type { ComponentState } from "@/lib/types/state/component"; import { isSubscriptable } from "@/lib/utils/isSubscriptable"; @@ -22,17 +22,16 @@ export function getInputValue( hostState?: S | undefined, ): unknown { let inputValue: unknown = undefined; - const inputKind = input.type || "Input"; - if ( - (inputKind === "Input" || inputKind === "State") && - contributionState.component - ) { + const dataSource = input.link || "component"; + if (dataSource === "component" && contributionState.component) { // Return value of a property of some component in the tree inputValue = getComponentStateValue(contributionState.component, input); - } else if (inputKind === "AppInput" && hostState) { + } else if (dataSource === "container" && contributionState.state) { + inputValue = getInputValueFromState(input, hostState); + } else if (dataSource === "app" && hostState) { inputValue = getInputValueFromState(input, hostState); } else { - console.warn(`input of unknown kind:`, input); + console.warn(`input with unknown data source:`, input); } if (inputValue === undefined || inputValue === noValue) { // We use null, because undefined is not JSON-serializable. diff --git a/dashi/src/lib/components/Component.tsx b/dashi/src/lib/components/Component.tsx index f8f21cd1..dd05ee17 100644 --- a/dashi/src/lib/components/Component.tsx +++ b/dashi/src/lib/components/Component.tsx @@ -15,7 +15,7 @@ export function Component({ type, ...props }: ComponentProps) { // TODO: allow for registering components via their types // and make following code generic. // - // const DashiComp = Registry.getComponent(type); + // const DashiComp = Registry.getComponent(link); // return return ; // if (type === "Plot") { diff --git a/dashi/src/lib/types/model/callback.ts b/dashi/src/lib/types/model/callback.ts index b82f8ad3..d8f94219 100644 --- a/dashi/src/lib/types/model/callback.ts +++ b/dashi/src/lib/types/model/callback.ts @@ -1,3 +1,5 @@ +import type { Input, Output } from "@/lib/types/model/channel"; + export interface Callback { function: CbFunction; inputs?: Input[]; @@ -30,43 +32,6 @@ export interface CbParameter { default?: unknown; } -export type InputType = "AppInput" | "Input" | "State"; - -export type OutputType = "AppOutput" | "Output"; - -export type InputOutputType = InputType | OutputType; - -export interface InputOutput { - /** - * The type of input or output. - */ - type: InputOutputType; - - /** - * The identifier of a subcomponent. - * `id` is not needed if type == "AppInput" | "AppOutput". - */ - id?: string; - - // TODO: we must allow `property` to be a constant - // expression of the form: name {"." name | index}. - // Then we get the normalized form - // property: string[]; - - /** - * The property of an object or array index. - */ - property: string; -} - -export interface Input extends InputOutput { - type: InputType; -} - -export interface Output extends InputOutput { - type: OutputType; -} - /** * A reference to a specific contribution. */ diff --git a/dashi/src/lib/types/model/channel.ts b/dashi/src/lib/types/model/channel.ts new file mode 100644 index 00000000..145f2de4 --- /dev/null +++ b/dashi/src/lib/types/model/channel.ts @@ -0,0 +1,33 @@ +export type Link = "component" | "container" | "app"; + +/** + * Base for `Input` and `Output`. + */ +export interface Channel { + /** + * The link provides the source for inputs and that target for outputs. + */ + link: Link; + + /** + * The identifier of a subcomponent. + * `id` is not needed if link == "AppInput" | "AppOutput". + */ + id?: string; + + // TODO: we must allow `property` to be a constant + // expression of the form: name {"." name | index}. + // Then we get the normalized form + // property: string[]; + + /** + * The property of an object or array index. + */ + property: string; +} + +export interface Input extends Channel { + noTrigger?: boolean; +} + +export interface Output extends Channel {} From b765c18d0e3591a07cb7529dcd1668fbf12bf16c Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Wed, 6 Nov 2024 17:26:37 +0100 Subject: [PATCH 06/10] ContributionState.state --> ContributionState.container; using it in the demo as such --- dashi/src/demo/actions/hidePanel.ts | 6 ++- dashi/src/demo/actions/showPanel.ts | 6 ++- dashi/src/demo/components/Panel.tsx | 38 +++++++++---------- dashi/src/demo/components/PanelsControl.tsx | 5 ++- dashi/src/demo/components/PanelsRow.tsx | 30 +++++++-------- .../helpers/applyStateChangeRequests.test.ts | 2 +- .../helpers/applyStateChangeRequests.ts | 15 +++----- .../actions/helpers/getInputValues.test.ts | 12 +++--- .../src/lib/actions/helpers/getInputValues.ts | 8 ++-- .../lib/actions/initializeContributions.ts | 2 +- ...tate.ts => updateContributionContainer.ts} | 15 ++++---- dashi/src/lib/index.ts | 3 +- dashi/src/lib/types/state/contribution.ts | 4 +- dashi/src/lib/utils/isContainerState.ts | 6 +-- 14 files changed, 75 insertions(+), 77 deletions(-) rename dashi/src/lib/actions/{updateContributionState.ts => updateContributionContainer.ts} (87%) diff --git a/dashi/src/demo/actions/hidePanel.ts b/dashi/src/demo/actions/hidePanel.ts index ffb11113..4dd33c69 100644 --- a/dashi/src/demo/actions/hidePanel.ts +++ b/dashi/src/demo/actions/hidePanel.ts @@ -1,6 +1,8 @@ -import { updateContributionState } from "@/lib"; +import { updateContributionContainer } from "@/lib"; import type { PanelState } from "@/demo/types"; export function hidePanel(panelIndex: number) { - updateContributionState("panels", panelIndex, { visible: false }); + updateContributionContainer("panels", panelIndex, { + visible: false, + }); } diff --git a/dashi/src/demo/actions/showPanel.ts b/dashi/src/demo/actions/showPanel.ts index 39190ce2..5a61bb6b 100644 --- a/dashi/src/demo/actions/showPanel.ts +++ b/dashi/src/demo/actions/showPanel.ts @@ -1,6 +1,8 @@ -import { updateContributionState } from "@/lib"; +import { updateContributionContainer } from "@/lib"; import type { PanelState } from "@/demo/types"; export function showPanel(panelIndex: number) { - updateContributionState("panels", panelIndex, { visible: true }); + updateContributionContainer("panels", panelIndex, { + visible: true, + }); } diff --git a/dashi/src/demo/components/Panel.tsx b/dashi/src/demo/components/Panel.tsx index 7e85d6eb..d2a286fa 100644 --- a/dashi/src/demo/components/Panel.tsx +++ b/dashi/src/demo/components/Panel.tsx @@ -1,11 +1,7 @@ import type { CSSProperties, ReactElement } from "react"; import CircularProgress from "@mui/material/CircularProgress"; - -import { - type ComponentChangeHandler, - type ContributionState, - Component, -} from "@/lib"; +import { Component } from "@/lib"; +import type { ComponentState, ComponentChangeHandler } from "@/lib"; import type { PanelState } from "@/demo/types"; const panelContainerStyle: CSSProperties = { @@ -32,39 +28,43 @@ const panelContentStyle: CSSProperties = { padding: 2, }; -interface PanelProps extends ContributionState { +interface PanelProps extends PanelState { + componentProps?: ComponentState; + componentStatus?: string; + componentError?: { message: string }; onChange: ComponentChangeHandler; } function Panel({ - name, - state, - component, - componentResult, + title, + visible, + componentProps, + componentStatus, + componentError, onChange, }: PanelProps) { - if (!state.visible) { + if (!visible) { return null; } let panelElement: ReactElement | null = null; - if (component) { - panelElement = ; - } else if (componentResult.error) { + if (componentProps) { + panelElement = ; + } else if (componentError) { panelElement = ( - Error loading {name}: {componentResult.error.message} + Error loading panel {title}: {componentError.message} ); - } else if (componentResult.status === "pending") { + } else if (componentStatus === "pending") { panelElement = ( - Loading {name}... + Loading {title}... ); } return (
-
{state.title}
+
{title}
{panelElement}
); diff --git a/dashi/src/demo/components/PanelsControl.tsx b/dashi/src/demo/components/PanelsControl.tsx index 9214b7de..ac13d783 100644 --- a/dashi/src/demo/components/PanelsControl.tsx +++ b/dashi/src/demo/components/PanelsControl.tsx @@ -17,15 +17,16 @@ function PanelsControl() { {panelStates.map((panelState, panelIndex) => { const id = `panels.${panelIndex}`; + const { title, visible } = panelState.container; return ( { if (e.currentTarget.checked) { diff --git a/dashi/src/demo/components/PanelsRow.tsx b/dashi/src/demo/components/PanelsRow.tsx index cf0bd986..8a5a5b43 100644 --- a/dashi/src/demo/components/PanelsRow.tsx +++ b/dashi/src/demo/components/PanelsRow.tsx @@ -1,5 +1,3 @@ -import type { JSX } from "react"; - import { type ComponentChangeEvent, handleComponentChange } from "@/lib"; import { usePanelStates } from "@/demo/hooks"; import Panel from "./Panel"; @@ -17,23 +15,21 @@ function PanelsRow() { ) => { handleComponentChange("panels", panelIndex, panelEvent); }; - const visiblePanels: JSX.Element[] = []; - panelStates.forEach((panelState, panelIndex) => { - if (panelState.state.visible) { - visiblePanels.push( - handlePanelChange(panelIndex, e)} - />, - ); - } + const panels = panelStates.map((panelState, panelIndex) => { + const { container, component, componentResult } = panelState; + return ( + handlePanelChange(panelIndex, e)} + /> + ); }); - const panelElements = <>{visiblePanels}; return ( -
- {panelElements} -
+
{panels}
); } diff --git a/dashi/src/lib/actions/helpers/applyStateChangeRequests.test.ts b/dashi/src/lib/actions/helpers/applyStateChangeRequests.test.ts index a29aa32e..972721d3 100644 --- a/dashi/src/lib/actions/helpers/applyStateChangeRequests.test.ts +++ b/dashi/src/lib/actions/helpers/applyStateChangeRequests.test.ts @@ -35,7 +35,7 @@ describe("Test that applyContributionChangeRequests()", () => { name: "", extension: "", componentResult: { status: "ok" }, - state: { visible: true }, + container: { visible: true }, component: componentTree, }, ], diff --git a/dashi/src/lib/actions/helpers/applyStateChangeRequests.ts b/dashi/src/lib/actions/helpers/applyStateChangeRequests.ts index abee3098..da187ee9 100644 --- a/dashi/src/lib/actions/helpers/applyStateChangeRequests.ts +++ b/dashi/src/lib/actions/helpers/applyStateChangeRequests.ts @@ -69,22 +69,19 @@ export function applyContributionChangeRequests( stateChangeRequests.forEach( ({ contribPoint, contribIndex, stateChanges }) => { const contribution = contributionsRecord[contribPoint][contribIndex]; - const state = applyStateChanges( - contribution.state, - stateChanges.filter( - (stateChange) => stateChange.link === "container" && !stateChange.id, - ), + const container = applyStateChanges( + contribution.container, + stateChanges.filter((stateChange) => stateChange.link === "container"), ); const component = applyComponentStateChanges( contribution.component, stateChanges.filter( (stateChange) => - (!stateChange.link || stateChange.link === "component") && - stateChange.id, + !stateChange.link || stateChange.link === "component", ), ); if ( - state !== contribution.state || + container !== contribution.container || component !== contribution.component ) { contributionsRecord = { @@ -92,7 +89,7 @@ export function applyContributionChangeRequests( [contribPoint]: updateArray( contributionsRecord[contribPoint], contribIndex, - { ...contribution, state, component }, + { ...contribution, container, component }, ), }; } diff --git a/dashi/src/lib/actions/helpers/getInputValues.test.ts b/dashi/src/lib/actions/helpers/getInputValues.test.ts index 980f33a8..ce83e2e1 100644 --- a/dashi/src/lib/actions/helpers/getInputValues.test.ts +++ b/dashi/src/lib/actions/helpers/getInputValues.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect } from "vitest"; import type { ComponentState, PlotState } from "@/lib/types/state/component"; import { - getComponentStateValue, + getInputValueFromComponent, getInputValueFromState, } from "./getInputValues"; @@ -23,10 +23,10 @@ const componentState: ComponentState = { value: 14, }; -describe("Test that getComponentStateValue()", () => { +describe("Test that getInputValueFromComponent()", () => { it("works on 1st level", () => { expect( - getComponentStateValue(componentState, { + getInputValueFromComponent(componentState, { link: "component", id: "b1", property: "value", @@ -36,7 +36,7 @@ describe("Test that getComponentStateValue()", () => { it("works on 2nd level", () => { expect( - getComponentStateValue(componentState, { + getInputValueFromComponent(componentState, { link: "component", id: "p1", property: "chart", @@ -46,7 +46,7 @@ describe("Test that getComponentStateValue()", () => { it("works on 3rd level", () => { expect( - getComponentStateValue(componentState, { + getInputValueFromComponent(componentState, { link: "component", id: "cb1", property: "value", @@ -54,7 +54,7 @@ describe("Test that getComponentStateValue()", () => { ).toEqual(true); expect( - getComponentStateValue(componentState, { + getInputValueFromComponent(componentState, { link: "component", id: "dd1", property: "value", diff --git a/dashi/src/lib/actions/helpers/getInputValues.ts b/dashi/src/lib/actions/helpers/getInputValues.ts index aaf436ca..8407a582 100644 --- a/dashi/src/lib/actions/helpers/getInputValues.ts +++ b/dashi/src/lib/actions/helpers/getInputValues.ts @@ -25,8 +25,8 @@ export function getInputValue( const dataSource = input.link || "component"; if (dataSource === "component" && contributionState.component) { // Return value of a property of some component in the tree - inputValue = getComponentStateValue(contributionState.component, input); - } else if (dataSource === "container" && contributionState.state) { + inputValue = getInputValueFromComponent(contributionState.component, input); + } else if (dataSource === "container" && contributionState.container) { inputValue = getInputValueFromState(input, hostState); } else if (dataSource === "app" && hostState) { inputValue = getInputValueFromState(input, hostState); @@ -42,7 +42,7 @@ export function getInputValue( } // we export for testing only -export function getComponentStateValue( +export function getInputValueFromComponent( componentState: ComponentState, input: Input, ): unknown { @@ -53,7 +53,7 @@ export function getComponentStateValue( } else if (isContainerState(componentState)) { for (let i = 0; i < componentState.components.length; i++) { const item = componentState.components[i]; - const itemValue = getComponentStateValue(item, input); + const itemValue = getInputValueFromComponent(item, input); if (itemValue !== noValue) { return itemValue; } diff --git a/dashi/src/lib/actions/initializeContributions.ts b/dashi/src/lib/actions/initializeContributions.ts index 4bd0516e..64109047 100644 --- a/dashi/src/lib/actions/initializeContributions.ts +++ b/dashi/src/lib/actions/initializeContributions.ts @@ -49,7 +49,7 @@ function newContributionState( ): ContributionState { return { ...rawContribution, - state: { ...rawContribution.initialState }, + container: { ...rawContribution.initialState }, componentResult: {}, }; } diff --git a/dashi/src/lib/actions/updateContributionState.ts b/dashi/src/lib/actions/updateContributionContainer.ts similarity index 87% rename from dashi/src/lib/actions/updateContributionState.ts rename to dashi/src/lib/actions/updateContributionContainer.ts index 742c8b0f..dfc11169 100644 --- a/dashi/src/lib/actions/updateContributionState.ts +++ b/dashi/src/lib/actions/updateContributionContainer.ts @@ -6,31 +6,30 @@ import { updateArray } from "@/lib/utils/updateArray"; import type { ContribPoint } from "@/lib/types/model/extension"; import type { ContributionState } from "@/lib/types/state/contribution"; -export function updateContributionState( +export function updateContributionContainer( contribPoint: ContribPoint, contribIndex: number, - contribState: Partial, + container: Partial, requireComponent: boolean = true, ) { const { configuration, contributionsRecord } = store.getState(); const contributionStates = contributionsRecord[contribPoint]; const contributionState = contributionStates[contribIndex]; - if (contributionState.state === contribState) { + if (contributionState.container === container) { return; // nothing to do } const componentStatus = contributionState.componentResult.status; if (!requireComponent || componentStatus) { _updateContributionState(contribPoint, contribIndex, { - state: contribState, + container, }); } else if (!componentStatus) { // No status yet, so we must load the component _updateContributionState(contribPoint, contribIndex, { - state: contribState, + container, componentResult: { status: "pending" }, }); const inputValues = getLayoutInputValues(contribPoint, contribIndex); - console.log("inputValues:", inputValues); fetchApiResult( fetchInitialComponentState, contribPoint, @@ -70,10 +69,10 @@ function _updateContributionState( const { contributionsRecord } = store.getState(); const contribStates = contributionsRecord[contribPoint]!; const contribStateOld = contribStates[contribIndex]; - const contribStateNew = contribState.state + const contribStateNew = contribState.container ? { ...contribState, - state: { ...contribStateOld.state, ...contribState.state }, + container: { ...contribStateOld.container, ...contribState.container }, } : contribState; store.setState({ diff --git a/dashi/src/lib/index.ts b/dashi/src/lib/index.ts index e09a1205..145ed712 100644 --- a/dashi/src/lib/index.ts +++ b/dashi/src/lib/index.ts @@ -1,6 +1,7 @@ // Types export { type Contribution } from "@/lib/types/model/contribution"; export { type ContributionState } from "@/lib/types/state/contribution"; +export { type ComponentState } from "@/lib/types/state/component"; export { type ComponentChangeEvent, type ComponentChangeHandler, @@ -9,7 +10,7 @@ export { export { initializeContributions } from "@/lib/actions/initializeContributions"; export { configureFramework } from "@/lib/actions/configureFramework"; export { handleComponentChange } from "@/lib/actions/handleComponentChange"; -export { updateContributionState } from "@/lib/actions/updateContributionState"; +export { updateContributionContainer } from "@/lib/actions/updateContributionContainer"; // React Components export { Component } from "@/lib/components/Component"; // React Hooks diff --git a/dashi/src/lib/types/state/contribution.ts b/dashi/src/lib/types/state/contribution.ts index aaafd46e..25237e89 100644 --- a/dashi/src/lib/types/state/contribution.ts +++ b/dashi/src/lib/types/state/contribution.ts @@ -5,9 +5,9 @@ import type { ComponentState } from "./component"; export interface ContributionState extends Contribution { /** - * Contribution-private state properties. + * State that is used by the container of this contribution. */ - state: S; + container: S; /** * The result of loading the initial component tree. */ diff --git a/dashi/src/lib/utils/isContainerState.ts b/dashi/src/lib/utils/isContainerState.ts index 01a5930d..8d2a1d1b 100644 --- a/dashi/src/lib/utils/isContainerState.ts +++ b/dashi/src/lib/utils/isContainerState.ts @@ -4,7 +4,7 @@ import { } from "@/lib/types/state/component"; export function isContainerState( - componentModel: ComponentState, -): componentModel is ContainerState { - return !!componentModel.components; + component: ComponentState, +): component is ContainerState { + return !!component.components; } From a3652760414d0f20b3c088cd31e5a3d4543673fd Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Wed, 6 Nov 2024 17:39:57 +0100 Subject: [PATCH 07/10] log callbacks only in dev mode --- .../src/lib/actions/helpers/invokeCallbacks.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/dashi/src/lib/actions/helpers/invokeCallbacks.ts b/dashi/src/lib/actions/helpers/invokeCallbacks.ts index 83082a86..e528ad2a 100644 --- a/dashi/src/lib/actions/helpers/invokeCallbacks.ts +++ b/dashi/src/lib/actions/helpers/invokeCallbacks.ts @@ -6,7 +6,10 @@ import { applyStateChangeRequests } from "@/lib/actions/helpers/applyStateChange export function invokeCallbacks(callbackRequests: CallbackRequest[]) { const { configuration } = store.getState(); - console.debug("invokeCallbacks -->", callbackRequests); + const invocationId = getInvocationId(); + if (import.meta.env.DEV) { + console.debug(`invokeCallbacks (${invocationId})-->`, callbackRequests); + } if (callbackRequests.length) { fetchApiResult( fetchStateChangeRequests, @@ -14,7 +17,12 @@ export function invokeCallbacks(callbackRequests: CallbackRequest[]) { configuration.api, ).then((changeRequestsResult) => { if (changeRequestsResult.data) { - console.debug("invokeCallbacks <--", changeRequestsResult.data); + if (import.meta.env.DEV) { + console.debug( + `invokeCallbacks <--(${invocationId})`, + changeRequestsResult.data, + ); + } applyStateChangeRequests(changeRequestsResult.data); } else { console.error( @@ -27,3 +35,9 @@ export function invokeCallbacks(callbackRequests: CallbackRequest[]) { }); } } + +let invocationCounter = 0; + +function getInvocationId() { + return invocationCounter++; +} From 7fd1e57a514eda8633f0903e40f43d924f527015 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Wed, 6 Nov 2024 17:59:40 +0100 Subject: [PATCH 08/10] fixed build --- dashi/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/dashi/tsconfig.json b/dashi/tsconfig.json index c344941d..34a17a3a 100644 --- a/dashi/tsconfig.json +++ b/dashi/tsconfig.json @@ -17,6 +17,7 @@ "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", + "types": ["vite/client"], /* Linting */ "strict": true, From e2938f538c8730e1c033edbc2ab8a6b1328c578f Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Wed, 6 Nov 2024 18:00:04 +0100 Subject: [PATCH 09/10] moved stuff from utils into actions/helpers --- dashi/src/lib/actions/configureFramework.ts | 2 +- dashi/src/lib/actions/helpers/applyStateChangeRequests.ts | 2 +- dashi/src/lib/{utils => actions/helpers}/configureLogging.ts | 0 dashi/src/lib/actions/helpers/getInputValues.ts | 2 +- dashi/src/lib/{utils => actions/helpers}/isContainerState.ts | 0 dashi/src/lib/index.ts | 2 -- dashi/src/lib/types/state/store.ts | 2 +- 7 files changed, 4 insertions(+), 6 deletions(-) rename dashi/src/lib/{utils => actions/helpers}/configureLogging.ts (100%) rename dashi/src/lib/{utils => actions/helpers}/isContainerState.ts (100%) diff --git a/dashi/src/lib/actions/configureFramework.ts b/dashi/src/lib/actions/configureFramework.ts index 67123138..ce0dc3e2 100644 --- a/dashi/src/lib/actions/configureFramework.ts +++ b/dashi/src/lib/actions/configureFramework.ts @@ -1,6 +1,6 @@ import { store } from "@/lib/store"; import type { FrameworkOptions } from "@/lib/types/state/store"; -import { configureLogging } from "@/lib/utils/configureLogging"; +import { configureLogging } from "@/lib/actions/helpers/configureLogging"; import { handleHostStoreChange } from "./handleHostStoreChange"; export function configureFramework( diff --git a/dashi/src/lib/actions/helpers/applyStateChangeRequests.ts b/dashi/src/lib/actions/helpers/applyStateChangeRequests.ts index da187ee9..f17d6120 100644 --- a/dashi/src/lib/actions/helpers/applyStateChangeRequests.ts +++ b/dashi/src/lib/actions/helpers/applyStateChangeRequests.ts @@ -10,7 +10,7 @@ import type { import type { ContribPoint } from "@/lib/types/model/extension"; import type { ContributionState } from "@/lib"; import { updateArray } from "@/lib/utils/updateArray"; -import { isContainerState } from "@/lib/utils/isContainerState"; +import { isContainerState } from "@/lib/actions/helpers/isContainerState"; export function applyStateChangeRequests( stateChangeRequests: StateChangeRequest[], diff --git a/dashi/src/lib/utils/configureLogging.ts b/dashi/src/lib/actions/helpers/configureLogging.ts similarity index 100% rename from dashi/src/lib/utils/configureLogging.ts rename to dashi/src/lib/actions/helpers/configureLogging.ts diff --git a/dashi/src/lib/actions/helpers/getInputValues.ts b/dashi/src/lib/actions/helpers/getInputValues.ts index 8407a582..77d1b5c0 100644 --- a/dashi/src/lib/actions/helpers/getInputValues.ts +++ b/dashi/src/lib/actions/helpers/getInputValues.ts @@ -2,7 +2,7 @@ import type { Input } from "@/lib/types/model/channel"; import type { ContributionState } from "@/lib/types/state/contribution"; import type { ComponentState } from "@/lib/types/state/component"; import { isSubscriptable } from "@/lib/utils/isSubscriptable"; -import { isContainerState } from "@/lib/utils/isContainerState"; +import { isContainerState } from "@/lib/actions/helpers/isContainerState"; export function getInputValues( inputs: Input[], diff --git a/dashi/src/lib/utils/isContainerState.ts b/dashi/src/lib/actions/helpers/isContainerState.ts similarity index 100% rename from dashi/src/lib/utils/isContainerState.ts rename to dashi/src/lib/actions/helpers/isContainerState.ts diff --git a/dashi/src/lib/index.ts b/dashi/src/lib/index.ts index 145ed712..2a3f76a9 100644 --- a/dashi/src/lib/index.ts +++ b/dashi/src/lib/index.ts @@ -20,5 +20,3 @@ export { useContributionsResult, useContributionsRecord, } from "@/lib/hooks"; -// Utilities -export { configureLogging } from "@/lib/utils/configureLogging"; diff --git a/dashi/src/lib/types/state/store.ts b/dashi/src/lib/types/state/store.ts index 688b1995..038cc5a3 100644 --- a/dashi/src/lib/types/state/store.ts +++ b/dashi/src/lib/types/state/store.ts @@ -6,7 +6,7 @@ import type { import type { ApiResult } from "@/lib/utils/fetchApiResult"; import type { ContributionState } from "@/lib/types/state/contribution"; import type { ApiOptions } from "@/lib/api"; -import type { LoggingOptions } from "@/lib/utils/configureLogging"; +import type { LoggingOptions } from "@/lib/actions/helpers/configureLogging"; import type { StoreApi } from "zustand"; export interface FrameworkOptions { From ff534c21416f19343bd5b4a055f21fb8b7c6d16e Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Wed, 6 Nov 2024 18:11:49 +0100 Subject: [PATCH 10/10] released 0.0.8 --- dashi/package-lock.json | 4 ++-- dashi/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dashi/package-lock.json b/dashi/package-lock.json index 311b7cce..86867941 100644 --- a/dashi/package-lock.json +++ b/dashi/package-lock.json @@ -1,12 +1,12 @@ { "name": "dashipopashi", - "version": "0.0.7", + "version": "0.0.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dashipopashi", - "version": "0.0.7", + "version": "0.0.8", "license": "MIT", "dependencies": { "@emotion/react": "^11.13.3", diff --git a/dashi/package.json b/dashi/package.json index e7a1ea15..265c7ff8 100644 --- a/dashi/package.json +++ b/dashi/package.json @@ -1,6 +1,6 @@ { "name": "dashipopashi", - "version": "0.0.7", + "version": "0.0.8", "description": "An experimental library for integrating interactive charts into existing JavaScript applications.", "type": "module", "files": [