Skip to content

fix(sports): let the plugin declare its own directory, don't deduce it - #519

Merged
ChuckBuilds merged 1 commit into
mainfrom
fix/plugin-dir-from-host
Sep 3, 2026
Merged

fix(sports): let the plugin declare its own directory, don't deduce it#519
ChuckBuilds merged 1 commit into
mainfrom
fix/plugin-dir-from-host

Conversation

@ChuckBuilds

@ChuckBuilds ChuckBuilds commented Sep 3, 2026

Copy link
Copy Markdown
Owner

_plugin_dir() returned None on every device. The consequence was silent and reached the panel:

_plugin_dir()       -> None
_schema_font_size() -> None for every element
-> a configured size equal to the schema default stops looking like a default
   and is treated as a deliberate user choice
-> the snap to the font's pixel grid is skipped
-> 4x6-font.ttf renders at 6 instead of 7: 3px-wide glyphs, not 4px

On a 256×64 panel that made the odds, the team records and the date row hard to read. Both odds and detail were affected — anything resolving a grid-snapped schema default was a pixel narrow.

Why no gate caught it

# PluginLoader._namespace_plugin_modules
namespaced = f"_plg_{safe_id}_{mod_name}"
sys.modules[namespaced] = mod
del sys.modules[mod_name]        # the bare entry goes away

A class defined in sports.py still reports __module__ == "sports", but sys.modules["sports"] no longer exists — so walking the MRO for a module with a __file__ finds nothing.

Every test here imported plugins directly, which leaves the bare entry in place, so the walk succeeded. The safety harness loads plugins its own way and never reproduced it either. It was found by a user counting pixels on a photo of the panel.

Fix

The directory is declared by the plugin (_PLUGIN_DIR), and only deduced as a fallback for hosts that declare nothing — the plugins' own probe harnesses build classes with type().

Needs the companion plugins PR, which adds the one-line declaration to each of the eight scoreboards.

Verified on hardware

The only place the original failure appeared:

live service
before plugin_dir=None, 4x6-font.ttf@6 for all six football managers
after plugin_dir=/…/football-scoreboard, odds=@7 detail=@7

Five regression tests, including the production shape: a class whose __module__ is absent from sys.modules still resolves via its declared directory. The precondition — that the MRO walk alone returns None — is pinned, so the test keeps meaning something if the fallback ever changes.

Summary by CodeRabbit

  • Bug Fixes

    • Corrected an issue that could prevent configured scoreboard font sizes from being recognized in production.
    • Improved font sizing, colors, dates, and upcoming-game display consistency across scoreboard views.
  • Refactor

    • Standardized shared scoreboard behavior across sports plugins, helping deliver more consistent presentation and reducing differences between views.

_plugin_dir() returned None on every device. The consequence was silent and
reached the panel:

    _plugin_dir()       -> None
    _schema_font_size() -> None for every element
    -> a configured size equal to the schema default stops looking like a
       default and is treated as a deliberate user choice
    -> the snap to the font's pixel grid is skipped
    -> 4x6-font.ttf renders at 6 instead of 7: 3px-wide glyphs, not 4px

On a 256x64 panel that made the odds, the team records and the date row hard to
read. Both `odds` and `detail` were affected -- anything resolving a
grid-snapped schema default was a pixel narrow.

Why it was invisible here. PluginLoader._namespace_plugin_modules renames every
bare module a plugin brought in (sports, game_renderer, ...) to
"_plg_<plugin_id>_<module>" and REMOVES the bare sys.modules entry, so two
plugins owning a module of the same name cannot collide. A class defined in
sports.py still reports __module__ == "sports", but sys.modules["sports"] is
gone, so walking the MRO for a module with a __file__ finds nothing.

Every test here imported plugins directly, which leaves the bare entry in
place, so the walk succeeded. The safety harness loads plugins its own way and
never reproduced it either. It was found by a user counting pixels on the
panel.

