pyglove 0.5.0.dev202510230131__py3-none-any.whl → 0.5.0.dev202601060812__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.
@@ -12,10 +12,15 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ import datetime
15
16
  import os
16
17
  import pathlib
18
+ import shutil
17
19
  import tempfile
20
+ import time
18
21
  import unittest
22
+ from unittest import mock
23
+ import fsspec
19
24
  from pyglove.core.io import file_system
20
25
 
21
26
 
@@ -82,6 +87,111 @@ class StdFileSystemTest(unittest.TestCase):
82
87
  fs.rmdirs(os.path.join(dir_a, 'b/c'))
83
88
  self.assertEqual(sorted(fs.listdir(dir_a)), ['file1']) # pylint: disable=g-generic-assert
84
89
 
90
+ def test_rename(self):
91
+ tmp_dir = tempfile.mkdtemp()
92
+ fs = file_system.StdFileSystem()
93
+
94
+ _ = fs.mkdirs(os.path.join(tmp_dir, 'a/b'))
95
+ file_foo = os.path.join(tmp_dir, 'a/foo.txt')
96
+ file_bar = os.path.join(tmp_dir, 'a/bar.txt')
97
+
98
+ with fs.open(file_foo, 'w') as f:
99
+ f.write('foo')
100
+ with fs.open(file_bar, 'w') as f:
101
+ f.write('bar')
102
+
103
+ # Rename file to a new name.
104
+ file_foo_new = os.path.join(tmp_dir, 'a/foo-new.txt')
105
+ fs.rename(file_foo, file_foo_new)
106
+ self.assertFalse(fs.exists(file_foo))
107
+ self.assertTrue(fs.exists(file_foo_new))
108
+
109
+ # Rename file to an existing file name.
110
+ fs.rename(file_foo_new, file_bar)
111
+ self.assertFalse(fs.exists(file_foo_new))
112
+ with fs.open(file_bar, 'r') as f:
113
+ self.assertEqual(f.read(), 'foo')
114
+
115
+ # Rename directory to a new name.
116
+ dir_b = os.path.join(tmp_dir, 'a/b')
117
+ dir_c = os.path.join(tmp_dir, 'a/c')
118
+ fs.rename(dir_b, dir_c)
119
+ self.assertFalse(fs.exists(dir_b))
120
+ self.assertTrue(fs.exists(dir_c))
121
+ self.assertTrue(fs.isdir(dir_c))
122
+
123
+ # Rename directory to an existing empty directory.
124
+ dir_d = os.path.join(tmp_dir, 'a/d')
125
+ fs.mkdirs(dir_d)
126
+ fs.rename(dir_c, dir_d)
127
+ self.assertFalse(fs.exists(dir_c))
128
+ self.assertTrue(fs.exists(dir_d))
129
+
130
+ # Rename directory to a non-empty directory.
131
+ dir_x = os.path.join(tmp_dir, 'x')
132
+ dir_a = os.path.join(tmp_dir, 'a')
133
+ fs.mkdirs(os.path.join(dir_x, 'y'))
134
+ with self.assertRaises(OSError):
135
+ fs.rename(dir_a, dir_x)
136
+ self.assertTrue(fs.exists(dir_a))
137
+ self.assertTrue(fs.exists(os.path.join(dir_x, 'y')))
138
+
139
+ # Errors
140
+ dir_u = os.path.join(tmp_dir, 'u')
141
+ dir_u_v = os.path.join(dir_u, 'v')
142
+ file_u_a = os.path.join(dir_u, 'a.txt')
143
+ fs.mkdirs(dir_u_v)
144
+ with fs.open(file_u_a, 'w') as f:
145
+ f.write('a')
146
+
147
+ with self.assertRaises((OSError, NotADirectoryError)):
148
+ fs.rename(dir_u, file_u_a)
149
+
150
+ with self.assertRaises(IsADirectoryError):
151
+ fs.rename(file_u_a, dir_u_v)
152
+
153
+ with self.assertRaises(FileNotFoundError):
154
+ fs.rename(
155
+ os.path.join(tmp_dir, 'non-existent'),
156
+ os.path.join(tmp_dir, 'y')
157
+ )
158
+
159
+ def test_copy(self):
160
+ tmp_dir = tempfile.mkdtemp()
161
+ fs = file_system.StdFileSystem()
162
+ foo_txt = os.path.join(tmp_dir, 'foo.txt')
163
+ bar_txt = os.path.join(tmp_dir, 'bar.txt')
164
+ sub_dir = os.path.join(tmp_dir, 'sub')
165
+ sub_foo_txt = os.path.join(sub_dir, 'foo.txt')
166
+
167
+ with fs.open(foo_txt, 'w') as f:
168
+ f.write('hello')
169
+ fs.copy(foo_txt, bar_txt)
170
+ with fs.open(bar_txt) as f:
171
+ self.assertEqual(f.read(), 'hello')
172
+ fs.mkdir(sub_dir)
173
+ fs.copy(foo_txt, sub_dir)
174
+ with fs.open(sub_foo_txt) as f:
175
+ self.assertEqual(f.read(), 'hello')
176
+ with fs.open(foo_txt, 'w') as f:
177
+ f.write('overwrite')
178
+ fs.copy(foo_txt, bar_txt)
179
+ with fs.open(bar_txt) as f:
180
+ self.assertEqual(f.read(), 'overwrite')
181
+ shutil.rmtree(tmp_dir)
182
+
183
+ def test_getctime_getmtime(self):
184
+ tmp_dir = tempfile.mkdtemp()
185
+ fs = file_system.StdFileSystem()
186
+ file1 = os.path.join(tmp_dir, 'file1')
187
+ with fs.open(file1, 'w') as f:
188
+ f.write('hello')
189
+ ctime = fs.getctime(file1)
190
+ mtime = fs.getmtime(file1)
191
+ self.assertLess(0, ctime)
192
+ self.assertLessEqual(ctime, mtime)
193
+ shutil.rmtree(tmp_dir)
194
+
85
195
 
