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
+40
View File
@@ -422,3 +422,43 @@ class TestUnshareKeepsTheLayoutEngine:
fonts = {"score": shared, "time": shared}
self._host()._unshare_element_fonts(fonts)
assert fonts["time"].getlength("88-88") == shared.getlength("88-88")
class TestPromotedLayoutOffset:
"""_get_layout_offset moved here so every scoreboard reads offsets the
way the scroll card does.
Each plugin still carries its own copy in its bundled sports.py, which
wins by MRO. That is the migration property: adopting this is a
deletion in the plugin, and until that deletion nothing changes.
"""
CONFIG = {"customization": {
"layout": {"score": {"y_offset": -3}},
"modes": {"recent": {"layout": {"score": {"y_offset": 9}}}},
}}
def _host(self, **attrs):
return type("H", (SportsCoreSharedMixin,),
dict({"config": self.CONFIG}, **attrs))()
def test_it_reads_the_configured_offset(self):
assert self._host()._get_layout_offset("score", "y_offset") == -3
def test_it_resolves_through_an_alias(self):
"""What a plugin gains by deleting its own copy: the style block
says score_text where the layout block says score."""
assert self._host()._get_layout_offset("score_text", "y_offset") == -3
def test_skin_mode_selects_the_per_mode_offset(self):
assert self._host(SKIN_MODE="recent")._get_layout_offset(
"score", "y_offset") == 9
def test_a_plugins_own_copy_still_wins(self):
"""Until a plugin deletes its copy, this changes nothing for it."""
host = self._host(
_get_layout_offset=lambda self, element, axis, default=0: 99)
assert host._get_layout_offset("score", "y_offset") == 99
def test_an_unset_offset_is_the_default(self):
assert self._host()._get_layout_offset("nothing", "y_offset", 5) == 5