Skip to content

fix(scoreboards): import datetime, timezone and ZoneInfo in game_renderer - #339

Merged
ChuckBuilds merged 3 commits into
mainfrom
fix/scoreboard-weekday-nameerror
Aug 30, 2026
Merged

fix(scoreboards): import datetime, timezone and ZoneInfo in game_renderer#339
ChuckBuilds merged 3 commits into
mainfrom
fix/scoreboard-weekday-nameerror

Conversation

@ChuckBuilds

@ChuckBuilds ChuckBuilds commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Summary

scroll_card.date_format offers "weekday" (documented as weekday "Fri Sep 19") in every scoreboard's config_schema.json, but seven of the eight game_renderer.py copies used datetime, timezone and ZoneInfo without importing any of them. Choosing that option raised NameError, and neither helper's except catches it — _weekday_for catches (ValueError, TypeError), _card_tzinfo catches (KeyError, ValueError, TypeError, OSError) — so it propagated out of the render instead of falling back to the plain date.

Reproduced directly:

>>> GameRenderer(128, 64, {"scroll_card": {"date_format": "weekday"}})._format_game_date("10/12", game)
NameError: name 'datetime' is not defined

hockey-scoreboard already had the imports and rendered the option correctly. The other seven now match it.

Type of change

  • Bug fix in an existing plugin

Plugin(s) affected

afl, baseball, basketball, football, lacrosse, nrl, soccer (all -scoreboard).

game_renderer.py is a shared-shape module, so all lineage members are fixed in this PR per docs/plugin-development/08-shared-sports-code.md. baseball needed only timezone and ZoneInfo — it already imported datetime. Hockey is untouched.

Related issues

N/A — found by pyflakes (undefined name) while reviewing a static-analysis report on #337, then traced to a reachable config path.

Why nothing caught it

The render harness and every golden image use the default abbrev format, so the weekday branch never executed. Two lines of imports were missing for as long as the feature has existed and no test touched it.

Test plan

  • Safety harness green for all seven: 24/24 each (lacrosse 16/16).
  • Each plugin's own suite matches its origin/main baseline exactly, pass for pass and fail for fail — baseline captured by stashing this change, so the pre-existing Windows-environment failures are accounted for rather than assumed.
  • check_module_collisions.py clean across 43 plugins.
  • Every other repo-level guard unchanged from baseline.

New guard: scripts/test_weekday_date_format.py

Exercises the option itself across all eight copies. Every copy is called game_renderer, so each is loaded under its own module name via importlib.util rather than imported — otherwise whichever landed in sys.modules first would be silently tested eight times. Per plugin it asserts:

  • date_format: "weekday" does not raise
  • it renders an actual weekday
  • the configured timezone is applied — 01:00 UTC on the Tuesday is still Monday in New York, so a Tue result would mean the conversion was skipped or silently fell back to UTC
  • a bogus zone name falls back instead of raising
  • the default format still works

40 checks, all passing. Stripping the imports from any one plugin fails it with the original NameError. The timezone assertion self-skips where the platform has no tz database (Windows without tzdata), since the renderer correctly falls back to UTC there — an environment limitation, not a defect.

Required for plugin changes

  • Bumped version in all seven manifests, new entry at the top of versions
  • class_name / entry_point unchanged
  • No config keys changed, so no README or schema edits
  • plugins.json regenerated with update_registry.py — only the seven latest_version fields changed

Checklist

  • Commit message follows CONTRIBUTING.md
  • No secrets committed

Notes for reviewer

Rebuilt on main after #336 landed, which bumped all seven versions and touched the same files. Versions here are the post-#336 patch bumps (e.g. football 2.27.0 → 2.27.1).

Version coordination with #337: that PR takes football-scoreboard to 2.27.2, this one to 2.27.1, so they merge in either order without a re-bump. They are otherwise independent and touch different parts of game_renderer.py — imports here, the down & distance ladder there.

The except clauses are deliberately left alone. Once the names resolve, an unusable timezone falls back to UTC rather than raising, which the new guard asserts — so NameError was not masking a needed fallback, it was preventing one.

