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.
- pyglove/core/io/file_system.py +452 -2
- pyglove/core/io/file_system_test.py +442 -0
- pyglove/core/symbolic/__init__.py +7 -0
- pyglove/core/symbolic/base.py +24 -9
- pyglove/core/symbolic/list_test.py +1 -2
- pyglove/core/symbolic/object.py +2 -1
- pyglove/core/symbolic/object_test.py +13 -10
- pyglove/core/symbolic/unknown_symbols.py +147 -0
- pyglove/core/symbolic/unknown_symbols_test.py +100 -0
- pyglove/core/typing/annotation_conversion.py +3 -0
- pyglove/core/typing/annotation_conversion_test.py +11 -19
- pyglove/core/typing/type_conversion.py +17 -3
- pyglove/core/typing/type_conversion_test.py +7 -2
- pyglove/core/typing/value_specs.py +5 -1
- pyglove/core/typing/value_specs_test.py +5 -0
- pyglove/core/utils/json_conversion.py +107 -49
- pyglove/core/utils/json_conversion_test.py +85 -10
- pyglove/core/views/html/controls/tab.py +33 -0
- pyglove/core/views/html/controls/tab_test.py +37 -0
- pyglove/ext/evolution/base_test.py +1 -1
- {pyglove-0.5.0.dev202510230131.dist-info → pyglove-0.5.0.dev202601060812.dist-info}/METADATA +8 -1
- {pyglove-0.5.0.dev202510230131.dist-info → pyglove-0.5.0.dev202601060812.dist-info}/RECORD +25 -23
- {pyglove-0.5.0.dev202510230131.dist-info → pyglove-0.5.0.dev202601060812.dist-info}/WHEEL +0 -0
- {pyglove-0.5.0.dev202510230131.dist-info → pyglove-0.5.0.dev202601060812.dist-info}/licenses/LICENSE +0 -0
- {pyglove-0.5.0.dev202510230131.dist-info → pyglove-0.5.0.dev202601060812.dist-info}/top_level.txt +0 -0
|
@@ -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
|
pyglove/core/symbolic/base.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
2077
|
-
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
2209
|
-
|
|
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
|
-
|
|
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.
|
pyglove/core/symbolic/object.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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(
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
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):
|