monkeyfs 0.2.2__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.
Files changed (46) hide show
  1. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/PKG-INFO +2 -2
  2. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/README.md +1 -1
  3. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs/virtual.py +30 -5
  4. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs.egg-info/PKG-INFO +2 -2
  5. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/pyproject.toml +1 -1
  6. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_metadata.py +67 -0
  7. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/LICENSE +0 -0
  8. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs/__init__.py +0 -0
  9. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs/base.py +0 -0
  10. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs/conformance.py +0 -0
  11. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs/context.py +0 -0
  12. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs/isolated.py +0 -0
  13. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs/mount.py +0 -0
  14. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs/patching/__init__.py +0 -0
  15. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs/patching/core.py +0 -0
  16. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs/patching/fdtable.py +0 -0
  17. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs/patching/install.py +0 -0
  18. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs/patching/patches.py +0 -0
  19. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs/py.typed +0 -0
  20. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs/readonly.py +0 -0
  21. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs/virtualfile.py +0 -0
  22. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs.egg-info/SOURCES.txt +0 -0
  23. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs.egg-info/dependency_links.txt +0 -0
  24. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs.egg-info/requires.txt +0 -0
  25. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/monkeyfs.egg-info/top_level.txt +0 -0
  26. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/setup.cfg +0 -0
  27. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_bulk_operations.py +0 -0
  28. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_conformance.py +0 -0
  29. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_directory_ops.py +0 -0
  30. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_expanduser.py +0 -0
  31. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_fd_emulation.py +0 -0
  32. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_finalization_and_invalidate.py +0 -0
  33. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_isolated.py +0 -0
  34. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_lazy_binary_read.py +0 -0
  35. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_mount.py +0 -0
  36. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_open_optional.py +0 -0
  37. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_patching.py +0 -0
  38. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_patching_pathlib.py +0 -0
  39. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_patching_safe_paths.py +0 -0
  40. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_protocol.py +0 -0
  41. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_ranged_read.py +0 -0
  42. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_readonly.py +0 -0
  43. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_symlink_entries.py +0 -0
  44. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_vfs_optional.py +0 -0
  45. {monkeyfs-0.2.2 → monkeyfs-0.2.3}/tests/test_vfs_size_limit.py +0 -0
  46. {monkeyfs-0.2.2 → 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.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) `Staged` instances for a versioned filesystem with commit/rollback.
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) `Staged` instances for a versioned filesystem with commit/rollback.
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
 
@@ -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 Staged state.
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
- if not self.is_metadata_key(key):
422
- continue
423
- raw = self._state.get(key)
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: monkeyfs
3
- Version: 0.2.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) `Staged` instances for a versioned filesystem with commit/rollback.
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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "monkeyfs"
3
- version = "0.2.2"
3
+ version = "0.2.3"
4
4
  description = "Transparent filesystem interception via monkey-patching."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -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