pyglove 0.5.0.dev202510200810__py3-none-any.whl → 0.5.0.dev202511240812__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.

Files changed (34) hide show
  1. pyglove/core/geno/base.py +7 -3
  2. pyglove/core/io/file_system.py +295 -2
  3. pyglove/core/io/file_system_test.py +291 -0
  4. pyglove/core/symbolic/__init__.py +7 -0
  5. pyglove/core/symbolic/base.py +89 -35
  6. pyglove/core/symbolic/base_test.py +3 -3
  7. pyglove/core/symbolic/dict.py +31 -12
  8. pyglove/core/symbolic/dict_test.py +49 -0
  9. pyglove/core/symbolic/list.py +17 -3
  10. pyglove/core/symbolic/list_test.py +24 -2
  11. pyglove/core/symbolic/object.py +3 -1
  12. pyglove/core/symbolic/object_test.py +13 -10
  13. pyglove/core/symbolic/ref.py +19 -7
  14. pyglove/core/symbolic/ref_test.py +94 -7
  15. pyglove/core/symbolic/unknown_symbols.py +147 -0
  16. pyglove/core/symbolic/unknown_symbols_test.py +100 -0
  17. pyglove/core/typing/annotation_conversion.py +8 -1
  18. pyglove/core/typing/annotation_conversion_test.py +14 -19
  19. pyglove/core/typing/class_schema.py +4 -1
  20. pyglove/core/typing/type_conversion.py +17 -3
  21. pyglove/core/typing/type_conversion_test.py +7 -2
  22. pyglove/core/typing/value_specs.py +5 -1
  23. pyglove/core/typing/value_specs_test.py +5 -0
  24. pyglove/core/utils/__init__.py +1 -0
  25. pyglove/core/utils/json_conversion.py +360 -63
  26. pyglove/core/utils/json_conversion_test.py +146 -13
  27. pyglove/core/views/html/controls/tab.py +33 -0
  28. pyglove/core/views/html/controls/tab_test.py +37 -0
  29. pyglove/ext/evolution/base_test.py +1 -1
  30. {pyglove-0.5.0.dev202510200810.dist-info → pyglove-0.5.0.dev202511240812.dist-info}/METADATA +8 -1
  31. {pyglove-0.5.0.dev202510200810.dist-info → pyglove-0.5.0.dev202511240812.dist-info}/RECORD +34 -32
  32. {pyglove-0.5.0.dev202510200810.dist-info → pyglove-0.5.0.dev202511240812.dist-info}/WHEEL +0 -0
  33. {pyglove-0.5.0.dev202510200810.dist-info → pyglove-0.5.0.dev202511240812.dist-info}/licenses/LICENSE +0 -0
  34. {pyglove-0.5.0.dev202510200810.dist-info → pyglove-0.5.0.dev202511240812.dist-info}/top_level.txt +0 -0
@@ -16,6 +16,8 @@ import os
16
16
  import pathlib
17
17
  import tempfile
18
18
  import unittest
19
+ from unittest import mock
20
+ import fsspec
19
21
  from pyglove.core.io import file_system
20
22
 
21
23
 
@@ -82,6 +84,75 @@ class StdFileSystemTest(unittest.TestCase):
82
84
  fs.rmdirs(os.path.join(dir_a, 'b/c'))
83
85
  self.assertEqual(sorted(fs.listdir(dir_a)), ['file1']) # pylint: disable=g-generic-assert
84
86
 
