From dc545281a94ae558d5bf30f540350c57af2148a7 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Sat, 23 Nov 2024 16:51:46 +0100 Subject: [PATCH 1/2] Removed link from channel --> 0.0.26 --- chartlets.js/CHANGES.md | 6 + chartlets.js/package-lock.json | 4 +- chartlets.js/package.json | 2 +- .../src/lib/actions/handleComponentChange.ts | 4 +- .../src/lib/actions/handleHostStoreChange.ts | 2 +- .../helpers/applyStateChangeRequests.test.ts | 5 - .../helpers/applyStateChangeRequests.ts | 14 +- .../actions/helpers/getInputValues.test.ts | 28 +-- .../src/lib/actions/helpers/getInputValues.ts | 14 +- chartlets.js/src/lib/types/model/channel.ts | 28 +-- chartlets.py/CHANGES.md | 9 +- chartlets.py/chartlets/channel.py | 170 ++++++------------ chartlets.py/chartlets/version.py | 2 +- chartlets.py/my_extension/my_panel_2.py | 6 +- chartlets.py/my_extension/my_panel_3.py | 4 +- chartlets.py/tests/callback_test.py | 7 - chartlets.py/tests/channel_test.py | 34 ++-- 17 files changed, 130 insertions(+), 209 deletions(-) diff --git a/chartlets.js/CHANGES.md b/chartlets.js/CHANGES.md index afe0069d..c33ad80f 100644 --- a/chartlets.js/CHANGES.md +++ b/chartlets.js/CHANGES.md @@ -1,3 +1,9 @@ +## Version 0.0.26 (from 2024/11/23) + +* Channels such as `Input`, `State`, `Output` no longer have a `link` property. + Instead, we use a special `id` format, namely `"@app"` and `@container` + to address states other than components. (#52) + ## Version 0.0.25 (from 2024/11/23) * `Registry.register()` now requires the `type` diff --git a/chartlets.js/package-lock.json b/chartlets.js/package-lock.json index ed22c50e..522f6bbe 100644 --- a/chartlets.js/package-lock.json +++ b/chartlets.js/package-lock.json @@ -1,12 +1,12 @@ { "name": "chartlets", - "version": "0.0.25", + "version": "0.0.26", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "chartlets", - "version": "0.0.25", + "version": "0.0.26", "license": "MIT", "dependencies": { "@emotion/react": "^11.13.3", diff --git a/chartlets.js/package.json b/chartlets.js/package.json index befc21f2..f84bfaf8 100644 --- a/chartlets.js/package.json +++ b/chartlets.js/package.json @@ -1,6 +1,6 @@ { "name": "chartlets", - "version": "0.0.25", + "version": "0.0.26", "description": "An experimental library for integrating interactive charts into existing JavaScript applications.", "type": "module", "files": [ diff --git a/chartlets.js/src/lib/actions/handleComponentChange.ts b/chartlets.js/src/lib/actions/handleComponentChange.ts index 7bae1093..fe3d4546 100644 --- a/chartlets.js/src/lib/actions/handleComponentChange.ts +++ b/chartlets.js/src/lib/actions/handleComponentChange.ts @@ -23,7 +23,6 @@ export function handleComponentChange( contribIndex, stateChanges: [ { - link: "component", id: changeEvent.id, property: changeEvent.property, value: changeEvent.value, @@ -65,7 +64,8 @@ function getCallbackRequests( const inputIndex = inputs.findIndex( (input) => !input.noTrigger && - (!input.link || input.link === "component") && + input.id && + !input.id.startsWith("@") && input.id === changeEvent.id && equalObjPaths(input.property, changeEvent.property), ); diff --git a/chartlets.js/src/lib/actions/handleHostStoreChange.ts b/chartlets.js/src/lib/actions/handleHostStoreChange.ts index d021e115..4f2922bf 100644 --- a/chartlets.js/src/lib/actions/handleHostStoreChange.ts +++ b/chartlets.js/src/lib/actions/handleHostStoreChange.ts @@ -79,7 +79,7 @@ function getHostStorePropertyRefs(): PropertyRef[] { (contribution.callbacks || []).forEach( (callback, callbackIndex) => (callback.inputs || []).forEach((input, inputIndex) => { - if (!input.noTrigger && input.link === "app" && input.property) { + if (!input.noTrigger && input.id === "@app" && input.property) { propertyRefs.push({ contribPoint, contribIndex, diff --git a/chartlets.js/src/lib/actions/helpers/applyStateChangeRequests.test.ts b/chartlets.js/src/lib/actions/helpers/applyStateChangeRequests.test.ts index b78a9ae7..85748358 100644 --- a/chartlets.js/src/lib/actions/helpers/applyStateChangeRequests.test.ts +++ b/chartlets.js/src/lib/actions/helpers/applyStateChangeRequests.test.ts @@ -43,7 +43,6 @@ describe("Test that applyContributionChangeRequests()", () => { contribIndex: 0, stateChanges: [ { - link: "component", id: "dd1", property: "value", value: 14, @@ -56,7 +55,6 @@ describe("Test that applyContributionChangeRequests()", () => { contribIndex: 0, stateChanges: [ { - link: "component", id: "dd1", property: "value", value: 13, @@ -88,7 +86,6 @@ describe("Test that applyContributionChangeRequests()", () => { describe("Test that applyComponentStateChange()", () => { it("changes state if values are different", () => { const newState = applyComponentStateChange(componentTree, { - link: "component", id: "cb1", property: "value", value: false, @@ -102,7 +99,6 @@ describe("Test that applyComponentStateChange()", () => { it("doesn't change the state if value stays the same", () => { const newState = applyComponentStateChange(componentTree, { - link: "component", id: "cb1", property: "value", value: true, @@ -117,7 +113,6 @@ describe("Test that applyComponentStateChange()", () => { children: ["Hello", "World"], }; const newState = applyComponentStateChange(componentTree, { - link: "component", id: "b1", property: "", value, diff --git a/chartlets.js/src/lib/actions/helpers/applyStateChangeRequests.ts b/chartlets.js/src/lib/actions/helpers/applyStateChangeRequests.ts index 64bdb2a5..f40cf11a 100644 --- a/chartlets.js/src/lib/actions/helpers/applyStateChangeRequests.ts +++ b/chartlets.js/src/lib/actions/helpers/applyStateChangeRequests.ts @@ -22,6 +22,11 @@ import { isMutableHostStore, type MutableHostStore, } from "@/lib/types/state/options"; +import { + isHostChannel, + isComponentChannel, + isContainerChannel, +} from "@/lib/types/model/channel"; export function applyStateChangeRequests( stateChangeRequests: StateChangeRequest[], @@ -52,14 +57,11 @@ export function applyContributionChangeRequests( const contribution = contributionsRecord[contribPoint][contribIndex]; const container = applyStateChanges( contribution.container, - stateChanges.filter((stateChange) => stateChange.link === "container"), + stateChanges.filter(isContainerChannel), ); const component = applyComponentStateChanges( contribution.component, - stateChanges.filter( - (stateChange) => - !stateChange.link || stateChange.link === "component", - ), + stateChanges.filter(isComponentChannel), ); if ( container !== contribution.container || @@ -148,7 +150,7 @@ function applyHostStateChanges( ) { stateChangeRequests.forEach((stateChangeRequest) => { stateChangeRequest.stateChanges.forEach((stateChange) => { - if (stateChange.link === "app") { + if (isHostChannel(stateChange)) { hostStore.set(formatObjPath(stateChange.property), stateChange.value); } }); diff --git a/chartlets.js/src/lib/actions/helpers/getInputValues.test.ts b/chartlets.js/src/lib/actions/helpers/getInputValues.test.ts index 5e75e14a..2f9f7bda 100644 --- a/chartlets.js/src/lib/actions/helpers/getInputValues.test.ts +++ b/chartlets.js/src/lib/actions/helpers/getInputValues.test.ts @@ -27,7 +27,6 @@ describe("Test that getInputValueFromComponent()", () => { expect( getInputValueFromComponent( { - link: "component", id: "b1", property: "value", }, @@ -40,7 +39,6 @@ describe("Test that getInputValueFromComponent()", () => { expect( getInputValueFromComponent( { - link: "component", id: "p1", property: "chart", }, @@ -53,7 +51,6 @@ describe("Test that getInputValueFromComponent()", () => { expect( getInputValueFromComponent( { - link: "component", id: "cb1", property: "value", }, @@ -64,7 +61,6 @@ describe("Test that getInputValueFromComponent()", () => { expect( getInputValueFromComponent( { - link: "component", id: "dd1", property: "value", }, @@ -77,42 +73,36 @@ describe("Test that getInputValueFromComponent()", () => { describe("Test that getInputValueFromState()", () => { it("works with input.id and input.property", () => { const state = { x: { y: 26 } }; - expect( - getInputValueFromState( - { link: "component", id: "x", property: "y" }, - state, - ), - ).toEqual(26); + expect(getInputValueFromState({ id: "x", property: "y" }, state)).toEqual( + 26, + ); }); it("works with arrays indexes", () => { const state = { x: [4, 5, 6] }; - expect( - getInputValueFromState( - { link: "component", id: "x", property: "1" }, - state, - ), - ).toEqual(5); + expect(getInputValueFromState({ id: "x", property: "1" }, state)).toEqual( + 5, + ); }); it("works without input.id", () => { const state = { x: [4, 5, 6] }; expect( - getInputValueFromState({ link: "container", property: "x" }, state), + getInputValueFromState({ id: "@container", property: "x" }, state), ).toEqual([4, 5, 6]); }); it("works on 2nd level", () => { const state = { x: { y: 15 } }; expect( - getInputValueFromState({ link: "container", property: "x.y" }, state), + getInputValueFromState({ id: "@container", property: "x.y" }, state), ).toEqual(15); }); it("works on 3nd level", () => { const state = { x: { y: [4, 5, 6] } }; expect( - getInputValueFromState({ link: "container", property: "x.y.2" }, state), + getInputValueFromState({ id: "@container", property: "x.y.2" }, state), ).toEqual(6); }); }); diff --git a/chartlets.js/src/lib/actions/helpers/getInputValues.ts b/chartlets.js/src/lib/actions/helpers/getInputValues.ts index dbabd729..3980c665 100644 --- a/chartlets.js/src/lib/actions/helpers/getInputValues.ts +++ b/chartlets.js/src/lib/actions/helpers/getInputValues.ts @@ -1,4 +1,9 @@ -import type { Input } from "@/lib/types/model/channel"; +import { + type Input, + isComponentChannel, + isContainerChannel, + isHostChannel, +} from "@/lib/types/model/channel"; import type { ContributionState } from "@/lib/types/state/contribution"; import { type ComponentState, @@ -27,12 +32,11 @@ export function getInputValue( hostStore?: HostStore, ): unknown { let inputValue: unknown = undefined; - const dataSource = input.link || "component"; - if (dataSource === "component" && contributionState.component) { + if (isComponentChannel(input) && contributionState.component) { inputValue = getInputValueFromComponent(input, contributionState.component); - } else if (dataSource === "container" && contributionState.container) { + } else if (isContainerChannel(input) && contributionState.container) { inputValue = getInputValueFromState(input, contributionState.container); - } else if (dataSource === "app" && hostStore) { + } else if (isHostChannel(input) && hostStore) { inputValue = getInputValueFromHostStore(input, hostStore); } else { console.warn(`input with unknown data source:`, input); diff --git a/chartlets.js/src/lib/types/model/channel.ts b/chartlets.js/src/lib/types/model/channel.ts index abf2b4d1..1744abe9 100644 --- a/chartlets.js/src/lib/types/model/channel.ts +++ b/chartlets.js/src/lib/types/model/channel.ts @@ -1,21 +1,17 @@ import type { ObjPathLike } from "@/lib/utils/objPath"; -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. + * The identifier for a component or state. + * + * Special identifiers are: + * - `"@app"` the application state referred to by `HostStore` + * - `"@container"` the state referred to by contribution's container */ - link: Link; - - /** - * The identifier of a subcomponent. - * `id` is not needed if link == "AppInput" | "AppOutput". - */ - id?: string; + id: "@app" | "@container" | string; /** * The property of an object or array index. @@ -28,3 +24,15 @@ export interface Input extends Channel { } export interface Output extends Channel {} + +export function isComponentChannel(channel: Channel): boolean { + return Boolean(channel.id) && !channel.id.startsWith("@"); +} + +export function isHostChannel(channel: Channel): boolean { + return channel.id === "@app"; +} + +export function isContainerChannel(channel: Channel): boolean { + return channel.id === "@container"; +} diff --git a/chartlets.py/CHANGES.md b/chartlets.py/CHANGES.md index 815c2ddd..356767c0 100644 --- a/chartlets.py/CHANGES.md +++ b/chartlets.py/CHANGES.md @@ -1,4 +1,11 @@ -## Version 0.0.22 (in development) +## Version 0.0.x (in development) + +* Channels such as `Input`, `State`, `Output` no longer have a `link` property. + Instead, we use a special `id` format, namely `"@app"` and `@container` + to address states other than components. + This way, the call syntax `Input(id, property)` is the same for all states, + e.g., `Input("@app", "selectedDatasetId")`, instead of + `Input(source="app", property="selectedDatasetId")`. (#52) * Added progress components `CircularProgress`, `CircularProgressWithLabel`, `LinearProgress`, `LinearProgressWithLabel`. diff --git a/chartlets.py/chartlets/channel.py b/chartlets.py/chartlets/channel.py index 6b57f89b..e59199b3 100644 --- a/chartlets.py/chartlets/channel.py +++ b/chartlets.py/chartlets/channel.py @@ -1,52 +1,52 @@ from abc import ABC -from typing import Any, Literal +from typing import Any from .util.assertions import ( assert_is_given, assert_is_instance_of, - assert_is_none, assert_is_one_of, ) -Link = Literal["component"] | Literal["container"] | Literal["app"] - -COMPONENT = "" -"""Special property value that can be used -to refer to the entire component. -""" - - +# noinspection PyShadowingBuiltins 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 __init__(self, id: str, property: str | None = None): + self.id, self.property = self._validate_params(id, 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) + """Convert this channel into a JSON-serializable dictionary.""" + if isinstance(self, State): + return dict(id=self.id, property=self.property, noTrigger=True) + else: + return dict(id=self.id, property=self.property) + + def _validate_params(self, id_: Any, property: Any) -> tuple[str, str | None]: + assert_is_given("id", id_) + assert_is_instance_of("id", id_, str) + id: str = id_ + if id.startswith("@"): + # Other states than component states + assert_is_one_of("id", id, ("@app", "@container")) + assert_is_given("property", property) + assert_is_instance_of("property", property, str) + else: + # Component state + if property is None: + # Default property value for components is "value" + property = "value" + elif isinstance(self, Output) and property == "": + # Outputs may have an empty property + pass + else: + # Components must have valid properties + assert_is_given("property", property) + assert_is_instance_of("property", property, str) + return id, property class Input(Channel): @@ -60,27 +60,19 @@ class Input(Channel): Args: id: - Value of a component's "id" property. - Required, if `source` is `"component"` (the default). - Otherwise, it must not be passed. + Either the value of a component's `id` property, + or a special state of the form `"@"`, e.g., + `"@app"` or `@container`. property: Name of the property of a component or state. To address properties in nested objects or arrays use a dot (`.`) to separate property names and array indexes. - source: One of `"component"` (the default), `"container"`, - or `"app"`. """ # 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) + def __init__(self, id: str, property: str | None = None): + super().__init__(id, property) class State(Input): @@ -95,25 +87,19 @@ class State(Input): Args: id: - Value of a component's "id" property. - Used only if `source` is `"component"`. + Either the value of a component's `id` property, + or a special state of the form `"@"`, e.g., + `"@app"` or `@container`. property: Name of the property of a component or state. To address properties in nested objects or arrays use a dot (`.`) to separate property names and array indexes. - source: One of `"component"` (the default), `"container"`, - or `"app"`. """ # 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) + def __init__(self, id: str, property: str | None = None): + super().__init__(id, property) class Output(Channel): @@ -127,75 +113,19 @@ class Output(Channel): Args: id: - Value of a component's "id" property. - Used only if `source` is `"component"`. + Either the value of a component's `id` property, + or a special state of the form `"@"`, e.g., + `"@app"` or `@container`. property: Name of the property of a component or state. To address properties in nested objects or arrays use a dot (`.`) to separate property names and array indexes. - If `target` is `"component"` the empty string can be used - to refer to entire components. - target: One of `"component"` (the default), `"container"`, - or `"app"`. + If `id` identifies a component, then `property` may + be passed an empty string to replace components. + and the output's value must be a component or `None`. """ # 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, output=True) - - -# noinspection PyShadowingBuiltins -def _validate_params( - link_name: str, - link: Link | None, - id: str | None, - property: str | None, - output: bool = False, -) -> tuple[Link, str | None, str | None]: - if link is None or link == "component": - # Component states require an id - # and property which defaults to "value" - link = "component" - assert_is_given("id", id) - assert_is_instance_of("id", id, str) - assert_is_instance_of("property", id, (str, NoneType)) - if property is None: - # property, if not provided, defaults to "value" - property = "value" - elif not output: - # outputs are allowed to have an empty property value - assert_is_given("property", property) - else: - # Other states require a link and property - # and should have no id - assert_is_given(link_name, link) - assert_is_one_of(link_name, link, ("container", "app")) - assert_is_given("property", property) - assert_is_instance_of("property", property, str) - assert_is_none("id", id) - # noinspection PyTypeChecker - return link, id, property + def __init__(self, id: str, property: str | None = None): + super().__init__(id, property) diff --git a/chartlets.py/chartlets/version.py b/chartlets.py/chartlets/version.py index acbb201d..b67a45dc 100644 --- a/chartlets.py/chartlets/version.py +++ b/chartlets.py/chartlets/version.py @@ -1 +1 @@ -version = "0.0.22" +version = "0.0.26" diff --git a/chartlets.py/my_extension/my_panel_2.py b/chartlets.py/my_extension/my_panel_2.py index 9188ab6c..1009d9e3 100644 --- a/chartlets.py/my_extension/my_panel_2.py +++ b/chartlets.py/my_extension/my_panel_2.py @@ -10,7 +10,7 @@ panel = Panel(__name__, title="Panel B") -@panel.layout(Input(property="selectedDatasetId", source="app")) +@panel.layout(Input("@app", "selectedDatasetId")) def render_panel( ctx: Context, selected_dataset_id: str = "", @@ -51,7 +51,7 @@ def render_panel( @panel.callback( - Input(property="selectedDatasetId", source="app"), + Input("@app", "selectedDatasetId"), Input("selected_variable_name"), Output("plot", "chart"), ) @@ -89,7 +89,7 @@ def make_figure( @panel.callback( - Input(property="selectedDatasetId", source="app"), + Input("@app", "selectedDatasetId"), Output("selected_variable_name", "options"), Output("selected_variable_name", "value"), ) diff --git a/chartlets.py/my_extension/my_panel_3.py b/chartlets.py/my_extension/my_panel_3.py index 6d5df8a7..cb34167d 100644 --- a/chartlets.py/my_extension/my_panel_3.py +++ b/chartlets.py/my_extension/my_panel_3.py @@ -11,7 +11,7 @@ @panel.layout( - Input(source="app", property="selectedDatasetId"), + Input("@app", "selectedDatasetId"), ) def render_panel( ctx: Context, @@ -53,7 +53,7 @@ def render_panel( # noinspection PyUnusedLocal @panel.callback( - Input(source="app", property="selectedDatasetId"), + Input("@app", "selectedDatasetId"), Input("opaque"), Input("color"), State("info_text", "children"), diff --git a/chartlets.py/tests/callback_test.py b/chartlets.py/tests/callback_test.py index a46c6254..8d8e3f66 100644 --- a/chartlets.py/tests/callback_test.py +++ b/chartlets.py/tests/callback_test.py @@ -61,22 +61,18 @@ def test_to_dict_with_no_outputs(self): }, "inputs": [ { - "link": "component", "id": "a", "property": "value", }, { - "link": "component", "id": "b", "property": "value", }, { - "link": "component", "id": "c", "property": "value", }, { - "link": "component", "id": "d", "property": "value", }, @@ -111,19 +107,16 @@ def test_to_dict_with_two_outputs(self): }, "inputs": [ { - "link": "component", "id": "n", "property": "value", } ], "outputs": [ { - "link": "component", "id": "select", "property": "options", }, { - "link": "component", "id": "select", "property": "value", }, diff --git a/chartlets.py/tests/channel_test.py b/chartlets.py/tests/channel_test.py index 3525b1f5..4748302e 100644 --- a/chartlets.py/tests/channel_test.py +++ b/chartlets.py/tests/channel_test.py @@ -9,22 +9,22 @@ def make_base(): class ChannelTest(unittest.TestCase): channel_cls: Type[Channel] - link_name: str def test_no_args_given(self): - with pytest.raises(ValueError, match="value for 'id' must be given"): + with pytest.raises( + TypeError, match="missing 1 required positional argument: 'id'" + ): + # noinspection PyArgumentList obj = self.channel_cls() 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_ok(self): - obj = self.channel_cls(property="datasetId", **{self.link_name: "app"}) - self.assertEqual("app", obj.link) - self.assertEqual(None, obj.id) + obj = self.channel_cls("@app", "datasetId") + self.assertEqual("@app", obj.id) self.assertEqual("datasetId", obj.property) def test_container_with_id(self): @@ -32,38 +32,29 @@ def test_container_with_id(self): ValueError, match="value for 'property' must be given", ): - self.channel_cls("test_id", **{self.link_name: "container"}) + self.channel_cls("@container") def test_app_no_prop(self): with pytest.raises( ValueError, match="value for 'property' must be given", ): - self.channel_cls(**{self.link_name: "app"}) + self.channel_cls("@app") def test_wrong_link(self): with pytest.raises( ValueError, match=( - f"value of {self.link_name!r} must be one of" - r" \('container', 'app'\), but was 'host'" + r"value of 'id' must be one of \('@app', '@container'\), but was '@horst'" ), ): - self.channel_cls(**{self.link_name: "host"}) - - def test_no_trigger(self): - obj = self.channel_cls("some_id") - if isinstance(obj, State): - self.assertTrue(obj.no_trigger) - else: - self.assertFalse(obj.no_trigger) + self.channel_cls("@horst") 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, @@ -73,7 +64,6 @@ def test_to_dict(self): else: self.assertEqual( { - "link": "component", "id": "test_id", "property": "value", }, @@ -85,7 +75,6 @@ def test_to_dict(self): class InputTest(make_base(), unittest.TestCase): channel_cls = Input - link_name = "source" def test_disallow_empty_property(self): with pytest.raises(ValueError, match="value for 'property' must be given"): @@ -94,15 +83,12 @@ def test_disallow_empty_property(self): class StateTest(make_base(), unittest.TestCase): channel_cls = State - link_name = "source" class OutputTest(make_base(), unittest.TestCase): channel_cls = Output - link_name = "target" def test_allow_empty_property(self): obj = self.channel_cls("some_id", "") - self.assertEqual("component", obj.link) self.assertEqual("some_id", obj.id) self.assertEqual("", obj.property) From 3b8769d7dd269f2b874a78bd6496eca29506d163 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Sat, 23 Nov 2024 17:11:32 +0100 Subject: [PATCH 2/2] Fixed tests --- .../actions/helpers/getInputValues.test.ts | 79 +++++-------------- .../src/lib/actions/helpers/getInputValues.ts | 40 +++++----- 2 files changed, 40 insertions(+), 79 deletions(-) diff --git a/chartlets.js/src/lib/actions/helpers/getInputValues.test.ts b/chartlets.js/src/lib/actions/helpers/getInputValues.test.ts index 2f9f7bda..fb95191a 100644 --- a/chartlets.js/src/lib/actions/helpers/getInputValues.test.ts +++ b/chartlets.js/src/lib/actions/helpers/getInputValues.test.ts @@ -24,85 +24,48 @@ const componentState = { describe("Test that getInputValueFromComponent()", () => { it("works on 1st level", () => { - expect( - getInputValueFromComponent( - { - id: "b1", - property: "value", - }, - componentState, - ), - ).toEqual(14); + expect(getInputValueFromComponent(componentState, "b1", "value")).toEqual( + 14, + ); }); it("works on 2nd level", () => { - expect( - getInputValueFromComponent( - { - id: "p1", - property: "chart", - }, - componentState, - ), - ).toEqual(null); + expect(getInputValueFromComponent(componentState, "p1", "chart")).toEqual( + null, + ); }); it("works on 3rd level", () => { - expect( - getInputValueFromComponent( - { - id: "cb1", - property: "value", - }, - componentState, - ), - ).toEqual(true); - - expect( - getInputValueFromComponent( - { - id: "dd1", - property: "value", - }, - componentState, - ), - ).toEqual(13); - }); -}); - -describe("Test that getInputValueFromState()", () => { - it("works with input.id and input.property", () => { - const state = { x: { y: 26 } }; - expect(getInputValueFromState({ id: "x", property: "y" }, state)).toEqual( - 26, + expect(getInputValueFromComponent(componentState, "cb1", "value")).toEqual( + true, ); - }); - it("works with arrays indexes", () => { - const state = { x: [4, 5, 6] }; - expect(getInputValueFromState({ id: "x", property: "1" }, state)).toEqual( - 5, + expect(getInputValueFromComponent(componentState, "dd1", "value")).toEqual( + 13, ); }); +}); +describe("Test that getInputValueFromState()", () => { it("works without input.id", () => { const state = { x: [4, 5, 6] }; - expect( - getInputValueFromState({ id: "@container", property: "x" }, state), - ).toEqual([4, 5, 6]); + expect(getInputValueFromState(state, "x")).toEqual([4, 5, 6]); }); it("works on 2nd level", () => { const state = { x: { y: 15 } }; - expect( - getInputValueFromState({ id: "@container", property: "x.y" }, state), - ).toEqual(15); + expect(getInputValueFromState(state, "x.y")).toEqual(15); }); it("works on 3nd level", () => { const state = { x: { y: [4, 5, 6] } }; + expect(getInputValueFromState(state, "x.y.2")).toEqual(6); + }); + + it("works with non-object states", () => { + const state = 13; expect( - getInputValueFromState({ id: "@container", property: "x.y.2" }, state), - ).toEqual(6); + getInputValueFromState(state as unknown as object, "x.y.2"), + ).toBeUndefined(); }); }); diff --git a/chartlets.js/src/lib/actions/helpers/getInputValues.ts b/chartlets.js/src/lib/actions/helpers/getInputValues.ts index 3980c665..a66e1f82 100644 --- a/chartlets.js/src/lib/actions/helpers/getInputValues.ts +++ b/chartlets.js/src/lib/actions/helpers/getInputValues.ts @@ -10,7 +10,7 @@ import { isComponentState, isContainerState, } from "@/lib/types/state/component"; -import { formatObjPath, getValue, normalizeObjPath } from "@/lib/utils/objPath"; +import { formatObjPath, getValue, type ObjPathLike } from "@/lib/utils/objPath"; import { isObject } from "@/lib/utils/isObject"; import type { HostStore } from "@/lib/types/state/options"; @@ -32,12 +32,17 @@ export function getInputValue( hostStore?: HostStore, ): unknown { let inputValue: unknown = undefined; + const { id, property } = input; if (isComponentChannel(input) && contributionState.component) { - inputValue = getInputValueFromComponent(input, contributionState.component); + inputValue = getInputValueFromComponent( + contributionState.component, + id, + property, + ); } else if (isContainerChannel(input) && contributionState.container) { - inputValue = getInputValueFromState(input, contributionState.container); + inputValue = getInputValueFromState(contributionState.container, property); } else if (isHostChannel(input) && hostStore) { - inputValue = getInputValueFromHostStore(input, hostStore); + inputValue = getInputValueFromHostStore(hostStore, property); } else { console.warn(`input with unknown data source:`, input); } @@ -51,16 +56,17 @@ export function getInputValue( // we export for testing only export function getInputValueFromComponent( - input: Input, componentState: ComponentState, + id: string, + property: ObjPathLike, ): unknown { - if (componentState.id === input.id) { - return getValue(componentState, input.property); + if (componentState.id === id) { + return getValue(componentState, property); } else if (isContainerState(componentState)) { for (let i = 0; i < componentState.children.length; i++) { const item = componentState.children[i]; if (isComponentState(item)) { - const itemValue = getInputValueFromComponent(input, item); + const itemValue = getInputValueFromComponent(item, id, property); if (itemValue !== noValue) { return itemValue; } @@ -72,24 +78,16 @@ export function getInputValueFromComponent( // we export for testing only export function getInputValueFromState( - input: Input, state: object | undefined, + property: ObjPathLike, ): unknown { - let inputValue: unknown = state; - if (input.id && isObject(inputValue)) { - inputValue = inputValue[input.id]; - } - if (isObject(inputValue)) { - const state = inputValue; - const property = normalizeObjPath(input.property); - inputValue = getValue(state, property); - } - return inputValue; + return isObject(state) ? getValue(state, property) : undefined; } +// we export for testing only export function getInputValueFromHostStore( - input: Input, hostStore: HostStore, + property: ObjPathLike, ): unknown { - return hostStore.get(formatObjPath(input.property)); + return hostStore.get(formatObjPath(property)); }