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.
Files changed (118) hide show
  1. pyglove/core/__init__.py +40 -21
  2. pyglove/core/coding/__init__.py +42 -0
  3. pyglove/core/coding/errors.py +111 -0
  4. pyglove/core/coding/errors_test.py +98 -0
  5. pyglove/core/coding/execution.py +312 -0
  6. pyglove/core/coding/execution_test.py +333 -0
  7. pyglove/core/{object_utils/codegen.py → coding/function_generation.py} +10 -4
  8. pyglove/core/{object_utils/codegen_test.py → coding/function_generation_test.py} +5 -7
  9. pyglove/core/coding/parsing.py +153 -0
  10. pyglove/core/coding/parsing_test.py +150 -0
  11. pyglove/core/coding/permissions.py +100 -0
  12. pyglove/core/coding/permissions_test.py +93 -0
  13. pyglove/core/geno/base.py +53 -38
  14. pyglove/core/geno/base_test.py +2 -4
  15. pyglove/core/geno/categorical.py +36 -27
  16. pyglove/core/geno/custom.py +18 -15
  17. pyglove/core/geno/numerical.py +19 -16
  18. pyglove/core/geno/space.py +3 -4
  19. pyglove/core/hyper/base.py +6 -6
  20. pyglove/core/hyper/categorical.py +91 -52
  21. pyglove/core/hyper/custom.py +7 -7
  22. pyglove/core/hyper/custom_test.py +9 -10
  23. pyglove/core/hyper/derived.py +30 -22
  24. pyglove/core/hyper/derived_test.py +3 -5
  25. pyglove/core/hyper/dynamic_evaluation.py +3 -4
  26. pyglove/core/hyper/evolvable.py +57 -46
  27. pyglove/core/hyper/numerical.py +48 -24
  28. pyglove/core/hyper/numerical_test.py +9 -9
  29. pyglove/core/hyper/object_template.py +58 -46
  30. pyglove/core/logging_test.py +0 -2
  31. pyglove/core/patching/object_factory.py +4 -4
  32. pyglove/core/patching/pattern_based.py +4 -4
  33. pyglove/core/patching/rule_based.py +4 -3
  34. pyglove/core/symbolic/__init__.py +4 -0
  35. pyglove/core/symbolic/base.py +200 -136
  36. pyglove/core/symbolic/base_test.py +17 -19
  37. pyglove/core/symbolic/boilerplate.py +4 -5
  38. pyglove/core/symbolic/class_wrapper.py +10 -14
  39. pyglove/core/symbolic/class_wrapper_test.py +2 -2
  40. pyglove/core/symbolic/compounding.py +2 -2
  41. pyglove/core/symbolic/compounding_test.py +2 -4
  42. pyglove/core/symbolic/contextual_object.py +288 -0
  43. pyglove/core/symbolic/contextual_object_test.py +327 -0
  44. pyglove/core/symbolic/dict.py +115 -87
  45. pyglove/core/symbolic/dict_test.py +188 -131
  46. pyglove/core/symbolic/diff.py +12 -12
  47. pyglove/core/symbolic/flags.py +1 -1
  48. pyglove/core/symbolic/functor.py +16 -15
  49. pyglove/core/symbolic/functor_test.py +2 -4
  50. pyglove/core/symbolic/inferred.py +2 -2
  51. pyglove/core/symbolic/list.py +70 -47
  52. pyglove/core/symbolic/list_test.py +117 -98
  53. pyglove/core/symbolic/object.py +59 -58
  54. pyglove/core/symbolic/object_test.py +143 -90
  55. pyglove/core/symbolic/origin.py +5 -7
  56. pyglove/core/symbolic/pure_symbolic.py +4 -3
  57. pyglove/core/symbolic/ref.py +33 -16
  58. pyglove/core/symbolic/ref_test.py +17 -0
  59. pyglove/core/tuning/local_backend.py +2 -2
  60. pyglove/core/tuning/protocols.py +3 -3
  61. pyglove/core/typing/annotation_conversion.py +8 -3
  62. pyglove/core/typing/annotation_conversion_test.py +8 -0
  63. pyglove/core/typing/callable_ext.py +11 -13
  64. pyglove/core/typing/callable_signature.py +22 -19
  65. pyglove/core/typing/callable_signature_test.py +3 -5
  66. pyglove/core/typing/class_schema.py +93 -54
  67. pyglove/core/typing/class_schema_test.py +4 -5
  68. pyglove/core/typing/custom_typing.py +5 -4
  69. pyglove/core/typing/key_specs.py +5 -7
  70. pyglove/core/typing/key_specs_test.py +4 -4
  71. pyglove/core/typing/type_conversion.py +4 -5
  72. pyglove/core/typing/type_conversion_test.py +12 -12
  73. pyglove/core/typing/typed_missing.py +6 -7
  74. pyglove/core/typing/typed_missing_test.py +7 -8
  75. pyglove/core/typing/value_specs.py +287 -144
  76. pyglove/core/typing/value_specs_test.py +148 -25
  77. pyglove/core/utils/__init__.py +172 -0
  78. pyglove/core/{object_utils → utils}/common_traits.py +2 -2
  79. pyglove/core/{object_utils → utils}/common_traits_test.py +1 -3
  80. pyglove/core/utils/contextual.py +147 -0
  81. pyglove/core/utils/contextual_test.py +88 -0
  82. pyglove/core/{object_utils → utils}/docstr_utils_test.py +1 -3
  83. pyglove/core/{object_utils → utils}/error_utils.py +3 -3
  84. pyglove/core/{object_utils → utils}/error_utils_test.py +1 -1
  85. pyglove/core/{object_utils → utils}/formatting.py +1 -1
  86. pyglove/core/{object_utils → utils}/formatting_test.py +1 -2
  87. pyglove/core/{object_utils → utils}/hierarchical.py +23 -25
  88. pyglove/core/{object_utils → utils}/hierarchical_test.py +3 -5
  89. pyglove/core/{object_utils → utils}/json_conversion.py +1 -1
  90. pyglove/core/{object_utils → utils}/json_conversion_test.py +1 -3
  91. pyglove/core/{object_utils → utils}/missing.py +2 -2
  92. pyglove/core/{object_utils → utils}/missing_test.py +2 -4
  93. pyglove/core/utils/text_color.py +128 -0
  94. pyglove/core/utils/text_color_test.py +94 -0
  95. pyglove/core/{object_utils → utils}/thread_local_test.py +1 -3
  96. pyglove/core/{object_utils → utils}/timing.py +21 -10
  97. pyglove/core/{object_utils → utils}/timing_test.py +14 -12
  98. pyglove/core/{object_utils → utils}/value_location.py +2 -2
  99. pyglove/core/{object_utils → utils}/value_location_test.py +2 -4
  100. pyglove/core/views/base.py +25 -29
  101. pyglove/core/views/html/base.py +15 -16
  102. pyglove/core/views/html/controls/base.py +46 -9
  103. pyglove/core/views/html/controls/label.py +13 -2
  104. pyglove/core/views/html/controls/label_test.py +27 -8
  105. pyglove/core/views/html/controls/progress_bar.py +3 -5
  106. pyglove/core/views/html/controls/progress_bar_test.py +2 -2
  107. pyglove/core/views/html/controls/tab.py +217 -66
  108. pyglove/core/views/html/controls/tab_test.py +46 -15
  109. pyglove/core/views/html/tree_view.py +39 -37
  110. {pyglove-0.4.5.dev202411132359.dist-info → pyglove-0.4.5.dev202501250807.dist-info}/METADATA +17 -3
  111. pyglove-0.4.5.dev202501250807.dist-info/RECORD +218 -0
  112. {pyglove-0.4.5.dev202411132359.dist-info → pyglove-0.4.5.dev202501250807.dist-info}/WHEEL +1 -1
  113. pyglove/core/object_utils/__init__.py +0 -164
  114. pyglove-0.4.5.dev202411132359.dist-info/RECORD +0 -203
  115. /pyglove/core/{object_utils → utils}/docstr_utils.py +0 -0
  116. /pyglove/core/{object_utils → utils}/thread_local.py +0 -0
  117. {pyglove-0.4.5.dev202411132359.dist-info → pyglove-0.4.5.dev202501250807.dist-info}/LICENSE +0 -0
  118. {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, '<a class="label">foo</a>')
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"><a class="label">foo</a>'
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('foo', 'bar', 'http://google.com', interactive=True)
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, '<a class="label badge">foo</a>')
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"><a class="label group-name">baz</a>'
125
- '<a class="label group-value">foo</a>'
126
- '<a class="label group-value">bar</a></div>'
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 object_utils
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
- 'sub-progress',
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"><a class="label progress-label"'
46
- f' id="{bar._progress_label.element_id()}">n/a</a><span class='
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
- 'div',
175
+ 'table',
67
176
  [
177
+ '<tr><td>',
68
178
  Html.element(
69
179
  'div',
70
- [
71
- Html.element(
72
- 'button',
73
- [
74
- tab.label
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
- tab.content
92
- ],
93
- css_classes=['tab-content'],
94
- styles=dict(
95
- display='block' if i == self.selected else 'none'
96
- ),
97
- id=self.element_id(str(i))
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', self.tab_position] + self.css_classes,
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 + '> .tab-button-group > .tab-button');
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].style.display = 'none';
211
+ tabContents[i].classList.remove('selected')
113
212
  }
114
213
  const tabButton = event.currentTarget;
115
214
  tabButton.classList.add('selected');
116
- document.getElementById(tabId).style.display = "block";
215
+ document.getElementById(tabId).classList.add('selected');
117
216
  }
118
217
  """
119
218
  ).add_style(
120
219
  """
121
- .top .tab-button-group {
122
- overflow-x: hidden;
123
- border-bottom: 1px solid #EEE;
220
+ .tab-control {
221
+ border-spacing: 0px;
222
+ border-collapse: collapse;
223
+ margin-top: 10px;
124
224
  }
125
- .left .tab-button-group {
126
- float: left;
127
- top: 0;
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: #EEE;
131
- border: 1px solid #EEE;
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
- .left .tab-button {
141
- display: block;
142
- width: 100%;
143
- border-left-width: 2px;
248
+ .tab-button:hover {
249
+ background-color: #fff1dd;
144
250
  }
145
- .top .tab-button.selected {
146
- border-bottom-color: #fff;
147
- border-top-color: #B721FF;
148
- background: #fff;
251
+ .tab-button.selected {
252
+ background-color: #f0ecf9;
253
+ color: black;
149
254
  }
150
- .left .tab-button.selected {
151
- border-right-color: #fff;
152
- border-left-color: #B721FF;
153
- background: #fff;
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:hover {
156
- border-top-color: orange;
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
- .left .tab-content {
166
- float: left;
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__':