testsAndMisc/python_pkg/brother_printer/_queue_fix.py
Krzysztof kuhy Rudnicki cf44e54215 Split brother_printer queue display and fix prompt out of cups_queue
cups_queue.py 424 -> 245. display_cups_queue_status, _offer_queue_fix,
the _dwj_* choices and the _handle_* dispatchers move to _queue_fix.py.

The _cups_* action primitives stay put: test_cups_queue.py patches
subprocess and time on cups_queue, and those primitives are their only
consumers, so moving them would have left subprocess.run unpatched and
free to restart the real CUPS service mid-suite. test_cups_queue.py
therefore needs no edit; only part2/part3 repoint their MOD.

Pass count is 402 before and after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-16 16:53:51 +02:00

204 lines
6.9 KiB
Python

"""CUPS queue status display and the interactive queue-fix prompt.
Split out of :mod:`python_pkg.brother_printer.cups_queue` to keep it under the
250-line cap. The ``_cups_*`` action primitives deliberately stay in
``cups_queue``: ``tests/test_cups_queue.py`` patches ``subprocess`` and
``time`` on that module, and moving them would let a real ``subprocess.run``
reach the live CUPS daemon.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from python_pkg.brother_printer.constants import (
BOLD,
CYAN,
DIM,
GREEN,
RED,
RESET,
YELLOW,
_out,
_prompt,
)
from python_pkg.brother_printer.cups_queue import (
_cups_cancel_all_jobs,
_cups_enable_printer,
_cups_restart_service,
)
if TYPE_CHECKING:
from collections.abc import Callable
from python_pkg.brother_printer.data_classes import CUPSQueueStatus
# ── Queue status display ────────────────────────────────────────────
def display_cups_queue_status(queue: CUPSQueueStatus) -> None:
"""Display CUPS queue status and offer interactive fixes."""
if not queue.printer_name:
return
if queue.enabled and not queue.jobs and not queue.has_backend_errors:
return
_out()
_out(f"{BOLD}── Print Queue ──{RESET}")
_out()
if queue.has_backend_errors and queue.enabled and not queue.jobs:
_out(f" {YELLOW}{BOLD}⚡ CUPS backend has stale errors{RESET}")
_out(
f" {DIM}New print jobs may silently fail."
f" A CUPS restart usually fixes this.{RESET}"
)
_out()
if not queue.enabled:
_out(f" {RED}{BOLD}⚠ Printer queue is DISABLED{RESET}")
if queue.reason:
_out(f" {DIM}Reason: {queue.reason}{RESET}")
_out()
if queue.jobs:
_out(f" {BOLD}Pending jobs ({len(queue.jobs)}):{RESET}")
for job in queue.jobs:
_out(f" {job.job_id} {DIM}{job.user} {job.size}B {job.date}{RESET}")
_out()
_offer_queue_fix(queue)
# ── Interactive queue fix ────────────────────────────────────────────
def _offer_queue_fix(queue: CUPSQueueStatus) -> None:
"""Prompt the user to fix a disabled queue / pending jobs."""
_out(f" {BOLD}Available actions:{RESET}")
options: list[str] = []
if not queue.enabled and queue.jobs:
_out(f" {CYAN}1){RESET} Re-enable printer and retry all jobs")
_out(f" {CYAN}2){RESET} Re-enable printer and cancel all jobs")
_out(f" {CYAN}3){RESET} Cancel all jobs (keep printer disabled)")
_out(f" {CYAN}4){RESET} Restart CUPS service (fixes stale backend)")
_out(f" {CYAN}5){RESET} Restart CUPS + re-enable + retry all jobs")
_out(f" {CYAN}6){RESET} Do nothing")
options = ["1", "2", "3", "4", "5", "6"]
elif not queue.enabled:
_out(f" {CYAN}1){RESET} Re-enable printer")
_out(f" {CYAN}2){RESET} Restart CUPS service (fixes stale backend)")
_out(f" {CYAN}3){RESET} Do nothing")
options = ["1", "2", "3"]
elif queue.jobs:
_out(f" {CYAN}1){RESET} Cancel all pending jobs")
_out(f" {CYAN}2){RESET} Restart CUPS service (fixes stale backend)")
_out(f" {CYAN}3){RESET} Do nothing")
options = ["1", "2", "3"]
else:
_out(f" {CYAN}1){RESET} Restart CUPS service (fixes stale backend)")
_out(f" {CYAN}2){RESET} Do nothing")
options = ["1", "2"]
_out()
choice = _prompt(f" Choose [{'/'.join(options)}]: ")
_out()
if not queue.enabled and queue.jobs:
_handle_disabled_with_jobs(queue, choice)
elif not queue.enabled:
_handle_disabled_no_jobs(queue, choice)
elif queue.jobs:
_handle_enabled_with_jobs(queue, choice)
else:
_handle_backend_errors_only(choice)
def _dwj_enable_only(printer_name: str) -> None:
"""Choice 1: re-enable printer so queued jobs are retried."""
if _cups_enable_printer(printer_name):
_out(f" {GREEN}✓ Printer re-enabled. Jobs will be retried.{RESET}")
def _dwj_cancel_and_enable(printer_name: str) -> None:
"""Choice 2: cancel all jobs then re-enable."""
_cups_cancel_all_jobs(printer_name)
if _cups_enable_printer(printer_name):
_out(f" {GREEN}✓ All jobs cancelled and printer re-enabled.{RESET}")
def _dwj_cancel_only(printer_name: str) -> None:
"""Choice 3: cancel all jobs."""
if _cups_cancel_all_jobs(printer_name):
_out(f" {GREEN}✓ All jobs cancelled.{RESET}")
def _dwj_restart_only(_printer_name: str) -> None:
"""Choice 4: restart CUPS."""
if _cups_restart_service():
_out(f" {GREEN}✓ CUPS restarted.{RESET}")
def _dwj_restart_and_enable(printer_name: str) -> None:
"""Choice 5: restart CUPS and re-enable printer."""
if _cups_restart_service():
_cups_enable_printer(printer_name)
_out(
f" {GREEN}✓ CUPS restarted, printer re-enabled."
f" Jobs will be retried.{RESET}"
)
_DWJ_ACTIONS: dict[str, Callable[[str], None]] = {
"1": _dwj_enable_only,
"2": _dwj_cancel_and_enable,
"3": _dwj_cancel_only,
"4": _dwj_restart_only,
"5": _dwj_restart_and_enable,
}
def _handle_disabled_with_jobs(queue: CUPSQueueStatus, choice: str) -> None:
"""Handle fix for disabled printer with pending jobs."""
action = _DWJ_ACTIONS.get(choice)
if action is not None:
action(queue.printer_name)
else:
_out(f" {DIM}No changes made.{RESET}")
def _handle_disabled_no_jobs(queue: CUPSQueueStatus, choice: str) -> None:
"""Handle fix for disabled printer with no pending jobs."""
if choice == "1":
if _cups_enable_printer(queue.printer_name):
_out(f" {GREEN}✓ Printer re-enabled.{RESET}")
elif choice == "2":
if _cups_restart_service():
_cups_enable_printer(queue.printer_name)
_out(f" {GREEN}✓ CUPS restarted and printer re-enabled.{RESET}")
else:
_out(f" {DIM}No changes made.{RESET}")
def _handle_enabled_with_jobs(queue: CUPSQueueStatus, choice: str) -> None:
"""Handle fix for enabled printer with stuck jobs."""
if choice == "1":
if _cups_cancel_all_jobs(queue.printer_name):
_out(f" {GREEN}✓ All jobs cancelled.{RESET}")
elif choice == "2":
if _cups_restart_service():
_out(f" {GREEN}✓ CUPS restarted.{RESET}")
else:
_out(f" {DIM}No changes made.{RESET}")
def _handle_backend_errors_only(choice: str) -> None:
"""Handle fix when only stale backend errors are detected."""
if choice == "1":
if _cups_restart_service():
_out(f" {GREEN}✓ CUPS restarted. Stale backend errors cleared.{RESET}")
else:
_out(f" {DIM}No changes made.{RESET}")