pyglove 0.4.5.dev202410290809__py3-none-any.whl → 0.4.5.dev202411010809__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 +1 -0
- pyglove/core/io/__init__.py +1 -0
- pyglove/core/io/file_system.py +14 -4
- 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/object_utils/__init__.py +4 -3
- pyglove/core/object_utils/error_utils.py +65 -1
- pyglove/core/object_utils/error_utils_test.py +56 -2
- pyglove/core/object_utils/json_conversion.py +1 -2
- pyglove/core/object_utils/{profiling.py → timing.py} +70 -21
- pyglove/core/object_utils/{profiling_test.py → timing_test.py} +34 -17
- pyglove/core/symbolic/__init__.py +1 -0
- pyglove/core/symbolic/base.py +75 -24
- pyglove/core/symbolic/dict.py +13 -4
- pyglove/core/symbolic/list.py +13 -4
- pyglove/core/symbolic/object.py +5 -2
- pyglove/core/views/html/base.py +18 -3
- pyglove/core/views/html/base_test.py +2 -0
- pyglove/core/views/html/tree_view.py +7 -1
- pyglove/core/views/html/tree_view_test.py +9 -0
- {pyglove-0.4.5.dev202410290809.dist-info → pyglove-0.4.5.dev202411010809.dist-info}/METADATA +1 -1
- {pyglove-0.4.5.dev202410290809.dist-info → pyglove-0.4.5.dev202411010809.dist-info}/RECORD +26 -24
- {pyglove-0.4.5.dev202410290809.dist-info → pyglove-0.4.5.dev202411010809.dist-info}/WHEEL +1 -1
- {pyglove-0.4.5.dev202410290809.dist-info → pyglove-0.4.5.dev202411010809.dist-info}/LICENSE +0 -0
- {pyglove-0.4.5.dev202410290809.dist-info → pyglove-0.4.5.dev202411010809.dist-info}/top_level.txt +0 -0
@@ -11,12 +11,15 @@
|
|
11
11
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
|
-
"""Utilities for
|
14
|
+
"""Utilities for timing."""
|
15
15
|
|
16
|
+
import collections
|
16
17
|
import dataclasses
|
17
18
|
import time
|
18
|
-
from typing import Dict, List, Optional
|
19
|
+
from typing import Any, Dict, List, Optional
|
19
20
|
|
21
|
+
from pyglove.core.object_utils import error_utils
|
22
|
+
from pyglove.core.object_utils import json_conversion
|
20
23
|
from pyglove.core.object_utils import thread_local
|
21
24
|
|
22
25
|
|
@@ -24,12 +27,12 @@ class TimeIt:
|
|
24
27
|
"""Context manager for timing the execution of a code block."""
|
25
28
|
|
26
29
|
@dataclasses.dataclass(frozen=True)
|
27
|
-
class Status:
|
30
|
+
class Status(json_conversion.JSONConvertible):
|
28
31
|
"""Status of a single `pg.timeit`."""
|
29
32
|
name: str
|
30
|
-
elapse: float
|
31
|
-
has_ended: bool
|
32
|
-
error: Optional[
|
33
|
+
elapse: float = 0.0
|
34
|
+
has_ended: bool = True
|
35
|
+
error: Optional[error_utils.ErrorInfo] = None
|
33
36
|
|
34
37
|
@property
|
35
38
|
def has_started(self) -> bool:
|
@@ -41,18 +44,33 @@ class TimeIt:
|
|
41
44
|
"""Returns whether the context has error."""
|
42
45
|
return self.error is not None
|
43
46
|
|
47
|
+
def to_json(self, **kwargs) -> Dict[str, Any]:
|
48
|
+
return self.to_json_dict(
|
49
|
+
fields=dict(
|
50
|
+
name=(self.name, None),
|
51
|
+
elapse=(self.elapse, 0.0),
|
52
|
+
has_ended=(self.has_ended, True),
|
53
|
+
error=(self.error, None),
|
54
|
+
),
|
55
|
+
exclude_default=True,
|
56
|
+
**kwargs,
|
57
|
+
)
|
58
|
+
|
44
59
|
@dataclasses.dataclass
|
45
|
-
class StatusSummary:
|
60
|
+
class StatusSummary(json_conversion.JSONConvertible):
|
46
61
|
"""Aggregated summary for repeated calls for `pg.timeit`."""
|
47
62
|
|
48
63
|
@dataclasses.dataclass
|
49
|
-
class Entry:
|
64
|
+
class Entry(json_conversion.JSONConvertible):
|
50
65
|
"""Aggregated status from the `pg.timeit` calls of the same name."""
|
51
66
|
|
52
67
|
num_started: int = 0
|
53
68
|
num_ended: int = 0
|
54
69
|
num_failed: int = 0
|
55
70
|
avg_duration: float = 0.0
|
71
|
+
error_tags: Dict[str, int] = dataclasses.field(
|
72
|
+
default_factory=lambda: collections.defaultdict(int)
|
73
|
+
)
|
56
74
|
|
57
75
|
def update(self, status: 'TimeIt.Status'):
|
58
76
|
self.avg_duration = (
|
@@ -64,24 +82,52 @@ class TimeIt:
|
|
64
82
|
self.num_ended += 1
|
65
83
|
if status.has_error:
|
66
84
|
self.num_failed += 1
|
85
|
+
assert status.error is not None
|
86
|
+
self.error_tags[status.error.tag] += 1
|
87
|
+
|
88
|
+
def to_json(self, **kwargs) -> Dict[str, Any]:
|
89
|
+
return self.to_json_dict(
|
90
|
+
fields=dict(
|
91
|
+
num_started=(self.num_started, 0),
|
92
|
+
num_ended=(self.num_ended, 0),
|
93
|
+
num_failed=(self.num_failed, 0),
|
94
|
+
avg_duration=(self.avg_duration, 0.0),
|
95
|
+
error_tags=(self.error_tags, {}),
|
96
|
+
),
|
97
|
+
exclude_default=True,
|
98
|
+
**kwargs,
|
99
|
+
)
|
67
100
|
|
68
101
|
breakdown: dict[str, 'TimeIt.StatusSummary.Entry'] = (
|
69
102
|
dataclasses.field(default_factory=dict)
|
70
103
|
)
|
71
104
|
|
72
|
-
def
|
73
|
-
|
105
|
+
def __bool__(self) -> bool:
|
106
|
+
"""Returns True if the summary is non-empty."""
|
107
|
+
return bool(self.breakdown)
|
108
|
+
|
109
|
+
def aggregate(self, timeit_status: Dict[str, 'TimeIt.Status']):
|
110
|
+
for k, v in timeit_status.items():
|
74
111
|
if k not in self.breakdown:
|
75
112
|
self.breakdown[k] = TimeIt.StatusSummary.Entry()
|
76
113
|
self.breakdown[k].update(v)
|
77
114
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
115
|
+
def to_json(self, **kwargs) -> Dict[str, Any]:
|
116
|
+
return self.to_json_dict(
|
117
|
+
fields=dict(
|
118
|
+
breakdown=(self.breakdown, {}),
|
119
|
+
),
|
120
|
+
exclude_default=True,
|
121
|
+
**kwargs,
|
122
|
+
)
|
123
|
+
|
124
|
+
def __init__(self, name: str = ''):
|
125
|
+
self._name: str = name
|
126
|
+
self._start_time: Optional[float] = None
|
127
|
+
self._end_time: Optional[float] = None
|
128
|
+
self._child_contexts: Dict[str, TimeIt] = {}
|
129
|
+
self._error: Optional[error_utils.ErrorInfo] = None
|
130
|
+
self._parent: Optional[TimeIt] = None
|
85
131
|
|
86
132
|
@property
|
87
133
|
def name(self) -> str:
|
@@ -107,7 +153,9 @@ class TimeIt:
|
|
107
153
|
"""Ends timing."""
|
108
154
|
if not self.has_ended:
|
109
155
|
self._end_time = time.time()
|
110
|
-
self._error =
|
156
|
+
self._error = (
|
157
|
+
None if error is None else error_utils.ErrorInfo.from_exception(error)
|
158
|
+
)
|
111
159
|
return True
|
112
160
|
return False
|
113
161
|
|
@@ -132,7 +180,7 @@ class TimeIt:
|
|
132
180
|
return self._end_time
|
133
181
|
|
134
182
|
@property
|
135
|
-
def error(self) -> Optional[
|
183
|
+
def error(self) -> Optional[error_utils.ErrorInfo]:
|
136
184
|
"""Returns error."""
|
137
185
|
return self._error
|
138
186
|
|
@@ -161,7 +209,8 @@ class TimeIt:
|
|
161
209
|
for child in self._child_contexts.values():
|
162
210
|
child_result = child.status()
|
163
211
|
for k, v in child_result.items():
|
164
|
-
|
212
|
+
key = f'{self.name}.{k}' if self.name else k
|
213
|
+
result[key] = v
|
165
214
|
return result
|
166
215
|
|
167
216
|
def __enter__(self):
|
@@ -182,6 +231,6 @@ class TimeIt:
|
|
182
231
|
thread_local.thread_local_set('__timing_context__', self._parent)
|
183
232
|
|
184
233
|
|
185
|
-
def timeit(name: str) -> TimeIt:
|
234
|
+
def timeit(name: str = '') -> TimeIt:
|
186
235
|
"""Context manager to time a block of code."""
|
187
236
|
return TimeIt(name)
|
@@ -15,14 +15,15 @@
|
|
15
15
|
import time
|
16
16
|
import unittest
|
17
17
|
|
18
|
-
from pyglove.core.object_utils import
|
18
|
+
from pyglove.core.object_utils import json_conversion
|
19
|
+
from pyglove.core.object_utils import timing
|
19
20
|
|
20
21
|
|
21
22
|
class TimeItTest(unittest.TestCase):
|
22
23
|
"""Tests for `pg.symbolic.thread_local`."""
|
23
24
|
|
24
25
|
def test_basics(self):
|
25
|
-
tc =
|
26
|
+
tc = timing.TimeIt('node')
|
26
27
|
self.assertFalse(tc.has_started)
|
27
28
|
self.assertEqual(tc.elapse, 0)
|
28
29
|
|
@@ -37,7 +38,7 @@ class TimeItTest(unittest.TestCase):
|
|
37
38
|
self.assertEqual(tc.elapse, elapse1)
|
38
39
|
|
39
40
|
def test_timeit(self):
|
40
|
-
with
|
41
|
+
with timing.timeit('node') as t:
|
41
42
|
self.assertEqual(t.name, 'node')
|
42
43
|
self.assertIsNotNone(t.start_time)
|
43
44
|
self.assertTrue(t.has_started)
|
@@ -46,9 +47,9 @@ class TimeItTest(unittest.TestCase):
|
|
46
47
|
time.sleep(0.5)
|
47
48
|
elapse1 = t.elapse
|
48
49
|
self.assertEqual(t.children, [])
|
49
|
-
with
|
50
|
+
with timing.timeit('child') as t1:
|
50
51
|
time.sleep(0.5)
|
51
|
-
with
|
52
|
+
with timing.timeit('grandchild') as t2:
|
52
53
|
time.sleep(0.5)
|
53
54
|
|
54
55
|
self.assertEqual(t.children, [t1])
|
@@ -63,7 +64,7 @@ class TimeItTest(unittest.TestCase):
|
|
63
64
|
self.assertTrue(r['node.child.grandchild'].has_ended)
|
64
65
|
|
65
66
|
with self.assertRaisesRegex(ValueError, '.* already exists'):
|
66
|
-
with
|
67
|
+
with timing.timeit('grandchild'):
|
67
68
|
pass
|
68
69
|
|
69
70
|
elapse2 = t.elapse
|
@@ -83,42 +84,49 @@ class TimeItTest(unittest.TestCase):
|
|
83
84
|
)
|
84
85
|
self.assertTrue(all(v.has_ended for v in statuss.values()))
|
85
86
|
self.assertFalse(any(v.has_error for v in statuss.values()))
|
87
|
+
status = t.status()
|
88
|
+
json_dict = json_conversion.to_json(status)
|
89
|
+
status2 = json_conversion.from_json(json_dict)
|
90
|
+
self.assertIsNot(status2, status)
|
91
|
+
self.assertEqual(status2, status)
|
86
92
|
|
87
93
|
def test_timeit_with_error(self):
|
88
94
|
with self.assertRaises(ValueError):
|
89
|
-
with
|
90
|
-
with
|
91
|
-
with
|
95
|
+
with timing.timeit('node') as t:
|
96
|
+
with timing.timeit('child') as t1:
|
97
|
+
with timing.timeit('grandchild') as t2:
|
92
98
|
raise ValueError('error')
|
93
99
|
|
94
100
|
r = t.status()
|
95
101
|
self.assertTrue(r['node'].has_error)
|
96
102
|
self.assertTrue(t.has_error)
|
97
|
-
self.
|
98
|
-
self.
|
103
|
+
self.assertTrue(t.error.tag.startswith('ValueError'))
|
104
|
+
self.assertTrue(r['node'].error.tag.startswith('ValueError'))
|
99
105
|
self.assertTrue(r['node.child'].has_error)
|
100
106
|
self.assertTrue(t1.has_error)
|
101
107
|
self.assertTrue(r['node.child.grandchild'].has_error)
|
102
108
|
self.assertTrue(t2.has_error)
|
103
109
|
|
104
110
|
def test_timeit_summary(self):
|
105
|
-
summary =
|
111
|
+
summary = timing.TimeIt.StatusSummary()
|
112
|
+
self.assertFalse(summary)
|
106
113
|
for i in range(10):
|
107
|
-
with
|
114
|
+
with timing.timeit() as t:
|
108
115
|
time.sleep(0.1)
|
109
|
-
with
|
116
|
+
with timing.timeit('child'):
|
110
117
|
time.sleep(0.1)
|
111
118
|
try:
|
112
|
-
with
|
119
|
+
with timing.timeit('grandchild'):
|
113
120
|
time.sleep(0.1)
|
114
121
|
if i < 2:
|
115
122
|
raise ValueError('error')
|
116
123
|
except ValueError:
|
117
124
|
pass
|
118
|
-
summary.aggregate(t)
|
125
|
+
summary.aggregate(t.status())
|
126
|
+
self.assertTrue(summary)
|
119
127
|
self.assertEqual(
|
120
128
|
list(summary.breakdown.keys()),
|
121
|
-
['
|
129
|
+
['', 'child', 'child.grandchild']
|
122
130
|
)
|
123
131
|
self.assertEqual(
|
124
132
|
[x.num_started for x in summary.breakdown.values()],
|
@@ -132,6 +140,15 @@ class TimeItTest(unittest.TestCase):
|
|
132
140
|
[x.num_failed for x in summary.breakdown.values()],
|
133
141
|
[0, 0, 2]
|
134
142
|
)
|
143
|
+
self.assertEqual(
|
144
|
+
summary.breakdown['child.grandchild'].error_tags,
|
145
|
+
{'ValueError': 2},
|
146
|
+
)
|
147
|
+
# Test serialization.
|
148
|
+
json_dict = summary.to_json()
|
149
|
+
summary2 = timing.TimeIt.StatusSummary.from_json(json_dict)
|
150
|
+
self.assertIsNot(summary2, summary)
|
151
|
+
self.assertEqual(summary2.breakdown, summary.breakdown)
|
135
152
|
|
136
153
|
|
137
154
|
if __name__ == '__main__':
|
@@ -120,6 +120,7 @@ from pyglove.core.symbolic.base import to_json
|
|
120
120
|
from pyglove.core.symbolic.base import to_json_str
|
121
121
|
from pyglove.core.symbolic.base import load
|
122
122
|
from pyglove.core.symbolic.base import save
|
123
|
+
from pyglove.core.symbolic.base import open_jsonl
|
123
124
|
|
124
125
|
# Interfaces for pure symbolic objects.
|
125
126
|
from pyglove.core.symbolic.pure_symbolic import PureSymbolic
|
pyglove/core/symbolic/base.py
CHANGED
@@ -2041,13 +2041,16 @@ def contains(
|
|
2041
2041
|
return not traverse(x, _contains)
|
2042
2042
|
|
2043
2043
|
|
2044
|
-
def from_json(
|
2045
|
-
|
2046
|
-
|
2047
|
-
|
2048
|
-
|
2049
|
-
|
2050
|
-
|
2044
|
+
def from_json(
|
2045
|
+
json_value: Any,
|
2046
|
+
*,
|
2047
|
+
allow_partial: bool = False,
|
2048
|
+
root_path: Optional[object_utils.KeyPath] = None,
|
2049
|
+
auto_import: bool = True,
|
2050
|
+
auto_dict: bool = False,
|
2051
|
+
value_spec: Optional[pg_typing.ValueSpec] = None,
|
2052
|
+
**kwargs
|
2053
|
+
) -> Any:
|
2051
2054
|
"""Deserializes a (maybe) symbolic value from JSON value.
|
2052
2055
|
|
2053
2056
|
Example::
|
@@ -2073,6 +2076,7 @@ def from_json(json_value: Any,
|
|
2073
2076
|
find the class 'A' within the imported module.
|
2074
2077
|
auto_dict: If True, dict with '_type' that cannot be loaded will remain
|
2075
2078
|
as dict, with '_type' renamed to 'type_name'.
|
2079
|
+
value_spec: The value spec for the symbolic list or dict.
|
2076
2080
|
**kwargs: Allow passing through keyword arguments to from_json of specific
|
2077
2081
|
types.
|
2078
2082
|
|
@@ -2093,10 +2097,15 @@ def from_json(json_value: Any,
|
|
2093
2097
|
json_value, auto_import=auto_import, auto_dict=auto_dict
|
2094
2098
|
)
|
2095
2099
|
|
2096
|
-
|
2097
|
-
|
2098
|
-
|
2099
|
-
|
2100
|
+
def _load_child(k, v):
|
2101
|
+
return from_json(
|
2102
|
+
v,
|
2103
|
+
root_path=object_utils.KeyPath(k, root_path),
|
2104
|
+
_typename_resolved=True,
|
2105
|
+
allow_partial=allow_partial,
|
2106
|
+
**kwargs
|
2107
|
+
)
|
2108
|
+
|
2100
2109
|
if isinstance(json_value, list):
|
2101
2110
|
if (json_value
|
2102
2111
|
and json_value[0] == object_utils.JSONConvertible.TUPLE_MARKER):
|
@@ -2106,21 +2115,27 @@ def from_json(json_value: Any,
|
|
2106
2115
|
f'Tuple should have at least one element '
|
2107
2116
|
f'besides \'{object_utils.JSONConvertible.TUPLE_MARKER}\'. '
|
2108
2117
|
f'Encountered: {json_value}', root_path))
|
2109
|
-
|
2110
|
-
|
2111
|
-
|
2112
|
-
|
2113
|
-
|
2114
|
-
|
2115
|
-
|
2116
|
-
|
2117
|
-
for i, v in enumerate(json_value[1:])
|
2118
|
-
])
|
2119
|
-
return Symbolic.ListType(json_value, **kwargs) # pytype: disable=not-callable # pylint: disable=not-callable
|
2118
|
+
return tuple(_load_child(i, v) for i, v in enumerate(json_value[1:]))
|
2119
|
+
return Symbolic.ListType.from_json( # pytype: disable=attribute-error
|
2120
|
+
json_value,
|
2121
|
+
value_spec=value_spec,
|
2122
|
+
root_path=root_path,
|
2123
|
+
allow_partial=allow_partial,
|
2124
|
+
**kwargs,
|
2125
|
+
)
|
2120
2126
|
elif isinstance(json_value, dict):
|
2121
2127
|
if object_utils.JSONConvertible.TYPE_NAME_KEY not in json_value:
|
2122
|
-
return Symbolic.DictType.from_json(
|
2123
|
-
|
2128
|
+
return Symbolic.DictType.from_json( # pytype: disable=attribute-error
|
2129
|
+
json_value,
|
2130
|
+
value_spec=value_spec,
|
2131
|
+
root_path=root_path,
|
2132
|
+
allow_partial=allow_partial,
|
2133
|
+
**kwargs,
|
2134
|
+
)
|
2135
|
+
return object_utils.from_json(
|
2136
|
+
json_value, _typename_resolved=True,
|
2137
|
+
root_path=root_path, allow_partial=allow_partial, **kwargs
|
2138
|
+
)
|
2124
2139
|
return json_value
|
2125
2140
|
|
2126
2141
|
|
@@ -2310,6 +2325,42 @@ def save(value: Any, path: str, *args, **kwargs) -> Any:
|
|
2310
2325
|
return save_handler(value, path, *args, **kwargs)
|
2311
2326
|
|
2312
2327
|
|
2328
|
+
def open_jsonl(
|
2329
|
+
path: str,
|
2330
|
+
mode: str = 'r',
|
2331
|
+
**kwargs
|
2332
|
+
) -> pg_io.Sequence:
|
2333
|
+
"""Open a JSONL file for reading or writing.
|
2334
|
+
|
2335
|
+
Example::
|
2336
|
+
|
2337
|
+
with pg.open_jsonl('my_file.jsonl', 'w') as f:
|
2338
|
+
f.add(1)
|
2339
|
+
f.add('foo')
|
2340
|
+
f.add(dict(x=1))
|
2341
|
+
|
2342
|
+
with pg.open_jsonl('my_file.jsonl', 'r') as f:
|
2343
|
+
for value in f:
|
2344
|
+
print(value)
|
2345
|
+
|
2346
|
+
Args:
|
2347
|
+
path: The path to the file.
|
2348
|
+
mode: The mode of the file.
|
2349
|
+
**kwargs: Additional keyword arguments that will be passed to
|
2350
|
+
``pg_io.open_sequence``.
|
2351
|
+
|
2352
|
+
Returns:
|
2353
|
+
A sequence for PyGlove objects.
|
2354
|
+
"""
|
2355
|
+
return pg_io.open_sequence(
|
2356
|
+
path,
|
2357
|
+
mode,
|
2358
|
+
serializer=to_json_str,
|
2359
|
+
deserializer=from_json_str,
|
2360
|
+
**kwargs
|
2361
|
+
)
|
2362
|
+
|
2363
|
+
|
2313
2364
|
def default_load_handler(
|
2314
2365
|
path: str,
|
2315
2366
|
file_format: Literal['json', 'txt'] = 'json',
|
pyglove/core/symbolic/dict.py
CHANGED
@@ -152,10 +152,19 @@ class Dict(dict, base.Symbolic, pg_typing.CustomTyping):
|
|
152
152
|
# Not okay:
|
153
153
|
d.a.f2.abc = 1
|
154
154
|
"""
|
155
|
-
return cls(
|
156
|
-
|
157
|
-
|
158
|
-
|
155
|
+
return cls(
|
156
|
+
{
|
157
|
+
k: base.from_json(
|
158
|
+
v,
|
159
|
+
root_path=object_utils.KeyPath(k, root_path),
|
160
|
+
allow_partial=allow_partial,
|
161
|
+
**kwargs
|
162
|
+
) for k, v in json_value.items()
|
163
|
+
},
|
164
|
+
value_spec=value_spec,
|
165
|
+
root_path=root_path,
|
166
|
+
allow_partial=allow_partial,
|
167
|
+
)
|
159
168
|
|
160
169
|
def __init__(self,
|
161
170
|
dict_obj: Union[
|
pyglove/core/symbolic/list.py
CHANGED
@@ -132,10 +132,19 @@ class List(list, base.Symbolic, pg_typing.CustomTyping):
|
|
132
132
|
Returns:
|
133
133
|
A schema-less symbolic list, but its items maybe symbolic.
|
134
134
|
"""
|
135
|
-
return cls(
|
136
|
-
|
137
|
-
|
138
|
-
|
135
|
+
return cls(
|
136
|
+
[
|
137
|
+
base.from_json(
|
138
|
+
v,
|
139
|
+
root_path=object_utils.KeyPath(i, root_path),
|
140
|
+
allow_partial=allow_partial,
|
141
|
+
**kwargs
|
142
|
+
) for i, v in enumerate(json_value)
|
143
|
+
],
|
144
|
+
value_spec=value_spec,
|
145
|
+
root_path=root_path,
|
146
|
+
allow_partial=allow_partial,
|
147
|
+
)
|
139
148
|
|
140
149
|
def __init__(
|
141
150
|
self,
|
pyglove/core/symbolic/object.py
CHANGED
@@ -543,7 +543,9 @@ class Object(base.Symbolic, metaclass=ObjectMeta):
|
|
543
543
|
json_value: Any,
|
544
544
|
*,
|
545
545
|
allow_partial: bool = False,
|
546
|
-
root_path: Optional[object_utils.KeyPath] = None
|
546
|
+
root_path: Optional[object_utils.KeyPath] = None,
|
547
|
+
**kwargs
|
548
|
+
) -> 'Object':
|
547
549
|
"""Class method that load an symbolic Object from a JSON value.
|
548
550
|
|
549
551
|
Example::
|
@@ -580,12 +582,13 @@ class Object(base.Symbolic, metaclass=ObjectMeta):
|
|
580
582
|
json_value: Input JSON value, only JSON dict is acceptable.
|
581
583
|
allow_partial: Whether to allow elements of the list to be partial.
|
582
584
|
root_path: KeyPath of loaded object in its object tree.
|
585
|
+
**kwargs: Additional keyword arguments to pass through.
|
583
586
|
|
584
587
|
Returns:
|
585
588
|
A symbolic Object instance.
|
586
589
|
"""
|
587
590
|
return cls(allow_partial=allow_partial, root_path=root_path, **{
|
588
|
-
k: base.from_json(v, allow_partial=allow_partial)
|
591
|
+
k: base.from_json(v, allow_partial=allow_partial, **kwargs)
|
589
592
|
for k, v in json_value.items()
|
590
593
|
})
|
591
594
|
|
pyglove/core/views/html/base.py
CHANGED
@@ -333,7 +333,11 @@ class Html(base.Content):
|
|
333
333
|
return s
|
334
334
|
|
335
335
|
@classmethod
|
336
|
-
def escape(
|
336
|
+
def escape(
|
337
|
+
cls,
|
338
|
+
s: WritableTypes,
|
339
|
+
javascript_str: bool = False
|
340
|
+
) -> WritableTypes:
|
337
341
|
"""Escapes an HTML writable object."""
|
338
342
|
if s is None:
|
339
343
|
return None
|
@@ -341,11 +345,22 @@ class Html(base.Content):
|
|
341
345
|
if callable(s):
|
342
346
|
s = s()
|
343
347
|
|
344
|
-
|
348
|
+
def _escape(s: str) -> str:
|
349
|
+
if javascript_str:
|
350
|
+
return (
|
351
|
+
s.replace('\\', '\\\\')
|
352
|
+
.replace('"', '\\"')
|
353
|
+
.replace('\r', '\\r')
|
354
|
+
.replace('\n', '\\n')
|
355
|
+
.replace('\t', '\\t')
|
356
|
+
)
|
345
357
|
return html_lib.escape(s)
|
358
|
+
|
359
|
+
if isinstance(s, str):
|
360
|
+
return _escape(s)
|
346
361
|
else:
|
347
362
|
assert isinstance(s, Html), s
|
348
|
-
return Html(
|
363
|
+
return Html(_escape(s.content)).write(
|
349
364
|
s, shared_parts_only=True
|
350
365
|
)
|
351
366
|
|
@@ -695,6 +695,8 @@ class HtmlTest(TestCase):
|
|
695
695
|
self.assertEqual(Html.escape('foo"bar'), 'foo"bar')
|
696
696
|
self.assertEqual(Html.escape(Html('foo"bar')), Html('foo"bar'))
|
697
697
|
self.assertEqual(Html.escape(lambda: 'foo"bar'), 'foo"bar')
|
698
|
+
self.assertEqual(Html.escape('"x=y"', javascript_str=True), '\\"x=y\\"')
|
699
|
+
self.assertEqual(Html.escape('x\n"', javascript_str=True), 'x\\n\\"')
|
698
700
|
|
699
701
|
def test_concate(self):
|
700
702
|
self.assertIsNone(Html.concate(None))
|
@@ -1215,6 +1215,7 @@ class HtmlTreeView(HtmlView):
|
|
1215
1215
|
parent: Any = None,
|
1216
1216
|
root_path: Optional[KeyPath] = None,
|
1217
1217
|
css_classes: Optional[Sequence[str]] = None,
|
1218
|
+
id: Optional[str] = None, # pylint: disable=redefined-builtin
|
1218
1219
|
content: Union[str, Html, None] = None,
|
1219
1220
|
**kwargs,
|
1220
1221
|
) -> Html:
|
@@ -1225,8 +1226,9 @@ class HtmlTreeView(HtmlView):
|
|
1225
1226
|
parent: The parent of the value.
|
1226
1227
|
root_path: The root path of the value.
|
1227
1228
|
css_classes: CSS classes to add to the HTML element.
|
1229
|
+
id: The ID of the tooltip span element. If None, no ID will be added.
|
1228
1230
|
content: The content to render. If None, the value will be rendered.
|
1229
|
-
**kwargs: Additional keyword arguments passed from the user that
|
1231
|
+
**kwargs: Additional keyword arguments passed from the user that
|
1230
1232
|
will be ignored.
|
1231
1233
|
|
1232
1234
|
Returns:
|
@@ -1248,6 +1250,7 @@ class HtmlTreeView(HtmlView):
|
|
1248
1250
|
return Html.element(
|
1249
1251
|
'span',
|
1250
1252
|
[content],
|
1253
|
+
id=id,
|
1251
1254
|
css_classes=[
|
1252
1255
|
'tooltip',
|
1253
1256
|
css_classes,
|
@@ -1266,6 +1269,9 @@ class HtmlTreeView(HtmlView):
|
|
1266
1269
|
position: absolute;
|
1267
1270
|
z-index: 1;
|
1268
1271
|
}
|
1272
|
+
span.tooltip:hover {
|
1273
|
+
visibility: visible;
|
1274
|
+
}
|
1269
1275
|
"""
|
1270
1276
|
)
|
1271
1277
|
|
@@ -74,6 +74,9 @@ class TooltipTest(TestCase):
|
|
74
74
|
position: absolute;
|
75
75
|
z-index: 1;
|
76
76
|
}
|
77
|
+
span.tooltip:hover {
|
78
|
+
visibility: visible;
|
79
|
+
}
|
77
80
|
</style>
|
78
81
|
"""
|
79
82
|
)
|
@@ -122,6 +125,9 @@ class SummaryTest(TestCase):
|
|
122
125
|
position: absolute;
|
123
126
|
z-index: 1;
|
124
127
|
}
|
128
|
+
span.tooltip:hover {
|
129
|
+
visibility: visible;
|
130
|
+
}
|
125
131
|
/* Summary styles. */
|
126
132
|
details.pyglove summary {
|
127
133
|
font-weight: bold;
|
@@ -314,6 +320,9 @@ class ContentTest(TestCase):
|
|
314
320
|
position: absolute;
|
315
321
|
z-index: 1;
|
316
322
|
}
|
323
|
+
span.tooltip:hover {
|
324
|
+
visibility: visible;
|
325
|
+
}
|
317
326
|
/* Summary styles. */
|
318
327
|
details.pyglove summary {
|
319
328
|
font-weight: bold;
|