refactor(sports): one reader for element colours and layout offsets

There were two copies of the per-element colour read and three of the
layout-offset read. They had already drifted -- the scroll-card renderer
carries a comment about having ignored offsets its own schema advertised
-- and each new capability had to be added to all of them or silently work
in some places and not others.

All of them now go through src.element_style, which is what carries the
alias handling and the per-mode lookup. That lands immediately for the
nine plugins importing these modules: a scoreboard asking for `score_text`
offsets finds the `layout.score` its users configured, and a Live instance
resolves its own colours through SKIN_MODE without any call site passing a
mode.

_normalize_color learned "#RRGGBB" in the process. The scoreboards' own
readers have always accepted it, so the shared one had to, or consolidating
would have quietly dropped a form users' configs may hold. _coerce_offset
picked up the non-finite guard the scroll-card reader had and the other two
did not.

_get_layout_offset is promoted onto SportsCoreSharedMixin. Each plugin
still carries its own copy in its bundled sports.py, which wins by MRO --
so adopting this is a deletion in the plugin, and until that deletion
nothing changes for it.

Note for whoever runs the suite next: test_display_dirty_tracking.py is
order-dependent. Fifteen of its tests failed in one full run and passed in
the next with no change in between, and pass in isolation. Pre-existing,
unrelated to this, but it makes a full-run diff untrustworthy until it is
fixed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-09-11 11:45:21 -04:00
co-authored by Claude Opus 5
parent a46a908c79
commit b950871ca8
7 changed files with 287 additions and 83 deletions
+45
View File
@@ -256,3 +256,48 @@ class TestContract:
h._draw_upcoming_center(_draw(), {})
h._draw_upcoming_game_status(_draw(), {})
h.set_rankings_cache({})
class TestLayoutOffsetsAreShared:
"""The scroll/Vegas card reads offsets through the same function the
full-screen scorebug does.
This renderer already carries a comment about having ignored offsets its
own schema advertised -- it was the third copy of this read. Sharing one
means the alias handling and per-mode overrides land here too, without
this file knowing about either.
"""
CONFIG = {"customization": {
"layout": {"score": {"y_offset": -3}},
"modes": {"live": {"layout": {"score": {"y_offset": 7}}}},
}}
def _host(self, config, mode=None):
attrs = {"config": config}
if mode:
attrs["SKIN_MODE"] = mode
return type("Card", (SportsGameRendererMixin,), attrs)()
def test_it_reads_the_configured_offset(self):
assert self._host(self.CONFIG)._layout_offset("score", "y_offset") == -3
def test_it_resolves_through_an_alias(self):
"""A caller asking for score_text finds the layout.score the user
configured -- the two namespaces disagree by convention."""
assert self._host(self.CONFIG)._layout_offset(
"score_text", "y_offset") == -3
def test_skin_mode_selects_the_per_mode_offset(self):
assert self._host(self.CONFIG, "live")._layout_offset(
"score", "y_offset") == 7
def test_a_host_without_a_skin_mode_gets_the_base(self):
assert self._host(self.CONFIG)._layout_offset("score", "y_offset") == -3
@pytest.mark.parametrize("written", [
float("inf"), float("nan"), True, "bad", None, [], {},
])
def test_nonsense_degrades_to_the_default(self, written):
config = {"customization": {"layout": {"score": {"y_offset": written}}}}
assert self._host(config)._layout_offset("score", "y_offset", 4) == 4