krz/nba-scores

A CLI/TUI app for NBA scores.

clone: git clone https://gitbay.org/krz/nba-scores.git

953307b17732f8988ae099932eb2d7f4ec70404e

verified · cmc

author: Christian Cleberg <hello@cleberg.net> · 2026-08-22T23:52:06Z

Fix the lint failures and stop ruff drifting

CI has been red since 2026-08-02 with no change to this repo's code. The
workflow installs whatever ruff shipped most recently and there is no ruff
config, so every release that adds a rule turns the build red on its own. Last
green run was May.

Sixteen findings. Ten were worth fixing: import sorting, an implicit string
concat now parenthesised, typing.Optional replaced with X | None under deferred
annotations, and BINDINGS annotated ClassVar as Textual's own stubs declare it.

Two were the linter fighting the code and are now ignored with the reason
recorded next to them: blind excepts that exist to keep a duration parser and
the TUI from dying on the cases they absorb, and compose()'s nested with blocks,
where the nesting is the widget tree.

ruff is pinned in the workflow so a future release cannot turn this red again
without someone choosing to move.
 .github/workflows/pylint.yml |  5 +++++
 nba/box_score.py             |  2 +-
 nba/bracket.py               | 13 ++++++++-----
 nba/cli.py                   |  1 +
 nba/fetch_data.py            |  1 +
 nba/leaders.py               |  2 +-
 nba/playoff.py               |  2 +-
 nba/tui/app.py               |  8 +++++---
 pyproject.toml               | 20 ++++++++++++++++++++
 9 files changed, 43 insertions(+), 11 deletions(-)

diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml
index 80f35df..bb264c8 100644
--- a/.github/workflows/pylint.yml
+++ b/.github/workflows/pylint.yml
@@ -19,8 +19,13 @@ jobs:
       run: |
         python -m pip install --upgrade pip
         pip install -r requirements.txt
+    # Pinned. Unpinned, this installs whatever ruff shipped this week, and a
+    # release that adds a rule turns the build red with no change to the code —
+    # which is what happened between May and August 2026.
     - name: Install Ruff
       uses: astral-sh/ruff-action@v3.2.2
+      with:
+        version: "0.16.4"
     - name: Ruff Actions
       run: |
         ruff check --fix
diff --git a/nba/box_score.py b/nba/box_score.py
index f641397..5b1fc68 100644
--- a/nba/box_score.py
+++ b/nba/box_score.py
@@ -4,8 +4,8 @@ Fetches and formats live box scores for individual games.
 
 import json
 
-from tabulate import tabulate
 from nba_api.live.nba.endpoints.boxscore import BoxScore
+from tabulate import tabulate
 
 BOLD = "\033[1m"
 END = "\033[0m"
diff --git a/nba/bracket.py b/nba/bracket.py
index cc41fcd..e2bc1fb 100644
--- a/nba/bracket.py
+++ b/nba/bracket.py
@@ -2,10 +2,11 @@
 Fetches and formats the NBA playoff bracket.
 """
 
+from __future__ import annotations
+
 import json
 import re
 from collections import defaultdict
-from typing import Optional
 
 from nba_api.stats.endpoints.commonplayoffseries import CommonPlayoffSeries
 from nba_api.stats.endpoints.leaguegamelog import LeagueGameLog
@@ -59,8 +60,10 @@ def get_bracket_table(data: dict) -> str:
     return "\n".join(
         [
             _center(f"{BOLD}NBA Playoff Bracket{END}", 74),
-            f"{GREEN}* Advanced{END}  {RED}x Eliminated{END}  "
-            f"{CYAN}> Series lead{END}  {YELLOW}! Can clinch next win{END}",
+            (
+                f"{GREEN}* Advanced{END}  {RED}x Eliminated{END}  "
+                f"{CYAN}> Series lead{END}  {YELLOW}! Can clinch next win{END}"
+            ),
             "",
             _render_conference("Western", summaries),
             "",
@@ -205,11 +208,11 @@ def _conference_round(summaries: dict, conference: str, round_number: int) -> li
     )
 
 
-def _slot(items: list, idx: int) -> Optional[dict]:
+def _slot(items: list, idx: int) -> dict | None:
     return items[idx] if idx < len(items) else None
 
 
-def _series_box(summary: Optional[dict], width: int) -> str:
+def _series_box(summary: dict | None, width: int) -> str:
     if summary is None:
         return _blank(width, "TBD")
 
diff --git a/nba/cli.py b/nba/cli.py
index 2e04b8b..d6fd679 100644
--- a/nba/cli.py
+++ b/nba/cli.py
@@ -5,6 +5,7 @@ It imports the required modules and sets up a parser with basic options for demo
 """
 
 import argparse
+
 from nba import fetch_data, scores, standings
 
 
diff --git a/nba/fetch_data.py b/nba/fetch_data.py
index 49503cc..e1a5765 100644
--- a/nba/fetch_data.py
+++ b/nba/fetch_data.py
@@ -3,6 +3,7 @@ Fetches data for use in other modules.
 """
 
 import json
+
 from nba_api.live.nba.endpoints import scoreboard
 from nba_api.stats.endpoints import leaguestandings
 
diff --git a/nba/leaders.py b/nba/leaders.py
index 27e7901..8648cc8 100644
--- a/nba/leaders.py
+++ b/nba/leaders.py
@@ -4,8 +4,8 @@ Fetches and formats NBA statistical leaders.
 
 import json
 
-from tabulate import tabulate
 from nba_api.stats.endpoints.leagueleaders import LeagueLeaders
+from tabulate import tabulate
 
 BOLD = "\033[1m"
 END = "\033[0m"
diff --git a/nba/playoff.py b/nba/playoff.py
index 42c3b0b..5b9560b 100644
--- a/nba/playoff.py
+++ b/nba/playoff.py
@@ -4,8 +4,8 @@ Fetches and formats the NBA playoff picture.
 
 import json
 
-from tabulate import tabulate
 from nba_api.stats.endpoints.playoffpicture import PlayoffPicture
+from tabulate import tabulate
 
 BOLD = "\033[1m"
 END = "\033[0m"
diff --git a/nba/tui/app.py b/nba/tui/app.py
index 4ade311..a575271 100644
--- a/nba/tui/app.py
+++ b/nba/tui/app.py
@@ -6,17 +6,19 @@ from __future__ import annotations
 
 import asyncio
 from pathlib import Path
+from typing import ClassVar
 
 from rich.text import Text
 from textual.app import App, ComposeResult
+from textual.binding import BindingType
 from textual.containers import Horizontal
 from textual.widgets import Footer, Header, Static, TabbedContent, TabPane
 
-from nba import fetch_data
+from nba import box_score as box_score_mod
 from nba import bracket as bracket_mod
+from nba import fetch_data
 from nba import leaders as leaders_mod
 from nba import playoff as playoff_mod
-from nba import box_score as box_score_mod
 from nba.scores import get_scoreboard_table
 from nba.standings import get_east_standings_table, get_west_standings_table
 from nba.tui.widgets import CountdownBar, ScoresWidget
@@ -27,7 +29,7 @@ class NBAApp(App):
 
     CSS_PATH = Path(__file__).parent / "styles.tcss"
 
-    BINDINGS = [
+    BINDINGS: ClassVar[list[BindingType]] = [
         ("q", "quit", "Quit"),
         ("s", "show_scores", "Scores"),
         ("t", "show_standings", "Standings"),
diff --git a/pyproject.toml b/pyproject.toml
index 3bc4240..44c7326 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -39,3 +39,23 @@ include = ["nba*"]
 
 [tool.setuptools.package-data]
 "nba.tui" = ["*.tcss"]
+
+[tool.ruff.lint]
+# ruff's defaults widen with each release, and the workflow installs whatever is
+# newest — which is how this repo went from green in May to sixteen findings in
+# August without a line of its own code changing. The rules below are the two
+# that fight the code rather than improve it; everything else ruff flags is
+# treated as worth fixing.
+ignore = [
+  # Blind `except Exception`. Deliberate here: a duration parser that falls back
+  # to the raw string, and TUI handlers that render "Error loading …" instead of
+  # taking the whole app down. Narrowing them would make the app crash on the
+  # cases they exist to absorb.
+  "BLE001",
+]
+
+[tool.ruff.lint.per-file-ignores]
+# Textual's compose() nests `with` blocks because the nesting *is* the widget
+# tree. Collapsing them into a single `with A, B:` would flatten a hierarchy
+# that is meant to be read as one.
+"nba/tui/app.py" = ["SIM117"]