fix(types): mypy clean again over src; stub-less libraries and the benchmark imports handled in pyproject

66 errors had accumulated in src since the last green run (the hunt modules of groups 4-6, stats, candidates,
runner). Codex typed them without behaviour change; the 17 inline import-untyped ignores it added are replaced by
one override for dspy, pandas, statsmodels and scikit-learn, and the hunt package's lazy imports of benchmarks/
(never type-checked, not in files) are followed silently so src stays strict against their signatures.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
Marius Ciepluch
2026-10-14 03:03:12 +02:00
co-authored by Claude Fable 5.1
parent 0d7d3f0dc9
commit c8f7610313
17 changed files with 78 additions and 36 deletions
+11
View File
@@ -90,6 +90,17 @@ ignore_missing_imports = true
module = "yaml"
ignore_missing_imports = true
# dspy, pandas, statsmodels and scikit-learn ship no py.typed marker and no stub packages are dev dependencies.
[[tool.mypy.overrides]]
module = ["dspy", "dspy.*", "pandas", "pandas.*", "statsmodels.*", "sklearn.*"]
ignore_missing_imports = true
# The hunt package reaches the benchmark scorers and manifests lazily. benchmarks/ is not in `files` and has never
# been type-checked; src stays strict against its signatures while errors inside benchmarks/ are not reported.
[[tool.mypy.overrides]]
module = "benchmarks.*"
follow_imports = "silent"
[dependency-groups]
dev = [
"dagger-io>=0.21.7",
+1 -1
View File
@@ -190,7 +190,7 @@ def run(
if table.digest() != manifest.table_digest:
raise ValueError("table digest does not match registration")
runner.refuse_pilot(Path(pool_manifest_path), pilot_report=False)
folds: dict[str, tuple[str, int]] = {}
folds: dict[str, tuple[str, int | None]] = {}
for row in table.rows:
key, value = row["change_id"], (row["repo"], row.get("fold"))
if key in folds and folds[key] != value:
+5 -4
View File
@@ -77,7 +77,7 @@ class Decision:
artifact.configurations.append(record)
def _record(value: Any) -> dict[str, Any]:
def _record(value: Estimate | FoldResult | Mapping[str, Any]) -> dict[str, Any]:
"""Convert dataclass estimates and ordinary mappings to JSON records."""
return asdict(value) if is_dataclass(value) else dict(value)
@@ -124,7 +124,8 @@ def decide(
instructions = [state.get("signature", {}).get("instructions", "") for state in candidate.state.values()]
if any(not isinstance(text, str) or len(text) > rule.instruction_cap for text in instructions):
reasons.append("instruction_cap")
compiled, heldout = {}, {}
compiled: dict[str, dict[str, Any]] = {}
heldout: dict[str, dict[str, Any]] = {}
for scope, source, target in (("compile", compile_results, compiled), ("holdout", holdout_results, heldout)):
for family, result in source.items():
record = _record(result)
@@ -150,10 +151,10 @@ def decide(
reasons.append("no_positive_recall_difference")
if _valid_number(holdout_diff) and abs(holdout_diff) <= rule.tolerance:
reasons.append("holdout_tie")
for family, result in heldout.items():
for family, heldout_record in heldout.items():
if family == "pooled":
continue
diff = result["recall"].get("diff")
diff = heldout_record["recall"].get("diff")
floor = rule.regression_budget.get(family, rule.regression_budget.get("default", 0))
if _valid_number(diff) and diff < floor:
reasons.append(f"family_regression:{family}")
+1 -1
View File
@@ -28,7 +28,7 @@ def _exhausted(usage: Mapping[str, float], budget: Mapping[str, float]) -> bool:
class RunArtifact:
program_id: str
schema_version: int
mask: tuple[str, ...]
mask: tuple[str, ...] | list[str]
data_digests: dict[str, str]
configurations: list[dict[str, Any]]
usage: dict[str, float]
+3 -2
View File
@@ -4,13 +4,14 @@ from __future__ import annotations
import hashlib
import json
import re
import tomllib
from collections.abc import Iterable
from collections.abc import Callable, Iterable
from datetime import UTC, datetime
from pathlib import Path
def _eligibility():
def _eligibility() -> tuple[re.Pattern[str], Callable[[str], str]]:
"""The benchmark's name normaliser and GitHub URL pattern, imported late so the package imports without the benchmarks tree."""
from benchmarks.unseen.eligibility import URL, normalize
+1 -1
View File
@@ -234,7 +234,7 @@ def _roles(function: Any, facts: Any) -> dict[str, str]:
for call in function.calls:
for arguments in call.arg_names:
names.update(arguments)
roles = {}
roles: dict[str, str] = {}
for role, entries, attribute in (
("source", facts.sources, "patterns"),
("sink", facts.sinks, "calls"),
+5 -2
View File
@@ -67,7 +67,8 @@ def signature_for(mask: Iterable[str] = ()) -> type[dspy.Signature]:
continue
fields[name] = (str, dspy.InputField(desc=desc))
fields["findings"] = (list[Finding], dspy.OutputField(desc="JSON list; empty when the change is ordinary"))
return dspy.Signature(fields, BASELINE_INSTRUCTION)
signature: type[dspy.Signature] = dspy.Signature(fields, BASELINE_INSTRUCTION)
return signature
def numbered(lines: Sequence[str], start: int, end: int) -> str:
@@ -194,7 +195,9 @@ class _Loop(dspy.ReAct):
loop_parse_failed = False
def _call_with_potential_trajectory_truncation(self, module, trajectory, **input_args):
def _call_with_potential_trajectory_truncation(
self, module: dspy.Module, trajectory: dict[str, Any], **input_args: str
) -> dspy.Prediction:
try:
return super()._call_with_potential_trajectory_truncation(module, trajectory, **input_args)
except dspy.utils.exceptions.AdapterParseError as error:
+12 -4
View File
@@ -20,7 +20,7 @@ from .metric import search_metric, search_outcome
from .table import FEATURE_COLUMNS, canonical_json
class _Optimisers(Mapping):
class _Optimisers(Mapping[str, type]):
"""Load a DSPy optimiser class only when its registry entry is requested."""
names = {"gepa": "GEPA", "miprov2": "MIPROv2", "bootstrap": "BootstrapFewShot"}
@@ -28,7 +28,8 @@ class _Optimisers(Mapping):
def __getitem__(self, name: str) -> type:
import dspy
return getattr(dspy, self.names[name])
optimiser: type = getattr(dspy, self.names[name])
return optimiser
def __iter__(self) -> Iterator[str]:
return iter(self.names)
@@ -93,7 +94,8 @@ def compile_examples(
from .program import INPUTS
repo_folds, ids = {}, set()
repo_folds: dict[str, int] = {}
ids: set[str] = set()
for change in changes:
if change["id"] in ids:
raise ValueError("change IDs must be unique")
@@ -230,7 +232,13 @@ def _compile(round: Round, program: Any, examples: list, optimiser: Any, metric:
if dspy.settings.lm is None:
raise ValueError("GEPA needs a configured reflection model in dspy.context")
def feedback(example, prediction, trace=None, pred_name=None, pred_trace=None):
def feedback(
example: dspy.Example,
prediction: dspy.Prediction,
trace: Any = None,
pred_name: str | None = None,
pred_trace: Any = None,
) -> dspy.Prediction:
"""Give GEPA the same score and the metric's compile-only feedback."""
import dspy
+6 -6
View File
@@ -90,7 +90,7 @@ def _instrument(records: Sequence[Mapping]) -> dict:
)
def _clean(records: Sequence[Mapping]) -> list[dict]:
def _clean(records: Sequence[dict[str, Any]]) -> list[dict[str, Any]]:
"""Remove stale findings on failures while preserving the scorer's coverage checks."""
from benchmarks.unseen import score
@@ -109,7 +109,7 @@ def _clean(records: Sequence[Mapping]) -> list[dict]:
return result
def _rate(values: list[float], repos: list[str], *, binary: bool = False, scale: float = 1) -> dict:
def _rate(values: Sequence[float], repos: list[str], *, binary: bool = False, scale: float = 1) -> dict:
"""Report a mean with its count and a repository-clustered interval, plus Wilson for proportions."""
from benchmarks.unseen import score
@@ -127,7 +127,7 @@ def _rate(values: list[float], repos: list[str], *, binary: bool = False, scale:
return result
def _family_records(records: Sequence[Mapping], family: str | None) -> list[dict]:
def _family_records(records: Sequence[dict[str, Any]], family: str | None) -> list[dict[str, Any]]:
"""Keep a family's positives and count only its findings over all ordinary pushes."""
result = []
for original in records:
@@ -217,7 +217,7 @@ def _baseline_comparison(baselines: list[list[dict]], records: list[dict], famil
from benchmarks.unseen import score
from benchmarks.valid import metrics
units = [
units: list[dict[str, Any]] = [
dict(change=after, repeat=repeat, a=_outcome(before, before, 10), b=_outcome(after, after, 10))
for repeat, baseline in enumerate(baselines)
for before, after in zip(baseline, records, strict=True)
@@ -232,7 +232,7 @@ def _baseline_comparison(baselines: list[list[dict]], records: list[dict], famil
suspicions_per_push=asdict(suspicions),
mcnemar=asdict(stats.mcnemar_exact([u["a"]["hit"] for u in positives], [u["b"]["hit"] for u in positives])),
)
rows = {"recall": [], "false_alarms_per_100_pushes": [], "precision": []}
rows: dict[str, list[dict[str, Any]]] = {"recall": [], "false_alarms_per_100_pushes": [], "precision": []}
for baseline in baselines:
before = metrics.scoring_records(_family_records(baseline, family))
after = metrics.scoring_records(_family_records(records, family))
@@ -426,7 +426,7 @@ def pilot_gain(
records = _records(directory / "records", changes)
proof = _instrument(records)
_write(directory / "instrument.json", proof)
analysis = dict(
analysis: dict[str, Any] = dict(
program_id=candidate.program_id,
comparison=decision.comparison,
measurement_directory=directory.name,
+5 -3
View File
@@ -21,7 +21,8 @@ def _fold_changes(pool_manifest: Path, table: FeatureTable, load_changes: Any) -
from benchmarks.unseen.replay import load_slice
runner.refuse_pilot(pool_manifest, pilot_report=False)
folds, repo_folds = {}, {}
folds: dict[str, tuple[str, int]] = {}
repo_folds: dict[str, int] = {}
for row in table.rows:
key, repo, fold = row["change_id"], row["repo"], row.get("fold")
if type(fold) is not int or fold < 0:
@@ -183,7 +184,8 @@ def fold_results(
if len({c["id"] for c in changes}) != len(changes):
raise ValueError("change IDs must be unique")
directories = {"a": Path(current_dir), "b": Path(candidate_dir)}
studies, repeats = {}, set()
studies: dict[str, Any] = {}
repeats: set[int] = set()
for arm, directory in directories.items():
path = directory / "study.json"
if path.exists():
@@ -291,7 +293,7 @@ def round_trip(
)
)
results[scope] = fold_results(
*paths, [c for c in changes if c["fold"] in folds], scope=scope, tolerance=tolerance, seed=seed, resamples=resamples
paths[0], paths[1], [c for c in changes if c["fold"] in folds], scope=scope, tolerance=tolerance, seed=seed, resamples=resamples
)
decision = accept.decide(current_id, candidate, results["compile"], results["holdout"], rule)
if not decision.accepted:
+12 -3
View File
@@ -4,6 +4,7 @@ import os
import re
from collections.abc import Iterator
from pathlib import Path
from typing import NotRequired, TypedDict
from .preprocess import enumerate_source_files
from .source import callers_of, product_files
@@ -67,11 +68,19 @@ def _mtime(path: Path) -> int:
return -1
def grep_repo(root: Path, pattern: str, *, max_matches: int) -> list[dict[str, object]]:
class ToolMatch(TypedDict):
path: str
line: int
text: str
name: NotRequired[str]
source: NotRequired[str]
def grep_repo(root: Path, pattern: str, *, max_matches: int) -> list[ToolMatch]:
if max_matches <= 0:
return []
compiled = re.compile(pattern)
matches: list[dict[str, object]] = []
matches: list[ToolMatch] = []
for relative, lines in _source_lines(root):
for line_no, line in enumerate(lines, start=1):
if compiled.search(line):
@@ -81,7 +90,7 @@ def grep_repo(root: Path, pattern: str, *, max_matches: int) -> list[dict[str, o
return matches
def find_refs(root: Path, symbol: str) -> list[dict[str, object]]:
def find_refs(root: Path, symbol: str) -> list[ToolMatch]:
if not symbol:
return []
lines = dict(_source_lines(root))
+5 -2
View File
@@ -752,8 +752,11 @@ class LabelBuilder:
if not function:
lines = self._clone(population, str(case["id"])).lines(str(case["vulnerable"]), site_path)
language = detect_language(Path(site_path))
covered = lines and language in DECLARATION and line <= len(lines)
function = enclosing(lines, line - 1, language) if covered else MODULE_FUNCTION
function = (
enclosing(lines, line - 1, language)
if lines and language in DECLARATION and line <= len(lines)
else MODULE_FUNCTION
)
self._row(
candidate=f"{site_path}::{function}", family=str(row.get("family") or case["family"]), label=label, source=source.id,
source_ref=f"{Path(path).name}#{key}[{index}]", unit="pin", weight=1.0, split=population.id,
+3 -2
View File
@@ -20,7 +20,7 @@ from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING, Literal
from ..source import canonical_language
from ..source import FunctionRange, canonical_language
if TYPE_CHECKING:
from .taxonomy import FamilyTaxonomy
@@ -368,7 +368,7 @@ def labeled_family(case: object, taxonomy: FamilyTaxonomy | None = None) -> str:
def labeled_spans(
case: object, ranges: Mapping[str, Sequence[tuple[str, int, int]]], *, function: str = ""
case: object, ranges: Mapping[str, Sequence[tuple[str, int, int] | FunctionRange]], *, function: str = ""
) -> tuple[tuple[str, int, int], ...]:
"""The (path, start, end) spans of the labeled functions; ``function`` overrides the label for the fixed side.
@@ -378,6 +378,7 @@ def labeled_spans(
wanted = {function} if function else {str(getattr(row, "function", "") or "") for row in getattr(case, "expected", ()) or ()}
wanted.discard("")
found: list[tuple[str, int, int]] = []
_kind: list[str]
for path, entries in ranges.items():
for name, start, end, *_kind in entries:
if name in wanted:
+2 -2
View File
@@ -704,7 +704,7 @@ def safe_url(url: str) -> str:
def deliver(
url: str,
payload: bytes,
headers: Mapping[str, str],
headers: MutableMapping[str, str],
*,
attempts: int = DELIVERY_ATTEMPTS,
backoff: float = 1.0,
@@ -732,7 +732,7 @@ def deliver(
raise DeliveryError(f"{last} after {attempts} attempts to {safe_url(url)}")
def fetch(url: str, headers: Mapping[str, str], dest: Path, *, attempts: int = DELIVERY_ATTEMPTS, backoff: float = 1.0) -> int:
def fetch(url: str, headers: MutableMapping[str, str], dest: Path, *, attempts: int = DELIVERY_ATTEMPTS, backoff: float = 1.0) -> int:
"""GET ``url`` into ``dest`` (streamed to a sibling temp file, then renamed); retried like :func:`deliver`.
Returns the byte count; :class:`DeliveryError` names the last cause when every attempt failed."""
+4 -2
View File
@@ -409,9 +409,11 @@ def _side(side: Side, demo: Path, family: str, timeout: int, browser: BrowserExe
if task_boundary:
# Files are app data, so resolve their paths before writing;
# the task runner only translates argv and environment.
def fixture_path(_: re.Match[str], path: str = str(canary.root)) -> str:
return path
file_bindings = {
key: re.sub(r"/(?:fixture|scratch)(?=/|$)", lambda _, path=str(canary.root): path, value)
for key, value in bindings.items()
key: re.sub(r"/(?:fixture|scratch)(?=/|$)", fixture_path, value) for key, value in bindings.items()
}
private_values.extend((*bindings.values(), *file_bindings.values()))
write_start_files(checkout, start_spec, file_bindings)
+1
View File
@@ -307,6 +307,7 @@ _HEAD = re.compile(
def _regex_ranges(text: str, language: str) -> tuple[FunctionRange, ...]:
lines, code = text.splitlines(), _code_lines(text)
result = []
end: int | None
for index, line in enumerate(code):
name = declarator_name(line, language)
if language == "python":
+1 -1
View File
@@ -167,7 +167,7 @@ def separation(frame: Any, label: str, columns: Iterable[str]) -> Any:
import pandas as pd
from sklearn.metrics import roc_auc_score
rows = []
rows: list[dict[str, str | float | None]] = []
labelled = frame[frame[label].isin([0, 1])]
for column in columns:
series = pd.to_numeric(labelled[column], errors="coerce") if column in labelled else None