Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion .agents/skills/usethis-python-test/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: General guidelines for writing tests in the usethis project, includ
compatibility: usethis, Python, pytest
license: MIT
metadata:
version: "1.0"
version: "1.1"
---

# Python Test Guidelines
Expand Down Expand Up @@ -52,3 +52,44 @@ Use the minimum depth needed to clearly communicate the test's context. Avoid ne
### No docstrings on test classes or functions

Test classes and test functions should not have docstrings. The class and function names should be descriptive enough to communicate what is being tested.

## Using `files_manager` in tests

The `files_manager()` context manager defers all configuration file writes until the context exits. This has important implications for how tests are structured, especially when subprocess calls are involved.

### When to exit `files_manager` before a subprocess

Any subprocess that reads configuration files from the filesystem (e.g. `ruff`, `pytest`, `pre-commit`, `deptry`, `codespell`) will **not** see in-memory changes made inside a `files_manager()` context. You must exit the context (flush writes to disk) before running the subprocess.

```python
# Correct: exit files_manager before the subprocess reads config from disk
with change_cwd(tmp_path), files_manager():
use_ruff()

call_uv_subprocess(["run", "ruff", "check", "."], change_toml=False)
```

```python
# Wrong: subprocess runs inside the context, but config hasn't been flushed yet
with change_cwd(tmp_path), files_manager():
use_ruff()
call_uv_subprocess(["run", "ruff", "check", "."], change_toml=False)
# ruff may not see the configuration written by use_ruff()
```

### When it is safe to stay inside the same context

Multiple usethis function calls that operate through `FileManager`-based access (not subprocesses) can safely share a single `files_manager()` context. They see each other's uncommitted in-memory changes via `get()` and `commit()`.

```python
# Safe: both functions use FileManager access, no subprocess involved
with change_cwd(tmp_path), files_manager():
use_ruff()
use_deptry()
# Both tools' config changes are visible to each other in memory
```

### Rule of thumb

- **FileManager-only operations** (e.g. `use_*` functions, `get_deps_from_group`, assertions on config state): safe to combine in one context.
- **Subprocess calls** (e.g. `call_uv_subprocess`, `subprocess.run`, `call_subprocess`): require an atomic write first, so exit the `files_manager` context before running them.
19 changes: 18 additions & 1 deletion src/usethis/_config_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,24 @@

@contextlib.contextmanager
def files_manager() -> Iterator[None]:
"""Context manager that opens all configuration file managers for coordinated I/O."""
"""Context manager that opens all configuration file managers for coordinated I/O.

On entry, this context manager locks every known configuration file manager. Each
manager uses deferred (lazy) reads: a file is only read from disk the first time it
is accessed via `get()`. In-memory changes made with `commit()` are immediately
visible to other operations within the same context, even before they are written to
disk.

On exit, all modified files are written (flushed) to disk atomically. Files that were
only read but never modified are not rewritten.

Because writes are deferred until context exit, any operation that reads configuration
files via the filesystem (e.g. a subprocess such as `ruff`, `pytest`, or
`pre-commit`) will not see in-memory changes until the context manager has exited and
the files have been flushed. If you need to run a subprocess that depends on
configuration written by functions inside this context, exit the context first and
then run the subprocess.
"""
with (
PyprojectTOMLManager(),
SetupCFGManager(),
Expand Down
14 changes: 14 additions & 0 deletions src/usethis/_file/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,20 @@ class FileManager(Generic[DocumentT], metaclass=ABCMeta):
This class implements the Command Pattern, encapsulating file operations. It defers
writing changes to the file until the context is exited, ensuring that file I/O
operations are performed efficiently and only when necessary.

Lifecycle:
1. **Enter** (`__enter__`): The file is locked. No disk I/O occurs yet.
2. **Read** (`get`): The file is lazily read from disk on first access. Subsequent
calls return the in-memory copy.
3. **Write** (`commit`): Changes are stored in memory and the file is marked dirty.
The changes are immediately visible to other code that calls `get()` on the
same manager within the same context, but they are *not* yet on disk.
4. **Exit** (`__exit__`): All dirty files are flushed (written) to disk atomically.

Because writes are deferred, subprocesses that read the managed file from the
filesystem will not see uncommitted in-memory changes. Exit the context manager (or
call `write_file()` explicitly) before invoking a subprocess that depends on the
file's on-disk content.
"""

# https://github.com/python/mypy/issues/5144
Expand Down
Loading