mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
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:
co-authored by
Claude Fable 5.1
parent
0d7d3f0dc9
commit
c8f7610313
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user