mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-11 17:53:55 +01:00
`requires-python` and the CI pin already said 3.14, but ruff, mypy and pylint were still grading against 3.10 -- three tools judging an interpreter nothing runs any more, and ruff withholding the pyupgrade fixes that only become safe at the newer target. `pin_ci_python.py` rewrites exactly these three knobs. The fixes ruff then had to make are what the stale target was hiding: 18 files on `datetime.timezone.utc` instead of `datetime.UTC` (UP017), plus ruff's stable formatter applying PEP 758 -- `except (A, B):` becomes `except A, B:`. That second one makes the source 3.14-only at the syntax level, which is exactly what `requires-python = ">=3.14"` already promised; kuhy chose it deliberately over holding ruff a release back (2026-08-29). ruff, ruff-format, mypy --strict and pylint all clean at the new target. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SfnJmxxsZ534AKk16rC2Gr
111 lines
4.2 KiB
Python
111 lines
4.2 KiB
Python
"""Make it impossible for a test to write to real user state.
|
|
|
|
``diet_guard/tests/conftest.py`` redirects every known state path into
|
|
``tmp_path``, but that only protects tests that *load that conftest*. On
|
|
2026-07-26 a throwaway test written into a scratch directory ran outside it,
|
|
wrote to the real ``~/.config/diet_guard/sync_token``, and destroyed the
|
|
user's GitHub PAT; the same session also created stray files under
|
|
``~/.local/share/diet_guard/``.
|
|
|
|
A redirect is a convention -- something a new test can forget or sidestep.
|
|
This is a guard: while pytest is in the process, any ``pathlib.Path`` write
|
|
under the real diet_guard data or config directory raises immediately,
|
|
whatever conftest is or is not in play, and whatever module forgot to be
|
|
patched. Reads are left alone, so a test may still assert that a real path
|
|
is absent.
|
|
|
|
Scope, honestly: it covers ``pathlib.Path`` -- which is what every diet_guard
|
|
writer uses -- not builtin ``open()``, ``os.replace`` or ``shutil``. It is a
|
|
hardened barrier across the paths this codebase actually takes, not a
|
|
kernel-level sandbox.
|
|
|
|
Inert outside pytest: production never imports pytest, so the single
|
|
``sys.modules`` check below is the whole cost.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pathlib
|
|
import sys
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Callable
|
|
|
|
|
|
def _resolved(path: pathlib.Path) -> pathlib.Path:
|
|
"""Resolve ``path`` as far as the filesystem allows, never raising."""
|
|
try:
|
|
return path.expanduser().resolve()
|
|
except OSError, RuntimeError, ValueError:
|
|
return path
|
|
|
|
|
|
# Resolved once, from the real home -- deliberately NOT from _constants, whose
|
|
# values a test may legitimately have patched to a temp path. Resolving here
|
|
# matters: the candidate paths are resolved too, so if $HOME (or .local, or
|
|
# .local/share) is a symlink an unresolved root would never match and the
|
|
# guard would silently fail *open* -- the worst possible failure for a safety
|
|
# net.
|
|
_PROTECTED = (
|
|
_resolved(pathlib.Path.home() / ".local" / "share" / "diet_guard"),
|
|
_resolved(pathlib.Path.home() / ".config" / "diet_guard"),
|
|
)
|
|
|
|
|
|
class RealUserStateWriteError(RuntimeError):
|
|
"""Raised when a test tries to write to the user's real diet_guard state."""
|
|
|
|
def __init__(self, path: pathlib.Path) -> None:
|
|
"""Name the offending path and how to fix the test."""
|
|
super().__init__(
|
|
f"test tried to write real user state at {path}. Point the relevant "
|
|
"path at tmp_path (see diet_guard/tests/conftest.py's "
|
|
"_isolate_state) instead of writing the real file.",
|
|
)
|
|
|
|
|
|
def _is_protected(path: pathlib.Path) -> bool:
|
|
"""Return True if ``path`` lies inside a real user-state directory."""
|
|
resolved = _resolved(pathlib.Path(path))
|
|
return any(resolved.is_relative_to(root) for root in _PROTECTED)
|
|
|
|
|
|
# Marks an already-wrapped method, so installing twice is a no-op without
|
|
# needing module-level mutable state.
|
|
_MARKER = "_diet_guard_write_guarded"
|
|
|
|
|
|
def _guard(name: str, is_write: Callable[..., bool]) -> None:
|
|
"""Wrap ``pathlib.Path.<name>`` so protected writes raise.
|
|
|
|
Idempotent per method: a second call finds the marker and returns, so the
|
|
wrapper can never be stacked on itself.
|
|
"""
|
|
original = getattr(pathlib.Path, name)
|
|
if getattr(original, _MARKER, False):
|
|
return
|
|
|
|
def wrapper(self: pathlib.Path, *args: object, **kwargs: object) -> object:
|
|
if is_write(*args, **kwargs) and _is_protected(self):
|
|
raise RealUserStateWriteError(self)
|
|
return original(self, *args, **kwargs)
|
|
|
|
wrapper.__name__ = name
|
|
setattr(wrapper, _MARKER, True)
|
|
setattr(pathlib.Path, name, wrapper)
|
|
|
|
|
|
def install() -> None:
|
|
"""Install the guard, but only when running under pytest.
|
|
|
|
Safe to call repeatedly: each :func:`_guard` is individually idempotent.
|
|
"""
|
|
if "pytest" not in sys.modules:
|
|
return
|
|
_guard("open", lambda mode="r", *_a, **_k: any(c in mode for c in "wxa+"))
|
|
# ``mkdir`` is guarded too: creating the real state directory is half of
|
|
# what the 2026-07-26 accident did, and every writer calls it first.
|
|
for name in ("write_text", "write_bytes", "unlink", "rename", "replace", "mkdir"):
|
|
_guard(name, lambda *_a, **_k: True)
|