filestate
Version: 17.4.0
filestate declares a desired filesystem state — files, directories, permissions, ownership, and content — as a YAML configuration, then reconciles the disk to match it. It works through a provider-based architecture of options (desired properties such as name casing, line endings, or YAML key order) and operations (the writes that close the gap when a property is not already met). Python developers who need to enforce consistent file structure across a codebase, or who manage file trees programmatically, are its primary audience.
Table of Contents
- Installation
- Quickstart
- Tests
- Architecture
- Integration in the Suite
- Dependencies
- Versioning & Compatibility Policy
- License
- About us
- Known Limitations & Roadmap
- Status & Compatibility
- Useful Links
- Migration Notes
Installation
pip install wexample-filestate
Requires Python >=3.10.
Quickstart
Install the package:
pip install wexample-filestate
The example below declares that /tmp/demo/hello.txt must exist and contain Hello, world!. apply() creates the file if it is absent and writes the content if it differs:
from wexample_filestate.utils.file_state_manager import FileStateManager
from wexample_filestate.const.disk import DiskItemType
from wexample_prompt.common.io_manager import IoManager
manager = FileStateManager.create_from_path(path="/tmp/demo", io=IoManager())
manager.configure({
"children": [
{
"name": "hello.txt",
"type": DiskItemType.FILE,
"should_exist": True,
"content": "Hello, world!",
}
]
})
result = manager.apply()
apply() returns a FileStateResult. Its .operations list contains one entry per change made on disk — an empty list means the file already matched the declared state and nothing was touched.
To preview changes without writing anything, call dry_run() instead:
from wexample_filestate.enum.scopes import Scope
result = manager.dry_run(scopes=set(Scope))
print(len(result.operations), "operation(s) pending")
Tests
This project uses pytest for testing and pytest-cov for code coverage analysis.
Installation
First, install the required testing dependencies:
.venv/bin/python -m pip install pytest pytest-cov
Basic Usage
Run all tests with coverage:
.venv/bin/python -m pytest --cov --cov-report=html
Common Commands
# Run tests with coverage for a specific module
.venv/bin/python -m pytest --cov=your_module
# Show which lines are not covered
.venv/bin/python -m pytest --cov=your_module --cov-report=term-missing
# Generate an HTML coverage report
.venv/bin/python -m pytest --cov=your_module --cov-report=html
# Combine terminal and HTML reports
.venv/bin/python -m pytest --cov=your_module --cov-report=term-missing --cov-report=html
# Run specific test file with coverage
.venv/bin/python -m pytest tests/test_file.py --cov=your_module --cov-report=term-missing
Viewing HTML Reports
After generating an HTML report, open htmlcov/index.html in your browser to view detailed line-by-line coverage information.
Coverage Threshold
To enforce a minimum coverage percentage:
.venv/bin/python -m pytest --cov=your_module --cov-fail-under=80
This will cause the test suite to fail if coverage drops below 80%.
Architecture
filestate enforces a declared filesystem state against the real disk. The caller describes what files and directories should look like; the library compares that description to what is on disk, plans the mutations needed, and applies them — or simulates them in a dry run. Every concept that appears in configuration maps to a concrete Python class.
Entry point
src/wexample_filestate/utils/file_state_manager.py — FileStateManager is the public surface. It is a thin subclass of ItemTargetDirectory and adds nothing beyond a configure() override that optionally forces eager tree materialisation. Callers construct it, call configure(dict) or configure_from_file(path), then call apply() or dry_run().
Items
The item layer models the desired state as a tree.
Target items — what the caller declares:
- src/wexample_filestate/item/abstract_item_target.py —
AbstractItemTarget. Holdsbase_path/base_name, a reference to the source snapshot, the options dict, and the operations history stack. Implementsapply(),build_operations(),dry_run(), androllback(). - src/wexample_filestate/item/item_target_directory.py —
ItemTargetDirectory. Extends the base with lazy child materialisation (_tree_builtflag).get_children_list()callsbuild_item_tree()on first access; deeper levels are built lazily in turn.build_operations()recurses into children. - src/wexample_filestate/item/item_target_file.py —
ItemTargetFile. Leaf item; adds no behaviour beyond the mixin it carries.
Source items — read-only snapshot of the current disk state:
- src/wexample_filestate/item/abstract_item_source.py —
AbstractItemSource. Base class carryingWithPathMixin. - src/wexample_filestate/item/item_source_file.py, src/wexample_filestate/item/item_source_directory.py — concrete variants.
When AbstractItemTarget.configure() runs, it calls locate_source(path), which inspects the real filesystem and attaches an ItemSourceFile or ItemSourceDirectory (or leaves source = None when the path does not yet exist). Operations read the source to know the current state before writing the target.
Both hierarchies share ItemMixin (src/wexample_filestate/item/mixin/item_mixin.py), which owns base_name, base_path, and abstract is_file() / is_directory().
Options
Options declare what a target should look like. Each option is a config-option class that attaches to a target item and can examine the current state and create an operation when the state is wrong.
The contract lives in src/wexample_filestate/option/mixin/option_mixin.py:
create_required_operation(target, scopes) → AbstractOperation | None— compare desired to actual; return an operation if a change is needed,Noneif satisfied.prepare(root, scopes, filter_paths)— called once per unique option type before the scan; used for expensive upfront work (batch tool runs, cache warm-up).applicable_on_file(),applicable_on_directory(),applicable_on_missing()— guard methods controlling when the option is even considered.
All built-in options are registered in src/wexample_filestate/options_provider/default_options_provider.py. The canonical list (alphabetical):
| Key | Class | Scope |
|---|---|---|
active |
ActiveOption |
— |
children |
ChildrenOption |
LOCATION |
children_file_factory |
ChildrenFileFactoryOption |
LOCATION |
class |
ClassOption |
— |
content |
ContentOption |
CONTENT |
default_content |
DefaultContentOption |
CONTENT |
mode |
ModeOption |
PERMISSIONS, OWNERSHIP |
name |
NameOption |
NAME |
on_bad_format |
OnBadFormatOption |
NAME |
should_contain_lines |
ShouldContainLinesOption |
CONTENT |
should_exist |
ShouldExistOption |
LOCATION |
should_have_extension |
ShouldHaveExtensionOption |
NAME |
should_not_contain_lines |
ShouldNotContainLinesOption |
CONTENT |
sidecar_of |
SidecarOfOption |
LOCATION |
structured_keys |
StructuredKeysOption |
CONTENT |
text |
TextOption |
CONTENT |
type |
TypeOption |
— |
yaml |
YamlOption |
CONTENT |
Notable options in more detail:
- src/wexample_filestate/option/should_exist_option.py —
Truecreates a missing item;Falseremoves an existing one; omitting the key enforces nothing. - src/wexample_filestate/option/content_option.py — sets the full file content; accepts a string, a
ConfigValue, or a callable receiving the target. - src/wexample_filestate/option/text_option.py — nested option composing
TrimOption,EndNewLineOption,SortLinesOption,UniqueLinesOption; accepts a list shorthand (["trim", "sort_lines"]) or a dict. - src/wexample_filestate/option/yaml_option.py — YAML-specific transforms (currently
sort_recursive); never applies to directories or missing files. - src/wexample_filestate/option/name_option.py — nested option composing
ValueOption,CaseFormatOption,RegexOption,PrefixOption,SuffixOption,OnBadFormatOption; accepts a string, aPath, a dict, or a callable. - src/wexample_filestate/option/children_option.py — list of child item configs; each entry is resolved by
create_child_item(), which picksItemTargetFileorItemTargetDirectoryby thetypekey or by inspecting the real filesystem.
Scopes
src/wexample_filestate/enum/scopes.py defines Scope: CONTENT, LOCATION, NAME, OWNERSHIP, PERMISSIONS, REMOTE, TIMESTAMPS. Every option and operation declares which scopes it belongs to by overriding get_scopes() from src/wexample_filestate/common/mixin/with_scope_mixin.py.
matches_scope_filter(scopes) returns True when the declared scopes intersect the requested set. apply() and dry_run() accept an optional scopes argument; an unqualified call uses Scope.default() (all scopes). Restricting scopes skips prepare() and create_required_operation() on options outside the requested scopes, making partial passes (e.g., content-only) cheap.
Operations
Operations are the mutations. Each one is created by an option, holds enough state to undo itself, and implements two abstract methods from src/wexample_filestate/operation/abstract_operation.py:
apply_operation()— perform the disk change.undo()— reverse it.
src/wexample_filestate/operation/abstract_file_manipulation_operation.py — intermediate base for operations that touch file content or mode; provides _backup_target_file() and _restore_target_file() so undo() can restore the original bytes and permissions.
Concrete operations shipped by the library:
| Class | Scope | What it does |
|---|---|---|
FileCreateOperation |
LOCATION | touch() or makedirs(); writes default_content if provided |
FileRemoveOperation |
LOCATION | remove() or shutil.rmtree() |
FileRenameOperation |
LOCATION | rename on disk, then updates base_name and invalidates cached paths |
FileWriteOperation |
CONTENT | writes a string to a file via LocalFile.write() |
FileChangeModeOperation |
PERMISSIONS, OWNERSHIP | chmod (and optionally chown) in one pass; chowns first to acquire ownership before chmod |
FileChownOperation |
OWNERSHIP | sudo chown only, without touching mode |
Each operation carries target (the AbstractItemTarget it acts on), option (the option that created it), description, and applied. applied is set to True by the result after execution; applied operations are pushed to AbstractItemTarget.operations_history for sequential rollbacks.
Results
src/wexample_filestate/result/abstract_result.py — AbstractResult holds operations: list[AbstractOperation] and drives apply_operations(). The loop deduplicates by object identity, respects rollback (reverses order and calls undo() instead of apply_operation()), and delegates the actual execution to _apply_single_operation().
Two concrete subclasses:
- src/wexample_filestate/result/file_state_result.py —
FileStateResult. Callsoperation.apply_operation()directly; in interactive mode, prompts for confirmation first and dispatchesoperation.<name>.pre/operation.<name>.postevents around the call. - src/wexample_filestate/result/file_state_dry_run_result.py —
FileStateDryRunResult._apply_single_operation()returnsTruewithout touching disk.
Call path through apply()
- Caller invokes
FileStateManager.apply(scopes, filter_paths, interactive). AbstractItemTarget.apply()creates aFileStateResultand calls_prepare_options(scopes, filter_paths). This walks the entire tree, collects one instance of each unique option type, and callsoption.prepare()on it — batch tools run here, with visible log output, before the scan begins.build_operations(result, scopes, ...)is called on the root. For a directory it recurses: for each item it calls_find_first_operation(scopes, filter_operation)._find_first_operation()iteratesself.options.values()in registration order and callstry_create_operation_from_option(option, scopes)on each. That method checksmatches_scope_filter,applicable_on_file/directory/missing, then callsoption.create_required_operation(target, scopes). The first non-Noneresult is returned immediately; one operation per item per pass.- Each operation is appended to
result.operations. - After the tree walk,
result.apply_operations(interactive)iterates the list and calls_apply_single_operation()on each. - Applied operations are collected into a batch and pushed to
self.operations_history. A subsequentrollback()call pops the last batch and applies each operation'sundo()in reverse order.
Integration in the Suite
This package is part of the Wexample Suite — a collection of high-quality, modular tools designed to work seamlessly together across multiple languages and environments.
Related Packages
The suite includes packages for configuration management, file handling, prompts, and more. Each package can be used independently or as part of the integrated suite.
Visit the Wexample Suite documentation for the complete package ecosystem.
Dependencies
- attrs: >=23.1.0
- cattrs: >=23.1.0
- jinja2:
- python-dotenv:
- ruamel-yaml:
- tomlkit:
- wexample-config: >=7.2.0
- wexample-event: >=8.0.0
- wexample-file: >=9.2.0
- wexample-helpers-yaml: >=7.0.0
- wexample-prompt: >=16.0.0
- wexample-runner: >=9.5.0
- xmltodict:
Versioning & Compatibility Policy
Wexample packages follow Semantic Versioning (SemVer):
- MAJOR: Breaking changes
- MINOR: New features, backward compatible
- PATCH: Bug fixes, backward compatible
We maintain backward compatibility within major versions and provide clear migration guides for breaking changes.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Free to use in both personal and commercial projects.
About us
Wexample stands as a cornerstone of the digital ecosystem — a collective of seasoned engineers, researchers, and creators driven by a relentless pursuit of technological excellence. More than a media platform, it has grown into a vibrant community where innovation meets craftsmanship, and where every line of code reflects a commitment to clarity, durability, and shared intelligence.
This packages suite embodies this spirit. Trusted by professionals and enthusiasts alike, it delivers a consistent, high-quality foundation for modern development — open, elegant, and battle-tested. Its reputation is built on years of collaboration, refinement, and rigorous attention to detail, making it a natural choice for those who demand both robustness and beauty in their tools.
Wexample cultivates a culture of mastery. Each package, each contribution carries the mark of a community that values precision, ethics, and innovation — a community proud to shape the future of digital craftsmanship.
Known Limitations & Roadmap
Current limitations and planned features are tracked in the GitHub issues.
See the project roadmap for upcoming features and improvements.
Status & Compatibility
Maturity: Production-ready
Python Support: >=3.10
OS Support: Linux, macOS, Windows
Status: Actively maintained
Useful Links
- Homepage: https://github.com/wexample/python-filestate
- Documentation: docs.wexample.com
- Issue Tracker: https://github.com/wexample/python-filestate/issues
- Discussions: https://github.com/wexample/python-filestate/discussions
- PyPI: pypi.org/project/wexample-filestate
Migration Notes
When upgrading between major versions, refer to the migration guides in the documentation.
Breaking changes are clearly documented with upgrade paths and examples.
Metadata
Release files for wexample-filestate 17.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| wexample_filestate-17.4.0.tar.gz | 132.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| wexample_filestate-17.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 280.1 kB
Release files / wexample_filestate-17.4.0.tar.gz
| Download URL | wexample_filestate-17.4.0.tar.gz |
|---|---|
| Size | 132.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2796d6af0cba72636a9db0f3a0403331489690ca42b84a5001e3450b8b0b7160
|
|
BLAKE2b-256 checksum How to use checksums |
026474e2d503e603b72a1fadabcac51ef37a9f5ba1f32cf3ef2d5a5726e5afbb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
pdm/2.25.9 CPython/3.12.3 Linux/6.8.0-139-generic
|
Release files / wexample_filestate-17.4.0-py3-none-any.whl
| Download URL | wexample_filestate-17.4.0-py3-none-any.whl |
|---|---|
| Size | 147.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
480842a6743e52eaa2da0e8d12a2afbec61b75f4ac9c1275836dfeb460499d22
|
|
BLAKE2b-256 checksum How to use checksums |
3120e0864655d61c7265aae57df61eca1968e041965acb57f5eaa65499047091
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
pdm/2.25.9 CPython/3.12.3 Linux/6.8.0-139-generic
|