pyglove 0.4.5.dev202410290809__py3-none-any.whl → 0.4.5.dev202410301815__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.
Potentially problematic release.
This version of pyglove might be problematic. Click here for more details.
- pyglove/core/__init__.py +1 -0
- pyglove/core/io/__init__.py +1 -0
- pyglove/core/io/file_system.py +10 -0
- pyglove/core/io/file_system_test.py +2 -0
- pyglove/core/io/sequence.py +301 -0
- pyglove/core/io/sequence_test.py +137 -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/{profiling.py → timing.py} +48 -10
- pyglove/core/object_utils/{profiling_test.py → timing_test.py} +27 -14
- pyglove/core/symbolic/__init__.py +1 -0
- pyglove/core/symbolic/base.py +36 -0
- 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.dev202410301815.dist-info}/METADATA +1 -1
- {pyglove-0.4.5.dev202410290809.dist-info → pyglove-0.4.5.dev202410301815.dist-info}/RECORD +22 -20
- {pyglove-0.4.5.dev202410290809.dist-info → pyglove-0.4.5.dev202410301815.dist-info}/WHEEL +1 -1
- {pyglove-0.4.5.dev202410290809.dist-info → pyglove-0.4.5.dev202410301815.dist-info}/LICENSE +0 -0
- {pyglove-0.4.5.dev202410290809.dist-info → pyglove-0.4.5.dev202410301815.dist-info}/top_level.txt +0 -0
pyglove/core/__init__.py
CHANGED
@@ -163,6 +163,7 @@ to_json = symbolic.to_json
|
|
163
163
|
to_json_str = symbolic.to_json_str
|
164
164
|
save = symbolic.save
|
165
165
|
load = symbolic.load
|
166
|
+
open_jsonl = symbolic.open_jsonl
|
166
167
|
get_load_handler = symbolic.get_load_handler
|
167
168
|
set_load_handler = symbolic.set_load_handler
|
168
169
|
get_save_handler = symbolic.get_save_handler
|
pyglove/core/io/__init__.py
CHANGED
pyglove/core/io/file_system.py
CHANGED
@@ -51,6 +51,10 @@ class File(metaclass=abc.ABCMeta):
|
|
51
51
|
def tell(self) -> int:
|
52
52
|
"""Returns the current position of the file."""
|
53
53
|
|
54
|
+
@abc.abstractmethod
|
55
|
+
def flush(self) -> None:
|
56
|
+
"""Flushes the written content to the storage."""
|
57
|
+
|
54
58
|
@abc.abstractmethod
|
55
59
|
def close(self) -> None:
|
56
60
|
"""Closes the file."""
|
@@ -148,6 +152,9 @@ class StdFile(File):
|
|
148
152
|
def tell(self) -> int:
|
149
153
|
return self._file_object.tell()
|
150
154
|
|
155
|
+
def flush(self) -> None:
|
156
|
+
self._file_object.flush()
|
157
|
+
|
151
158
|
def close(self) -> None:
|
152
159
|
self._file_object.close()
|
153
160
|
|
@@ -220,6 +227,9 @@ class MemoryFile(File):
|
|
220
227
|
def tell(self) -> int:
|
221
228
|
return self._buffer.tell()
|
222
229
|
|
230
|
+
def flush(self) -> None:
|
231
|
+
pass
|
232
|
+
|
223
233
|
def close(self) -> None:
|
224
234
|
self.seek(0)
|
225
235
|
|
@@ -133,6 +133,7 @@ class MemoryFileSystemTest(unittest.TestCase):
|
|
133
133
|
self.assertFalse(fs.exists(file1))
|
134
134
|
with fs.open(file1, 'w') as f:
|
135
135
|
f.write('hello')
|
136
|
+
f.flush()
|
136
137
|
self.assertTrue(fs.exists(file1))
|
137
138
|
self.assertFalse(fs.isdir(file1))
|
138
139
|
|
@@ -224,6 +225,7 @@ class FileIoApiTest(unittest.TestCase):
|
|
224
225
|
with file_system.open(file1, 'w') as f:
|
225
226
|
self.assertIsInstance(f, file_system.MemoryFile)
|
226
227
|
f.write('foo')
|
228
|
+
f.flush()
|
227
229
|
self.assertTrue(file_system.path_exists(file1))
|
228
230
|
self.assertEqual(file_system.readfile(file1), 'foo')
|
229
231
|
file_system.writefile(file1, 'bar')
|
@@ -0,0 +1,301 @@
|
|
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
|
+
"""Pluggable record IO."""
|
15
|
+
|
16
|
+
import abc
|
17
|
+
import collections
|
18
|
+
import os
|
19
|
+
from typing import Any, Callable, Iterator, Optional, Union
|
20
|
+
|
21
|
+
from pyglove.core.io import file_system
|
22
|
+
|
23
|
+
|
24
|
+
class Sequence(metaclass=abc.ABCMeta):
|
25
|
+
"""Interface for a sequence of records."""
|
26
|
+
|
27
|
+
def __init__(
|
28
|
+
self,
|
29
|
+
serializer: Optional[Callable[[Any], Union[bytes, str]]] = None,
|
30
|
+
deserializer: Optional[Callable[[Union[bytes, str]], Any]] = None
|
31
|
+
):
|
32
|
+
self._serializer = serializer
|
33
|
+
self._deserializer = deserializer
|
34
|
+
|
35
|
+
def add(self, record: Any) -> None:
|
36
|
+
"""Adds a record to the reader."""
|
37
|
+
if self._serializer:
|
38
|
+
record = self._serializer(record)
|
39
|
+
if not isinstance(record, (str, bytes)):
|
40
|
+
raise ValueError(
|
41
|
+
f'Cannot write record with type {type(record)}. '
|
42
|
+
'Did you forget to pass a serializer?'
|
43
|
+
)
|
44
|
+
self._add(record)
|
45
|
+
|
46
|
+
@abc.abstractmethod
|
47
|
+
def _add(self, record: Union[bytes, str]) -> None:
|
48
|
+
"""Adds a raw record to the reader."""
|
49
|
+
|
50
|
+
@abc.abstractmethod
|
51
|
+
def __len__(self):
|
52
|
+
"""Gets the number of records in the reader."""
|
53
|
+
|
54
|
+
def __iter__(self) -> Iterator[Any]:
|
55
|
+
"""Iterates over the records in the reader."""
|
56
|
+
for record in self._iter():
|
57
|
+
if self._deserializer:
|
58
|
+
yield self._deserializer(record)
|
59
|
+
else:
|
60
|
+
yield record
|
61
|
+
|
62
|
+
@abc.abstractmethod
|
63
|
+
def _iter(self) -> Iterator[Union[bytes, str]]:
|
64
|
+
"""Iterates over the raw records in the reader."""
|
65
|
+
|
66
|
+
@abc.abstractmethod
|
67
|
+
def close(self) -> None:
|
68
|
+
"""Closes the reader."""
|
69
|
+
|
70
|
+
@abc.abstractmethod
|
71
|
+
def flush(self) -> None:
|
72
|
+
"""Flushes the read records to the storage."""
|
73
|
+
|
74
|
+
def __enter__(self):
|
75
|
+
return self
|
76
|
+
|
77
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
78
|
+
del exc_type, exc_value, traceback
|
79
|
+
self.close()
|
80
|
+
|
81
|
+
|
82
|
+
class SequenceIO(metaclass=abc.ABCMeta):
|
83
|
+
"""Interface for a record IO system."""
|
84
|
+
|
85
|
+
@abc.abstractmethod
|
86
|
+
def open(
|
87
|
+
self,
|
88
|
+
path: Union[str, os.PathLike[str]],
|
89
|
+
mode: str,
|
90
|
+
*,
|
91
|
+
serializer: Optional[Callable[[Any], Union[bytes, str]]],
|
92
|
+
deserializer: Optional[Callable[[Union[bytes, str]], Any]],
|
93
|
+
**kwargs
|
94
|
+
) -> Sequence:
|
95
|
+
"""Opens a sequence for reading or writing."""
|
96
|
+
|
97
|
+
|
98
|
+
class _SequenceIORegistry(object):
|
99
|
+
"""Registry for record IO systems."""
|
100
|
+
|
101
|
+
def __init__(self):
|
102
|
+
self._registry = {}
|
103
|
+
|
104
|
+
def add(self, extension: str, sequence_io: SequenceIO) -> None:
|
105
|
+
"""Adds a record IO system with a prefix."""
|
106
|
+
self._registry[extension] = sequence_io
|
107
|
+
|
108
|
+
def get(self, path: Union[str, os.PathLike[str]]) -> SequenceIO:
|
109
|
+
"""Gets the record IO system for a path."""
|
110
|
+
path = _resolve_path(path)
|
111
|
+
parts = path.split('.')
|
112
|
+
if parts:
|
113
|
+
extension = parts[-1].lower()
|
114
|
+
if '@' in extension:
|
115
|
+
extension = extension.split('@')[0]
|
116
|
+
if extension in self._registry:
|
117
|
+
return self._registry[extension]
|
118
|
+
return LineSequenceIO()
|
119
|
+
|
120
|
+
|
121
|
+
_registry = _SequenceIORegistry()
|
122
|
+
|
123
|
+
|
124
|
+
def add_sequence_io(extension: str, sequence_io: SequenceIO) -> None:
|
125
|
+
"""Adds a record IO system with a prefix."""
|
126
|
+
_registry.add(extension, sequence_io)
|
127
|
+
|
128
|
+
|
129
|
+
def _resolve_path(path: Union[str, os.PathLike[str]]) -> str:
|
130
|
+
if isinstance(path, str):
|
131
|
+
return path
|
132
|
+
elif hasattr(path, '__fspath__'):
|
133
|
+
return path.__fspath__()
|
134
|
+
else:
|
135
|
+
raise ValueError(f'Unsupported path: {path!r}.')
|
136
|
+
|
137
|
+
|
138
|
+
def open_sequence(
|
139
|
+
path: Union[str, os.PathLike[str]],
|
140
|
+
mode: str = 'r',
|
141
|
+
*,
|
142
|
+
serializer: Optional[
|
143
|
+
Callable[[Any], Union[bytes, str]]
|
144
|
+
] = None,
|
145
|
+
deserializer: Optional[
|
146
|
+
Callable[[Union[bytes, str]], Any]
|
147
|
+
] = None,
|
148
|
+
) -> Sequence:
|
149
|
+
"""Open sequence for reading or writing.
|
150
|
+
|
151
|
+
Args:
|
152
|
+
path: The path to the sequence.
|
153
|
+
mode: The mode of the sequence.
|
154
|
+
serializer: (Optional) A serializer function for converting a structured
|
155
|
+
object to a string or bytes.
|
156
|
+
deserializer: (Optional) A deserializer function for converting a string or
|
157
|
+
bytes to a structured object.
|
158
|
+
|
159
|
+
Returns:
|
160
|
+
A sequence for reading or writing.
|
161
|
+
"""
|
162
|
+
return _registry.get(path).open(
|
163
|
+
path, mode, serializer=serializer, deserializer=deserializer
|
164
|
+
)
|
165
|
+
|
166
|
+
|
167
|
+
class MemorySequence(Sequence):
|
168
|
+
"""An in-memory sequence."""
|
169
|
+
|
170
|
+
def __init__(
|
171
|
+
self,
|
172
|
+
path: str,
|
173
|
+
mode: str,
|
174
|
+
records: list[Union[str, bytes]],
|
175
|
+
serializer: Optional[Callable[[Any], Union[bytes, str]]],
|
176
|
+
deserializer: Optional[Callable[[Union[bytes, str]], Any]]
|
177
|
+
):
|
178
|
+
super().__init__(serializer, deserializer)
|
179
|
+
self._path = path
|
180
|
+
self._mode = mode
|
181
|
+
self._records = records
|
182
|
+
self._closed = False
|
183
|
+
|
184
|
+
def _add(self, record: Union[str, bytes]) -> None:
|
185
|
+
if 'w' not in self._mode and 'a' not in self._mode:
|
186
|
+
raise ValueError(
|
187
|
+
f'Cannot write record {record!r} to memory sequence {self._path!r} '
|
188
|
+
f'with mode {self._mode!r}.'
|
189
|
+
)
|
190
|
+
if self._closed:
|
191
|
+
raise ValueError(
|
192
|
+
f'Cannot write record {record!r} to a closed writer for '
|
193
|
+
f'{self._path!r}.'
|
194
|
+
)
|
195
|
+
self._records.append(record)
|
196
|
+
|
197
|
+
def __len__(self):
|
198
|
+
return len(self._records)
|
199
|
+
|
200
|
+
def _iter(self):
|
201
|
+
if 'r' not in self._mode:
|
202
|
+
raise ValueError(
|
203
|
+
f'Cannot read memory sequence {self._path!r} with '
|
204
|
+
f'mode {self._mode!r}.'
|
205
|
+
)
|
206
|
+
if self._closed:
|
207
|
+
raise ValueError(
|
208
|
+
f'Cannot iterate over a closed sequence reader {self._path!r}.'
|
209
|
+
)
|
210
|
+
return iter(self._records)
|
211
|
+
|
212
|
+
def flush(self):
|
213
|
+
pass
|
214
|
+
|
215
|
+
def close(self) -> None:
|
216
|
+
self._closed = True
|
217
|
+
|
218
|
+
|
219
|
+
class MemorySequenceIO(SequenceIO):
|
220
|
+
"""Memory-based record IO."""
|
221
|
+
|
222
|
+
def __init__(self):
|
223
|
+
super().__init__()
|
224
|
+
self._root = collections.defaultdict(list)
|
225
|
+
|
226
|
+
def open(
|
227
|
+
self,
|
228
|
+
path: Union[str, os.PathLike[str]],
|
229
|
+
mode: str,
|
230
|
+
*,
|
231
|
+
serializer: Optional[Callable[[Any], Union[bytes, str]]],
|
232
|
+
deserializer: Optional[Callable[[Union[bytes, str]], Any]],
|
233
|
+
**kwargs
|
234
|
+
) -> Sequence:
|
235
|
+
"""Opens a reader for a sequence."""
|
236
|
+
del kwargs
|
237
|
+
if 'w' in mode:
|
238
|
+
self._root[path] = []
|
239
|
+
return MemorySequence(
|
240
|
+
path, mode, self._root[path],
|
241
|
+
serializer=serializer, deserializer=deserializer
|
242
|
+
)
|
243
|
+
|
244
|
+
|
245
|
+
add_sequence_io('mem', MemorySequenceIO())
|
246
|
+
|
247
|
+
|
248
|
+
class LineSequence(Sequence):
|
249
|
+
"""A new-line broken sequence."""
|
250
|
+
|
251
|
+
def __init__(
|
252
|
+
self,
|
253
|
+
file: file_system.File,
|
254
|
+
serializer: Optional[Callable[[Any], Union[bytes, str]]],
|
255
|
+
deserializer: Optional[Callable[[Union[bytes, str]], Any]],
|
256
|
+
) -> None:
|
257
|
+
super().__init__(serializer, deserializer)
|
258
|
+
self._file = file
|
259
|
+
|
260
|
+
def __len__(self):
|
261
|
+
raise NotImplementedError(
|
262
|
+
'__len__ is not supported for LineSequence. '
|
263
|
+
'Use `len(list(iter(sequence)))` instead.'
|
264
|
+
)
|
265
|
+
|
266
|
+
def _iter(self):
|
267
|
+
while True:
|
268
|
+
line = self._file.readline()
|
269
|
+
if not line:
|
270
|
+
break
|
271
|
+
yield line.rstrip('\n')
|
272
|
+
|
273
|
+
def _add(self, record: Union[str, bytes]) -> None:
|
274
|
+
self._file.write(record.rstrip('\n'))
|
275
|
+
self._file.write('\n')
|
276
|
+
|
277
|
+
def flush(self) -> None:
|
278
|
+
self._file.flush()
|
279
|
+
|
280
|
+
def close(self) -> None:
|
281
|
+
self._file.close()
|
282
|
+
|
283
|
+
|
284
|
+
class LineSequenceIO(SequenceIO):
|
285
|
+
"""Line-based record IO."""
|
286
|
+
|
287
|
+
def open(
|
288
|
+
self,
|
289
|
+
path: Union[str, os.PathLike[str]],
|
290
|
+
mode: str,
|
291
|
+
*,
|
292
|
+
serializer: Optional[Callable[[Any], Union[bytes, str]]],
|
293
|
+
deserializer: Optional[Callable[[Union[bytes, str]], Any]],
|
294
|
+
**kwargs
|
295
|
+
) -> Sequence:
|
296
|
+
"""Opens a reader for a sequence."""
|
297
|
+
del kwargs
|
298
|
+
return LineSequence(
|
299
|
+
file_system.open(path, mode), serializer, deserializer
|
300
|
+
)
|
301
|
+
|
@@ -0,0 +1,137 @@
|
|
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
|
+
|
15
|
+
import os
|
16
|
+
import pathlib
|
17
|
+
import tempfile
|
18
|
+
import unittest
|
19
|
+
from pyglove.core.io import sequence as sequence_io
|
20
|
+
# We need to import this module to register the default parser/serializer.
|
21
|
+
import pyglove.core.symbolic as pg_symbolic
|
22
|
+
|
23
|
+
|
24
|
+
class LineSequenceIOTest(unittest.TestCase):
|
25
|
+
|
26
|
+
def test_read_write(self):
|
27
|
+
tmp_dir = tempfile.gettempdir()
|
28
|
+
file1 = os.path.join(tmp_dir, 'file1')
|
29
|
+
with pg_symbolic.open_jsonl(file1, 'w') as f:
|
30
|
+
self.assertIsInstance(f, sequence_io.LineSequence)
|
31
|
+
f.add(1)
|
32
|
+
f.add(' foo')
|
33
|
+
f.add(' bar ')
|
34
|
+
f.flush()
|
35
|
+
self.assertTrue(os.path.exists(file1))
|
36
|
+
|
37
|
+
f.add('baz\n')
|
38
|
+
f.add(dict(x=1))
|
39
|
+
|
40
|
+
self.assertTrue(os.path.exists(file1))
|
41
|
+
with pg_symbolic.open_jsonl(file1, 'r') as f:
|
42
|
+
self.assertIsInstance(f, sequence_io.LineSequence)
|
43
|
+
self.assertEqual(list(iter(f)), [1, ' foo', ' bar ', 'baz\n', dict(x=1)])
|
44
|
+
|
45
|
+
with pg_symbolic.open_jsonl(file1, 'a') as f:
|
46
|
+
f.add('qux')
|
47
|
+
|
48
|
+
with pg_symbolic.open_jsonl(file1, 'r') as f:
|
49
|
+
with self.assertRaisesRegex(
|
50
|
+
NotImplementedError, '__len__ is not supported'
|
51
|
+
):
|
52
|
+
_ = len(f)
|
53
|
+
self.assertEqual(
|
54
|
+
list(iter(f)), [1, ' foo', ' bar ', 'baz\n', dict(x=1), 'qux']
|
55
|
+
)
|
56
|
+
|
57
|
+
def test_read_write_with_raw_texts(self):
|
58
|
+
tmp_dir = tempfile.gettempdir()
|
59
|
+
file2 = os.path.join(tmp_dir, 'file2')
|
60
|
+
with sequence_io.open_sequence(file2, 'w') as f:
|
61
|
+
self.assertIsInstance(f, sequence_io.LineSequence)
|
62
|
+
with self.assertRaisesRegex(
|
63
|
+
ValueError, 'Cannot write record with type'
|
64
|
+
):
|
65
|
+
f.add(1)
|
66
|
+
f.add('foo\nbar\n')
|
67
|
+
|
68
|
+
with sequence_io.open_sequence(file2, 'r') as f:
|
69
|
+
with self.assertRaisesRegex(
|
70
|
+
ValueError, 'not writable'
|
71
|
+
):
|
72
|
+
f.add('baz')
|
73
|
+
self.assertEqual(list(iter(f)), ['foo', 'bar'])
|
74
|
+
|
75
|
+
|
76
|
+
class MemoryRecordIOIOTest(unittest.TestCase):
|
77
|
+
|
78
|
+
def test_resolve_path(self):
|
79
|
+
self.assertEqual(
|
80
|
+
sequence_io._resolve_path('/file1.mem@123'),
|
81
|
+
'/file1.mem@123'
|
82
|
+
)
|
83
|
+
self.assertEqual(
|
84
|
+
sequence_io._resolve_path(pathlib.Path('/file1.mem@123.txt')),
|
85
|
+
'/file1.mem@123.txt'
|
86
|
+
)
|
87
|
+
with self.assertRaisesRegex(ValueError, 'Unsupported path'):
|
88
|
+
sequence_io._resolve_path(1)
|
89
|
+
|
90
|
+
def test_read_write(self):
|
91
|
+
with sequence_io.open_sequence('/file1.mem@123', 'w') as f:
|
92
|
+
self.assertIsInstance(f, sequence_io.MemorySequence)
|
93
|
+
f.add(' foo')
|
94
|
+
f.add(' bar ')
|
95
|
+
f.flush()
|
96
|
+
f.add('baz')
|
97
|
+
with self.assertRaisesRegex(
|
98
|
+
ValueError, 'Cannot write record with type'
|
99
|
+
):
|
100
|
+
f.add(1)
|
101
|
+
with self.assertRaisesRegex(
|
102
|
+
ValueError, 'Cannot read memory sequence'
|
103
|
+
):
|
104
|
+
next(iter(f))
|
105
|
+
|
106
|
+
with self.assertRaisesRegex(
|
107
|
+
ValueError, 'Cannot write record .* to a closed writer'
|
108
|
+
):
|
109
|
+
f.add('qux')
|
110
|
+
|
111
|
+
with sequence_io.open_sequence('/file1.mem@123', 'a') as f:
|
112
|
+
self.assertIsInstance(f, sequence_io.MemorySequence)
|
113
|
+
f.add('qux')
|
114
|
+
|
115
|
+
with sequence_io.open_sequence('/file1.mem@123') as f:
|
116
|
+
self.assertIsInstance(f, sequence_io.MemorySequence)
|
117
|
+
self.assertEqual(len(f), 4)
|
118
|
+
self.assertEqual(list(f), [' foo', ' bar ', 'baz', 'qux'])
|
119
|
+
with self.assertRaisesRegex(
|
120
|
+
ValueError, 'Cannot write record .* to memory sequence'
|
121
|
+
):
|
122
|
+
f.add('abc')
|
123
|
+
|
124
|
+
with self.assertRaisesRegex(
|
125
|
+
ValueError, 'Cannot iterate over a closed sequence reader'
|
126
|
+
):
|
127
|
+
next(iter(f))
|
128
|
+
|
129
|
+
with sequence_io.open_sequence('/file1.mem@123', 'w') as f:
|
130
|
+
f.add('abc')
|
131
|
+
|
132
|
+
with sequence_io.open_sequence('/file1.mem@123', 'r') as f:
|
133
|
+
self.assertEqual(list(iter(f)), ['abc'])
|
134
|
+
|
135
|
+
|
136
|
+
if __name__ == '__main__':
|
137
|
+
unittest.main()
|
@@ -154,10 +154,11 @@ from pyglove.core.object_utils.docstr_utils import docstr
|
|
154
154
|
# Handling exceptions.
|
155
155
|
from pyglove.core.object_utils.error_utils import catch_errors
|
156
156
|
from pyglove.core.object_utils.error_utils import CatchErrorsContext
|
157
|
+
from pyglove.core.object_utils.error_utils import ErrorInfo
|
157
158
|
|
158
|
-
#
|
159
|
-
from pyglove.core.object_utils.
|
160
|
-
from pyglove.core.object_utils.
|
159
|
+
# Timing.
|
160
|
+
from pyglove.core.object_utils.timing import timeit
|
161
|
+
from pyglove.core.object_utils.timing import TimeIt
|
161
162
|
|
162
163
|
# pylint: enable=g-importing-member
|
163
164
|
# pylint: enable=g-bad-import-order
|
@@ -17,7 +17,71 @@ import contextlib
|
|
17
17
|
import dataclasses
|
18
18
|
import inspect
|
19
19
|
import re
|
20
|
-
|
20
|
+
import sys
|
21
|
+
import traceback
|
22
|
+
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Type, Union
|
23
|
+
|
24
|
+
from pyglove.core.object_utils import formatting
|
25
|
+
from pyglove.core.object_utils import json_conversion
|
26
|
+
|
27
|
+
|
28
|
+
@dataclasses.dataclass(frozen=True)
|
29
|
+
class ErrorInfo(json_conversion.JSONConvertible, formatting.Formattable):
|
30
|
+
"""Serializable error information.
|
31
|
+
|
32
|
+
Attributes:
|
33
|
+
type: The type path of the error. For example,
|
34
|
+
`ValueError.ZeroDivisionError` means the error is a `ZeroDivisionError`
|
35
|
+
raised at the first place and then reraised as a `ValueError`.
|
36
|
+
description: The description of the error.
|
37
|
+
stacktrace: The stacktrace of the error.
|
38
|
+
"""
|
39
|
+
|
40
|
+
type: str
|
41
|
+
description: str
|
42
|
+
stacktrace: str
|
43
|
+
|
44
|
+
@classmethod
|
45
|
+
def _get_type(cls, error: BaseException):
|
46
|
+
error_types = []
|
47
|
+
while error is not None:
|
48
|
+
error_types.append(error.__class__.__name__)
|
49
|
+
error = getattr(error, 'cause', error.__cause__)
|
50
|
+
return '.'.join(error_types)
|
51
|
+
|
52
|
+
@classmethod
|
53
|
+
def from_exception(cls, error: BaseException) -> 'ErrorInfo':
|
54
|
+
"""Creates an error info from an exception."""
|
55
|
+
return cls(
|
56
|
+
type=cls._get_type(error),
|
57
|
+
description=str(error),
|
58
|
+
stacktrace=''.join(
|
59
|
+
traceback.format_exception(*sys.exc_info())
|
60
|
+
)
|
61
|
+
)
|
62
|
+
|
63
|
+
def to_json(self, **kwargs) -> Dict[str, Any]:
|
64
|
+
return self.to_json_dict(
|
65
|
+
fields=dict(
|
66
|
+
type=(self.type, None),
|
67
|
+
description=(self.description, None),
|
68
|
+
stacktrace=(self.stacktrace, None),
|
69
|
+
),
|
70
|
+
exclude_default=True,
|
71
|
+
**kwargs,
|
72
|
+
)
|
73
|
+
|
74
|
+
def format(self, *args, **kwargs) -> str:
|
75
|
+
return formatting.kvlist_str(
|
76
|
+
[
|
77
|
+
('type', self.type, None),
|
78
|
+
('description', self.description, None),
|
79
|
+
('stacktrace', self.stacktrace, None),
|
80
|
+
],
|
81
|
+
*args,
|
82
|
+
label=self.__class__.__name__,
|
83
|
+
**kwargs,
|
84
|
+
)
|
21
85
|
|
22
86
|
|
23
87
|
@dataclasses.dataclass()
|
@@ -11,12 +11,66 @@
|
|
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
|
-
|
15
|
-
|
14
|
+
import inspect
|
16
15
|
import unittest
|
17
16
|
from pyglove.core.object_utils import error_utils
|
18
17
|
|
19
18
|
|
19
|
+
class ErrorInfoTest(unittest.TestCase):
|
20
|
+
"""Tests for ErrorInfo."""
|
21
|
+
|
22
|
+
def test_from_exception(self):
|
23
|
+
|
24
|
+
def foo():
|
25
|
+
return 1 / 0
|
26
|
+
|
27
|
+
def bar():
|
28
|
+
try:
|
29
|
+
foo()
|
30
|
+
except ZeroDivisionError as e:
|
31
|
+
raise ValueError('Bad call to `foo`') from e
|
32
|
+
|
33
|
+
error_info = None
|
34
|
+
try:
|
35
|
+
bar()
|
36
|
+
except ValueError as e:
|
37
|
+
error_info = error_utils.ErrorInfo.from_exception(e)
|
38
|
+
self.assertIsNotNone(error_info)
|
39
|
+
self.assertEqual(error_info.type, 'ValueError.ZeroDivisionError')
|
40
|
+
self.assertEqual(error_info.description, 'Bad call to `foo`')
|
41
|
+
self.assertIn('Traceback (most recent call last)', error_info.stacktrace)
|
42
|
+
|
43
|
+
def test_to_json(self):
|
44
|
+
error_info = error_utils.ErrorInfo(
|
45
|
+
type='ValueError.ZeroDivisionError',
|
46
|
+
description='Bad call to `foo`',
|
47
|
+
stacktrace='Traceback (most recent call last)',
|
48
|
+
)
|
49
|
+
json_dict = error_info.to_json()
|
50
|
+
error_info2 = error_utils.ErrorInfo.from_json(json_dict)
|
51
|
+
self.assertIsNot(error_info2, error_info)
|
52
|
+
self.assertEqual(error_info2, error_info)
|
53
|
+
|
54
|
+
def test_format(self):
|
55
|
+
error_info = error_utils.ErrorInfo(
|
56
|
+
type='ValueError.ZeroDivisionError',
|
57
|
+
description='Bad call to `foo`',
|
58
|
+
stacktrace='Traceback (most recent call last)',
|
59
|
+
)
|
60
|
+
self.assertEqual(
|
61
|
+
error_info.format(compact=False),
|
62
|
+
inspect.cleandoc(
|
63
|
+
"""
|
64
|
+
ErrorInfo(
|
65
|
+
type='ValueError.ZeroDivisionError',
|
66
|
+
description='Bad call to `foo`',
|
67
|
+
stacktrace='Traceback (most recent call last)'
|
68
|
+
)
|
69
|
+
"""
|
70
|
+
)
|
71
|
+
)
|
72
|
+
|
73
|
+
|
20
74
|
class CatchErrorsTest(unittest.TestCase):
|
21
75
|
|
22
76
|
def assert_caught_error(self, func, errors_to_catch):
|
@@ -11,12 +11,13 @@
|
|
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
16
|
import dataclasses
|
17
17
|
import time
|
18
|
-
from typing import Dict, List, Optional
|
18
|
+
from typing import Any, Dict, List, Optional
|
19
19
|
|
20
|
+
from pyglove.core.object_utils import json_conversion
|
20
21
|
from pyglove.core.object_utils import thread_local
|
21
22
|
|
22
23
|
|
@@ -24,12 +25,12 @@ class TimeIt:
|
|
24
25
|
"""Context manager for timing the execution of a code block."""
|
25
26
|
|
26
27
|
@dataclasses.dataclass(frozen=True)
|
27
|
-
class Status:
|
28
|
+
class Status(json_conversion.JSONConvertible):
|
28
29
|
"""Status of a single `pg.timeit`."""
|
29
30
|
name: str
|
30
|
-
elapse: float
|
31
|
-
has_ended: bool
|
32
|
-
error: Optional[Exception]
|
31
|
+
elapse: float = 0.0
|
32
|
+
has_ended: bool = True
|
33
|
+
error: Optional[Exception] = None
|
33
34
|
|
34
35
|
@property
|
35
36
|
def has_started(self) -> bool:
|
@@ -41,12 +42,24 @@ class TimeIt:
|
|
41
42
|
"""Returns whether the context has error."""
|
42
43
|
return self.error is not None
|
43
44
|
|
45
|
+
def to_json(self, **kwargs) -> Dict[str, Any]:
|
46
|
+
return self.to_json_dict(
|
47
|
+
fields=dict(
|
48
|
+
name=(self.name, None),
|
49
|
+
elapse=(self.elapse, 0.0),
|
50
|
+
has_ended=(self.has_ended, True),
|
51
|
+
error=(self.error, None),
|
52
|
+
),
|
53
|
+
exclude_default=True,
|
54
|
+
**kwargs,
|
55
|
+
)
|
56
|
+
|
44
57
|
@dataclasses.dataclass
|
45
|
-
class StatusSummary:
|
58
|
+
class StatusSummary(json_conversion.JSONConvertible):
|
46
59
|
"""Aggregated summary for repeated calls for `pg.timeit`."""
|
47
60
|
|
48
61
|
@dataclasses.dataclass
|
49
|
-
class Entry:
|
62
|
+
class Entry(json_conversion.JSONConvertible):
|
50
63
|
"""Aggregated status from the `pg.timeit` calls of the same name."""
|
51
64
|
|
52
65
|
num_started: int = 0
|
@@ -65,16 +78,41 @@ class TimeIt:
|
|
65
78
|
if status.has_error:
|
66
79
|
self.num_failed += 1
|
67
80
|
|
81
|
+
def to_json(self, **kwargs) -> Dict[str, Any]:
|
82
|
+
return self.to_json_dict(
|
83
|
+
fields=dict(
|
84
|
+
num_started=(self.num_started, 0),
|
85
|
+
num_ended=(self.num_ended, 0),
|
86
|
+
num_failed=(self.num_failed, 0),
|
87
|
+
avg_duration=(self.avg_duration, 0.0),
|
88
|
+
),
|
89
|
+
exclude_default=True,
|
90
|
+
**kwargs,
|
91
|
+
)
|
92
|
+
|
68
93
|
breakdown: dict[str, 'TimeIt.StatusSummary.Entry'] = (
|
69
94
|
dataclasses.field(default_factory=dict)
|
70
95
|
)
|
71
96
|
|
72
|
-
def
|
73
|
-
|
97
|
+
def __bool__(self) -> bool:
|
98
|
+
"""Returns True if the summary is non-empty."""
|
99
|
+
return bool(self.breakdown)
|
100
|
+
|
101
|
+
def aggregate(self, timeit_status: Dict[str, 'TimeIt.Status']):
|
102
|
+
for k, v in timeit_status.items():
|
74
103
|
if k not in self.breakdown:
|
75
104
|
self.breakdown[k] = TimeIt.StatusSummary.Entry()
|
76
105
|
self.breakdown[k].update(v)
|
77
106
|
|
107
|
+
def to_json(self, **kwargs) -> Dict[str, Any]:
|
108
|
+
return self.to_json_dict(
|
109
|
+
fields=dict(
|
110
|
+
breakdown=(self.breakdown, {}),
|
111
|
+
),
|
112
|
+
exclude_default=True,
|
113
|
+
**kwargs,
|
114
|
+
)
|
115
|
+
|
78
116
|
def __init__(self, name: str):
|
79
117
|
self._name = name
|
80
118
|
self._start_time = None
|
@@ -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,12 +84,17 @@ 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()
|
@@ -102,20 +108,22 @@ class TimeItTest(unittest.TestCase):
|
|
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('node') 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
|
['node', 'node.child', 'node.child.grandchild']
|
@@ -132,6 +140,11 @@ class TimeItTest(unittest.TestCase):
|
|
132
140
|
[x.num_failed for x in summary.breakdown.values()],
|
133
141
|
[0, 0, 2]
|
134
142
|
)
|
143
|
+
# Test serialization.
|
144
|
+
json_dict = summary.to_json()
|
145
|
+
summary2 = timing.TimeIt.StatusSummary.from_json(json_dict)
|
146
|
+
self.assertIsNot(summary2, summary)
|
147
|
+
self.assertEqual(summary2.breakdown, summary.breakdown)
|
135
148
|
|
136
149
|
|
137
150
|
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
@@ -2310,6 +2310,42 @@ def save(value: Any, path: str, *args, **kwargs) -> Any:
|
|
2310
2310
|
return save_handler(value, path, *args, **kwargs)
|
2311
2311
|
|
2312
2312
|
|
2313
|
+
def open_jsonl(
|
2314
|
+
path: str,
|
2315
|
+
mode: str = 'r',
|
2316
|
+
**kwargs
|
2317
|
+
) -> pg_io.Sequence:
|
2318
|
+
"""Open a JSONL file for reading or writing.
|
2319
|
+
|
2320
|
+
Example::
|
2321
|
+
|
2322
|
+
with pg.open_jsonl('my_file.jsonl', 'w') as f:
|
2323
|
+
f.add(1)
|
2324
|
+
f.add('foo')
|
2325
|
+
f.add(dict(x=1))
|
2326
|
+
|
2327
|
+
with pg.open_jsonl('my_file.jsonl', 'r') as f:
|
2328
|
+
for value in f:
|
2329
|
+
print(value)
|
2330
|
+
|
2331
|
+
Args:
|
2332
|
+
path: The path to the file.
|
2333
|
+
mode: The mode of the file.
|
2334
|
+
**kwargs: Additional keyword arguments that will be passed to
|
2335
|
+
``pg_io.open_sequence``.
|
2336
|
+
|
2337
|
+
Returns:
|
2338
|
+
A sequence for PyGlove objects.
|
2339
|
+
"""
|
2340
|
+
return pg_io.open_sequence(
|
2341
|
+
path,
|
2342
|
+
mode,
|
2343
|
+
serializer=to_json_str,
|
2344
|
+
deserializer=from_json_str,
|
2345
|
+
**kwargs
|
2346
|
+
)
|
2347
|
+
|
2348
|
+
|
2313
2349
|
def default_load_handler(
|
2314
2350
|
path: str,
|
2315
2351
|
file_format: Literal['json', 'txt'] = 'json',
|
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;
|
@@ -1,5 +1,5 @@
|
|
1
1
|
pyglove/__init__.py,sha256=YPUWALRDu4QuI7N3TUmVaVr513hUt0qXrjY582I1q5c,1352
|
2
|
-
pyglove/core/__init__.py,sha256=
|
2
|
+
pyglove/core/__init__.py,sha256=omnsY79mvCd39G9HKtuYHltlgwB5kTdgqKQAoXWgT-I,8992
|
3
3
|
pyglove/core/logging.py,sha256=Sr5nLyUQmc4CAEYAq8qbP3ghpjmpz2sOuhq2A0tgQ6I,2456
|
4
4
|
pyglove/core/logging_test.py,sha256=QM59tCAFOUKm4gM6ef_9eG1AvSGEYRDLrrprUQyslcA,1956
|
5
5
|
pyglove/core/detouring/__init__.py,sha256=ck_n2VSuU31HNVYQkbG4Zvnx90mNYtSVc2StN3rXbU8,1891
|
@@ -42,18 +42,20 @@ pyglove/core/hyper/numerical.py,sha256=ifOdw4G9xukeELEiB8__zsR0X6O06_k9isq0i6YZw
|
|
42
42
|
pyglove/core/hyper/numerical_test.py,sha256=_xZe0qe3wJdrQL4F8pgt0zf04zpbc_9c_5va3mdd5_8,4259
|
43
43
|
pyglove/core/hyper/object_template.py,sha256=x6aBe-6ueo3CsFJ2yhbXbcdJKfId6wm_KsgkHHbpSoY,22325
|
44
44
|
pyglove/core/hyper/object_template_test.py,sha256=TEFX7LIqUvdCdJILnK_gP5xIgNJKzRnioUF0CGVBzcY,9105
|
45
|
-
pyglove/core/io/__init__.py,sha256=
|
46
|
-
pyglove/core/io/file_system.py,sha256=
|
47
|
-
pyglove/core/io/file_system_test.py,sha256=
|
48
|
-
pyglove/core/
|
45
|
+
pyglove/core/io/__init__.py,sha256=4ZT1a595DqQuLTNYc2JP_eCp_KesXvHmKRkr777bzpg,785
|
46
|
+
pyglove/core/io/file_system.py,sha256=KR1riWNBVZpkgIcqPUhR5xqjCN5Lhp937x7Ubjpv-jE,13283
|
47
|
+
pyglove/core/io/file_system_test.py,sha256=FX0ySuh_Xcg1RO68do7ikD4pvslKUzfSpwZ6P4wIP7c,8691
|
48
|
+
pyglove/core/io/sequence.py,sha256=Kir8JM8lxjrlKr1gs3hvhCB8F08PPz4K8-aDI_US9Vo,8042
|
49
|
+
pyglove/core/io/sequence_test.py,sha256=iqDR-MPm64VbUPKFYocVKH91Y6Z6tP1TZB0PzCn92tE,4265
|
50
|
+
pyglove/core/object_utils/__init__.py,sha256=bFyt-_uNzpOjJGDfYFTRP1kV6rL4du9xGx_OaFMcIxI,8758
|
49
51
|
pyglove/core/object_utils/codegen.py,sha256=HQlpjz42oh_2lrGSflwuAcEneE9Bv8pgPahQzvJm1KI,1568
|
50
52
|
pyglove/core/object_utils/codegen_test.py,sha256=ThPVyE805plNvM8vwlDxMrcjV-GGdDgbBLSqSOpCvJ0,2166
|
51
53
|
pyglove/core/object_utils/common_traits.py,sha256=9wfWvcq3uuVgL8mQexgJmWdF2aaQ-wWKzLXO9dqaqLk,3621
|
52
54
|
pyglove/core/object_utils/common_traits_test.py,sha256=wIeVsF3kon9K0Kbblcaib9hBJeZ76LyGZDD-5A1BD9w,1182
|
53
55
|
pyglove/core/object_utils/docstr_utils.py,sha256=5BY40kXozPKVGOB0eN8jy1P5_GHIzqFJ9FXAu_kzxaw,5119
|
54
56
|
pyglove/core/object_utils/docstr_utils_test.py,sha256=1NIsXXpp87HGC8LB7vx1KEMVVrmeGi733Xpuf6yTUq0,4287
|
55
|
-
pyglove/core/object_utils/error_utils.py,sha256=
|
56
|
-
pyglove/core/object_utils/error_utils_test.py,sha256=
|
57
|
+
pyglove/core/object_utils/error_utils.py,sha256=wnGX_aR4TdbCylvUOh-x6PPYzGSJBnfyX9MANpk03u8,5780
|
58
|
+
pyglove/core/object_utils/error_utils_test.py,sha256=vlFkR41jNLzUnwqF6IXy7HmXhpLlwOvUvlVMoMSOitk,4175
|
57
59
|
pyglove/core/object_utils/formatting.py,sha256=cXGWJd6zleS9QiEfY3w4fqWfFYgi_QF8S2IapTOzAj8,14994
|
58
60
|
pyglove/core/object_utils/formatting_test.py,sha256=QGbpUS8u70NoF8-3v5IYmPWEsML3ZbIwkQpUMwQdTkQ,13589
|
59
61
|
pyglove/core/object_utils/hierarchical.py,sha256=za9sA6XnlUOKUHMKNwQPNSOK9E3wDqXFQB0AND2Yghw,19754
|
@@ -62,10 +64,10 @@ pyglove/core/object_utils/json_conversion.py,sha256=1XEwz4LzEh8wHsTIC7jL7lD7L5tK
|
|
62
64
|
pyglove/core/object_utils/json_conversion_test.py,sha256=KGt0r628KHi4fTfeCgrhirfINpYXIR45l5ER4z09jjI,11907
|
63
65
|
pyglove/core/object_utils/missing.py,sha256=0liMs9iEyQxxu6UohdJ5hEM246e9Nu05qp0hqriHsl0,1459
|
64
66
|
pyglove/core/object_utils/missing_test.py,sha256=B36p-vqUvAnXWMszAj9GOPBN0_8cq7vVF61AkcsZ9qU,1396
|
65
|
-
pyglove/core/object_utils/profiling.py,sha256=hNMd4BY2jCOOV3o0VTlweHxZwd8yGylDzW6GgHU1-j8,5385
|
66
|
-
pyglove/core/object_utils/profiling_test.py,sha256=LvFy8qrAJUD-A4O7NBWZJ5YfjSGtc_zpmATCMs9hSp8,4322
|
67
67
|
pyglove/core/object_utils/thread_local.py,sha256=i-CnyY3VREtLfAj4_JndBnsKuQLIgwG29ma8dAyRxbI,4839
|
68
68
|
pyglove/core/object_utils/thread_local_test.py,sha256=EvU1-TF7KqpLQxxBvHd7dxtuY22YUQSIwQ0UcR-NORA,6816
|
69
|
+
pyglove/core/object_utils/timing.py,sha256=PeSN59g8vQDpenPgsWf4oYP45c0UDSpnFYDKhrtcSBc,6665
|
70
|
+
pyglove/core/object_utils/timing_test.py,sha256=Ye4qBrNl5ix2C_K-qT5TxKHui4Kvwmea3uUTx8t26qk,4828
|
69
71
|
pyglove/core/object_utils/value_location.py,sha256=lSFQNTazY2M6_nRLmMbouqZAcZSiOLZQnmQPMD2FDMs,26770
|
70
72
|
pyglove/core/object_utils/value_location_test.py,sha256=wCZYqWf_tq3l4ZM6dnkAs1obmq8LfYawkns2NKos9kk,21406
|
71
73
|
pyglove/core/patching/__init__.py,sha256=C1Q1cWPV74YL3eXbzGvc-8aPw1DR8EK6lRhQYDCwHek,2059
|
@@ -75,8 +77,8 @@ pyglove/core/patching/pattern_based.py,sha256=g56J19s6uTsaX5mHt--TnGifUQCjYjWpLX
|
|
75
77
|
pyglove/core/patching/pattern_based_test.py,sha256=PW1EcVfsFPB6wtgwg3s4dzvigWn3b5S8eMNGo0SJiZ0,2771
|
76
78
|
pyglove/core/patching/rule_based.py,sha256=QJsDsYVpyfUoaTZeXZtAdFuxpvB1eniY0qYdZvVqzk0,17056
|
77
79
|
pyglove/core/patching/rule_based_test.py,sha256=qfy0ILmczV_LMHWEnwo2y079OrJsGYO0nKxSZdmIUcI,18782
|
78
|
-
pyglove/core/symbolic/__init__.py,sha256=
|
79
|
-
pyglove/core/symbolic/base.py,sha256=
|
80
|
+
pyglove/core/symbolic/__init__.py,sha256=jYq-LwR1Ql3iMChjz9lN-0heDKiocmxR0ZJFEJ8RHHQ,5787
|
81
|
+
pyglove/core/symbolic/base.py,sha256=JNgh6eU_d5DRKQKlw4QP0FSJJg1RaVTrgGBjumg3ELs,78292
|
80
82
|
pyglove/core/symbolic/base_test.py,sha256=YLMlQGF4TbxAB2PcN1-ckI70gRIVDxabv-goh43CV7A,7333
|
81
83
|
pyglove/core/symbolic/boilerplate.py,sha256=YO8ZTZJ3VfAqeHqJpIY_ORVDxc1XIzxdplNlpexWEyc,6000
|
82
84
|
pyglove/core/symbolic/boilerplate_test.py,sha256=1CZ1W6kq3l-3tpaknhGFa04V18bO7vPzis5qzWnxHEs,5252
|
@@ -141,10 +143,10 @@ pyglove/core/views/__init__.py,sha256=hPqM3agvS3skQmLmZyV-5ai9vkhzJI65RLXmS9P-mO
|
|
141
143
|
pyglove/core/views/base.py,sha256=yuDHX5g1MgqyeVJLJPpWeh33opEafPygV1fjIOshWLE,26163
|
142
144
|
pyglove/core/views/base_test.py,sha256=F6nou7reS_Kmr2H57MUgcnc1bIp9Z3BWCvHOvMqjGkQ,16642
|
143
145
|
pyglove/core/views/html/__init__.py,sha256=FpwzhpomAkIjaOQh6mkIjaXC0KWHvGFIRCAQUAdxdC8,1029
|
144
|
-
pyglove/core/views/html/base.py,sha256=
|
145
|
-
pyglove/core/views/html/base_test.py,sha256=
|
146
|
-
pyglove/core/views/html/tree_view.py,sha256=
|
147
|
-
pyglove/core/views/html/tree_view_test.py,sha256=
|
146
|
+
pyglove/core/views/html/base.py,sha256=ob_wyUhlrZNQfrzeF5BWr8Bt8PH7zkYBwvR0xa40ABQ,15837
|
147
|
+
pyglove/core/views/html/base_test.py,sha256=Oko2B0vw3NYzJplEK4rhFoizOPeuBuebYZU-k39tMX0,22343
|
148
|
+
pyglove/core/views/html/tree_view.py,sha256=MM3Dct8kujaubdK_99KMwKJx7GFjLxsyTJV0sewH20M,51474
|
149
|
+
pyglove/core/views/html/tree_view_test.py,sha256=AzStpY_iuvnJUWViuFXFGyrGbNv3P2qVFQyw105O1Jg,74483
|
148
150
|
pyglove/ext/__init__.py,sha256=3jp8cJvKW6PENOZlmVAbT0w-GBRn_kjhc0wDX3XjpOE,755
|
149
151
|
pyglove/ext/early_stopping/__init__.py,sha256=_xkT3K_MycFuF3k2N74sfr9FPFcXMD3pfv5vuc4Tx5M,1116
|
150
152
|
pyglove/ext/early_stopping/base.py,sha256=KolexwKDAXhI9iY7hoaDO1mbc3TA4gDn0UifxURwbk0,2358
|
@@ -184,8 +186,8 @@ pyglove/ext/scalars/randoms.py,sha256=LkMIIx7lOq_lvJvVS3BrgWGuWl7Pi91-lA-O8x_gZs
|
|
184
186
|
pyglove/ext/scalars/randoms_test.py,sha256=nEhiqarg8l_5EOucp59CYrpO2uKxS1pe0hmBdZUzRNM,2000
|
185
187
|
pyglove/ext/scalars/step_wise.py,sha256=IDw3tuTpv0KVh7AN44W43zqm1-E0HWPUlytWOQC9w3Y,3789
|
186
188
|
pyglove/ext/scalars/step_wise_test.py,sha256=TL1vJ19xVx2t5HKuyIzGoogF7N3Rm8YhLE6JF7i0iy8,2540
|
187
|
-
pyglove-0.4.5.
|
188
|
-
pyglove-0.4.5.
|
189
|
-
pyglove-0.4.5.
|
190
|
-
pyglove-0.4.5.
|
191
|
-
pyglove-0.4.5.
|
189
|
+
pyglove-0.4.5.dev202410301815.dist-info/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
|
190
|
+
pyglove-0.4.5.dev202410301815.dist-info/METADATA,sha256=jIeDOiGyeIig84equ3ZG2EjO4s1his-CfD1PjMETKpU,6666
|
191
|
+
pyglove-0.4.5.dev202410301815.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
192
|
+
pyglove-0.4.5.dev202410301815.dist-info/top_level.txt,sha256=wITzJSKcj8GZUkbq-MvUQnFadkiuAv_qv5qQMw0fIow,8
|
193
|
+
pyglove-0.4.5.dev202410301815.dist-info/RECORD,,
|
File without changes
|
{pyglove-0.4.5.dev202410290809.dist-info → pyglove-0.4.5.dev202410301815.dist-info}/top_level.txt
RENAMED
File without changes
|