From e8077d37339b286e635b14b2ea8a281efe23ff25 Mon Sep 17 00:00:00 2001 From: ChuckBuilds Date: Thu, 3 Sep 2026 18:04:21 -0400 Subject: [PATCH] fix(sports): say when the schema cannot be read, instead of failing silently _schema_font_size swallowed every exception and cached an empty dict. That is not cosmetic. With no schema, a configured font size can no longer be compared against the schema default, so every size is treated as a deliberate user choice and skips the snap to the font's pixel grid -- which renders 4x6-font.ttf at 6 instead of 7: a 3px-wide glyph instead of 4px. That shipped. On a 256x64 panel it made the odds, the team records and the date row hard to read, and it was found by a user counting pixels on a photo of the panel rather than by anything here. The cause (_plugin_dir returning None under the real plugin loader) is fixed in #519; this makes the same class of failure audible next time: Orphan: could not read config_schema.json (FileNotFoundError: ...); every font size will be treated as user-chosen and will skip its pixel grid snap. Font sizes may render a pixel narrow. The message names the consequence, not just the error, because the error alone does not suggest "your fonts are a pixel narrow". Logged rather than raised: an unreadable schema must not stop a plugin rendering. The cache is built once per class (per schema path in sports_card), so this cannot repeat per frame. Scope deliberately small. An audit of the three shared modules found 23 handlers that swallow and return a default, but all 23 catch specific types -- TypeError, ValueError, ImportError -- turning bad config values into defaults, which is what they are for. Of 77 broad handlers across the font and odds paths, 74 already log. Only these two were both broad and silent. --- src/common/sports_card.py | 12 +++++++++++- src/common/sports_shared.py | 18 +++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/common/sports_card.py b/src/common/sports_card.py index 5a1f8ebb..6b8bd7ea 100644 --- a/src/common/sports_card.py +++ b/src/common/sports_card.py @@ -17,10 +17,13 @@ the extra guard only stops a None size raising TypeError. """ +import logging from datetime import datetime, timezone from typing import Any, Dict, Optional, Tuple from zoneinfo import ZoneInfo +logger = logging.getLogger(__name__) + __all__ = [ "ELEMENT_FOR_FONT", "FAVORITE_RESULT_COLOR_DEFAULTS", "FONT_NAME_ALIASES", "FONT_PIXEL_GRID", "MONTH_ABBR", "WEEKDAY_ABBR", @@ -395,7 +398,14 @@ def schema_font_size(schema_path: str, element_key) -> Optional[int]: size = spec.get('properties', {}).get('font_size', {}).get('default') if size is not None: cache[key] = int(size) - except Exception: + except Exception as exc: + # See sports_shared._schema_font_size: an unreadable schema + # silently disables the pixel-grid snap for every element. + # Built once per schema path, so this cannot repeat per frame. + logger.warning( + "could not read %s (%s: %s); font sizes will skip their " + "pixel grid snap and may render a pixel narrow", + schema_path, type(exc).__name__, exc) cache = {} _SCHEMA_FONT_SIZE_CACHE[schema_path] = cache return cache.get(element_key) diff --git a/src/common/sports_shared.py b/src/common/sports_shared.py index 1187b846..b120e532 100644 --- a/src/common/sports_shared.py +++ b/src/common/sports_shared.py @@ -288,7 +288,23 @@ def _schema_font_size(self, element_key): size = spec.get('properties', {}).get('font_size', {}).get('default') if size is not None: cache[key] = int(size) - except Exception: + except Exception as exc: + # Say so. An unreadable schema is not cosmetic: every element's + # configured size then stops matching "the schema default", is + # treated as a deliberate user choice, and skips the snap to the + # font's pixel grid -- which renders 4x6-font.ttf at 6 instead + # of 7, a 3px-wide glyph instead of 4px. That shipped once, + # silently, and was found by a user counting pixels on a photo + # of the panel. + # + # Logged, not raised: a missing schema must not stop a plugin + # rendering. The cache is built once per class, so this cannot + # repeat per frame. + logger.warning( + "%s: could not read config_schema.json (%s: %s); every font " + "size will be treated as user-chosen and will skip its pixel " + "grid snap. Font sizes may render a pixel narrow.", + type(self).__name__, type(exc).__name__, exc) cache = {} self.__class__._SCHEMA_FONT_SIZES = cache return cache.get(element_key)