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 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
@@ -16,5 +16,6 @@
16
16
  # pylint: disable=g-bad-import-order
17
17
 
18
18
  from pyglove.core.io.file_system import *
19
+ from pyglove.core.io.sequence import *
19
20
 
20
21
  # pylint: enable=g-bad-import-order
@@ -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
- # Profiling
159
- from pyglove.core.object_utils.profiling import timeit
160
- from pyglove.core.object_utils.profiling import TimeIt
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
- from typing import Callable, Dict, List, Optional, Sequence, Tuple, Type, Union
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
- """Tests for pyglove.object_utils.error_utils."""
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 profiling."""
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 aggregate(self, timeit_obj: 'TimeIt'):
73
- for k, v in timeit_obj.status().items():
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 profiling
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 = profiling.TimeIt('node')
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 profiling.timeit('node') as t:
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 profiling.timeit('child') as t1:
50
+ with timing.timeit('child') as t1:
50
51
  time.sleep(0.5)
51
- with profiling.timeit('grandchild') as t2:
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 profiling.timeit('grandchild'):
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 profiling.timeit('node') as t:
90
- with profiling.timeit('child') as t1:
91
- with profiling.timeit('grandchild') as t2:
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 = profiling.TimeIt.StatusSummary()
111
+ summary = timing.TimeIt.StatusSummary()
112
+ self.assertFalse(summary)
106
113
  for i in range(10):
107
- with profiling.timeit('node') as t:
114
+ with timing.timeit('node') as t:
108
115
  time.sleep(0.1)
109
- with profiling.timeit('child'):
116
+ with timing.timeit('child'):
110
117
  time.sleep(0.1)
111
118
  try:
112
- with profiling.timeit('grandchild'):
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
@@ -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',
@@ -333,7 +333,11 @@ class Html(base.Content):
333
333
  return s
334
334
 
335
335
  @classmethod
336
- def escape(cls, s: WritableTypes) -> WritableTypes:
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
- if isinstance(s, str):
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(html_lib.escape(s.content)).write(
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&quot;bar')
696
696
  self.assertEqual(Html.escape(Html('foo"bar')), Html('foo&quot;bar'))
697
697
  self.assertEqual(Html.escape(lambda: 'foo"bar'), 'foo&quot;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,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyglove
3
- Version: 0.4.5.dev202410290809
3
+ Version: 0.4.5.dev202410301815
4
4
  Summary: PyGlove: A library for manipulating Python objects.
5
5
  Home-page: https://github.com/google/pyglove
6
6
  Author: PyGlove Authors
@@ -1,5 +1,5 @@
1
1
  pyglove/__init__.py,sha256=YPUWALRDu4QuI7N3TUmVaVr513hUt0qXrjY582I1q5c,1352
2
- pyglove/core/__init__.py,sha256=tHz9DcjN_vGIaYPt4TDWiJ9XXfQmKXsMR7tYwQaoUX4,8959
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=_wqJqPGny8LcVOze2ym_5q8S5OGTZRVpTsQ0_DAmJvY,746
46
- pyglove/core/io/file_system.py,sha256=q-XDwk9A2oWX90OaZf7CedpNwu_aPPKbHQC2mCwNo4A,13084
47
- pyglove/core/io/file_system_test.py,sha256=I4tlj7oi8jWXEYVfPGimhHYL9s_nHVrxZsSbwOyeFSc,8659
48
- pyglove/core/object_utils/__init__.py,sha256=AghdwjgQQIB9lu90aWeBUYHP3CrrolX1YiPgXNMeVfg,8706
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=4uZxi9dGYVsJgvctt8dFnbpbfT8Kqma6KwqjHJ5okn8,3949
56
- pyglove/core/object_utils/error_utils_test.py,sha256=YOL2gL5_v_KspA49ulyYV3Vse5jIFfYHYxoq09GGKdg,2605
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=p9NvBRakrFQl_jGQFglFwOblxu9B3OOPs34xTLCscMw,5737
79
- pyglove/core/symbolic/base.py,sha256=WMdwqVriVOwuPBXp8qkXh69nenvNPs__0FWRIT5cyEo,77579
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=i87aldEsw8V6vW2RkmVyCPX70R_yh0-gwD45gbW1e-4,15513
145
- pyglove/core/views/html/base_test.py,sha256=JtzM22QPLINW9YvOuSCtj_eXsDWD9zG8VVf_LJHgfxw,22192
146
- pyglove/core/views/html/tree_view.py,sha256=orhMI7zOFKBY5v0Bhca700ulKsVKXI_Rj90kzpiz56U,51245
147
- pyglove/core/views/html/tree_view_test.py,sha256=7T0kjlEW2DxUKsfOnFU_9xmgP6DM4Rq2Mp32teHNYZs,74273
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.dev202410290809.dist-info/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
188
- pyglove-0.4.5.dev202410290809.dist-info/METADATA,sha256=IioX481iPq1LaxeDpORWZVVgaV3mxy3WKlpGM8ibHb0,6666
189
- pyglove-0.4.5.dev202410290809.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
190
- pyglove-0.4.5.dev202410290809.dist-info/top_level.txt,sha256=wITzJSKcj8GZUkbq-MvUQnFadkiuAv_qv5qQMw0fIow,8
191
- pyglove-0.4.5.dev202410290809.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.2.0)
2
+ Generator: setuptools (75.3.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5