86
196
  class MemoryFileSystemTest(unittest.TestCase):
87
197
 
@@ -180,6 +290,145 @@ class MemoryFileSystemTest(unittest.TestCase):
180
290
  fs.rmdirs(os.path.join(dir_a, 'b/c'))
181
291
  self.assertEqual(fs.listdir(dir_a), ['file1']) # pylint: disable=g-generic-assert
182
292
 
293
+ def test_glob(self):
294
+ fs = file_system.MemoryFileSystem()
295
+ fs.mkdirs('/mem/a/b/c')
296
+ with fs.open('/mem/a/foo.txt', 'w') as f:
297
+ f.write('foo')
298
+ with fs.open('/mem/a/bar.json', 'w') as f:
299
+ f.write('bar')
300
+ with fs.open('/mem/a/b/baz.txt', 'w') as f:
301
+ f.write('baz')
302
+
303
+ self.assertEqual(
304
+ sorted(fs.glob('/mem/a/*')),
305
+ ['/mem/a/b', '/mem/a/b/baz.txt', '/mem/a/b/c',
306
+ '/mem/a/bar.json', '/mem/a/foo.txt'])
307
+ self.assertEqual(
308
+ sorted(fs.glob('/mem/a/*.txt')),
309
+ ['/mem/a/b/baz.txt', '/mem/a/foo.txt'])
310
+ self.assertEqual(
311
+ sorted(fs.glob('/mem/a/b/*')),
312
+ ['/mem/a/b/baz.txt', '/mem/a/b/c'])
313
+ self.assertEqual(fs.glob('/mem/a/b/*.txt'), ['/mem/a/b/baz.txt'])
314
+ self.assertEqual(fs.glob('/mem/a/b/c/*'), [])
315
+ self.assertEqual(fs.glob('/mem/a/???.txt'), ['/mem/a/foo.txt'])
316
+ self.assertEqual(fs.glob('/mem/a/bar.*'), ['/mem/a/bar.json'])
317
+ self.assertEqual(
318
+ sorted(fs.glob('/mem/a/*.*')),
319
+ ['/mem/a/b/baz.txt', '/mem/a/bar.json', '/mem/a/foo.txt'])
320
+
321
+ def test_rename(self):
322
+ fs = file_system.MemoryFileSystem()
323
+ fs.mkdirs('/mem/a/b')
324
+ with fs.open('/mem/a/foo.txt', 'w') as f:
325
+ f.write('foo')
326
+ with fs.open('/mem/a/bar.txt', 'w') as f:
327
+ f.write('bar')
328
+
329
+ # Rename file to a new name.
330
+ fs.rename('/mem/a/foo.txt', '/mem/a/foo-new.txt')
331
+ self.assertFalse(fs.exists('/mem/a/foo.txt'))
332
+ self.assertTrue(fs.exists('/mem/a/foo-new.txt'))
333
+
334
+ # Rename file to an existing file name.
335
+ fs.rename('/mem/a/foo-new.txt', '/mem/a/bar.txt')
336
+ self.assertFalse(fs.exists('/mem/a/foo-new.txt'))
337
+ with fs.open('/mem/a/bar.txt', 'r') as f:
338
+ self.assertEqual(f.read(), 'foo')
339
+
340
+ # Rename directory to a new name.
341
+ fs.rename('/mem/a/b', '/mem/a/c')
342
+ self.assertFalse(fs.exists('/mem/a/b'))
343
+ self.assertTrue(fs.exists('/mem/a/c'))
344
+ self.assertTrue(fs.isdir('/mem/a/c'))
345
+
346
+ # Rename directory to an existing empty directory.
347
+ fs.mkdirs('/mem/a/d')
348
+ fs.rename('/mem/a/c', '/mem/a/d')
349
+ self.assertFalse(fs.exists('/mem/a/c'))
350
+ self.assertTrue(fs.exists('/mem/a/d'))
351
+
352
+ # Rename directory to a non-empty directory.
353
+ fs.mkdirs('/mem/x/y')
354
+ with self.assertRaisesRegex(OSError, "Directory not empty: '/mem/x'"):
355
+ fs.rename('/mem/a', '/mem/x')
356
+ self.assertTrue(fs.exists('/mem/a'))
357
+ self.assertTrue(fs.exists('/mem/x/y'))
358
+
359
+ # Errors
360
+ fs.mkdirs('/mem/u/v')
361
+ with fs.open('/mem/u/a.txt', 'w') as f:
362
+ f.write('a')
363
+
364
+ with self.assertRaisesRegex(
365
+ OSError, "Cannot move directory '/mem/u' to a subdirectory of itself"):
366
+ fs.rename('/mem/u', '/mem/u/v/w')
367
+
368
+ with self.assertRaisesRegex(
369
+ NotADirectoryError,
370
+ "Cannot rename directory '/mem/u' to non-directory '/mem/u/a.txt'"):
371
+ fs.rename('/mem/u', '/mem/u/a.txt')
372
+
373
+ with self.assertRaisesRegex(
374
+ IsADirectoryError,
375
+ "Cannot rename non-directory '/mem/u/a.txt' to directory '/mem/u/v'"):
376
+ fs.rename('/mem/u/a.txt', '/mem/u/v')
377
+
378
+ with self.assertRaises(FileNotFoundError):
379
+ fs.rename('/mem/non-existent', '/mem/y')
380
+
381
+ def test_copy(self):
382
+ fs = file_system.MemoryFileSystem()
383
+ fs.mkdirs('/mem/a')
384
+ with fs.open('/mem/a/foo.txt', 'w') as f:
385
+ f.write('hello')
386
+ fs.copy('/mem/a/foo.txt', '/mem/a/bar.txt')
387
+ self.assertEqual(fs.open('/mem/a/bar.txt').read(), 'hello')
388
+ fs.mkdir('/mem/b')
389
+ fs.copy('/mem/a/foo.txt', '/mem/b')
390
+ self.assertEqual(fs.open('/mem/b/foo.txt').read(), 'hello')
391
+ with fs.open('/mem/a/foo.txt', 'w') as f:
392
+ f.write('overwrite')
393
+ fs.copy('/mem/a/foo.txt', '/mem/a/bar.txt')
394
+ self.assertEqual(fs.open('/mem/a/bar.txt').read(), 'overwrite')
395
+
396
+ # Test exceptions
397
+ with self.assertRaises(FileNotFoundError):
398
+ fs.copy('/mem/non-existent', '/mem/y')
399
+ with self.assertRaisesRegex(IsADirectoryError, '/mem/a'):
400
+ fs.copy('/mem/a', '/mem/y')
401
+
402
+ fs.mkdirs('/mem/c/foo.txt')
403
+ with self.assertRaisesRegex(IsADirectoryError, '/mem/c/foo.txt'):
404
+ fs.copy('/mem/a/foo.txt', '/mem/c')
405
+
406
+ def test_getctime_getmtime(self):
407
+ fs = file_system.MemoryFileSystem()
408
+ fs.mkdirs('/mem/a')
409
+ file1 = '/mem/file1_times'
410
+ with fs.open(file1, 'w') as f:
411
+ f.write('hello')
412
+ ctime = fs.getctime(file1)
413
+ mtime = fs.getmtime(file1)
414
+ self.assertLess(0, ctime)
415
+ self.assertLess(ctime, mtime)
416
+ time.sleep(0.01)
417
+ with fs.open(file1, 'w') as f:
418
+ f.write('world')
419
+ self.assertEqual(fs.getctime(file1), ctime)
420
+ self.assertLess(mtime, fs.getmtime(file1))
421
+
422
+ with self.assertRaises(IsADirectoryError):
423
+ fs.getctime('/mem/a')
424
+ with self.assertRaises(FileNotFoundError):
425
+ fs.getctime('/mem/non_existent')
426
+
427
+ with self.assertRaises(IsADirectoryError):
428
+ fs.getmtime('/mem/a')
429
+ with self.assertRaises(FileNotFoundError):
430
+ fs.getmtime('/mem/non_existent')
431
+
183
432
 