87
+ def test_rename(self):
88
+ tmp_dir = tempfile.mkdtemp()
89
+ fs = file_system.StdFileSystem()
90
+
91
+ _ = fs.mkdirs(os.path.join(tmp_dir, 'a/b'))
92
+ file_foo = os.path.join(tmp_dir, 'a/foo.txt')
93
+ file_bar = os.path.join(tmp_dir, 'a/bar.txt')
94
+
95
+ with fs.open(file_foo, 'w') as f:
96
+ f.write('foo')
97
+ with fs.open(file_bar, 'w') as f:
98
+ f.write('bar')
99
+
100
+ # Rename file to a new name.
101
+ file_foo_new = os.path.join(tmp_dir, 'a/foo-new.txt')
102
+ fs.rename(file_foo, file_foo_new)
103
+ self.assertFalse(fs.exists(file_foo))
104
+ self.assertTrue(fs.exists(file_foo_new))
105
+
106
+ # Rename file to an existing file name.
107
+ fs.rename(file_foo_new, file_bar)
108
+ self.assertFalse(fs.exists(file_foo_new))
109
+ with fs.open(file_bar, 'r') as f:
110
+ self.assertEqual(f.read(), 'foo')
111
+
112
+ # Rename directory to a new name.
113
+ dir_b = os.path.join(tmp_dir, 'a/b')
114
+ dir_c = os.path.join(tmp_dir, 'a/c')
115
+ fs.rename(dir_b, dir_c)
116
+ self.assertFalse(fs.exists(dir_b))
117
+ self.assertTrue(fs.exists(dir_c))
118
+ self.assertTrue(fs.isdir(dir_c))
119
+
120
+ # Rename directory to an existing empty directory.
121
+ dir_d = os.path.join(tmp_dir, 'a/d')
122
+ fs.mkdirs(dir_d)
123
+ fs.rename(dir_c, dir_d)
124
+ self.assertFalse(fs.exists(dir_c))
125
+ self.assertTrue(fs.exists(dir_d))
126
+
127
+ # Rename directory to a non-empty directory.
128
+ dir_x = os.path.join(tmp_dir, 'x')
129
+ dir_a = os.path.join(tmp_dir, 'a')
130
+ fs.mkdirs(os.path.join(dir_x, 'y'))
131
+ with self.assertRaises(OSError):
132
+ fs.rename(dir_a, dir_x)
133
+ self.assertTrue(fs.exists(dir_a))
134
+ self.assertTrue(fs.exists(os.path.join(dir_x, 'y')))
135
+
136
+ # Errors
137
+ dir_u = os.path.join(tmp_dir, 'u')
138
+ dir_u_v = os.path.join(dir_u, 'v')
139
+ file_u_a = os.path.join(dir_u, 'a.txt')
140
+ fs.mkdirs(dir_u_v)
141
+ with fs.open(file_u_a, 'w') as f:
142
+ f.write('a')
143
+
144
+ with self.assertRaises((OSError, NotADirectoryError)):
145
+ fs.rename(dir_u, file_u_a)
146
+
147
+ with self.assertRaises(IsADirectoryError):
148
+ fs.rename(file_u_a, dir_u_v)
149
+
150
+ with self.assertRaises(FileNotFoundError):
151
+ fs.rename(
152
+ os.path.join(tmp_dir, 'non-existent'),
153
+ os.path.join(tmp_dir, 'y')
154
+ )
155
+
85
156
 
86
157
  class MemoryFileSystemTest(unittest.TestCase):
87
158
 
@@ -180,6 +251,94 @@ class MemoryFileSystemTest(unittest.TestCase):
180
251
  fs.rmdirs(os.path.join(dir_a, 'b/c'))
181
252
  self.assertEqual(fs.listdir(dir_a), ['file1']) # pylint: disable=g-generic-assert
182
253
 
