Skip to content

Commit 3938023

Browse files
daiyippyglove authors
authored and
pyglove authors
committed
pg.io.FileSystem to support os.PathLike objects as path.
PiperOrigin-RevId: 584387134
1 parent b367568 commit 3938023

File tree

2 files changed

+96
-47
lines changed

2 files changed

+96
-47
lines changed

pyglove/core/io/file_system.py

Lines changed: 91 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -67,42 +67,60 @@ class FileSystem(metaclass=abc.ABCMeta):
6767
"""Interface for a file system."""
6868

6969
@abc.abstractmethod
70-
def open(self, path: str, mode: str = 'r', **kwargs) -> File:
70+
def open(
71+
self, path: Union[str, os.PathLike[str]], mode: str = 'r', **kwargs
72+
) -> File:
7173
"""Opens a file with a path."""
7274

7375
@abc.abstractmethod
74-
def exists(self, path: str) -> bool:
76+
def exists(self, path: Union[str, os.PathLike[str]]) -> bool:
7577
"""Returns True if a path exists."""
7678

7779
@abc.abstractmethod
78-
def listdir(self, path: str) -> list[str]:
80+
def listdir(self, path: Union[str, os.PathLike[str]]) -> list[str]:
7981
"""Lists all files or sub-directories."""
8082

8183
@abc.abstractmethod
82-
def isdir(self, path: str) -> bool:
84+
def isdir(self, path: Union[str, os.PathLike[str]]) -> bool:
8385
"""Returns True if a path is a directory."""
8486

8587
@abc.abstractmethod
86-
def mkdir(self, path: str, mode: int = 0o777) -> None:
88+
def mkdir(
89+
self, path: Union[str, os.PathLike[str]], mode: int = 0o777
90+
) -> None:
8791
"""Makes a directory based on a path."""
8892

8993
@abc.abstractmethod
9094
def mkdirs(
91-
self, path: str, mode: int = 0o777, exist_ok: bool = False) -> None:
95+
self,
96+
path: Union[str, os.PathLike[str]],
97+
mode: int = 0o777,
98+
exist_ok: bool = False,
99+
) -> None:
92100
"""Makes a directory chain based on a path."""
93101

94102
@abc.abstractmethod
95-
def rm(self, path: str) -> None:
103+
def rm(self, path: Union[str, os.PathLike[str]]) -> None:
96104
"""Removes a file based on a path."""
97105

98106
@abc.abstractmethod
99-
def rmdir(self, path: str) -> bool:
107+
def rmdir(self, path: Union[str, os.PathLike[str]]) -> bool:
100108
"""Removes a directory based on a path."""
101109

102110
@abc.abstractmethod
103-
def rmdirs(self, path: str) -> None:
111+
def rmdirs(self, path: Union[str, os.PathLike[str]]) -> None:
104112
"""Removes a directory chain based on a path."""
105113

114+
115+
def _resolve_path(path: Union[str, os.PathLike[str]]) -> str:
116+
if isinstance(path, str):
117+
return path
118+
elif hasattr(path, '__fspath__'):
119+
return path.__fspath__()
120+
else:
121+
raise ValueError(f'Unsupported path: {path!r}.')
122+
123+
106124
#
107125
# The standard file system.
108126
#
@@ -137,32 +155,40 @@ def close(self) -> None:
137155
class StdFileSystem(FileSystem):
138156
"""The standard file system."""
139157

140-
def open(self, path: str, mode: str = 'r', **kwargs) -> File:
158+
def open(
159+
self, path: Union[str, os.PathLike[str]], mode: str = 'r', **kwargs
160+
) -> File:
141161
return StdFile(io.open(path, mode, **kwargs))
142162

143-
def exists(self, path: str) -> bool:
163+
def exists(self, path: Union[str, os.PathLike[str]]) -> bool:
144164
return os.path.exists(path)
145165

146-
def listdir(self, path: str) -> list[str]:
166+
def listdir(self, path: Union[str, os.PathLike[str]]) -> list[str]:
147167
return os.listdir(path)
148168

149-
def isdir(self, path: str) -> bool:
169+
def isdir(self, path: Union[str, os.PathLike[str]]) -> bool:
150170
return os.path.isdir(path)
151171

152-
def mkdir(self, path: str, mode: int = 0o777) -> None:
172+
def mkdir(
173+
self, path: Union[str, os.PathLike[str]], mode: int = 0o777
174+
) -> None:
153175
os.mkdir(path, mode)
154176

155177
def mkdirs(
156-
self, path: str, mode: int = 0o777, exist_ok: bool = False) -> None:
178+
self,
179+
path: Union[str, os.PathLike[str]],
180+
mode: int = 0o777,
181+
exist_ok: bool = False,
182+
) -> None:
157183
os.makedirs(path, mode, exist_ok)
158184

159-
def rm(self, path: str) -> None:
185+
def rm(self, path: Union[str, os.PathLike[str]]) -> None:
160186
os.remove(path)
161187

162-
def rmdir(self, path: str) -> None:
188+
def rmdir(self, path: Union[str, os.PathLike[str]]) -> None:
163189
os.rmdir(path)
164190

