Skip to content

Commit a99ccd2

Browse files
committed
* Don't allow MountFS to have an empty mount-path
* Don't allow files/dirs to be created inside MountFS's internal MemoryFS * Change MountFS's .mounts attribute to a dict instead of a list of tuples * Add a MountFS.unmount method * Add associated unit-tests
1 parent 2d0ffc3 commit a99ccd2

File tree

2 files changed

+123
-23
lines changed

2 files changed

+123
-23
lines changed

fs/mountfs.py

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
from .base import FS
1414
from .memoryfs import MemoryFS
1515
from .path import abspath
16+
from .path import dirname
1617
from .path import forcedir
1718
from .path import normpath
19+
from .path import recursepath
1820
from .mode import validate_open_mode
1921
from .mode import validate_openbin_mode
2022

@@ -23,10 +25,10 @@
2325
Any,
2426
BinaryIO,
2527
Collection,
28+
Dict,
2629
Iterator,
2730
IO,
2831
List,
29-
MutableSequence,
3032
Optional,
3133
Text,
3234
Tuple,
@@ -67,7 +69,7 @@ def __init__(self, auto_close=True):
6769
super(MountFS, self).__init__()
6870
self.auto_close = auto_close
6971
self.default_fs = MemoryFS() # type: FS
70-
self.mounts = [] # type: MutableSequence[Tuple[Text, FS]]
72+
self.mounts = {} # type: Dict[Text, FS]
7173