254
+ def test_glob(self):
255
+ fs = file_system.MemoryFileSystem()
256
+ fs.mkdirs('/mem/a/b/c')
257
+ with fs.open('/mem/a/foo.txt', 'w') as f:
258
+ f.write('foo')
259
+ with fs.open('/mem/a/bar.json', 'w') as f:
260
+ f.write('bar')
261
+ with fs.open('/mem/a/b/baz.txt', 'w') as f:
262
+ f.write('baz')
263
+
264
+ self.assertEqual(
265
+ sorted(fs.glob('/mem/a/*')),
266
+ ['/mem/a/b', '/mem/a/b/baz.txt', '/mem/a/b/c',
267
+ '/mem/a/bar.json', '/mem/a/foo.txt'])
268
+ self.assertEqual(
269
+ sorted(fs.glob('/mem/a/*.txt')),
270
+ ['/mem/a/b/baz.txt', '/mem/a/foo.txt'])
271
+ self.assertEqual(
272
+ sorted(fs.glob('/mem/a/b/*')),
273
+ ['/mem/a/b/baz.txt', '/mem/a/b/c'])
274
+ self.assertEqual(fs.glob('/mem/a/b/*.txt'), ['/mem/a/b/baz.txt'])
275
+ self.assertEqual(fs.glob('/mem/a/b/c/*'), [])
276
+ self.assertEqual(fs.glob('/mem/a/???.txt'), ['/mem/a/foo.txt'])
277
+ self.assertEqual(fs.glob('/mem/a/bar.*'), ['/mem/a/bar.json'])
278
+ self.assertEqual(
279
+ sorted(fs.glob('/mem/a/*.*')),
280
+ ['/mem/a/b/baz.txt', '/mem/a/bar.json', '/mem/a/foo.txt'])
281
+
282
+ def test_rename(self):
283
+ fs = file_system.MemoryFileSystem()
284
+ fs.mkdirs('/mem/a/b')
285
+ with fs.open('/mem/a/foo.txt', 'w') as f:
286
+ f.write('foo')
287
+ with fs.open('/mem/a/bar.txt', 'w') as f:
288
+ f.write('bar')
289
+
290
+ # Rename file to a new name.
291
+ fs.rename('/mem/a/foo.txt', '/mem/a/foo-new.txt')
292
+ self.assertFalse(fs.exists('/mem/a/foo.txt'))
293
+ self.assertTrue(fs.exists('/mem/a/foo-new.txt'))
294
+
295
+ # Rename file to an existing file name.
296
+ fs.rename('/mem/a/foo-new.txt', '/mem/a/bar.txt')
297
+ self.assertFalse(fs.exists('/mem/a/foo-new.txt'))
298
+ with fs.open('/mem/a/bar.txt', 'r') as f:
299
+ self.assertEqual(f.read(), 'foo')
300
+
301
+ # Rename directory to a new name.
302
+ fs.rename('/mem/a/b', '/mem/a/c')
303
+ self.assertFalse(fs.exists('/mem/a/b'))
304
+ self.assertTrue(fs.exists('/mem/a/c'))
305
+ self.assertTrue(fs.isdir('/mem/a/c'))
306
+
307
+ # Rename directory to an existing empty directory.
308
+ fs.mkdirs('/mem/a/d')
309
+ fs.rename('/mem/a/c', '/mem/a/d')
310
+ self.assertFalse(fs.exists('/mem/a/c'))
311
+ self.assertTrue(fs.exists('/mem/a/d'))
312
+
313
+ # Rename directory to a non-empty directory.
314
+ fs.mkdirs('/mem/x/y')
315
+ with self.assertRaisesRegex(OSError, "Directory not empty: '/mem/x'"):
316
+ fs.rename('/mem/a', '/mem/x')
317
+ self.assertTrue(fs.exists('/mem/a'))
318
+ self.assertTrue(fs.exists('/mem/x/y'))
319
+
320
+ # Errors
321
+ fs.mkdirs('/mem/u/v')
322
+ with fs.open('/mem/u/a.txt', 'w') as f:
323
+ f.write('a')
324
+
325
+ with self.assertRaisesRegex(
326
+ OSError, "Cannot move directory '/mem/u' to a subdirectory of itself"):
327
+ fs.rename('/mem/u', '/mem/u/v/w')
328
+
329
+ with self.assertRaisesRegex(
330
+ NotADirectoryError,
331
+ "Cannot rename directory '/mem/u' to non-directory '/mem/u/a.txt'"):
332
+ fs.rename('/mem/u', '/mem/u/a.txt')
333
+
334
+ with self.assertRaisesRegex(
335
+ IsADirectoryError,
336
+ "Cannot rename non-directory '/mem/u/a.txt' to directory '/mem/u/v'"):
337
+ fs.rename('/mem/u/a.txt', '/mem/u/v')
338
+
339
+ with self.assertRaises(FileNotFoundError):
340
+ fs.rename('/mem/non-existent', '/mem/y')
341
+
183
342
 
184
343
  class FileIoApiTest(unittest.TestCase):
185
344
 
@@ -217,6 +376,14 @@ class FileIoApiTest(unittest.TestCase):
217
376
  file_system.rm(file2)
218
377
  self.assertFalse(file_system.path_exists(file2))
219
378
 
