Skip to content

Commit 6f13685

Browse files
committed
Strengthen Python tooling and reorganize E2E driver
1 parent 24085b0 commit 6f13685

145 files changed

Lines changed: 925 additions & 517 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎build/sbc_packager/application/archive.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def create_application_archive(application_dir: Path, platform: str) -> Path:
2222

2323
def write_file_manifest(application_dir: Path) -> Path:
2424
manifest_path = application_dir / "files.md5.gz"
25-
entries = []
25+
entries: list[str] = []
2626
for path in sorted(application_dir.rglob("*")):
2727
if path == manifest_path or path.is_symlink() or not path.is_file() or path.suffix == ".dbg":
2828
continue

‎build/sbc_packager/application/cli.py‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,15 @@
44

55
import typer
66

7-
from ..editor.archive import find_engine_loading_image, normalize_platform, prepare_game_archive, resolve_game_name
8-
from ..shared.archive import write_sha256
9-
from ..shared.files import collect_generated_top_level_excludes
7+
from sbc_packager.editor.archive import (
8+
find_engine_loading_image,
9+
normalize_platform,
10+
prepare_game_archive,
11+
resolve_game_name,
12+
)
13+
from sbc_packager.shared.archive import write_sha256
14+
from sbc_packager.shared.files import collect_generated_top_level_excludes
15+
1016
from .archive import create_application_archive, write_file_manifest
1117
from .config import read_application_config, render_start_script, write_springsettings
1218
from .engine import install_engine, prune_engine, rename_engine

‎build/sbc_packager/application/config.py‎

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import json
22
from dataclasses import dataclass
33
from pathlib import Path
4-
from typing import Any
4+
from typing import cast
55

66

77
@dataclass(frozen=True)
88
class ApplicationConfig:
9-
launch: dict[str, Any]
10-
springsettings: dict[str, Any]
9+
launch: dict[str, object]
10+
springsettings: dict[str, object]
1111

1212

1313
def read_application_config(path: Path) -> ApplicationConfig:
@@ -16,12 +16,12 @@ def read_application_config(path: Path) -> ApplicationConfig:
1616
if not isinstance(data, dict):
1717
raise RuntimeError(f"Distribution configuration must be an object: {path}")
1818
return ApplicationConfig(
19-
launch=require_object(data, "launch"),
20-
springsettings=require_object(data, "springsettings"),
19+
launch=require_object(cast("dict[str, object]", data), "launch"),
20+
springsettings=require_object(cast("dict[str, object]", data), "springsettings"),
2121
)
2222

2323

24-
def render_start_script(game_name: str, launch: dict[str, Any]) -> str:
24+
def render_start_script(game_name: str, launch: dict[str, object]) -> str:
2525
map_name = require_string(launch, "map")
2626
game_options = render_game_options(launch.get("game_options"))
2727
map_options = render_options_block("MapOptions", launch.get("map_options"))
@@ -51,22 +51,22 @@ def render_start_script(game_name: str, launch: dict[str, Any]) -> str:
5151
)
5252

5353

54-
def write_springsettings(settings: dict[str, Any], destination: Path) -> Path:
54+
def write_springsettings(settings: dict[str, object], destination: Path) -> Path:
5555
values = {**settings, "DefaultStartScript": "script.txt"}
5656
output = destination / "springsettings.cfg"
5757
lines = [f"{key}={format_setting(value)}" for key, value in sorted(values.items())]
5858
output.write_text("\n".join(lines) + "\n", encoding="utf-8")
5959
return output
6060

6161

62-
def require_object(values: dict[str, Any], key: str) -> dict[str, Any]:
62+
def require_object(values: dict[str, object], key: str) -> dict[str, object]:
6363
value = values.get(key)
6464
if not isinstance(value, dict):
6565
raise RuntimeError(f"Distribution configuration requires an object '{key}'")
66-
return value
66+
return cast("dict[str, object]", value)
6767

6868

69-
def require_string(values: dict[str, Any], key: str) -> str:
69+
def require_string(values: dict[str, object], key: str) -> str:
7070
value = values.get(key)
7171
if not isinstance(value, str) or not value:
7272
raise RuntimeError(f"Launch configuration requires a non-empty '{key}' value")
@@ -86,12 +86,12 @@ def render_options_block(name: str, value: object) -> str:
8686
return f"\t[{name}]\n\t{{\n{entries}\n\t}}"
8787

