From e066e84d79e3f8ae54efdb6f94fa2806900b0202 Mon Sep 17 00:00:00 2001 From: Stefan Jansen Date: Mon, 7 Sep 2026 14:43:29 -0400 Subject: [PATCH] One ruff rule set, defined in pyproject and used by CI CI ran `--isolated --select E4,E7,E9,F`, so the config file could never be the definition: a bare `ruff check` reported 41 findings under ruff 0.16.6's defaults while CI passed clean. The config now selects E4/E7/E9/F plus I, UP and B, and CI runs the plain command. The wider default set is not adopted. S102 and BLE001 fire on deliberate choices here - `exec` rebuilds a student's saved component, and a broad `except` is what keeps one wrong component from ending their session. Twenty findings were mechanical: `typing.Callable` to `collections.abc`, unquoted self-references, import order. Four needed a decision. Three zips get `strict=True`; the one over `names` and `stages` in `checks.py` is the reason the rule earns its place, since a stage added without a name would otherwise have been skipped in silence. --- .github/workflows/ci.yml | 2 +- pyproject.toml | 10 ++++++++++ src/ml4t_coursework/checks.py | 8 +++++--- src/ml4t_coursework/components.py | 5 +++-- src/ml4t_coursework/contracts.py | 5 +++-- src/ml4t_coursework/course.py | 2 +- src/ml4t_coursework/markets.py | 4 ++-- src/ml4t_coursework/reference/_config.py | 3 ++- src/ml4t_coursework/reference/fold_splitter.py | 2 +- src/ml4t_coursework/reference/model_gbm.py | 2 +- src/ml4t_coursework/reference/model_linear.py | 2 +- src/ml4t_coursework/reference/preprocessor.py | 2 +- src/ml4t_coursework/runner.py | 3 ++- tests/test_helper.py | 13 +++++++++++-- tests/test_runner.py | 4 +++- 15 files changed, 47 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b0b9f2..e6bf317 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: version: "0.10.9" - run: uv python install 3.14 - run: uv sync --python 3.14 --all-extras --locked - - run: uv run ruff check --isolated --select E4,E7,E9,F src tests + - run: uv run ruff check src tests qualify: name: Qualify (${{ matrix.os }}, Python ${{ matrix.python-version }}) diff --git a/pyproject.toml b/pyproject.toml index e09fee5..319ebec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,5 +48,15 @@ exclude = ["/.workspace", "/publish.sh"] [tool.ruff] line-length = 100 +[tool.ruff.lint] +# One definition, used by CI and by a bare `ruff check` alike. E4/E7/E9/F is what +# ruff enforces with no configuration at all; I, UP and B are the three that pay +# for themselves here - import order, syntax this package's floor (3.10) already +# supports, and the bugbear checks that catch a real defect rather than a style +# preference. The wider default set is not adopted: S102 and BLE001 both fire on +# deliberate choices - `exec` rebuilds a student's saved component, and a broad +# `except` is what stops one wrong component from ending their session. +select = ["E4", "E7", "E9", "F", "I", "UP", "B"] + [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/src/ml4t_coursework/checks.py b/src/ml4t_coursework/checks.py index 881c86e..5dd17cb 100644 --- a/src/ml4t_coursework/checks.py +++ b/src/ml4t_coursework/checks.py @@ -8,8 +8,9 @@ from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass, field -from typing import Any, Callable +from typing import Any import numpy as np import pandas as pd @@ -93,7 +94,8 @@ def same(left: Any, right: Any, tol: float = 1e-12) -> bool: return bool(np.allclose(left.to_numpy(dtype=float), right.to_numpy(dtype=float), atol=tol, rtol=0, equal_nan=True)) if isinstance(left, (list, tuple)) and isinstance(right, (list, tuple)): - return len(left) == len(right) and all(same(a, b, tol) for a, b in zip(left, right)) + return len(left) == len(right) and all( + same(a, b, tol) for a, b in zip(left, right, strict=True)) if isinstance(left, pd.Index) and isinstance(right, pd.Index): return left.equals(right) if isinstance(left, np.ndarray) and isinstance(right, np.ndarray): @@ -187,7 +189,7 @@ def evaluate(contract, obj) -> Conformance: lambda: run_invariants(contract, obj), ] names = ["interface", "leakage probe", "determinism", "domain invariants"] - for name, stage in zip(names, stages): + for name, stage in zip(names, stages, strict=True): try: result.checks.extend(stage()) except Failure as failure: diff --git a/src/ml4t_coursework/components.py b/src/ml4t_coursework/components.py index e7b00c4..703c78a 100644 --- a/src/ml4t_coursework/components.py +++ b/src/ml4t_coursework/components.py @@ -12,7 +12,8 @@ import inspect import json import textwrap -from typing import Any, Callable, Sequence +from collections.abc import Callable, Sequence +from typing import Any from . import contracts, project from .checks import Conformance, evaluate @@ -187,7 +188,7 @@ def load_component(name: str, quiet: bool = False) -> Any: def status() -> str: """Every component the course asks for, and where each one stands.""" lines = [] - for name, contract in sorted(contracts.load_all().items()): + for name, _contract in sorted(contracts.load_all().items()): meta = _meta(name) if meta is None: state = "not written yet" diff --git a/src/ml4t_coursework/contracts.py b/src/ml4t_coursework/contracts.py index 74f49a2..01b5014 100644 --- a/src/ml4t_coursework/contracts.py +++ b/src/ml4t_coursework/contracts.py @@ -16,11 +16,12 @@ import importlib import pkgutil +from collections.abc import Callable from dataclasses import dataclass from types import ModuleType -from typing import Any, Callable +from typing import Any -CONTRACTS: dict[str, "Contract"] = {} +CONTRACTS: dict[str, Contract] = {} _SOURCES: list[str] = [f"{__package__}.reference"] _LOADED: set[str] = set() diff --git a/src/ml4t_coursework/course.py b/src/ml4t_coursework/course.py index 5fb773c..bdc457f 100644 --- a/src/ml4t_coursework/course.py +++ b/src/ml4t_coursework/course.py @@ -15,7 +15,7 @@ from dataclasses import dataclass -COURSES: dict[str, "Course"] = {} +COURSES: dict[str, Course] = {} _active: str | None = None diff --git a/src/ml4t_coursework/markets.py b/src/ml4t_coursework/markets.py index 4aac8c0..a5851db 100644 --- a/src/ml4t_coursework/markets.py +++ b/src/ml4t_coursework/markets.py @@ -16,11 +16,11 @@ from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass, field from pathlib import Path -from typing import Callable -MARKETS: dict[str, "MarketSpec"] = {} +MARKETS: dict[str, MarketSpec] = {} BARS_PER_YEAR = {"daily": 252, "8h": 3 * 365, "hourly": 24 * 365, "weekly": 52} diff --git a/src/ml4t_coursework/reference/_config.py b/src/ml4t_coursework/reference/_config.py index 3b29bdd..8b8ba94 100644 --- a/src/ml4t_coursework/reference/_config.py +++ b/src/ml4t_coursework/reference/_config.py @@ -7,7 +7,8 @@ from __future__ import annotations -from typing import Any, Callable +from collections.abc import Callable +from typing import Any from ..checks import require diff --git a/src/ml4t_coursework/reference/fold_splitter.py b/src/ml4t_coursework/reference/fold_splitter.py index 2a6c918..d313904 100644 --- a/src/ml4t_coursework/reference/fold_splitter.py +++ b/src/ml4t_coursework/reference/fold_splitter.py @@ -43,7 +43,7 @@ def _interface(obj) -> None: for k, fold in enumerate(folds): require(isinstance(fold, tuple) and len(fold) == 2, "interface", "each fold to be a (train, validation) pair", f"fold {k} is {type(fold).__name__}") - for part, label in zip(fold, ("train", "validation")): + for part, label in zip(fold, ("train", "validation"), strict=True): require(len(part) > 0, "interface", f"a non-empty {label} block", f"fold {k}'s {label} block is empty") diff --git a/src/ml4t_coursework/reference/model_gbm.py b/src/ml4t_coursework/reference/model_gbm.py index 8a870d5..de4337b 100644 --- a/src/ml4t_coursework/reference/model_gbm.py +++ b/src/ml4t_coursework/reference/model_gbm.py @@ -27,7 +27,7 @@ def __init__(self, seed: int = seed, max_depth: int = max_depth, self.model_ = None self.columns_ = None - def fit(self, X: pd.DataFrame, y: pd.Series) -> "GbmModel": + def fit(self, X: pd.DataFrame, y: pd.Series) -> GbmModel: from sklearn.ensemble import HistGradientBoostingRegressor self.columns_ = list(X.columns) diff --git a/src/ml4t_coursework/reference/model_linear.py b/src/ml4t_coursework/reference/model_linear.py index d1dcf13..2930241 100644 --- a/src/ml4t_coursework/reference/model_linear.py +++ b/src/ml4t_coursework/reference/model_linear.py @@ -23,7 +23,7 @@ def __init__(self, alpha: float = alpha): self.intercept_ = None self.columns_ = None - def fit(self, X: pd.DataFrame, y: pd.Series) -> "LinearModel": + def fit(self, X: pd.DataFrame, y: pd.Series) -> LinearModel: self.columns_ = list(X.columns) design = X.to_numpy(dtype=float) centre = design.mean(axis=0) diff --git a/src/ml4t_coursework/reference/preprocessor.py b/src/ml4t_coursework/reference/preprocessor.py index 9954907..b55c472 100644 --- a/src/ml4t_coursework/reference/preprocessor.py +++ b/src/ml4t_coursework/reference/preprocessor.py @@ -26,7 +26,7 @@ def __init__(self, clip: float = clip): self.spread_ = None self.fill_ = None - def fit(self, X: pd.DataFrame) -> "Preprocessor": + def fit(self, X: pd.DataFrame) -> Preprocessor: self.lower_ = X.quantile(self.clip) self.upper_ = X.quantile(1 - self.clip) trimmed = X.clip(self.lower_, self.upper_, axis=1) diff --git a/src/ml4t_coursework/runner.py b/src/ml4t_coursework/runner.py index 3c0e6af..26af4ae 100644 --- a/src/ml4t_coursework/runner.py +++ b/src/ml4t_coursework/runner.py @@ -19,8 +19,9 @@ from __future__ import annotations import datetime as dt +from collections.abc import Callable from dataclasses import dataclass -from typing import Any, Callable +from typing import Any import numpy as np import pandas as pd diff --git a/tests/test_helper.py b/tests/test_helper.py index 9d9419f..3752d28 100644 --- a/tests/test_helper.py +++ b/tests/test_helper.py @@ -7,8 +7,17 @@ import pandas as pd import pytest -from ml4t_coursework import (append_result, contract, load_component, project, report, results, - save_component, source_of, status) +from ml4t_coursework import ( + append_result, + contract, + load_component, + project, + report, + results, + save_component, + source_of, + status, +) from ml4t_coursework.components import _meta diff --git a/tests/test_runner.py b/tests/test_runner.py index 9e69bbe..c29fa40 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -13,9 +13,11 @@ import pytest from ml4t_coursework import fixtures, markets, project, runner + # `ml4t_coursework.results` is shadowed by the function of the same name that the # package re-exports, so the module's own names are imported directly. -from ml4t_coursework.results import REQUIRED, results as results_log +from ml4t_coursework.results import REQUIRED +from ml4t_coursework.results import results as results_log @pytest.fixture