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
+50
View File
@@ -262,3 +262,53 @@ class TestUnshareElementFonts:
fonts = {"score": a, "time": b}
C.unshare_element_fonts(log, fonts)
assert fonts["score"] is a and fonts["time"] is b
class TestSharedElementReaders:
"""The scoreboards' colour and offset reads now go through one
implementation.
There were two copies of the colour read and three of the offset read.
They had already drifted: the scroll-card renderer carries a comment
noting it used to ignore offsets its own schema advertised. Sharing one
reader is what lets a fix (or an alias, or a per-mode override) reach
the full-screen scorebug and the scroll card together.
"""
CONFIG = {
"customization": {
"score_text": {"text_color": [255, 200, 0]},
"status": {"text_color": "#00ff00"},
"layout": {"score": {"y_offset": -3}},
"modes": {
"live": {"score_text": {"text_color": [255, 0, 0]},
"layout": {"score": {"y_offset": 7}}},
},
},
}
def test_colour_reads_the_configured_value(self, log):
assert C.element_color(self.CONFIG, "score_text") == (255, 200, 0)
def test_hex_colours_are_still_accepted(self, log):
"""These readers have always taken '#RRGGBB' as well as [r, g, b],
so the shared one had to learn it rather than the callers losing it."""
assert C.element_color(self.CONFIG, "status") == (0, 255, 0)
def test_colour_resolves_through_an_alias(self, log):
"""The style block says status_text where this config says status."""
assert C.element_color(self.CONFIG, "status_text") == (0, 255, 0)
def test_a_mode_overrides_the_colour(self, log):
assert C.element_color(self.CONFIG, "score_text",
mode="live") == (255, 0, 0)
def test_an_unset_element_gets_the_default(self, log):
assert C.element_color(self.CONFIG, "nothing", (1, 2, 3)) == (1, 2, 3)
@pytest.mark.parametrize("config", [
None, {}, "nonsense", {"customization": "nonsense"},
{"customization": {"score_text": "nonsense"}},
{"customization": {"score_text": {"text_color": "not a colour"}}},
])
def test_a_hostile_config_gives_the_default(self, config, log):
assert C.element_color(config, "score_text", (9, 9, 9)) == (9, 9, 9)