pyflakes reports one remaining finding in these files, 'freetype' imported but unused, which is pre-existing and looks intentional (an availability probe). Left as is.

@coderabbitai

coderabbitai Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7add6f55-df52-4096-b0eb-d3f89a094b45


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.

@codacy-production

codacy-production Bot commented Aug 29, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

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.

…erer

`scroll_card.date_format` offers "weekday" ('weekday "Fri Sep 19"') in
every scoreboard's config_schema.json, but seven of the eight
game_renderer.py copies used datetime, timezone and ZoneInfo without
importing any of them. Selecting that option raised NameError, and
neither helper catches it:

    _weekday_for   except (ValueError, TypeError)
    _card_tzinfo   except (KeyError, ValueError, TypeError, OSError)

so it propagated out of the render instead of falling back to the plain
date. Reproduced directly:

    >>> GameRenderer(128, 64, {"scroll_card": {"date_format": "weekday"}})
    ...     ._format_game_date("10/12", game)
    NameError: name 'datetime' is not defined

hockey-scoreboard already had the imports and rendered the option
correctly; the other seven now match it. baseball needed only timezone
and ZoneInfo -- it already imported datetime.

Affected: afl, baseball, basketball, football, lacrosse, nrl, soccer.
game_renderer.py is a shared-shape module, so all lineage members are
fixed here per docs/plugin-development/08-shared-sports-code.md.

The except clauses are left as they are: with the names resolving, an
unusable timezone falls back to UTC rather than raising, which the new
guard asserts.

Why nothing caught this: the render harness and every golden image use
the default "abbrev" format, so the weekday branch never executed. The
new scripts/test_weekday_date_format.py exercises the option itself
across all eight copies -- each in its own subprocess, since they all
import as the bare name `game_renderer` and would shadow one another.
It checks the option does not raise, renders an actual weekday, applies
the configured timezone (01:00 UTC Tuesday is still Monday in New York,
so a "Tue" result would mean the conversion was skipped), and that a
bogus zone name falls back instead of blowing up. Removing the imports
again fails it.

Verified: harness green for all seven (24/24, lacrosse 16/16); each
plugin's own suite matches its origin/main baseline exactly, pass for
pass and fail for fail; module-collision check clean.

Rebuilt on main after #336, which bumped all seven versions and touched
the same files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ChuckBuilds
ChuckBuilds force-pushed the fix/scoreboard-weekday-nameerror branch from e3c49c5 to e5aac62 Compare August 30, 2026 03:55
ChuckBuilds added a commit that referenced this pull request Aug 30, 2026
…dage

The possession football was placed a fixed 3px from the down & distance
text and guarded only by `ball_x_center > 0` -- a check that a number had
not come out negative, not that the ball fit anywhere in particular. On a
64px-wide panel it landed on the timeout bars; the guard could not see
the right-hand edge at all, so home possession pushed it off a narrow
panel; and on the adaptive 256x128 card it was drawn at x 262-274 of a
256px card, entirely off the edge.

The ball's size is unchanged -- a fixed 7x5 on the classic paths, px()
radii on the adaptive card -- and so is the wording beside it. The ball
is placed around the text now, never the other way round. Where a row
genuinely cannot hold both (a 64x32 leaves ~30px between the timeout
bars and the text alone wants 29) the down & distance wins and the ball
is omitted rather than the wording shortened.

Fixed on all three paths that draw it: football.py's classic scorebug,
GameRenderer's classic card (scroll mode), and its adaptive card.

Separately, the adaptive card drew the down & distance in PressStart2P,
~4x wider per character than the 4x6 face: on a 192x48 panel that
rendered "3rd & 8 at KC 42" 128px wide against the classic layout's
65px, and on a 256x128 it reached the full 256px, clipping the line at
both ends. That line alone now uses a dedicated ADAPTIVE_LADDER_DETAIL
on the compact 4x6 face, both rungs verified crisp
(measure_font_crispness == 0.0). The clock, status band, records and
dates keep ADAPTIVE_LADDER_TEXT and the face they had -- every recent
and upcoming golden is pixel-identical to main.

