Skip to content

Commit db218d1

Browse files
authored
Merge pull request #6 from bcdev/forman-add_callback_function_details
Include callback function details
2 parents fcfd313 + d34289b commit db218d1

File tree

3 files changed

+136
-21
lines changed

3 files changed

+136
-21
lines changed

dashi/src/model/callback.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
export interface Callback {
2-
function: string;
2+
function: CbFunction;
33
inputs?: Input[];
44
outputs?: Output[];
55
}
66

7+
export interface CbFunction {
8+
name: string;
9+
parameters: CbParameter[];
10+
returnType: string | string[];
11+
}
12+
13+
export interface CbParameter {
14+
name: string;
15+
type?: string | string[];
16+
default?: unknown;
17+
}
18+
719
export type InputOutputKind = "AppState" | "State" | "Component";
820

921
export interface InputOutput {

dashipy/dashipy/lib/callback.py

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import inspect
2+
import types
23
from abc import ABC
34
from typing import Callable, Any, Literal
45

6+
from .component import Component
7+
58
ComponentKind = Literal["Component"]
69
AppStateKind = Literal["AppState"]
710
StateKind = Literal["State"]
@@ -130,11 +133,19 @@ def invoke(self, context: Any, input_values: list | tuple):
130133
return self.function(*args, **kwargs)
131134

132135
def to_dict(self) -> dict[str, Any]:
133-
d = dict(function=self.function.__qualname__)
136+
# skip ctx parameter:
137+
parameters = list(self.signature.parameters.values())[1:]
138+
d = {
139+
"function": {
140+
"name": self.function.__qualname__,
141+
"parameters": [_parameter_to_dict(p) for p in parameters],
142+
"returnType": _annotation_to_str(self.signature.return_annotation),
143+
}
144+
}
134145
if self.inputs:
135-
d.update(inputs=[inp.to_dict() for inp in self.inputs])
146+
d.update({"inputs": [inp.to_dict() for inp in self.inputs]})
136147
if self.outputs:
137-
d.update(outputs=[out.to_dict() for out in self.outputs])
148+
d.update({"outputs": [out.to_dict() for out in self.outputs]})
138149
return d
139150

140151
def make_function_args(
@@ -163,3 +174,65 @@ def make_function_args(
163174
kwargs[param_name] = param_value
164175

165176
return tuple(args), kwargs
177+
178+
179+
def _parameter_to_dict(parameter: inspect.Parameter) -> dict[str, Any]:
180+
empty = inspect.Parameter.empty
181+
d = {"name": parameter.name}
182+
if parameter.annotation is not empty:
183+
d |= {"type": _annotation_to_str(parameter.annotation)}
184+
if parameter.default is not empty:
185+
d |= {"default": parameter.default}
186+
return d
187+
188+
189+
_scalar_types = {
190+
"None": "null",
191+
"NoneType": "null",
192+
"bool": "boolean",
193+
"int": "integer",
194+
"float": "float",
195+
"str": "string",
196+
"Component": "Component",
197+
}
198+
199+
_array_types = {
200+
"list[bool]": "boolean[]",
201+
"list[int]": "integer[]",
202+
"list[float]": "float[]",
203+
"list[str]": "string[]",
204+
"list[Component]": "Component[]",
205+
}
206+
207+
_object_types = {
208+
"Figure": "Figure",
209+
"Component": "Component",
210+
}
211+
212+
213+
def _annotation_to_str(annotation: Any) -> str | list[str]:
214+
if isinstance(annotation, types.UnionType):
215+
type_name = str(annotation)
216+
try:
217+
return [_scalar_types[t] for t in type_name.split(" | ")]
218+
except KeyError:
219+
pass
220+
elif isinstance(annotation, types.GenericAlias):
221+
type_name = str(annotation)
222+
try:
223+
return _array_types[type_name]
224+
except KeyError:
225+
pass
226+
else:
227+
type_name = (
228+
annotation.__name__ if hasattr(annotation, "__name__") else str(annotation)
229+
)
230+
try:
231+
return _scalar_types[type_name]
232+
except KeyError:
233+
pass
234+
try:
235+
return _object_types[type_name]
236+
except KeyError:
237+
pass
238+
raise TypeError(f"unsupported type: {type_name}")

dashipy/tests/lib/callback_test.py

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,45 +5,73 @@
55
from dashipy.lib.callback import Input, Callback
66

77

8-
def my_callback(ctx, a: int, /, b: str = "", c: bool = False) -> str:
9-
return f"{a}-{b}-{c}"
8+
def my_callback(
9+
ctx,
10+
a: int,
11+
/,
12+
b: str | int = "",
13+
c: bool | None = False,
14+
d: list[str] = (),
15+
) -> str:
16+
return f"{a}-{b}-{c}-{d}"
1017

1118

12-
class CallTest(unittest.TestCase):
19+
class CallbackTest(unittest.TestCase):
1320
def test_make_function_args(self):
1421
callback = Callback(my_callback, [Input("a"), Input("b"), Input("c")], [])
1522
ctx = object()
1623
args, kwargs = callback.make_function_args(ctx, [13, "Wow", True])
1724
self.assertEqual((ctx, 13), args)
1825
self.assertEqual({"b": "Wow", "c": True}, kwargs)
1926

27+
def test_to_dict(self):
28+
callback = Callback(
29+
my_callback, [Input("a"), Input("b"), Input("c"), Input("d")], []
30+
)
31+
d = callback.to_dict()
32+
# print(json.dumps(d, indent=2))
33+
self.assertEqual(
34+
{
35+
"function": {
36+
"name": "my_callback",
37+
"parameters": [
38+
{"name": "a", "type": "integer"},
39+
{"name": "b", "type": ["string", "integer"], "default": ""},
40+
{"name": "c", "type": ["boolean", "null"], "default": False},
41+
{"name": "d", "type": "string[]", "default": ()},
42+
],
43+
"returnType": "string",
44+
},
45+
"inputs": [
46+
{"id": "a", "property": "value", "kind": "Component"},
47+
{"id": "b", "property": "value", "kind": "Component"},
48+
{"id": "c", "property": "value", "kind": "Component"},
49+
{"id": "d", "property": "value", "kind": "Component"},
50+
],
51+
},
52+
d,
53+
)
54+
2055

2156
# noinspection PyMethodMayBeStatic
2257
class FromDecoratorTest(unittest.TestCase):
2358

24-
def test_inputs_given_but_not_in_order(self):
25-
callback = Callback.from_decorator(
26-
"test", (Input("b"), Input("c"), Input("a")), my_callback
27-
)
28-
self.assertIsInstance(callback, Callback)
29-
self.assertIs(my_callback, callback.function)
30-
self.assertEqual(3, len(callback.inputs))
31-
self.assertEqual(0, len(callback.outputs))
32-
3359
def test_too_few_inputs(self):
3460
with pytest.raises(
3561
TypeError,
36-
match="too few inputs in decorator 'test' for function 'my_callback': expected 3, but got 0",
62+
match="too few inputs in decorator 'test' for function"
63+
" 'my_callback': expected 4, but got 0",
3764
):
3865
Callback.from_decorator("test", (), my_callback)
3966

4067
def test_too_many_inputs(self):
4168
with pytest.raises(
4269
TypeError,
43-
match="too many inputs in decorator 'test' for function 'my_callback': expected 3, but got 4",
70+
match="too many inputs in decorator 'test' for function"
71+
" 'my_callback': expected 4, but got 5",
4472
):
4573
Callback.from_decorator(
46-
"test", tuple(Input(c) for c in "abcd"), my_callback
74+
"test", tuple(Input(c) for c in "abcde"), my_callback
4775
)
4876

4977
def test_decorator_target(self):
@@ -57,12 +85,14 @@ def test_decorator_target(self):
5785
def test_decorator_args(self):
5886
with pytest.raises(
5987
TypeError,
60-
match="arguments for decorator 'test' must be of type Input, but got 'int'",
88+
match="arguments for decorator 'test' must be of"
89+
" type Input, but got 'int'",
6190
):
6291
Callback.from_decorator("test", (13,), my_callback)
6392

6493
with pytest.raises(
6594
TypeError,
66-
match="arguments for decorator 'test' must be of type Input or Output, but got 'int'",
95+
match="arguments for decorator 'test' must be of"
96+
" type Input or Output, but got 'int'",
6797
):
6898
Callback.from_decorator("test", (13,), my_callback, outputs_allowed=True)

0 commit comments

Comments
 (0)