Metadata-Version: 2.1
Name: wexample-filestate
Version: 17.0.0
Summary: Declares a desired filesystem state — files, directories, permissions, ownership, and content — via YAML config, then reconciles the disk to match it
Author-Email: weeger <contact@wexample.com>
License: MIT
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Project-URL: homepage, https://github.com/wexample/python-filestate
Requires-Python: >=3.10
Requires-Dist: attrs>=23.1.0
Requires-Dist: cattrs>=23.1.0
Requires-Dist: jinja2
Requires-Dist: python-dotenv
Requires-Dist: ruamel.yaml
Requires-Dist: tomlkit
Requires-Dist: wexample-config>=7.2.0
Requires-Dist: wexample-event>=8.0.0
Requires-Dist: wexample-file>=9.1.0
Requires-Dist: wexample-helpers-yaml>=7.0.0
Requires-Dist: wexample-prompt>=15.0.0
Requires-Dist: wexample-runner>=9.3.0
Requires-Dist: xmltodict
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-benchmark>=5.2.3; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Description-Content-Type: text/markdown

# filestate

Version: 17.0.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](#installation)
- [Quickstart](#quickstart)
- [Tests](#tests)
- [Architecture](#architecture)
- [Integration in the Suite](#integration-in-the-suite)
- [Dependencies](#dependencies)
- [Versioning & Compatibility Policy](#versioning--compatibility-policy)
- [License](#license)
- [About us](#about-us)
- [Known Limitations & Roadmap](#known-limitations--roadmap)
- [Status & Compatibility](#status--compatibility)
- [Useful Links](#useful-links)
- [Migration Notes](#migration-notes)

## Installation

```bash
pip install wexample-filestate
```

Requires Python >=3.10.

## Quickstart

Install the package:

```bash
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:

```python
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:

```python
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:
```bash
.venv/bin/python -m pip install pytest pytest-cov
```

### Basic Usage

Run all tests with coverage:
```bash
.venv/bin/python -m pytest --cov --cov-report=html
```

### Common Commands
```bash
# 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:
```bash
.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`. Holds `base_path`/`base_name`, a reference to the source snapshot, the options dict, and the operations history stack. Implements `apply()`, `build_operations()`, `dry_run()`, and `rollback()`.
- src/wexample_filestate/item/item_target_directory.py — `ItemTargetDirectory`. Extends the base with lazy child materialisation (`_tree_built` flag). `get_children_list()` calls `build_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 carrying `WithPathMixin`.
- 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, `None` if 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 — `True` creates a missing item; `False` removes 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, a `Path`, 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 picks `ItemTargetFile` or `ItemTargetDirectory` by the `type` key 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`. Calls `operation.apply_operation()` directly; in interactive mode, prompts for confirmation first and dispatches `operation.<name>.pre` / `operation.<name>.post` events around the call.
- src/wexample_filestate/result/file_state_dry_run_result.py — `FileStateDryRunResult`. `_apply_single_operation()` returns `True` without touching disk.

### Call path through `apply()`

1. Caller invokes `FileStateManager.apply(scopes, filter_paths, interactive)`.
2. `AbstractItemTarget.apply()` creates a `FileStateResult` and calls `_prepare_options(scopes, filter_paths)`. This walks the entire tree, collects one instance of each unique option type, and calls `option.prepare()` on it — batch tools run here, with visible log output, before the scan begins.
3. `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)`.
4. `_find_first_operation()` iterates `self.options.values()` in registration order and calls `try_create_operation_from_option(option, scopes)` on each. That method checks `matches_scope_filter`, `applicable_on_file/directory/missing`, then calls `option.create_required_operation(target, scopes)`. The first non-`None` result is returned immediately; one operation per item per pass.
5. Each operation is appended to `result.operations`.
6. After the tree walk, `result.apply_operations(interactive)` iterates the list and calls `_apply_single_operation()` on each.
7. Applied operations are collected into a batch and pushed to `self.operations_history`. A subsequent `rollback()` call pops the last batch and applies each operation's `undo()` 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](https://docs.wexample.com) 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.1.0
- wexample-helpers-yaml: >=7.0.0
- wexample-prompt: >=15.0.0
- wexample-runner: >=9.3.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](LICENSE) file for details.

Free to use in both personal and commercial projects.

## About us

[Wexample](https://wexample.com) 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](https://github.com/wexample/python-filestate/issues) 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](https://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](https://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.
