pyglove 0.4.5.dev20240319__py3-none-any.whl → 0.4.5.dev202501132210__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 (145) hide show
  1. pyglove/core/__init__.py +54 -20
  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 +309 -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 +54 -41
  14. pyglove/core/geno/base_test.py +2 -4
  15. pyglove/core/geno/categorical.py +37 -28
  16. pyglove/core/geno/custom.py +19 -16
  17. pyglove/core/geno/numerical.py +20 -17
  18. pyglove/core/geno/space.py +4 -5
  19. pyglove/core/hyper/base.py +6 -6
  20. pyglove/core/hyper/categorical.py +94 -55
  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 +2 -4
  25. pyglove/core/hyper/dynamic_evaluation.py +5 -6
  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/io/__init__.py +1 -0
  31. pyglove/core/io/file_system.py +17 -7
  32. pyglove/core/io/file_system_test.py +2 -0
  33. pyglove/core/io/sequence.py +299 -0
  34. pyglove/core/io/sequence_test.py +124 -0
  35. pyglove/core/logging_test.py +0 -2
  36. pyglove/core/patching/object_factory.py +4 -4
  37. pyglove/core/patching/pattern_based.py +4 -4
  38. pyglove/core/patching/rule_based.py +17 -5
  39. pyglove/core/patching/rule_based_test.py +27 -4
  40. pyglove/core/symbolic/__init__.py +2 -7
  41. pyglove/core/symbolic/base.py +320 -183
  42. pyglove/core/symbolic/base_test.py +123 -19
  43. pyglove/core/symbolic/boilerplate.py +7 -13
  44. pyglove/core/symbolic/boilerplate_test.py +25 -23
  45. pyglove/core/symbolic/class_wrapper.py +48 -45
  46. pyglove/core/symbolic/class_wrapper_test.py +2 -2
  47. pyglove/core/symbolic/compounding.py +9 -15
  48. pyglove/core/symbolic/compounding_test.py +2 -4
  49. pyglove/core/symbolic/dict.py +154 -110
  50. pyglove/core/symbolic/dict_test.py +238 -130
  51. pyglove/core/symbolic/diff.py +199 -10
  52. pyglove/core/symbolic/diff_test.py +226 -0
  53. pyglove/core/symbolic/flags.py +1 -1
  54. pyglove/core/symbolic/functor.py +29 -26
  55. pyglove/core/symbolic/functor_test.py +102 -50
  56. pyglove/core/symbolic/inferred.py +2 -2
  57. pyglove/core/symbolic/list.py +81 -50
  58. pyglove/core/symbolic/list_test.py +119 -97
  59. pyglove/core/symbolic/object.py +225 -113
  60. pyglove/core/symbolic/object_test.py +320 -108
  61. pyglove/core/symbolic/origin.py +17 -14
  62. pyglove/core/symbolic/origin_test.py +4 -2
  63. pyglove/core/symbolic/pure_symbolic.py +4 -3
  64. pyglove/core/symbolic/ref.py +108 -21
  65. pyglove/core/symbolic/ref_test.py +93 -0
  66. pyglove/core/symbolic/symbolize_test.py +10 -2
  67. pyglove/core/tuning/local_backend.py +2 -2
  68. pyglove/core/tuning/protocols.py +3 -3
  69. pyglove/core/tuning/sample_test.py +3 -3
  70. pyglove/core/typing/__init__.py +14 -5
  71. pyglove/core/typing/annotation_conversion.py +43 -27
  72. pyglove/core/typing/annotation_conversion_test.py +23 -0
  73. pyglove/core/typing/callable_ext.py +241 -3
  74. pyglove/core/typing/callable_ext_test.py +255 -0
  75. pyglove/core/typing/callable_signature.py +510 -66
  76. pyglove/core/typing/callable_signature_test.py +619 -99
  77. pyglove/core/typing/class_schema.py +229 -154
  78. pyglove/core/typing/class_schema_test.py +149 -95
  79. pyglove/core/typing/custom_typing.py +5 -4
  80. pyglove/core/typing/inspect.py +63 -0
  81. pyglove/core/typing/inspect_test.py +39 -0
  82. pyglove/core/typing/key_specs.py +10 -11
  83. pyglove/core/typing/key_specs_test.py +7 -4
  84. pyglove/core/typing/type_conversion.py +4 -5
  85. pyglove/core/typing/type_conversion_test.py +12 -12
  86. pyglove/core/typing/typed_missing.py +6 -7
  87. pyglove/core/typing/typed_missing_test.py +7 -8
  88. pyglove/core/typing/value_specs.py +604 -362
  89. pyglove/core/typing/value_specs_test.py +328 -90
  90. pyglove/core/utils/__init__.py +164 -0
  91. pyglove/core/{object_utils → utils}/common_traits.py +3 -67
  92. pyglove/core/utils/common_traits_test.py +36 -0
  93. pyglove/core/{object_utils → utils}/docstr_utils.py +23 -0
  94. pyglove/core/{object_utils → utils}/docstr_utils_test.py +36 -4
  95. pyglove/core/{object_utils → utils}/error_utils.py +78 -9
  96. pyglove/core/{object_utils → utils}/error_utils_test.py +61 -5
  97. pyglove/core/utils/formatting.py +464 -0
  98. pyglove/core/utils/formatting_test.py +453 -0
  99. pyglove/core/{object_utils → utils}/hierarchical.py +23 -25
  100. pyglove/core/{object_utils → utils}/hierarchical_test.py +3 -5
  101. pyglove/core/{object_utils → utils}/json_conversion.py +177 -52
  102. pyglove/core/{object_utils → utils}/json_conversion_test.py +97 -16
  103. pyglove/core/{object_utils → utils}/missing.py +3 -3
  104. pyglove/core/{object_utils → utils}/missing_test.py +2 -4
  105. pyglove/core/utils/text_color.py +128 -0
  106. pyglove/core/utils/text_color_test.py +94 -0
  107. pyglove/core/{object_utils → utils}/thread_local_test.py +1 -3
  108. pyglove/core/utils/timing.py +236 -0
  109. pyglove/core/utils/timing_test.py +154 -0
  110. pyglove/core/{object_utils → utils}/value_location.py +275 -6
  111. pyglove/core/utils/value_location_test.py +707 -0
  112. pyglove/core/views/__init__.py +32 -0
  113. pyglove/core/views/base.py +804 -0
  114. pyglove/core/views/base_test.py +580 -0
  115. pyglove/core/views/html/__init__.py +27 -0
  116. pyglove/core/views/html/base.py +547 -0
  117. pyglove/core/views/html/base_test.py +830 -0
  118. pyglove/core/views/html/controls/__init__.py +35 -0
  119. pyglove/core/views/html/controls/base.py +275 -0
  120. pyglove/core/views/html/controls/label.py +207 -0
  121. pyglove/core/views/html/controls/label_test.py +157 -0
  122. pyglove/core/views/html/controls/progress_bar.py +183 -0
  123. pyglove/core/views/html/controls/progress_bar_test.py +97 -0
  124. pyglove/core/views/html/controls/tab.py +320 -0
  125. pyglove/core/views/html/controls/tab_test.py +87 -0
  126. pyglove/core/views/html/controls/tooltip.py +99 -0
  127. pyglove/core/views/html/controls/tooltip_test.py +99 -0
  128. pyglove/core/views/html/tree_view.py +1517 -0
  129. pyglove/core/views/html/tree_view_test.py +1461 -0
  130. {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/METADATA +18 -4
  131. pyglove-0.4.5.dev202501132210.dist-info/RECORD +214 -0
  132. {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/WHEEL +1 -1
  133. pyglove/core/object_utils/__init__.py +0 -154
  134. pyglove/core/object_utils/common_traits_test.py +0 -82
  135. pyglove/core/object_utils/formatting.py +0 -234
  136. pyglove/core/object_utils/formatting_test.py +0 -223
  137. pyglove/core/object_utils/value_location_test.py +0 -385
  138. pyglove/core/symbolic/schema_utils.py +0 -327
  139. pyglove/core/symbolic/schema_utils_test.py +0 -57
  140. pyglove/core/typing/class_schema_utils.py +0 -202
  141. pyglove/core/typing/class_schema_utils_test.py +0 -194
  142. pyglove-0.4.5.dev20240319.dist-info/RECORD +0 -185
  143. /pyglove/core/{object_utils → utils}/thread_local.py +0 -0
  144. {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/LICENSE +0 -0
  145. {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,35 @@
1
+ # Copyright 2024 The PyGlove Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Common HTML controls."""
15
+
16
+ # pylint: disable=g-importing-member
17
+ # pylint: disable=g-bad-import-order
18
+
19
+ from pyglove.core.views.html.controls.base import HtmlControl
20
+
21
+
22
+ from pyglove.core.views.html.controls.label import Label
23
+ from pyglove.core.views.html.controls.label import LabelGroup
24
+ from pyglove.core.views.html.controls.label import Badge
25
+
26
+ from pyglove.core.views.html.controls.tooltip import Tooltip
27
+
28
+ from pyglove.core.views.html.controls.tab import Tab
29
+ from pyglove.core.views.html.controls.tab import TabControl
30
+
31
+ from pyglove.core.views.html.controls.progress_bar import ProgressBar
32
+ from pyglove.core.views.html.controls.progress_bar import SubProgress
33
+
34
+ # pylint: enable=g-bad-import-order
35
+ # pylint: enable=g-importing-member
@@ -0,0 +1,275 @@
1
+ # Copyright 2024 The PyGlove Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Base for HTML controls."""
15
+
16
+ import abc
17
+ import contextlib
18
+ import inspect
19
+ import sys
20
+ from typing import Annotated, Any, Dict, Iterator, List, Optional, Union
21
+
22
+ from pyglove.core import utils
23
+ from pyglove.core.symbolic import object as pg_object
24
+ from pyglove.core.views.html import base
25
+
26
+ Html = base.Html
27
+
28
+
29
+ try:
30
+ _notebook = sys.modules['IPython'].display
31
+ except Exception: # pylint: disable=broad-except
32
+ _notebook = None
33
+
34
+
35
+ class HtmlControl(pg_object.Object):
36
+ """Base class for HTML controls."""
37
+
38
+ id: Annotated[
39
+ Optional[str],
40
+ 'Optional ID of the root element.'
41
+ ] = None
42
+
43
+ css_classes: Annotated[
44
+ List[str],
45
+ 'Optional CSS classes of the root element.'
46
+ ] = []
47
+
48
+ styles: Annotated[
49
+ Dict[str, Any],
50
+ 'Ad-hoc styles (vs. class-based) of the root element.'
51
+ ] = {}
52
+
53
+ interactive: Annotated[
54
+ bool,
55
+ (
56
+ 'Whether the control is interactive. If False, optimizations will be '
57
+ 'applied to reduce the size of the HTML.'
58
+ )
59
+ ] = False
60
+
61
+ def _on_bound(self):
62
+ super()._on_bound()
63
+ self._rendered = False
64
+ self._css_styles = []
65
+ self._dynamic_injected_css = set()
66
+ self._scripts = []
67
+
68
+ def add_style(self, *css: str) -> 'HtmlControl':
69
+ """Adds CSS styles to the HTML."""
70
+ self._css_styles.extend(css)
71
+ return self
72
+
73
+ def add_script(self, *scripts: str) -> 'HtmlControl':
74
+ self._scripts.extend(scripts)
75
+ return self
76
+
77
+ def to_html(self, **kwargs) -> Html:
78
+ """Returns the HTML representation of the control."""
79
+ self._rendered = True
80
+ self._dynamic_injected_css = set()
81
+ html = self._to_html(**kwargs)
82
+ return html.add_style(*self._css_styles).add_script(*self._scripts)
83
+
84
+ @abc.abstractmethod
85
+ def _to_html(self, **kwargs) -> Html:
86
+ """Returns the HTML representation of the control."""
87
+
88
+ @classmethod
89
+ @contextlib.contextmanager
90
+ def track_scripts(cls) -> Iterator[List[str]]:
91
+ del cls
92
+ all_tracked = utils.thread_local_get(_TLS_TRACKED_SCRIPTS, [])
93
+ current = []
94
+ all_tracked.append(current)
95
+ utils.thread_local_set(_TLS_TRACKED_SCRIPTS, all_tracked)
96
+ try:
97
+ yield current
98
+ finally:
99
+ all_tracked.pop(-1)
100
+ if not all_tracked:
101
+ utils.thread_local_del(_TLS_TRACKED_SCRIPTS)
102
+
103
+ def _sync_members(self, **fields) -> None:
104
+ """Synchronizes displayed values to members."""
105
+ self.rebind(fields, skip_notification=True, raise_on_no_change=False)
106
+
107
+ def _run_javascript(self, code: str, debug: bool = False) -> None:
108
+ """Runs the given JavaScript code."""
109
+ if not self.interactive:
110
+ raise ValueError(
111
+ f'Non-interactive control {self} cannot be updated. '
112
+ 'Please set `interactive=True` in the constructor.'
113
+ )
114
+ if not self._rendered:
115
+ return
116
+
117
+ code = inspect.cleandoc(code)
118
+ if debug:
119
+ print('RUN JAVSCRIPT:\n', code)
120
+ if _notebook is not None:
121
+ _notebook.display(_notebook.Javascript(code))
122
+
123
+ # Track script execution.
124
+ all_tracked = utils.thread_local_get(_TLS_TRACKED_SCRIPTS, [])
125
+ for tracked in all_tracked:
126
+ tracked.append(code)
127
+
128
+ def _add_css_rules(self, css: str) -> None:
129
+ if not self._rendered or not css or css in self._dynamic_injected_css:
130
+ return
131
+ self._run_javascript(
132
+ f"""
133
+ const style = document.createElement('style');
134
+ style.type = 'text/css';
135
+ style.textContent = "{Html.escape(css, javascript_str=True)}";
136
+ document.head.appendChild(style);
137
+ """
138
+ )
139
+ self._dynamic_injected_css.add(css)
140
+
141
+ def _apply_css_rules(self, html: Html) -> None:
142
+ self._add_css_rules(html.styles.content)
143
+
144
+ def _insert_adjacent_html(
145
+ self,
146
+ element_selector_js: str,
147
+ html: Html,
148
+ var_name: str = 'elem',
149
+ position: str = 'beforeend'
150
+ ):
151
+ self._run_javascript(
152
+ f"""
153
+ {element_selector_js}
154
+ {var_name}.insertAdjacentHTML(
155
+ "{position}",
156
+ "{Html.escape(html, javascript_str=True).to_str(content_only=True)}"
157
+ );
158
+ """,
159
+ )
160
+ self._apply_css_rules(html)
161
+
162
+ def element_id(self, child: Optional[str] = None) -> Optional[str]:
163
+ """Returns the element id of this control or a child."""
164
+ if self.id is not None:
165
+ return self.id
166
+ elif not self.interactive:
167
+ return None
168
+ elif child is None:
169
+ return f'control-{id(self)}'
170
+ else:
171
+ return f'control-{id(self)}-{child}'
172
+
173
+ def _update_content(
174
+ self,
175
+ content: Union[str, Html],
176
+ child: Optional[str] = None,
177
+ ) -> None:
178
+ """Updates the content of the control."""
179
+ if isinstance(content, str):
180
+ return self._update_text(content, child)
181
+ else:
182
+ return self._update_inner_html(content, child)
183
+
184
+ def _update_text(
185
+ self,
186
+ content: str,
187
+ child: Optional[str] = None
188
+ ) -> str:
189
+ """Updates the content of the control."""
190
+ self._run_javascript(
191
+ f"""
192
+ elem = document.getElementById("{self.element_id(child)}");
193
+ elem.textContent = "{Html.escape(content, javascript_str=True)}";
194
+ """
195
+ )
196
+ return content
197
+
198
+ def _update_inner_html(
199
+ self,
200
+ html: base.Html,
201
+ child: Optional[str] = None,
202
+ ) -> base.Html:
203
+ """Updates the inner HTML of the control."""
204
+ self._run_javascript(
205
+ f"""
206
+ elem = document.getElementById("{self.element_id(child)}");
207
+ elem.innerHTML = "{Html.escape(html, javascript_str=True).to_str(content_only=True)}";
208
+ """
209
+ )
210
+ self._add_css_rules(html.styles.content)
211
+ return html
212
+
213
+ def _update_style(
214
+ self,
215
+ styles: Dict[str, Any],
216
+ *,
217
+ child: Optional[str] = None,
218
+ updates_only: bool = False,
219
+ ) -> Dict[str, Any]:
220
+ """Updates the style of the control."""
221
+ updated_styles = {} if updates_only else dict(self.styles or {})
222
+ updated_styles.update(styles)
223
+ self._run_javascript(
224
+ f"""
225
+ elem = document.getElementById("{self.element_id(child)}");
226
+ elem.style = "{Html.style_str(updated_styles)}";
227
+ """
228
+ )
229
+ return updated_styles
230
+
231
+ def _update_property(
232
+ self,
233
+ name: str,
234
+ value: str,
235
+ child: Optional[str] = None,
236
+ ) -> str:
237
+ """Updates a property of the control."""
238
+ self._run_javascript(
239
+ f"""
240
+ elem = document.getElementById("{self.element_id(child)}");
241
+ elem.{name} = "{value}";
242
+ """
243
+ )
244
+ return value
245
+
246
+ def _add_css_class(
247
+ self,
248
+ css_class: str,
249
+ child: Optional[str] = None,
250
+ ) -> str:
251
+ """Adds a CSS class to the control."""
252
+ self._run_javascript(
253
+ f"""
254
+ elem = document.getElementById("{self.element_id(child)}");
255
+ elem.classList.add("{css_class}");
256
+ """
257
+ )
258
+ return css_class
259
+
260
+ def _remove_css_class(
261
+ self,
262
+ css_class: str,
263
+ child: Optional[str] = None,
264
+ ) -> str:
265
+ """Removes a CSS class from the control."""
266
+ self._run_javascript(
267
+ f"""
268
+ elem = document.getElementById("{self.element_id(child)}");
269
+ elem.classList.remove("{css_class}");
270
+ """
271
+ )
272
+ return css_class
273
+
274
+
275
+ _TLS_TRACKED_SCRIPTS = '__tracked_scripts__'
@@ -0,0 +1,207 @@
1
+ # Copyright 2024 The PyGlove Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Html label control."""
15
+
16
+ from typing import Annotated, Any, Dict, List, Optional, Union
17
+
18
+ from pyglove.core import typing as pg_typing
19
+ from pyglove.core.symbolic import flags as pg_flags
20
+ from pyglove.core.symbolic import object as pg_object
21
+ # pylint: disable=g-importing-member
22
+ from pyglove.core.views.html.base import Html
23
+ from pyglove.core.views.html.controls.base import HtmlControl
24
+ from pyglove.core.views.html.controls.tooltip import Tooltip
25
+ # pylint: enable=g-importing-member
26
+
27
+
28
+ @pg_object.use_init_args(
29
+ ['text', 'tooltip', 'link', 'id', 'css_classes', 'styles']
30
+ )
31
+ class Label(HtmlControl):
32
+ """Html label."""
33
+
34
+ text: Annotated[
35
+ Union[str, Html],
36
+ 'The text or HTML content of the label.'
37
+ ]
38
+
39
+ tooltip: Annotated[
40
+ Optional[Tooltip],
41
+ '(Optional) The tooltip for the label.'
42
+ ] = None
43
+
44
+ link: Annotated[
45
+ Optional[str],
46
+ '(Optional) The URL for the link of the label.'
47
+ ] = None
48
+
49
+ target: Annotated[
50
+ Optional[str],
51
+ '(Optional) The target for the link of the label.'
52
+ ] = None
53
+
54
+ def _on_bound(self):
55
+ super()._on_bound()
56
+ if self.tooltip is not None:
57
+ self.tooltip.rebind(
58
+ for_element='.label',
59
+ interactive=self.tooltip.interactive or self.interactive,
60
+ notify_parents=False
61
+ )
62
+ self.add_style(
63
+ """
64
+ .label {
65
+ display: inline-block;
66
+ color: inherit;
67
+ padding: 5px;
68
+ }
69
+ .label-container {
70
+ display: inline-block;
71
+ }
72
+ """
73
+ )
74
+
75
+ def _to_html(self, **kwargs) -> Html:
76
+ text_elem = Html.element(
77
+ 'a' if self.link is not None else 'span',
78
+ [self.text],
79
+ id=self.element_id(),
80
+ href=self.link,
81
+ css_classes=['label'] + self.css_classes,
82
+ styles=self.styles,
83
+ target=self.target,
84
+ )
85
+ if self.tooltip is None:
86
+ return text_elem
87
+ return Html.element(
88
+ 'div',
89
+ [text_elem, self.tooltip],
90
+ css_classes=['label-container'],
91
+ )
92
+
93
+ def update(
94
+ self,
95
+ text: Union[str, Html, None] = None,
96
+ tooltip: Union[str, Html, None] = None,
97
+ link: Optional[str] = None,
98
+ styles: Optional[Dict[str, Any]] = None,
99
+ add_class: Optional[List[str]] = None,
100
+ remove_class: Optional[List[str]] = None,
101
+ ) -> None:
102
+ if text is not None:
103
+ self._sync_members(text=self._update_content(text))
104
+ if styles:
105
+ self._sync_members(styles=self._update_style(styles))
106
+ if link is not None:
107
+ self._sync_members(link=self._update_property('href', link))
108
+ if tooltip is not None:
109
+ self.tooltip.update(content=tooltip)
110
+ if add_class or remove_class:
111
+ css_classes = list(self.css_classes)
112
+ for x in add_class or []:
113
+ self._add_css_class(x)
114
+ css_classes.append(x)
115
+ for x in remove_class or []:
116
+ self._remove_css_class(x)
117
+ if x in css_classes:
118
+ css_classes.remove(x)
119
+ self._sync_members(css_classes=css_classes)
120
+
121
+ # Register converter for automatic conversion.
122
+ pg_typing.register_converter(str, Label, Label)
123
+ pg_typing.register_converter(Html, Label, Label)
124
+
125
+
126
+ class Badge(Label):
127
+ """A badge."""
128
+
129
+ def _on_bound(self):
130
+ super()._on_bound()
131
+ with pg_flags.notify_on_change(False):
132
+ self.css_classes.append('badge')
133
+ self.add_style(
134
+ """
135
+ .badge {
136
+ background-color: #EEE;
137
+ border-radius: 5px;
138
+ color: #777;
139
+ }
140
+ """
141
+ )
142
+
143
+
144
+ @pg_object.use_init_args(
145
+ ['labels', 'name', 'id', 'css_classes', 'styles']
146
+ )
147
+ class LabelGroup(HtmlControl):
148
+ """Label group."""
149
+
150
+ labels: Annotated[
151
+ List[Label],
152
+ 'The labels in the group.'
153
+ ]
154
+
155
+ name: Annotated[
156
+ Optional[Label],
157
+ 'The label for the name of the group.'
158
+ ] = None
159
+
160
+ def _on_bound(self):
161
+ super()._on_bound()
162
+ with pg_flags.notify_on_change(False):
163
+ if self.name is not None:
164
+ self.name.rebind(
165
+ interactive=self.interactive or self.name.interactive,
166
+ raise_on_no_change=False
167
+ )
168
+ self.name.css_classes.append('group-name')
169
+ for label in self.labels:
170
+ label.css_classes.append('group-value')
171
+ label.rebind(
172
+ interactive=self.interactive or label.interactive,
173
+ raise_on_no_change=False,
174
+ )
175
+
176
+ def _to_html(self, **kwargs) -> Html:
177
+ return Html.element(
178
+ 'div',
179
+ [self.name] + self.labels,
180
+ id=self.id or None,
181
+ css_classes=['label-group'] + self.css_classes,
182
+ styles=self.styles,
183
+ ).add_style(
184
+ """
185
+ .label-group {
186
+ display: inline-flex;
187
+ overflow: hidden;
188
+ border-radius: 5px;
189
+ border: 1px solid #DDD;
190
+ padding: 0px;
191
+ margin: 5px
192
+ }
193
+ .label-group .group-name {
194
+ padding: 5px;
195
+ color: white;
196
+ background-color: dodgerblue;
197
+ }
198
+ .label-group .group-name > .text {
199
+ color: white;
200
+ }
201
+ .label-group .group-value {
202
+ }
203
+ .label-group .badge {
204
+ border-radius: 0px;
205
+ }
206
+ """
207
+ )
@@ -0,0 +1,157 @@
1
+ # Copyright 2024 The PyGlove Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ import inspect
15
+ import unittest
16
+
17
+ from pyglove.core import symbolic # pylint: disable=unused-import
18
+ from pyglove.core.views.html.controls import label as label_lib
19
+
20
+
21
+ class TestCase(unittest.TestCase):
22
+
23
+ def assert_html_content(self, control, expected):
24
+ expected = inspect.cleandoc(expected).strip()
25
+ actual = control.to_html().content.strip()
26
+ if actual != expected:
27
+ print(actual)
28
+ self.assertEqual(actual, expected)
29
+
30
+
31
+ class LabelTest(TestCase):
32
+
33
+ def test_text_only(self):
34
+ label = label_lib.Label('foo')
35
+ self.assertIsNone(label.tooltip)
36
+ self.assertIsNone(label.link)
37
+ self.assert_html_content(label, '<span class="label">foo</span>')
38
+ with self.assertRaisesRegex(ValueError, 'Non-interactive .*'):
39
+ label.update('bar')
40
+
41
+ def test_with_link(self):
42
+ label = label_lib.Label('foo', link='http://google.com', id='my-foo')
43
+ self.assertEqual(label.element_id(), 'my-foo')
44
+ self.assertIsNone(label.tooltip)
45
+ self.assertEqual(label.link, 'http://google.com')
46
+ self.assert_html_content(
47
+ label, '<a class="label" id="my-foo" href="http://google.com">foo</a>'
48
+ )
49
+
50
+ def test_with_tooltip(self):
51
+ label = label_lib.Label('foo', tooltip='bar')
52
+ self.assertEqual(label.tooltip.for_element, '.label')
53
+ self.assertIsNone(label.link)
54
+ self.assert_html_content(
55
+ label,
56
+ (
57
+ '<div class="label-container"><span class="label">foo</span>'
58
+ '<span class="tooltip">bar</span></div>'
59
+ )
60
+ )
61
+
62
+ def test_update(self):
63
+ label = label_lib.Label(
64
+ 'foo', 'bar', 'http://google.com',
65
+ interactive=True,
66
+ css_classes=['foo', 'bar'],
67
+ )
68
+ self.assertIn('id="control-', label.to_html_str(content_only=True))
69
+ with label.track_scripts() as scripts:
70
+ label.update(
71
+ 'bar',
72
+ tooltip='baz',
73
+ link='http://www.yahoo.com',
74
+ styles=dict(color='red'),
75
+ add_class=['baz'],
76
+ remove_class=['bar'],
77
+ )
78
+ self.assertEqual(label.text, 'bar')
79
+ self.assertEqual(label.tooltip.content, 'baz')
80
+ self.assertEqual(label.link, 'http://www.yahoo.com')
81
+ self.assertEqual(label.styles, dict(color='red'))
82
+ self.assertEqual(label.css_classes, ['foo', 'baz'])
83
+ self.assertEqual(
84
+ scripts,
85
+ [
86
+ inspect.cleandoc(
87
+ f"""
88
+ elem = document.getElementById("{label.element_id()}");
89
+ elem.textContent = "bar";
90
+ """
91
+ ),
92
+ inspect.cleandoc(
93
+ f"""
94
+ elem = document.getElementById("{label.element_id()}");
95
+ elem.style = "color:red;";
96
+ """
97
+ ),
98
+ inspect.cleandoc(
99
+ f"""
100
+ elem = document.getElementById("{label.element_id()}");
101
+ elem.href = "http://www.yahoo.com";
102
+ """
103
+ ),
104
+ inspect.cleandoc(
105
+ f"""
106
+ elem = document.getElementById("{label.tooltip.element_id()}");
107
+ elem.textContent = "baz";
108
+ """
109
+ ),
110
+ inspect.cleandoc(
111
+ f"""
112
+ elem = document.getElementById("{label.tooltip.element_id()}");
113
+ elem.classList.remove("html-content");
114
+ """
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
+ ),
128
+ ]
129
+ )
130
+
131
+ def test_badge(self):
132
+ badge = label_lib.Badge('foo')
133
+ self.assert_html_content(badge, '<span class="label badge">foo</span>')
134
+
135
+
136
+ class LabelGroupTest(TestCase):
137
+
138
+ def test_basic(self):
139
+ group = label_lib.LabelGroup(['foo', 'bar'], name='baz')
140
+ self.assert_html_content(
141
+ group,
142
+ (
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>'
146
+ )
147
+ )
148
+
149
+ def test_interactive(self):
150
+ group = label_lib.LabelGroup(['foo', 'bar'], name='baz', interactive=True)
151
+ self.assertTrue(group.name.interactive)
152
+ self.assertTrue(group.labels[0].interactive)
153
+ self.assertTrue(group.labels[1].interactive)
154
+
155
+
156
+ if __name__ == '__main__':
157
+ unittest.main()