With the narrower face the yardage also fits where it previously did
not. The long form was gated on `display_width > 128`, a proxy for "is
there room" that got a 128x64 wrong: the string is 65px in the 4x6 face
and the free band there is wider. It is now preferred whenever it fits
the space the timeout bars and records leave free, the same trade the
classic layout already makes via _fit_text.

ADAPTIVE_LADDER_TEXT and the possession helpers exist only in this
plugin's game_renderer.py, so no lineage port is needed.

test_possession_ball_has_room.py pins the two properties that matter:
the ball is exactly the size it always was, and the down & distance
drawn is identical whether possession is home, away or unknown -- so the
ball can never be paid for out of the wording. 671 checks across three
render paths x eight harness sizes.

Harness 24/24. Plugin suite 26 passed with the same two pre-existing
environment failures as main.

Rebased onto main after #336, which bumped the version and touched the
same renderer; football goes to 2.27.2 since #339 takes 2.27.1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…bprocess

Codacy gates newly-introduced issues, and spawning a subprocess per plugin
introduced two Bandit findings (B404 importing subprocess, B603 call with
untrusted input) that the repo's existing users of it -- run_plugin_tests.py
and test_odds_centre_collision.py -- are grandfathered against.

Subprocesses were only ever a way to keep eight modules that are all called
`game_renderer` from shadowing one another. importlib.util does that directly:
each copy is loaded under a name of its own, with the plugin directory on
sys.path for the duration because one copy (baseball) imports a plugin-local
helper. Fewer moving parts and faster, and the file is now clean under
pyflakes, pycodestyle and Bandit alike.

