pyglove 0.4.5.dev202410020809__py3-none-any.whl → 0.4.5.dev202410090808__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 +9 -0
- pyglove/core/object_utils/__init__.py +1 -0
- pyglove/core/object_utils/value_location.py +10 -0
- pyglove/core/object_utils/value_location_test.py +22 -0
- pyglove/core/symbolic/base.py +40 -2
- pyglove/core/symbolic/base_test.py +67 -0
- pyglove/core/symbolic/diff.py +190 -1
- pyglove/core/symbolic/diff_test.py +290 -0
- pyglove/core/symbolic/object_test.py +3 -8
- pyglove/core/symbolic/ref.py +29 -0
- pyglove/core/symbolic/ref_test.py +143 -0
- pyglove/core/typing/__init__.py +4 -0
- pyglove/core/typing/callable_ext.py +240 -1
- pyglove/core/typing/callable_ext_test.py +255 -0
- pyglove/core/typing/inspect.py +63 -0
- pyglove/core/typing/inspect_test.py +39 -0
- pyglove/core/views/__init__.py +30 -0
- pyglove/core/views/base.py +906 -0
- pyglove/core/views/base_test.py +615 -0
- pyglove/core/views/html/__init__.py +27 -0
- pyglove/core/views/html/base.py +529 -0
- pyglove/core/views/html/base_test.py +804 -0
- pyglove/core/views/html/tree_view.py +1052 -0
- pyglove/core/views/html/tree_view_test.py +748 -0
- {pyglove-0.4.5.dev202410020809.dist-info → pyglove-0.4.5.dev202410090808.dist-info}/METADATA +1 -1
- {pyglove-0.4.5.dev202410020809.dist-info → pyglove-0.4.5.dev202410090808.dist-info}/RECORD +29 -21
- {pyglove-0.4.5.dev202410020809.dist-info → pyglove-0.4.5.dev202410090808.dist-info}/LICENSE +0 -0
- {pyglove-0.4.5.dev202410020809.dist-info → pyglove-0.4.5.dev202410090808.dist-info}/WHEEL +0 -0
- {pyglove-0.4.5.dev202410020809.dist-info → pyglove-0.4.5.dev202410090808.dist-info}/top_level.txt +0 -0
pyglove/core/__init__.py
CHANGED
@@ -297,6 +297,15 @@ docstr = object_utils.docstr
|
|
297
297
|
catch_errors = object_utils.catch_errors
|
298
298
|
timeit = object_utils.timeit
|
299
299
|
|
300
|
+
# Symbols from 'views' sub-module.
|
301
|
+
|
302
|
+
from pyglove.core import views
|
303
|
+
view = views.view
|
304
|
+
View = views.View
|
305
|
+
Html = views.Html
|
306
|
+
to_html = views.to_html
|
307
|
+
to_html_str = views.to_html_str
|
308
|
+
|
300
309
|
#
|
301
310
|
# Symbols from `io` sub-module.
|
302
311
|
#
|
@@ -137,6 +137,7 @@ from pyglove.core.object_utils.thread_local import thread_local_increment
|
|
137
137
|
from pyglove.core.object_utils.thread_local import thread_local_decrement
|
138
138
|
from pyglove.core.object_utils.thread_local import thread_local_push
|
139
139
|
from pyglove.core.object_utils.thread_local import thread_local_pop
|
140
|
+
from pyglove.core.object_utils.thread_local import thread_local_peek
|
140
141
|
|
141
142
|
# Handling docstrings.
|
142
143
|
from pyglove.core.object_utils.docstr_utils import DocStr
|
@@ -400,6 +400,16 @@ class KeyPath(formatting.Formattable):
|
|
400
400
|
except KeyError:
|
401
401
|
return False
|
402
402
|
|
403
|
+
def is_relative_to(self, other: Union[int, str, 'KeyPath']) -> bool:
|
404
|
+
"""Returns whether current path is relative to another path."""
|
405
|
+
other = KeyPath.from_value(other)
|
406
|
+
if len(self) < len(other):
|
407
|
+
return False
|
408
|
+
for i in range(len(other)):
|
409
|
+
if self.keys[i] != other.keys[i]:
|
410
|
+
return False
|
411
|
+
return True
|
412
|
+
|
403
413
|
@property
|
404
414
|
def path(self) -> str:
|
405
415
|
"""JSONPath representation of current path."""
|
@@ -226,6 +226,28 @@ class KeyPathTest(unittest.TestCase):
|
|
226
226
|
with self.assertRaisesRegex(TypeError, 'Cannot subtract KeyPath'):
|
227
227
|
_ = value_location.KeyPath.parse('a.b') - 1.0
|
228
228
|
|
229
|
+
def test_is_relative_to(self):
|
230
|
+
self.assertTrue(
|
231
|
+
value_location.KeyPath.parse('a.b.c').is_relative_to(
|
232
|
+
value_location.KeyPath())
|
233
|
+
)
|
234
|
+
self.assertTrue(
|
235
|
+
value_location.KeyPath.parse('a.b.c').is_relative_to(
|
236
|
+
value_location.KeyPath.parse('a.b'))
|
237
|
+
)
|
238
|
+
self.assertTrue(
|
239
|
+
value_location.KeyPath.parse('a.b.c').is_relative_to(
|
240
|
+
value_location.KeyPath.parse('a.b.c'))
|
241
|
+
)
|
242
|
+
self.assertFalse(
|
243
|
+
value_location.KeyPath.parse('a.b').is_relative_to(
|
244
|
+
value_location.KeyPath.parse('a.b.c'))
|
245
|
+
)
|
246
|
+
self.assertFalse(
|
247
|
+
value_location.KeyPath.parse('a.b.d').is_relative_to(
|
248
|
+
value_location.KeyPath.parse('a.b.c'))
|
249
|
+
)
|
250
|
+
|
229
251
|
def test_hash(self):
|
230
252
|
self.assertIn(value_location.KeyPath.parse('a.b.c'), {'a.b.c': 1})
|
231
253
|
self.assertNotIn(value_location.KeyPath.parse('a.b.c'), {'a.b': 1})
|
pyglove/core/symbolic/base.py
CHANGED
@@ -31,6 +31,7 @@ from pyglove.core.symbolic import flags
|
|
31
31
|
from pyglove.core.symbolic.origin import Origin
|
32
32
|
from pyglove.core.symbolic.pure_symbolic import NonDeterministic
|
33
33
|
from pyglove.core.symbolic.pure_symbolic import PureSymbolic
|
34
|
+
from pyglove.core.views import html
|
34
35
|
|
35
36
|
|
36
37
|
class WritePermissionError(Exception):
|
@@ -171,9 +172,10 @@ RAISE_IF_NOT_FOUND = (pg_typing.MISSING_VALUE,)
|
|
171
172
|
|
172
173
|
class Symbolic(
|
173
174
|
TopologyAware,
|
175
|
+
object_utils.Formattable,
|
174
176
|
object_utils.JSONConvertible,
|
175
177
|
object_utils.MaybePartial,
|
176
|
-
|
178
|
+
html.HtmlTreeView.Extension
|
177
179
|
):
|
178
180
|
"""Base for all symbolic types.
|
179
181
|
|
@@ -525,7 +527,7 @@ class Symbolic(
|
|
525
527
|
def sym_contains(
|
526
528
|
self,
|
527
529
|
value: Any = None,
|
528
|
-
type: Union[None, Type[Any], Tuple[Type[Any]]] = None # pylint: disable=redefined-builtin
|
530
|
+
type: Union[None, Type[Any], Tuple[Type[Any], ...]] = None # pylint: disable=redefined-builtin
|
529
531
|
) -> bool:
|
530
532
|
"""Returns True if the object contains sub-nodes of given value or type."""
|
531
533
|
return contains(self, value, type)
|
@@ -947,6 +949,42 @@ class Symbolic(
|
|
947
949
|
"""Serializes current object into a JSON string."""
|
948
950
|
return to_json_str(self, json_indent=json_indent, **kwargs)
|
949
951
|
|
952
|
+
# pytype: disable=annotation-type-mismatch
|
953
|
+
def _html_tree_view_content(
|
954
|
+
self,
|
955
|
+
*,
|
956
|
+
view: html.HtmlTreeView,
|
957
|
+
name: Optional[str],
|
958
|
+
parent: Any,
|
959
|
+
root_path: object_utils.KeyPath,
|
960
|
+
hide_frozen: bool = html.HtmlView.PresetArgValue(True),
|
961
|
+
hide_default_values: bool = html.HtmlView.PresetArgValue(False),
|
962
|
+
use_inferred: bool = html.HtmlView.PresetArgValue(True),
|
963
|
+
**kwargs,
|
964
|
+
) -> html.Html:
|
965
|
+
# pytype: enable=annotation-type-mismatch
|
966
|
+
"""Returns the content HTML for a symbolic object.."""
|
967
|
+
kv = {}
|
968
|
+
for k, v in self.sym_items():
|
969
|
+
# Apply frozen filter.
|
970
|
+
field = self.sym_attr_field(k)
|
971
|
+
if hide_frozen and field and field.frozen:
|
972
|
+
continue
|
973
|
+
|
974
|
+
# Apply inferred value.
|
975
|
+
if use_inferred and isinstance(v, Inferential):
|
976
|
+
v = self.sym_inferred(k, default=v)
|
977
|
+
|
978
|
+
# Apply default value filter.
|
979
|
+
if field and hide_default_values and eq(v, field.default_value):
|
980
|
+
continue
|
981
|
+
kv[k] = v
|
982
|
+
return view.complex_value(
|
983
|
+
kv, name=name, parent=self, root_path=root_path,
|
984
|
+
hide_frozen=hide_frozen, hide_default_values=hide_default_values,
|
985
|
+
use_inferred=use_inferred, **kwargs
|
986
|
+
)
|
987
|
+
|
950
988
|
@classmethod
|
951
989
|
def load(cls, *args, **kwargs) -> Any:
|
952
990
|
"""Loads an instance of this type using the global load handler."""
|
@@ -14,12 +14,16 @@
|
|
14
14
|
"""Tests for pyglove.symbolic.base."""
|
15
15
|
|
16
16
|
import copy
|
17
|
+
import inspect
|
18
|
+
from typing import Any
|
17
19
|
import unittest
|
18
20
|
|
19
21
|
from pyglove.core import object_utils
|
20
22
|
from pyglove.core import typing as pg_typing
|
21
23
|
from pyglove.core.symbolic import base
|
22
24
|
from pyglove.core.symbolic.dict import Dict
|
25
|
+
from pyglove.core.symbolic.inferred import ValueFromParentChain
|
26
|
+
from pyglove.core.symbolic.object import Object
|
23
27
|
|
24
28
|
|
25
29
|
class FieldUpdateTest(unittest.TestCase):
|
@@ -85,5 +89,68 @@ class FieldUpdateTest(unittest.TestCase):
|
|
85
89
|
)
|
86
90
|
|
87
91
|
|
92
|
+
class HtmlFormattableTest(unittest.TestCase):
|
93
|
+
|
94
|
+
def assert_content(self, html, expected):
|
95
|
+
expected = inspect.cleandoc(expected).strip()
|
96
|
+
actual = html.content.strip()
|
97
|
+
if actual != expected:
|
98
|
+
print(actual)
|
99
|
+
self.assertEqual(actual.strip(), expected)
|
100
|
+
|
101
|
+
def test_to_html(self):
|
102
|
+
|
103
|
+
class Foo(Object):
|
104
|
+
x: int
|
105
|
+
y: Any = 'foo'
|
106
|
+
z: pg_typing.Int().freeze(1)
|
107
|
+
|
108
|
+
# Disable tooltip.
|
109
|
+
self.assert_content(
|
110
|
+
Foo(x=1, y='foo').to_html(
|
111
|
+
enable_summary_tooltip=False,
|
112
|
+
enable_key_tooltip=False
|
113
|
+
),
|
114
|
+
"""
|
115
|
+
<details open class="pyglove foo"><summary><div class="summary_title">Foo(...)</div></summary><div class="complex_value foo"><table><tr><td><span class="object_key">x</span></td><td><span class="simple_value int">1</span></td></tr><tr><td><span class="object_key">y</span></td><td><span class="simple_value str">'foo'</span></td></tr></table></div></details>
|
116
|
+
"""
|
117
|
+
)
|
118
|
+
# Hide frozen and default values.
|
119
|
+
self.assert_content(
|
120
|
+
Foo(x=1, y='foo').to_html(
|
121
|
+
enable_summary_tooltip=False,
|
122
|
+
enable_key_tooltip=False,
|
123
|
+
collapse_level=0,
|
124
|
+
hide_frozen=True,
|
125
|
+
hide_default_values=True
|
126
|
+
),
|
127
|
+
"""
|
128
|
+
<details class="pyglove foo"><summary><div class="summary_title">Foo(...)</div></summary><div class="complex_value foo"><table><tr><td><span class="object_key">x</span></td><td><span class="simple_value int">1</span></td></tr></table></div></details>
|
129
|
+
"""
|
130
|
+
)
|
131
|
+
# Use inferred values.
|
132
|
+
x = Dict(x=Dict(y=ValueFromParentChain()), y=2)
|
133
|
+
self.assert_content(
|
134
|
+
x.x.to_html(
|
135
|
+
enable_summary_tooltip=False,
|
136
|
+
enable_key_tooltip=False,
|
137
|
+
use_inferred=False
|
138
|
+
),
|
139
|
+
"""
|
140
|
+
<details open class="pyglove dict"><summary><div class="summary_title">Dict(...)</div></summary><div class="complex_value dict"><table><tr><td><span class="object_key">y</span></td><td><details class="pyglove value-from-parent-chain"><summary><div class="summary_title">ValueFromParentChain(...)</div></summary><div class="complex_value value-from-parent-chain"><span class="empty_container"></span></div></details></td></tr></table></div></details>
|
141
|
+
"""
|
142
|
+
)
|
143
|
+
self.assert_content(
|
144
|
+
x.x.to_html(
|
145
|
+
enable_summary_tooltip=False,
|
146
|
+
enable_key_tooltip=False,
|
147
|
+
use_inferred=True
|
148
|
+
),
|
149
|
+
"""
|
150
|
+
<details open class="pyglove dict"><summary><div class="summary_title">Dict(...)</div></summary><div class="complex_value dict"><table><tr><td><span class="object_key">y</span></td><td><span class="simple_value int">2</span></td></tr></table></div></details>
|
151
|
+
"""
|
152
|
+
)
|
153
|
+
|
154
|
+
|
88
155
|
if __name__ == '__main__':
|
89
156
|
unittest.main()
|
pyglove/core/symbolic/diff.py
CHANGED
@@ -13,7 +13,7 @@
|
|
13
13
|
# limitations under the License.
|
14
14
|
"""Symbolic differences."""
|
15
15
|
|
16
|
-
from typing import Any, Callable, Tuple, Union
|
16
|
+
from typing import Any, Callable, Optional, Sequence, Tuple, Union
|
17
17
|
|
18
18
|
from pyglove.core import object_utils
|
19
19
|
from pyglove.core import typing as pg_typing
|
@@ -21,6 +21,7 @@ from pyglove.core.symbolic import base
|
|
21
21
|
from pyglove.core.symbolic import list as pg_list
|
22
22
|
from pyglove.core.symbolic import object as pg_object
|
23
23
|
from pyglove.core.symbolic.pure_symbolic import PureSymbolic
|
24
|
+
from pyglove.core.views import html
|
24
25
|
|
25
26
|
|
26
27
|
class Diff(PureSymbolic, pg_object.Object):
|
@@ -144,6 +145,194 @@ class Diff(PureSymbolic, pg_object.Object):
|
|
144
145
|
**kwargs,
|
145
146
|
)
|
146
147
|
|
148
|
+
# pytype: disable=annotation-type-mismatch
|
149
|
+
def _html_tree_view_summary(
|
150
|
+
self,
|
151
|
+
*,
|
152
|
+
view: html.HtmlTreeView,
|
153
|
+
title: Optional[str] = None,
|
154
|
+
max_summary_len_for_str: int = html.HtmlView.PresetArgValue(40),
|
155
|
+
**kwargs,
|
156
|
+
) -> Optional[html.Html]:
|
157
|
+
# pytype: enable=annotation-type-mismatch
|
158
|
+
if not bool(self):
|
159
|
+
v = self.value
|
160
|
+
if (isinstance(v, (int, float, bool, type(None)))
|
161
|
+
or (isinstance(v, str) and len(v) <= max_summary_len_for_str)):
|
162
|
+
return None
|
163
|
+
|
164
|
+
if v == Diff.MISSING:
|
165
|
+
return view.summary(
|
166
|
+
self,
|
167
|
+
title=title or 'Diff',
|
168
|
+
max_summary_len_for_str=max_summary_len_for_str,
|
169
|
+
**kwargs
|
170
|
+
)
|
171
|
+
return view.summary(
|
172
|
+
self.value,
|
173
|
+
title=title,
|
174
|
+
max_summary_len_for_str=max_summary_len_for_str,
|
175
|
+
**kwargs
|
176
|
+
)
|
177
|
+
elif self.is_leaf:
|
178
|
+
return None
|
179
|
+
else:
|
180
|
+
assert isinstance(self.left, type), self.left
|
181
|
+
assert isinstance(self.right, type), self.right
|
182
|
+
if self.left is self.right:
|
183
|
+
diff_title = self.left.__name__
|
184
|
+
else:
|
185
|
+
diff_title = f'{self.left.__name__} | {self.right.__name__}'
|
186
|
+
return view.summary(
|
187
|
+
self,
|
188
|
+
title=title or diff_title,
|
189
|
+
max_summary_len_for_str=max_summary_len_for_str,
|
190
|
+
**kwargs
|
191
|
+
)
|
192
|
+
|
193
|
+
def _html_tree_view_content(
|
194
|
+
self,
|
195
|
+
*,
|
196
|
+
view: html.HtmlTreeView,
|
197
|
+
parent: Any,
|
198
|
+
root_path: object_utils.KeyPath,
|
199
|
+
css_class: Optional[Sequence[str]] = None,
|
200
|
+
**kwargs
|
201
|
+
) -> html.Html:
|
202
|
+
del parent
|
203
|
+
user_specified_css_class = css_class or []
|
204
|
+
if not bool(self):
|
205
|
+
if self.value == Diff.MISSING:
|
206
|
+
root = html.Html.element(
|
207
|
+
'span',
|
208
|
+
# CSS class already defined in HtmlTreeView.
|
209
|
+
css_class=['diff_empty'] + user_specified_css_class,
|
210
|
+
)
|
211
|
+
else:
|
212
|
+
# When there is no diff, but the same value needs to be displayed
|
213
|
+
# we simply return the value.
|
214
|
+
root = view.content(
|
215
|
+
self.value,
|
216
|
+
css_class=['diff_left', 'diff_right'] + user_specified_css_class,
|
217
|
+
root_path=root_path, **kwargs
|
218
|
+
)
|
219
|
+
elif self.is_leaf:
|
220
|
+
root = html.Html.element(
|
221
|
+
'div',
|
222
|
+
[
|
223
|
+
# Render left value.
|
224
|
+
lambda: html.Html.element( # pylint: disable=g-long-ternary
|
225
|
+
'div',
|
226
|
+
[
|
227
|
+
view.render(
|
228
|
+
self.left, root_path=root_path + 'left', **kwargs
|
229
|
+
)
|
230
|
+
],
|
231
|
+
css_class=['diff_left'],
|
232
|
+
) if self.left != Diff.MISSING else None,
|
233
|
+
# Render right value.
|
234
|
+
lambda: html.Html.element( # pylint: disable=g-long-ternary
|
235
|
+
'div',
|
236
|
+
[
|
237
|
+
view.render(
|
238
|
+
self.right, root_path=root_path + 'right', **kwargs
|
239
|
+
)
|
240
|
+
],
|
241
|
+
css_class=['diff_right'],
|
242
|
+
) if self.right != Diff.MISSING else None,
|
243
|
+
],
|
244
|
+
css_class=['diff_value'] + user_specified_css_class,
|
245
|
+
)
|
246
|
+
else:
|
247
|
+
assert isinstance(self.left, type)
|
248
|
+
assert isinstance(self.right, type)
|
249
|
+
|
250
|
+
if issubclass(self.left, list):
|
251
|
+
key_fn = int
|
252
|
+
else:
|
253
|
+
key_fn = lambda k: k
|
254
|
+
|
255
|
+
s = html.Html()
|
256
|
+
for k, v in self.children.items():
|
257
|
+
k = key_fn(k)
|
258
|
+
child_path = root_path + k
|
259
|
+
|
260
|
+
s.write('<tr><td>')
|
261
|
+
# Print Key.
|
262
|
+
s.write(
|
263
|
+
view.object_key(
|
264
|
+
k, parent=v, root_path=child_path,
|
265
|
+
css_class=None if bool(v) else ['no_diff_key'],
|
266
|
+
**kwargs
|
267
|
+
),
|
268
|
+
)
|
269
|
+
# Print Value.
|
270
|
+
s.write(
|
271
|
+
'</td><td>',
|
272
|
+
view.render(v, root_path=child_path, **kwargs),
|
273
|
+
'</td></tr>'
|
274
|
+
)
|
275
|
+
root = html.Html.element(
|
276
|
+
'div',
|
277
|
+
[
|
278
|
+
'<table>', s, '</table>'
|
279
|
+
],
|
280
|
+
css_class=[
|
281
|
+
'complex_value', view.css_class_name(self.left)
|
282
|
+
] + user_specified_css_class,
|
283
|
+
)
|
284
|
+
return root.add_style(
|
285
|
+
"""
|
286
|
+
.diff .summary_title::after {
|
287
|
+
content: ' (diff)';
|
288
|
+
color: #aaa;
|
289
|
+
}
|
290
|
+
.diff .summary_title {
|
291
|
+
background-color: yellow;
|
292
|
+
}
|
293
|
+
.diff table td {
|
294
|
+
padding: 4px;
|
295
|
+
}
|
296
|
+
.diff_value {
|
297
|
+
display: inline-block;
|
298
|
+
align-items: center;
|
299
|
+
}
|
300
|
+
.no_diff_key {
|
301
|
+
opacity: 0.4;
|
302
|
+
}
|
303
|
+
.diff_left.diff_right::after {
|
304
|
+
content: '(no diff)';
|
305
|
+
margin-left: 0.5em;
|
306
|
+
color: #aaa;
|
307
|
+
font-style: italic;
|
308
|
+
}
|
309
|
+
.diff_empty::before {
|
310
|
+
content: '(empty)';
|
311
|
+
font-style: italic;
|
312
|
+
margin-left: 0.5em;
|
313
|
+
color: #aaa;
|
314
|
+
}
|
315
|
+
.diff_value .diff_left::before {
|
316
|
+
content: '🇱';
|
317
|
+
}
|
318
|
+
.diff_value .diff_left > .simple_value {
|
319
|
+
background-color: #ffcccc;
|
320
|
+
}
|
321
|
+
.diff_value .diff_left > details {
|
322
|
+
background-color: #ffcccc;
|
323
|
+
}
|
324
|
+
.diff_value .diff_right::before {
|
325
|
+
content: '🇷';
|
326
|
+
}
|
327
|
+
.diff_value .diff_right > .simple_value {
|
328
|
+
background-color: #ccffcc;
|
329
|
+
}
|
330
|
+
.diff_value .diff_right > details {
|
331
|
+
background-color: #ccffcc;
|
332
|
+
}
|
333
|
+
"""
|
334
|
+
)
|
335
|
+
|
147
336
|
|
148
337
|
# NOTE(daiyip): we add the symbolic attribute to Diff after its declaration
|
149
338
|
# since we need to access Diff.MISSING as the default value for `left` and
|