monkeyfs 0.2.1__tar.gz → 0.2.3__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.
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/PKG-INFO +2 -2
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/README.md +1 -1
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/monkeyfs/conformance.py +10 -1
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/monkeyfs/mount.py +2 -1
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/monkeyfs/virtual.py +40 -10
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/monkeyfs.egg-info/PKG-INFO +2 -2
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/pyproject.toml +1 -1
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/tests/test_directory_ops.py +48 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/tests/test_metadata.py +67 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/tests/test_open_optional.py +1 -1
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/LICENSE +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/monkeyfs/__init__.py +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/monkeyfs/base.py +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/monkeyfs/context.py +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/monkeyfs/isolated.py +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/monkeyfs/patching/__init__.py +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/monkeyfs/patching/core.py +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/monkeyfs/patching/fdtable.py +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/monkeyfs/patching/install.py +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/monkeyfs/patching/patches.py +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/monkeyfs/py.typed +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/monkeyfs/readonly.py +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/monkeyfs/virtualfile.py +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/monkeyfs.egg-info/SOURCES.txt +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/monkeyfs.egg-info/dependency_links.txt +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/monkeyfs.egg-info/requires.txt +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/monkeyfs.egg-info/top_level.txt +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/setup.cfg +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/tests/test_bulk_operations.py +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/tests/test_conformance.py +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/tests/test_expanduser.py +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/tests/test_fd_emulation.py +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/tests/test_finalization_and_invalidate.py +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/tests/test_isolated.py +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/tests/test_lazy_binary_read.py +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/tests/test_mount.py +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/tests/test_patching.py +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/tests/test_patching_pathlib.py +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/tests/test_patching_safe_paths.py +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/tests/test_protocol.py +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/tests/test_ranged_read.py +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/tests/test_readonly.py +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/tests/test_symlink_entries.py +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/tests/test_vfs_optional.py +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/tests/test_vfs_size_limit.py +0 -0
- {monkeyfs-0.2.1 → monkeyfs-0.2.3}/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
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Transparent filesystem interception via monkey-patching.
|
|
5
5
|
Author: ashenfad
|
|
6
6
|
License-Expression: MIT
|
|
@@ -142,7 +142,7 @@ It raises `AssertionError` naming the method and what was expected of it -- incl
|
|
|
142
142
|
|
|
143
143
|
## Part of the agex stack
|
|
144
144
|
|
|
145
|
-
monkeyfs provides filesystem interception for [sandtrap](https://github.com/ashenfad/sandtrap) and [agex](https://github.com/ashenfad/agex), giving sandboxed agent code an isolated virtual filesystem. `VirtualFS` accepts any dict-like backing store -- including [kvgit](https://github.com/ashenfad/kvgit)
|
|
145
|
+
monkeyfs provides filesystem interception for [sandtrap](https://github.com/ashenfad/sandtrap) and [agex](https://github.com/ashenfad/agex), giving sandboxed agent code an isolated virtual filesystem. `VirtualFS` accepts any dict-like backing store -- including a [kvgit](https://github.com/ashenfad/kvgit) worktree for a versioned filesystem with commit/rollback. A store that offers `get_many(*keys)`, as kvgit worktrees do, has its metadata read in one batched call rather than one per file.
|
|
146
146
|
|
|
147
147
|
## Documentation
|
|
148
148
|
|
|
@@ -108,7 +108,7 @@ It raises `AssertionError` naming the method and what was expected of it -- incl
|
|
|
108
108
|
|
|
109
109
|
## Part of the agex stack
|
|
110
110
|
|
|
111
|
-
monkeyfs provides filesystem interception for [sandtrap](https://github.com/ashenfad/sandtrap) and [agex](https://github.com/ashenfad/agex), giving sandboxed agent code an isolated virtual filesystem. `VirtualFS` accepts any dict-like backing store -- including [kvgit](https://github.com/ashenfad/kvgit)
|
|
111
|
+
monkeyfs provides filesystem interception for [sandtrap](https://github.com/ashenfad/sandtrap) and [agex](https://github.com/ashenfad/agex), giving sandboxed agent code an isolated virtual filesystem. `VirtualFS` accepts any dict-like backing store -- including a [kvgit](https://github.com/ashenfad/kvgit) worktree for a versioned filesystem with commit/rollback. A store that offers `get_many(*keys)`, as kvgit worktrees do, has its metadata read in one batched call rather than one per file.
|
|
112
112
|
|
|
113
113
|
## Documentation
|
|
114
114
|
|
|
@@ -212,12 +212,21 @@ def _check_listing_paths(fs: Any, scratch: str) -> None:
|
|
|
212
212
|
f"{absolute.get('probe.bin')!r}",
|
|
213
213
|
)
|
|
214
214
|
|
|
215
|
-
|
|
215
|
+
entries = fs.list_detailed(scratch, True)
|
|
216
|
+
deep = [info.path for info in entries]
|
|
216
217
|
_require(
|
|
217
218
|
f"{scratch}/nested/leaf.bin" in deep,
|
|
218
219
|
f"a recursive list_detailed({scratch!r}) must name a nested entry "
|
|
219
220
|
f"{scratch + '/nested/leaf.bin'!r}, got {deep!r}",
|
|
220
221
|
)
|
|
222
|
+
for info in entries:
|
|
223
|
+
_require(
|
|
224
|
+
info.name == info.path.rsplit("/", 1)[-1],
|
|
225
|
+
f"list_detailed(...).name is the entry's own name, the last "
|
|
226
|
+
f"component of its path, at every depth: for path "
|
|
227
|
+
f"{info.path!r} expected {info.path.rsplit('/', 1)[-1]!r}, "
|
|
228
|
+
f"got {info.name!r}",
|
|
229
|
+
)
|
|
221
230
|
|
|
222
231
|
start = fs.getcwd()
|
|
223
232
|
fs.chdir(scratch)
|
|
@@ -285,10 +285,11 @@ class MountFS:
|
|
|
285
285
|
for name in names:
|
|
286
286
|
full_abs = f"{norm}/{name}" if norm else f"/{name}"
|
|
287
287
|
display = f"{user_prefix}/{name}" if user_prefix != "." else name
|
|
288
|
+
basename = name.rsplit("/", 1)[-1]
|
|
288
289
|
meta = self.stat(full_abs)
|
|
289
290
|
result.append(
|
|
290
291
|
FileInfo(
|
|
291
|
-
name=
|
|
292
|
+
name=basename,
|
|
292
293
|
path=display,
|
|
293
294
|
size=meta.size,
|
|
294
295
|
created_at=meta.created_at,
|
|
@@ -10,6 +10,7 @@ import json
|
|
|
10
10
|
import os
|
|
11
11
|
from collections.abc import MutableMapping
|
|
12
12
|
from datetime import datetime, timezone
|
|
13
|
+
from typing import Any
|
|
13
14
|
|
|
14
15
|
from .base import FileInfo, FileMetadata
|
|
15
16
|
from .virtualfile import LazyBinaryFile, VirtualFile, open_file
|
|
@@ -19,7 +20,8 @@ class VirtualFS:
|
|
|
19
20
|
"""State-backed virtual filesystem with metadata tracking.
|
|
20
21
|
|
|
21
22
|
Provides file operations backed by agent state. Each file is stored
|
|
22
|
-
as a separate state key, enabling granular versioning with
|
|
23
|
+
as a separate state key, enabling granular versioning with a versioned
|
|
24
|
+
state such as a kvgit worktree.
|
|
23
25
|
|
|
24
26
|
File metadata (size, creation time, modification time) is automatically
|
|
25
27
|
tracked for all files and can be accessed via stat() or list_detailed().
|
|
@@ -77,6 +79,11 @@ class VirtualFS:
|
|
|
77
79
|
|
|
78
80
|
Args:
|
|
79
81
|
state: State backend for file storage. Defaults to an empty dict.
|
|
82
|
+
A state may also offer ``get_many(*keys)``, returning a
|
|
83
|
+
mapping of the keys that exist to their values (as kvgit
|
|
84
|
+
worktrees do). Scans that read many keys then ask for them
|
|
85
|
+
in one call, which over a networked store is one round
|
|
86
|
+
trip instead of one per key.
|
|
80
87
|
max_size_mb: Maximum total size of all files in megabytes.
|
|
81
88
|
None means unlimited.
|
|
82
89
|
"""
|
|
@@ -417,10 +424,10 @@ class VirtualFS:
|
|
|
417
424
|
return self._all_meta
|
|
418
425
|
|
|
419
426
|
rows: dict[str, FileMetadata] = {}
|
|
420
|
-
for key in list(self._state.keys())
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
raw =
|
|
427
|
+
keys = [key for key in list(self._state.keys()) if self.is_metadata_key(key)]
|
|
428
|
+
found = self._read_many(keys)
|
|
429
|
+
for key in keys:
|
|
430
|
+
raw = found.get(key)
|
|
424
431
|
if raw is None:
|
|
425
432
|
continue
|
|
426
433
|
try:
|
|
@@ -436,6 +443,24 @@ class VirtualFS:
|
|
|
436
443
|
self._all_meta = rows
|
|
437
444
|
return rows
|
|
438
445
|
|
|
446
|
+
def _read_many(self, keys: list[str]) -> dict[str, Any]:
|
|
447
|
+
"""The values of ``keys`` that exist in the state.
|
|
448
|
+
|
|
449
|
+
One ``get_many`` call when the state offers it, else one ``get``
|
|
450
|
+
per key.
|
|
451
|
+
"""
|
|
452
|
+
if not keys:
|
|
453
|
+
return {}
|
|
454
|
+
get_many = getattr(self._state, "get_many", None)
|
|
455
|
+
if callable(get_many):
|
|
456
|
+
return dict(get_many(*keys))
|
|
457
|
+
found: dict[str, Any] = {}
|
|
458
|
+
for key in keys:
|
|
459
|
+
value = self._state.get(key)
|
|
460
|
+
if value is not None:
|
|
461
|
+
found[key] = value
|
|
462
|
+
return found
|
|
463
|
+
|
|
439
464
|
def _get_current_size(self) -> int:
|
|
440
465
|
"""Get total size of all files in the VFS.
|
|
441
466
|
|
|
@@ -1325,7 +1350,10 @@ class VirtualFS:
|
|
|
1325
1350
|
recursive: If True, list all nested files and directories.
|
|
1326
1351
|
|
|
1327
1352
|
Returns:
|
|
1328
|
-
List of FileInfo objects sorted by
|
|
1353
|
+
List of FileInfo objects sorted by path relative to the queried
|
|
1354
|
+
directory, so a directory precedes its contents. ``name`` is the
|
|
1355
|
+
entry's own name, the last component of ``path``, at every depth;
|
|
1356
|
+
it is not what the list is ordered by.
|
|
1329
1357
|
|
|
1330
1358
|
Example:
|
|
1331
1359
|
>>> files = vfs.list_detailed("/shared")
|
|
@@ -1350,7 +1378,9 @@ class VirtualFS:
|
|
|
1350
1378
|
internal_path = f"{base}/{name}" if base else name
|
|
1351
1379
|
absolute = "/" + internal_path
|
|
1352
1380
|
|
|
1353
|
-
# Display path preserves the user's queried prefix
|
|
1381
|
+
# Display path preserves the user's queried prefix; the name
|
|
1382
|
+
# is the last component alone, whatever depth the entry sits at.
|
|
1383
|
+
basename = name.rsplit("/", 1)[-1]
|
|
1354
1384
|
display = f"{user_prefix}/{name}" if user_prefix != "." else name
|
|
1355
1385
|
|
|
1356
1386
|
meta = self._meta_at(internal_path)
|
|
@@ -1360,7 +1390,7 @@ class VirtualFS:
|
|
|
1360
1390
|
now = self._now_iso()
|
|
1361
1391
|
result.append(
|
|
1362
1392
|
FileInfo(
|
|
1363
|
-
name=
|
|
1393
|
+
name=basename,
|
|
1364
1394
|
path=display,
|
|
1365
1395
|
size=0,
|
|
1366
1396
|
created_at=meta.created_at if meta is not None else now,
|
|
@@ -1371,7 +1401,7 @@ class VirtualFS:
|
|
|
1371
1401
|
elif meta is not None:
|
|
1372
1402
|
result.append(
|
|
1373
1403
|
FileInfo(
|
|
1374
|
-
name=
|
|
1404
|
+
name=basename,
|
|
1375
1405
|
path=display,
|
|
1376
1406
|
size=meta.size,
|
|
1377
1407
|
created_at=meta.created_at,
|
|
@@ -1385,7 +1415,7 @@ class VirtualFS:
|
|
|
1385
1415
|
now = self._now_iso()
|
|
1386
1416
|
result.append(
|
|
1387
1417
|
FileInfo(
|
|
1388
|
-
name=
|
|
1418
|
+
name=basename,
|
|
1389
1419
|
path=display,
|
|
1390
1420
|
size=len(content),
|
|
1391
1421
|
created_at=now,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: monkeyfs
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Transparent filesystem interception via monkey-patching.
|
|
5
5
|
Author: ashenfad
|
|
6
6
|
License-Expression: MIT
|
|
@@ -142,7 +142,7 @@ It raises `AssertionError` naming the method and what was expected of it -- incl
|
|
|
142
142
|
|
|
143
143
|
## Part of the agex stack
|
|
144
144
|
|
|
145
|
-
monkeyfs provides filesystem interception for [sandtrap](https://github.com/ashenfad/sandtrap) and [agex](https://github.com/ashenfad/agex), giving sandboxed agent code an isolated virtual filesystem. `VirtualFS` accepts any dict-like backing store -- including [kvgit](https://github.com/ashenfad/kvgit)
|
|
145
|
+
monkeyfs provides filesystem interception for [sandtrap](https://github.com/ashenfad/sandtrap) and [agex](https://github.com/ashenfad/agex), giving sandboxed agent code an isolated virtual filesystem. `VirtualFS` accepts any dict-like backing store -- including a [kvgit](https://github.com/ashenfad/kvgit) worktree for a versioned filesystem with commit/rollback. A store that offers `get_many(*keys)`, as kvgit worktrees do, has its metadata read in one batched call rather than one per file.
|
|
146
146
|
|
|
147
147
|
## Documentation
|
|
148
148
|
|
|
@@ -466,3 +466,51 @@ class TestImplicitDirectories:
|
|
|
466
466
|
assert vfs.list("/") == ["pkg", "top.py"]
|
|
467
467
|
assert vfs.list("pkg") == ["__init__.py", "sub", "utils.py"]
|
|
468
468
|
assert vfs.list("pkg/sub") == ["mod.py"]
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
# -- list_detailed().name is the entry's own name at every depth ---------------
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _nested_tree(fs):
|
|
475
|
+
fs.makedirs("/probe/sub")
|
|
476
|
+
fs.write("/probe/one.txt", b"1")
|
|
477
|
+
fs.write("/probe/sub/two.txt", b"2")
|
|
478
|
+
return fs
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _names_and_paths(fs):
|
|
482
|
+
# The sequence as returned, not a sorted copy: every backend orders a
|
|
483
|
+
# recursive listing by path relative to the queried directory, so a
|
|
484
|
+
# directory precedes its contents, and a backend that drifts from
|
|
485
|
+
# that order fails here rather than being tidied into agreement.
|
|
486
|
+
return [(i.name, i.path) for i in fs.list_detailed("/probe", recursive=True)]
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def test_virtualfs_recursive_listing_names_are_basenames():
|
|
490
|
+
from monkeyfs import VirtualFS
|
|
491
|
+
|
|
492
|
+
assert _names_and_paths(_nested_tree(VirtualFS({}))) == [
|
|
493
|
+
("one.txt", "/probe/one.txt"),
|
|
494
|
+
("sub", "/probe/sub"),
|
|
495
|
+
("two.txt", "/probe/sub/two.txt"),
|
|
496
|
+
]
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def test_mountfs_recursive_listing_names_are_basenames():
|
|
500
|
+
from monkeyfs import MountFS, VirtualFS
|
|
501
|
+
|
|
502
|
+
assert _names_and_paths(_nested_tree(MountFS(VirtualFS({})))) == [
|
|
503
|
+
("one.txt", "/probe/one.txt"),
|
|
504
|
+
("sub", "/probe/sub"),
|
|
505
|
+
("two.txt", "/probe/sub/two.txt"),
|
|
506
|
+
]
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def test_isolatedfs_recursive_listing_names_are_basenames(tmp_path):
|
|
510
|
+
from monkeyfs import IsolatedFS
|
|
511
|
+
|
|
512
|
+
assert _names_and_paths(_nested_tree(IsolatedFS(str(tmp_path)))) == [
|
|
513
|
+
("one.txt", "/probe/one.txt"),
|
|
514
|
+
("sub", "/probe/sub"),
|
|
515
|
+
("two.txt", "/probe/sub/two.txt"),
|
|
516
|
+
]
|
|
@@ -707,3 +707,70 @@ class TestLegacyTableMigration:
|
|
|
707
707
|
assert vfs.isdir("d")
|
|
708
708
|
assert set(rows(state)) == {"d/a.txt"}
|
|
709
709
|
assert set(legacy_table(state)) == {"d"}
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
class BatchingState(dict):
|
|
713
|
+
"""A dict that also answers ``get_many``, counting how it is read."""
|
|
714
|
+
|
|
715
|
+
def __init__(self, *args, **kwargs):
|
|
716
|
+
super().__init__(*args, **kwargs)
|
|
717
|
+
self.gets: list[str] = []
|
|
718
|
+
self.batches: list[tuple[str, ...]] = []
|
|
719
|
+
|
|
720
|
+
def get(self, key, default=None):
|
|
721
|
+
self.gets.append(key)
|
|
722
|
+
return super().get(key, default)
|
|
723
|
+
|
|
724
|
+
def get_many(self, *keys):
|
|
725
|
+
self.batches.append(keys)
|
|
726
|
+
return {k: dict.__getitem__(self, k) for k in keys if k in self}
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def _tree(vfs):
|
|
730
|
+
vfs.mkdir("/empty")
|
|
731
|
+
for i in range(30):
|
|
732
|
+
vfs.write(f"/d{i % 3}/f{i}.txt", b"x" * i)
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
class TestBatchedMetadataScan:
|
|
736
|
+
def test_a_scan_reads_every_row_in_one_call(self):
|
|
737
|
+
state = BatchingState()
|
|
738
|
+
_tree(VirtualFS(state))
|
|
739
|
+
state.gets.clear()
|
|
740
|
+
|
|
741
|
+
fresh = VirtualFS(state) # nothing cached
|
|
742
|
+
fresh.list("/", recursive=True)
|
|
743
|
+
|
|
744
|
+
assert len(state.batches) == 1
|
|
745
|
+
assert set(state.batches[0]) == {
|
|
746
|
+
k for k in state if VirtualFS.is_metadata_key(k)
|
|
747
|
+
}
|
|
748
|
+
assert not [k for k in state.gets if VirtualFS.is_metadata_key(k)]
|
|
749
|
+
|
|
750
|
+
def test_a_batching_state_answers_as_a_plain_one_does(self):
|
|
751
|
+
plain: dict = {}
|
|
752
|
+
_tree(VirtualFS(plain))
|
|
753
|
+
batching = BatchingState(plain)
|
|
754
|
+
|
|
755
|
+
def listing(state):
|
|
756
|
+
vfs = VirtualFS(state)
|
|
757
|
+
return [
|
|
758
|
+
(info.path, info.size, info.is_dir)
|
|
759
|
+
for info in vfs.list_detailed("/", recursive=True)
|
|
760
|
+
]
|
|
761
|
+
|
|
762
|
+
assert listing(batching) == listing(plain)
|
|
763
|
+
assert (
|
|
764
|
+
VirtualFS(batching)._get_current_size()
|
|
765
|
+
== VirtualFS(plain)._get_current_size()
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
def test_the_legacy_table_still_fills_paths_without_rows(self):
|
|
769
|
+
state = BatchingState(
|
|
770
|
+
_legacy_state({"/old.txt": _entry(3)}, blobs={"/old.txt": b"abc"})
|
|
771
|
+
)
|
|
772
|
+
vfs = VirtualFS(state)
|
|
773
|
+
vfs.write("/new.txt", b"hello")
|
|
774
|
+
fresh = VirtualFS(state)
|
|
775
|
+
assert fresh.stat("/old.txt").size == 3
|
|
776
|
+
assert {i.path for i in fresh.list_detailed("/")} == {"/old.txt", "/new.txt"}
|
|
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
|
|
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
|
|
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
|
|
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
|