-
-
Notifications
You must be signed in to change notification settings - Fork 477
/
widget.py
204 lines (181 loc) · 7.46 KB
/
widget.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
from __future__ import annotations
from collections.abc import Iterable, Mapping
from inspect import Parameter
from numbers import Integral, Number, Real
from typing import Any, Optional, Tuple
empty = Parameter.empty
import param
from .base import Widget
from .input import Checkbox, TextInput
from .select import Select
from .slider import DiscreteSlider, FloatSlider, IntSlider
class fixed(param.Parameterized):
"""
A pseudo-widget whose value is fixed and never synced to the client.
"""
description = param.String(default='')
value = param.Parameter(doc="Any Python object")
def __init__(self, value: Any, **kwargs: Any):
super().__init__(value=value, **kwargs)
def get_interact_value(self):
"""
Return the value for this widget which should be passed to
interactive functions. Custom widgets can change this method
to process the raw value ``self.value``.
"""
return self.value
def _get_min_max_value(
min: Number, max: Number, value: Optional[Number] = None, step: Optional[Number] = None
) -> Tuple[Number, Number, Number]:
"""Return min, max, value given input values with possible None."""
# Either min and max need to be given, or value needs to be given
if value is None:
if min is None or max is None:
raise ValueError('unable to infer range, value from: ({0}, {1}, {2})'.format(min, max, value))
diff = max - min
value = min + (diff / 2)
# Ensure that value has the same type as diff
if not isinstance(value, type(diff)):
value = min + (diff // 2)
else: # value is not None
if not isinstance(value, Real):
raise TypeError('expected a real number, got: %r' % value)
# Infer min/max from value
if value == 0:
# This gives (0, 1) of the correct type
vrange = (value, value + 1)
elif value > 0:
vrange = (-value, 3*value)
else:
vrange = (3*value, -value)
if min is None:
min = vrange[0]
if max is None:
max = vrange[1]
if step is not None:
# ensure value is on a step
tick = int((value - min) / step)
value = min + tick * step
if not min <= value <= max:
raise ValueError('value must be between min and max (min={0}, value={1}, max={2})'.format(min, value, max))
return min, max, value
def _matches(o: str, pattern: str) -> bool:
"""Match a pattern of types in a sequence."""
if not len(o) == len(pattern):
return False
comps = zip(o,pattern)
return all(isinstance(obj,kind) for obj,kind in comps)
class widget(param.ParameterizedFunction):
"""
Attempts to find a widget appropriate for a given value.
Arguments
---------
name: str
The name of the resulting widget.
value: Any
The value to deduce a widget from.
default: Any
The default value for the resulting widget.
**params: Any
Additional keyword arguments to pass to the widget.
Returns
-------
Widget
"""
def __call__(self, value: Any, name: str, default=empty, **params):
"""Build a ValueWidget instance given an abbreviation or Widget."""
if isinstance(value, Widget):
widget = value
elif isinstance(value, tuple):
widget = self.widget_from_tuple(value, name, default)
if default is not empty:
try:
widget.value = default
except Exception:
# ignore failure to set default
pass
else:
# Try single value
widget = self.widget_from_single_value(value, name)
# Something iterable (list, dict, generator, ...). Note that str and
# tuple should be handled before, that is why we check this case last.
if widget is None and isinstance(value, Iterable):
widget = self.widget_from_iterable(value, name)
if default is not empty:
try:
widget.value = default
except Exception:
# ignore failure to set default
pass
if widget is None:
widget = fixed(value)
if params:
widget.param.update(**params)
return widget
@staticmethod
def widget_from_single_value(o, name):
"""Make widgets from single values, which can be used as parameter defaults."""
if isinstance(o, str):
return TextInput(value=str(o), name=name)
elif isinstance(o, bool):
return Checkbox(value=o, name=name)
elif isinstance(o, Integral):
min, max, value = _get_min_max_value(None, None, o)
return IntSlider(value=o, start=min, end=max, name=name)
elif isinstance(o, Real):
min, max, value = _get_min_max_value(None, None, o)
return FloatSlider(value=o, start=min, end=max, name=name)
else:
return None
@staticmethod
def widget_from_tuple(o, name, default=empty):
"""Make widgets from a tuple abbreviation."""
int_default = (default is empty or isinstance(default, int))
if _matches(o, (Real, Real)):
min, max, value = _get_min_max_value(o[0], o[1])
if all(isinstance(_, Integral) for _ in o) and int_default:
cls = IntSlider
else:
cls = FloatSlider
return cls(value=value, start=min, end=max, name=name)
elif _matches(o, (Real, Real, Real)):
step = o[2]
if step <= 0:
raise ValueError("step must be >= 0, not %r" % step)
min, max, value = _get_min_max_value(o[0], o[1], step=step)
if all(isinstance(_, Integral) for _ in o) and int_default:
cls = IntSlider
else:
cls = FloatSlider
return cls(value=value, start=min, end=max, step=step, name=name)
elif _matches(o, (Real, Real, Real, Real)):
step = o[2]
if step <= 0:
raise ValueError("step must be >= 0, not %r" % step)
min, max, value = _get_min_max_value(o[0], o[1], value=o[3], step=step)
if all(isinstance(_, Integral) for _ in o):
cls = IntSlider
else:
cls = FloatSlider
return cls(value=value, start=min, end=max, step=step, name=name)
elif len(o) == 4:
min, max, value = _get_min_max_value(o[0], o[1], value=o[3])
if all(isinstance(_, Integral) for _ in [o[0], o[1], o[3]]):
cls = IntSlider
else:
cls = FloatSlider
return cls(value=value, start=min, end=max, name=name)
@staticmethod
def widget_from_iterable(o, name):
"""Make widgets from an iterable. This should not be done for
a string or tuple."""
# Select expects a dict or list, so we convert an arbitrary
# iterable to either of those.
values = list(o.values()) if isinstance(o, Mapping) else list(o)
widget_type = DiscreteSlider if all(param._is_number(v) for v in values) else Select
if isinstance(o, (list, dict)):
return widget_type(options=o, name=name)
elif isinstance(o, Mapping):
return widget_type(options=list(o.items()), name=name)
else:
return widget_type(options=list(o), name=name)