Skip to content

Commit 6eeeea5

Browse files
Initial commit: D-MemFS v0.2.0
0 parents  commit 6eeeea5

81 files changed

Lines changed: 14919 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* text=auto

.github/workflows/test.yml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
test:
11+
runs-on: ${{ matrix.os }}
12+
strategy:
13+
fail-fast: false
14+
matrix:
15+
os: [ubuntu-latest, windows-latest, macos-latest]
16+
python-version: ["3.11", "3.12", "3.13"]
17+
18+
steps:
19+
- uses: actions/checkout@v4
20+
- name: Install uv
21+
uses: astral-sh/setup-uv@v5
22+
- name: Set up Python ${{ matrix.python-version }}
23+
uses: actions/setup-python@v5
24+
with:
25+
python-version: ${{ matrix.python-version }}
26+
- name: Compile requirements
27+
run: uv pip compile requirements.in -o requirements.txt
28+
- name: Run tests with coverage
29+
run: uvx --with-requirements requirements.txt --with-editable . pytest tests/ -v --timeout=30 --cov=dmemfs --cov-report=xml --cov-report=term-missing
30+
- name: Upload coverage artifact
31+
if: always()
32+
uses: actions/upload-artifact@v4
33+
with:
34+
name: coverage-${{ matrix.os }}-py${{ matrix.python-version }}
35+
path: coverage.xml
36+
- name: Upload coverage to Codecov
37+
if: ${{ vars.ENABLE_CODECOV == 'true' }}
38+
uses: codecov/codecov-action@v4
39+
with:
40+
files: coverage.xml
41+
fail_ci_if_error: true
42+
- name: Generate API docs (pdoc)
43+
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11'
44+
run: uvx --with-requirements requirements.txt pdoc dmemfs -o docs/api
45+
- name: Upload API docs artifact
46+
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11'
47+
uses: actions/upload-artifact@v4
48+
with:
49+
name: api-docs-pdoc
50+
path: docs/api

.gitignore

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*.pyo
5+
*.pyd
6+
*.so
7+
8+
# Distribution / packaging
9+
dist/
10+
build/
11+
*.egg-info/
12+
*.egg
13+
MANIFEST
14+
15+
# Virtual environments
16+
.venv/
17+
venv/
18+
env/
19+
20+
# Testing
21+
.pytest_cache/
22+
.hypothesis/
23+
.coverage
24+
coverage.xml
25+
htmlcov/
26+
*.lcov
27+
28+
# Type checking
29+
.mypy_cache/
30+
.pytype/
31+
32+
# Linters
33+
.ruff_cache/
34+
35+
# IDEs
36+
.idea/
37+
.vscode/
38+
*.swp
39+
*.swo
40+
*.code-workspace
41+
42+
# OS
43+
.DS_Store
44+
Thumbs.db
45+
ConsoleHost_history.txt
46+
47+
# Lock files (library, not app)
48+
uv.lock
49+
50+
# Generated API docs (HTML)
51+
docs/api/
52+
53+
# Non-public directories
54+
plan/
55+
Article/
56+
57+
# Backup files
58+
*.bak

