pyglove 0.4.5.dev202411132359__py3-none-any.whl → 0.4.5.dev202501250807__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyglove/core/__init__.py +40 -21
- pyglove/core/coding/__init__.py +42 -0
- pyglove/core/coding/errors.py +111 -0
- pyglove/core/coding/errors_test.py +98 -0
- pyglove/core/coding/execution.py +312 -0
- pyglove/core/coding/execution_test.py +333 -0
- pyglove/core/{object_utils/codegen.py → coding/function_generation.py} +10 -4
- pyglove/core/{object_utils/codegen_test.py → coding/function_generation_test.py} +5 -7
- pyglove/core/coding/parsing.py +153 -0
- pyglove/core/coding/parsing_test.py +150 -0
- pyglove/core/coding/permissions.py +100 -0
- pyglove/core/coding/permissions_test.py +93 -0
- pyglove/core/geno/base.py +53 -38
- pyglove/core/geno/base_test.py +2 -4
- pyglove/core/geno/categorical.py +36 -27
- pyglove/core/geno/custom.py +18 -15
- pyglove/core/geno/numerical.py +19 -16
- pyglove/core/geno/space.py +3 -4
- pyglove/core/hyper/base.py +6 -6
- pyglove/core/hyper/categorical.py +91 -52
- pyglove/core/hyper/custom.py +7 -7
- pyglove/core/hyper/custom_test.py +9 -10
- pyglove/core/hyper/derived.py +30 -22
- pyglove/core/hyper/derived_test.py +3 -5
- pyglove/core/hyper/dynamic_evaluation.py +3 -4
- pyglove/core/hyper/evolvable.py +57 -46
- pyglove/core/hyper/numerical.py +48 -24
- pyglove/core/hyper/numerical_test.py +9 -9
- pyglove/core/hyper/object_template.py +58 -46
- pyglove/core/logging_test.py +0 -2
- pyglove/core/patching/object_factory.py +4 -4
- pyglove/core/patching/pattern_based.py +4 -4
- pyglove/core/patching/rule_based.py +4 -3
- pyglove/core/symbolic/__init__.py +4 -0
- pyglove/core/symbolic/base.py +200 -136
- pyglove/core/symbolic/base_test.py +17 -19
- pyglove/core/symbolic/boilerplate.py +4 -5
- pyglove/core/symbolic/class_wrapper.py +10 -14
- pyglove/core/symbolic/class_wrapper_test.py +2 -2
- pyglove/core/symbolic/compounding.py +2 -2
- pyglove/core/symbolic/compounding_test.py +2 -4
- pyglove/core/symbolic/contextual_object.py +288 -0
- pyglove/core/symbolic/contextual_object_test.py +327 -0
- pyglove/core/symbolic/dict.py +115 -87
- pyglove/core/symbolic/dict_test.py +188 -131
- pyglove/core/symbolic/diff.py +12 -12
- pyglove/core/symbolic/flags.py +1 -1
- pyglove/core/symbolic/functor.py +16 -15
- pyglove/core/symbolic/functor_test.py +2 -4
- pyglove/core/symbolic/inferred.py +2 -2
- pyglove/core/symbolic/list.py +70 -47
- pyglove/core/symbolic/list_test.py +117 -98
- pyglove/core/symbolic/object.py +59 -58
- pyglove/core/symbolic/object_test.py +143 -90
- pyglove/core/symbolic/origin.py +5 -7
- pyglove/core/symbolic/pure_symbolic.py +4 -3
- pyglove/core/symbolic/ref.py +33 -16
- pyglove/core/symbolic/ref_test.py +17 -0
- pyglove/core/tuning/local_backend.py +2 -2
- pyglove/core/tuning/protocols.py +3 -3
- pyglove/core/typing/annotation_conversion.py +8 -3
- pyglove/core/typing/annotation_conversion_test.py +8 -0
- pyglove/core/typing/callable_ext.py +11 -13
- pyglove/core/typing/callable_signature.py +22 -19
- pyglove/core/typing/callable_signature_test.py +3 -5
- pyglove/core/typing/class_schema.py +93 -54
- pyglove/core/typing/class_schema_test.py +4 -5
- pyglove/core/typing/custom_typing.py +5 -4
- pyglove/core/typing/key_specs.py +5 -7
- pyglove/core/typing/key_specs_test.py +4 -4
- pyglove/core/typing/type_conversion.py +4 -5
- pyglove/core/typing/type_conversion_test.py +12 -12
- pyglove/core/typing/typed_missing.py +6 -7
- pyglove/core/typing/typed_missing_test.py +7 -8
- pyglove/core/typing/value_specs.py +287 -144
- pyglove/core/typing/value_specs_test.py +148 -25
- pyglove/core/utils/__init__.py +172 -0
- pyglove/core/{object_utils → utils}/common_traits.py +2 -2
- pyglove/core/{object_utils → utils}/common_traits_test.py +1 -3
- pyglove/core/utils/contextual.py +147 -0
- pyglove/core/utils/contextual_test.py +88 -0
- pyglove/core/{object_utils → utils}/docstr_utils_test.py +1 -3
- pyglove/core/{object_utils → utils}/error_utils.py +3 -3
- pyglove/core/{object_utils → utils}/error_utils_test.py +1 -1
- pyglove/core/{object_utils → utils}/formatting.py +1 -1
- pyglove/core/{object_utils → utils}/formatting_test.py +1 -2
- pyglove/core/{object_utils → utils}/hierarchical.py +23 -25
- pyglove/core/{object_utils → utils}/hierarchical_test.py +3 -5
- pyglove/core/{object_utils → utils}/json_conversion.py +1 -1
- pyglove/core/{object_utils → utils}/json_conversion_test.py +1 -3
- pyglove/core/{object_utils → utils}/missing.py +2 -2
- pyglove/core/{object_utils → utils}/missing_test.py +2 -4
- pyglove/core/utils/text_color.py +128 -0
- pyglove/core/utils/text_color_test.py +94 -0
- pyglove/core/{object_utils → utils}/thread_local_test.py +1 -3
- pyglove/core/{object_utils → utils}/timing.py +21 -10
- pyglove/core/{object_utils → utils}/timing_test.py +14 -12
- pyglove/core/{object_utils → utils}/value_location.py +2 -2
- pyglove/core/{object_utils → utils}/value_location_test.py +2 -4
- pyglove/core/views/base.py +25 -29
- pyglove/core/views/html/base.py +15 -16
- pyglove/core/views/html/controls/base.py +46 -9
- pyglove/core/views/html/controls/label.py +13 -2
- pyglove/core/views/html/controls/label_test.py +27 -8
- pyglove/core/views/html/controls/progress_bar.py +3 -5
- pyglove/core/views/html/controls/progress_bar_test.py +2 -2
- pyglove/core/views/html/controls/tab.py +217 -66
- pyglove/core/views/html/controls/tab_test.py +46 -15
- pyglove/core/views/html/tree_view.py +39 -37
- {pyglove-0.4.5.dev202411132359.dist-info → pyglove-0.4.5.dev202501250807.dist-info}/METADATA +17 -3
- pyglove-0.4.5.dev202501250807.dist-info/RECORD +218 -0
- {pyglove-0.4.5.dev202411132359.dist-info → pyglove-0.4.5.dev202501250807.dist-info}/WHEEL +1 -1
- pyglove/core/object_utils/__init__.py +0 -164
- pyglove-0.4.5.dev202411132359.dist-info/RECORD +0 -203
- /pyglove/core/{object_utils → utils}/docstr_utils.py +0 -0
- /pyglove/core/{object_utils → utils}/thread_local.py +0 -0
- {pyglove-0.4.5.dev202411132359.dist-info → pyglove-0.4.5.dev202501250807.dist-info}/LICENSE +0 -0
- {pyglove-0.4.5.dev202411132359.dist-info → pyglove-0.4.5.dev202501250807.dist-info}/top_level.txt +0 -0
@@ -34,7 +34,7 @@ class LabelTest(TestCase):
|
|
34
34
|
label = label_lib.Label('foo')
|
35
35
|
self.assertIsNone(label.tooltip)
|
36
36
|
self.assertIsNone(label.link)
|
37
|
-
self.assert_html_content(label, '<
|
37
|
+
self.assert_html_content(label, '<span class="label">foo</span>')
|
38
38
|
with self.assertRaisesRegex(ValueError, 'Non-interactive .*'):
|
39
39
|
label.update('bar')
|
40
40
|
|
@@ -54,25 +54,32 @@ class LabelTest(TestCase):
|
|
54
54
|
self.assert_html_content(
|
55
55
|
label,
|
56
56
|
(
|
57
|
-
'<div class="label-container"><
|
57
|
+
'<div class="label-container"><span class="label">foo</span>'
|
58
58
|
'<span class="tooltip">bar</span></div>'
|
59
59
|
)
|
60
60
|
)
|
61
61
|
|
62
62
|
def test_update(self):
|
63
|
-
label = label_lib.Label(
|
63
|
+
label = label_lib.Label(
|
64
|
+
'foo', 'bar', 'http://google.com',
|
65
|
+
interactive=True,
|
66
|
+
css_classes=['foo', 'bar'],
|
67
|
+
)
|
64
68
|
self.assertIn('id="control-', label.to_html_str(content_only=True))
|
65
69
|
with label.track_scripts() as scripts:
|
66
70
|
label.update(
|
67
71
|
'bar',
|
68
72
|
tooltip='baz',
|
69
73
|
link='http://www.yahoo.com',
|
70
|
-
styles=dict(color='red')
|
74
|
+
styles=dict(color='red'),
|
75
|
+
add_class=['baz'],
|
76
|
+
remove_class=['bar'],
|
71
77
|
)
|
72
78
|
self.assertEqual(label.text, 'bar')
|
73
79
|
self.assertEqual(label.tooltip.content, 'baz')
|
74
80
|
self.assertEqual(label.link, 'http://www.yahoo.com')
|
75
81
|
self.assertEqual(label.styles, dict(color='red'))
|
82
|
+
self.assertEqual(label.css_classes, ['foo', 'baz'])
|
76
83
|
self.assertEqual(
|
77
84
|
scripts,
|
78
85
|
[
|
@@ -106,12 +113,24 @@ class LabelTest(TestCase):
|
|
106
113
|
elem.classList.remove("html-content");
|
107
114
|
"""
|
108
115
|
),
|
116
|
+
inspect.cleandoc(
|
117
|
+
f"""
|
118
|
+
elem = document.getElementById("{label.element_id()}");
|
119
|
+
elem.classList.add("baz");
|
120
|
+
"""
|
121
|
+
),
|
122
|
+
inspect.cleandoc(
|
123
|
+
f"""
|
124
|
+
elem = document.getElementById("{label.element_id()}");
|
125
|
+
elem.classList.remove("bar");
|
126
|
+
"""
|
127
|
+
),
|
109
128
|
]
|
110
129
|
)
|
111
130
|
|
112
131
|
def test_badge(self):
|
113
132
|
badge = label_lib.Badge('foo')
|
114
|
-
self.assert_html_content(badge, '<
|
133
|
+
self.assert_html_content(badge, '<span class="label badge">foo</span>')
|
115
134
|
|
116
135
|
|
117
136
|
class LabelGroupTest(TestCase):
|
@@ -121,9 +140,9 @@ class LabelGroupTest(TestCase):
|
|
121
140
|
self.assert_html_content(
|
122
141
|
group,
|
123
142
|
(
|
124
|
-
'<div class="label-group"><
|
125
|
-
'<
|
126
|
-
'<
|
143
|
+
'<div class="label-group"><span class="label group-name">baz</span>'
|
144
|
+
'<span class="label group-value">foo</span>'
|
145
|
+
'<span class="label group-value">bar</span></div>'
|
127
146
|
)
|
128
147
|
)
|
129
148
|
|
@@ -16,7 +16,7 @@
|
|
16
16
|
import functools
|
17
17
|
from typing import Annotated, List, Optional, Union
|
18
18
|
|
19
|
-
from pyglove.core import
|
19
|
+
from pyglove.core import utils
|
20
20
|
from pyglove.core.symbolic import object as pg_object
|
21
21
|
# pylint: disable=g-importing-member
|
22
22
|
from pyglove.core.views.html.base import Html
|
@@ -73,10 +73,8 @@ class SubProgress(HtmlControl):
|
|
73
73
|
[],
|
74
74
|
id=self.element_id(),
|
75
75
|
styles=styles,
|
76
|
-
css_classes=[
|
77
|
-
|
78
|
-
object_utils.camel_to_snake(self.name, '-')
|
79
|
-
] + self.css_classes,
|
76
|
+
css_classes=['sub-progress', utils.camel_to_snake(self.name, '-')]
|
77
|
+
+ self.css_classes,
|
80
78
|
)
|
81
79
|
|
82
80
|
def increment(self, delta: int = 1):
|
@@ -42,8 +42,8 @@ class ProgressBarTest(unittest.TestCase):
|
|
42
42
|
f'<div class="sub-progress foo" id="{bar["foo"].element_id()}">'
|
43
43
|
'</div><div class="sub-progress bar" '
|
44
44
|
f'id="{bar["bar"].element_id()}"></div></div>'
|
45
|
-
'<div class="label-container"><
|
46
|
-
f' id="{bar._progress_label.element_id()}">n/a</
|
45
|
+
'<div class="label-container"><span class="label progress-label"'
|
46
|
+
f' id="{bar._progress_label.element_id()}">n/a</span><span class='
|
47
47
|
f'"tooltip" id="{bar._progress_label.tooltip.element_id()}">'
|
48
48
|
'Not started</span></div></div>'
|
49
49
|
)
|
@@ -13,9 +13,11 @@
|
|
13
13
|
# limitations under the License.
|
14
14
|
"""Tab control."""
|
15
15
|
|
16
|
-
from typing import Annotated, List, Literal, Union
|
16
|
+
from typing import Annotated, List, Literal, Optional, Union
|
17
17
|
|
18
|
+
from pyglove.core.symbolic import flags as pg_flags
|
18
19
|
from pyglove.core.symbolic import object as pg_object
|
20
|
+
|
19
21
|
# pylint: disable=g-importing-member
|
20
22
|
from pyglove.core.views.html.base import Html
|
21
23
|
from pyglove.core.views.html.base import HtmlConvertible
|
@@ -37,6 +39,16 @@ class Tab(pg_object.Object):
|
|
37
39
|
'The content of the tab.'
|
38
40
|
]
|
39
41
|
|
42
|
+
css_classes: Annotated[
|
43
|
+
List[str],
|
44
|
+
'The CSS classes of the tab.'
|
45
|
+
] = []
|
46
|
+
|
47
|
+
name: Annotated[
|
48
|
+
Optional[str],
|
49
|
+
'An optional name that can be used to identify a tab under a tab control'
|
50
|
+
] = None
|
51
|
+
|
40
52
|
|
41
53
|
@pg_object.use_init_args(
|
42
54
|
['tabs', 'selected', 'tab_position', 'id', 'css_classes', 'styles']
|
@@ -61,109 +73,248 @@ class TabControl(HtmlControl):
|
|
61
73
|
|
62
74
|
interactive = True
|
63
75
|
|
76
|
+
def append(self, tab: Tab) -> None:
|
77
|
+
with pg_flags.notify_on_change(False):
|
78
|
+
self.tabs.append(tab)
|
79
|
+
|
80
|
+
self._insert_adjacent_html(
|
81
|
+
f"""
|
82
|
+
const elem = document.getElementById('{self.element_id()}-button-group');
|
83
|
+
""",
|
84
|
+
self._tab_button(tab, len(self.tabs) - 1),
|
85
|
+
position='beforeend',
|
86
|
+
)
|
87
|
+
self._insert_adjacent_html(
|
88
|
+
f"""
|
89
|
+
const elem = document.getElementById('{self.element_id()}-content-group');
|
90
|
+
""",
|
91
|
+
self._tab_content(tab, len(self.tabs) - 1),
|
92
|
+
position='beforeend',
|
93
|
+
)
|
94
|
+
|
95
|
+
def insert(self, index_or_name: Union[int, str], tab: Tab) -> None:
|
96
|
+
"""Inserts a tab before a tab identified by index or name."""
|
97
|
+
index = self.indexof(index_or_name)
|
98
|
+
if index == -1:
|
99
|
+
raise ValueError(f'Tab not found: {index_or_name!r}')
|
100
|
+
with pg_flags.notify_on_change(False):
|
101
|
+
self.tabs.insert(index, tab)
|
102
|
+
|
103
|
+
self._insert_adjacent_html(
|
104
|
+
f"""
|
105
|
+
const elem = document.querySelectorAll('#{self.element_id()}-button-group > .tab-button')[{index}];
|
106
|
+
""",
|
107
|
+
self._tab_button(tab, len(self.tabs) - 1),
|
108
|
+
position='beforebegin',
|
109
|
+
)
|
110
|
+
self._insert_adjacent_html(
|
111
|
+
f"""
|
112
|
+
const elem = document.querySelectorAll('#{self.element_id()}-content-group > .tab-content')[{index}];
|
113
|
+
""",
|
114
|
+
self._tab_content(tab, len(self.tabs) - 1),
|
115
|
+
position='beforebegin',
|
116
|
+
)
|
117
|
+
|
118
|
+
def indexof(self, index_or_name: Union[int, str]) -> int:
|
119
|
+
if isinstance(index_or_name, int):
|
120
|
+
index = index_or_name
|
121
|
+
if index >= len(self.tabs):
|
122
|
+
return len(self.tabs) - 1
|
123
|
+
elif index < -len(self.tabs):
|
124
|
+
return -1
|
125
|
+
elif index < 0:
|
126
|
+
index = index + len(self.tabs)
|
127
|
+
return index
|
128
|
+
else:
|
129
|
+
name = index_or_name
|
130
|
+
assert isinstance(name, str), name
|
131
|
+
for i, tab in enumerate(self.tabs):
|
132
|
+
if tab.name == name:
|
133
|
+
return i
|
134
|
+
return -1
|
135
|
+
|
136
|
+
def extend(self, tabs: List[Tab]) -> None:
|
137
|
+
for tab in tabs:
|
138
|
+
self.append(tab)
|
139
|
+
|
140
|
+
def select(
|
141
|
+
self,
|
142
|
+
index_or_name: Union[int, str, List[str]]) -> Union[int, str]:
|
143
|
+
"""Selects a tab identified by an index or name.
|
144
|
+
|
145
|
+
Args:
|
146
|
+
index_or_name: The index or name of the tab to select. If a list of names
|
147
|
+
is provided, the first name in the list that is found will be selected.
|
148
|
+
|
149
|
+
Returns:
|
150
|
+
The index (if the index was provided) or name of the selected tab.
|
151
|
+
"""
|
152
|
+
selected_name = index_or_name if isinstance(index_or_name, str) else None
|
153
|
+
index = -1
|
154
|
+
if isinstance(index_or_name, list):
|
155
|
+
for name in index_or_name:
|
156
|
+
index = self.indexof(name)
|
157
|
+
if index != -1:
|
158
|
+
selected_name = name
|
159
|
+
break
|
160
|
+
else:
|
161
|
+
index = self.indexof(index_or_name)
|
162
|
+
if index == -1:
|
163
|
+
raise ValueError(f'Tab not found: {index_or_name!r}')
|
164
|
+
self._sync_members(selected=index)
|
165
|
+
self._run_javascript(
|
166
|
+
f"""
|
167
|
+
const tabButtons = document.querySelectorAll('#{self.element_id()}-button-group > .tab-button');
|
168
|
+
tabButtons[{index}].click();
|
169
|
+
"""
|
170
|
+
)
|
171
|
+
return selected_name or index
|
172
|
+
|
64
173
|
def _to_html(self, **kwargs):
|
65
174
|
return Html.element(
|
66
|
-
'
|
175
|
+
'table',
|
67
176
|
[
|
177
|
+
'<tr><td>',
|
68
178
|
Html.element(
|
69
179
|
'div',
|
70
|
-
[
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
css_classes=[
|
77
|
-
'tab-button',
|
78
|
-
'selected' if i == self.selected else None
|
79
|
-
],
|
80
|
-
onclick=(
|
81
|
-
f"""openTab(event, '{self.element_id()}', '{self.element_id(str(i))}')"""
|
82
|
-
)
|
83
|
-
) for i, tab in enumerate(self.tabs)
|
84
|
-
],
|
85
|
-
css_classes=['tab-button-group'],
|
180
|
+
[self._tab_button(tab, i) for i, tab in enumerate(self.tabs)],
|
181
|
+
css_classes=[
|
182
|
+
'tab-button-group',
|
183
|
+
self.tab_position
|
184
|
+
] + self.css_classes,
|
185
|
+
id=self.element_id('button-group'),
|
86
186
|
),
|
87
|
-
|
187
|
+
('</td><td>' if self.tab_position == 'left'
|
188
|
+
else '</td></tr><tr><td>'),
|
88
189
|
Html.element(
|
89
190
|
'div',
|
90
|
-
[
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
) for i, tab in enumerate(self.tabs)
|
191
|
+
[self._tab_content(tab, i) for i, tab in enumerate(self.tabs)],
|
192
|
+
css_classes=[
|
193
|
+
'tab-content-group',
|
194
|
+
self.tab_position
|
195
|
+
] + self.css_classes,
|
196
|
+
id=self.element_id('content-group'),
|
197
|
+
),
|
198
|
+
'</td></tr>'
|
99
199
|
],
|
100
|
-
css_classes=['tab-control'
|
101
|
-
id=self.element_id(),
|
200
|
+
css_classes=['tab-control'],
|
102
201
|
styles=self.styles,
|
103
202
|
).add_script(
|
104
203
|
"""
|
105
204
|
function openTab(event, controlId, tabId) {
|
106
|
-
const tabButtons = document.querySelectorAll('#' + controlId + '
|
205
|
+
const tabButtons = document.querySelectorAll('#' + controlId + '-button-group > .tab-button');
|
107
206
|
for (let i = 0; i < tabButtons.length; i++) {
|
108
207
|
tabButtons[i].classList.remove('selected');
|
109
208
|
}
|
110
|
-
const tabContents = document.querySelectorAll('#' + controlId + '> .tab-content');
|
209
|
+
const tabContents = document.querySelectorAll('#' + controlId + '-content-group > .tab-content');
|
111
210
|
for (let i = 0; i < tabContents.length; i++) {
|
112
|
-
tabContents[i].
|
211
|
+
tabContents[i].classList.remove('selected')
|
113
212
|
}
|
114
213
|
const tabButton = event.currentTarget;
|
115
214
|
tabButton.classList.add('selected');
|
116
|
-
document.getElementById(tabId).
|
215
|
+
document.getElementById(tabId).classList.add('selected');
|
117
216
|
}
|
118
217
|
"""
|
119
218
|
).add_style(
|
120
219
|
"""
|
121
|
-
.
|
122
|
-
|
123
|
-
border-
|
220
|
+
.tab-control {
|
221
|
+
border-spacing: 0px;
|
222
|
+
border-collapse: collapse;
|
223
|
+
margin-top: 10px;
|
124
224
|
}
|
125
|
-
.
|
126
|
-
|
127
|
-
|
225
|
+
.tab-control td {
|
226
|
+
padding: 0px;
|
227
|
+
margin: 0px;
|
228
|
+
vertical-align: top;
|
229
|
+
}
|
230
|
+
.top.tab-button-group {
|
231
|
+
border-left: 1px solid #DDD;
|
232
|
+
border-top: 1px solid #DDD;
|
233
|
+
border-right: 1px solid #DDD;
|
234
|
+
border-radius: 5px 5px 0px 0px;
|
235
|
+
padding: 0px 5px 0px 0px;
|
236
|
+
margin-bottom: -2px;
|
128
237
|
}
|
129
238
|
.tab-button {
|
130
|
-
background-color:
|
131
|
-
border:
|
239
|
+
background-color: transparent;
|
240
|
+
border-radius: 5px;
|
241
|
+
border: 0px;
|
242
|
+
font-weight: bold;
|
243
|
+
color: gray;
|
132
244
|
outline: none;
|
133
245
|
cursor: pointer;
|
134
246
|
transition: 0.3s;
|
135
|
-
padding: 10px 15px 10px 15px;
|
136
|
-
}
|
137
|
-
.top .tab-button {
|
138
|
-
border-top-width: 2px;
|
139
247
|
}
|
140
|
-
.
|
141
|
-
|
142
|
-
width: 100%;
|
143
|
-
border-left-width: 2px;
|
248
|
+
.tab-button:hover {
|
249
|
+
background-color: #fff1dd;
|
144
250
|
}
|
145
|
-
.
|
146
|
-
|
147
|
-
|
148
|
-
background: #fff;
|
251
|
+
.tab-button.selected {
|
252
|
+
background-color: #f0ecf9;
|
253
|
+
color: black;
|
149
254
|
}
|
150
|
-
.
|
151
|
-
border-
|
152
|
-
border-
|
153
|
-
|
255
|
+
.top.tab-content-group {
|
256
|
+
border-left: 1px solid #DDD;
|
257
|
+
border-right: 1px solid #DDD;
|
258
|
+
border-bottom: 1px solid #DDD;
|
259
|
+
border-radius: 0px 0px 5px 5px;
|
260
|
+
margin: 0px;
|
261
|
+
padding: 5px;
|
262
|
+
height: 100%;
|
154
263
|
}
|
155
|
-
.top .tab-button
|
156
|
-
|
157
|
-
}
|
158
|
-
.left .tab-button:hover {
|
159
|
-
border-left-color: orange;
|
264
|
+
.top > .tab-button {
|
265
|
+
margin: 5px 0px 5px 5px;
|
160
266
|
}
|
161
267
|
.tab-content {
|
162
268
|
display: none;
|
163
|
-
padding: 10px;
|
164
269
|
}
|
165
|
-
.
|
166
|
-
|
270
|
+
.tab-content.selected {
|
271
|
+
display: block;
|
272
|
+
}
|
273
|
+
.left.tab-button-group {
|
274
|
+
display: inline-flex;
|
275
|
+
flex-direction: column;
|
276
|
+
border: 1px solid #DDD;
|
277
|
+
border-radius: 5px;
|
278
|
+
margin-right: 5px;
|
279
|
+
padding: 0px 0px 5px 0px;
|
280
|
+
}
|
281
|
+
.left.tab-content-group {
|
282
|
+
border: 0px
|
283
|
+
margin: 0px;
|
284
|
+
padding: 0px;
|
285
|
+
height: 100%;
|
286
|
+
}
|
287
|
+
.left > .tab-button {
|
288
|
+
text-align: left;
|
289
|
+
margin: 5px 5px 0px 5px;
|
167
290
|
}
|
168
291
|
"""
|
169
292
|
)
|
293
|
+
|
294
|
+
def _tab_button(self, tab: Tab, i: int) -> Html:
|
295
|
+
return Html.element(
|
296
|
+
'button',
|
297
|
+
[
|
298
|
+
tab.label
|
299
|
+
],
|
300
|
+
css_classes=[
|
301
|
+
'tab-button',
|
302
|
+
'selected' if i == self.selected else None
|
303
|
+
] + tab.css_classes,
|
304
|
+
onclick=(
|
305
|
+
f"""openTab(event, '{self.element_id()}', '{self.element_id(str(i))}')"""
|
306
|
+
)
|
307
|
+
)
|
308
|
+
|
309
|
+
def _tab_content(self, tab: Tab, i: int) -> Html:
|
310
|
+
return Html.element(
|
311
|
+
'div',
|
312
|
+
[
|
313
|
+
tab.content
|
314
|
+
],
|
315
|
+
css_classes=[
|
316
|
+
'tab-content',
|
317
|
+
'selected' if i == self.selected else None
|
318
|
+
] + tab.css_classes,
|
319
|
+
id=self.element_id(str(i))
|
320
|
+
)
|
@@ -30,26 +30,57 @@ class TabControlTest(unittest.TestCase):
|
|
30
30
|
|
31
31
|
def test_basic(self):
|
32
32
|
tab = tab_lib.TabControl([
|
33
|
-
tab_lib.Tab('foo', base.Html('<h1>foo</h1>')),
|
33
|
+
tab_lib.Tab('foo', base.Html('<h1>foo</h1>'), css_classes=['foo']),
|
34
34
|
tab_lib.Tab('bar', base.Html('<h1>bar</h1>')),
|
35
35
|
])
|
36
|
+
elem_id = tab.element_id()
|
36
37
|
self.assert_html_content(
|
37
38
|
tab,
|
38
|
-
(
|
39
|
-
f'<div class="tab-control top" id="{tab.element_id()}">'
|
40
|
-
'<div class="tab-button-group"><button class="tab-button selected" '
|
41
|
-
f'''onclick="openTab(event, '{tab.element_id()}', '''
|
42
|
-
f''''{tab.element_id(str(0))}')">'''
|
43
|
-
'<a class="label">foo</a></button><button class="tab-button" '
|
44
|
-
f'''onclick="openTab(event, '{tab.element_id()}', '''
|
45
|
-
f''''{tab.element_id(str(1))}')">'''
|
46
|
-
'<a class="label">bar</a></button></div>'
|
47
|
-
'<div class="tab-content" style="display:block;" '
|
48
|
-
f'id="{tab.element_id(str(0))}"><h1>foo</h1></div>'
|
49
|
-
'<div class="tab-content" style="display:none;" '
|
50
|
-
f'id="{tab.element_id(str(1))}"><h1>bar</h1></div></div>'
|
51
|
-
)
|
39
|
+
f"""<table class="tab-control"><tr><td><div class="tab-button-group top" id="{elem_id}-button-group"><button class="tab-button selected foo" onclick="openTab(event, '{elem_id}', '{elem_id}-0')"><span class="label">foo</span></button><button class="tab-button" onclick="openTab(event, '{elem_id}', '{elem_id}-1')"><span class="label">bar</span></button></div></td></tr><tr><td><div class="tab-content-group top" id="{elem_id}-content-group"><div class="tab-content selected foo" id="{elem_id}-0"><h1>foo</h1></div><div class="tab-content" id="{elem_id}-1"><h1>bar</h1></div></div></td></tr></table>"""
|
52
40
|
)
|
41
|
+
with tab.track_scripts() as scripts:
|
42
|
+
tab.extend([
|
43
|
+
tab_lib.Tab(
|
44
|
+
'baz',
|
45
|
+
base.Html(
|
46
|
+
'<h1 class="a">baz</h1>').add_style('.a { color: red; }')
|
47
|
+
),
|
48
|
+
tab_lib.Tab('qux', base.Html('<h1>qux</h1>')),
|
49
|
+
])
|
50
|
+
self.assertEqual(len(scripts), 6)
|
51
|
+
with tab.track_scripts() as scripts:
|
52
|
+
tab.insert(0, tab_lib.Tab('x', 'foo', name='x'))
|
53
|
+
self.assertEqual(len(scripts), 2)
|
54
|
+
self.assertEqual(len(tab.tabs), 5)
|
55
|
+
self.assertEqual(tab.indexof(-1), 4)
|
56
|
+
self.assertEqual(tab.indexof(3), 3)
|
57
|
+
self.assertEqual(tab.indexof(10), 4)
|
58
|
+
self.assertEqual(tab.indexof(-10), -1)
|
59
|
+
self.assertEqual(tab.indexof('x'), 0)
|
60
|
+
self.assertEqual(tab.indexof('y'), -1)
|
61
|
+
self.assertEqual(tab.select(0), 0)
|
62
|
+
self.assertEqual(tab.select('x'), 'x')
|
63
|
+
self.assertEqual(tab.select(['y', 'x']), 'x')
|
64
|
+
|
65
|
+
with self.assertRaisesRegex(ValueError, 'Tab not found'):
|
66
|
+
tab.select('y')
|
67
|
+
with self.assertRaisesRegex(ValueError, 'Tab not found'):
|
68
|
+
tab.insert('y', tab_lib.Tab('z', 'bar'))
|
69
|
+
|
70
|
+
with tab.track_scripts() as scripts:
|
71
|
+
tab.insert('x', tab_lib.Tab('y', 'bar'))
|
72
|
+
self.assertEqual(len(scripts), 2)
|
73
|
+
self.assertEqual(len(tab.tabs), 6)
|
74
|
+
|
75
|
+
with tab.track_scripts() as scripts:
|
76
|
+
tab.select(3)
|
77
|
+
self.assertEqual(len(scripts), 1)
|
78
|
+
self.assertEqual(tab.selected, 3)
|
79
|
+
|
80
|
+
with tab.track_scripts() as scripts:
|
81
|
+
tab.select('x')
|
82
|
+
self.assertEqual(len(scripts), 1)
|
83
|
+
self.assertEqual(tab.selected, 1)
|
53
84
|
|
54
85
|
|
55
86
|
if __name__ == '__main__':
|