monkeyfs 0.2.3__tar.gz → 0.2.4__tar.gz

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.
Files changed (47) hide show
  1. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/PKG-INFO +1 -1
  2. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/monkeyfs/base.py +8 -1
  3. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/monkeyfs/isolated.py +17 -0
  4. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/monkeyfs/mount.py +38 -0
  5. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/monkeyfs/virtual.py +25 -1
  6. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/monkeyfs.egg-info/PKG-INFO +1 -1
  7. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/pyproject.toml +1 -1
  8. monkeyfs-0.2.4/tests/test_bulk_operations.py +239 -0
  9. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/tests/test_readonly.py +1 -0
  10. monkeyfs-0.2.3/tests/test_bulk_operations.py +0 -93
  11. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/LICENSE +0 -0
  12. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/README.md +0 -0
  13. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/monkeyfs/__init__.py +0 -0
  14. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/monkeyfs/conformance.py +0 -0
  15. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/monkeyfs/context.py +0 -0
  16. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/monkeyfs/patching/__init__.py +0 -0
  17. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/monkeyfs/patching/core.py +0 -0
  18. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/monkeyfs/patching/fdtable.py +0 -0
  19. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/monkeyfs/patching/install.py +0 -0
  20. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/monkeyfs/patching/patches.py +0 -0
  21. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/monkeyfs/py.typed +0 -0
  22. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/monkeyfs/readonly.py +0 -0
  23. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/monkeyfs/virtualfile.py +0 -0
  24. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/monkeyfs.egg-info/SOURCES.txt +0 -0
  25. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/monkeyfs.egg-info/dependency_links.txt +0 -0
  26. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/monkeyfs.egg-info/requires.txt +0 -0
  27. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/monkeyfs.egg-info/top_level.txt +0 -0
  28. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/setup.cfg +0 -0
  29. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/tests/test_conformance.py +0 -0
  30. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/tests/test_directory_ops.py +0 -0
  31. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/tests/test_expanduser.py +0 -0
  32. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/tests/test_fd_emulation.py +0 -0
  33. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/tests/test_finalization_and_invalidate.py +0 -0
  34. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/tests/test_isolated.py +0 -0
  35. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/tests/test_lazy_binary_read.py +0 -0
  36. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/tests/test_metadata.py +0 -0
  37. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/tests/test_mount.py +0 -0
  38. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/tests/test_open_optional.py +0 -0
  39. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/tests/test_patching.py +0 -0
  40. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/tests/test_patching_pathlib.py +0 -0
  41. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/tests/test_patching_safe_paths.py +0 -0
  42. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/tests/test_protocol.py +0 -0
  43. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/tests/test_ranged_read.py +0 -0
  44. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/tests/test_symlink_entries.py +0 -0
  45. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/tests/test_vfs_optional.py +0 -0
  46. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/tests/test_vfs_size_limit.py +0 -0
  47. {monkeyfs-0.2.3 → monkeyfs-0.2.4}/tests/test_virtual.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: monkeyfs
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Transparent filesystem interception via monkey-patching.
5
5
  Author: ashenfad
6
6
  License-Expression: MIT