379
+ # Test glob with standard file system.
380
+ glob_dir = os.path.join(tempfile.mkdtemp(), 'glob')
381
+ file_system.mkdirs(os.path.join(glob_dir, 'a/b'))
382
+ file_system.writefile(os.path.join(glob_dir, 'a/foo.txt'), 'foo')
383
+ self.assertEqual(
384
+ sorted(file_system.glob(os.path.join(glob_dir, 'a/*'))),
385
+ [os.path.join(glob_dir, 'a/b'), os.path.join(glob_dir, 'a/foo.txt')])
386
+
220
387
  def test_memory_filesystem(self):
221
388
  file1 = pathlib.Path('/mem/file1')
222
389
  with self.assertRaises(FileNotFoundError):
@@ -248,6 +415,130 @@ class FileIoApiTest(unittest.TestCase):
248
415
  file_system.rm(file2)
249
416
  self.assertFalse(file_system.path_exists(file2))
250
417
 
418
+ # Test glob with memory file system.
419
+ file_system.mkdirs('/mem/g/a/b')
420
+ file_system.writefile('/mem/g/a/foo.txt', 'foo')
421
+ file_system.rename('/mem/g/a/foo.txt', '/mem/g/a/foo2.txt')
422
+ file_system.writefile('/mem/g/a/b/bar.txt', 'bar')
423
+ self.assertEqual(
424
+ sorted(file_system.glob('/mem/g/a/*')),
425
+ ['/mem/g/a/b', '/mem/g/a/b/bar.txt', '/mem/g/a/foo2.txt'])
426
+
427
+
428
+ class FsspecFileSystemTest(unittest.TestCase):
429
+
430
+ def setUp(self):
431
+ super().setUp()
432
+ self.fs = fsspec.filesystem('memory')
433
+ self.fs.pipe('memory:///a/b/c', b'abc')
434
+ self.fs.pipe('memory:///a/b/d', b'abd')
435
+ self.fs.mkdir('memory:///a/e')
436
+
437
+ def tearDown(self):
438
+ super().tearDown()
439
+ fsspec.filesystem('memory').rm('/', recursive=True)
440
+
441
+ def test_read_file(self):
442
+ self.assertEqual(file_system.readfile('memory:///a/b/c', mode='rb'), b'abc')
443
+ with file_system.open('memory:///a/b/d', 'rb') as f:
444
+ self.assertEqual(f.read(), b'abd')
445
+
446
+ def test_fsspec_file_ops(self):
447
+ file_system.writefile('memory:///f', b'hello\nworld\n', mode='wb')
448
+ with file_system.open('memory:///f', 'rb') as f:
449
+ self.assertIsInstance(f, file_system.FsspecFile)
450
+ self.assertEqual(f.readline(), b'hello\n')
451
+ self.assertEqual(f.tell(), 6)
452
+ self.assertEqual(f.seek(8), 8)
453
+ self.assertEqual(f.read(), b'rld\n')
454
+ f.flush()
455
+
456
+ def test_write_file(self):
457
+ file_system.writefile('memory:///a/b/e', b'abe', mode='wb')
458
+ self.assertTrue(self.fs.exists('memory:///a/b/e'))
459
+ self.assertEqual(self.fs.cat('memory:///a/b/e'), b'abe')
460
+
461
+ def test_exists(self):
462
+ self.assertTrue(file_system.path_exists('memory:///a/b/c'))
463
+ self.assertFalse(file_system.path_exists('memory:///a/b/nonexist'))
464
+
465
+ def test_isdir(self):
466
+ self.assertTrue(file_system.isdir('memory:///a/b'))
467
+ self.assertTrue(file_system.isdir('memory:///a/e'))
468
+ self.assertFalse(file_system.isdir('memory:///a/b/c'))
469
+
470
+ def test_listdir(self):
471
+ self.assertCountEqual(file_system.listdir('memory:///a'), ['b', 'e'])
472
+ self.assertCountEqual(file_system.listdir('memory:///a/b'), ['c', 'd'])
473
+
474
+ def test_glob(self):
475
+ self.assertCountEqual(
476
+ file_system.glob('memory:///a/b/*'),
477
+ ['memory:///a/b/c', 'memory:///a/b/d']
478
+ )
479
+
480
+ def test_mkdir(self):
481
+ file_system.mkdir('memory:///a/f')
482
+ self.assertTrue(self.fs.isdir('memory:///a/f'))
483
+
484
+ def test_mkdirs(self):
485
+ file_system.mkdirs('memory:///g/h/i')
486
+ self.assertTrue(self.fs.isdir('memory:///g/h/i'))
487
+
488
+ def test_rm(self):
489
+ file_system.rm('memory:///a/b/c')
490
+ self.assertFalse(self.fs.exists('memory:///a/b/c'))
491
+
492
+ def test_rename(self):
493
+ file_system.rename('memory:///a/b/c', 'memory:///a/b/c_new')
494
+ self.assertFalse(self.fs.exists('memory:///a/b/c'))
495
+ self.assertTrue(self.fs.exists('memory:///a/b/c_new'))
496
+ with self.assertRaisesRegex(ValueError, 'Rename across different'):
497
+ file_system.rename('memory:///a/b/c_new', 'file:///a/b/c_d')
498
+
499
+ def test_chmod(self):
500
+ mock_fs = mock.Mock()
501
+ mock_fs.chmod = mock.Mock()
502
+ with mock.patch('fsspec.core.url_to_fs', return_value=(mock_fs, 'path')):
503
+ file_system.chmod('protocol:///path', 0o777)
504
+ mock_fs.chmod.assert_called_once_with('path', 0o777)
505
+
506
+ def test_rmdir(self):
507
+ file_system.rmdir('memory:///a/e')
508
+ self.assertFalse(self.fs.exists('memory:///a/e'))
509
+
510
+ def test_rmdirs(self):
511
+ file_system.mkdirs('memory:///x/y/z')
512
+ self.assertTrue(file_system.isdir('memory:///x/y/z'))
513
+ file_system.rmdirs('memory:///x')
514
+ self.assertFalse(file_system.path_exists('memory:///x'))
515
+
516
+ def test_fsspec_uri_catcher(self):
517
+ with mock.patch.object(
518
+ file_system.FsspecFileSystem, 'exists', return_value=True
519
+ ) as mock_fsspec_exists:
520
+ # We use a protocol that is not registered in
521
+ # fsspec.available_protocols() to make sure _FsspecUriCatcher is used.
522
+ self.assertTrue(file_system.path_exists('some-proto://foo'))
523
+ mock_fsspec_exists.assert_called_once_with('some-proto://foo')
524
+
525
+ # For full coverage of _FsspecUriCatcher.get_fs returning StdFileSystem,
526
+ # we need to test a non-URI path that doesn't match other prefixes.
527
+ # We mock StdFileSystem.exists to check if it's called.
528
+ with mock.patch.object(
529
+ file_system.StdFileSystem, 'exists', return_value=True
530
+ ) as mock_std_exists:
531
+ self.assertTrue(file_system.path_exists('/foo/bar/baz'))
532
+ mock_std_exists.assert_called_once_with('/foo/bar/baz')
533
+
534
+ with mock.patch.object(
535
+ file_system.FsspecFileSystem, 'rename', return_value=None
536
+ ) as mock_fsspec_rename:
537
+ file_system.rename('some-proto://foo', 'some-proto://bar')
538
+ mock_fsspec_rename.assert_called_once_with(
539
+ 'some-proto://foo', 'some-proto://bar'
540
+ )
541
+
251
542
 
