Skip to content

Commit 13ec621

Browse files
authored
Merge pull request #472 from PyFilesystem/memfs-opt
Add optimized implementations of several `MemoryFS` methods
2 parents 99d5314 + f26b7d1 commit 13ec621

File tree

4 files changed

+136
-17
lines changed

4 files changed

+136
-17
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
2020
Closes [#445](https://github.com/PyFilesystem/pyfilesystem2/pull/445).
2121
- Migrate continuous integration from Travis-CI to GitHub Actions and introduce several linters
2222
again in the build steps ([#448](https://github.com/PyFilesystem/pyfilesystem2/pull/448)).
23-
Closes [#446](https://github.com/PyFilesystem/pyfilesystem2/pull/446).
23+
Closes [#446](https://github.com/PyFilesystem/pyfilesystem2/issues/446).
2424
- Stop requiring `pytest` to run tests, allowing any test runner supporting `unittest`-style
2525
test suites.
2626
- `FSTestCases` now builds the large data required for `upload` and `download` tests only
2727
once in order to reduce the total testing time.
28+
- `MemoryFS.move` and `MemoryFS.movedir` will now avoid copying data.
29+
Closes [#452](https://github.com/PyFilesystem/pyfilesystem2/issues/452).
2830

2931
### Fixed
3032

fs/memoryfs.py

Lines changed: 107 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
SupportsInt,
3939
Union,
4040
Text,
41+
Tuple,
4142
)
4243
from .base import _OpendirFactory
4344
from .info import RawInfo
@@ -274,6 +275,10 @@ def remove_entry(self, name):
274275
# type: (Text) -> None
275276
del self._dir[name]
276277

278+
def clear(self):
279+
# type: () -> None
280+
self._dir.clear()
281+
277282
def __contains__(self, name):
278283
# type: (object) -> bool
279284
return name in self._dir
@@ -294,6 +299,21 @@ def remove_open_file(self, memory_file):
294299
# type: (_MemoryFile) -> None
295300
self._open_files.remove(memory_file)
296301

302+
def to_info(self, namespaces=None):
303+
# type: (Optional[Collection[Text]]) -> Info
304+
namespaces = namespaces or ()
305+
info = {"basic": {"name": self.name, "is_dir": self.is_dir}}
306+
if "details" in namespaces:
307+
info["details"] = {
308+
"_write": ["accessed", "modified"],
309+
"type": int(self.resource_type),
310+
"size": self.size,
311+
"accessed": self.accessed_time,
312+
"modified": self.modified_time,
313+
"created": self.created_time,
314+
}
315+
return Info(info)
316+
297317

298318
@six.python_2_unicode_compatible
299319
class MemoryFS(FS):
@@ -368,33 +388,24 @@ def close(self):
368388

369389
def getinfo(self, path, namespaces=None):
370390
# type: (Text, Optional[Collection[Text]]) -> Info
371-
namespaces = namespaces or ()
372391
_path = self.validatepath(path)
373392
dir_entry = self._get_dir_entry(_path)
374393
if dir_entry is None:
375394
raise errors.ResourceNotFound(path)
376-
info = {"basic": {"name": dir_entry.name, "is_dir": dir_entry.is_dir}}
377-
if "details" in namespaces:
378-
info["details"] = {
379-
"_write": ["accessed", "modified"],
380-
"type": int(dir_entry.resource_type),
381-
"size": dir_entry.size,
382-
"accessed": dir_entry.accessed_time,
383-
"modified": dir_entry.modified_time,
384-
"created": dir_entry.created_time,
385-
}
386-
return Info(info)
395+
return dir_entry.to_info(namespaces=namespaces)
387396

388397
def listdir(self, path):
389398
# type: (Text) -> List[Text]
390399
self.check()
391400
_path = self.validatepath(path)
392401
with self._lock:
402+
# locate and validate the entry corresponding to the given path
393403
dir_entry = self._get_dir_entry(_path)
394404
if dir_entry is None:
395405
raise errors.ResourceNotFound(path)
396406
if not dir_entry.is_dir:
397407
raise errors.DirectoryExpected(path)
408+
# return the filenames in the order they were created
398409
return dir_entry.list()
399410

400411
if typing.TYPE_CHECKING:
@@ -433,6 +444,46 @@ def makedir(
433444
parent_dir.set_entry(dir_name, new_dir)
434445
return self.opendir(path)
435446

447+
def move(self, src_path, dst_path, overwrite=False):
448+
src_dir, src_name = split(self.validatepath(src_path))
449+
dst_dir, dst_name = split(self.validatepath(dst_path))
450+
451+
with self._lock:
452+
src_dir_entry = self._get_dir_entry(src_dir)
453+
if src_dir_entry is None or src_name not in src_dir_entry:
454+
raise errors.ResourceNotFound(src_path)
455+
src_entry = src_dir_entry.get_entry(src_name)
456+
if src_entry.is_dir:
457+
raise errors.FileExpected(src_path)
458+
459+
dst_dir_entry = self._get_dir_entry(dst_dir)
460+
if dst_dir_entry is None:
461+
raise errors.ResourceNotFound(dst_path)
462+
elif not overwrite and dst_name in dst_dir_entry:
463+
raise errors.DestinationExists(dst_path)
464+
465+
dst_dir_entry.set_entry(dst_name, src_entry)
466+
src_dir_entry.remove_entry(src_name)
467+
468+
def movedir(self, src_path, dst_path, create=False):
469+
src_dir, src_name = split(self.validatepath(src_path))
470+
dst_dir, dst_name = split(self.validatepath(dst_path))
471+
472+
with self._lock:
473+
src_dir_entry = self._get_dir_entry(src_dir)
474+
if src_dir_entry is None or src_name not in src_dir_entry:
475+
raise errors.ResourceNotFound(src_path)
476+
src_entry = src_dir_entry.get_entry(src_name)
477+
if not src_entry.is_dir:
478+
raise errors.DirectoryExpected(src_path)
479+
480+
dst_dir_entry = self._get_dir_entry(dst_dir)
481+
if dst_dir_entry is None or (not create and dst_name not in dst_dir_entry):
482+
raise errors.ResourceNotFound(dst_path)
483+
484+
dst_dir_entry.set_entry(dst_name, src_entry)
485+
src_dir_entry.remove_entry(src_name)
486+
436487
def openbin(self, path, mode="r", buffering=-1, **options):
437488
# type: (Text, Text, int, **Any) -> BinaryIO
438489
_mode = Mode(mode)
@@ -499,12 +550,29 @@ def remove(self, path):
499550

500551
def removedir(self, path):
501552
# type: (Text) -> None
553+
# make sure we are not removing root
502554
_path = self.validatepath(path)
503-
504555
if _path == "/":
505556
raise errors.RemoveRootError()
557+
# make sure the directory is empty
558+
if not self.isempty(path):
559+
raise errors.DirectoryNotEmpty(path)
560+
# we can now delegate to removetree since we confirmed that
561+
# * path exists (isempty)
562+
# * path is a folder (isempty)
563+
# * path is not root
564+
self.removetree(_path)
565+
566+
def removetree(self, path):
567+
# type: (Text) -> None
568+
_path = self.validatepath(path)
506569

507570
with self._lock:
571+
572+
if _path == "/":
573+
self.root.clear()
574+
return
575+
508576
dir_path, file_name = split(_path)
509577
parent_dir_entry = self._get_dir_entry(dir_path)
510578

@@ -515,11 +583,34 @@ def removedir(self, path):
515583
if not dir_dir_entry.is_dir:
516584
raise errors.DirectoryExpected(path)
517585

518-
if len(dir_dir_entry):
519-
raise errors.DirectoryNotEmpty(path)
520-
521586
parent_dir_entry.remove_entry(file_name)
522587

588+
def scandir(
589+
self,
590+
path, # type: Text
591+
namespaces=None, # type: Optional[Collection[Text]]
592+
page=None, # type: Optional[Tuple[int, int]]
593+
):
594+
# type: (...) -> Iterator[Info]
595+
self.check()
596+
_path = self.validatepath(path)
597+
with self._lock:
598+
# locate and validate the entry corresponding to the given path
599+
dir_entry = self._get_dir_entry(_path)
600+
if dir_entry is None:
601+
raise errors.ResourceNotFound(path)
602+
if not dir_entry.is_dir:
603+
raise errors.DirectoryExpected(path)
604+
# if paging was requested, slice the filenames
605+
filenames = dir_entry.list()
606+
if page is not None:
607+
start, end = page
608+
filenames = filenames[start:end]
609+
# yield info with the right namespaces
610+
for name in filenames:
611+
entry = typing.cast(_DirEntry, dir_entry.get_entry(name))
612+
yield entry.to_info(namespaces=namespaces)
613+
523614
def setinfo(self, path, info):
524615
# type: (Text, RawInfo) -> None
525616
_path = self.validatepath(path)

fs/test.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1115,6 +1115,15 @@ def test_removetree(self):
11151115
self.fs.removetree("foo")
11161116
self.assert_not_exists("foo")
11171117

1118+
# Errors on files
1119+
self.fs.create("bar")
1120+
with self.assertRaises(errors.DirectoryExpected):
1121+
self.fs.removetree("bar")
1122+
1123+
# Errors on non-existing path
1124+
with self.assertRaises(errors.ResourceNotFound):
1125+
self.fs.removetree("foofoo")
1126+
11181127
def test_setinfo(self):
11191128
self.fs.create("birthday.txt")
11201129
now = math.floor(time.time())

tests/test_memoryfs.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,20 @@ def test_close_mem_free(self):
6666
"Memory usage increased after closing the file system; diff is %0.2f KiB."
6767
% (diff_close.size_diff / 1024.0),
6868
)
69+
70+
71+
class TestMemoryFile(unittest.TestCase):
72+
73+
def setUp(self):
74+
self.fs = memoryfs.MemoryFS()
75+
76+
def tearDown(self):
77+
self.fs.close()
78+
79+
def test_readline_writing(self):
80+
with self.fs.openbin("test.txt", "w") as f:
81+
self.assertRaises(IOError, f.readline)
82+
83+
def test_readinto_writing(self):
84+
with self.fs.openbin("test.txt", "w") as f:
85+
self.assertRaises(IOError, f.readinto, bytearray(10))

0 commit comments

Comments
 (0)