Same 40 checks, same coverage: stripping the imports from a plugin still fails
it with the original NameError.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ChuckBuilds added a commit that referenced this pull request Aug 30, 2026
…dage (#337)

The possession football was placed a fixed 3px from the down & distance
text and guarded only by `ball_x_center > 0` -- a check that a number had
not come out negative, not that the ball fit anywhere in particular. On a
64px-wide panel it landed on the timeout bars; the guard could not see
the right-hand edge at all, so home possession pushed it off a narrow
panel; and on the adaptive 256x128 card it was drawn at x 262-274 of a
256px card, entirely off the edge.

The ball's size is unchanged -- a fixed 7x5 on the classic paths, px()
radii on the adaptive card -- and so is the wording beside it. The ball
is placed around the text now, never the other way round. Where a row
genuinely cannot hold both (a 64x32 leaves ~30px between the timeout
bars and the text alone wants 29) the down & distance wins and the ball
is omitted rather than the wording shortened.

Fixed on all three paths that draw it: football.py's classic scorebug,
GameRenderer's classic card (scroll mode), and its adaptive card.

Separately, the adaptive card drew the down & distance in PressStart2P,
~4x wider per character than the 4x6 face: on a 192x48 panel that
rendered "3rd & 8 at KC 42" 128px wide against the classic layout's
65px, and on a 256x128 it reached the full 256px, clipping the line at
both ends. That line alone now uses a dedicated ADAPTIVE_LADDER_DETAIL
on the compact 4x6 face, both rungs verified crisp
(measure_font_crispness == 0.0). The clock, status band, records and
dates keep ADAPTIVE_LADDER_TEXT and the face they had -- every recent
and upcoming golden is pixel-identical to main.

With the narrower face the yardage also fits where it previously did
not. The long form was gated on `display_width > 128`, a proxy for "is
there room" that got a 128x64 wrong: the string is 65px in the 4x6 face
and the free band there is wider. It is now preferred whenever it fits
the space the timeout bars and records leave free, the same trade the
classic layout already makes via _fit_text.

ADAPTIVE_LADDER_TEXT and the possession helpers exist only in this
plugin's game_renderer.py, so no lineage port is needed.

test_possession_ball_has_room.py pins the two properties that matter:
the ball is exactly the size it always was, and the down & distance
drawn is identical whether possession is home, away or unknown -- so the
ball can never be paid for out of the wording. 671 checks across three
render paths x eight harness sizes.

Harness 24/24. Plugin suite 26 passed with the same two pre-existing
environment failures as main.

Rebased onto main after #336, which bumped the version and touched the
same renderer; football goes to 2.27.2 since #339 takes 2.27.1.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Chuck <chuck@example.com>
Football's manifest resolved by renumbering this branch's release entry
to 2.27.3, dated today, on top of main's 2.27.1 and 2.27.2 -- 2.27.1 was
claimed here before #335 took it. plugins.json regenerated by the hook.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YF7Q48EYCCkU1Vs932uDY1
@ChuckBuilds
ChuckBuilds merged commit 0855372 into main Aug 30, 2026
4 checks passed
@ChuckBuilds
ChuckBuilds deleted the fix/scoreboard-weekday-nameerror branch August 30, 2026 14:48
ChuckBuilds pushed a commit that referenced this pull request Aug 30, 2026
Seven manifests resolved by keeping each plugin's colour release on top
of main's arrays; plugins.json regenerated by the hook.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YF7Q48EYCCkU1Vs932uDY1
ChuckBuilds added a commit that referenced this pull request Aug 30, 2026
main landed four commits that touch the same files as this branch:

  0855372 fix(scoreboards): import datetime, timezone and ZoneInfo in game_renderer (#339)
  6e8b863 fix(football-scoreboard): give the possession ball room (#337)
  85a5555 feat(football): favourites first, ranked others, and a rotation (#335)
  93bd554 feat(sports): apply the matchup separator settings to every mode (#336)

17 conflicts: eight sports.py, eight manifest.json, and the generated plugins.json.

sports.py -- every conflict was the same shape. #336 moved the upcoming card's
centre (matchup separator, or the date and time stacked, or nothing) out of the
inline scorebug code and into _draw_upcoming_center_switch, which is exactly
where this branch had scaled the stacked date/time offsets. Those edits are dead
weight now: SportsUpcoming sets _DRAWS_SCORE = False, so _time_font_size()
returns the un-grown 8 and max(7, 8-1) / max(9, 8+1) are the original 7 and 9.
Resolved to main's side throughout, then re-asserted _DRAWS_SCORE on the
SportsUpcoming that main's restructure left behind (football's #335 moved that
class's body, so the flag came away with the hunk).

Audited rather than assumed: diffing each resolved file against origin/main
leaves 200-300 added lines -- the helper block and its comments -- and 3 to 6
removed, each one a line this branch deliberately replaced (return fonts, the
1.5x max_width, score_y's -14 and -3, date_y's -7, football's two "00-00"
probes and its centre-gap return). No upcoming-card line is removed, so #336's
relocation is intact.

manifest.json -- main released the very version numbers this branch had claimed
and then some, so the branch entries could not be kept. Took main's manifest
whole, including its full versions[] history, and re-stacked this branch's entry
on top a minor above where main now sits: afl 1.16.0, baseball 1.34.0,
basketball 1.23.0, football 2.28.0, hockey 1.19.0, lacrosse 1.18.0, nrl 1.15.0,
soccer 2.18.0. CHANGELOG headings follow. plugins.json regenerated.

Verified against the merged origin/main across ten sizes (64x32, 128x32, 256x32,
64x64, 96x48, 192x48, 128x64, 256x64, 256x128, 384x96):

  * 240 harness renders, all PASS, no overflow or fill warnings.
  * All 80 upcoming renders byte-identical to main -- the screen draws no score
    and this branch leaves it alone.
  * Live and recent byte-identical at 64x32, 128x32 and 256x32 for every plugin,
    and football also at 64x64. Taller panels take the larger score.
  * Adaptive layout: only 192x48 live and recent move; the "VS" separator and
    every other size are byte-identical.
  * Plugin self-test failures identical to origin/main (11 pre-existing on this
    machine), including main's new test_switch_upcoming_center, which covers the
    region resolved to its side.
  * check_module_collisions clean across 43 plugins.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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