fix(sports): declare the constants the mixins read, and test the contract

CodeRabbit found _QUALITY_CHOICES and _RANKING_COVERAGE_SECONDS read by
_normalise_quality and _check_ranking_coverage but never defined on a mixin.
Confirmed: both are declared by all eight scoreboards, so nothing fails today --
it would only have bitten the ninth plugin to adopt this, at runtime, mid-render.
Both are identical everywhere, so they get defaults here; each plugin's own copy
still shadows them.

Auditing for others showed those two were the only ones, but also that the
host-contract docstring was substantially incomplete: it listed 21 attributes
where the mixins actually read about 40, and omitted five hooks
(_is_favorite_game, _is_game_really_over, _is_ranked_game,
_passes_other_filters, _get_timezone). The section is now derived from that
audit rather than remembered.

test_sports_shared.py covers what is genuinely new, not the moved bodies:

- The contract itself. It parses the module for every ALL-CAPS `self.X` the
  mixins read and asserts each is defined, so the next omission fails here
  rather than in the field.
- _plugin_dir, the only new logic in the move. Including the case that made it
  necessary: SportsCore is an ABC, so a subclass built with type(name, bases,
  ns) -- which the plugins' own tests build -- reports __module__ as "abc". The
  test asserts that precondition before asserting the walk steps past it.
- The three SportsLive bodies. Hockey and lacrosse disable live mode in their
  harness fixtures, so the 176 renders never reach this path; testing the mixin
  directly means coverage no longer depends on which plugin happens to have a
  unit test.

Two of those tests pin things that would otherwise be silently undone.
SportsRecentSharedMixin does carry an __init__ -- SportsRecent.__init__ was one
of the 48 byte-identical bodies. Its bare super() binds to where it is defined,
now the mixin, so it only reaches the host because the mixin is listed first in
the bases. One test proves the chain runs; the next proves that reversing the
order silently skips the host constructor.
This commit is contained in:
ChuckBuilds
2026-09-02 16:31:34 -04:00
parent 1dea7b0706
commit 3fa69645f7
2 changed files with 343 additions and 8 deletions
+22 -8
View File
@@ -37,20 +37,30 @@ needs it -- so it is inlined below rather than left behind.
WHAT A HOST MUST PROVIDE
------------------------
Enumerated by walking every ``self.<attr>`` the mixins read and subtracting what
they define, so this list is derived rather than remembered. Everything below is
supplied by all eight scoreboards today.
State: ``config``, ``fonts``, ``logger``, ``display_width``, ``display_height``,
``display_manager``, ``league``, ``sport``, ``mode_config``, ``session``,
``headers``, ``schedule_lookback_days``, ``schedule_lookahead_days``,
``game_update_timestamps``, ``_zero_clock_timestamps``, ``_logo_cache``,
``_selection_pools``, ``_ranking_coverage_logged_at``, ``_empty_live_streak``,
``_last_warning_time``, ``_score_grew``.
``headers``, ``favorite_teams``, ``games_list``, ``current_game_index``,
``last_game_switch``, ``last_update``, ``update_interval``,
``no_data_interval``, ``game_display_duration``, ``stale_game_timeout``,
``other_games_min_quality``, ``schedule_lookback_days``,
``schedule_lookahead_days``, ``game_update_timestamps``,
``_zero_clock_timestamps``, ``_logo_cache``, ``_selection_pools``,
``_ranking_coverage_logged_at``, ``_empty_live_streak``, ``_last_warning_time``,
``_score_grew``.
Methods that stay per-plugin, because they are not identical across the eight:
Methods that stay per-plugin, because they are not identical across the eight
(or, for ``_get_timezone``, because they bind per-plugin modules):
``_get_layout_offset``, ``_by_importance``, ``_other_games_window``,
``_upcoming_date_and_time_text``, ``_extract_game_details_common``,
``_load_division_team_ids``.
``_load_division_team_ids``, ``_get_timezone``, ``_is_favorite_game``,
``_is_game_really_over``, ``_is_ranked_game``, ``_passes_other_filters``.
Of the twelve shared class constants, eleven are identical everywhere and live
here. Only ``_SCORE_PROBE_TEXT`` varies -- afl and basketball reach three digits
Of the fourteen shared class constants, thirteen are identical everywhere and
live here. Only ``_SCORE_PROBE_TEXT`` varies -- afl and basketball reach three digits
a side and override it, the same two that override ``_SCORE_PROBE`` on
``SportsGameRendererMixin``.
@@ -168,6 +178,10 @@ class SportsCoreSharedMixin:
"PressStart2P-Regular.ttf": 8, "4x6-font.ttf": 7}
_FONT_NAME_ALIASES: ClassVar[Dict[str, str]] = {
"press_start": "PressStart2P-Regular.ttf", "four_by_six": "4x6-font.ttf"}
#: Accepted values for the other-games quality filter.
_QUALITY_CHOICES: ClassVar[frozenset] = frozenset({"any", "ranked"})
#: How long to stay quiet between ranking-coverage warnings.
_RANKING_COVERAGE_SECONDS: ClassVar[int] = 60 * 60
def _get_season_schedule_dates(self) -> tuple[str, str]:
return "", ""