184
433
  class FileIoApiTest(unittest.TestCase):
185
434
 
@@ -217,6 +466,14 @@ class FileIoApiTest(unittest.TestCase):
217
466
  file_system.rm(file2)
218
467
  self.assertFalse(file_system.path_exists(file2))
219
468
 
469
+ # Test glob with standard file system.
470
+ glob_dir = os.path.join(tempfile.mkdtemp(), 'glob')
471
+ file_system.mkdirs(os.path.join(glob_dir, 'a/b'))
472
+ file_system.writefile(os.path.join(glob_dir, 'a/foo.txt'), 'foo')
473
+ self.assertEqual(
474
+ sorted(file_system.glob(os.path.join(glob_dir, 'a/*'))),
475
+ [os.path.join(glob_dir, 'a/b'), os.path.join(glob_dir, 'a/foo.txt')])
476
+
220
477
  def test_memory_filesystem(self):
221
478
  file1 = pathlib.Path('/mem/file1')
222
479
  with self.assertRaises(FileNotFoundError):
@@ -248,6 +505,191 @@ class FileIoApiTest(unittest.TestCase):
248
505
  file_system.rm(file2)
249
506
  self.assertFalse(file_system.path_exists(file2))
250
507
 
508
+ # Test glob with memory file system.
509
+ file_system.mkdirs('/mem/g/a/b')
510
+ file_system.writefile('/mem/g/a/foo.txt', 'foo')
511
+ file_system.rename('/mem/g/a/foo.txt', '/mem/g/a/foo2.txt')
512
+ file_system.writefile('/mem/g/a/b/bar.txt', 'bar')
513
+ self.assertEqual(
514
+ sorted(file_system.glob('/mem/g/a/*')),
515
+ ['/mem/g/a/b', '/mem/g/a/b/bar.txt', '/mem/g/a/foo2.txt'])
516
+
517
+ def test_getctime_getmtime(self):
518
+ # Test with standard file system.
519
+ std_file = os.path.join(tempfile.mkdtemp(), 'file_ctime_mtime')
520
+ file_system.writefile(std_file, 'foo')
521
+ self.assertLess(0, file_system.getctime(std_file))
522
+ self.assertLess(0, file_system.getmtime(std_file))
523
+
524
+ # Test with memory file system.
525
+ mem_file = '/mem/file_ctime_mtime'
526
+ file_system.writefile(mem_file, 'foo')
527
+ self.assertLess(0, file_system.getctime(mem_file))
528
+ self.assertLess(0, file_system.getmtime(mem_file))
529
+
530
+
531
+ class FsspecFileSystemTest(unittest.TestCase):
532
+
533
+ def setUp(self):
534
+ super().setUp()
535
+ self.fs = fsspec.filesystem('memory')
536
+ self.fs.pipe('memory:///a/b/c', b'abc')
537
+ self.fs.pipe('memory:///a/b/d', b'abd')
538
+ self.fs.mkdir('memory:///a/e')
539
+ self.tmp_dir = tempfile.mkdtemp()
540
+
541
+ def tearDown(self):
542
+ super().tearDown()
543
+ fsspec.filesystem('memory').rm('/', recursive=True)
544
+ shutil.rmtree(self.tmp_dir)
545
+
546
+ def test_read_file(self):
547
+ self.assertEqual(file_system.readfile('memory:///a/b/c', mode='rb'), b'abc')
548
+ with file_system.open('memory:///a/b/d', 'rb') as f:
549
+ self.assertEqual(f.read(), b'abd')
550
+
551
+ def test_fsspec_file_ops(self):
552
+ file_system.writefile('memory:///f', b'hello\nworld\n', mode='wb')
553
+ with file_system.open('memory:///f', 'rb') as f:
554
+ self.assertIsInstance(f, file_system.FsspecFile)
555
+ self.assertEqual(f.readline(), b'hello\n')
556
+ self.assertEqual(f.tell(), 6)
557
+ self.assertEqual(f.seek(8), 8)
558
+ self.assertEqual(f.read(), b'rld\n')
559
+ f.flush()
560
+
561
+ def test_write_file(self):
562
+ file_system.writefile('memory:///a/b/e', b'abe', mode='wb')
563
+ self.assertTrue(self.fs.exists('memory:///a/b/e'))
564
+ self.assertEqual(self.fs.cat('memory:///a/b/e'), b'abe')
565
+
566
+ def test_exists(self):
567
+ self.assertTrue(file_system.path_exists('memory:///a/b/c'))
568
+ self.assertFalse(file_system.path_exists('memory:///a/b/nonexist'))
569
+
570
+ def test_isdir(self):
571
+ self.assertTrue(file_system.isdir('memory:///a/b'))
572
+ self.assertTrue(file_system.isdir('memory:///a/e'))
573
+ self.assertFalse(file_system.isdir('memory:///a/b/c'))
574
+
575
+ def test_listdir(self):
576
+ self.assertCountEqual(file_system.listdir('memory:///a'), ['b', 'e'])
577
+ self.assertCountEqual(file_system.listdir('memory:///a/b'), ['c', 'd'])
578
+
579
+ def test_glob(self):
580
+ self.assertCountEqual(
581
+ file_system.glob('memory:///a/b/*'),
582
+ ['memory:///a/b/c', 'memory:///a/b/d']
583
+ )
584
+
585
+ def test_mkdir(self):
586
+ file_system.mkdir('memory:///a/f')
587
+ self.assertTrue(self.fs.isdir('memory:///a/f'))
588
+
589
+ def test_mkdirs(self):
590
+ file_system.mkdirs('memory:///g/h/i')
591
+ self.assertTrue(self.fs.isdir('memory:///g/h/i'))
592
+
593
+ def test_rm(self):
594
+ file_system.rm('memory:///a/b/c')
595
+ self.assertFalse(self.fs.exists('memory:///a/b/c'))
596
+
597
+ def test_rename(self):
598
+ file_system.rename('memory:///a/b/c', 'memory:///a/b/c_new')
599
+ self.assertFalse(self.fs.exists('memory:///a/b/c'))
600
+ self.assertTrue(self.fs.exists('memory:///a/b/c_new'))
601
+ with self.assertRaisesRegex(ValueError, 'Rename across different'):
602
+ file_system.rename('memory:///a/b/c_new', 'file:///a/b/c_d')
603
+
604
+ def test_chmod(self):
605
+ mock_fs = mock.Mock()
606
+ mock_fs.chmod = mock.Mock()
607
+ with mock.patch('fsspec.core.url_to_fs', return_value=(mock_fs, 'path')):
608
+ file_system.chmod('protocol:///path', 0o777)
609
+ mock_fs.chmod.assert_called_once_with('path', 0o777)
610
+
611
+ def test_rmdir(self):
612
+ file_system.rmdir('memory:///a/e')
613
+ self.assertFalse(self.fs.exists('memory:///a/e'))
614
+
615
+ def test_rmdirs(self):
616
+ file_system.mkdirs('memory:///x/y/z')
617
+ self.assertTrue(file_system.isdir('memory:///x/y/z'))
618
+ file_system.rmdirs('memory:///x')
619
+ self.assertFalse(file_system.path_exists('memory:///x'))
620
+
621
+ def test_copy(self):
622
+ # same FS copy
623
+ file_system.copy('memory:///a/b/c', 'memory:///a/f')
624
+ self.assertEqual(file_system.readfile('memory:///a/f', mode='rb'), b'abc')
625
+
626
+ # same FS copy to dir
627
+ file_system.copy('memory:///a/b/d', 'memory:///a/e')
628
+ self.assertEqual(file_system.readfile('memory:///a/e/d', mode='rb'), b'abd')
629
+
630
+ # cross FS copy: memory to local
631
+ local_path = os.path.join(self.tmp_dir, 'test.txt')
632
+ file_system.copy('memory:///a/b/c', f'file://{local_path}')
633
+ self.assertEqual(file_system.readfile(local_path, mode='rb'), b'abc')
634
+
635
+ # cross FS copy: local to memory
636
+ file_system.copy(f'file://{local_path}', 'memory:///a/g')
637
+ self.assertEqual(file_system.readfile('memory:///a/g', mode='rb'), b'abc')
638
+
639
+ def test_getctime_getmtime(self):
640
+ self.fs.touch('memory:///a/b/c_time')
641
+ now = datetime.datetime.now()
642
+ with mock.patch.object(self.fs, 'created', return_value=now):
643
+ self.assertEqual(
644
+ file_system.getctime('memory:///a/b/c_time'), now.timestamp()
645
+ )
646
+ with mock.patch.object(self.fs, 'modified', return_value=now):
647
+ self.assertEqual(
648
+ file_system.getmtime('memory:///a/b/c_time'), now.timestamp()
649
+ )
650
+
651
+ with mock.patch.object(self.fs, 'created', return_value=None):
652
+ with self.assertRaisesRegex(OSError, 'c-time is not available'):
653
+ file_system.getctime('memory:///a/b/c_time')
654
+
655
+ with mock.patch.object(self.fs, 'modified', return_value=None):
656
+ with self.assertRaisesRegex(OSError, 'm-time is not available'):
657
+ file_system.getmtime('memory:///a/b/c_time')
658
+
659
+ def test_fsspec_uri_catcher(self):
660
+ with mock.patch.object(
661
+ file_system.FsspecFileSystem, 'exists', return_value=True
662
+ ) as mock_fsspec_exists:
663
+ # We use a protocol that is not registered in
664
+ # fsspec.available_protocols() to make sure _FsspecUriCatcher is used.
665
+ self.assertTrue(file_system.path_exists('some-proto://foo'))
666
+ mock_fsspec_exists.assert_called_once_with('some-proto://foo')
667
+
668
+ # For full coverage of _FsspecUriCatcher.get_fs returning StdFileSystem,
669
+ # we need to test a non-URI path that doesn't match other prefixes.
670
+ # We mock StdFileSystem.exists to check if it's called.
671
+ with mock.patch.object(
672
+ file_system.StdFileSystem, 'exists', return_value=True
673
+ ) as mock_std_exists:
674
+ self.assertTrue(file_system.path_exists('/foo/bar/baz'))
675
+ mock_std_exists.assert_called_once_with('/foo/bar/baz')
676
+
677
+ with mock.patch.object(
678
+ file_system.FsspecFileSystem, 'rename', return_value=None
679
+ ) as mock_fsspec_rename:
680
+ file_system.rename('some-proto://foo', 'some-proto://bar')
681
+ mock_fsspec_rename.assert_called_once_with(
682
+ 'some-proto://foo', 'some-proto://bar'
683
+ )
684
+
685
+ with mock.patch.object(
686
+ file_system.FsspecFileSystem, 'copy', return_value=None
687
+ ) as mock_fsspec_copy:
688
+ file_system.copy('some-proto://foo', 'some-proto://bar')
689
+ mock_fsspec_copy.assert_called_once_with(
690
+ 'some-proto://foo', 'some-proto://bar'
691
+ )
692
+
251
693
 
