Глава 23 · Часть IV · Реализуем SafeSort
От анализа к плану действий
План перемещений — обычные данные: его можно вывести на экран, проверить или отбросить, не тронув ни одного файла.
SafeSort · Часть 4 из 6Реализация
Add safe organization plan
Area: SafetyPriority: High
git switch -c feat/plan-and-collisions
У SafeSort есть список найденных файлов и правило классификации — но само по себе это ещё не план действий. План — список конкретных перемещений: откуда и куда переместится каждый файл, если пользователь подтвердит выполнение.
Только читают
scan / plan / duplicates только читают
Меняют файлы
apply сортирует; undo восстанавливает
src/safesort/models.py
@dataclass(frozen=True)
class MoveOperation:
source: Path
destination: Path
@dataclass(frozen=True)
class SortPlan:
root: Path
operations: tuple[MoveOperation, ...]
План — это данные, а не действие
SortPlan не содержит ни одного вызова, который перемещает файлы, — только описание того, что можно было бы сделать. Эта мысль — ключевая для всей архитектуры SafeSort: пока объект остаётся данными, его можно вывести на экран, проверить тестами, сохранить или просто отбросить, не рискуя ни одним файлом пользователя.Функция build_plan() строит план по списку файлов от
сканера, не трогая диск ни разу, кроме одной read-only проверки — существует ли уже файл
с таким именем в месте назначения. Что делать, если да, — тема следующего этапа.
src/safesort/planner.py
def build_plan(files: list[FileInfo], root: Path, config: Config) -> SortPlan:
dest_root = Path(root) / config.destination
operations = []
for file in files:
category = classify(file.extension, config.extensions)
dest_dir = dest_root / category
candidate = dest_dir / file.path.name
destination = _resolve_collision(candidate, reserved)
operations.append(MoveOperation(source=file.path, destination=destination))
return SortPlan(root=root, operations=tuple(operations))
Тест-чекпойнт: чистое планирование
tests/test_planner.py
def test_build_plan_describes_move_without_executing_it(tmp_path):
source = tmp_path / "report.pdf"
source.write_bytes(b"report")
file = FileInfo(source, source.stat().st_size, ".pdf")
plan = build_plan([file], tmp_path, Config())
assert plan.operations[0].destination == tmp_path / "Sorted/documents/report.pdf"
assert source.exists()
assert not plan.operations[0].destination.exists()
Практика: строим план из списка файлов
Интерактивный ноутбук прямо в браузере — Python 3.14 через Pyodide, без установки
Чекпойнт · Issue #3
git commit -m "feat: add safe organization plan with collision handling"
Слился как Pull Request #17. Заголовок PR называет обе вещи сразу: этот же PR закрыл и Issue #6 (обработка конфликта имён), показанную отдельной страницей 23-14, потому что как урок это два разных, самостоятельных понятия, даже если в репозитории они попали в одну ветку.
Статус в реальном Project: Done