7274
def __repr__(self):
7375
# type: () -> str
@@ -85,19 +87,26 @@ def _delegate(self, path):
8587
path (str): A path.
8688
8789
Returns:
88-
(FS, str): a tuple of ``(<fs>, <path>)`` for a mounted filesystem,
89-
or ``(None, None)`` if no filesystem is mounted on the
90+
(FS, str): a tuple of ``(<fs>, <path>)`` for a mounted filesystem.
91+
Raises ``ResourceNotFound`` if no filesystem is mounted on the
9092
given ``path``.
9193
9294
"""
9395
_path = forcedir(abspath(normpath(path)))
9496
is_mounted = _path.startswith
9597

96-
for mount_path, fs in self.mounts:
98+
if _path == "/":
99+
return self.default_fs, path
100+
101+
for mount_path in self.mounts:
97102
if is_mounted(mount_path):
98-
return fs, _path[len(mount_path) :].rstrip("/")
103+
return self.mounts[mount_path], _path[len(mount_path) :].rstrip("/")
104+
105+
for mount_path in self.mounts:
106+
if mount_path.startswith(_path):
107+
return self.default_fs, path
99108

100-
return self.default_fs, path
109+
raise errors.ResourceNotFound(path)
101110

102111
def mount(self, path, fs):
103112
# type: (Text, Union[FS, Text]) -> None
@@ -120,28 +129,53 @@ def mount(self, path, fs):
120129
raise ValueError("Unable to mount self")
121130
_path = forcedir(abspath(normpath(path)))
122131

123-
for mount_path, _ in self.mounts:
132+
if _path == "/":
133+
raise MountError("mount point can't be empty")
134+
135+
for mount_path in self.mounts:
124136
if _path.startswith(mount_path):
125137
raise MountError("mount point overlaps existing mount")
126138

127-
self.mounts.append((_path, fs))
139+
self.mounts[_path] = fs
128140
self.default_fs.makedirs(_path, recreate=True)
129141

142+
def unmount(self, path):
143+
# type: (Text) -> None
144+
"""Unmounts a previously-mounted FS.
145+
146+
Arguments:
147+
path (str): A mount-path previously used with ``mount``.
148+
149+
"""
150+
_path = forcedir(abspath(normpath(path)))
151+
if _path not in self.mounts:
152+
raise ValueError("not a current mount point")
153+
154+
if self.auto_close:
155+
self.mounts[_path].close()
156+
del self.mounts[_path]
157+
self.default_fs.removedir(_path)
158+
for parent in recursepath(dirname(_path.rstrip("/")), reverse=True):
159+
if parent == "/" or not self.default_fs.isempty(parent):
160+
break
161+
else:
162+
self.default_fs.removedir(parent)
163+
130164
def close(self):
131165
# type: () -> None
132166
# Explicitly closes children if requested
133167
if self.auto_close:
134-
for _path, fs in self.mounts:
168+
for fs in self.mounts.values():
135169
fs.close()
136-
del self.mounts[:]
170+
self.mounts.clear()
137171
self.default_fs.close()
138172
super(MountFS, self).close()
139173

140174
def desc(self, path):
141175
# type: (Text) -> Text
176+
fs, delegate_path = self._delegate(path)
142177
if not self.exists(path):
143178
raise errors.ResourceNotFound(path)
144-
fs, delegate_path = self._delegate(path)
145179
if fs is self.default_fs:
146180
fs = self
147181
return "{path} on {fs}".format(fs=fs, path=delegate_path)

tests/test_mountfs.py

Lines changed: 77 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import unittest
44

5+
from fs.errors import ResourceNotFound
56
from fs.mountfs import MountError, MountFS
67
from fs.memoryfs import MemoryFS
78
from fs.tempfs import TempFS
@@ -11,16 +12,6 @@
1112
class TestMountFS(FSTestCases, unittest.TestCase):
1213
"""Test OSFS implementation."""
1314

14-
def make_fs(self):
15-
fs = MountFS()
16-
mem_fs = MemoryFS()
17-
fs.mount("/", mem_fs)
18-
return fs
19-
20-
21-
class TestMountFS2(FSTestCases, unittest.TestCase):
22-
"""Test OSFS implementation."""
23-
2415
def make_fs(self):
2516
fs = MountFS()
2617
mem_fs = MemoryFS()
@@ -35,21 +26,44 @@ def test_bad_mount(self):
3526
mount_fs.mount("foo", 5)
3627
with self.assertRaises(TypeError):
3728
mount_fs.mount("foo", b"bar")
29+
m1 = MemoryFS()
30+
with self.assertRaises(MountError):
31+
mount_fs.mount("", m1)
32+
with self.assertRaises(MountError):
33+
mount_fs.mount("/", m1)
3834

3935
def test_listdir(self):
4036
mount_fs = MountFS()
4137
self.assertEqual(mount_fs.listdir("/"), [])
4238
m1 = MemoryFS()
4339
m3 = MemoryFS()
4440
m4 = TempFS()
41+
m5 = MemoryFS()
4542
mount_fs.mount("/m1", m1)
4643
mount_fs.mount("/m2", "temp://")
4744
mount_fs.mount("/m3", m3)
4845
with self.assertRaises(MountError):
4946
mount_fs.mount("/m3/foo", m4)
5047
self.assertEqual(sorted(mount_fs.listdir("/")), ["m1", "m2", "m3"])
48+
mount_fs.makedir("/m2/foo")
49+
self.assertEqual(sorted(mount_fs.listdir("/m2")), ["foo"])
5150
m3.makedir("foo")
5251
self.assertEqual(sorted(mount_fs.listdir("/m3")), ["foo"])
52+
mount_fs.mount("/subdir/m4", m4)
53+
self.assertEqual(sorted(mount_fs.listdir("/")), ["m1", "m2", "m3", "subdir"])
54+
self.assertEqual(mount_fs.listdir("/subdir"), ["m4"])
55+
self.assertEqual(mount_fs.listdir("/subdir/m4"), [])
56+
mount_fs.mount("/subdir/m5", m5)
57+
self.assertEqual(sorted(mount_fs.listdir("/subdir")), ["m4", "m5"])
58+
self.assertEqual(mount_fs.listdir("/subdir/m5"), [])
59+
mount_fs.makedir("/subdir/m4/foo")
60+
mount_fs.makedir("/subdir/m5/bar")
61+
self.assertEqual(mount_fs.listdir("/subdir/m4"), ["foo"])
62+
self.assertEqual(mount_fs.listdir("/subdir/m5"), ["bar"])
63+
self.assertEqual(m4.listdir("/"), ["foo"])
64+
self.assertEqual(m5.listdir("/"), ["bar"])
65+
m5.removedir("/bar")
66+
self.assertEqual(mount_fs.listdir("/subdir/m5"), [])
5367

5468
def test_auto_close(self):
5569
"""Test MountFS auto close is working"""
@@ -85,8 +99,60 @@ def test_empty(self):
8599
def test_mount_self(self):
86100
mount_fs = MountFS()
87101
with self.assertRaises(ValueError):
88-
mount_fs.mount("/", mount_fs)
102+
mount_fs.mount("/m1", mount_fs)
89103

90104
def test_desc(self):
91105
mount_fs = MountFS()
92106
mount_fs.desc("/")
107+
108+
def test_makedirs(self):
109+
mount_fs = MountFS()
110+
with self.assertRaises(ResourceNotFound):
111+
mount_fs.makedir("empty")
112+
m1 = MemoryFS()
113+
m2 = MemoryFS()
114+
with self.assertRaises(ResourceNotFound):
115+
mount_fs.makedirs("/m1/foo/bar", recreate=True)
116+
mount_fs.mount("/m1", m1)
117+
mount_fs.makedirs("/m1/foo/bar", recreate=True)
118+
self.assertEqual(m1.listdir("foo"), ["bar"])
119+
with self.assertRaises(ResourceNotFound):
120+
mount_fs.makedirs("/subdir/m2/bar/foo", recreate=True)
121+
mount_fs.mount("/subdir/m2", m2)
122+
mount_fs.makedirs("/subdir/m2/bar/foo", recreate=True)
123+
self.assertEqual(m2.listdir("bar"), ["foo"])
124+
with self.assertRaises(ResourceNotFound):
125+
mount_fs.makedir("/subdir/m3", recreate=True)
126+
127+
def test_unmount(self):
128+
mount_fs = MountFS()
129+
m1 = MemoryFS()
130+
m2 = MemoryFS()
131+
m3 = MemoryFS()
132+
m4 = MemoryFS()
133+
mount_fs.mount("/m1", m1)
134+
with self.assertRaises(ValueError):
135+
mount_fs.unmount("/m2")
136+
mount_fs.mount("/m2", m2)
137+
self.assertEqual(sorted(mount_fs.listdir("/")), ["m1", "m2"])
138+
mount_fs.unmount("/m1")
139+
with self.assertRaises(ResourceNotFound):
140+
mount_fs.listdir("/m1")
141+
self.assertEqual(mount_fs.listdir("/"), ["m2"])
142+
with self.assertRaises(ValueError):
143+
mount_fs.unmount("/m1")
144+
mount_fs.mount("/subdir/m3", m3)
145+
with self.assertRaises(ValueError):
146+
mount_fs.unmount("/subdir")
147+
mount_fs.mount("/subdir/m4", m4)
148+
self.assertEqual(sorted(mount_fs.listdir("/")), ["m2", "subdir"])
149+
mount_fs.makedir("/subdir/m4/foo")
150+
with self.assertRaises(ValueError):
151+
mount_fs.unmount("/subdir/m4/foo")
152+
mount_fs.unmount("/subdir/m4")
153+
self.assertEqual(sorted(mount_fs.listdir("/")), ["m2", "subdir"])
154+
self.assertEqual(mount_fs.listdir("/subdir"), ["m3"])
155+
mount_fs.unmount("/subdir/m3")
156+
self.assertEqual(mount_fs.listdir("/"), ["m2"])
157+
with self.assertRaises(ResourceNotFound):
158+
mount_fs.listdir("/subdir")

0 commit comments

Comments
 (0)