pyglove 0.4.5.dev202411030808__py3-none-any.whl → 0.4.5.dev202411060809__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.
Potentially problematic release.
This version of pyglove might be problematic. Click here for more details.
- pyglove/core/__init__.py +8 -0
- pyglove/core/symbolic/base.py +2 -38
- pyglove/core/symbolic/diff.py +14 -13
- pyglove/core/symbolic/object_test.py +1 -0
- pyglove/core/symbolic/ref.py +7 -7
- pyglove/core/views/__init__.py +1 -0
- pyglove/core/views/base.py +14 -8
- pyglove/core/views/base_test.py +1 -1
- pyglove/core/views/html/__init__.py +1 -1
- pyglove/core/views/html/base.py +53 -79
- pyglove/core/views/html/base_test.py +17 -4
- pyglove/core/views/html/controls/__init__.py +35 -0
- pyglove/core/views/html/controls/base.py +238 -0
- pyglove/core/views/html/controls/label.py +190 -0
- pyglove/core/views/html/controls/label_test.py +138 -0
- pyglove/core/views/html/controls/progress_bar.py +185 -0
- pyglove/core/views/html/controls/progress_bar_test.py +97 -0
- pyglove/core/views/html/controls/tab.py +169 -0
- pyglove/core/views/html/controls/tab_test.py +54 -0
- pyglove/core/views/html/controls/tooltip.py +99 -0
- pyglove/core/views/html/controls/tooltip_test.py +99 -0
- pyglove/core/views/html/tree_view.py +24 -6
- pyglove/core/views/html/tree_view_test.py +7 -2
- {pyglove-0.4.5.dev202411030808.dist-info → pyglove-0.4.5.dev202411060809.dist-info}/METADATA +1 -1
- {pyglove-0.4.5.dev202411030808.dist-info → pyglove-0.4.5.dev202411060809.dist-info}/RECORD +28 -18
- {pyglove-0.4.5.dev202411030808.dist-info → pyglove-0.4.5.dev202411060809.dist-info}/LICENSE +0 -0
- {pyglove-0.4.5.dev202411030808.dist-info → pyglove-0.4.5.dev202411060809.dist-info}/WHEEL +0 -0
- {pyglove-0.4.5.dev202411030808.dist-info → pyglove-0.4.5.dev202411060809.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,238 @@
|
|
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 object_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._scripts = []
|
66
|
+
|
67
|
+
def add_style(self, *css: str) -> 'HtmlControl':
|
68
|
+
"""Adds CSS styles to the HTML."""
|
69
|
+
self._css_styles.extend(css)
|
70
|
+
return self
|
71
|
+
|
72
|
+
def add_script(self, *scripts: str) -> 'HtmlControl':
|
73
|
+
self._scripts.extend(scripts)
|
74
|
+
return self
|
75
|
+
|
76
|
+
def to_html(self, **kwargs) -> Html:
|
77
|
+
"""Returns the HTML representation of the control."""
|
78
|
+
self._rendered = True
|
79
|
+
html = self._to_html(**kwargs)
|
80
|
+
return html.add_style(*self._css_styles).add_script(*self._scripts)
|
81
|
+
|
82
|
+
@abc.abstractmethod
|
83
|
+
def _to_html(self, **kwargs) -> Html:
|
84
|
+
"""Returns the HTML representation of the control."""
|
85
|
+
|
86
|
+
@classmethod
|
87
|
+
@contextlib.contextmanager
|
88
|
+
def track_scripts(cls) -> Iterator[List[str]]:
|
89
|
+
del cls
|
90
|
+
all_tracked = object_utils.thread_local_get(_TLS_TRACKED_SCRIPTS, [])
|
91
|
+
current = []
|
92
|
+
all_tracked.append(current)
|
93
|
+
object_utils.thread_local_set(_TLS_TRACKED_SCRIPTS, all_tracked)
|
94
|
+
try:
|
95
|
+
yield current
|
96
|
+
finally:
|
97
|
+
all_tracked.pop(-1)
|
98
|
+
if not all_tracked:
|
99
|
+
object_utils.thread_local_del(_TLS_TRACKED_SCRIPTS)
|
100
|
+
|
101
|
+
def _sync_members(self, **fields) -> None:
|
102
|
+
"""Synchronizes displayed values to members."""
|
103
|
+
self.rebind(fields, skip_notification=True, raise_on_no_change=False)
|
104
|
+
|
105
|
+
def _run_javascript(self, code: str) -> None:
|
106
|
+
"""Runs the given JavaScript code."""
|
107
|
+
if not self.interactive:
|
108
|
+
raise ValueError(
|
109
|
+
f'Non-interactive control {self} cannot be updated. '
|
110
|
+
'Please set `interactive=True` in the constructor.'
|
111
|
+
)
|
112
|
+
if not self._rendered:
|
113
|
+
return
|
114
|
+
|
115
|
+
code = inspect.cleandoc(code)
|
116
|
+
if _notebook is not None:
|
117
|
+
_notebook.display(_notebook.Javascript(code))
|
118
|
+
|
119
|
+
# Track script execution.
|
120
|
+
all_tracked = object_utils.thread_local_get(_TLS_TRACKED_SCRIPTS, [])
|
121
|
+
for tracked in all_tracked:
|
122
|
+
tracked.append(code)
|
123
|
+
|
124
|
+
def element_id(self, child: Optional[str] = None) -> Optional[str]:
|
125
|
+
"""Returns the element id of this control or a child."""
|
126
|
+
if self.id is not None:
|
127
|
+
return self.id
|
128
|
+
elif not self.interactive:
|
129
|
+
return None
|
130
|
+
elif child is None:
|
131
|
+
return f'control-{id(self)}'
|
132
|
+
else:
|
133
|
+
return f'control-{id(self)}-{child}'
|
134
|
+
|
135
|
+
def _update_content(
|
136
|
+
self,
|
137
|
+
content: Union[str, Html],
|
138
|
+
child: Optional[str] = None,
|
139
|
+
) -> None:
|
140
|
+
"""Updates the content of the control."""
|
141
|
+
if isinstance(content, str):
|
142
|
+
return self._update_text(content, child)
|
143
|
+
else:
|
144
|
+
return self._update_inner_html(content, child)
|
145
|
+
|
146
|
+
def _update_text(
|
147
|
+
self,
|
148
|
+
content: str,
|
149
|
+
child: Optional[str] = None
|
150
|
+
) -> str:
|
151
|
+
"""Updates the content of the control."""
|
152
|
+
self._run_javascript(
|
153
|
+
f"""
|
154
|
+
elem = document.getElementById("{self.element_id(child)}");
|
155
|
+
elem.textContent = "{Html.escape(content, javascript_str=True)}";
|
156
|
+
"""
|
157
|
+
)
|
158
|
+
return content
|
159
|
+
|
160
|
+
def _update_inner_html(
|
161
|
+
self,
|
162
|
+
html: base.Html,
|
163
|
+
child: Optional[str] = None,
|
164
|
+
) -> base.Html:
|
165
|
+
"""Updates the inner HTML of the control."""
|
166
|
+
js_html = Html.escape(html, javascript_str=True)
|
167
|
+
assert isinstance(js_html, Html), js_html
|
168
|
+
self._run_javascript(
|
169
|
+
f"""
|
170
|
+
elem = document.getElementById("{self.element_id(child)}");
|
171
|
+
elem.innerHTML = "{js_html.to_str(content_only=True)}";
|
172
|
+
"""
|
173
|
+
)
|
174
|
+
return html
|
175
|
+
|
176
|
+
def _update_style(
|
177
|
+
self,
|
178
|
+
styles: Dict[str, Any],
|
179
|
+
*,
|
180
|
+
child: Optional[str] = None,
|
181
|
+
updates_only: bool = False,
|
182
|
+
) -> Dict[str, Any]:
|
183
|
+
"""Updates the style of the control."""
|
184
|
+
updated_styles = {} if updates_only else dict(self.styles or {})
|
185
|
+
updated_styles.update(styles)
|
186
|
+
self._run_javascript(
|
187
|
+
f"""
|
188
|
+
elem = document.getElementById("{self.element_id(child)}");
|
189
|
+
elem.style = "{Html.style_str(updated_styles)}";
|
190
|
+
"""
|
191
|
+
)
|
192
|
+
return updated_styles
|
193
|
+
|
194
|
+
def _update_property(
|
195
|
+
self,
|
196
|
+
name: str,
|
197
|
+
value: str,
|
198
|
+
child: Optional[str] = None,
|
199
|
+
) -> str:
|
200
|
+
"""Updates a property of the control."""
|
201
|
+
self._run_javascript(
|
202
|
+
f"""
|
203
|
+
elem = document.getElementById("{self.element_id(child)}");
|
204
|
+
elem.{name} = "{value}";
|
205
|
+
"""
|
206
|
+
)
|
207
|
+
return value
|
208
|
+
|
209
|
+
def _add_css_class(
|
210
|
+
self,
|
211
|
+
css_class: str,
|
212
|
+
child: Optional[str] = None,
|
213
|
+
) -> str:
|
214
|
+
"""Adds a CSS class to the control."""
|
215
|
+
self._run_javascript(
|
216
|
+
f"""
|
217
|
+
elem = document.getElementById("{self.element_id(child)}");
|
218
|
+
elem.classList.add("{css_class}");
|
219
|
+
"""
|
220
|
+
)
|
221
|
+
return css_class
|
222
|
+
|
223
|
+
def _remove_css_class(
|
224
|
+
self,
|
225
|
+
css_class: str,
|
226
|
+
child: Optional[str] = None,
|
227
|
+
) -> str:
|
228
|
+
"""Removes a CSS class from the control."""
|
229
|
+
self._run_javascript(
|
230
|
+
f"""
|
231
|
+
elem = document.getElementById("{self.element_id(child)}");
|
232
|
+
elem.classList.remove("{css_class}");
|
233
|
+
"""
|
234
|
+
)
|
235
|
+
return css_class
|
236
|
+
|
237
|
+
|
238
|
+
_TLS_TRACKED_SCRIPTS = '__tracked_scripts__'
|
@@ -0,0 +1,190 @@
|
|
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
|
+
def _on_bound(self):
|
50
|
+
super()._on_bound()
|
51
|
+
if self.tooltip is not None:
|
52
|
+
self.tooltip.rebind(
|
53
|
+
for_element='.label',
|
54
|
+
interactive=self.tooltip.interactive or self.interactive,
|
55
|
+
notify_parents=False
|
56
|
+
)
|
57
|
+
self.add_style(
|
58
|
+
"""
|
59
|
+
.label {
|
60
|
+
display: inline-block;
|
61
|
+
color: inherit;
|
62
|
+
padding: 5px;
|
63
|
+
}
|
64
|
+
.label-container {
|
65
|
+
display: inline-block;
|
66
|
+
}
|
67
|
+
"""
|
68
|
+
)
|
69
|
+
|
70
|
+
def _to_html(self, **kwargs) -> Html:
|
71
|
+
text_elem = Html.element(
|
72
|
+
'a',
|
73
|
+
[self.text],
|
74
|
+
id=self.element_id(),
|
75
|
+
href=self.link,
|
76
|
+
css_classes=['label'] + self.css_classes,
|
77
|
+
styles=self.styles,
|
78
|
+
)
|
79
|
+
if self.tooltip is None:
|
80
|
+
return text_elem
|
81
|
+
return Html.element(
|
82
|
+
'div',
|
83
|
+
[text_elem, self.tooltip],
|
84
|
+
css_classes=['label-container'],
|
85
|
+
)
|
86
|
+
|
87
|
+
def update(
|
88
|
+
self,
|
89
|
+
text: Union[str, Html, None] = None,
|
90
|
+
tooltip: Union[str, Html, None] = None,
|
91
|
+
link: Optional[str] = None,
|
92
|
+
styles: Optional[Dict[str, Any]] = None,
|
93
|
+
) -> None:
|
94
|
+
if text is not None:
|
95
|
+
self._sync_members(text=self._update_content(text))
|
96
|
+
if styles:
|
97
|
+
self._sync_members(styles=self._update_style(styles))
|
98
|
+
if link is not None:
|
99
|
+
self._sync_members(link=self._update_property('href', link))
|
100
|
+
if tooltip is not None:
|
101
|
+
self.tooltip.update(content=tooltip)
|
102
|
+
|
103
|
+
|
104
|
+
# Register converter for automatic conversion.
|
105
|
+
pg_typing.register_converter(str, Label, Label)
|
106
|
+
pg_typing.register_converter(Html, Label, Label)
|
107
|
+
|
108
|
+
|
109
|
+
class Badge(Label):
|
110
|
+
"""A badge."""
|
111
|
+
|
112
|
+
def _on_bound(self):
|
113
|
+
super()._on_bound()
|
114
|
+
with pg_flags.notify_on_change(False):
|
115
|
+
self.css_classes.append('badge')
|
116
|
+
self.add_style(
|
117
|
+
"""
|
118
|
+
.badge {
|
119
|
+
background-color: #EEE;
|
120
|
+
border-radius: 5px;
|
121
|
+
color: #777;
|
122
|
+
}
|
123
|
+
"""
|
124
|
+
)
|
125
|
+
|
126
|
+
|
127
|
+
@pg_object.use_init_args(
|
128
|
+
['labels', 'name', 'id', 'css_classes', 'styles']
|
129
|
+
)
|
130
|
+
class LabelGroup(HtmlControl):
|
131
|
+
"""Label group."""
|
132
|
+
|
133
|
+
labels: Annotated[
|
134
|
+
List[Label],
|
135
|
+
'The labels in the group.'
|
136
|
+
]
|
137
|
+
|
138
|
+
name: Annotated[
|
139
|
+
Optional[Label],
|
140
|
+
'The label for the name of the group.'
|
141
|
+
] = None
|
142
|
+
|
143
|
+
def _on_bound(self):
|
144
|
+
super()._on_bound()
|
145
|
+
with pg_flags.notify_on_change(False):
|
146
|
+
if self.name is not None:
|
147
|
+
self.name.rebind(
|
148
|
+
interactive=self.interactive or self.name.interactive,
|
149
|
+
raise_on_no_change=False
|
150
|
+
)
|
151
|
+
self.name.css_classes.append('group-name')
|
152
|
+
for label in self.labels:
|
153
|
+
label.css_classes.append('group-value')
|
154
|
+
label.rebind(
|
155
|
+
interactive=self.interactive or label.interactive,
|
156
|
+
raise_on_no_change=False,
|
157
|
+
)
|
158
|
+
|
159
|
+
def _to_html(self, **kwargs) -> Html:
|
160
|
+
return Html.element(
|
161
|
+
'div',
|
162
|
+
[self.name] + self.labels,
|
163
|
+
id=self.id or None,
|
164
|
+
css_classes=['label-group'] + self.css_classes,
|
165
|
+
styles=self.styles,
|
166
|
+
).add_style(
|
167
|
+
"""
|
168
|
+
.label-group {
|
169
|
+
display: inline-flex;
|
170
|
+
overflow: hidden;
|
171
|
+
border-radius: 5px;
|
172
|
+
border: 1px solid #DDD;
|
173
|
+
padding: 0px;
|
174
|
+
margin: 5px
|
175
|
+
}
|
176
|
+
.label-group .group-name {
|
177
|
+
padding: 5px;
|
178
|
+
color: white;
|
179
|
+
background-color: dodgerblue;
|
180
|
+
}
|
181
|
+
.label-group .group-name > .text {
|
182
|
+
color: white;
|
183
|
+
}
|
184
|
+
.label-group .group-value {
|
185
|
+
}
|
186
|
+
.label-group .badge {
|
187
|
+
border-radius: 0px;
|
188
|
+
}
|
189
|
+
"""
|
190
|
+
)
|
@@ -0,0 +1,138 @@
|
|
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, '<a class="label">foo</a>')
|
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"><a class="label">foo</a>'
|
58
|
+
'<span class="tooltip">bar</span></div>'
|
59
|
+
)
|
60
|
+
)
|
61
|
+
|
62
|
+
def test_update(self):
|
63
|
+
label = label_lib.Label('foo', 'bar', 'http://google.com', interactive=True)
|
64
|
+
self.assertIn('id="control-', label.to_html_str(content_only=True))
|
65
|
+
with label.track_scripts() as scripts:
|
66
|
+
label.update(
|
67
|
+
'bar',
|
68
|
+
tooltip='baz',
|
69
|
+
link='http://www.yahoo.com',
|
70
|
+
styles=dict(color='red')
|
71
|
+
)
|
72
|
+
self.assertEqual(label.text, 'bar')
|
73
|
+
self.assertEqual(label.tooltip.content, 'baz')
|
74
|
+
self.assertEqual(label.link, 'http://www.yahoo.com')
|
75
|
+
self.assertEqual(label.styles, dict(color='red'))
|
76
|
+
self.assertEqual(
|
77
|
+
scripts,
|
78
|
+
[
|
79
|
+
inspect.cleandoc(
|
80
|
+
f"""
|
81
|
+
elem = document.getElementById("{label.element_id()}");
|
82
|
+
elem.textContent = "bar";
|
83
|
+
"""
|
84
|
+
),
|
85
|
+
inspect.cleandoc(
|
86
|
+
f"""
|
87
|
+
elem = document.getElementById("{label.element_id()}");
|
88
|
+
elem.style = "color:red;";
|
89
|
+
"""
|
90
|
+
),
|
91
|
+
inspect.cleandoc(
|
92
|
+
f"""
|
93
|
+
elem = document.getElementById("{label.element_id()}");
|
94
|
+
elem.href = "http://www.yahoo.com";
|
95
|
+
"""
|
96
|
+
),
|
97
|
+
inspect.cleandoc(
|
98
|
+
f"""
|
99
|
+
elem = document.getElementById("{label.tooltip.element_id()}");
|
100
|
+
elem.textContent = "baz";
|
101
|
+
"""
|
102
|
+
),
|
103
|
+
inspect.cleandoc(
|
104
|
+
f"""
|
105
|
+
elem = document.getElementById("{label.tooltip.element_id()}");
|
106
|
+
elem.classList.remove("html-content");
|
107
|
+
"""
|
108
|
+
),
|
109
|
+
]
|
110
|
+
)
|
111
|
+
|
112
|
+
def test_badge(self):
|
113
|
+
badge = label_lib.Badge('foo')
|
114
|
+
self.assert_html_content(badge, '<a class="label badge">foo</a>')
|
115
|
+
|
116
|
+
|
117
|
+
class LabelGroupTest(TestCase):
|
118
|
+
|
119
|
+
def test_basic(self):
|
120
|
+
group = label_lib.LabelGroup(['foo', 'bar'], name='baz')
|
121
|
+
self.assert_html_content(
|
122
|
+
group,
|
123
|
+
(
|
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>'
|
127
|
+
)
|
128
|
+
)
|
129
|
+
|
130
|
+
def test_interactive(self):
|
131
|
+
group = label_lib.LabelGroup(['foo', 'bar'], name='baz', interactive=True)
|
132
|
+
self.assertTrue(group.name.interactive)
|
133
|
+
self.assertTrue(group.labels[0].interactive)
|
134
|
+
self.assertTrue(group.labels[1].interactive)
|
135
|
+
|
136
|
+
|
137
|
+
if __name__ == '__main__':
|
138
|
+
unittest.main()
|