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,804 @@
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
+ """Interface of PyGlove's view system: Content, View, and Extension.
15
+
16
+ PyGlove introduces a flexible and extensible view system for rendering objects
17
+ in different formats, such as text or HTML. This system allows users to plug in
18
+ various predefined views or create their own custom views with minimal effort.
19
+ The two core concepts of this system are ``View`` and ``Extension``.
20
+
21
+ A ``View`` defines how objects are displayed, with specific classes like
22
+ ``HtmlView`` and more specialized subclasses such as ``HtmlTreeView``. Each
23
+ concrete ``View`` class has a ``VIEW_ID`` that can be used to select the view
24
+ when calling functions like ``pg.view`` or ``pg.to_html``. Views provide default
25
+ rendering behavior for built-in Python types such as ``int``, ``str``, ``bool``,
26
+ ``list``, and ``dict``. However, users can extend this behavior by subclassing
27
+ the ``View.Extension`` class to define custom rendering logic for user-defined
28
+ classes.
29
+
30
+ .. code-block::
31
+
32
+ pg.to_html
33
+ |
34
+ | (calls)
35
+ v
36
+ pg.view
37
+ |
38
+ | (refers)
39
+ v
40
+ pg.View --------------------------------> pg.View.Extension
41
+ ^ (calls) ^
42
+ | (extends) |
43
+ +-----------+ |
44
+ | | |
45
+ ... pg.views.HtmlView ---------------------> pg.views.HtmlView.Extension
46
+ ^ (calls) ^
47
+ | (extends) | (extends)
48
+ pg.views.HtmlTreeView ------------------> pg.views.HtmlTreeView.Extension
49
+ (calls) ^
50
+ |
51
+ pg.Symbolic
52
+ ^
53
+ |
54
+ pg.Object
55
+
56
+ Each ``View`` class also defines an inner ``Extension`` class that manages
57
+ interactions with user-defined types. The ``Extension`` class typically
58
+ implements several methods chosen by the ``View`` developer, such as
59
+ ``_html_tree_view_render``, ``_html_tree_view__summary``, etc., depending on
60
+ the specific renderin needs.
61
+ The ``View.extension_method(<extension_method_name>)`` decorator enables
62
+ flexible mapping between a ``View`` method and its corresponding ``Extension``
63
+ method. This allows users to define custom rendering logic within their
64
+ ``Extension`` class and bind it to the view.
65
+
66
+ An example of introducing a new view is described below:
67
+
68
+ .. code-block:: python
69
+
70
+ class MyView(pg.View):
71
+ VIEW_ID = 'my_view'
72
+
73
+ class Extension(pg.View.Extension):
74
+ def _myview_render(self, **kwargs):
75
+ return ...
76
+
77
+ def _myview_part1(self, **kwargs):
78
+ return ...
79
+
80
+ def _myview_part2(self, **kwargs):
81
+ return ...
82
+
83
+ @pg.View.extension_method('_myview_render')
84
+ def render(self, value: Any, **kwargs):
85
+ part1 = self.render_part1(value.part1, **kwargs)
86
+ part2 = self.render_part2(value.part2, **kwargs)
87
+ return ...
88
+
89
+ @pg.View.extension_method('_myview_part1')
90
+ def render_part1(self, value: Any, **kwargs):
91
+ return ...
92
+
93
+ @pg.View.extension_method('_myview_part2')
94
+ def render_part2(self, value: Any, **kwargs):
95
+ return ...
96
+
97
+ The ``View.extension_method`` decorator also supports multiple views for a
98
+ single user class by allowing multi-inheritance of ``Extension`` classes.
99
+ For example, a class can support both ``View1`` and ``View2`` by inheriting
100
+ their respective ``Extension`` classes and implementing methods like
101
+ ``_render_view1`` and ``_render_view2`` to handle different view-specific
102
+ rendering:
103
+
104
+ .. code-block:: python
105
+
106
+ class MyObject(MyView1.Extension, MyView2.Extension):
107
+ def _myview1_render(self, value, **kwargs):
108
+ return ...
109
+
110
+ def _myview2_render(self, value, **kwargs):
111
+ return ...
112
+
113
+ For ``extension_method``-decorated ``View`` methods, they must include a
114
+ ``value`` argument, which is the target object to be rendered. The view will
115
+ delegate rendering to the user logic only when the ``value`` is an instance of
116
+ the ``Extension`` class.
117
+
118
+ `pg.view` is the function to display an object with a specific view. It takes
119
+ a ``view_id`` argument to specify which view to use, and returns a ``Content``
120
+ object, which acts as the media to be displayed. For example::
121
+
122
+ .. code-block:: python
123
+
124
+ pg.view([1, 2, MyObject(...)], view_id='my_view1')
125
+
126
+ """
127
+ import abc
128
+ import collections
129
+ import contextlib
130
+ import copy as copy_lib
131
+ import functools
132
+ import inspect
133
+ import io
134
+ import os
135
+ import types
136
+ from typing import Any, Callable, Dict, Iterator, Optional, Sequence, Set, Type, Union
137
+
138
+ from pyglove.core import io as pg_io
139
+ from pyglove.core import typing as pg_typing
140
+ from pyglove.core import utils
141
+
142
+
143
+ # Type definition for the value filter function.
144
+ NodeFilter = Callable[
145
+ [
146
+ utils.KeyPath, # The path to the value.
147
+ Any, # Current value.
148
+ Any, # Parent value
149
+ ],
150
+ bool, # Whether to include the value.
151
+ ]
152
+
153
+
154
+ _VIEW_ARGS_PRESET_NAME = 'pyglove_view_args'
155
+ _VIEW_REGISTRY = {}
156
+ _TLS_KEY_OPERAND_STACK_BY_METHOD = '__view_operand_stack__'
157
+ _TLS_KEY_VIEW_OPTIONS = '__view_options__'
158
+
159
+
160
+ class Content(utils.Formattable, metaclass=abc.ABCMeta):
161
+ """Content: A type of media to be displayed in a view.
162
+
163
+ For example, `pg.Html` is a `Content` type that represents HTML to be
164
+ displayed in an HTML view.
165
+ """
166
+
167
+ WritableTypes = Union[ # pylint: disable=invalid-name
168
+ str,
169
+ 'Content',
170
+ Callable[[], Union[str, 'Content', None]],
171
+ None
172
+ ]
173
+
174
+ class SharedParts(utils.Formattable):
175
+ """A part of the content that should appear just once.
176
+
177
+ For example, `pg.Html.Styles` is a `SharedParts` type that represents
178
+ a group of CSS styles that should only be included once in the HEAD section.
179
+ """
180
+
181
+ __slots__ = ('_parts',)
182
+
183
+ def __init__(
184
+ self,
185
+ *parts: Union[str, 'Content.SharedParts', None]
186
+ ) -> None:
187
+ self._parts: Dict[str, int] = collections.defaultdict(int)
188
+ self.add(*parts)
189
+
190
+ def add(self, *parts: Union[str, 'Content.SharedParts', None]) -> bool:
191
+ """Adds one or multiple parts."""
192
+ updated = False
193
+ for part in parts:
194
+ if part is None:
195
+ continue
196
+ if isinstance(part, Content.SharedParts):
197
+ assert isinstance(part, self.__class__), (part, self.__class__)
198
+ for p, count in part.parts.items():
199
+ self._parts[p] += count
200
+ updated = True
201
+ else:
202
+ assert isinstance(part, str), part
203
+ updated |= (part not in self._parts)
204
+ self._parts[part] += 1
205
+
206
+ if updated:
207
+ self.__dict__.pop('content', None)
208
+ return updated
209
+
210
+ @property
211
+ def parts(self) -> Dict[str, int]:
212
+ """Returns all parts and their reference counts."""
213
+ return self._parts
214
+
215
+ def __bool__(self) -> bool:
216
+ """Returns True if there is any part."""
217
+ return bool(self._parts)
218
+
219
+ def __contains__(self, part: str) -> bool:
220
+ """Returns True if the part is in the shared parts."""
221
+ return part in self._parts
222
+
223
+ def __iter__(self) -> Iterator[str]:
224
+ """Iterates all parts."""
225
+ return iter(self._parts.keys())
226
+
227
+ def __copy__(self) -> 'Content.SharedParts':
228
+ """Returns a copy of the shared parts."""
229
+ return self.__class__(self) # pytype: disable=not-instantiable
230
+
231
+ def __eq__(self, other: Any):
232
+ if not isinstance(other, self.__class__):
233
+ return False
234
+ return set(self._parts.keys()) == set(other.parts.keys())
235
+
236
+ def __ne__(self, other: Any):
237
+ return not self.__eq__(other)
238
+
239
+ def format(
240
+ self,
241
+ compact: bool = False,
242
+ verbose: bool = True,
243
+ root_indent: int = 0,
244
+ **kwargs
245
+ ) -> str:
246
+ if compact:
247
+ return utils.kvlist_str(
248
+ [
249
+ ('parts', self._parts, {}),
250
+ ],
251
+ label=self.__class__.__name__,
252
+ compact=compact,
253
+ verbose=verbose,
254
+ root_indent=root_indent,
255
+ bracket_type=utils.BracketType.ROUND,
256
+ )
257
+ return self.content
258
+
259
+ @property
260
+ @abc.abstractmethod
261
+ def content(self) -> str:
262
+ """Returns the content string representing the the shared parts."""
263
+
264
+ __slots__ = ('_content_stream', '_shared_parts',)
265
+
266
+ def __init__(
267
+ self,
268
+ *content: WritableTypes,
269
+ **shared_parts: 'Content.SharedParts'
270
+ ):
271
+ self._content_stream = io.StringIO()
272
+ self._shared_parts = shared_parts
273
+
274
+ for c in content:
275
+ c = self._to_content(c)
276
+ if c is None:
277
+ continue
278
+ elif isinstance(c, str):
279
+ self._content_stream.write(c)
280
+ else:
281
+ self.write(c)
282
+
283
+ @functools.cached_property
284
+ def content(self) -> str:
285
+ """Returns the content."""
286
+ return self._content_stream.getvalue()
287
+
288
+ @property
289
+ def shared_parts(self) -> Dict[str, 'Content.SharedParts']:
290
+ """Returns the shared parts."""
291
+ return self._shared_parts
292
+
293
+ def write(
294
+ self, *parts: WritableTypes, shared_parts_only: bool = False
295
+ ) -> 'Content':
296
+ """Writes one or more parts to current Content.
297
+
298
+ Args:
299
+ *parts: The parts to be written. Each part can be a string, a Content
300
+ object, a callable that returns one of the above, or None.
301
+ shared_parts_only: If True, only write the shared parts.
302
+
303
+ Returns:
304
+ The current Content object for chaining.
305
+ """
306
+ content_updated = False
307
+ for p in parts:
308
+ p = self._to_content(p)
309
+ if p is None:
310
+ continue
311
+
312
+ if not isinstance(p, (str, self.__class__)):
313
+ raise TypeError(
314
+ f'{p!r} ({type(p)}) cannot be writable. '
315
+ f'Only str, None, {self.__class__.__name__} and callable object '
316
+ 'that returns one of them are supported.'
317
+ )
318
+
319
+ if isinstance(p, Content):
320
+ current = self._shared_parts
321
+ for k, v in p.shared_parts.items():
322
+ # Since `p` is the same type of `self`, we expect they have the
323
+ # same set of shared parts, which is determined at the __init__ time.
324
+ current[k].add(v)
325
+ p = p.content
326
+
327
+ if not shared_parts_only:
328
+ self._content_stream.write(p)
329
+ content_updated = True
330
+
331
+ if content_updated:
332
+ self.__dict__.pop('content', None)
333
+ return self
334
+
335
+ def save(self, file: str, **kwargs):
336
+ """Save content to a file."""
337
+ pg_io.mkdirs(os.path.dirname(file), exist_ok=True)
338
+ pg_io.writefile(file, self.to_str(**kwargs))
339
+
340
+ def __add__(self, other: WritableTypes) -> 'Content':
341
+ """Operator +: Concatenates two Content objects."""
342
+ other = self._to_content(other)
343
+ if not other:
344
+ return self
345
+ s = copy_lib.deepcopy(self)
346
+ s.write(other)
347
+ return s
348
+
349
+ def __radd__(self, other: WritableTypes) -> 'Content':
350
+ """Right-hand operator +: concatenates two Content objects."""
351
+ s = self.from_value(other, copy=True)
352
+ if s is None:
353
+ return self
354
+ s.write(self)
355
+ return s
356
+
357
+ def format(self,
358
+ compact: bool = False,
359
+ verbose: bool = True,
360
+ root_indent: int = 0,
361
+ content_only: bool = False,
362
+ **kwargs) -> str:
363
+ """Formats the Content object."""
364
+ del kwargs
365
+ if compact:
366
+ return utils.kvlist_str(
367
+ [
368
+ ('content', self.content, ''),
369
+ ]
370
+ + [(k, v, None) for k, v in self._shared_parts.items()],
371
+ label=self.__class__.__name__,
372
+ compact=compact,
373
+ verbose=verbose,
374
+ root_indent=root_indent,
375
+ bracket_type=utils.BracketType.ROUND,
376
+ )
377
+ return self.to_str(content_only=content_only)
378
+
379
+ @abc.abstractmethod
380
+ def to_str(self, *, content_only: bool = False, **kwargs) -> str:
381
+ """Returns the string representation of the content."""
382
+
383
+ def __eq__(self, other: Any) -> bool:
384
+ if not isinstance(other, self.__class__):
385
+ return False
386
+ return (
387
+ self.content == other.content
388
+ and self._shared_parts == other._shared_parts # pylint: disable=protected-access
389
+ )
390
+
391
+ def __ne__(self, other: Any) -> bool:
392
+ return not self.__eq__(other)
393
+
394
+ def __hash__(self):
395
+ return hash(self.to_str())
396
+
397
+ @classmethod
398
+ def from_value(
399
+ cls,
400
+ value: WritableTypes,
401
+ copy: bool = False
402
+ ) -> Union['Content', None]:
403
+ """Returns a Content object or None from a writable type."""
404
+ if value is None:
405
+ return None
406
+
407
+ if isinstance(value, Content):
408
+ assert isinstance(value, cls), (value, cls)
409
+ if copy:
410
+ return copy_lib.deepcopy(value)
411
+ return value
412
+ return cls(value) # pytype: disable=not-instantiable
413
+
414
+ @classmethod
415
+ def _to_content(cls, value: WritableTypes) -> Union['Content', str, None]:
416
+ """Returns a Content object or None from a writable type."""
417
+ if callable(value):
418
+ value = value()
419
+ if value is None:
420
+ return None
421
+ assert isinstance(value, (str, cls)), value
422
+ return value
423
+
424
+
425
+ def view(
426
+ value: Any,
427
+ *,
428
+ name: Optional[str] = None,
429
+ root_path: Optional[utils.KeyPath] = None,
430
+ view_id: str = 'html-tree-view',
431
+ **kwargs,
432
+ ) -> Content:
433
+ """Views an object through generating content based on a specific view.
434
+
435
+ Args:
436
+ value: The value to view.
437
+ name: The name of the value.
438
+ root_path: The root path of the value.
439
+ view_id: The ID of the view to use. See `pg.View.dir()` for all available
440
+ view IDs.
441
+ **kwargs: Additional keyword arguments passed to the view, wich
442
+ will be used as the preset arguments for the View and Extension methods.
443
+
444
+ Returns:
445
+ The rendered `Content` object.
446
+ """
447
+ if isinstance(value, Content):
448
+ return value
449
+
450
+ with view_options(**kwargs) as options:
451
+ view_object = View.create(view_id)
452
+ return view_object.render(
453
+ value, name=name, root_path=root_path or utils.KeyPath(), **options
454
+ )
455
+
456
+
457
+ @contextlib.contextmanager
458
+ def view_options(**kwargs) -> Iterator[Dict[str, Any]]:
459
+ """Context manager to inject rendering args to view.
460
+
461
+ Example:
462
+
463
+ with pg.view_options(enable_summary_tooltip=False):
464
+ MyObject().to_html()
465
+
466
+ Args:
467
+ **kwargs: Keyword arguments for View.render method.
468
+
469
+ Yields:
470
+ The merged keyword arguments.
471
+ """
472
+ parent_options = utils.thread_local_peek(_TLS_KEY_VIEW_OPTIONS, {})
473
+ # Deep merge the two dict.
474
+ options = utils.merge([parent_options, kwargs])
475
+ utils.thread_local_push(_TLS_KEY_VIEW_OPTIONS, options)
476
+ try:
477
+ yield options
478
+ finally:
479
+ utils.thread_local_pop(_TLS_KEY_VIEW_OPTIONS)
480
+
481
+
482
+ class View(metaclass=abc.ABCMeta):
483
+ """Base class for views.
484
+
485
+ A view defines an unique way/format of rendering an object (e.g. int, str,
486
+ list, dict, user-defined object). For example, `pg.HtmlView` is a view that
487
+ renders an object into HTML. `pg.HtmlTreeViews` is a concrete `pg.HtmlView`
488
+ that renders an object into a HTML tree view.
489
+ """
490
+
491
+ # The ID of the view, which will be used as the value for the `view`
492
+ # argument of `pg.view()`. Must be set for non-abstract subclasses.
493
+ VIEW_ID = None
494
+
495
+ class Extension:
496
+ """Extension for the View class.
497
+
498
+ View developers should always create a corresponding ``Extension`` class as
499
+ an inner class of the ``View`` class. The ``Extension`` class defines custom
500
+ rendering logic for user-defined types and provides methods that can be
501
+ bound to the ``View`` methods via the ``@pg.View.extension_method``
502
+ decorator.
503
+
504
+ Example:
505
+
506
+ .. code-block:: python
507
+
508
+ class MyView(pg.View):
509
+ VIEW_TYPE = 'my_view'
510
+
511
+ class Extension(pg.View.Extension):
512
+
513
+ def _my_view_render(self, value, *, view, **kwargs):
514
+ return view.render(value, **kwargs)
515
+
516
+ def _my_view_title(self, value, *, view, **kwargs):
517
+ return view.render_title(value, **kwargs)
518
+
519
+ @pg.View.extension_method('_my_view_title')
520
+ def title(self, value: Any, **kwargs):
521
+ return pg.Html('<h1>' + str(value) + '</h1>')
522
+
523
+ @pg.View.extension_method('_my_view_render')
524
+ def render(self, value: Any, **kwargs):
525
+ return pg.Html(str(value))
526
+
527
+ To use the ``Extension`` class, users can subclass it and override the
528
+ extension methods in their own classes.
529
+
530
+ For example:
531
+
532
+ .. code-block:: python
533
+
534
+ class MyObject(MyView.Extension):
535
+
536
+ def _my_view_title(self, value, *, view, **kwargs):
537
+ return pg.Html('Custom title for ' + str(value) + '</h1>')
538
+
539
+ def _my_view_render(self, value, *, view, **kwargs):
540
+ return self._my_view_title(value, **kwargs) + pg.Html(str(value))
541
+
542
+ In this example, ``MyObject`` subclasses the ``Extension`` class of
543
+ ``MyView`` and overrides the ``_my_view_title`` and ``_my_view_render``
544
+ methods to provide custom view rendering for the object.
545
+ """
546
+
547
+ @classmethod
548
+ @functools.cache
549
+ def supported_view_classes(cls) -> Set[Type['View']]:
550
+ """Returns all non-abstract View classes that the current class supports.
551
+
552
+ A class can inherit from multiple ``View.Extension`` classes. For example:
553
+
554
+ .. code-block:: python
555
+
556
+ class MyObject(View1.Extension, View2.Extension):
557
+ ...
558
+
559
+ In this case, ``MyObject`` supports both ``View1`` and ``View2``.
560
+
561
+ Returns:
562
+ All non-abstract View classes that the current class supports.
563
+ """
564
+ supported_view_classes = set()
565
+ view_class = pg_typing.get_outer_class(
566
+ cls, base_cls=View, immediate=True
567
+ )
568
+ if view_class is not None and not inspect.isabstract(view_class):
569
+ supported_view_classes.add(view_class)
570
+
571
+ for base_cls in cls.__bases__:
572
+ if issubclass(base_cls, View.Extension):
573
+ supported_view_classes.update(base_cls.supported_view_classes())
574
+ return supported_view_classes
575
+
576
+ @classmethod
577
+ def extension_method(cls, method_name: str) -> Any:
578
+ """Decorator that dispatches a View method to a View.Extension method.
579
+
580
+ A few things to note:
581
+ 1) The View method being decorated must have a `value` argument, based on
582
+ which the Extension method will be dispatched.
583
+ 2) The View method's `value` argument will map to the Extension method's
584
+ `self` argument.
585
+ 3) The Extension method can optionally have a `view` argument, which will
586
+ be set to the current View class.
587
+
588
+ Args:
589
+ method_name: The name of the method in the Extension class to dispatch
590
+ from current View method.
591
+
592
+ Returns:
593
+ A decorator that dispatches a View method to a View.Extension method.
594
+ """
595
+
596
+ def decorator(func):
597
+ sig = pg_typing.signature(
598
+ func, auto_typing=False, auto_doc=False
599
+ )
600
+ # We substract 1 to offset the `self` argument.
601
+ try:
602
+ extension_arg_index = sig.arg_names.index('value') - 1
603
+ except ValueError as e:
604
+ raise TypeError(
605
+ f'View method {func.__name__!r} must have a `value` argument, '
606
+ 'which represents the target object to render.'
607
+ ) from e
608
+ if sig.varargs is not None:
609
+ raise TypeError(
610
+ f'View method must not have variable positional argument. '
611
+ f'Found `*{sig.varargs.name}` in {func.__name__!r}'
612
+ )
613
+
614
+ def get_extension(args: Sequence[Any], kwargs: Dict[str, Any]) -> Any:
615
+ if 'value' in kwargs:
616
+ return kwargs['value']
617
+ if extension_arg_index < len(args):
618
+ return args[extension_arg_index]
619
+ raise ValueError(
620
+ 'No value is provided for the `value` argument '
621
+ f'for {func.__name__!r}.'
622
+ )
623
+
624
+ def map_args(
625
+ args: Sequence[Any], kwargs: Dict[str, Any]
626
+ ) -> Dict[str, Any]:
627
+ # `args` does not contain self, therefore we use less than.
628
+ assert len(args) < len(sig.args), (args, sig.args)
629
+ kwargs.update({
630
+ sig.args[i].name: arg
631
+ for i, arg in enumerate(args) if i != extension_arg_index
632
+ })
633
+ kwargs.pop('value', None)
634
+ return kwargs
635
+
636
+ # We use the original function signature to generate the view method.
637
+ @functools.wraps(func)
638
+ def _generated_view_fn(self, *args, **kwargs):
639
+ # This allows a View method to consume the preset kwargs from the
640
+ # parent call to `pg.view`, yet also customizes the preset kwargs
641
+ # to the calls to other View/Extension methods within this context.
642
+ return self._maybe_dispatch( # pylint: disable=protected-access
643
+ *args, **kwargs,
644
+ extension=get_extension(args, kwargs),
645
+ view_method=func,
646
+ extension_method_name=method_name,
647
+ arg_map_fn=map_args
648
+ )
649
+ return _generated_view_fn
650
+ return decorator
651
+
652
+ def __init_subclass__(cls):
653
+ if inspect.isabstract(cls):
654
+ return
655
+
656
+ # Register the view type.
657
+ if cls.VIEW_ID is None:
658
+ raise ValueError(
659
+ f'`VIEW_ID` must be set for non-abstract View subclass {cls!r}.'
660
+ )
661
+ if cls.VIEW_ID == cls.__base__.VIEW_ID:
662
+ raise ValueError(
663
+ f'The `VIEW_ID` {cls.VIEW_ID!r} is the same as the base class '
664
+ f'{cls.__base__.__name__}. Please choose a different ID.'
665
+ )
666
+
667
+ _VIEW_REGISTRY[cls.VIEW_ID] = cls
668
+ super().__init_subclass__()
669
+
670
+ @classmethod
671
+ def dir(cls) -> Dict[str, Type['View']]:
672
+ """Returns all registered View classes with their view IDs."""
673
+ return {
674
+ view_id: view_cls
675
+ for view_id, view_cls in _VIEW_REGISTRY.items()
676
+ if issubclass(view_cls, cls)
677
+ }
678
+
679
+ @staticmethod
680
+ def create(view_id: str, **kwargs) -> 'View':
681
+ """Creates a View instance with the given view ID."""
682
+ if view_id not in _VIEW_REGISTRY:
683
+ raise ValueError(
684
+ f'No view class found with VIEW_ID: {view_id!r}'
685
+ )
686
+ return _VIEW_REGISTRY[view_id](**kwargs)
687
+
688
+ def __init__(self, **kwargs):
689
+ del kwargs
690
+ super().__init__()
691
+
692
+ @abc.abstractmethod
693
+ def render(
694
+ self,
695
+ value: Any,
696
+ *,
697
+ name: Optional[str] = None,
698
+ root_path: Optional[utils.KeyPath] = None,
699
+ **kwargs,
700
+ ) -> Content:
701
+ """Renders the input value.
702
+
703
+ Args:
704
+ value: The value to render.
705
+ name: (Optional) The referred name of the value from its container.
706
+ root_path: (Optional) The path of `value` under its object tree.
707
+ **kwargs: Additional keyword arguments passed from `pg.view` or wrapper
708
+ functions (e.g. `pg.to_html`).
709
+
710
+ Returns:
711
+ The rendered content.
712
+ """
713
+
714
+ #
715
+ # Implementation for routing calls to View methods to to Extension methods.
716
+ #
717
+
718
+ def _maybe_dispatch(
719
+ self,
720
+ *args,
721
+ extension: Any,
722
+ view_method: types.FunctionType,
723
+ extension_method_name: str,
724
+ arg_map_fn: Callable[[Sequence[Any], Dict[str, Any]], Dict[str, Any]],
725
+ **kwargs
726
+ ) -> Any:
727
+ """Dispatches the call to View method to corresponding Extension method.
728
+
729
+ The dispatching should take care of these three scenarios.
730
+
731
+ Scenario 1:
732
+ The user code within an Extension delegates the handling of value `v` back
733
+ to a View method.
734
+
735
+ In this case, we should dispatch the call on `v` to the Extension method
736
+ first, and use the View method if we detects that the request is routed
737
+ back to the View method based on the same value.
738
+
739
+ Scenario 2:
740
+ The Extension (user code) employes a View method to handle a value `v`'s
741
+ child value `w`.
742
+
743
+ In this case, we should allow the Extension method to handle `w` first.
744
+
745
+ Scenario 3:
746
+ A Extension (custom render) uses another method of the view to render the
747
+ same value `v`:
748
+
749
+ To address these 3 scenarios, we use a thread-local stack to track the
750
+ value being rendered by each view method.
751
+
752
+ Args:
753
+ *args: Positional arguments passed to the render method.
754
+ extension: Extension to render.
755
+ view_method: The default render method to call.
756
+ extension_method_name: The name of the method in the Extension class
757
+ to call.
758
+ arg_map_fn: A function to map the view method args to the extension method
759
+ args.
760
+ **kwargs: Keyword arguments passed to the render method.
761
+
762
+ Returns:
763
+ The rendered HTML.
764
+ """
765
+ # No Extension involved, call the view's default render method.
766
+ if not isinstance(extension, self.Extension):
767
+ return view_method(self, *args, **kwargs)
768
+
769
+ # Identify whether an extension is calling the view's method
770
+ # within its extension method, in such case, we should delegate the
771
+ # rendering logic to the default render method to avoid infinite recursion.
772
+ with self._track_rendering(extension, view_method) as being_rendered:
773
+ if being_rendered is extension:
774
+ return view_method(self, *args, **kwargs)
775
+
776
+ # Call the extension's method.
777
+ mapped = arg_map_fn(args, kwargs)
778
+ return getattr(extension, extension_method_name)(
779
+ view=self, **mapped
780
+ )
781
+
782
+ @contextlib.contextmanager
783
+ def _track_rendering(
784
+ self,
785
+ value: Extension,
786
+ view_method
787
+ ) -> Iterator[Any]:
788
+ """Context manager for tracking the value being rendered."""
789
+ del self
790
+ rendering_stack = utils.thread_local_get(
791
+ _TLS_KEY_OPERAND_STACK_BY_METHOD, {}
792
+ )
793
+ callsite_value = rendering_stack.get(view_method, None)
794
+ rendering_stack[view_method] = value
795
+ utils.thread_local_set(_TLS_KEY_OPERAND_STACK_BY_METHOD, rendering_stack)
796
+ try:
797
+ yield callsite_value
798
+ finally:
799
+ if callsite_value is None:
800
+ rendering_stack.pop(view_method)
801
+ if not rendering_stack:
802
+ utils.thread_local_del(_TLS_KEY_OPERAND_STACK_BY_METHOD)
803
+ else:
804
+ rendering_stack[view_method] = callsite_value