252
543
  if __name__ == '__main__':
253
544
  unittest.main()
@@ -147,4 +147,11 @@ from pyglove.core.symbolic.list import mark_as_insertion
147
147
  from pyglove.core.symbolic.base import WritePermissionError
148
148
  from pyglove.core.symbolic.error_info import ErrorInfo
149
149
 
150
+ # Unknown symbols.
151
+ from pyglove.core.symbolic.unknown_symbols import UnknownSymbol
152
+ from pyglove.core.symbolic.unknown_symbols import UnknownType
153
+ from pyglove.core.symbolic.unknown_symbols import UnknownFunction
154
+ from pyglove.core.symbolic.unknown_symbols import UnknownMethod
155
+ from pyglove.core.symbolic.unknown_symbols import UnknownTypedObject
156
+
150
157
  # pylint: enable=g-bad-import-order
@@ -503,10 +503,12 @@ class Symbolic(
503
503
  return default
504
504
 
505
505
  def _sym_inferred(self, key: Union[str, int], **kwargs) -> Any:
506
- v = self.sym_getattr(key)
507
- if isinstance(v, Inferential):
508
- v = v.infer(**kwargs)
509
- return v
506
+ return self._infer_if_applicable(self.sym_getattr(key), **kwargs)
507
+
508
+ def _infer_if_applicable(self, value: Any, **kwargs) -> Any:
509
+ if isinstance(value, Inferential):
510
+ return value.infer(**kwargs)
511
+ return value
510
512
 
511
513
  @abc.abstractmethod
512
514
  def sym_keys(self) -> Iterator[Union[str, int]]:
@@ -944,7 +946,7 @@ class Symbolic(
944
946
 
945
947
  def to_json(self, **kwargs) -> utils.JSONValueType:
946
948
  """Alias for `sym_jsonify`."""
947
- return to_json(self, **kwargs)
949
+ return utils.to_json(self, **kwargs)
948
950
 
949
951
  def to_json_str(self, json_indent: Optional[int] = None, **kwargs) -> str:
950
952
  """Serializes current object into a JSON string."""
@@ -1983,10 +1985,12 @@ def is_abstract(x: Any) -> bool:
1983
1985
  def contains(
1984
1986
  x: Any,
1985
1987
  value: Any = None,
1986
- type: Optional[Union[ # pylint: disable=redefined-builtin
1988
+ type: Union[ # pylint: disable=redefined-builtin
1987
1989
  Type[Any],
1988
- Tuple[Type[Any]]]]=None
1989
- ) -> bool:
1990
+ Tuple[Type[Any], ...],
1991
+ None,
1992
+ ]=None,
1993
+ ) -> bool:
1990
1994
  """Returns if a value contains values of specific type.
1991
1995
 
1992
1996
  Example::
@@ -2035,10 +2039,12 @@ def contains(
2035
2039
  def from_json(
2036
2040
  json_value: Any,
2037
2041
  *,
2042
+ context: Optional[utils.JSONConversionContext] = None,
2043
+ auto_symbolic: bool = True,
2044
+ auto_import: bool = True,
2045
+ convert_unknown: bool = False,
2038
2046
  allow_partial: bool = False,
2039
2047
  root_path: Optional[utils.KeyPath] = None,
2040
- auto_import: bool = True,
2041
- auto_dict: bool = False,
2042
2048
  value_spec: Optional[pg_typing.ValueSpec] = None,
2043
2049
  **kwargs,
2044
2050
  ) -> Any:
@@ -2059,14 +2065,23 @@ def from_json(
2059
2065
 
2060
2066
  Args:
2061
2067
  json_value: Input JSON value.
2062
- allow_partial: Whether to allow elements of the list to be partial.
2063
- root_path: KeyPath of loaded object in its object tree.
2068
+ context: JSON conversion context.
2069
+ auto_symbolic: If True, list and dict will be automatically converted to
2070
+ `pg.List` and `pg.Dict`. Otherwise, they will be plain lists
2071
+ and dicts.
2064
2072
  auto_import: If True, when a '_type' is not registered, PyGlove will
2065
2073
  identify its parent module and automatically import it. For example,
2066
2074
  if the type is 'foo.bar.A', PyGlove will try to import 'foo.bar' and
2067
2075
  find the class 'A' within the imported module.
2068
- auto_dict: If True, dict with '_type' that cannot be loaded will remain
2069
- as dict, with '_type' renamed to 'type_name'.
2076
+ convert_unknown: If True, when a '_type' is not registered and cannot
2077
+ be imported, PyGlove will create objects of:
2078
+ - `pg.symbolic.UnknownType` for unknown types;
2079
+ - `pg.symbolic.UnknownTypedObject` for objects of unknown types;
2080
+ - `pg.symbolic.UnknownFunction` for unknown functions;
2081
+ - `pg.symbolic.UnknownMethod` for unknown methods.
2082
+ If False, TypeError will be raised.
2083
+ allow_partial: Whether to allow elements of the list to be partial.
2084
+ root_path: KeyPath of loaded object in its object tree.
2070
2085
  value_spec: The value spec for the symbolic list or dict.
2071
2086
  **kwargs: Allow passing through keyword arguments to from_json of specific
2072
2087
  types.
@@ -2082,10 +2097,23 @@ def from_json(
2082
2097
  if isinstance(json_value, Symbolic):
2083
2098
  return json_value
2084
2099
 
2100
+ if context is None:
2101
+ if (isinstance(json_value, dict) and (
2102
+ context_node := json_value.get(utils.JSONConvertible.CONTEXT_KEY))):
2103
+ context = utils.JSONConversionContext.from_json(
2104
+ context_node,
2105
+ auto_import=auto_import,
2106
+ convert_unknown=convert_unknown,
2107
+ **kwargs
2108
+ )
2109
+ json_value = json_value[utils.JSONConvertible.ROOT_VALUE_KEY]
2110
+ else:
2111
+ context = utils.JSONConversionContext()
2112
+
2085
2113
  typename_resolved = kwargs.pop('_typename_resolved', False)
2086
2114
  if not typename_resolved:
2087
2115
  json_value = utils.json_conversion.resolve_typenames(
2088
- json_value, auto_import=auto_import, auto_dict=auto_dict
2116
+ json_value, auto_import, convert_unknown
2089
2117
  )
2090
2118
 
2091
2119
  def _load_child(k, v):
@@ -2094,6 +2122,7 @@ def from_json(
2094
2122
  root_path=utils.KeyPath(k, root_path),
2095
2123
  _typename_resolved=True,
2096
2124
  allow_partial=allow_partial,
2125
+ context=context,
2097
2126
  **kwargs,
2098
2127
  )
2099
2128
 
@@ -2109,24 +2138,42 @@ def from_json(
2109
2138
  )
2110
2139
  )
2111
2140
  return tuple(_load_child(i, v) for i, v in enumerate(json_value[1:]))
2112
- return Symbolic.ListType.from_json( # pytype: disable=attribute-error
2141
+ if json_value and json_value[0] == utils.JSONConvertible.SYMBOLIC_MARKER:
2142
+ auto_symbolic = True
2143
+ if auto_symbolic:
2144
+ from_json_fn = Symbolic.ListType.from_json # pytype: disable=attribute-error
2145
+ else:
2146
+ from_json_fn = utils.from_json
2147
+ return from_json_fn(
2113
2148
  json_value,
2149
+ context=context,
2114
2150
  value_spec=value_spec,
2115
2151
  root_path=root_path,
2116
2152
  allow_partial=allow_partial,
2117
2153
  **kwargs,
2118
2154
  )
2119
2155
  elif isinstance(json_value, dict):
2156
+ if utils.JSONConvertible.REF_KEY in json_value:
2157
+ x = context.get_shared(
2158
+ json_value[utils.JSONConvertible.REF_KEY]
2159
+ ).value
2160
+ return x
2120
2161
  if utils.JSONConvertible.TYPE_NAME_KEY not in json_value:
2121
- return Symbolic.DictType.from_json( # pytype: disable=attribute-error
2122
- json_value,
2123
- value_spec=value_spec,
2124
- root_path=root_path,
2125
- allow_partial=allow_partial,
2126
- **kwargs,
2162
+ auto_symbolic = json_value.get(
2163
+ utils.JSONConvertible.SYMBOLIC_MARKER, auto_symbolic
2127
2164
  )
2165
+ if auto_symbolic:
2166
+ return Symbolic.DictType.from_json( # pytype: disable=attribute-error
2167
+ json_value,
2168
+ context=context,
2169
+ value_spec=value_spec,
2170
+ root_path=root_path,
2171
+ allow_partial=allow_partial,
2172
+ **kwargs,
2173
+ )
2128
2174
  return utils.from_json(
2129
2175
  json_value,
2176
+ context=context,
2130
2177
  _typename_resolved=True,
2131
2178
  root_path=root_path,
2132
2179
  allow_partial=allow_partial,
@@ -2138,10 +2185,12 @@ def from_json(
2138
2185
  def from_json_str(
2139
2186
  json_str: str,
2140
2187
  *,
2188
+ context: Optional[utils.JSONConversionContext] = None,
2189
+ auto_import: bool = True,
2190
+ convert_unknown: bool = False,
2141
2191
  allow_partial: bool = False,
2142
2192
  root_path: Optional[utils.KeyPath] = None,
2143
- auto_import: bool = True,
2144
- auto_dict: bool = False,
2193
+ value_spec: Optional[pg_typing.ValueSpec] = None,
2145
2194
  **kwargs,
2146
2195
  ) -> Any:
2147
2196
  """Deserialize (maybe) symbolic object from JSON string.
@@ -2161,15 +2210,22 @@ def from_json_str(
2161
2210
 
2162
2211
  Args:
2163
2212
  json_str: JSON string.
2164
- allow_partial: If True, allow a partial symbolic object to be created.
2165
- Otherwise error will be raised on partial value.
2166
- root_path: The symbolic path used for the deserialized root object.
2213
+ context: JSON conversion context.
2167
2214
  auto_import: If True, when a '_type' is not registered, PyGlove will
2168
2215
  identify its parent module and automatically import it. For example,
2169
2216
  if the type is 'foo.bar.A', PyGlove will try to import 'foo.bar' and
2170
2217
  find the class 'A' within the imported module.
2171
- auto_dict: If True, dict with '_type' that cannot be loaded will remain
2172
- as dict, with '_type' renamed to 'type_name'.
2218
+ convert_unknown: If True, when a '_type' is not registered and cannot
2219
+ be imported, PyGlove will create objects of:
2220
+ - `pg.symbolic.UnknownType` for unknown types;
2221
+ - `pg.symbolic.UnknownTypedObject` for objects of unknown types;
2222
+ - `pg.symbolic.UnknownFunction` for unknown functions;
2223
+ - `pg.symbolic.UnknownMethod` for unknown methods.
2224
+ If False, TypeError will be raised.
2225
+ allow_partial: If True, allow a partial symbolic object to be created.
2226
+ Otherwise error will be raised on partial value.
2227
+ root_path: The symbolic path used for the deserialized root object.
2228
+ value_spec: The value spec for the symbolic list or dict.
2173
2229
  **kwargs: Additional keyword arguments that will be passed to
2174
2230
  ``pg.from_json``.
2175
2231
 
@@ -2193,10 +2249,12 @@ def from_json_str(
2193
2249
 
2194
2250
  return from_json(
2195
2251
  _decode_int_keys(json.loads(json_str)),
2252
+ context=context,
2253
+ auto_import=auto_import,
2254
+ convert_unknown=convert_unknown,
2196
2255
  allow_partial=allow_partial,
2197
2256
  root_path=root_path,
2198
- auto_import=auto_import,
2199
- auto_dict=auto_dict,
2257
+ value_spec=value_spec,
2200
2258
  **kwargs
2201
2259
  )
2202
2260
 
@@ -2232,10 +2290,6 @@ def to_json(value: Any, **kwargs) -> Any:
2232
2290
  Returns:
2233
2291
  JSON value.
2234
2292
  """
2235
- # NOTE(daiyip): special handling `sym_jsonify` since symbolized
2236
- # classes may have conflicting `to_json` method in their existing classes.
2237
- if isinstance(value, Symbolic):
2238
- return value.sym_jsonify(**kwargs)
2239
2293
  return utils.to_json(value, **kwargs)
2240
2294
 
2241
2295
 
@@ -20,9 +20,9 @@ from pyglove.core import typing as pg_typing
20
20
  from pyglove.core import utils
21
21
  from pyglove.core import views
22
22
  from pyglove.core.symbolic import base
23
- from pyglove.core.symbolic.dict import Dict
24
- from pyglove.core.symbolic.inferred import ValueFromParentChain
25
- from pyglove.core.symbolic.object import Object
23
+ from pyglove.core.symbolic.dict import Dict # pylint: disable=g-importing-member
24
+ from pyglove.core.symbolic.inferred import ValueFromParentChain # pylint: disable=g-importing-member
25
+ from pyglove.core.symbolic.object import Object # pylint: disable=g-importing-member
26
26
 
27
27
 
28
28
  class FieldUpdateTest(unittest.TestCase):