8888

89-
def require_options(value: object, name: str) -> dict[str, Any]:
89+
def require_options(value: object, name: str) -> dict[str, object]:
9090
if value is None:
9191
return {}
9292
if not isinstance(value, dict):
9393
raise RuntimeError(f"Launch configuration '{name}' must be an object")
94-
return value
94+
return cast("dict[str, object]", value)
9595

9696

9797
def render_value(value: object) -> str:

‎build/sbc_packager/editor/archive.py‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
import shutil
44
import tempfile
55
from pathlib import Path
6-
from typing import Literal
6+
from typing import Literal, cast
77

8-
from ..shared.archive import write_game_archive
9-
from ..shared.files import copy_repo_filtered
8+
from sbc_packager.shared.archive import write_game_archive
9+
from sbc_packager.shared.files import copy_repo_filtered
1010

1111
Platform = Literal["linux", "win32"]
1212
SUPPORTED_IMAGE_SUFFIXES = {".jpg", ".jpeg", ".png"}
@@ -102,9 +102,10 @@ def install_port_flags(run_config: Path, game_directory: Path) -> None:
102102
config = json.load(source)
103103
if not isinstance(config, dict):
104104
raise RuntimeError(f"Run configuration must be an object: {run_config}")
105+
raw_config = cast("dict[str, object]", config)
105106
flags: dict[str, str] = {}
106107
for key in PORT_FLAG_KEYS:
107-
value = config.get(key)
108+
value = raw_config.get(key)
108109
valid_values = VALID_PORT_FLAGS[key]
109110
if not isinstance(value, str) or value not in valid_values:
110111
choices = ", ".join(sorted(valid_values))

‎build/sbc_packager/editor/cli.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55

66
import typer
77

8-
from ..shared.archive import write_sha256
9-
from ..shared.files import collect_generated_top_level_excludes
8+
from sbc_packager.shared.archive import write_sha256
9+
from sbc_packager.shared.files import collect_generated_top_level_excludes
10+
1011
from .archive import normalize_platform, prepare_game_archive, resolve_game_name
1112

1213
app = typer.Typer(add_completion=False, no_args_is_help=True)

‎justfile‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ tool_pythonpath := "build:tools"
1111
default:
1212
just --list
1313

14-
# Check Rust formatting for the native crate.
14+
# Apply mechanical formatting before the checks below.
1515
[group('lint')]
1616
fmt:
17-
cd native && cargo fmt --check
17+
cd native && cargo fmt
18+
PYTHONPATH="{{tool_pythonpath}}" uv run --locked ruff check --fix build tools
19+
PYTHONPATH="{{tool_pythonpath}}" uv run --locked ruff format build tools
1820

1921
# Run clippy on all native targets with warnings denied.
2022
[group('lint')]

‎native/src/sbc/heightmap/brushes.rs‎

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,6 @@ pub(crate) struct ShapeModify;
2323
pub(crate) struct Level;
2424
pub(crate) struct Smooth;
2525

