Глава 23 · Часть IV · Реализуем SafeSort

От анализа к плану действий

План перемещений — обычные данные: его можно вывести на экран, проверить или отбросить, не тронув ни одного файла.

SafeSort · Часть 4 из 6Реализация
GitHubIssue #3 · Project «SafeSort — первый релиз»
Add safe organization plan
Area: SafetyPriority: High
git switch -c feat/plan-and-collisions

У SafeSort есть список найденных файлов и правило классификации — но само по себе это ещё не план действий. План — список конкретных перемещений: откуда и куда переместится каждый файл, если пользователь подтвердит выполнение.

Файловая система(сейчас)не тронутаbuild_plan()только читаетPLANтолько описание
Только читают
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