testsAndMisc/python_pkg/brother_printer/_supply.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

132 lines
4.4 KiB
Python

"""Turning a raw SNMP supply reading into a percentage, a bar and a verdict.
Split out of :mod:`python_pkg.brother_printer.display` to keep it under the
250-line cap. Only the arithmetic lives here. The functions that *print*
(``_display_supply_levels``, ``_display_supply_warnings``) stay in ``display``
because ``tests/test_display_part2.py`` patches them there, and a patched name
has to remain resolvable in the module the test names.
``display`` re-exports everything below, because the tests import these names
from there rather than from here.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from python_pkg.brother_printer.constants import (
BOLD,
GREEN,
PROGRESS_BAR_WIDTH,
RED,
RESET,
SNMP_LEVEL_LOW,
SNMP_LEVEL_OK,
SUPPLY_LOW_PCT,
SUPPLY_WARN_PCT,
YELLOW,
_out,
)
from python_pkg.brother_printer.data_classes import SupplyStatus
if TYPE_CHECKING:
from python_pkg.brother_printer.data_classes import NetworkResult
def _classify_percentage_level(desc: str, pct: int) -> tuple[int, str, str, str, bool]:
"""Classify a supply by its calculated percentage."""
if pct <= SUPPLY_LOW_PCT:
return pct, f"{pct}%", RED, f"{desc} at {pct}%.", True
if pct <= SUPPLY_WARN_PCT:
return pct, f"{pct}%", YELLOW, f"{desc} at {pct}% -- order soon.", False
return pct, f"{pct}%", GREEN, "", False
def _classify_supply_level(
desc: str, max_val: int, level: int
) -> tuple[int, str, str, str, bool]:
"""Classify a supply level. Returns (pct, status, color, warning, replace)."""
if level == SNMP_LEVEL_OK:
return -1, "OK", GREEN, "", False
if level == SNMP_LEVEL_LOW:
return -1, "LOW", RED, f"{desc} is LOW.", True
if level == 0:
return 0, "EMPTY", RED, f"{desc} is EMPTY -- replace now!", True
if max_val > 0:
pct = min(level * 100 // max_val, 100)
return _classify_percentage_level(desc, pct)
return -1, "", GREEN, "", False
def _format_supply_bar(pct: int) -> str:
"""Build a progress bar string for a supply percentage."""
if pct < 0:
return ""
filled = pct * PROGRESS_BAR_WIDTH // 100
empty = PROGRESS_BAR_WIDTH - filled
return f"[{'█' * filled}{'░' * empty}]"
def _process_supply_item(desc: str, max_val: int, level: int) -> SupplyStatus:
"""Process a single supply item into display info."""
pct, status_text, color, warning, needs_replacement = _classify_supply_level(
desc, max_val, level
)
bar_text = _format_supply_bar(pct)
return SupplyStatus(color, bar_text, status_text, warning, needs_replacement)
def _parse_supply_value(values: list[str], index: int) -> int:
"""Safely parse an integer from a supply value list."""
try:
return int(values[index])
except (IndexError, ValueError):
return 0
def _collect_supply_items(
result: NetworkResult,
) -> tuple[list[SupplyStatus], list[str]]:
"""Parse and collect supply items with their descriptions."""
items: list[SupplyStatus] = []
descs: list[str] = []
for i, desc in enumerate(result.supplies.descriptions):
max_val = _parse_supply_value(result.supplies.max_values, i)
level = _parse_supply_value(result.supplies.levels, i)
items.append(_process_supply_item(desc, max_val, level))
descs.append(desc)
return items, descs
def render_life_bar(
label: str,
pct: int,
*,
exhausted: bool = False,
low: bool = False,
exhausted_note: str = "",
low_note: str = "",
) -> None:
"""Print one consumable's remaining-life bar.
The toner and drum bars in the page-count estimate were byte-for-byte the
same logic with different colours and notes, which is what this collapses.
Args:
label: Displayed name, already padded by the caller if needed.
pct: Percentage remaining, 0-100.
exhausted: Render red with ``exhausted_note``.
low: Render yellow with ``low_note``. Ignored when *exhausted*.
exhausted_note: Suffix shown when *exhausted*.
low_note: Suffix shown when *low*.
"""
filled = pct * PROGRESS_BAR_WIDTH // 100
meter = f"[{'█' * filled}{'░' * (PROGRESS_BAR_WIDTH - filled)}]"
if exhausted:
color, note = RED, exhausted_note
elif low:
color, note = YELLOW, low_note
else:
color, note = GREEN, ""
_out(f" {BOLD}{label}{RESET} {color}{meter} ~{pct}%{note}{RESET}")