165-
def rmdirs(self, path: str) -> None:
191+
def rmdirs(self, path: Union[str, os.PathLike[str]]) -> None:
166192
os.removedirs(path)
167193

168194

@@ -206,10 +232,10 @@ def __init__(self, prefix: str = '/mem/'):
206232
self._root = {}
207233
self._prefix = prefix
208234

209-
def _internal_path(self, path: str) -> str:
210-
return '/' + path.lstrip(self._prefix)
235+
def _internal_path(self, path: Union[str, os.PathLike[str]]) -> str:
236+
return '/' + _resolve_path(path).lstrip(self._prefix)
211237

212-
def _locate(self, path: str) -> Any:
238+
def _locate(self, path: Union[str, os.PathLike[str]]) -> Any:
213239
current = self._root
214240
for x in self._internal_path(path).split('/'):
215241
if not x:
@@ -219,7 +245,9 @@ def _locate(self, path: str) -> Any:
219245
current = current[x]
220246
return current
221247

222-
def open(self, path: str, mode: str = 'r', **kwargs) -> File:
248+
def open(
249+
self, path: Union[str, os.PathLike[str]], mode: str = 'r', **kwargs
250+
) -> File:
223251
file = self._locate(path)
224252
if isinstance(file, dict):
225253
raise IsADirectoryError(path)
@@ -234,19 +262,22 @@ def open(self, path: str, mode: str = 'r', **kwargs) -> File:
234262
raise FileNotFoundError(path)
235263
return file
236264

237-
def exists(self, path: str) -> bool:
265+
def exists(self, path: Union[str, os.PathLike[str]]) -> bool:
238266
return self._locate(path) is not None
239267

240-
def listdir(self, path: str) -> list[str]:
268+
def listdir(self, path: Union[str, os.PathLike[str]]) -> list[str]:
241269
d = self._locate(path)
242270
if not isinstance(d, dict):
243271
raise FileNotFoundError(path)
244272
return list(d.keys())
245273

246-
def isdir(self, path: str) -> bool:
274+
def isdir(self, path: Union[str, os.PathLike[str]]) -> bool:
247275
return isinstance(self._locate(path), dict)
248276

249-
def _parent_and_name(self, path: str) -> tuple[dict[str, Any], str]:
277+
def _parent_and_name(
278+
self, path: Union[str, os.PathLike[str]]
279+
) -> tuple[dict[str, Any], str]:
280+
path = _resolve_path(path)
250281
rpos = path.rfind('/')
251282
assert rpos >= 0, path
252283
name = path[rpos + 1:]
@@ -255,15 +286,21 @@ def _parent_and_name(self, path: str) -> tuple[dict[str, Any], str]:
255286
raise FileNotFoundError(path)
256287
return parent_dir, name
257288

258-
def mkdir(self, path: str, mode: int = 0o777) -> None:
289+
def mkdir(
290+
self, path: Union[str, os.PathLike[str]], mode: int = 0o777
291+
) -> None:
259292
del mode
260293
parent_dir, name = self._parent_and_name(path)
261294
if name in parent_dir:
262295
raise FileExistsError(path)
263296
parent_dir[name] = {}
264297

265298
def mkdirs(
266-
self, path: str, mode: int = 0o777, exist_ok: bool = False) -> None:
299+
self,
300+
path: Union[str, os.PathLike[str]],
301+
mode: int = 0o777,
302+
exist_ok: bool = False,
303+
) -> None:
267304
del mode
268305
current = self._root
269306
dirs = self._internal_path(path).split('/')
@@ -280,7 +317,7 @@ def mkdirs(
280317
raise NotADirectoryError(path)
281318
current = entry
282319

283-
def rm(self, path: str) -> None:
320+
def rm(self, path: Union[str, os.PathLike[str]]) -> None:
284321
parent_dir, name = self._parent_and_name(path)
285322
entry = parent_dir.get(name)
286323
if entry is None:
@@ -289,7 +326,7 @@ def rm(self, path: str) -> None:
289326
raise IsADirectoryError(path)
290327
del parent_dir[name]
291328

292-
def rmdir(self, path: str) -> None:
329+
def rmdir(self, path: Union[str, os.PathLike[str]]) -> None:
293330
parent_dir, name = self._parent_and_name(path)
294331
entry = parent_dir.get(name)
295332
if entry is None:
@@ -300,7 +337,7 @@ def rmdir(self, path: str) -> None:
300337
raise OSError(f'Directory not empty: {path!r}')
301338
del parent_dir[name]
302339

303-
def rmdirs(self, path: str) -> None:
340+
def rmdirs(self, path: Union[str, os.PathLike[str]]) -> None:
304341
def _rmdir(dir_dict, subpath: str) -> bool:
305342
if not subpath:
306343
if dir_dict:
@@ -333,9 +370,9 @@ def add(self, prefix, fs: FileSystem) -> None:
333370
self._filesystems.append((prefix, fs))
334371
self._filesystems.sort(key=lambda x: x[0], reverse=True)
335372

336-
def get(self, path: str) -> FileSystem:
373+
def get(self, path: Union[str, os.PathLike[str]]) -> FileSystem:
337374
"""Gets the file system for a path."""
338-
path = path.lower()
375+
path = _resolve_path(path)
339376
for prefix, fs in self._filesystems:
340377
if path.startswith(prefix):
341378
return fs
@@ -359,16 +396,17 @@ def add_file_system(prefix: str, fs: FileSystem) -> None:
359396
#
360397

361398

362-
def open(path: str, mode: str = 'r', **kwargs) -> File: # pylint:disable=redefined-builtin
399+
def open(path: Union[str, os.PathLike[str]], mode: str = 'r', **kwargs) -> File: # pylint:disable=redefined-builtin
363400
"""Opens a file with a path."""
364401
return _fs.get(path).open(path, mode, **kwargs)
365402

366403

367404
def readfile(
368-
path: str,
405+
path: Union[str, os.PathLike[str]],
369406
mode: str = 'r',
370407
nonexist_ok: bool = False,
371-
**kwargs) -> Union[bytes, str, None]:
408+
**kwargs,
409+
) -> Union[bytes, str, None]:
372410
"""Reads content from a file."""
373411
try:
374412
with _fs.get(path).open(path, mode=mode, **kwargs) as f:
@@ -380,54 +418,61 @@ def readfile(
380418

381419

382420
def writefile(
383-
path: str,
421+
path: Union[str, os.PathLike[str]],
384422
content: Union[str, bytes],
385423
*,
386424
mode: str = 'w',
387-
**kwargs) -> None:
425+
**kwargs,
426+
) -> None:
388427
"""Writes content to a file."""
389428
with _fs.get(path).open(path, mode=mode, **kwargs) as f:
390429
f.write(content)
391430

