Fix lint, format and typing findings and pin dev tools

Order imports, format with ruff, make the account key constants Final
so mypy --strict accepts them, and type the test helpers. Configure
ruff and mypy in pyproject.toml and pin the dev tool versions. All
modules, classes and functions already carry Doxygen comments;
doxygen Doxyfile builds without warnings.

Task: MIL-004#2
Closes #18
This commit is contained in:
2026-10-04 23:59:15 +08:00
parent 6e52a242fa
commit 4abf984382
6 changed files with 74 additions and 29 deletions
+14 -1
View File
@@ -12,7 +12,7 @@ license = { file = "LICENSE" }
dependencies = [] dependencies = []
[project.optional-dependencies] [project.optional-dependencies]
dev = ["pytest>=8"] dev = ["pytest>=9,<10", "ruff>=0.16,<0.17", "mypy>=2.4,<3"]
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
where = ["src"] where = ["src"]
@@ -20,3 +20,16 @@ where = ["src"]
[tool.pytest.ini_options] [tool.pytest.ini_options]
testpaths = ["tests"] testpaths = ["tests"]
pythonpath = ["src"] pythonpath = ["src"]
[tool.ruff]
line-length = 88
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "FLY", "B"]
[tool.ruff.lint.per-file-ignores]
"src/higher_lower/game_data.py" = ["E501"] # assignment data kept verbatim
[tool.mypy]
strict = true
mypy_path = "src"
+28 -14
View File
@@ -9,23 +9,37 @@ string, so Doxygen does not read the backticks in the art as markup.
## The game logo. ## The game logo.
logo = ( logo = (
"\n" "\n"
r" __ ___ __ " "\n" r" __ ___ __ "
r" / / / (_)___ _/ /_ ___ _____" "\n" "\n"
r" / /_/ / / __ `/ __ \/ _ \/ ___/" "\n" r" / / / (_)___ _/ /_ ___ _____"
r" / __ / / /_/ / / / / __/ / " "\n" "\n"
r"/_/ ///_/\__, /_/ /_/\___/_/ " "\n" r" / /_/ / / __ `/ __ \/ _ \/ ___/"
r" / / /____/_ _____ _____" "\n" "\n"
r" / / / __ \ | /| / / _ \/ ___/" "\n" r" / __ / / /_/ / / / / __/ / "
r" / /___/ /_/ / |/ |/ / __/ / " "\n" "\n"
r"/_____/\____/|__/|__/\___/_/ " "\n" r"/_/ ///_/\__, /_/ /_/\___/_/ "
"\n"
r" / / /____/_ _____ _____"
"\n"
r" / / / __ \ | /| / / _ \/ ___/"
"\n"
r" / /___/ /_/ / |/ |/ / __/ / "
"\n"
r"/_____/\____/|__/|__/\___/_/ "
"\n"
) )
## The "vs" shown between the two accounts. ## The "vs" shown between the two accounts.
vs = ( vs = (
"\n" "\n"
r" _ __ " "\n" r" _ __ "
r"| | / /____" "\n" "\n"
r"| | / / ___/" "\n" r"| | / /____"
r"| |/ (__ ) " "\n" "\n"
r"|___/____(_)" "\n" r"| | / / ___/"
"\n"
r"| |/ (__ ) "
"\n"
r"|___/____(_)"
"\n"
) )
+6 -4
View File
@@ -2,11 +2,13 @@
@brief Constants shared by the game modules. @brief Constants shared by the game modules.
""" """
from typing import Final
## Keys of an account dictionary. ## Keys of an account dictionary.
KEY_NAME = "name" KEY_NAME: Final = "name"
KEY_FOLLOWER_COUNT = "follower_count" KEY_FOLLOWER_COUNT: Final = "follower_count"
KEY_DESCRIPTION = "description" KEY_DESCRIPTION: Final = "description"
KEY_COUNTRY = "country" KEY_COUNTRY: Final = "country"
## The two choices the player can type, in lower case. ## The two choices the player can type, in lower case.
CHOICE_A = "a" CHOICE_A = "a"
+2 -2
View File
@@ -1,5 +1,5 @@
"""@file game_logic.py """@file game_logic.py
@brief Pure functions of the game: drawing accounts, describing them and checking guesses. @brief Pure game functions: draw accounts, describe them, check guesses.
""" """
import random import random
@@ -9,9 +9,9 @@ from higher_lower.constants import (
ACCOUNT_TEMPLATE, ACCOUNT_TEMPLATE,
CHOICE_A, CHOICE_A,
CHOICE_B, CHOICE_B,
KEY_COUNTRY,
KEY_DESCRIPTION, KEY_DESCRIPTION,
KEY_NAME, KEY_NAME,
KEY_COUNTRY,
) )
from higher_lower.game_data import Account, data from higher_lower.game_data import Account, data
+23 -6
View File
@@ -14,20 +14,37 @@ from higher_lower.game import ask_choice, play_game, show_round
from higher_lower.game_data import Account from higher_lower.game_data import Account
ACCOUNTS: list[Account] = [ ACCOUNTS: list[Account] = [
{"name": "First", "follower_count": 30, "description": "Test one", "country": "Denmark"}, {
{"name": "Second", "follower_count": 20, "description": "Test two", "country": "Norway"}, "name": "First",
{"name": "Third", "follower_count": 10, "description": "Test three", "country": "Sweden"}, "follower_count": 30,
"description": "Test one",
"country": "Denmark",
},
{
"name": "Second",
"follower_count": 20,
"description": "Test two",
"country": "Norway",
},
{
"name": "Third",
"follower_count": 10,
"description": "Test three",
"country": "Sweden",
},
] ]
class FirstRng(random.Random): class FirstRng(random.Random):
"""@brief Random generator that always takes the first entries, so a game is fixed.""" """@brief Random generator that takes the first entries, so a game is fixed."""
def sample(self, population: Sequence, k: int, **kwargs: object) -> list: # type: ignore[override] def sample( # type: ignore[override]
self, population: Sequence[Account], k: int, **kwargs: object
) -> list[Account]:
"""@brief Return the first k entries.""" """@brief Return the first k entries."""
return list(population)[:k] return list(population)[:k]
def choice(self, seq: Sequence) -> Account: # type: ignore[override] def choice(self, seq: Sequence[Account]) -> Account: # type: ignore[override]
"""@brief Return the first entry.""" """@brief Return the first entry."""
return seq[0] return seq[0]
+1 -2
View File
@@ -58,8 +58,7 @@ def test_format_data_describes_the_account() -> None:
"country": "United States", "country": "United States",
} }
assert ( assert (
format_data(account) format_data(account) == "Nike, a Sportswear multinational, from United States"
== "Nike, a Sportswear multinational, from United States"
) )