The directory is now declared by the plugin (_PLUGIN_DIR) and only deduced as
a fallback, for hosts that declare nothing -- the plugins' own probe harnesses
build classes with type().

Verified on hardware, which is the only place the original failure appeared:
before, the live service logged plugin_dir=None and 4x6-font.ttf@6 for all six
football managers; after, plugin_dir resolves and both odds and detail are @7.

Five regression tests, including the production shape: a class whose __module__
is absent from sys.modules still resolves via its declared directory, and the
precondition that the MRO walk alone returns None is pinned so the test keeps
meaning something if the fallback changes.
@coderabbitai

coderabbitai Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 51b1caf1-5c97-457a-ac47-5e80facd5be5

📥 Commits

Reviewing files that changed from the base of the PR and between 32d637a and b070758.

📒 Files selected for processing (2)
  • src/common/sports_shared.py
  • test/test_sports_shared.py

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The change adds shared sports mixins and centralizes common scoreboard behavior. It also lets plugins declare their directory through _PLUGIN_DIR, with validated fallback discovery and regression tests for schema-based font sizing.

Changes

Shared sports plugin infrastructure

Layer / File(s) Summary
Shared behavior and plugin directory resolution
src/common/sports_shared.py, test/test_sports_shared.py
Shared sports mixins consolidate common scoreboard methods. _plugin_dir prefers a valid declared _PLUGIN_DIR, retains the MRO fallback, and tests validate production loader behavior and schema font-size lookup.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to b0707

This change enables sports plugins to declare validated directories so schema defaults and font grid snapping resolve correctly, with regression coverage for the production loader scenario. No current merge-blocking risk remains.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: allowing sports plugins to declare their own directory instead of relying on deduction.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/plugin-dir-from-host

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ChuckBuilds
ChuckBuilds merged commit 0730d95 into main Sep 3, 2026
8 of 9 checks passed
@ChuckBuilds
ChuckBuilds deleted the fix/plugin-dir-from-host branch September 3, 2026 21:01
ChuckBuilds added a commit to ChuckBuilds/ledmatrix-plugins that referenced this pull request Sep 3, 2026
Every grid-snapped font was rendering a pixel narrow on every device.

The shared sports code reads this plugin's config_schema.json to tell a default
font size from one the user chose: a default is snapped to the font's pixel
grid, a choice is left alone. It located the schema by inspecting loaded
modules, which cannot work under the real plugin loader --
PluginLoader._namespace_plugin_modules renames a plugin's modules to
"_plg_<id>_<module>" and removes the original names, so nothing is left to
inspect.

The lookup returned nothing, every configured size then looked user-chosen, the
snap was skipped, and 4x6-font.ttf drew at 6 instead of 7: 3px-wide glyphs
instead of 4px. On a 256x64 panel the betting odds, the team records and the
date row were hard to read.

Each SportsCore now declares _PLUGIN_DIR from its own __file__, which the
shared code prefers over guessing (ChuckBuilds/LEDMatrix#519).

scripts/test_plugin_dir_under_loader.py stops this recurring. It reproduces the
loader's sequence exactly -- import sports.py under its BARE name, then rename
it away -- because that is what makes the class report __module__ == "sports"
with no such entry in sys.modules.

Both halves matter, and the first draft of this guard got it wrong: it imported
under the namespaced name, __module__ still resolved, and it passed against the
broken code. Verified the corrected version fails for all eight with the
declarations removed and passes with them restored.

Found by a user counting pixels on a photo of the panel. No gate here caught
it: every test imported plugins directly, which leaves the bare module entry in
place, and the safety harness loads plugins its own way.

Co-authored-by: Claude <noreply@anthropic.com>
@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 0 complexity · 0 duplication

Metric Results
Complexity 0
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

ChuckBuilds added a commit that referenced this pull request Sep 4, 2026
…ilently (#520)

_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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant