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