26-
fn centre(stamp: BrushStamp) -> (f32, f32) {
27-
(stamp.x + stamp.size / 2.0, stamp.z + stamp.size / 2.0)
28-
}
29-
30-
fn signed_strength(brush: &BrushSettings, button: BrushButton) -> f32 {
31-
if button.is_secondary() {
32-
-brush.strength
33-
} else {
34-
brush.strength
35-
}
36-
}
37-
3826
pub(crate) fn prepare_pattern(
3927
brush: &BrushSettings,
4028
uploaded: &mut HashSet<String>,
@@ -57,6 +45,18 @@ pub(crate) fn prepare_pattern(
5745
true
5846
}
5947

48+
fn centre(stamp: BrushStamp) -> (f32, f32) {
49+
(stamp.x + stamp.size / 2.0, stamp.z + stamp.size / 2.0)
50+
}
51+
52+
fn signed_strength(brush: &BrushSettings, button: BrushButton) -> f32 {
53+
if button.is_secondary() {
54+
-brush.strength
55+
} else {
56+
brush.strength
57+
}
58+
}
59+
6060
impl MapBrush for ShapeModify {
6161
fn name(&self) -> &'static str {
6262
"terrain-shape-modify"

‎native/src/sbc/states/map_editing.rs‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,17 @@ pub(crate) enum BrushButton {
3434
}
3535

3636
impl BrushButton {
37+
pub(crate) fn is_secondary(self) -> bool {
38+
self == Self::Secondary
39+
}
40+
3741
fn from_mouse(button: i32) -> Self {
3842
if button == RIGHT {
3943
Self::Secondary
4044
} else {
4145
Self::Primary
4246
}
4347
}
44-
45-
pub(crate) fn is_secondary(self) -> bool {
46-
self == Self::Secondary
47-
}
4848
}
4949

5050
/// Domain-owned behaviour for a map brush. Adding a tool means implementing

‎pyproject.toml‎

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,66 @@ line-length = 120
3939
target-version = "py313"
4040

4141
[tool.ruff.lint]
42-
select = ["E", "F", "I", "UP", "B", "SIM"]
42+
select = [
43+
"A",
44+
"ANN",
45+
"ARG",
46+
"ASYNC",
47+
"B",
48+
"C4",
49+
"C90",
50+
"DTZ",
51+
"E",
52+
"ERA",
53+
"EXE",
54+
"F",
55+
"FA",
56+
"FLY",
57+
"FURB",
58+
"G",
59+
"I",
60+
"ICN",
61+
"INP",
62+
"ISC",
63+
"LOG",
64+
"N",
65+
"PERF",
66+
"PIE",
67+
"PT",
68+
"PTH",
69+
"RET",
70+
"RSE",
71+
"RUF",
72+
"SIM",
73+
"SLOT",
74+
"T10",
75+
"TC",
76+
"TID",
77+
"UP",
78+
"YTT",
79+
]
80+
81+
[tool.ruff.lint.per-file-ignores]
82+
"tools/smoke/test_*.py" = ["S101"]
4383

4484
[tool.pyright]
4585
pythonVersion = "3.13"
46-
typeCheckingMode = "standard"
86+
typeCheckingMode = "strict"
4787
reportMissingTypeStubs = false
88+
reportImplicitOverride = "error"
89+
reportMissingTypeArgument = "error"
90+
reportPrivateImportUsage = "error"
91+
reportUnknownArgumentType = "error"
92+
reportUnknownLambdaType = "error"
93+
reportUnknownMemberType = "error"
94+
reportUnknownParameterType = "error"
95+
reportUnknownVariableType = "error"
96+
reportUnnecessaryCast = "error"
97+
reportUnnecessaryComparison = "error"
98+
reportUnnecessaryContains = "error"
99+
reportUnnecessaryIsInstance = "error"
100+
reportUnnecessaryTypeIgnoreComment = "error"
101+
reportUntypedBaseClass = "error"
102+
reportUntypedClassDecorator = "error"
103+
reportUntypedFunctionDecorator = "error"
104+
reportUntypedNamedTuple = "error"

‎tools/e2e/cli_e2e.py‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
import typer
44

5-
from .cases import TARGETS, select_cases, target_cases
6-
from .golden import STATUS_APPROVED, approve_review, load_review
5+
from .driver.cases import TARGETS, select_cases, target_cases
6+
from .driver.utils.golden import GOLDEN_ROOT, STATUS_APPROVED, approve_review, load_review
77
from .runner import E2ERun
88

99
app = typer.Typer(no_args_is_help=True)
@@ -53,19 +53,17 @@ def run(
5353

5454
@app.command("goldens-status")
5555
def goldens_status() -> None:
56-
from .golden import GOLDEN_ROOT
57-
5856
pending = 0
5957
for case_dir in sorted(GOLDEN_ROOT.iterdir()):
6058
if not case_dir.is_dir():
6159
continue
6260
review = load_review(case_dir.name)
6361
shots = sorted(path.stem for path in case_dir.glob("*.png"))
64-
approved = sum(review.get(shot, {}).get("status") == STATUS_APPROVED for shot in shots)
62+
approved = sum(shot in review and review[shot]["status"] == STATUS_APPROVED for shot in shots)
6563
pending += len(shots) - approved
6664
typer.echo(f"{case_dir.name:26} {approved}/{len(shots)} approved")
6765
for shot in shots:
68-
if review.get(shot, {}).get("status") != STATUS_APPROVED:
66+
if shot not in review or review[shot]["status"] != STATUS_APPROVED:
6967
typer.echo(f" ai-reviewed {shot}")
7068
if pending:
7169
typer.echo(f"\n{pending} image(s) awaiting approval.")

0 commit comments

Comments
 (0)