pyglove 0.5.0.dev202510230131__py3-none-any.whl → 0.5.0.dev202511300809__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.
@@ -14,8 +14,11 @@
14
14
  """Pluggable file system."""
15
15
 
16
16
  import abc
17
+ import fnmatch
18
+ import glob as std_glob
17
19
  import io
18
20
  import os
21
+ import re
19
22
  from typing import Any, Literal, Optional, Union
20
23
 
21
24
 
@@ -84,6 +87,10 @@ class FileSystem(metaclass=abc.ABCMeta):
84
87
  def exists(self, path: Union[str, os.PathLike[str]]) -> bool:
85
88
  """Returns True if a path exists."""
86
89
 
90
+ @abc.abstractmethod
91
+ def glob(self, pattern: Union[str, os.PathLike[str]]) -> list[str]:
92
+ """Lists all files or sub-directories."""
93
+
87
94
  @abc.abstractmethod
88
95
  def listdir(self, path: Union[str, os.PathLike[str]]) -> list[str]:
89
96
  """Lists all files or sub-directories."""
@@ -111,6 +118,14 @@ class FileSystem(metaclass=abc.ABCMeta):
111
118
  def rm(self, path: Union[str, os.PathLike[str]]) -> None:
112
119
  """Removes a file based on a path."""
113
120
 
121
+ @abc.abstractmethod
122
+ def rename(
123
+ self,
124
+ oldpath: Union[str, os.PathLike[str]],
125
+ newpath: Union[str, os.PathLike[str]],
126
+ ) -> None:
127
+ """Renames a file based on a path."""
128
+
114
129
  @abc.abstractmethod
115
130
  def rmdir(self, path: Union[str, os.PathLike[str]]) -> bool:
116
131
  """Removes a directory based on a path."""
@@ -137,9 +152,10 @@ def resolve_path(path: Union[str, os.PathLike[str]]) -> str:
137
152
  class StdFile(File):
138
153
  """The standard file."""
139
154
 
140
- def __init__(self, file_object) -> None:
155
+ def __init__(self, file_object, path: Union[str, os.PathLike[str]]) -> None:
141
156
  super().__init__()
142
157
  self._file_object = file_object
158
+ self._path = path
143
159
 
144
160
  def read(self, size: Optional[int] = None) -> Union[str, bytes]:
145
161
  return self._file_object.read(size)
@@ -169,7 +185,7 @@ class StdFileSystem(FileSystem):
169
185
  def open(
170
186
  self, path: Union[str, os.PathLike[str]], mode: str = 'r', **kwargs
171
187
  ) -> File:
172
- return StdFile(io.open(path, mode, **kwargs))
188
+ return StdFile(io.open(path, mode, **kwargs), path)
173
189
 
174
190
  def chmod(self, path: Union[str, os.PathLike[str]], mode: int) -> None:
175
191
  os.chmod(path, mode)
@@ -177,6 +193,9 @@ class StdFileSystem(FileSystem):
177
193
  def exists(self, path: Union[str, os.PathLike[str]]) -> bool:
178
194
  return os.path.exists(path)
179
195
 
196
+ def glob(self, pattern: Union[str, os.PathLike[str]]) -> list[str]:
197
+ return std_glob.glob(pattern)
198
+
180
199
  def listdir(self, path: Union[str, os.PathLike[str]]) -> list[str]:
181
200
  return os.listdir(path)
182
201
 
@@ -196,6 +215,13 @@ class StdFileSystem(FileSystem):
196
215
  ) -> None:
197
216
  os.makedirs(path, mode, exist_ok)
198
217
 
218
+ def rename(
219
+ self,
220
+ oldpath: Union[str, os.PathLike[str]],
221
+ newpath: Union[str, os.PathLike[str]],
222
+ ) -> None:
223
+ os.rename(oldpath, newpath)
224
+
199
225
  def rm(self, path: Union[str, os.PathLike[str]]) -> None:
200
226
  os.remove(path)
201
227
 
@@ -286,6 +312,21 @@ class MemoryFileSystem(FileSystem):
286
312
  def exists(self, path: Union[str, os.PathLike[str]]) -> bool:
287
313
  return self._locate(path) is not None
288
314
 
