Skip to content

Commit 4d4fe7e

Browse files
committed
chore: unify linting with pre-commit in CI
1 parent 3382351 commit 4d4fe7e

12 files changed

Lines changed: 115 additions & 57 deletions

File tree

.github/workflows/ci.yml

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,19 @@ on:
77
branches: [main]
88

99
jobs:
10+
lint:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- name: Set up Python
16+
uses: actions/setup-python@v5
17+
with:
18+
python-version: "3.11"
19+
20+
- name: Run pre-commit
21+
uses: pre-commit/action@v3.0.1
22+
1023
test:
1124
runs-on: ubuntu-latest
1225
strategy:
@@ -27,14 +40,5 @@ jobs:
2740
- name: Install dependencies
2841
run: uv sync --all-extras
2942

30-
- name: Run ruff check
31-
run: uv run ruff check src/
32-
33-
- name: Run ruff format check
34-
run: uv run ruff format --check src/
35-
36-
- name: Run mypy
37-
run: uv run mypy src/
38-
3943
- name: Run tests
4044
run: uv run pytest -v

.pre-commit-config.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,13 @@ repos:
1414
- id: check-yaml
1515
- id: check-added-large-files
1616
- id: check-toml
17+
18+
- repo: https://github.com/pre-commit/mirrors-mypy
19+
rev: v1.14.1
20+
hooks:
21+
- id: mypy
22+
pass_filenames: false
23+
additional_dependencies:
24+
- pydantic>=2.0.0
25+
- httpx>=0.27.0
26+
- pytest>=8.0.0

examples/leaderboards.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525
phase_lb = mcsrranked.leaderboards.phase()
2626
if phase_lb.phase.number:
2727
print(f"Phase {phase_lb.phase.number}")
28-
for player in phase_lb.users[:5]:
29-
points = player.season_result.phase_point
30-
print(f"{player.nickname}: {points} points")
28+
for phase_player in phase_lb.users[:5]:
29+
points = phase_player.season_result.phase_point
30+
print(f"{phase_player.nickname}: {points} points")
3131
print()
3232

3333
# Record leaderboard (best times)

examples/matches.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ def format_time(ms: int) -> str:
5454

