monkeyfs 0.1.0__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.1.0/LICENSE +21 -0
- monkeyfs-0.1.0/PKG-INFO +97 -0
- monkeyfs-0.1.0/README.md +61 -0
- monkeyfs-0.1.0/monkeyfs/__init__.py +23 -0
- monkeyfs-0.1.0/monkeyfs/base.py +159 -0
- monkeyfs-0.1.0/monkeyfs/config.py +98 -0
- monkeyfs-0.1.0/monkeyfs/context.py +29 -0
- monkeyfs-0.1.0/monkeyfs/isolated.py +526 -0
- monkeyfs-0.1.0/monkeyfs/patching/__init__.py +14 -0
- monkeyfs-0.1.0/monkeyfs/patching/core.py +148 -0
- monkeyfs-0.1.0/monkeyfs/patching/fdtable.py +226 -0
- monkeyfs-0.1.0/monkeyfs/patching/install.py +322 -0
- monkeyfs-0.1.0/monkeyfs/patching/patches.py +750 -0
- monkeyfs-0.1.0/monkeyfs/py.typed +0 -0
- monkeyfs-0.1.0/monkeyfs/virtual.py +1098 -0
- monkeyfs-0.1.0/monkeyfs/virtualfile.py +128 -0
- monkeyfs-0.1.0/monkeyfs.egg-info/PKG-INFO +97 -0
- monkeyfs-0.1.0/monkeyfs.egg-info/SOURCES.txt +32 -0
- monkeyfs-0.1.0/monkeyfs.egg-info/dependency_links.txt +1 -0
- monkeyfs-0.1.0/monkeyfs.egg-info/requires.txt +10 -0
- monkeyfs-0.1.0/monkeyfs.egg-info/top_level.txt +3 -0
- monkeyfs-0.1.0/pyproject.toml +65 -0
- monkeyfs-0.1.0/setup.cfg +4 -0
- monkeyfs-0.1.0/tests/test_bulk_operations.py +93 -0
- monkeyfs-0.1.0/tests/test_directory_ops.py +397 -0
- monkeyfs-0.1.0/tests/test_expanduser.py +163 -0
- monkeyfs-0.1.0/tests/test_fd_emulation.py +453 -0
- monkeyfs-0.1.0/tests/test_isolated.py +683 -0
- monkeyfs-0.1.0/tests/test_metadata.py +146 -0
- monkeyfs-0.1.0/tests/test_patching.py +890 -0
- monkeyfs-0.1.0/tests/test_patching_pathlib.py +252 -0
- monkeyfs-0.1.0/tests/test_vfs_optional.py +356 -0
- monkeyfs-0.1.0/tests/test_vfs_size_limit.py +296 -0
- monkeyfs-0.1.0/tests/test_virtual.py +320 -0
monkeyfs-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adam Ashenfelter
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
monkeyfs-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: monkeyfs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Transparent filesystem interception via monkey-patching.
|
|
5
|
+
Author: ashenfad
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ashenfad/monkeyfs
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/ashenfad/monkeyfs/issues
|
|
9
|
+
Project-URL: Documentation, https://github.com/ashenfad/monkeyfs#readme
|
|
10
|
+
Project-URL: Source, https://github.com/ashenfad/monkeyfs
|
|
11
|
+
Keywords: filesystem,monkey-patch,sandbox,vfs,interception
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Topic :: System :: Filesystems
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: ruff; extra == "dev"
|
|
29
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-timeout; extra == "dev"
|
|
32
|
+
Provides-Extra: test
|
|
33
|
+
Requires-Dist: pytest; extra == "test"
|
|
34
|
+
Requires-Dist: pytest-timeout; extra == "test"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# monkeyfs 🐒
|
|
38
|
+
|
|
39
|
+
Filesystem interception via monkey-patching.
|
|
40
|
+
|
|
41
|
+
Patches `open()`, `os.listdir()`, `os.stat()`, and 30+ other stdlib functions to route through a virtual or isolated filesystem. Patches are applied lazily on first `patch()` call and are inert outside the context. Uses `contextvars` for async-safe isolation between concurrent tasks. Zero dependencies.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install monkeyfs
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quick example
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from monkeyfs import VirtualFS, patch
|
|
53
|
+
|
|
54
|
+
vfs = VirtualFS({})
|
|
55
|
+
|
|
56
|
+
with patch(vfs):
|
|
57
|
+
with open("data.csv", "w") as f:
|
|
58
|
+
f.write("name,score\nalice,98\nbob,87\n")
|
|
59
|
+
|
|
60
|
+
import os
|
|
61
|
+
print(os.listdir("/")) # ['data.csv']
|
|
62
|
+
print(os.path.getsize("data.csv")) # 30
|
|
63
|
+
|
|
64
|
+
with open("data.csv") as f:
|
|
65
|
+
print(f.read()) # name,score\nalice,98\nbob,87\n
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## IsolatedFS
|
|
69
|
+
|
|
70
|
+
Restricts file operations to a root directory on the real filesystem:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from monkeyfs import IsolatedFS, patch
|
|
74
|
+
|
|
75
|
+
isolated = IsolatedFS(root="/tmp/sandbox")
|
|
76
|
+
|
|
77
|
+
with patch(isolated):
|
|
78
|
+
with open("notes.txt", "w") as f:
|
|
79
|
+
f.write("hello") # Written to /tmp/sandbox/notes.txt
|
|
80
|
+
|
|
81
|
+
open("/etc/passwd") # PermissionError -- outside root
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Documentation
|
|
85
|
+
|
|
86
|
+
- [API Reference](docs/api.md) -- public API, FileSystem protocol, patched functions
|
|
87
|
+
|
|
88
|
+
## Development
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
uv sync --extra dev
|
|
92
|
+
uv run pytest
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
MIT
|
monkeyfs-0.1.0/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# monkeyfs 🐒
|
|
2
|
+
|
|
3
|
+
Filesystem interception via monkey-patching.
|
|
4
|
+
|
|
5
|
+
Patches `open()`, `os.listdir()`, `os.stat()`, and 30+ other stdlib functions to route through a virtual or isolated filesystem. Patches are applied lazily on first `patch()` call and are inert outside the context. Uses `contextvars` for async-safe isolation between concurrent tasks. Zero dependencies.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install monkeyfs
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick example
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from monkeyfs import VirtualFS, patch
|
|
17
|
+
|
|
18
|
+
vfs = VirtualFS({})
|
|
19
|
+
|
|
20
|
+
with patch(vfs):
|
|
21
|
+
with open("data.csv", "w") as f:
|
|
22
|
+
f.write("name,score\nalice,98\nbob,87\n")
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
print(os.listdir("/")) # ['data.csv']
|
|
26
|
+
print(os.path.getsize("data.csv")) # 30
|
|
27
|
+
|
|
28
|
+
with open("data.csv") as f:
|
|
29
|
+
print(f.read()) # name,score\nalice,98\nbob,87\n
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## IsolatedFS
|
|
33
|
+
|
|
34
|
+
Restricts file operations to a root directory on the real filesystem:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from monkeyfs import IsolatedFS, patch
|
|
38
|
+
|
|
39
|
+
isolated = IsolatedFS(root="/tmp/sandbox")
|
|
40
|
+
|
|
41
|
+
with patch(isolated):
|
|
42
|
+
with open("notes.txt", "w") as f:
|
|
43
|
+
f.write("hello") # Written to /tmp/sandbox/notes.txt
|
|
44
|
+
|
|
45
|
+
open("/etc/passwd") # PermissionError -- outside root
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Documentation
|
|
49
|
+
|
|
50
|
+
- [API Reference](docs/api.md) -- public API, FileSystem protocol, patched functions
|
|
51
|
+
|
|
52
|
+
## Development
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
uv sync --extra dev
|
|
56
|
+
uv run pytest
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
MIT
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""monkeyfs: Transparent filesystem interception via monkey-patching."""
|
|
2
|
+
|
|
3
|
+
from .base import FileInfo, FileMetadata, FileSystem
|
|
4
|
+
from .config import FSConfig, IsolatedFSConfig, VirtualFSConfig, connect_fs
|
|
5
|
+
from .context import current_fs, suspend
|
|
6
|
+
from .isolated import IsolatedFS
|
|
7
|
+
from .patching import patch
|
|
8
|
+
from .virtual import VirtualFS
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"connect_fs",
|
|
12
|
+
"current_fs",
|
|
13
|
+
"FileInfo",
|
|
14
|
+
"FileMetadata",
|
|
15
|
+
"FileSystem",
|
|
16
|
+
"FSConfig",
|
|
17
|
+
"IsolatedFS",
|
|
18
|
+
"IsolatedFSConfig",
|
|
19
|
+
"patch",
|
|
20
|
+
"suspend",
|
|
21
|
+
"VirtualFS",
|
|
22
|
+
"VirtualFSConfig",
|
|
23
|
+
]
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Base filesystem interface and dataclasses.
|
|
2
|
+
|
|
3
|
+
Defines the common interface for filesystem implementations (VirtualFS, IsolatedFS).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import Any, Protocol, runtime_checkable
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class FileMetadata:
|
|
16
|
+
"""Metadata for a single file or directory.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
size: File size in bytes (0 for directories).
|
|
20
|
+
created_at: ISO 8601 timestamp when file was created (UTC).
|
|
21
|
+
modified_at: ISO 8601 timestamp when file was last modified (UTC).
|
|
22
|
+
is_dir: True if this is a directory, False for files.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
size: int
|
|
26
|
+
created_at: str
|
|
27
|
+
modified_at: str
|
|
28
|
+
is_dir: bool = False
|
|
29
|
+
|
|
30
|
+
# os.stat_result-compatible properties — allows FileMetadata to be
|
|
31
|
+
# returned directly from stat() when used with sandtrap's os.stat() patch.
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def st_size(self) -> int:
|
|
35
|
+
return self.size
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def st_mode(self) -> int:
|
|
39
|
+
return 0o040755 if self.is_dir else 0o100644
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def st_ino(self) -> int:
|
|
43
|
+
return 0
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def st_dev(self) -> int:
|
|
47
|
+
return 0
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def st_nlink(self) -> int:
|
|
51
|
+
return 1
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def st_uid(self) -> int:
|
|
55
|
+
return os.getuid() if hasattr(os, "getuid") else 0
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def st_gid(self) -> int:
|
|
59
|
+
return os.getgid() if hasattr(os, "getgid") else 0
|
|
60
|
+
|
|
61
|
+
def _parse_ts(self, iso_str: str) -> float:
|
|
62
|
+
try:
|
|
63
|
+
return datetime.fromisoformat(iso_str).timestamp()
|
|
64
|
+
except ValueError:
|
|
65
|
+
return 0.0
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def st_atime(self) -> float:
|
|
69
|
+
return self._parse_ts(self.modified_at)
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def st_mtime(self) -> float:
|
|
73
|
+
return self._parse_ts(self.modified_at)
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def st_ctime(self) -> float:
|
|
77
|
+
return self._parse_ts(self.created_at)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class FileInfo:
|
|
82
|
+
"""Complete file information for UI display.
|
|
83
|
+
|
|
84
|
+
Attributes:
|
|
85
|
+
name: File or directory name (basename).
|
|
86
|
+
path: Full path to file or directory.
|
|
87
|
+
size: File size in bytes (0 for directories).
|
|
88
|
+
created_at: ISO 8601 timestamp when created (UTC).
|
|
89
|
+
modified_at: ISO 8601 timestamp when last modified (UTC).
|
|
90
|
+
is_dir: True if this is a directory, False if file.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
name: str
|
|
94
|
+
path: str
|
|
95
|
+
size: int
|
|
96
|
+
created_at: str
|
|
97
|
+
modified_at: str
|
|
98
|
+
is_dir: bool
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@runtime_checkable
|
|
102
|
+
class FileSystem(Protocol):
|
|
103
|
+
"""Minimal interface for patch() patching.
|
|
104
|
+
|
|
105
|
+
Only methods that the patching layer dispatches to are listed here.
|
|
106
|
+
Implementations may (and typically do) have additional methods like
|
|
107
|
+
read(), write(), glob(), list_detailed(), etc. — those are
|
|
108
|
+
application-level concerns, not part of the interception contract.
|
|
109
|
+
|
|
110
|
+
Required — patching will fail without these:
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
def open(self, path: str, mode: str = "r", **kwargs: Any) -> Any:
|
|
114
|
+
"""Open a file."""
|
|
115
|
+
...
|
|
116
|
+
|
|
117
|
+
def stat(self, path: str) -> FileMetadata:
|
|
118
|
+
"""Get file metadata."""
|
|
119
|
+
...
|
|
120
|
+
|
|
121
|
+
def exists(self, path: str) -> bool:
|
|
122
|
+
"""Check if path exists."""
|
|
123
|
+
...
|
|
124
|
+
|
|
125
|
+
def isfile(self, path: str) -> bool:
|
|
126
|
+
"""Check if path is a file."""
|
|
127
|
+
...
|
|
128
|
+
|
|
129
|
+
def isdir(self, path: str) -> bool:
|
|
130
|
+
"""Check if path is a directory."""
|
|
131
|
+
...
|
|
132
|
+
|
|
133
|
+
def list(self, path: str = ".", recursive: bool = False) -> list[str]:
|
|
134
|
+
"""List directory contents (filenames only)."""
|
|
135
|
+
...
|
|
136
|
+
|
|
137
|
+
def remove(self, path: str) -> None:
|
|
138
|
+
"""Remove a file."""
|
|
139
|
+
...
|
|
140
|
+
|
|
141
|
+
def mkdir(self, path: str, parents: bool = False, exist_ok: bool = False) -> None:
|
|
142
|
+
"""Create a directory."""
|
|
143
|
+
...
|
|
144
|
+
|
|
145
|
+
def makedirs(self, path: str, exist_ok: bool = True) -> None:
|
|
146
|
+
"""Create directory tree."""
|
|
147
|
+
...
|
|
148
|
+
|
|
149
|
+
def rename(self, src: str, dst: str) -> None:
|
|
150
|
+
"""Rename/move a file or directory."""
|
|
151
|
+
...
|
|
152
|
+
|
|
153
|
+
def getcwd(self) -> str:
|
|
154
|
+
"""Get current working directory."""
|
|
155
|
+
...
|
|
156
|
+
|
|
157
|
+
def chdir(self, path: str) -> None:
|
|
158
|
+
"""Change current working directory."""
|
|
159
|
+
...
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Configuration for filesystem access.
|
|
2
|
+
|
|
3
|
+
Provides configuration dataclasses and connect_fs factory function for
|
|
4
|
+
configuring filesystem access (virtual or isolated).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Literal
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class VirtualFSConfig:
|
|
13
|
+
"""Configuration for virtual (in-memory) filesystem.
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
type: Always "virtual".
|
|
17
|
+
max_size_mb: Maximum total size of all files in megabytes.
|
|
18
|
+
None means unlimited.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
type: Literal["virtual"] = "virtual"
|
|
22
|
+
max_size_mb: int | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class IsolatedFSConfig:
|
|
27
|
+
"""Configuration for isolated (real) filesystem with path restriction.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
type: Always "isolated".
|
|
31
|
+
root: Absolute path to root directory (all file operations restricted to this path).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
type: Literal["isolated"] = "isolated"
|
|
35
|
+
root: str = ""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Type alias for all filesystem configs
|
|
39
|
+
FSConfig = VirtualFSConfig | IsolatedFSConfig
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def connect_fs(
|
|
43
|
+
type: Literal["virtual", "isolated"] = "virtual",
|
|
44
|
+
**kwargs,
|
|
45
|
+
) -> FSConfig:
|
|
46
|
+
"""Configure filesystem access.
|
|
47
|
+
|
|
48
|
+
Creates a filesystem configuration.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
type: FileSystem type.
|
|
52
|
+
- "virtual": In-memory filesystem backed by a mapping.
|
|
53
|
+
Files persist with state and participate in versioning.
|
|
54
|
+
- "isolated": Real filesystem restricted to a directory.
|
|
55
|
+
Requires 'root' argument.
|
|
56
|
+
**kwargs: Additional configuration for the filesystem type.
|
|
57
|
+
For type="virtual":
|
|
58
|
+
- max_size_mb (int): Optional. Max total file size in MB.
|
|
59
|
+
For type="isolated":
|
|
60
|
+
- root (str): Required. Absolute path to root directory.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
FSConfig for initialization.
|
|
64
|
+
|
|
65
|
+
Examples:
|
|
66
|
+
Virtual filesystem:
|
|
67
|
+
>>> connect_fs(type="virtual")
|
|
68
|
+
VirtualFSConfig(type='virtual', max_size_mb=None)
|
|
69
|
+
|
|
70
|
+
Isolated filesystem:
|
|
71
|
+
>>> connect_fs(type="isolated", root="/path/to/project")
|
|
72
|
+
IsolatedFSConfig(type='isolated', root='/path/to/project')
|
|
73
|
+
"""
|
|
74
|
+
if type == "virtual":
|
|
75
|
+
max_size_mb = kwargs.pop("max_size_mb", None)
|
|
76
|
+
if kwargs:
|
|
77
|
+
raise ValueError(
|
|
78
|
+
f"Unexpected arguments for virtual fs: {list(kwargs.keys())}"
|
|
79
|
+
)
|
|
80
|
+
return VirtualFSConfig(type=type, max_size_mb=max_size_mb)
|
|
81
|
+
|
|
82
|
+
elif type == "isolated":
|
|
83
|
+
root = kwargs.pop("root", "")
|
|
84
|
+
|
|
85
|
+
if kwargs:
|
|
86
|
+
raise ValueError(
|
|
87
|
+
f"Unexpected arguments for isolated fs: {list(kwargs.keys())}"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if not root:
|
|
91
|
+
raise ValueError("Isolated filesystem requires 'root' parameter")
|
|
92
|
+
|
|
93
|
+
return IsolatedFSConfig(root=root)
|
|
94
|
+
|
|
95
|
+
else:
|
|
96
|
+
raise ValueError(
|
|
97
|
+
f"Unsupported filesystem type: {type}. Use 'virtual' or 'isolated'."
|
|
98
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Context variables for filesystem isolation.
|
|
2
|
+
|
|
3
|
+
Shared context variables used by patching.py and filesystem implementations
|
|
4
|
+
to coordinate filesystem routing and prevent recursion loops.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import contextvars
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from typing import Any, Iterator
|
|
10
|
+
|
|
11
|
+
# Context variable holding the current filesystem
|
|
12
|
+
current_fs: contextvars.ContextVar[Any] = contextvars.ContextVar(
|
|
13
|
+
"monkeyfs_current_fs", default=None
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@contextmanager
|
|
18
|
+
def suspend() -> Iterator[None]:
|
|
19
|
+
"""Temporarily disable filesystem interception in the current context.
|
|
20
|
+
|
|
21
|
+
Use this when implementing internal filesystem operations (like inside
|
|
22
|
+
IsolatedFS) that need to perform real I/O without triggering the
|
|
23
|
+
patched functions recursively.
|
|
24
|
+
"""
|
|
25
|
+
token = current_fs.set(None)
|
|
26
|
+
try:
|
|
27
|
+
yield
|
|
28
|
+
finally:
|
|
29
|
+
current_fs.reset(token)
|