315
+ def glob(self, pattern: Union[str, os.PathLike[str]]) -> list[str]:
316
+ pattern = resolve_path(pattern)
317
+ regex = re.compile(fnmatch.translate(pattern))
318
+
319
+ results = []
320
+ def _traverse(node, prefix_parts):
321
+ for k, v in node.items():
322
+ path = self._prefix + '/'.join(prefix_parts + [k])
323
+ if regex.match(path):
324
+ results.append(path)
325
+ if isinstance(v, dict):
326
+ _traverse(v, prefix_parts + [k])
327
+ _traverse(self._root, [])
328
+ return results
329
+
289
330
  def listdir(self, path: Union[str, os.PathLike[str]]) -> list[str]:
290
331
  d = self._locate(path)
291
332
  if not isinstance(d, dict):
@@ -338,6 +379,46 @@ class MemoryFileSystem(FileSystem):
338
379
  raise NotADirectoryError(path)
339
380
  current = entry
340
381
 
382
+ def rename(
383
+ self,
384
+ oldpath: Union[str, os.PathLike[str]],
385
+ newpath: Union[str, os.PathLike[str]],
386
+ ) -> None:
387
+ oldpath_str = resolve_path(oldpath)
388
+ newpath_str = resolve_path(newpath)
389
+
390
+ old_parent_dir, old_name = self._parent_and_name(oldpath_str)
391
+ entry = old_parent_dir.get(old_name)
392
+
393
+ if entry is None:
394
+ raise FileNotFoundError(oldpath_str)
395
+
396
+ new_entry = self._locate(newpath_str)
397
+ if new_entry is not None:
398
+ if isinstance(entry, dict): # oldpath is dir
399
+ if not isinstance(new_entry, dict):
400
+ raise NotADirectoryError(
401
+ f'Cannot rename directory {oldpath_str!r} '
402
+ f'to non-directory {newpath_str!r}')
403
+ elif new_entry:
404
+ raise OSError(f'Directory not empty: {newpath_str!r}')
405
+ else:
406
+ self.rmdir(newpath_str)
407
+ else: # oldpath is file
408
+ if isinstance(new_entry, dict):
409
+ raise IsADirectoryError(
410
+ f'Cannot rename non-directory {oldpath_str!r} '
411
+ f'to directory {newpath_str!r}')
412
+ else:
413
+ self.rm(newpath_str)
414
+ elif isinstance(entry, dict) and newpath_str.startswith(oldpath_str + '/'):
415
+ raise OSError(f'Cannot move directory {oldpath_str!r} '
416
+ f'to a subdirectory of itself {newpath_str!r}')
417
+
418
+ new_parent_dir, new_name = self._parent_and_name(newpath_str)
419
+ new_parent_dir[new_name] = entry
420
+ del old_parent_dir[old_name]
421
+
341
422
  def rm(self, path: Union[str, os.PathLike[str]]) -> None:
342
423
  parent_dir, name = self._parent_and_name(path)
343
424
  entry = parent_dir.get(name)
@@ -412,6 +493,205 @@ def add_file_system(prefix: str, fs: FileSystem) -> None:
412
493
  add_file_system('/mem/', MemoryFileSystem('/mem/'))
413
494
 
414
495
 
