pyglove 0.4.5.dev20240319__py3-none-any.whl → 0.4.5.dev202501140808__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 +54 -20
- 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 +309 -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 +54 -41
- pyglove/core/geno/base_test.py +2 -4
- pyglove/core/geno/categorical.py +37 -28
- pyglove/core/geno/custom.py +19 -16
- pyglove/core/geno/numerical.py +20 -17
- pyglove/core/geno/space.py +4 -5
- pyglove/core/hyper/base.py +6 -6
- pyglove/core/hyper/categorical.py +94 -55
- 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 +2 -4
- pyglove/core/hyper/dynamic_evaluation.py +5 -6
- 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/io/__init__.py +1 -0
- pyglove/core/io/file_system.py +17 -7
- pyglove/core/io/file_system_test.py +2 -0
- pyglove/core/io/sequence.py +299 -0
- pyglove/core/io/sequence_test.py +124 -0
- 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 +17 -5
- pyglove/core/patching/rule_based_test.py +27 -4
- pyglove/core/symbolic/__init__.py +2 -7
- pyglove/core/symbolic/base.py +320 -183
- pyglove/core/symbolic/base_test.py +123 -19
- pyglove/core/symbolic/boilerplate.py +7 -13
- pyglove/core/symbolic/boilerplate_test.py +25 -23
- pyglove/core/symbolic/class_wrapper.py +48 -45
- pyglove/core/symbolic/class_wrapper_test.py +2 -2
- pyglove/core/symbolic/compounding.py +9 -15
- pyglove/core/symbolic/compounding_test.py +2 -4
- pyglove/core/symbolic/dict.py +154 -110
- pyglove/core/symbolic/dict_test.py +238 -130
- pyglove/core/symbolic/diff.py +199 -10
- pyglove/core/symbolic/diff_test.py +226 -0
- pyglove/core/symbolic/flags.py +1 -1
- pyglove/core/symbolic/functor.py +29 -26
- pyglove/core/symbolic/functor_test.py +102 -50
- pyglove/core/symbolic/inferred.py +2 -2
- pyglove/core/symbolic/list.py +81 -50
- pyglove/core/symbolic/list_test.py +119 -97
- pyglove/core/symbolic/object.py +225 -113
- pyglove/core/symbolic/object_test.py +320 -108
- pyglove/core/symbolic/origin.py +17 -14
- pyglove/core/symbolic/origin_test.py +4 -2
- pyglove/core/symbolic/pure_symbolic.py +4 -3
- pyglove/core/symbolic/ref.py +108 -21
- pyglove/core/symbolic/ref_test.py +93 -0
- pyglove/core/symbolic/symbolize_test.py +10 -2
- pyglove/core/tuning/local_backend.py +2 -2
- pyglove/core/tuning/protocols.py +3 -3
- pyglove/core/tuning/sample_test.py +3 -3
- pyglove/core/typing/__init__.py +14 -5
- pyglove/core/typing/annotation_conversion.py +43 -27
- pyglove/core/typing/annotation_conversion_test.py +23 -0
- pyglove/core/typing/callable_ext.py +241 -3
- pyglove/core/typing/callable_ext_test.py +255 -0
- pyglove/core/typing/callable_signature.py +510 -66
- pyglove/core/typing/callable_signature_test.py +619 -99
- pyglove/core/typing/class_schema.py +229 -154
- pyglove/core/typing/class_schema_test.py +149 -95
- pyglove/core/typing/custom_typing.py +5 -4
- pyglove/core/typing/inspect.py +63 -0
- pyglove/core/typing/inspect_test.py +39 -0
- pyglove/core/typing/key_specs.py +10 -11
- pyglove/core/typing/key_specs_test.py +7 -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 +604 -362
- pyglove/core/typing/value_specs_test.py +328 -90
- pyglove/core/utils/__init__.py +164 -0
- pyglove/core/{object_utils → utils}/common_traits.py +3 -67
- pyglove/core/utils/common_traits_test.py +36 -0
- pyglove/core/{object_utils → utils}/docstr_utils.py +23 -0
- pyglove/core/{object_utils → utils}/docstr_utils_test.py +36 -4
- pyglove/core/{object_utils → utils}/error_utils.py +78 -9
- pyglove/core/{object_utils → utils}/error_utils_test.py +61 -5
- pyglove/core/utils/formatting.py +464 -0
- pyglove/core/utils/formatting_test.py +453 -0
- 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 +177 -52
- pyglove/core/{object_utils → utils}/json_conversion_test.py +97 -16
- pyglove/core/{object_utils → utils}/missing.py +3 -3
- 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/utils/timing.py +236 -0
- pyglove/core/utils/timing_test.py +154 -0
- pyglove/core/{object_utils → utils}/value_location.py +275 -6
- pyglove/core/utils/value_location_test.py +707 -0
- pyglove/core/views/__init__.py +32 -0
- pyglove/core/views/base.py +804 -0
- pyglove/core/views/base_test.py +580 -0
- pyglove/core/views/html/__init__.py +27 -0
- pyglove/core/views/html/base.py +547 -0
- pyglove/core/views/html/base_test.py +830 -0
- pyglove/core/views/html/controls/__init__.py +35 -0
- pyglove/core/views/html/controls/base.py +275 -0
- pyglove/core/views/html/controls/label.py +207 -0
- pyglove/core/views/html/controls/label_test.py +157 -0
- pyglove/core/views/html/controls/progress_bar.py +183 -0
- pyglove/core/views/html/controls/progress_bar_test.py +97 -0
- pyglove/core/views/html/controls/tab.py +320 -0
- pyglove/core/views/html/controls/tab_test.py +87 -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 +1517 -0
- pyglove/core/views/html/tree_view_test.py +1461 -0
- {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501140808.dist-info}/METADATA +18 -4
- pyglove-0.4.5.dev202501140808.dist-info/RECORD +214 -0
- {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501140808.dist-info}/WHEEL +1 -1
- pyglove/core/object_utils/__init__.py +0 -154
- pyglove/core/object_utils/common_traits_test.py +0 -82
- pyglove/core/object_utils/formatting.py +0 -234
- pyglove/core/object_utils/formatting_test.py +0 -223
- pyglove/core/object_utils/value_location_test.py +0 -385
- pyglove/core/symbolic/schema_utils.py +0 -327
- pyglove/core/symbolic/schema_utils_test.py +0 -57
- pyglove/core/typing/class_schema_utils.py +0 -202
- pyglove/core/typing/class_schema_utils_test.py +0 -194
- pyglove-0.4.5.dev20240319.dist-info/RECORD +0 -185
- /pyglove/core/{object_utils → utils}/thread_local.py +0 -0
- {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501140808.dist-info}/LICENSE +0 -0
- {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501140808.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,547 @@
|
|
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 and the base HtmlView."""
|
15
|
+
|
16
|
+
import abc
|
17
|
+
import functools
|
18
|
+
import html as html_lib
|
19
|
+
import inspect
|
20
|
+
import typing
|
21
|
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Union
|
22
|
+
|
23
|
+
from pyglove.core import typing as pg_typing
|
24
|
+
from pyglove.core import utils
|
25
|
+
from pyglove.core.views import base
|
26
|
+
|
27
|
+
NestableStr = Union[
|
28
|
+
str,
|
29
|
+
Sequence[Union[str, None, Sequence[Optional[str]]]],
|
30
|
+
None,
|
31
|
+
]
|
32
|
+
|
33
|
+
NodeFilter = base.NodeFilter
|
34
|
+
NodeColor = Callable[
|
35
|
+
[
|
36
|
+
utils.KeyPath, # The path to the value.
|
37
|
+
Any, # Current value.
|
38
|
+
Any, # Parent value
|
39
|
+
],
|
40
|
+
Optional[str], # The color of the node.
|
41
|
+
]
|
42
|
+
|
43
|
+
|
44
|
+
class Html(base.Content):
|
45
|
+
"""HTML with consolidated CSS and Scripts.
|
46
|
+
|
47
|
+
Example::
|
48
|
+
|
49
|
+
.. code-block:: python
|
50
|
+
|
51
|
+
def foo() -> pg.Html:
|
52
|
+
s = pg.Html()
|
53
|
+
s.add_style('div.foo { color: red; }')
|
54
|
+
s.add_script('function myFoo() { console.log("foo");}')
|
55
|
+
s.write('<div class="foo">Foo</div>')
|
56
|
+
return s
|
57
|
+
|
58
|
+
def bar() -> pg.Html:
|
59
|
+
s = pg.Html()
|
60
|
+
s.add_style('div.bar { color: green; }')
|
61
|
+
s.add_script('function myBar() { console.log("bar");}')
|
62
|
+
s.write('<div class="bar">')
|
63
|
+
s.write(foo())
|
64
|
+
s.write('</div>')
|
65
|
+
return s
|
66
|
+
|
67
|
+
html = bar.html_str()
|
68
|
+
|
69
|
+
This will output::
|
70
|
+
|
71
|
+
<html>
|
72
|
+
<head>
|
73
|
+
<style>
|
74
|
+
div.bar { color: green; }
|
75
|
+
div.foo { color: red; }
|
76
|
+
</style>
|
77
|
+
<script>
|
78
|
+
function myBar() { console.log("bar");}
|
79
|
+
function myFoo() { console.log("foo");}
|
80
|
+
</script>
|
81
|
+
</head>
|
82
|
+
<body><div class="bar"><div class="foo">Foo</div></div></body></html>
|
83
|
+
|
84
|
+
"""
|
85
|
+
|
86
|
+
WritableTypes = Union[ # pylint: disable=invalid-name
|
87
|
+
str,
|
88
|
+
'Html',
|
89
|
+
'HtmlConvertible',
|
90
|
+
Callable[[], Union[str, 'Html', 'HtmlConvertible', None]],
|
91
|
+
None
|
92
|
+
]
|
93
|
+
|
94
|
+
class Scripts(base.Content.SharedParts):
|
95
|
+
"""Shared script definitions in the HEAD section."""
|
96
|
+
|
97
|
+
@functools.cached_property
|
98
|
+
def content(self) -> str:
|
99
|
+
if self.parts:
|
100
|
+
code = '\n'.join([inspect.cleandoc(v) for v in self.parts.keys()])
|
101
|
+
return f'<script>\n{code}\n</script>'
|
102
|
+
return ''
|
103
|
+
|
104
|
+
class ScriptFiles(base.Content.SharedParts):
|
105
|
+
"""Shared script files to link to in the HEAD section."""
|
106
|
+
|
107
|
+
@functools.cached_property
|
108
|
+
def content(self) -> str:
|
109
|
+
return '\n'.join(
|
110
|
+
[f'<script src="{url}"></script>' for url in self.parts.keys()]
|
111
|
+
)
|
112
|
+
|
113
|
+
class Styles(base.Content.SharedParts):
|
114
|
+
"""Shared style definitions in the HEAD section."""
|
115
|
+
|
116
|
+
@functools.cached_property
|
117
|
+
def content(self) -> str:
|
118
|
+
if self.parts:
|
119
|
+
styles = '\n'.join([inspect.cleandoc(v) for v in self.parts.keys()])
|
120
|
+
return f'<style>\n{styles}\n</style>'
|
121
|
+
return ''
|
122
|
+
|
123
|
+
class StyleFiles(base.Content.SharedParts):
|
124
|
+
"""Shared style files to link to in the HEAD section."""
|
125
|
+
|
126
|
+
@functools.cached_property
|
127
|
+
def content(self) -> str:
|
128
|
+
return '\n'.join(
|
129
|
+
[
|
130
|
+
f'<link rel="stylesheet" href="{url}">'
|
131
|
+
for url in self.parts.keys()
|
132
|
+
]
|
133
|
+
)
|
134
|
+
|
135
|
+
def __init__( # pylint: disable=useless-super-delegation
|
136
|
+
self,
|
137
|
+
*content: WritableTypes,
|
138
|
+
style_files: Optional[Iterable[str]] = None,
|
139
|
+
styles: Optional[Iterable[str]] = None,
|
140
|
+
script_files: Optional[Iterable[str]] = None,
|
141
|
+
scripts: Optional[Iterable[str]] = None,
|
142
|
+
) -> None:
|
143
|
+
"""Constructor.
|
144
|
+
|
145
|
+
Args:
|
146
|
+
*content: One or multiple body part (str, Html, lambda, None) of the HTML.
|
147
|
+
style_files: URLs for external styles to include.
|
148
|
+
styles: CSS styles to include.
|
149
|
+
script_files: URLs for external scripts to include.
|
150
|
+
scripts: JavaScript scripts to include.
|
151
|
+
"""
|
152
|
+
super().__init__(
|
153
|
+
*content,
|
154
|
+
style_files=Html.StyleFiles(*(style_files or [])),
|
155
|
+
styles=Html.Styles(*(styles or [])),
|
156
|
+
script_files=Html.ScriptFiles(*(script_files or [])),
|
157
|
+
scripts=Html.Scripts(*(scripts or [])),
|
158
|
+
)
|
159
|
+
|
160
|
+
def _repr_html_(self) -> str:
|
161
|
+
return self.to_str()
|
162
|
+
|
163
|
+
@property
|
164
|
+
def styles(self) -> 'Html.Styles':
|
165
|
+
"""Returns the styles to include in the HTML."""
|
166
|
+
return self._shared_parts['styles']
|
167
|
+
|
168
|
+
@property
|
169
|
+
def style_files(self) -> 'Html.StyleFiles':
|
170
|
+
"""Returns the style files to link to."""
|
171
|
+
return self._shared_parts['style_files']
|
172
|
+
|
173
|
+
@property
|
174
|
+
def scripts(self) -> 'Html.Scripts':
|
175
|
+
"""Returns the scripts to include in the HTML."""
|
176
|
+
return self._shared_parts['scripts']
|
177
|
+
|
178
|
+
@property
|
179
|
+
def script_files(self) -> 'Html.ScriptFiles':
|
180
|
+
"""Returns the script files to link to."""
|
181
|
+
return self._shared_parts['script_files']
|
182
|
+
|
183
|
+
@property
|
184
|
+
def head_section(self) -> str:
|
185
|
+
"""Returns the head section."""
|
186
|
+
return '\n'.join(
|
187
|
+
[
|
188
|
+
v for v in [
|
189
|
+
'<head>', self.style_section, self.script_section, '</head>']
|
190
|
+
if v
|
191
|
+
]
|
192
|
+
)
|
193
|
+
|
194
|
+
@property
|
195
|
+
def style_section(self) -> str:
|
196
|
+
"""Returns the style section."""
|
197
|
+
return '\n'.join(
|
198
|
+
[
|
199
|
+
v for v in [self.style_files.content, self.styles.content]
|
200
|
+
if v
|
201
|
+
]
|
202
|
+
)
|
203
|
+
|
204
|
+
@property
|
205
|
+
def script_section(self) -> str:
|
206
|
+
"""Returns the script section."""
|
207
|
+
return '\n'.join(
|
208
|
+
[
|
209
|
+
v for v in [self.script_files.content, self.scripts.content]
|
210
|
+
if v
|
211
|
+
]
|
212
|
+
)
|
213
|
+
|
214
|
+
@property
|
215
|
+
def body_section(self) -> str:
|
216
|
+
"""Returns the body section."""
|
217
|
+
return f'<body>\n{self.content}\n</body>'
|
218
|
+
|
219
|
+
#
|
220
|
+
# Methods for adding shared parts and writing HTML content.
|
221
|
+
#
|
222
|
+
|
223
|
+
def add_style(self, *css: str) -> 'Html':
|
224
|
+
"""Adds CSS styles to the HTML."""
|
225
|
+
self.styles.add(*css)
|
226
|
+
return self
|
227
|
+
|
228
|
+
def add_script(self, *js: str) -> 'Html':
|
229
|
+
"""Adds JavaScript scripts to the HTML."""
|
230
|
+
self.scripts.add(*js)
|
231
|
+
return self
|
232
|
+
|
233
|
+
def add_style_file(self, *url: str) -> 'Html':
|
234
|
+
"""Adds a style file to the HTML."""
|
235
|
+
self.style_files.add(*url)
|
236
|
+
return self
|
237
|
+
|
238
|
+
def add_script_file(self, *url: str) -> 'Html':
|
239
|
+
"""Adds a script file to the HTML."""
|
240
|
+
self.script_files.add(*url)
|
241
|
+
return self
|
242
|
+
|
243
|
+
def to_str(
|
244
|
+
self,
|
245
|
+
*,
|
246
|
+
content_only: bool = False,
|
247
|
+
**kwargs
|
248
|
+
) -> str:
|
249
|
+
"""Returns the HTML str.
|
250
|
+
|
251
|
+
Args:
|
252
|
+
content_only: If True, only the content will be returned.
|
253
|
+
**kwargs: Additional keyword arguments passed from the user that
|
254
|
+
will be ignored.
|
255
|
+
|
256
|
+
Returns:
|
257
|
+
The generated HTML str.
|
258
|
+
"""
|
259
|
+
if content_only:
|
260
|
+
return self.content
|
261
|
+
return '\n'.join(
|
262
|
+
[
|
263
|
+
v for v in ['<html>', self.head_section,
|
264
|
+
self.body_section, '</html>'] if v
|
265
|
+
]
|
266
|
+
)
|
267
|
+
|
268
|
+
@classmethod
|
269
|
+
def from_value(
|
270
|
+
cls,
|
271
|
+
value: WritableTypes,
|
272
|
+
copy: bool = False
|
273
|
+
) -> Union['Html', None]:
|
274
|
+
return typing.cast(
|
275
|
+
Html, super().from_value(value, copy=copy)
|
276
|
+
)
|
277
|
+
|
278
|
+
@classmethod
|
279
|
+
def _to_content(
|
280
|
+
cls, value: WritableTypes
|
281
|
+
) -> Union['Html', str, None]:
|
282
|
+
if callable(value):
|
283
|
+
value = value()
|
284
|
+
if value is None:
|
285
|
+
return None
|
286
|
+
elif isinstance(value, HtmlConvertible):
|
287
|
+
value = value.to_html()
|
288
|
+
elif not isinstance(value, (str, cls)):
|
289
|
+
raise TypeError(f'Not a writable value for `{cls.__name__}`: {value!r}')
|
290
|
+
return value
|
291
|
+
|
292
|
+
#
|
293
|
+
# Helper methods for creating templated Html objects.
|
294
|
+
#
|
295
|
+
|
296
|
+
@classmethod
|
297
|
+
def element(
|
298
|
+
cls,
|
299
|
+
tag: str,
|
300
|
+
inner_html: Optional[List[WritableTypes]] = None,
|
301
|
+
*,
|
302
|
+
options: Union[str, Iterable[str], None] = None,
|
303
|
+
css_classes: NestableStr = None,
|
304
|
+
styles: Union[str, Dict[str, Any], None] = None,
|
305
|
+
**properties
|
306
|
+
) -> 'Html':
|
307
|
+
"""Creates an HTML element.
|
308
|
+
|
309
|
+
Args:
|
310
|
+
tag: The HTML tag name.
|
311
|
+
inner_html: The inner HTML of the element.
|
312
|
+
options: Positional options that will be added to the element. E.g. 'open'
|
313
|
+
for `<details open>`.
|
314
|
+
css_classes: The CSS class name or a list of CSS class names.
|
315
|
+
styles: A single CSS style string or a dictionary of CSS properties.
|
316
|
+
**properties: Keyword arguments for HTML properties. For properties with
|
317
|
+
underscore in the name, the underscore will be replaced by dash in the
|
318
|
+
generated HTML. E.g. `background_color` will be converted to
|
319
|
+
`background-color`.
|
320
|
+
|
321
|
+
Returns:
|
322
|
+
The opening tag of an HTML element.
|
323
|
+
"""
|
324
|
+
s = cls()
|
325
|
+
|
326
|
+
# Write the open tag.
|
327
|
+
css_classes = cls.concate(css_classes)
|
328
|
+
options = cls.concate(options)
|
329
|
+
styles = cls.style_str(styles)
|
330
|
+
s.write(
|
331
|
+
f'<{tag}',
|
332
|
+
f' {options}' if options else None,
|
333
|
+
f' class="{css_classes}"' if css_classes else None,
|
334
|
+
f' style="{styles}"' if styles else None,
|
335
|
+
)
|
336
|
+
for k, v in properties.items():
|
337
|
+
if v is not None:
|
338
|
+
s.write(f' {k.replace("_", "-")}="{v}"')
|
339
|
+
s.write('>')
|
340
|
+
|
341
|
+
# Write the inner HTML.
|
342
|
+
if inner_html:
|
343
|
+
for child in inner_html:
|
344
|
+
s.write(child)
|
345
|
+
|
346
|
+
# Write the closing tag.
|
347
|
+
s.write(f'</{tag}>')
|
348
|
+
return s
|
349
|
+
|
350
|
+
@classmethod
|
351
|
+
def escape(
|
352
|
+
cls,
|
353
|
+
s: WritableTypes,
|
354
|
+
javascript_str: bool = False
|
355
|
+
) -> Any:
|
356
|
+
"""Escapes an HTML writable object."""
|
357
|
+
if s is None:
|
358
|
+
return None
|
359
|
+
|
360
|
+
if callable(s):
|
361
|
+
s = s()
|
362
|
+
if isinstance(s, HtmlConvertible):
|
363
|
+
s = s.to_html()
|
364
|
+
|
365
|
+
def _escape(s: str) -> str:
|
366
|
+
if javascript_str:
|
367
|
+
return (
|
368
|
+
s.replace('\\', '\\\\')
|
369
|
+
.replace('"', '\\"')
|
370
|
+
.replace('\r', '\\r')
|
371
|
+
.replace('\n', '\\n')
|
372
|
+
.replace('\t', '\\t')
|
373
|
+
)
|
374
|
+
return html_lib.escape(s)
|
375
|
+
|
376
|
+
if isinstance(s, str):
|
377
|
+
return _escape(s)
|
378
|
+
else:
|
379
|
+
assert isinstance(s, Html), s
|
380
|
+
return Html(_escape(s.content)).write(
|
381
|
+
s, shared_parts_only=True
|
382
|
+
)
|
383
|
+
|
384
|
+
@classmethod
|
385
|
+
def concate(
|
386
|
+
cls, nestable_str: NestableStr, separator: str = ' ', dedup: bool = True
|
387
|
+
) -> Optional[str]:
|
388
|
+
"""Concates the string nodes in a nestable object."""
|
389
|
+
flattened = utils.flatten(nestable_str)
|
390
|
+
if isinstance(flattened, str):
|
391
|
+
return flattened
|
392
|
+
elif isinstance(flattened, dict):
|
393
|
+
str_items = [v for v in flattened.values() if isinstance(v, str)]
|
394
|
+
if dedup:
|
395
|
+
str_items = list(dict.fromkeys(str_items).keys())
|
396
|
+
if str_items:
|
397
|
+
return separator.join(str_items)
|
398
|
+
return None
|
399
|
+
|
400
|
+
@classmethod
|
401
|
+
def style_str(
|
402
|
+
cls, style: Union[str, Dict[str, Any], None],
|
403
|
+
) -> Optional[str]:
|
404
|
+
"""Gets a string representing an inline CSS style.
|
405
|
+
|
406
|
+
Args:
|
407
|
+
style: A single CSS style string, or a dictionary for CSS properties.
|
408
|
+
When dictionary form is used, underscore in the key name will be
|
409
|
+
replaced by dash in the generated CSS style string.
|
410
|
+
For example, `background_color` will be converted to `background-color`.
|
411
|
+
|
412
|
+
Returns:
|
413
|
+
A CSS style string or None if no CSS property is provided.
|
414
|
+
"""
|
415
|
+
if not style:
|
416
|
+
return None
|
417
|
+
if isinstance(style, str):
|
418
|
+
return style
|
419
|
+
else:
|
420
|
+
assert isinstance(style, dict), style
|
421
|
+
return ''.join(
|
422
|
+
[
|
423
|
+
f'{k.replace("_", "-")}:{v};'
|
424
|
+
for k, v in style.items() if v is not None
|
425
|
+
]
|
426
|
+
) or None
|
427
|
+
|
428
|
+
|
429
|
+
# Allow automatic conversion from str to Html.
|
430
|
+
pg_typing.register_converter(str, Html, convert_fn=Html.from_value)
|
431
|
+
|
432
|
+
|
433
|
+
class HtmlConvertible:
|
434
|
+
"""Base class for HTML convertible objects."""
|
435
|
+
|
436
|
+
def to_html(self, **kwargs) -> Html:
|
437
|
+
"""Returns the HTML representation of the object."""
|
438
|
+
return to_html(self, **kwargs)
|
439
|
+
|
440
|
+
def to_html_str(self, *, content_only: bool = False, **kwargs) -> str:
|
441
|
+
"""Returns the HTML str of the object."""
|
442
|
+
return self.to_html(**kwargs).to_str(content_only=content_only)
|
443
|
+
|
444
|
+
def _repr_html_(self) -> str:
|
445
|
+
return self.to_html_str()
|
446
|
+
|
447
|
+
|
448
|
+
class HtmlView(base.View):
|
449
|
+
"""Base class for HTML views."""
|
450
|
+
|
451
|
+
class Extension(base.View.Extension, HtmlConvertible):
|
452
|
+
"""Base class for HtmlView extensions."""
|
453
|
+
|
454
|
+
def render(
|
455
|
+
self,
|
456
|
+
value: Any,
|
457
|
+
*,
|
458
|
+
name: Optional[str] = None,
|
459
|
+
root_path: Optional[utils.KeyPath] = None,
|
460
|
+
**kwargs,
|
461
|
+
) -> Html:
|
462
|
+
"""Renders the input value into an HTML object."""
|
463
|
+
# For customized HtmlConvertible objects, call their `to_html()` method.
|
464
|
+
if (isinstance(value, HtmlConvertible)
|
465
|
+
and not isinstance(value, self.__class__.Extension)
|
466
|
+
and value.__class__.to_html is not HtmlConvertible.to_html):
|
467
|
+
return value.to_html(name=name, root_path=root_path, **kwargs)
|
468
|
+
return self._render(value, name=name, root_path=root_path, **kwargs)
|
469
|
+
|
470
|
+
@abc.abstractmethod
|
471
|
+
def _render(
|
472
|
+
self,
|
473
|
+
value: Any,
|
474
|
+
*,
|
475
|
+
name: Optional[str] = None,
|
476
|
+
root_path: Optional[utils.KeyPath] = None,
|
477
|
+
**kwargs,
|
478
|
+
) -> Html:
|
479
|
+
"""View's implementation of HTML rendering."""
|
480
|
+
|
481
|
+
|
482
|
+
def to_html(
|
483
|
+
value: Any,
|
484
|
+
*,
|
485
|
+
name: Optional[str] = None,
|
486
|
+
root_path: Optional[utils.KeyPath] = None,
|
487
|
+
view_id: str = 'html-tree-view',
|
488
|
+
**kwargs,
|
489
|
+
) -> Html:
|
490
|
+
"""Returns the HTML representation of a value.
|
491
|
+
|
492
|
+
Args:
|
493
|
+
value: The value to render.
|
494
|
+
name: The name of the value.
|
495
|
+
root_path: The root path of the value.
|
496
|
+
view_id: The ID of the view to render the value.
|
497
|
+
See `pg.views.HtmlView.dir()` for all available HTML view IDs.
|
498
|
+
**kwargs: Additional keyword arguments passed from `pg.to_html`, wich
|
499
|
+
will be passed to the `HtmlView.render_xxx()` (thus
|
500
|
+
`Extension._html_xxx()`) methods.
|
501
|
+
|
502
|
+
Returns:
|
503
|
+
The rendered HTML.
|
504
|
+
"""
|
505
|
+
content = base.view(
|
506
|
+
value,
|
507
|
+
name=name,
|
508
|
+
root_path=root_path,
|
509
|
+
view_id=view_id,
|
510
|
+
**kwargs
|
511
|
+
)
|
512
|
+
assert isinstance(content, Html), content
|
513
|
+
return content
|
514
|
+
|
515
|
+
|
516
|
+
def to_html_str(
|
517
|
+
value: Any,
|
518
|
+
*,
|
519
|
+
name: Optional[str] = None,
|
520
|
+
root_path: Optional[utils.KeyPath] = None,
|
521
|
+
view_id: str = 'html-tree-view',
|
522
|
+
content_only: bool = False,
|
523
|
+
**kwargs,
|
524
|
+
) -> str:
|
525
|
+
"""Returns a HTML str for a value.
|
526
|
+
|
527
|
+
Args:
|
528
|
+
value: The value to render.
|
529
|
+
name: The name of the value.
|
530
|
+
root_path: The root path of the value.
|
531
|
+
view_id: The ID of the view to render the value.
|
532
|
+
See `pg.views.HtmlView.dir()` for all available HTML view IDs.
|
533
|
+
content_only: If True, only the content will be returned.
|
534
|
+
**kwargs: Additional keyword arguments passed from `pg.to_html`, wich
|
535
|
+
will be passed to the `HtmlView.render_xxx()` (thus
|
536
|
+
`Extension._html_xxx()`) methods.
|
537
|
+
|
538
|
+
Returns:
|
539
|
+
The rendered HTML str.
|
540
|
+
"""
|
541
|
+
return to_html(
|
542
|
+
value,
|
543
|
+
name=name,
|
544
|
+
root_path=root_path,
|
545
|
+
view_id=view_id,
|
546
|
+
**kwargs
|
547
|
+
).to_str(content_only=content_only)
|