252
694
  if __name__ == '__main__':
253
695
  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
@@ -2042,7 +2042,7 @@ def from_json(
2042
2042
  context: Optional[utils.JSONConversionContext] = None,
2043
2043
  auto_symbolic: bool = True,
2044
2044
  auto_import: bool = True,
2045
- auto_dict: bool = False,
2045
+ convert_unknown: bool = False,
2046
2046
  allow_partial: bool = False,
2047
2047
  root_path: Optional[utils.KeyPath] = None,
2048
2048
  value_spec: Optional[pg_typing.ValueSpec] = None,
@@ -2073,8 +2073,13 @@ def from_json(
2073
2073
  identify its parent module and automatically import it. For example,
2074
2074
  if the type is 'foo.bar.A', PyGlove will try to import 'foo.bar' and
2075
2075
  find the class 'A' within the imported module.
2076
- auto_dict: If True, dict with '_type' that cannot be loaded will remain
2077
- 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.
2078
2083
  allow_partial: Whether to allow elements of the list to be partial.
2079
2084
  root_path: KeyPath of loaded object in its object tree.
2080
2085
  value_spec: The value spec for the symbolic list or dict.
@@ -2095,7 +2100,12 @@ def from_json(
2095
2100
  if context is None:
2096
2101
  if (isinstance(json_value, dict) and (
2097
2102
  context_node := json_value.get(utils.JSONConvertible.CONTEXT_KEY))):
2098
- context = utils.JSONConversionContext.from_json(context_node, **kwargs)
2103
+ context = utils.JSONConversionContext.from_json(
2104
+ context_node,
2105
+ auto_import=auto_import,
2106
+ convert_unknown=convert_unknown,
2107
+ **kwargs
2108
+ )
2099
2109
  json_value = json_value[utils.JSONConvertible.ROOT_VALUE_KEY]
2100
2110
  else:
2101
2111
  context = utils.JSONConversionContext()
@@ -2103,7 +2113,7 @@ def from_json(
2103
2113
  typename_resolved = kwargs.pop('_typename_resolved', False)
2104
2114
  if not typename_resolved:
2105
2115
  json_value = utils.json_conversion.resolve_typenames(
2106
- json_value, auto_import, auto_dict
2116
+ json_value, auto_import, convert_unknown
2107
2117
  )
2108
2118
 
2109
2119
  def _load_child(k, v):
@@ -2177,7 +2187,7 @@ def from_json_str(
2177
2187
  *,
2178
2188
  context: Optional[utils.JSONConversionContext] = None,
2179
2189
  auto_import: bool = True,
2180
- auto_dict: bool = False,
2190
+ convert_unknown: bool = False,
2181
2191
  allow_partial: bool = False,
2182
2192
  root_path: Optional[utils.KeyPath] = None,
2183
2193
  value_spec: Optional[pg_typing.ValueSpec] = None,
@@ -2205,8 +2215,13 @@ def from_json_str(
2205
2215
  identify its parent module and automatically import it. For example,
2206
2216
  if the type is 'foo.bar.A', PyGlove will try to import 'foo.bar' and
2207
2217
  find the class 'A' within the imported module.
2208
- auto_dict: If True, dict with '_type' that cannot be loaded will remain
2209
- 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.
2210
2225
  allow_partial: If True, allow a partial symbolic object to be created.
2211
2226
  Otherwise error will be raised on partial value.
2212
2227
  root_path: The symbolic path used for the deserialized root object.
@@ -2236,7 +2251,7 @@ def from_json_str(
2236
2251
  _decode_int_keys(json.loads(json_str)),
2237
2252
  context=context,
2238
2253
  auto_import=auto_import,
2239
- auto_dict=auto_dict,
2254
+ convert_unknown=convert_unknown,
2240
2255
  allow_partial=allow_partial,
2241
2256
  root_path=root_path,
2242
2257
  value_spec=value_spec,
@@ -506,8 +506,7 @@ class ListTest(unittest.TestCase):
506
506
  def test_index(self):
507
507
  sl = List([0, 1, 2, 1])
508
508
  self.assertEqual(sl.index(1), 1)
509
- with self.assertRaisesRegex(
510
- ValueError, '3 is not in list'):
509
+ with self.assertRaisesRegex(ValueError, '.* not in list'):
511
510
  _ = sl.index(3)
512
511
 
513
512
  # Index of inferred value is based on its symbolic form.
@@ -339,7 +339,8 @@ class Object(base.Symbolic, metaclass=ObjectMeta):
339
339
 
340
340
  # Set `__serialization_key__` before JSONConvertible.__init_subclass__
341
341
  # is called.
342
- setattr(cls, '__serialization_key__', cls.__type_name__)
342
+ if '__serialization_key__' not in cls.__dict__:
343
+ setattr(cls, '__serialization_key__', cls.__type_name__)
343
344
 
344
345
  super().__init_subclass__()
345
346
 
@@ -38,6 +38,7 @@ from pyglove.core.symbolic.object import use_init_args as pg_use_init_args
38
38
  from pyglove.core.symbolic.origin import Origin
39
39
  from pyglove.core.symbolic.pure_symbolic import NonDeterministic
40
40
  from pyglove.core.symbolic.pure_symbolic import PureSymbolic
41
+ from pyglove.core.symbolic.unknown_symbols import UnknownTypedObject
41
42
  from pyglove.core.views.html import tree_view # pylint: disable=unused-import
42
43
 
43
44
 
@@ -3158,7 +3159,7 @@ class SerializationTest(unittest.TestCase):
3158
3159
  Q.partial(P.partial()).to_json_str(), allow_partial=True),
3159
3160
  Q.partial(P.partial()))
3160
3161
 
3161
- def test_serialization_with_auto_dict(self):
3162
+ def test_serialization_with_convert_unknown(self):
3162
3163
 
3163
3164
  class P(Object):
3164
3165
  auto_register = False
@@ -3181,15 +3182,17 @@ class SerializationTest(unittest.TestCase):
3181
3182
  }
3182
3183
  )
3183
3184
  self.assertEqual(
3184
- base.from_json_str(Q(P(1), y='foo').to_json_str(), auto_dict=True),
3185
- {
3186
- 'p': {
3187
- 'type_name': P.__type_name__,
3188
- 'x': 1
3189
- },
3190
- 'y': 'foo',
3191
- 'type_name': Q.__type_name__,
3192
- }
3185
+ base.from_json_str(
3186
+ Q(P(1), y='foo').to_json_str(), convert_unknown=True
3187
+ ),
3188
+ UnknownTypedObject(
3189
+ type_name=Q.__type_name__,
3190
+ p=UnknownTypedObject(
3191
+ type_name=P.__type_name__,
3192
+ x=1
3193
+ ),
3194
+ y='foo'
3195
+ )
3193
3196
  )
3194
3197
 
3195
3198
  def test_serialization_with_converter(self):