496
+ try:
497
+ # pylint: disable=g-import-not-at-top
498
+ # pytype: disable=import-error
499
+ import fsspec
500
+ # pytype: enable=import-error
501
+ # pylint: enable=g-import-not-at-top
502
+ except ImportError:
503
+ fsspec = None
504
+
505
+
506
+ class FsspecFile(File):
507
+ """File object based on fsspec."""
508
+
509
+ def __init__(self, f):
510
+ self._f = f
511
+
512
+ def read(self, size: Optional[int] = None) -> Union[str, bytes]:
513
+ return self._f.read(size)
514
+
515
+ def readline(self) -> Union[str, bytes]:
516
+ return self._f.readline()
517
+
518
+ def write(self, content: Union[str, bytes]) -> None:
519
+ self._f.write(content)
520
+
521
+ def seek(self, offset: int, whence: Literal[0, 1, 2] = 0) -> int:
522
+ return self._f.seek(offset, whence)
523
+
524
+ def tell(self) -> int:
525
+ return self._f.tell()
526
+
527
+ def flush(self) -> None:
528
+ self._f.flush()
529
+
530
+ def close(self) -> None:
531
+ self._f.close()
532
+
533
+
534
+ class FsspecFileSystem(FileSystem):
535
+ """File system based on fsspec."""
536
+
537
+ def open(
538
+ self, path: Union[str, os.PathLike[str]], mode: str = 'r', **kwargs
539
+ ) -> File:
540
+ assert fsspec is not None, '`fsspec` is not installed.'
541
+ return FsspecFile(fsspec.open(path, mode, **kwargs).open())
542
+
543
+ def chmod(self, path: Union[str, os.PathLike[str]], mode: int) -> None:
544
+ assert fsspec is not None, '`fsspec` is not installed.'
545
+ fs, path = fsspec.core.url_to_fs(path)
546
+ if hasattr(fs, 'chmod'):
547
+ fs.chmod(path, mode)
548
+
549
+ def exists(self, path: Union[str, os.PathLike[str]]) -> bool:
550
+ assert fsspec is not None, '`fsspec` is not installed.'
551
+ fs, path = fsspec.core.url_to_fs(path)
552
+ return fs.exists(path)
553
+
554
+ def glob(self, pattern: Union[str, os.PathLike[str]]) -> list[str]:
555
+ assert fsspec is not None, '`fsspec` is not installed.'
556
+ fs, path = fsspec.core.url_to_fs(pattern)
557
+ protocol = fsspec.utils.get_protocol(pattern)
558
+ return [f'{protocol}:///{r.lstrip("/")}' for r in fs.glob(path)]
559
+
560
+ def listdir(self, path: Union[str, os.PathLike[str]]) -> list[str]:
561
+ assert fsspec is not None, '`fsspec` is not installed.'
562
+ fs, path = fsspec.core.url_to_fs(path)
563
+ return [os.path.basename(f) for f in fs.ls(path, detail=False)]
564
+
565
+ def isdir(self, path: Union[str, os.PathLike[str]]) -> bool:
566
+ assert fsspec is not None, '`fsspec` is not installed.'
567
+ fs, path = fsspec.core.url_to_fs(path)
568
+ return fs.isdir(path)
569
+
570
+ def mkdir(
571
+ self,
572
+ path: Union[str, os.PathLike[str]], mode: int = 0o777
573
+ ) -> None:
574
+ assert fsspec is not None, '`fsspec` is not installed.'
575
+ fs, path = fsspec.core.url_to_fs(path)
576
+ fs.mkdir(path)
577
+
578
+ def mkdirs(
579
+ self,
580
+ path: Union[str, os.PathLike[str]],
581
+ mode: int = 0o777,
582
+ exist_ok: bool = True,
583
+ ) -> None:
584
+ assert fsspec is not None, '`fsspec` is not installed.'
585
+ fs, path = fsspec.core.url_to_fs(path)
586
+ fs.makedirs(path, exist_ok=exist_ok)
587
+
588
+ def rm(self, path: Union[str, os.PathLike[str]]) -> None:
589
+ assert fsspec is not None, '`fsspec` is not installed.'
590
+ fs, path = fsspec.core.url_to_fs(path)
591
+ fs.rm(path)
592
+
593
+ def rename(
594
+ self,
595
+ oldpath: Union[str, os.PathLike[str]],
596
+ newpath: Union[str, os.PathLike[str]],
597
+ ) -> None:
598
+ assert fsspec is not None, '`fsspec` is not installed.'
599
+ fs, old_path = fsspec.core.url_to_fs(oldpath)
600
+ fs2, new_path = fsspec.core.url_to_fs(newpath)
601
+ if fs.__class__ != fs2.__class__:
602
+ raise ValueError(
603
+ f'Rename across different filesystems is not supported: '
604
+ f'{type(fs)} vs {type(fs2)}'
605
+ )
606
+ fs.rename(old_path, new_path)
607
+
608
+ def rmdir(self, path: Union[str, os.PathLike[str]]) -> None: # pytype: disable=signature-mismatch
609
+ assert fsspec is not None, '`fsspec` is not installed.'
610
+ fs, path = fsspec.core.url_to_fs(path)
611
+ fs.rmdir(path)
612
+
613
+ def rmdirs(self, path: Union[str, os.PathLike[str]]) -> None:
614
+ assert fsspec is not None, '`fsspec` is not installed.'
615
+ fs, path = fsspec.core.url_to_fs(path)
616
+ fs.rm(path, recursive=True)
617
+
618
+
619
+ class _FsspecUriCatcher(FileSystem):
620
+ """File system to catch URI paths and redirect to FsspecFileSystem."""
621
+
622
+ # Catch all paths that contains '://' but not registered by
623
+ # available_protocols.
624
+ _URI_PATTERN = re.compile(r'^[a-zA-Z][a-zA-Z0-9+-.]*://.*')
625
+
626
+ def __init__(self):
627
+ super().__init__()
628
+ self._std_fs = StdFileSystem()
629
+ self._fsspec_fs = FsspecFileSystem()
630
+
631
+ def get_fs(self, path: Union[str, os.PathLike[str]]) -> FileSystem:
632
+ if self._URI_PATTERN.match(resolve_path(path)):
633
+ return self._fsspec_fs
634
+ return self._std_fs
635
+
636
+ def open(
637
+ self, path: Union[str, os.PathLike[str]], mode: str = 'r', **kwargs
638
+ ) -> File:
639
+ return self.get_fs(path).open(path, mode, **kwargs)
640
+
641
+ def chmod(self, path: Union[str, os.PathLike[str]], mode: int) -> None:
642
+ self.get_fs(path).chmod(path, mode)
643
+
644
+ def exists(self, path: Union[str, os.PathLike[str]]) -> bool:
645
+ return self.get_fs(path).exists(path)
646
+
647
+ def glob(self, pattern: Union[str, os.PathLike[str]]) -> list[str]:
648
+ return self.get_fs(pattern).glob(pattern)
649
+
650
+ def listdir(self, path: Union[str, os.PathLike[str]]) -> list[str]:
651
+ return self.get_fs(path).listdir(path)
652
+
653
+ def isdir(self, path: Union[str, os.PathLike[str]]) -> bool:
654
+ return self.get_fs(path).isdir(path)
655
+
656
+ def mkdir(
657
+ self,
658
+ path: Union[str, os.PathLike[str]],
659
+ mode: int = 0o777
660
+ ) -> None:
661
+ self.get_fs(path).mkdir(path, mode)
662
+
663
+ def mkdirs(
664
+ self,
665
+ path: Union[str, os.PathLike[str]],
666
+ mode: int = 0o777,
667
+ exist_ok: bool = True,
668
+ ) -> None:
669
+ self.get_fs(path).mkdirs(path, mode, exist_ok)
670
+
671
+ def rm(self, path: Union[str, os.PathLike[str]]) -> None:
672
+ self.get_fs(path).rm(path)
673
+
674
+ def rename(
675
+ self,
676
+ oldpath: Union[str, os.PathLike[str]],
677
+ newpath: Union[str, os.PathLike[str]],
678
+ ) -> None:
679
+ self.get_fs(oldpath).rename(oldpath, newpath)
680
+
681
+ def rmdir(self, path: Union[str, os.PathLike[str]]) -> None: # pytype: disable=signature-mismatch
682
+ self.get_fs(path).rmdir(path)
683
+
684
+ def rmdirs(self, path: Union[str, os.PathLike[str]]) -> None:
685
+ self.get_fs(path).rmdirs(path)
686
+
687
+
688
+ if fsspec is not None:
689
+ fsspec_fs = FsspecFileSystem()
690
+ for p in fsspec.available_protocols():
691
+ add_file_system(p + '://', fsspec_fs)
692
+ add_file_system('', _FsspecUriCatcher())
693
+
694
+
415
695
  #
416
696
  # APIs for file IO.
417
697
  #
@@ -458,6 +738,14 @@ def writefile(
458
738
  chmod(path, perms)
459
739
 
460
740
 
741
+ def rename(
742
+ oldpath: Union[str, os.PathLike[str]],
743
+ newpath: Union[str, os.PathLike[str]],
744
+ ) -> None:
745
+ """Renames a file."""
746
+ _fs.get(oldpath).rename(oldpath, newpath)
747
+
748
+
461
749
  def rm(path: Union[str, os.PathLike[str]]) -> None:
462
750
  """Removes a file."""
463
751
  _fs.get(path).rm(path)
@@ -468,6 +756,11 @@ def path_exists(path: Union[str, os.PathLike[str]]) -> bool:
468
756
  return _fs.get(path).exists(path)
469
757
 
470
758
 
759
+ def glob(pattern: Union[str, os.PathLike[str]]) -> list[str]:
760
+ """Lists all files or sub-directories."""
761
+ return _fs.get(pattern).glob(pattern)
762
+
763
+
471
764
  def listdir(
472
765
  path: Union[str, os.PathLike[str]], fullpath: bool = False
473
766
  ) -> list[str]: # pylint: disable=redefined-builtin
@@ -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()