392431

393-
def rm(path: str) -> None:
432+
def rm(path: Union[str, os.PathLike[str]]) -> None:
394433
"""Removes a file."""
395434
_fs.get(path).rm(path)
396435

397436

398-
def path_exists(path: str) -> bool:
437+
def path_exists(path: Union[str, os.PathLike[str]]) -> bool:
399438
"""Returns True if path exists."""
400439
return _fs.get(path).exists(path)
401440

402441

403-
def listdir(path: str, fullpath: bool = False) -> list[str]: # pylint: disable=redefined-builtin
442+
def listdir(
443+
path: Union[str, os.PathLike[str]], fullpath: bool = False
444+
) -> list[str]: # pylint: disable=redefined-builtin
404445
"""Lists all files or sub-directories under a dir."""
405446
entries = _fs.get(path).listdir(path)
406447
if fullpath:
407448
return [os.path.join(path, entry) for entry in entries]
408449
return entries
409450

410451

411-
def isdir(path: str) -> bool:
452+
def isdir(path: Union[str, os.PathLike[str]]) -> bool:
412453
"""Returns True if path is a directory."""
413454
return _fs.get(path).isdir(path)
414455

415456

416-
def mkdir(path: str, mode: int = 0o777) -> None:
457+
def mkdir(path: Union[str, os.PathLike[str]], mode: int = 0o777) -> None:
417458
"""Makes a directory."""
418459
_fs.get(path).mkdir(path, mode=mode)
419460

420461

421-
def mkdirs(path: str, mode: int = 0o777, exist_ok: bool = False) -> None:
462+
def mkdirs(
463+
path: Union[str, os.PathLike[str]],
464+
mode: int = 0o777,
465+
exist_ok: bool = False,
466+
) -> None:
422467
"""Makes a directory chain."""
423468
_fs.get(path).mkdirs(path, mode=mode, exist_ok=exist_ok)
424469

425470

426-
def rmdir(path: str) -> bool:
471+
def rmdir(path: Union[str, os.PathLike[str]]) -> bool:
427472
"""Removes a directory."""
428473
return _fs.get(path).rmdir(path)
429474

430475

431-
def rmdirs(path: str) -> bool:
476+
def rmdirs(path: Union[str, os.PathLike[str]]) -> bool:
432477
"""Removes a directory chain until a parent directory is not empty."""
433478
return _fs.get(path).rmdirs(path)

pyglove/core/io/file_system_test.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
import os
16+
import pathlib
1617
import tempfile
1718
import unittest
1819
from pyglove.core.io import file_system
@@ -91,6 +92,9 @@ def test_file(self):
9192
with self.assertRaises(FileNotFoundError):
9293
fs.open(file1)
9394

95+
with self.assertRaisesRegex(ValueError, 'Unsupported path'):
96+
fs.open(1)
97+
9498
with fs.open(file1, 'w') as f:
9599
f.write('hello\npyglove')
96100

@@ -213,7 +217,7 @@ def test_standard_filesystem(self):
213217
self.assertFalse(file_system.path_exists(file2))
214218

215219
def test_memory_filesystem(self):
216-
file1 = '/mem/file1'
220+
file1 = pathlib.Path('/mem/file1')
217221
with self.assertRaises(FileNotFoundError):
218222
file_system.readfile(file1)
219223
self.assertIsNone(file_system.readfile(file1, nonexist_ok=True))

0 commit comments

Comments
 (0)