Skip to content

Commit f1c9b34

Browse files
committed
Back to just Input, State, Output
1 parent a28ac03 commit f1c9b34

File tree

11 files changed

+222
-77
lines changed

11 files changed

+222
-77
lines changed

dashipy/dashipy/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
from .callback import Callback
2-
from .inputoutput import AppInput
3-
from .inputoutput import AppOutput
42
from .inputoutput import Input
53
from .inputoutput import Output
64
from .inputoutput import State

dashipy/dashipy/callback.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
from typing import Any, Callable
44

55
from dashipy.inputoutput import (
6-
AppInput,
7-
AppOutput,
86
Input,
97
Output,
108
State,
@@ -44,12 +42,12 @@ def from_decorator(
4442
f" context parameter"
4543
)
4644

47-
inputs: list[Input | State | AppInput] = []
48-
outputs: list[Output | AppOutput] = []
45+
inputs: list[Input | State] = []
46+
outputs: list[Output] = []
4947
for arg in decorator_args:
50-
if isinstance(arg, (Input, State, AppInput)):
48+
if isinstance(arg, (Input, State)):
5149
inputs.append(arg)
52-
elif outputs_allowed and isinstance(arg, (Output, AppOutput)):
50+
elif outputs_allowed and isinstance(arg, Output):
5351
outputs.append(arg)
5452
elif outputs_allowed:
5553
raise TypeError(
@@ -81,8 +79,8 @@ def from_decorator(
8179
def __init__(
8280
self,
8381
function: Callable,
84-
inputs: list[Input | State | AppInput],
85-
outputs: list[Output | AppOutput],
82+
inputs: list[Input | State],
83+
outputs: list[Output],
8684
signature: inspect.Signature | None = None,
8785
):
8886
"""Private constructor.

dashipy/dashipy/inputoutput.py

Lines changed: 74 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,44 @@
11
from abc import ABC
2-
from typing import Any
2+
from typing import Any, Literal
3+
4+
from .util.assertions import assert_is_one_of
5+
from .util.assertions import assert_is_instance_of
6+
7+
8+
Source = Literal["component"] | Literal["container"] | Literal["app"]
9+
Target = Literal["component"] | Literal["container"] | Literal["app"]
10+
NoneType = type(None)
11+
12+
13+
# noinspection PyShadowingBuiltins
14+
def _validate_input(
15+
source: str | None, id: str | None, property: str | None
16+
) -> tuple[str, str | None, str | None]:
17+
return _validate_kind("source", source, id, property)
18+
19+
20+
# noinspection PyShadowingBuiltins
21+
def _validate_output(
22+
target: str | None, id: str | None, property: str | None
23+
) -> tuple[str, str | None, str | None]:
24+
return _validate_kind("target", target, id, property)
25+
26+
27+
# noinspection PyShadowingBuiltins
28+
def _validate_kind(
29+
kind_name: str, kind: str | None, id: str | None, property: str | None
30+
) -> tuple[str, str | None, str | None]:
31+
assert_is_one_of(kind_name, kind, ("component", "container", "app", None))
32+
if not kind or kind == "component":
33+
assert_is_instance_of("id", id, (str, NoneType))
34+
assert_is_instance_of("property", id, (str, NoneType))
35+
kind = kind or "component"
36+
if property is None and id is not None:
37+
property = "value"
38+
else:
39+
assert_is_instance_of("id", id, NoneType)
40+
assert_is_instance_of("property", property, str)
41+
return kind, id, property
342

443

544
class InputOutput(ABC):
@@ -13,12 +52,14 @@ def __init__(
1352
self.property = property
1453

1554
def to_dict(self) -> dict[str, Any]:
16-
d = {"type": self.__class__.__name__}
17-
d.update({
18-
k: v
19-
for k, v in self.__dict__.items()
20-
if not k.startswith("_") and v is not None
21-
})
55+
d = {"class": self.__class__.__name__}
56+
d.update(
57+
{
58+
k: v
59+
for k, v in self.__dict__.items()
60+
if not k.startswith("_") and v is not None
61+
}
62+
)
2263
return d
2364

2465

@@ -28,41 +69,42 @@ class Input(InputOutput):
2869
"""
2970

3071
# noinspection PyShadowingBuiltins
31-
def __init__(self, id: str, property: str = "value"):
32-
super().__init__(id, property)
72+
def __init__(
73+
self,
74+
id: str | None = None,
75+
property: str | None = None,
76+
source: Source | None = None,
77+
):
78+
source, id, property = _validate_input(source, id, property)
79+
super().__init__(id=id, property=property)
80+
self.source = source
3381

3482

35-
class State(InputOutput):
83+
class State(Input):
3684
"""An input value read from component state.
3785
Does not trigger callback invocation.
3886
"""
3987

4088
# noinspection PyShadowingBuiltins
41-
def __init__(self, id: str, property: str = "value"):
42-
super().__init__(id, property)
89+
def __init__(
90+
self,
91+
id: str | None = None,
92+
property: str | None = None,
93+
source: Source | None = None,
94+
):
95+
super().__init__(id=id, property=property, source=source)
4396

4497

4598
class Output(InputOutput):
4699
"""Callback output."""
47100

48101
# noinspection PyShadowingBuiltins
49-
def __init__(self, id: str, property: str = "value"):
50-
super().__init__(id, property)
51-
52-
53-
class AppInput(InputOutput):
54-
"""An input value read from application state.
55-
An application state change may trigger callback invocation.
56-
"""
57-
58-
# noinspection PyShadowingBuiltins
59-
def __init__(self, property: str):
60-
super().__init__(property=property)
61-
62-
63-
class AppOutput(InputOutput):
64-
"""An output written to application state."""
65-
66-
# noinspection PyShadowingBuiltins
67-
def __init__(self, property: str):
68-
super().__init__(property=property)
102+
def __init__(
103+
self,
104+
id: str | None = None,
105+
property: str | None = None,
106+
target: Target | None = None,
107+
):
108+
target, id, property = _validate_output(target, id, property)
109+
super().__init__(id=id, property=property)
110+
self.target = target

dashipy/dashipy/util/__init__.py

Whitespace-only changes.

dashipy/dashipy/util/assertions.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from typing import Any, Container, Type
2+
3+
4+
def assert_is_one_of(name: str, value: Any, value_set: Container):
5+
if value not in value_set:
6+
raise ValueError(
7+
f"value of {name!r} must be one of {value_set!r}, but was {value!r}"
8+
)
9+
10+
11+
def assert_is_instance_of(name: str, value: Any, type_set: Type | tuple[Type, ...]):
12+
if not isinstance(value, type_set):
13+
raise ValueError(
14+
f"value of {name!r} must be an instance of {type_set!r}, but was {value!r}"
15+
)

dashipy/my_extension/my_panel_2.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import altair as alt
22
import pandas as pd
33

4-
from dashipy import Component, AppInput, Input, Output
4+
from dashipy import Component, Input, Output
55
from dashipy.components import Plot, Box, Dropdown
66
from dashipy.demo.contribs import Panel
77
from dashipy.demo.context import Context
@@ -10,7 +10,7 @@
1010
panel = Panel(__name__, title="Panel B")
1111

1212

13-
@panel.layout(AppInput("selectedDatasetId"))
13+
@panel.layout(Input(property="selectedDatasetId", source="app"))
1414
def render_panel(
1515
ctx: Context,
1616
selected_dataset_id: str = "",
@@ -51,7 +51,7 @@ def render_panel(
5151

5252

5353
@panel.callback(
54-
AppInput("selectedDatasetId"),
54+
Input(property="selectedDatasetId", source="app"),
5555
Input("selected_variable_name"),
5656
Output("plot", "chart"),
5757
)
@@ -89,7 +89,7 @@ def make_figure(
8989

9090

9191
@panel.callback(
92-
AppInput("selectedDatasetId"),
92+
Input(property="selectedDatasetId", source="app"),
9393
Output("selected_variable_name", "options"),
9494
Output("selected_variable_name", "value"),
9595
)

dashipy/my_extension/my_panel_3.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from dashipy import Component, AppInput, Input, Output
1+
from dashipy import Component, Input, Output
22
from dashipy.components import Box, Dropdown, Checkbox, Typography
33
from dashipy.demo.contribs import Panel
44
from dashipy.demo.context import Context
@@ -11,7 +11,7 @@
1111

1212

1313
@panel.layout(
14-
AppInput("selectedDatasetId"),
14+
Input(property="selectedDatasetId", source="app"),
1515
)
1616
def render_panel(
1717
ctx: Context,
@@ -53,7 +53,7 @@ def render_panel(
5353

5454
# noinspection PyUnusedLocal
5555
@panel.callback(
56-
AppInput(property="selectedDatasetId"),
56+
Input("selectedDatasetId", source="app"),
5757
Input("opaque"),
5858
Input("color"),
5959
Output("info_text", "text"),

dashipy/tests/components/box_test.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import json
22
import unittest
33

4-
import plotly.graph_objects as go
5-
from plotly.graph_objs import Layout
6-
74
from dashipy.components import Box
85

96

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,7 @@
1-
import json
21
import unittest
32

4-
import plotly.graph_objects as go
5-
from plotly.graph_objs import Layout
6-
7-
from dashipy.components import Plot
8-
93

104
class PlotTest(unittest.TestCase):
115

126
def test_is_json_serializable(self):
13-
figure = go.Figure(layout=Layout(title="Bar Chart", autosize=True))
14-
figure.add_trace(go.Bar(x=["A", "B", "C"], y=[0.2, 0.3, 0.4]))
15-
plot = Plot(figure=figure)
16-
17-
d = plot.to_dict()
18-
self.assertIsInstance(d, dict)
19-
self.assertIsInstance(d.get("figure"), dict)
20-
json_text = json.dumps(d)
21-
self.assertEqual("{", json_text[0])
22-
self.assertEqual("}", json_text[-1])
7+
pass

dashipy/tests/inputoutput_test.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import unittest
2+
from typing import Type
3+
4+
import pytest
5+
6+
from dashipy.inputoutput import Input, State, Output
7+
8+
9+
def make_base():
10+
class Base(unittest.TestCase):
11+
kind_attr: str
12+
cls: Type[Input] | Type[State] | Type[Output]
13+
14+
def kind(self, obj):
15+
return getattr(obj, self.kind_attr)
16+
17+
def test_no_args(self):
18+
obj = self.cls()
19+
self.assertEqual("component", self.kind(obj))
20+
self.assertEqual(None, obj.id)
21+
self.assertEqual(None, obj.property)
22+
23+
def test_id_given(self):
24+
obj = self.cls("dataset_select")
25+
self.assertEqual("component", self.kind(obj))
26+
self.assertEqual("dataset_select", obj.id)
27+
self.assertEqual("value", obj.property)
28+
29+
def test_app(self):
30+
obj = self.cls(property="datasetId", **{self.kind_attr: "app"})
31+
self.assertEqual("app", self.kind(obj))
32+
self.assertEqual(None, obj.id)
33+
self.assertEqual("datasetId", obj.property)
34+
35+
def test_app_no_prop(self):
36+
with pytest.raises(
37+
ValueError,
38+
match=(
39+
"value of 'property' must be an instance"
40+
" of <class 'str'>, but was None"
41+
),
42+
):
43+
self.cls(**{self.kind_attr: "app"})
44+
45+
def test_wrong_kind(self):
46+
with pytest.raises(
47+
ValueError,
48+
match=(
49+
f"value of '{self.kind_attr}' must be one of"
50+
f" \\('component', 'container', 'app', None\\),"
51+
f" but was 'host'"
52+
),
53+
):
54+
self.cls(**{self.kind_attr: "host"})
55+
56+
return Base
57+
58+
59+
class InputTest(make_base(), unittest.TestCase):
60+
cls = Input
61+
kind_attr = "source"
62+
63+
64+
class StateTest(make_base(), unittest.TestCase):
65+
cls = State
66+
kind_attr = "source"
67+
68+
69+
class OutputTest(make_base(), unittest.TestCase):
70+
cls = Output
71+
kind_attr = "target"

0 commit comments

Comments
 (0)