5555
print("Elo Changes:")
5656
for change in match.changes:
57-
player = next((p for p in match.players if p.uuid == change.uuid), None)
58-
name = player.nickname if player else change.uuid
57+
found_player = next((p for p in match.players if p.uuid == change.uuid), None)
58+
name = found_player.nickname if found_player else change.uuid
5959
change_str = (
6060
f"+{change.change}"
6161
if change.change and change.change > 0
@@ -67,6 +67,6 @@ def format_time(ms: int) -> str:
6767
if match.timelines:
6868
print("Timeline:")
6969
for event in match.timelines[:10]:
70-
player = next((p for p in match.players if p.uuid == event.uuid), None)
71-
name = player.nickname if player else event.uuid[:8]
70+
found_player = next((p for p in match.players if p.uuid == event.uuid), None)
71+
name = found_player.nickname if found_player else event.uuid[:8]
7272
print(f" {format_time(event.time)} - {name}: {event.type}")

examples/versus.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ def format_time(ms: int) -> str:
3030
total = ranked.get("total", 0)
3131
for uuid, wins in ranked.items():
3232
if uuid != "total":
33-
player = next((p for p in stats.players if p.uuid == uuid), None)
34-
name = player.nickname if player else uuid[:8]
33+
found_player = next((p for p in stats.players if p.uuid == uuid), None)
34+
name = found_player.nickname if found_player else uuid[:8]
3535
print(f" {name}: {wins} wins")
3636
print(f" Total matches: {total}")
3737
print()
@@ -42,17 +42,17 @@ def format_time(ms: int) -> str:
4242
total = casual.get("total", 0)
4343
for uuid, wins in casual.items():
4444
if uuid != "total":
45-
player = next((p for p in stats.players if p.uuid == uuid), None)
46-
name = player.nickname if player else uuid[:8]
45+
found_player = next((p for p in stats.players if p.uuid == uuid), None)
46+
name = found_player.nickname if found_player else uuid[:8]
4747
print(f" {name}: {wins} wins")
4848
print(f" Total matches: {total}")
4949
print()
5050

5151
# Display elo changes
5252
print("Total Elo Changes:")
5353
for uuid, change in stats.changes.items():
54-
player = next((p for p in stats.players if p.uuid == uuid), None)
55-
name = player.nickname if player else uuid[:8]
54+
found_player = next((p for p in stats.players if p.uuid == uuid), None)
55+
name = found_player.nickname if found_player else uuid[:8]
5656
sign = "+" if change > 0 else ""
5757
print(f" {name}: {sign}{change}")
5858
print()

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ quote-style = "double"
7777
indent-style = "space"
7878

7979
[tool.mypy]
80+
files = ["src", "tests", "examples"]
8081
python_version = "3.11"
8182
strict = true
8283
warn_return_any = true

src/mcsrranked/types/user.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
from typing import Any
4+
35
from pydantic import BaseModel, Field, model_validator
46

57
from mcsrranked.types.shared import Achievement
@@ -48,7 +50,7 @@ class MatchTypeStats(BaseModel):
4850
model_config = {"populate_by_name": True}
4951

5052

51-
def _pivot_stats(data: dict) -> dict:
53+
def _pivot_stats(data: dict[str, Any]) -> dict[str, Any]:
5254
"""Transform API format {stat: {mode: val}} to {mode: {stat: val}}.
5355
5456
The API returns statistics grouped by stat type first (e.g., wins.ranked),
@@ -65,8 +67,8 @@ def _pivot_stats(data: dict) -> dict:
6567
return data
6668

6769
# Pivot from {wins: {ranked: X, casual: Y}} to {ranked: {wins: X}, casual: {wins: Y}}
68-
ranked: dict = {}
69-
casual: dict = {}
70+
ranked: dict[str, Any] = {}
71+
casual: dict[str, Any] = {}
7072

7173
field_mapping = {
7274
# API key -> model field name
@@ -107,7 +109,7 @@ class SeasonStats(BaseModel):
107109

108110
@model_validator(mode="before")
109111
@classmethod
110-
def pivot_stats(cls, data: dict) -> dict:
112+
def pivot_stats(cls, data: dict[str, Any]) -> dict[str, Any]:
111113
"""Transform API stat-first format to mode-first format."""
112114
return _pivot_stats(data)
113115

@@ -126,7 +128,7 @@ class TotalStats(BaseModel):
126128

127129
@model_validator(mode="before")
128130
@classmethod
129-
def pivot_stats(cls, data: dict) -> dict:
131+
def pivot_stats(cls, data: dict[str, Any]) -> dict[str, Any]:
130132
"""Transform API stat-first format to mode-first format."""
131133
return _pivot_stats(data)
132134

tests/conftest.py

Lines changed: 54 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
"""Pytest configuration and fixtures."""
22

33
import json
4+
from collections.abc import Generator
45
from pathlib import Path
6+
from typing import Any
57

68
import pytest
79
import respx
@@ -11,10 +13,11 @@
1113
FIXTURES_DIR = Path(__file__).parent / "fixtures"
1214

1315

14-
def load_fixture(name: str) -> dict | list:
16+
def load_fixture(name: str) -> dict[str, Any] | list[Any]:
1517
"""Load a JSON fixture file."""
1618
with open(FIXTURES_DIR / name) as f:
17-
return json.load(f)
19+
data: dict[str, Any] | list[Any] = json.load(f)
20+
return data
1821

1922

2023
@pytest.fixture
@@ -24,79 +27,103 @@ def client() -> MCSRRanked:
2427

2528

2629
@pytest.fixture
27-
def mock_api() -> respx.MockRouter:
30+
def mock_api() -> Generator[respx.MockRouter, None, None]:
2831
"""Create a mock API router."""
2932
with respx.mock(base_url="https://api.mcsrranked.com") as respx_mock:
3033
yield respx_mock
3134

3235

3336
@pytest.fixture
34-
def user_fixture() -> dict:
37+
def user_fixture() -> dict[str, Any]:
3538
"""Load user.json fixture (Feinberg's profile)."""
36-
return load_fixture("user.json")
39+
result = load_fixture("user.json")
40+
assert isinstance(result, dict)
41+
return result
3742

3843

3944
@pytest.fixture
40-
def user_matches_fixture() -> list:
45+
def user_matches_fixture() -> list[Any]:
4146
"""Load user_matches.json fixture (Feinberg's recent matches)."""
42-
return load_fixture("user_matches.json")
47+
result = load_fixture("user_matches.json")
48+
assert isinstance(result, list)
49+
return result
4350

4451

4552
@pytest.fixture
46-
def user_seasons_fixture() -> dict:
53+
def user_seasons_fixture() -> dict[str, Any]:
4754
"""Load user_seasons.json fixture (Feinberg's season history)."""
48-
return load_fixture("user_seasons.json")
55+
result = load_fixture("user_seasons.json")
56+
assert isinstance(result, dict)
57+
return result
4958

5059

5160
@pytest.fixture
52-
def versus_fixture() -> dict:
61+
def versus_fixture() -> dict[str, Any]:
5362
"""Load versus.json fixture (Feinberg vs Couriway stats)."""
54-
return load_fixture("versus.json")
63+
result = load_fixture("versus.json")
64+
assert isinstance(result, dict)
65+
return result
5566

5667

5768
@pytest.fixture
58-
def versus_matches_fixture() -> list:
69+
def versus_matches_fixture() -> list[Any]:
5970
"""Load versus_matches.json fixture (Feinberg vs Couriway matches)."""
60-
return load_fixture("versus_matches.json")
71+
result = load_fixture("versus_matches.json")
72+
assert isinstance(result, list)
73+
return result
6174

6275

6376
@pytest.fixture
64-
def matches_fixture() -> list:
77+
def matches_fixture() -> list[Any]:
6578
"""Load matches.json fixture (recent ranked matches)."""
66-
return load_fixture("matches.json")
79+
result = load_fixture("matches.json")
80+
assert isinstance(result, list)
81+
return result
6782

6883

6984
@pytest.fixture
70-
def match_detail_fixture() -> dict:
85+
def match_detail_fixture() -> dict[str, Any]:
7186
"""Load match_detail.json fixture (single match details)."""
72-
return load_fixture("match_detail.json")
87+
result = load_fixture("match_detail.json")
88+
assert isinstance(result, dict)
89+
return result
7390

7491

7592
@pytest.fixture
76-
def leaderboard_fixture() -> dict:
93+
def leaderboard_fixture() -> dict[str, Any]:
7794
"""Load leaderboard.json fixture (elo leaderboard)."""
78-
return load_fixture("leaderboard.json")
95+
result = load_fixture("leaderboard.json")
96+
assert isinstance(result, dict)
97+
return result
7998

8099

81100
@pytest.fixture
82-
def phase_leaderboard_fixture() -> dict:
101+
def phase_leaderboard_fixture() -> dict[str, Any]:
83102
"""Load phase_leaderboard.json fixture."""
84-
return load_fixture("phase_leaderboard.json")
103+
result = load_fixture("phase_leaderboard.json")
104+
assert isinstance(result, dict)
105+
return result
85106

86107

87108
@pytest.fixture
88-
def record_leaderboard_fixture() -> dict:
109+
def record_leaderboard_fixture() -> list[Any]:
89110
"""Load record_leaderboard.json fixture."""
90-
return load_fixture("record_leaderboard.json")
111+
result = load_fixture("record_leaderboard.json")
112+
assert isinstance(result, list)
113+
return result
91114

92115

93116
@pytest.fixture
94-
def live_fixture() -> dict:
117+
def live_fixture() -> dict[str, Any]:
95118
"""Load live.json fixture (live matches)."""
96-
return load_fixture("live.json")
119+
result = load_fixture("live.json")
120+
assert isinstance(result, dict)
121+
return result
97122

98123

99124
@pytest.fixture
100-
def weekly_race_fixture() -> dict:
125+
def weekly_race_fixture() -> dict[str, Any]:
101126
"""Load weekly_race.json fixture."""
102-
return load_fixture("weekly_race.json")
127+
result = load_fixture("weekly_race.json")
128+
assert isinstance(result, dict)
129+
return result

tests/test_client.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# mypy: disable-error-code="no-untyped-def,misc"
12
"""Tests for the client module."""
23

34
import httpx

tests/test_field_coverage.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# mypy: disable-error-code="no-untyped-def"
2+
"""Field coverage tests with isinstance() checks."""
3+
14
from mcsrranked.types.leaderboard import (
25
EloLeaderboard,
36
LeaderboardSeasonResult,
@@ -131,6 +134,7 @@ def test_season_result_all_fields(self, user_fixture):
131134
def test_last_season_state_all_fields(self, user_fixture):
132135
"""Verify all LastSeasonState fields."""
133136
user = User.model_validate(user_fixture)
137+
assert user.season_result is not None
134138
last = user.season_result.last
135139

136140
assert last.elo_rate is None or isinstance(last.elo_rate, int)
@@ -504,7 +508,7 @@ class TestVodInfoFieldCoverage:
504508
def test_vod_info_structure(self):
505509
"""Test VodInfo can be constructed with all fields."""
506510
# VodInfo is rare in the fixtures, so test the model directly
507-
vod = VodInfo(uuid="abc123", url="https://twitch.tv/example", startsAt=12345)
511+
vod = VodInfo(uuid="abc123", url="https://twitch.tv/example", starts_at=12345)
508512

509513
assert isinstance(vod.uuid, str)
510514
assert isinstance(vod.url, str)
@@ -530,7 +534,7 @@ class TestPhaseResultFieldCoverage:
530534
def test_phase_result_structure(self):
531535
"""Test PhaseResult can be constructed with all fields."""
532536
# Phases are often empty, so test the model directly
533-
phase = PhaseResult(phase=1, eloRate=1500, eloRank=100, point=50)
537+
phase = PhaseResult(phase=1, elo_rate=1500, elo_rank=100, point=50)
534538

535539
assert isinstance(phase.phase, int)
536540
assert isinstance(phase.elo_rate, int)

0 commit comments

Comments
 (0)