@@ -162,7 +162,14 @@ OPTIONAL_WRITE_METHODS = frozenset(
162
162
  #: termish's shell over a filesystem. Not dispatched by any stdlib shim, but
163
163
  #: composable — a wrapper that routes by path can forward them.
164
164
  DIRECT_READ_METHODS = frozenset(
165
- {"get_metadata_snapshot", "glob", "invalidate", "lexists", "list_detailed"}
165
+ {
166
+ "get_metadata_snapshot",
167
+ "glob",
168
+ "invalidate",
169
+ "lexists",
170
+ "list_detailed",
171
+ "read_many",
172
+ }
166
173
  )
167
174
  DIRECT_WRITE_METHODS = frozenset({"remove_many", "write_many"})
168
175
 
@@ -7,6 +7,7 @@ from __future__ import annotations
7
7
 
8
8
  import io
9
9
  import os
10
+ from collections.abc import Iterable
10
11
  from datetime import datetime, timezone
11
12
  from pathlib import Path
12
13
  from typing import Any
@@ -557,6 +558,22 @@ class IsolatedFS:
557
558
  """Create directory tree (alias for mkdir with parents=True)."""
558
559
  self.mkdir(path, parents=True, exist_ok=exist_ok)
559
560
 
561
+ def read_many(self, paths: Iterable[str]) -> dict[str, bytes]:
562
+ """Read several whole files: path (as given) to bytes, for the
563
+ paths that are files. A path that is not one is left out."""
564
+ found: dict[str, bytes] = {}
565
+ for path in paths:
566
+ # Regular files only (through a link, as ``isfile`` follows
567
+ # one): a socket cannot be read and a FIFO would block
568
+ # waiting for a writer.
569
+ if not self.isfile(path):
570
+ continue
571
+ try:
572
+ found[path] = self.read(path)
573
+ except (FileNotFoundError, IsADirectoryError, NotADirectoryError):
574
+ continue # gone between the check and the read
575
+ return found
576
+
560
577
  def write_many(self, files: dict[str, bytes]) -> None:
561
578
  """Write multiple files at once."""
562
579
  for path, content in files.items():
@@ -9,6 +9,7 @@ from __future__ import annotations
9
9
 
10
10
  import errno
11
11
  import os
12
+ from collections.abc import Iterable
12
13
  from datetime import datetime, timezone
13
14
  from typing import Any
14
15
 
@@ -16,6 +17,22 @@ from .base import FileInfo, FileMetadata
16
17
  from .virtualfile import open_file
17
18
 
18
19
 
20
+ def _read_many(fs: Any, paths: list[str]) -> dict[str, bytes]:
21
+ """``fs.read_many(paths)``, or one ``read`` per path on a filesystem
22
+ that predates it -- a mount can be any backend, and ``read_many`` is
23
+ not one of the methods a backend has to have."""
24
+ batch = getattr(fs, "read_many", None)
25
+ if batch is not None:
26
+ return batch(paths)
27
+ found: dict[str, bytes] = {}
28
+ for path in paths:
29
+ try:
30
+ found[path] = fs.read(path)
31
+ except (FileNotFoundError, IsADirectoryError, NotADirectoryError):
32
+ continue
33
+ return found
34
+
35
+
19
36
  class MountFS:
20
37
  """Routes filesystem operations to backing filesystems by path prefix.
21
38
 
@@ -389,6 +406,27 @@ class MountFS:
389
406
  if drop is not None:
390
407
  drop()
391
408
 
409
+ def read_many(self, paths: Iterable[str]) -> dict[str, bytes]:
410
+ # Group by filesystem, so each one answers its share in one call
411
+ groups: dict[int, tuple[Any, dict[str, list[str]]]] = {}
412
+ for path in paths:
413
+ if self._is_mount_point(self._to_absolute(path)):
414
+ # A mount point, or a directory the composition makes on
415
+ # the way to one: never a file here, whatever the base
416
+ # holds under the same name, just as ``isfile`` says.
417
+ continue
418
+ fs, inner = self._resolve(path)
419
+ key = id(fs)
420
+ if key not in groups:
421
+ groups[key] = (fs, {})
422
+ groups[key][1].setdefault(inner, []).append(path)
423
+ found: dict[str, bytes] = {}
424
+ for fs, inner_paths in groups.values():
425
+ for inner, content in _read_many(fs, list(inner_paths)).items():
426
+ for path in inner_paths[inner]:
427
+ found[path] = content
428
+ return found
429
+
392
430
  # -- Write operations --
393
431
 
394
432
  def write(self, path: str, content: bytes, mode: str = "w") -> None:
@@ -8,7 +8,7 @@ import fnmatch
8
8
  import io
9
9
  import json
10
10
  import os
11
- from collections.abc import MutableMapping
11
+ from collections.abc import Iterable, MutableMapping
12
12
  from datetime import datetime, timezone
13
13
  from typing import Any
14
14
 
@@ -678,6 +678,30 @@ class VirtualFS:
678
678
  return content[offset:]
679
679
  return content[offset : offset + size]
680
680
 
681
+ def read_many(self, paths: Iterable[str]) -> dict[str, bytes]:
682
+ """Read several whole files in one call to the backing state.
683
+
684
+ What ``read(path)`` returns for each path, gathered with a single
685
+ ``get_many`` when the state offers one -- so a caller reading a
686
+ whole tree pays one round trip to a remote store rather than one
687
+ per file.
688
+
689
+ Args:
690
+ paths: File paths to read, resolved against the current
691
+ working directory as ``read`` resolves them.
692
+
693
+ Returns:
694
+ Path (as given) to its bytes, for the paths that are files. A
695
+ path that is not one -- missing, or a directory -- is left out
696
+ rather than raised on: the caller asked about many at once and
697
+ learns which exist from the answer.
698
+ """
699
+ wanted: dict[str, list[str]] = {}
700
+ for path in paths:
701
+ wanted.setdefault(self._encode_path(path), []).append(path)
702
+ found = self._read_many(list(wanted))
703
+ return {path: content for key, content in found.items() for path in wanted[key]}
704
+
681
705
  def write(self, path: str, content: bytes, mode: str = "w") -> None:
682
706
  """Write bytes to a file.
683
707
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: monkeyfs
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Transparent filesystem interception via monkey-patching.
5
5
  Author: ashenfad
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "monkeyfs"
3
- version = "0.2.3"
3
+ version = "0.2.4"
4
4
  description = "Transparent filesystem interception via monkey-patching."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -0,0 +1,239 @@
1
+ """Tests for VirtualFS bulk operations (write_many, remove_many)."""
2
+
3
+ import pytest
4
+
5
+ from monkeyfs import VirtualFS
6
+
7
+
8
+ class TestBulkOperations:
9
+ """Test VirtualFS bulk operations."""
10
+
11
+ def test_write_many_basic(self):
12
+ """Test writing multiple files at once."""
13
+ vfs = VirtualFS({})
14
+
15
+ files = {
16
+ "file1.txt": b"content 1",
17
+ "file2.txt": b"content 2",
18
+ "dir/file3.txt": b"content 3",
19
+ }
20
+
21
+ vfs.write_many(files)
22
+
23
+ # All files should exist
24
+ assert vfs.read("file1.txt") == b"content 1"
25
+ assert vfs.read("file2.txt") == b"content 2"
26
+ assert vfs.read("dir/file3.txt") == b"content 3"
27
+
28
+ def test_write_many_validates_all_bytes(self):
29
+ """Test that write_many validates all content is bytes."""
30
+ vfs = VirtualFS({})
31
+
32
+ files = {
33
+ "file1.txt": b"content 1",
34
+ "file2.txt": "not bytes", # Invalid!
35
+ }
36
+
37
+ with pytest.raises(TypeError, match="Expected bytes for 'file2.txt'"):
38
+ vfs.write_many(files)
39
+
40
+ # No files should be written (validation happens first)
41
+ assert not vfs.exists("file1.txt")
42
+
43
+ def test_write_many_with_dict_state(self):
44
+ """Test that write_many works with dict state (no snapshot method)."""
45
+ vfs = VirtualFS({})
46
+
47
+ files = {
48
+ "file1.txt": b"content 1",
49
+ "file2.txt": b"content 2",
50
+ }
51
+
52
+ # Should not raise - dict doesn't have snapshot()
53
+ vfs.write_many(files)
54
+
55
+ assert vfs.exists("file1.txt")
56
+ assert vfs.exists("file2.txt")
57
+
58
+ def test_remove_many_basic(self):
59
+ """Test removing multiple files at once."""
60
+ vfs = VirtualFS({})
61
+
62
+ # Create files
63
+ vfs.write("file1.txt", b"content 1")
64
+ vfs.write("file2.txt", b"content 2")
65
+ vfs.write("file3.txt", b"content 3")
66
+
67
+ # Remove two of them
68
+ vfs.remove_many(["file1.txt", "file2.txt"])
69
+
70
+ # Removed files should not exist
71
+ assert not vfs.exists("file1.txt")
72
+ assert not vfs.exists("file2.txt")
73
+ # Remaining file should still exist
74
+ assert vfs.exists("file3.txt")
75
+
76
+ def test_remove_many_missing_file_removes_preceding(self):
77
+ """Test that remove_many removes files up to the missing one."""
78
+ vfs = VirtualFS({})
79
+
80
+ vfs.write("file1.txt", b"content 1")
81
+
82
+ with pytest.raises(FileNotFoundError):
83
+ vfs.remove_many(["file1.txt", "file2.txt"])
84
+
85
+ # file1.txt was removed before file2.txt failed
86
+ assert not vfs.exists("file1.txt")
87
+
88
+ def test_remove_many_empty_list(self):
89
+ """Test that removing empty list works."""
90
+ vfs = VirtualFS({})
91
+
92
+ # Should not raise
93
+ vfs.remove_many([])
94
+
95
+
96
+ class BatchingState(dict):
97
+ """A dict that also answers ``get_many``, counting how it is read."""
98
+
99
+ def __init__(self, *args, **kwargs):
100
+ super().__init__(*args, **kwargs)
101
+ self.gets: list[str] = []
102
+ self.batches: list[tuple[str, ...]] = []
103
+
104
+ def get(self, key, default=None):
105
+ self.gets.append(key)
106
+ return super().get(key, default)
107
+
108
+ def get_many(self, *keys):
109
+ self.batches.append(keys)
110
+ return {k: dict.__getitem__(self, k) for k in keys if k in self}
111
+
112
+
113
+ class TestReadMany:
114
+ def test_reads_every_file_in_one_call(self):
115
+ state = BatchingState()
116
+ vfs = VirtualFS(state)
117
+ for i in range(20):
118
+ vfs.write(f"/d{i % 2}/f{i}.txt", f"file {i}".encode())
119
+ state.gets.clear()
120
+ state.batches.clear()
121
+
122
+ found = vfs.read_many([f"/d{i % 2}/f{i}.txt" for i in range(20)])
123
+
124
+ assert found == {f"/d{i % 2}/f{i}.txt": f"file {i}".encode() for i in range(20)}
125
+ assert len(state.batches) == 1
126
+ assert state.gets == []
127
+
128
+ def test_leaves_out_what_is_not_a_file(self):
129
+ vfs = VirtualFS({})
130
+ vfs.write("/d/a.txt", b"a")
131
+
132
+ assert vfs.read_many(["/d/a.txt", "/d", "/nope.txt"]) == {"/d/a.txt": b"a"}
133
+
134
+ def test_answers_under_the_paths_as_given(self):
135
+ vfs = VirtualFS({})
136
+ vfs.write("/d/a.txt", b"a")
137
+ vfs.chdir("/d")
138
+
139
+ assert vfs.read_many(["a.txt", "/d/a.txt"]) == {
140
+ "a.txt": b"a",
141
+ "/d/a.txt": b"a",
142
+ }
143
+
144
+ def test_matches_read_without_get_many(self):
145
+ vfs = VirtualFS({})
146
+ vfs.write_many({"x.txt": b"x", "y/z.txt": b"z"})
147
+
148
+ assert vfs.read_many(["x.txt", "y/z.txt"]) == {
149
+ "x.txt": vfs.read("x.txt"),
150
+ "y/z.txt": vfs.read("y/z.txt"),
151
+ }
152
+
153
+ def test_empty(self):
154
+ assert VirtualFS({}).read_many([]) == {}
155
+
156
+ def test_isolated(self, tmp_path):
157
+ from monkeyfs import IsolatedFS
158
+
159
+ fs = IsolatedFS(str(tmp_path))
160
+ fs.write("a.txt", b"a")
161
+ fs.mkdir("d")
162
+
163
+ assert fs.read_many(["a.txt", "d", "missing.txt"]) == {"a.txt": b"a"}
164
+
165
+ def test_mount_routes_each_path_to_its_filesystem(self):
166
+ from monkeyfs import MountFS
167
+
168
+ base_state, mounted_state = BatchingState(), BatchingState()
169
+ base, mounted = VirtualFS(base_state), VirtualFS(mounted_state)
170
+ base.write("/top.txt", b"top")
171
+ mounted.write("/inner.txt", b"inner")
172
+ fs = MountFS(base, {"/m": mounted})
173
+ base_state.batches.clear()
174
+ mounted_state.batches.clear()
175
+
176
+ assert fs.read_many(["/top.txt", "/m/inner.txt", "/m/gone.txt"]) == {
177
+ "/top.txt": b"top",
178
+ "/m/inner.txt": b"inner",
179
+ }
180
+ assert len(base_state.batches) == 1
181
+ assert len(mounted_state.batches) == 1
182
+
183
+ def test_isolated_skips_what_is_not_a_regular_file(self, tmp_path):
184
+ import os
185
+
186
+ from monkeyfs import IsolatedFS
187
+
188
+ fs = IsolatedFS(str(tmp_path))
189
+ fs.write("a.txt", b"a")
190
+ os.mkfifo(tmp_path / "pipe") # a read would block for a writer
191
+ os.symlink(tmp_path / "a.txt", tmp_path / "link")
192
+
193
+ assert fs.read_many(["a.txt", "pipe", "link"]) == {
194
+ "a.txt": b"a",
195
+ "link": b"a",
196
+ }
197
+
198
+ def test_mount_leaves_out_a_directory_a_mount_makes(self):
199
+ from monkeyfs import MountFS
200
+
201
+ base = VirtualFS({})
202
+ base.write("/a", b"hidden by the mount below")
203
+ base.write("/top.txt", b"top")
204
+ mounted = VirtualFS({})
205
+ mounted.write("/c.txt", b"c")
206
+ fs = MountFS(base, {"/a/b": mounted})
207
+
208
+ assert not fs.isfile("/a")
209
+ assert fs.read_many(["/a", "/a/b", "/a/b/c.txt", "/top.txt"]) == {
210
+ "/a/b/c.txt": b"c",
211
+ "/top.txt": b"top",
212
+ }
213
+
214
+ def test_mount_reads_a_filesystem_without_it_file_by_file(self):
215
+ from monkeyfs import MountFS
216
+
217
+ class Plain:
218
+ """A mounted backend with ``read`` and no ``read_many``."""
219
+
220
+ def __init__(self, files):
221
+ self.files = files
222
+
223
+ def read(self, path):
224
+ try:
225
+ return self.files[path]
226
+ except KeyError:
227
+ raise FileNotFoundError(path) from None
228
+
229
+ fs = MountFS(VirtualFS({}), {"/p": Plain({"/x.txt": b"x"})})
230
+
231
+ assert fs.read_many(["/p/x.txt", "/p/y.txt"]) == {"/p/x.txt": b"x"}
232
+
233
+ def test_read_only_forwards_it(self):
234
+ from monkeyfs import ReadOnlyFS
235
+
236
+ vfs = VirtualFS({})
237
+ vfs.write("a.txt", b"a")
238
+
239
+ assert ReadOnlyFS(vfs).read_many(["a.txt"]) == {"a.txt": b"a"}
@@ -436,6 +436,7 @@ READING_METHODS = {
436
436
  "open",
437
437
  "path_for_metadata_key",
438
438
  "read",
439
+ "read_many",
439
440
  "readlink",
440
441
  "realpath",
441
442
  "resolve_path",
@@ -1,93 +0,0 @@
1
- """Tests for VirtualFS bulk operations (write_many, remove_many)."""
2
-
3
- import pytest
4
-
5
- from monkeyfs import VirtualFS
6
-
7
-
8
- class TestBulkOperations:
9
- """Test VirtualFS bulk operations."""
10
-
11
- def test_write_many_basic(self):
12
- """Test writing multiple files at once."""
13
- vfs = VirtualFS({})
14
-
15
- files = {
16
- "file1.txt": b"content 1",
17
- "file2.txt": b"content 2",
18
- "dir/file3.txt": b"content 3",
19
- }
20
-
21
- vfs.write_many(files)
22
-
23
- # All files should exist
24
- assert vfs.read("file1.txt") == b"content 1"
25
- assert vfs.read("file2.txt") == b"content 2"
26
- assert vfs.read("dir/file3.txt") == b"content 3"
27
-
28
- def test_write_many_validates_all_bytes(self):
29
- """Test that write_many validates all content is bytes."""
30
- vfs = VirtualFS({})
31
-
32
- files = {
33
- "file1.txt": b"content 1",
34
- "file2.txt": "not bytes", # Invalid!
35
- }
36
-
37
- with pytest.raises(TypeError, match="Expected bytes for 'file2.txt'"):
38
- vfs.write_many(files)
39
-
40
- # No files should be written (validation happens first)
41
- assert not vfs.exists("file1.txt")
42
-
43
- def test_write_many_with_dict_state(self):
44
- """Test that write_many works with dict state (no snapshot method)."""
45
- vfs = VirtualFS({})
46
-
47
- files = {
48
- "file1.txt": b"content 1",
49
- "file2.txt": b"content 2",
50
- }
51
-
52
- # Should not raise - dict doesn't have snapshot()
53
- vfs.write_many(files)
54
-
55
- assert vfs.exists("file1.txt")
56
- assert vfs.exists("file2.txt")
57
-
58
- def test_remove_many_basic(self):
59
- """Test removing multiple files at once."""
60
- vfs = VirtualFS({})
61
-
62
- # Create files
63
- vfs.write("file1.txt", b"content 1")
64
- vfs.write("file2.txt", b"content 2")
65
- vfs.write("file3.txt", b"content 3")
66
-
67
- # Remove two of them
68
- vfs.remove_many(["file1.txt", "file2.txt"])
69
-
70
- # Removed files should not exist
71
- assert not vfs.exists("file1.txt")
72
- assert not vfs.exists("file2.txt")
73
- # Remaining file should still exist
74
- assert vfs.exists("file3.txt")
75
-
76
- def test_remove_many_missing_file_removes_preceding(self):
77
- """Test that remove_many removes files up to the missing one."""
78
- vfs = VirtualFS({})
79
-
80
- vfs.write("file1.txt", b"content 1")
81
-
82
- with pytest.raises(FileNotFoundError):
83
- vfs.remove_many(["file1.txt", "file2.txt"])
84
-
85
- # file1.txt was removed before file2.txt failed
86
- assert not vfs.exists("file1.txt")
87
-
88
- def test_remove_many_empty_list(self):
89
- """Test that removing empty list works."""
90
- vfs = VirtualFS({})
91
-
92
- # Should not raise
93
- vfs.remove_many([])
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes