Commit 953307b177
953307b17732f8988ae099932eb2d7f4ec70404e
parent: 232e808bd8
Verified · cmc
cmc <hello@cleberg.net> · 2026-08-22 23:52 UTC
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.
Layout: unified · split
.github/workflows/pylint.yml
+5
| @@ -19,8 +19,13 @@ jobs: |
| 19 | 19 | run: | |
| 20 | 20 | python -m pip install --upgrade pip |
| 21 | 21 | pip install -r requirements.txt |
| 22 | # Pinned. Unpinned, this installs whatever ruff shipped this week, and a |
| 23 | # release that adds a rule turns the build red with no change to the code — |
| 24 | # which is what happened between May and August 2026. |
| 22 | 25 | - name: Install Ruff |
| 23 | 26 | uses: astral-sh/ruff-action@v3.2.2 |
| 27 | with: |
| 28 | version: "0.16.4" |
| 24 | 29 | - name: Ruff Actions |
| 25 | 30 | run: | |
| 26 | 31 | ruff check --fix |
nba/box_score.py
+1 −1
| @@ -4,8 +4,8 @@ Fetches and formats live box scores for individual games. |
| 4 | 4 | |
| 5 | 5 | import json |
| 6 | 6 | |
| 7 | | from tabulate import tabulate |
| 8 | 7 | from nba_api.live.nba.endpoints.boxscore import BoxScore |
| 8 | from tabulate import tabulate |
| 9 | 9 | |
| 10 | 10 | BOLD = "\033[1m" |
| 11 | 11 | END = "\033[0m" |
nba/bracket.py
+8 −5
| @@ -2,10 +2,11 @@ |
| 2 | 2 | Fetches and formats the NBA playoff bracket. |
| 3 | 3 | """ |
| 4 | 4 | |
| 5 | from __future__ import annotations |
| 6 | |
| 5 | 7 | import json |
| 6 | 8 | import re |
| 7 | 9 | from collections import defaultdict |
| 8 | | from typing import Optional |
| 9 | 10 | |
| 10 | 11 | from nba_api.stats.endpoints.commonplayoffseries import CommonPlayoffSeries |
| 11 | 12 | from nba_api.stats.endpoints.leaguegamelog import LeagueGameLog |
| @@ -59,8 +60,10 @@ def get_bracket_table(data: dict) -> str: |
| 59 | 60 | return "\n".join( |
| 60 | 61 | [ |
| 61 | 62 | _center(f"{BOLD}NBA Playoff Bracket{END}", 74), |
| 62 | | f"{GREEN}* Advanced{END} {RED}x Eliminated{END} " |
| 63 | | f"{CYAN}> Series lead{END} {YELLOW}! Can clinch next win{END}", |
| 63 | ( |
| 64 | f"{GREEN}* Advanced{END} {RED}x Eliminated{END} " |
| 65 | f"{CYAN}> Series lead{END} {YELLOW}! Can clinch next win{END}" |
| 66 | ), |
| 64 | 67 | "", |
| 65 | 68 | _render_conference("Western", summaries), |
| 66 | 69 | "", |
| @@ -205,11 +208,11 @@ def _conference_round(summaries: dict, conference: str, round_number: int) -> li |
| 205 | 208 | ) |
| 206 | 209 | |
| 207 | 210 | |
| 208 | | def _slot(items: list, idx: int) -> Optional[dict]: |
| 211 | def _slot(items: list, idx: int) -> dict | None: |
| 209 | 212 | return items[idx] if idx < len(items) else None |
| 210 | 213 | |
| 211 | 214 | |
| 212 | | def _series_box(summary: Optional[dict], width: int) -> str: |
| 215 | def _series_box(summary: dict | None, width: int) -> str: |
| 213 | 216 | if summary is None: |
| 214 | 217 | return _blank(width, "TBD") |
| 215 | 218 | |
nba/cli.py
+1
| @@ -5,6 +5,7 @@ It imports the required modules and sets up a parser with basic options for demo |
| 5 | 5 | """ |
| 6 | 6 | |
| 7 | 7 | import argparse |
| 8 | |
| 8 | 9 | from nba import fetch_data, scores, standings |
| 9 | 10 | |
| 10 | 11 | |
nba/fetch_data.py
+1
| @@ -3,6 +3,7 @@ Fetches data for use in other modules. |
| 3 | 3 | """ |
| 4 | 4 | |
| 5 | 5 | import json |
| 6 | |
| 6 | 7 | from nba_api.live.nba.endpoints import scoreboard |
| 7 | 8 | from nba_api.stats.endpoints import leaguestandings |
| 8 | 9 | |
nba/leaders.py
+1 −1
| @@ -4,8 +4,8 @@ Fetches and formats NBA statistical leaders. |
| 4 | 4 | |
| 5 | 5 | import json |
| 6 | 6 | |
| 7 | | from tabulate import tabulate |
| 8 | 7 | from nba_api.stats.endpoints.leagueleaders import LeagueLeaders |
| 8 | from tabulate import tabulate |
| 9 | 9 | |
| 10 | 10 | BOLD = "\033[1m" |
| 11 | 11 | END = "\033[0m" |
nba/playoff.py
+1 −1
| @@ -4,8 +4,8 @@ Fetches and formats the NBA playoff picture. |
| 4 | 4 | |
| 5 | 5 | import json |
| 6 | 6 | |
| 7 | | from tabulate import tabulate |
| 8 | 7 | from nba_api.stats.endpoints.playoffpicture import PlayoffPicture |
| 8 | from tabulate import tabulate |
| 9 | 9 | |
| 10 | 10 | BOLD = "\033[1m" |
| 11 | 11 | END = "\033[0m" |
nba/tui/app.py
+5 −3
| @@ -6,17 +6,19 @@ from __future__ import annotations |
| 6 | 6 | |
| 7 | 7 | import asyncio |
| 8 | 8 | from pathlib import Path |
| 9 | from typing import ClassVar |
| 9 | 10 | |
| 10 | 11 | from rich.text import Text |
| 11 | 12 | from textual.app import App, ComposeResult |
| 13 | from textual.binding import BindingType |
| 12 | 14 | from textual.containers import Horizontal |
| 13 | 15 | from textual.widgets import Footer, Header, Static, TabbedContent, TabPane |
| 14 | 16 | |
| 15 | | from nba import fetch_data |
| 17 | from nba import box_score as box_score_mod |
| 16 | 18 | from nba import bracket as bracket_mod |
| 19 | from nba import fetch_data |
| 17 | 20 | from nba import leaders as leaders_mod |
| 18 | 21 | from nba import playoff as playoff_mod |
| 19 | | from nba import box_score as box_score_mod |
| 20 | 22 | from nba.scores import get_scoreboard_table |
| 21 | 23 | from nba.standings import get_east_standings_table, get_west_standings_table |
| 22 | 24 | from nba.tui.widgets import CountdownBar, ScoresWidget |
| @@ -27,7 +29,7 @@ class NBAApp(App): |
| 27 | 29 | |
| 28 | 30 | CSS_PATH = Path(__file__).parent / "styles.tcss" |
| 29 | 31 | |
| 30 | | BINDINGS = [ |
| 32 | BINDINGS: ClassVar[list[BindingType]] = [ |
| 31 | 33 | ("q", "quit", "Quit"), |
| 32 | 34 | ("s", "show_scores", "Scores"), |
| 33 | 35 | ("t", "show_standings", "Standings"), |
pyproject.toml
+20
| @@ -39,3 +39,23 @@ include = ["nba*"] |
| 39 | 39 | |
| 40 | 40 | [tool.setuptools.package-data] |
| 41 | 41 | "nba.tui" = ["*.tcss"] |
| 42 | |
| 43 | [tool.ruff.lint] |
| 44 | # ruff's defaults widen with each release, and the workflow installs whatever is |
| 45 | # newest — which is how this repo went from green in May to sixteen findings in |
| 46 | # August without a line of its own code changing. The rules below are the two |
| 47 | # that fight the code rather than improve it; everything else ruff flags is |
| 48 | # treated as worth fixing. |
| 49 | ignore = [ |
| 50 | # Blind `except Exception`. Deliberate here: a duration parser that falls back |
| 51 | # to the raw string, and TUI handlers that render "Error loading …" instead of |
| 52 | # taking the whole app down. Narrowing them would make the app crash on the |
| 53 | # cases they exist to absorb. |
| 54 | "BLE001", |
| 55 | ] |
| 56 | |
| 57 | [tool.ruff.lint.per-file-ignores] |
| 58 | # Textual's compose() nests `with` blocks because the nesting *is* the widget |
| 59 | # tree. Collapsing them into a single `with A, B:` would flatten a hierarchy |
| 60 | # that is meant to be read as one. |
| 61 | "nba/tui/app.py" = ["SIM117"] |