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,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
|