BENCHMARK.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Benchmark Guide
2+
3+
This repository includes a minimal benchmark script to compare:
4+
5+
- `MemoryFileSystem` (MFS)
6+
- `io.BytesIO` (single stream or dict of streams)
7+
- `PyFilesystem2` (`fs.memoryfs.MemoryFS`)
8+
- `tempfile`-backed real filesystem I/O
9+
10+
> **Note on PyFilesystem2:** As of setuptools 82 (February 2026), `pyfilesystem2` fails to import due to its use of the deprecated `pkg_resources.declare_namespace()` API ([issue #597](https://github.com/PyFilesystem/pyfilesystem2/issues/597)). The project appears unmaintained and the fix has not been released. Benchmark results that include `PyFilesystem2` were measured in an environment with setuptools ≤ 81 and remain valid as historical comparison data, but the benchmark script will skip or error on PyFilesystem2 cases in current environments.
11+
12+
## Run
13+
14+
```bash
15+
uvx --with-requirements requirements.txt --with-editable . python benchmarks/compare_backends.py
16+
```
17+
18+
## Common options
19+
20+
```bash
21+
# More stable numbers
22+
uvx --with-requirements requirements.txt --with-editable . python benchmarks/compare_backends.py --repeat 10 --warmup 2
23+
24+
# Heavier workload
25+
uvx --with-requirements requirements.txt --with-editable . python benchmarks/compare_backends.py --small-files 1000 --stream-size-mb 64
26+
27+
# JSON output
28+
uvx --with-requirements requirements.txt --with-editable . python benchmarks/compare_backends.py --json
29+
30+
# Save reports into benchmarks/results/
31+
uvx --with-requirements requirements.txt --with-editable . python benchmarks/compare_backends.py --save-md auto --save-json auto
32+
```
33+
34+
When markdown/json reports are generated, the script also updates:
35+
36+
- `benchmarks/results/benchmark_current_result.md`
37+
- `benchmarks/results/benchmark_current_result.json`
38+
39+
You can link `benchmark_current_result.md` from README as the latest snapshot.
40+
41+
## Cases
42+
43+
- `small_files_rw`: write/read many small files
44+
- `stream_write_read`: write/read one larger stream in chunks
45+
46+
Saved reports are written under `benchmarks/results/` when `--save-md auto` or `--save-json auto` is used.
47+
48+
## Notes
49+
50+
- `tracemalloc` reports Python-heap allocations; OS page cache and kernel-level effects are not fully represented.
51+
- `tempfile` results vary by OS, filesystem, and disk state. The included benchmark results were measured with the system `%TEMP%` directory located on a RAM disk. On a physical (SSD/HDD) disk, `tempfile` numbers will be significantly slower.
52+
- For fair comparisons, run on an idle machine and repeat multiple times.

CHANGELOG.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [Unreleased]
9+
10+
### Added
11+
- `MemoryFileSystem.is_file(path)` for API symmetry with `exists()` / `is_dir()`
12+
- `MemoryFileHandle` file-like methods: `truncate()`, `flush()`, `readable()`, `writable()`, `seekable()`
13+
- Async counterparts for new file-like methods and `AsyncMemoryFileSystem.is_file()`
14+
- `IMemoryFile._bulk_load(data)` method for internal use by `import_tree()` / `_deep_copy_subtree()` (improves encapsulation)
15+
16+
### Changed
17+
- **[BREAKING]** `MFSStatResult.is_sequential` field removed. This internal implementation detail is no longer exposed in the public API.
18+
- **[BREAKING]** Package renamed from `memory-file-system` / `memory_file_system` to `D-MemFS` / `dmemfs`. Update imports: `from dmemfs import ...`
19+
- `stat()` directory support: previously raised `IsADirectoryError`; behavior unchanged in this release, but `is_dir` field added to `MFSStatResult` in future
20+
- `export_as_bytesio()` TOCTOU gap fixed: `_rw_lock.acquire_read()` now called inside `_global_lock` block
21+
- `exists()` / `is_dir()` now resolve paths under `_global_lock` for consistent thread-safety policy
22+
- `open(..., preallocate=...)` now performs preallocation under `_global_lock`
23+
- `import_tree()` quota accounting simplified to net-delta apply after successful write phase
24+
- `import_tree()` rollback now also cleans up auto-created parent directories
25+
- CI workflow now runs tests with coverage (`--cov`, `coverage.xml`) and uploads coverage artifact
26+
27+
### Documentation
28+
- README / README_en updated with lock behavior notes (`_global_lock` hold, writer starvation)
29+
- README / README_en API tables updated for `is_file()` and new handle methods
30+
- Clarified that `stat()` is file-only and that `MFSStatResult.is_sequential` is implementation detail
31+
32+
### Tests
33+
- Added tests for `is_file()`, handle file-like methods, async wrappers, and `import_tree()` parent-dir rollback
34+
35+
## [0.2.0] - 2026-02-26
36+
37+
### Added
38+
- **Directory Index Layer**: Internal refactoring from flat `dict[str, IMemoryFile]` to `DirNode`/`FileNode` tree structure
39+
- `listdir()` complexity improved from O(N total entries) to O(children count)
40+
- `rename()` / `rmtree()` no longer require prefix scanning
41+
- **`move(src, dst)`**: Move files/directories with automatic parent directory creation
42+
- **`copy_tree(src, dst)`**: Deep copy of directory subtrees with quota pre-check
43+
- **`stat(path)`**: Return `MFSStatResult` with size, timestamps, generation, storage type
44+
- **`glob("**")`**: Recursive glob pattern matching with `**` support
45+
- **File timestamps**: `created_at` / `modified_at` on `FileNode`, updated on write/truncate
46+
- **`MFSStatResult` TypedDict**: New typed return value for `stat()`
47+
- **`bytearray` shrink**: `RandomAccessMemoryFile.truncate()` reallocates buffer when new size ≤ 25% of old capacity
48+
- **`AsyncMemoryFileSystem` / `AsyncMemoryFileHandle`**: Async wrappers via `asyncio.to_thread()`
49+
- **`pytest-asyncio`** added to test dependencies
50+
51+
### Changed
52+
- `IMemoryFile` is now pure data storage; `is_dir`, `generation`, `_rw_lock` moved to `DirNode`/`FileNode`
53+
- `wb` mode: truncate now executes **after** write-lock acquisition (prevents data corruption during concurrent reads)
54+
- `export_as_bytesio()`: entry lookup now protected by `_global_lock` (prevents race with concurrent `remove()`)
55+
- `MemoryFileHandle.__del__`: `stacklevel` changed from 2 to 1 for correct warning source location
56+
- `__version__` bumped to `0.2.0`
57+
58+
### Tests
59+
- 288 tests (was 230): +58 new tests including `test_fs_coverage.py`
60+
- Coverage target documented at 99% (claim) with CI-side coverage XML generation in place
61+
- New test files: `test_timestamp.py`, `test_async.py`
62+
- Extended: `test_rename_move.py` (+13), `test_files_randomaccess.py` (+5), `test_concurrency.py` (+2), `test_open_modes.py` (+2), `test_export_import.py` (+1), `test_mkdir_listdir.py` (+2)
63+
64+
## [0.1.0] - 2026-02-23
65+
66+
### Added
67+
- Initial release of MemoryFileSystem (MFS)
68+
- In-process virtual filesystem with hard quota management
69+
- `MemoryFileSystem` class with full POSIX-like file operations:
70+
- `open()` supporting modes `rb`, `wb`, `ab`, `r+b`, `xb` with optional preallocate
71+
- `mkdir()`, `rename()`, `remove()`, `rmtree()`, `listdir()`, `exists()`, `is_dir()`
72+
- `stats()` for quota and filesystem metrics
73+
- `export_as_bytesio()`, `export_tree()`, `iter_export_tree()` with `only_dirty` support
74+
- `import_tree()` with All-or-Nothing atomicity and rollback
75+
- `MemoryFileHandle` with full `io.RawIOBase`-compatible interface (read, write, seek, tell, truncate)
76+
- `MFSQuotaExceededError` — hard quota enforcement before any write
77+
- `ReadWriteLock` with configurable timeout — concurrent reads, exclusive writes
78+
- Auto-promotion from `SequentialMemoryFile` to `RandomAccessMemoryFile` on random-access writes
79+
- PEP 561 `py.typed` marker — full type annotation support
80+
- Zero external dependencies (Python 3.11+ standard library only)
81+
- 163 tests across Unit / Integration / Scenario / Property (Hypothesis) layers
82+
- CI matrix: 3 OS × 3 Python versions (3.11, 3.12, 3.13)

CONTRIBUTING.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Contributing to D-MemFS
2+
3+
Thank you for your interest in contributing! This document explains how to get involved.
4+
5+
## Getting Started
6+
7+
1. **Fork** the repository on GitHub.
8+
2. **Clone** your fork locally:
9+
```bash
10+
git clone https://github.com/<your-username>/D-MemFS.git
11+
cd D-MemFS
12+
```
13+
3. **Install dependencies** using [uv](https://github.com/astral-sh/uv):
14+
```bash
15+
uv pip compile requirements.in -o requirements.txt
16+
```
17+
4. **Run the test suite** to confirm everything passes:
18+
```bash
19+
uvx --with-requirements requirements.txt --with-editable . pytest tests/ -v --timeout=30
20+
```
21+
22+
## Development Workflow
23+
24+
- Create a **feature branch**: `git checkout -b feat/my-feature`
25+
- Write tests **before** or **alongside** code changes.
26+
- All tests must pass before submitting a PR.
27+
- Keep commits focused and use descriptive commit messages.
28+
29+
## Code Style
30+
31+
- Follow existing patterns — no additional linting tools are required.
32+
- Public APIs must have type annotations.
33+
- New public methods must be documented in the docstring and, if applicable, reflected in `README.md` / `README_ja.md`.
34+
35+
## Submitting a Pull Request
36+
37+
1. Push your branch to your fork.
38+
2. Open a Pull Request against the `main` branch.
39+
3. Describe **what** you changed and **why**.
40+
4. Link any related issues.
41+
42+
## Reporting Bugs
43+
44+
Please open a GitHub Issue with:
45+
- A minimal reproducible example
46+
- Python version and OS
47+
- Expected vs. actual behaviour
48+
49+
## Design Principles
50+
51+
Before proposing new features, please review the [Non-Goals](README.md) section.
52+
MFS intentionally avoids `os.PathLike` compatibility and does not aim to replace `os` or `pathlib`.

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 D
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.

0 commit comments

Comments
 (0)