Skip to content

Commit aefb5c2

Browse files
committed
allow component outputs to have empty, none-None properties
1 parent e26c4b2 commit aefb5c2

File tree

3 files changed

+49
-23
lines changed

3 files changed

+49
-23
lines changed

chartlets.py/chartlets/channel.py

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
from abc import ABC
22
from typing import Any, Literal
33

4-
from .util.assertions import assert_is_instance_of
5-
from .util.assertions import assert_is_none
6-
from .util.assertions import assert_is_one_of
4+
from .util.assertions import (
5+
assert_is_given,
6+
assert_is_instance_of,
7+
assert_is_none,
8+
assert_is_one_of,
9+
)
710

811

912
Link = Literal["component"] | Literal["container"] | Literal["app"]
1013

14+
COMPONENT = ""
15+
"""Special property value that can be used
16+
to refer to the entire component.
17+
"""
18+
1119

1220
class Channel(ABC):
1321
"""Base class for `Input`, `State`, and `Output`.
@@ -53,7 +61,8 @@ class Input(Channel):
5361
Args:
5462
id:
5563
Value of a component's "id" property.
56-
Used only if `source` is `"component"`.
64+
Required, if `source` is `"component"` (the default).
65+
Otherwise, it must not be passed.
5766
property:
5867
Name of the property of a component or state.
5968
To address properties in nested objects or arrays
@@ -82,7 +91,7 @@ class State(Input):
8291
8392
Just like an `Input`, a `State` describes from which property in which state
8493
a parameter value is read, but according state changes
85-
will **not*Ü* trigger callback invocation.
94+
will **not* trigger callback invocation.
8695
8796
Args:
8897
id:
@@ -125,6 +134,8 @@ class Output(Channel):
125134
To address properties in nested objects or arrays
126135
use a dot (`.`) to separate property names and array
127136
indexes.
137+
If `target` is `"component"` the empty string can be used
138+
to refer to entire components.
128139
target: One of `"component"` (the default), `"container"`,
129140
or `"app"`.
130141
"""
@@ -154,22 +165,37 @@ def _validate_input_params(
154165
def _validate_output_params(
155166
target: Link | None, id: str | None, property: str | None
156167
) -> tuple[Link, str | None, str | None]:
157-
return _validate_params("target", target, id, property)
168+
return _validate_params("target", target, id, property, output=True)
158169

159170

160171
# noinspection PyShadowingBuiltins
161172
def _validate_params(
162-
link_name: str, link: Link | None, id: str | None, property: str | None
173+
link_name: str,
174+
link: Link | None,
175+
id: str | None,
176+
property: str | None,
177+
output: bool = False,
163178
) -> tuple[Link, str | None, str | None]:
164-
assert_is_one_of(link_name, link, ("component", "container", "app", None))
165-
if not link or link == "component":
166-
assert_is_instance_of("id", id, (str, NoneType))
179+
if link is None or link == "component":
180+
# Component states require an id
181+
# and property which defaults to "value"
182+
link = "component"
183+
assert_is_given("id", id)
184+
assert_is_instance_of("id", id, str)
167185
assert_is_instance_of("property", id, (str, NoneType))
168-
link = link or "component"
169-
if property is None and id is not None:
186+
if property is None:
187+
# property, if not provided, defaults to "value"
170188
property = "value"
189+
elif not output:
190+
# outputs are allowed to have an empty property value
191+
assert_is_given("property", property)
171192
else:
172-
assert_is_none("id", id)
193+
# Other states require a link and property
194+
# and should have no id
195+
assert_is_given(link_name, link)
196+
assert_is_one_of(link_name, link, ("container", "app"))
197+
assert_is_given("property", property)
173198
assert_is_instance_of("property", property, str)
199+
assert_is_none("id", id)
174200
# noinspection PyTypeChecker
175201
return link, id, property

chartlets.py/chartlets/util/assertions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,8 @@ def assert_is_instance_of(name: str, value: Any, type_set: Type | tuple[Type, ..
1818
def assert_is_none(name: str, value: Any):
1919
if value is not None:
2020
raise TypeError(f"value of {name!r} must be None, but was {value!r}")
21+
22+
23+
def assert_is_given(name: str, value: Any):
24+
if not value:
25+
raise TypeError(f"value for {name!r} must be given")

chartlets.py/tests/channel_test.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,17 @@ class ChannelTest(unittest.TestCase):
1111
channel_cls: Type[Channel]
1212
link_name: str
1313

14-
def test_no_args(self):
15-
obj = self.channel_cls()
16-
self.assertEqual("component", obj.link)
17-
self.assertEqual(None, obj.id)
18-
self.assertEqual(None, obj.property)
14+
def test_no_args_given(self):
15+
with pytest.raises(ValueError, match="value for 'id' must be given and must not be empty"):
16+
obj = self.channel_cls()
1917

2018
def test_id_given(self):
2119
obj = self.channel_cls("dataset_select")
2220
self.assertEqual("component", obj.link)
2321
self.assertEqual("dataset_select", obj.id)
2422
self.assertEqual("value", obj.property)
2523

26-
def test_app(self):
24+
def test_app_ok(self):
2725
obj = self.channel_cls(property="datasetId", **{self.link_name: "app"})
2826
self.assertEqual("app", obj.link)
2927
self.assertEqual(None, obj.id)
@@ -39,10 +37,7 @@ def test_container_with_id(self):
3937
def test_app_no_prop(self):
4038
with pytest.raises(
4139
TypeError,
42-
match=(
43-
"value of 'property' must be an instance"
44-
" of <class 'str'>, but was None"
45-
),
40+
match="value for 'property' must be given",
4641
):
4742
self.channel_cls(**{self.link_name: "app"})
4843

0 commit comments

Comments
 (0)