mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 19:47:12 +01:00
review.py held three separable concerns. Two move out:
_render.py emptiness, status_line, filename_line, title_line, render --
pure state-to-text, no toolkit and no transitions
_undo.py can_undo, on_undo, forget_last -- correcting the tally and
putting the on-screen video back at the front of the queue
review.py keeps the state transitions and re-exports all eight names through
__all__, so ui.py and test_review.py reach them at review.* exactly as before;
verified by resolving every name in __all__.
Both new modules reference SessionState under TYPE_CHECKING only, so there is
no runtime cycle. Also inlined _worker_message, a single-caller passthrough
that just returned event.message.
review.py 330 -> 196, _render.py 107, _undo.py 74.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
197 lines
5.4 KiB
Python
197 lines
5.4 KiB
Python
"""The reviewer's behaviour, with no toolkit anywhere in sight.
|
|
|
|
This module is the whole application as far as behaviour is concerned. It
|
|
decides what plays next, what a verdict does, when the queue has run dry and
|
|
exactly what the status bar reads. :mod:`python_pkg.wsg_grabber.ui` only
|
|
applies the :class:`ReviewCommand` this produces, so every decision is testable
|
|
from plain values and none of them need a display.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, replace
|
|
from typing import TYPE_CHECKING
|
|
|
|
# Re-exported through __all__ so review.render / review.status_line and friends
|
|
# keep working for ui.py and test_review.py; the implementations live in
|
|
# _render to keep this module under the 250-line cap. _render only needs
|
|
# SessionState for typing, so this import is not circular at runtime.
|
|
from python_pkg.wsg_grabber._render import (
|
|
emptiness,
|
|
filename_line,
|
|
render,
|
|
status_line,
|
|
title_line,
|
|
)
|
|
from python_pkg.wsg_grabber._undo import can_undo, forget_last, on_undo
|
|
from python_pkg.wsg_grabber.models import (
|
|
Downloaded,
|
|
FileReady,
|
|
Indexed,
|
|
ScanFinished,
|
|
Verdict,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Sequence
|
|
|
|
from python_pkg.wsg_grabber.models import DownloadEvent, ReviewItem
|
|
|
|
__all__ = [
|
|
"SessionState",
|
|
"can_undo",
|
|
"emptiness",
|
|
"filename_line",
|
|
"forget_last",
|
|
"initial_state",
|
|
"on_event",
|
|
"on_missing_locally",
|
|
"on_new_files",
|
|
"on_quit",
|
|
"on_undo",
|
|
"on_verdict",
|
|
"render",
|
|
"status_line",
|
|
"title_line",
|
|
]
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class SessionState:
|
|
"""Everything the reviewer knows, as one immutable value."""
|
|
|
|
pending: tuple[ReviewItem, ...] = ()
|
|
current: ReviewItem | None = None
|
|
kept: int = 0
|
|
passed: int = 0
|
|
indexed: int = 0
|
|
downloaded: int = 0
|
|
scan_complete: bool = False
|
|
quitting: bool = False
|
|
error: str | None = None
|
|
undoable: int = 0
|
|
"""Verdicts recorded in the index that could still be taken back."""
|
|
|
|
|
|
def initial_state(
|
|
indexed: int,
|
|
ready: Sequence[ReviewItem],
|
|
undoable: int = 0,
|
|
) -> SessionState:
|
|
"""Build the state a session starts from.
|
|
|
|
Args:
|
|
indexed: How many files the index already knows about.
|
|
ready: Videos downloaded on a previous run and still unreviewed.
|
|
undoable: Verdicts already in the index that can still be reversed, so
|
|
undo carries across restarts rather than starting empty.
|
|
|
|
Returns:
|
|
SessionState: Starting state, already showing the first video if any.
|
|
"""
|
|
state = SessionState(indexed=indexed, undoable=undoable)
|
|
return on_new_files(state, ready)
|
|
|
|
|
|
def on_new_files(state: SessionState, items: Sequence[ReviewItem]) -> SessionState:
|
|
"""Add freshly downloaded videos to the queue.
|
|
|
|
The first arrival is promoted straight to the screen, which is what lets
|
|
watching start while the rest of the board is still downloading.
|
|
|
|
Args:
|
|
state: Current state.
|
|
items: Newly available videos.
|
|
|
|
Returns:
|
|
SessionState: Updated state.
|
|
"""
|
|
if not items:
|
|
return state
|
|
queue = [*state.pending, *items]
|
|
current = state.current
|
|
if current is None:
|
|
current = queue.pop(0)
|
|
return replace(state, pending=tuple(queue), current=current)
|
|
|
|
|
|
def on_verdict(state: SessionState, choice: Verdict) -> SessionState:
|
|
"""Record a decision about the current video and advance.
|
|
|
|
Args:
|
|
state: Current state.
|
|
choice: What the user pressed.
|
|
|
|
Returns:
|
|
SessionState: Updated state showing the next video.
|
|
"""
|
|
if state.current is None:
|
|
return state
|
|
counted = (
|
|
replace(state, kept=state.kept + 1)
|
|
if choice is Verdict.KEEP
|
|
else replace(state, passed=state.passed + 1)
|
|
)
|
|
return _advance(replace(counted, undoable=state.undoable + 1))
|
|
|
|
|
|
def on_missing_locally(state: SessionState) -> SessionState:
|
|
"""Drop the current video because its file has disappeared.
|
|
|
|
Args:
|
|
state: Current state.
|
|
|
|
Returns:
|
|
SessionState: Updated state showing the next video.
|
|
"""
|
|
if state.current is None:
|
|
return state
|
|
return _advance(state)
|
|
|
|
|
|
def on_event(state: SessionState, event: DownloadEvent) -> SessionState:
|
|
"""Fold one background-worker event into the state.
|
|
|
|
Args:
|
|
state: Current state.
|
|
event: What the worker reported.
|
|
|
|
Returns:
|
|
SessionState: Updated state.
|
|
"""
|
|
if isinstance(event, FileReady):
|
|
return on_new_files(state, [event.item])
|
|
if isinstance(event, Indexed):
|
|
return replace(state, indexed=event.known)
|
|
if isinstance(event, Downloaded):
|
|
return replace(state, downloaded=event.count)
|
|
if isinstance(event, ScanFinished):
|
|
return replace(state, scan_complete=True)
|
|
return replace(state, error=event.message)
|
|
|
|
|
|
def on_quit(state: SessionState) -> SessionState:
|
|
"""Mark the session as finishing.
|
|
|
|
Args:
|
|
state: Current state.
|
|
|
|
Returns:
|
|
SessionState: Updated state.
|
|
"""
|
|
return replace(state, quitting=True)
|
|
|
|
|
|
def _advance(state: SessionState) -> SessionState:
|
|
"""Move to the next queued video.
|
|
|
|
Args:
|
|
state: State whose current item has been dealt with.
|
|
|
|
Returns:
|
|
SessionState: Updated state.
|
|
"""
|
|
queue = list(state.pending)
|
|
following = queue.pop(0) if queue else None
|
|
return replace(state, pending=tuple(queue), current=following)
|