testsAndMisc/python_pkg/brother_printer/display.py
Krzysztof kuhy Rudnicki 2832b7841f Reduce brother_printer/display.py and dedup the consumable bars
Partial: display.py 427 -> 342. Still a violation, not yet cleared.

Moved out only what no test patches:
  _supply.py    the supply-level arithmetic (imported by tests, not patched)
  _severity.py  the three _SEVERITY_* tables (pure data)

Also collapsed the toner and drum bars in _display_page_count_estimate into
_supply.render_life_bar. They were the same logic with different colours and
notes, so this removes duplication rather than relocating it.

The remaining ~92 lines are blocked on mock.patch pinning. This package's test
modules patch ~90 names via patch(f"{MOD}.<name>"), and a patched name only
resolves in the module the test names -- moving a function that calls one
breaks the mock. An attempt to move the page-count block was reverted for
exactly that (display has no attribute 'estimate_consumable_life'). Clearing
it needs either test edits or ~8 delegate stubs, which is a decision to take
rather than assume.

402 tests pass, unchanged.

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

343 lines
12 KiB
Python

"""Display and formatting functions for Brother printer status reports."""
from __future__ import annotations
import sys
from typing import TYPE_CHECKING
# ── Shared display helpers ───────────────────────────────────────────
# Re-exported through __all__: tests/test_display.py imports these names from
# this module. Only the un-patched supply *maths* moved to _supply -- everything
# the tests reach with mock.patch has to stay resolvable here.
from python_pkg.brother_printer._severity import (
_SEVERITY_COLORS,
_SEVERITY_ICONS,
_SEVERITY_SUMMARIES,
)
from python_pkg.brother_printer._supply import (
_classify_percentage_level,
_classify_supply_level,
_collect_supply_items,
_format_supply_bar,
_parse_supply_value,
_process_supply_item,
render_life_bar,
)
from python_pkg.brother_printer.constants import (
BOLD,
CYAN,
DIM,
DRUM_RATED_PAGES,
GREEN,
RED,
RESET,
TONER_RATED_PAGES,
YELLOW,
_out,
get_status_info,
)
from python_pkg.brother_printer.consumables import (
check_page_delivery,
estimate_consumable_life,
)
from python_pkg.brother_printer.cups_queue import (
display_cups_queue_status,
get_cups_queue_status,
)
if TYPE_CHECKING:
from python_pkg.brother_printer.data_classes import (
NetworkResult,
USBResult,
)
__all__ = [
"_classify_percentage_level",
"_classify_supply_level",
"_collect_supply_items",
"_format_supply_bar",
"_parse_supply_value",
"_process_supply_item",
"display_network_results",
"display_usb_results",
]
def _display_report_header() -> None:
"""Print the report banner box."""
_out()
_out(f"{BOLD}╔══════════════════════════════════════════════════╗{RESET}")
_out(f"{BOLD}║ Brother Laser Printer Status Report ║{RESET}")
_out(f"{BOLD}╚══════════════════════════════════════════════════╝{RESET}")
_out()
def _display_page_delivery_warning(printer_total: int, *, queue_idle: bool) -> None:
"""Warn when CUPS claims more pages than the printer actually counted.
Args:
printer_total: Lifetime count from the printer's own counter.
queue_idle: False while a job is queued or printing, when the two
counters legitimately disagree.
"""
check = check_page_delivery(printer_total, queue_idle=queue_idle)
if not check.suspected:
return
_out(f"{BOLD}── Dropped Pages ──{RESET}")
_out()
_out(
f" {RED}{BOLD}⚠ {check.dropped} pages did not print.{RESET}"
f" {RED}CUPS sent {check.cups_pages} pages since the last check;"
f" the printer's own counter advanced by"
f" {check.printer_pages}.{RESET}"
)
_out()
_out(
f" {DIM}The printer discards pages whose 600 dpi raster does not fit"
f" its memory, stays READY, and reports no error - and CUPS still calls"
f" the job successful. Reprint at a lower resolution:{RESET}"
)
_out(f" {DIM} lp -o Resolution=300dpi <file>{RESET}")
_out()
def _display_page_count_estimate(printer_total: int = 0) -> None:
"""Show estimated consumable life based on the printer's page count.
Args:
printer_total: Lifetime count from the printer's own counter, or zero
when it could not be read and the CUPS page log has to stand in.
"""
estimate = estimate_consumable_life(printer_total)
if estimate.total_pages <= 0:
return
_out(f"{BOLD}── Page Count Estimate ──{RESET}")
_out()
_out(
f" {BOLD}Total pages printed:{RESET} {estimate.total_pages}"
f" (toner: {estimate.toner_pages} since replacement,"
f" drum: {estimate.drum_pages} since replacement)"
)
if estimate.approximate:
_out(
f" {YELLOW}Approximate: counted from the CUPS log, not the"
f" printer's own counter, so it misses pages CUPS never saw"
f" and reads high.{RESET}"
)
_out()
render_life_bar(
"Toner:",
estimate.toner_pct_remaining,
exhausted=estimate.toner_exhausted,
low=estimate.toner_low,
exhausted_note=" ← REPLACE NOW",
low_note=" ← order soon",
)
render_life_bar(
"Drum: ",
estimate.drum_pct_remaining,
low=estimate.drum_near_end,
low_note=" ← nearing end",
)
_out(
f" {DIM}Based on pages since last replacement"
f" vs rated capacity (toner ~{TONER_RATED_PAGES},"
f" drum ~{DRUM_RATED_PAGES}).{RESET}"
)
_out(f" {DIM}Reset after replacing: --reset-toner or --reset-drum{RESET}")
if estimate.toner_exhausted:
_out()
_out(
f" {RED}{BOLD}⚠ Toner is likely exhausted."
f" This is probably why the orange light is flashing.{RESET}"
)
_out()
def _display_consumables_reference() -> None:
"""Print compatible consumables reference."""
_out(f"{BOLD}── Compatible Consumables ──{RESET}")
_out()
_out(f" {BOLD}Toner:{RESET} TN-1050 / TN-1030 (or compatible third-party)")
_out(f" {BOLD}Drum:{RESET} DR-1050 / DR-1030 (or compatible third-party)")
_out(f" {DIM} Toner rated ~1000 pages; Drum rated ~10000 pages.{RESET}")
_out()
# ── USB display helpers ──────────────────────────────────────────────
def _display_usb_device_info(result: USBResult) -> None:
"""Print device info block for USB results."""
_out(f"{BOLD}Printer:{RESET} {result.product or 'Unknown'}")
_out(f"{BOLD}Connection:{RESET} USB")
if result.serial:
_out(f"{BOLD}Serial:{RESET} {result.serial}")
if result.online == "TRUE":
_out(f"{BOLD}Online:{RESET} {GREEN}Yes{RESET}")
elif result.online == "FALSE":
_out(f"{BOLD}Online:{RESET} {YELLOW}No (needs attention){RESET}")
_out()
if result.economode:
if result.economode == "ON":
_out(
f"{BOLD}Toner Save:{RESET} {GREEN}ON{RESET}"
" (extends toner life, lighter prints)"
)
else:
_out(f"{BOLD}Toner Save:{RESET} OFF")
def _format_status_detail(
severity: str, short_text: str, action: str, result: USBResult
) -> None:
"""Print severity icon, display text, and action."""
color = _SEVERITY_COLORS.get(severity, GREEN)
icon = _SEVERITY_ICONS.get(severity, "✓")
_out(f" {color}{BOLD}{icon} {short_text}{RESET}")
if result.display and result.display != short_text:
_out(f" {DIM}Display: {result.display}{RESET}")
_out(f" {DIM}Status code: {result.status_code}{RESET}")
if action:
_out()
_out(f" {color}{BOLD}Action:{RESET} {color}{action}{RESET}")
_out()
_out(_SEVERITY_SUMMARIES.get(severity, ""))
def _display_pjl_status(result: USBResult) -> None:
"""Display PJL status code interpretation."""
_out()
_out(f"{BOLD}── Printer Status ──{RESET}")
_out()
if not result.status_code:
_out(f" {YELLOW}Could not read status from printer.{RESET}")
if result.display:
_out(f" Display message: {BOLD}{result.display}{RESET}")
return
severity, short_text, action = get_status_info(result.status_code)
_format_status_detail(severity, short_text, action, result)
def _display_cups_fallback_note(result: USBResult) -> None:
"""Show a note when running in CUPS fallback mode."""
_out()
if result.port_status is not None:
_out(
f" {DIM}Note: Hardware status obtained via USB port query."
f" Toner/drum percentages not available.{RESET}"
)
else:
_out(
f" {DIM}Note: pyusb not available; status obtained via"
f" CUPS only. Detailed toner/drum levels are not"
f" available in this mode.{RESET}"
)
# ── USB results display ─────────────────────────────────────────────
def display_usb_results(result: USBResult) -> None:
"""Print a formatted report for USB PJL query results."""
if result.error:
_out(f"{RED}Error: {result.error}{RESET}")
sys.exit(1)
_display_report_header()
_display_usb_device_info(result)
_display_pjl_status(result)
if result.device == "cups":
_display_cups_fallback_note(result)
_out()
printer_total = int(result.page_count) if result.page_count.isdigit() else 0
queue = get_cups_queue_status()
_display_page_delivery_warning(printer_total, queue_idle=not queue.jobs)
_display_page_count_estimate(printer_total)
_display_consumables_reference()
display_cups_queue_status(queue)
# ── Network supply level helpers ─────────────────────────────────────
def _display_supply_warnings(*, needs_replacement: bool, warnings: list[str]) -> None:
"""Display supply level warnings summary."""
_out()
if needs_replacement:
_out(f"{RED}{BOLD}⚠ ACTION NEEDED:{RESET}")
for w in warnings:
_out(f" {RED}• {w}{RESET}")
elif warnings:
_out(f"{YELLOW}{BOLD}⚡ HEADS UP:{RESET}")
for w in warnings:
_out(f" {YELLOW}• {w}{RESET}")
else:
_out(f"{GREEN}{BOLD}✓ All consumables are at healthy levels.{RESET}")
def _display_supply_levels(result: NetworkResult) -> None:
"""Display consumable supply levels section."""
_out()
_out(f"{BOLD}── Consumable Levels ──{RESET}")
_out()
needs_replacement = False
warnings: list[str] = []
items, descs = _collect_supply_items(result)
for desc, item in zip(descs, items, strict=True):
_out(
f" {BOLD}{desc:<25}{RESET}"
f" {item.color}{item.bar_text} {item.status_text}{RESET}"
)
if item.needs_replacement:
needs_replacement = True
if item.warning:
warnings.append(item.warning)
_display_supply_warnings(needs_replacement=needs_replacement, warnings=warnings)
def _display_network_device_info(result: NetworkResult) -> None:
"""Display device info section for network results."""
_out(f"{BOLD}Printer:{RESET} {result.product or 'Unknown'}")
_out(f"{BOLD}Connection:{RESET} Network ({result.ip})")
if result.serial:
_out(f"{BOLD}Serial:{RESET} {result.serial}")
if result.display:
_out(f"{BOLD}Display:{RESET} {result.display}")
if result.page_count and result.page_count.isdigit():
_out(f"{BOLD}Pages:{RESET} {result.page_count} total")
# ── Network results display ──────────────────────────────────────────
def display_network_results(result: NetworkResult) -> None:
"""Print a formatted report for SNMP network query results."""
if result.error:
_out(f"{RED}Error: {result.error}{RESET}")
sys.exit(1)
_display_report_header()
_display_network_device_info(result)
_display_supply_levels(result)
_out()
_out(
f"{CYAN}Tip: Visit http://{result.ip} for the full web management"
f" interface.{RESET}"
)
_out()