From 08805e94faf64954aed225a5ddcbf6a1eec3fc62 Mon Sep 17 00:00:00 2001 From: "Petrov, Nikolay A" Date: Thu, 12 Nov 2026 23:18:41 -0800 Subject: [PATCH 01/13] Enable ruff BLE001/S110/S112: no silent except Narrow the JSON-parse handlers to the errors json.loads raises, log the fallbacks in arms_run, mcp_protocol and the persona analyzer, and give every remaining catch-all a noqa reason; tests/test_noqa_reasons.py fails a bare one. Signed-off-by: Petrov, Nikolay A --- AGENTS.md | 4 + ...61112_230000_napetrov_blind-except-gate.md | 6 ++ dokimi/dokimi/commands/benchmark.py | 8 +- dokimi/dokimi/commands/evaluate.py | 2 +- dokimi/dokimi/commands/orchestrate.py | 2 +- dokimi/dokimi/commands/subjects.py | 2 +- dokimi/dokimi/dashboard/aggregator.py | 2 +- dokimi/dokimi/eval/answerer.py | 4 +- dokimi/dokimi/eval/arms_run.py | 5 ++ dokimi/dokimi/eval/cross_judge.py | 2 +- dokimi/dokimi/eval/pairwise.py | 2 +- dokimi/dokimi/eval/panel.py | 2 +- dokimi/dokimi/eval/ragas_eval.py | 2 +- dokimi/dokimi/eval/reranker.py | 2 +- dokimi/dokimi/eval/validity_scoring.py | 6 +- dokimi/dokimi/harnesses/egress_proxy.py | 2 +- dokimi/dokimi/judged.py | 2 +- dokimi/dokimi/llm.py | 8 +- dokimi/dokimi/llm_json.py | 8 +- dokimi/dokimi/llm_pricing.py | 2 +- dokimi/dokimi/mcp/context7.py | 2 +- dokimi/dokimi/mcp/local_markdown.py | 2 +- dokimi/dokimi/mcp/mcp_protocol.py | 3 +- dokimi/dokimi/mcp/okf_client.py | 2 +- dokimi/dokimi/mcp/url_client.py | 2 +- dokimi/dokimi/metrics/example_runner.py | 2 +- dokimi/dokimi/novelty/atoms.py | 6 +- dokimi/dokimi/novelty/batching.py | 2 +- dokimi/dokimi/novelty/client.py | 2 +- dokimi/dokimi/novelty/measure.py | 6 +- dokimi/dokimi/novelty/needs.py | 2 +- dokimi/dokimi/novelty/probes.py | 2 +- dokimi/dokimi/novelty/retention.py | 2 +- dokimi/dokimi/personas/analyzer.py | 9 ++- dokimi/dokimi/questions/chunk_gen.py | 4 +- dokimi/dokimi/questions/llm_gen.py | 2 +- dokimi/dokimi/questions/panel_reviewer.py | 2 +- dokimi/dokimi/questions/quality_analyzer.py | 2 +- dokimi/dokimi/questions/ragas_seed.py | 4 +- dokimi/dokimi/questions/refiner.py | 2 +- dokimi/dokimi/questions/validator.py | 2 +- dokimi/dokimi/repos/hits_judge.py | 2 +- dokimi/dokimi/repos/l0/branches.py | 2 +- dokimi/dokimi/repos/l0/versions.py | 2 +- dokimi/dokimi/repos/l0/workflows.py | 2 +- dokimi/dokimi/tool_preference.py | 2 +- dokimi/dokimi/treatments/arms.py | 2 +- dokimi/pyproject.toml | 1 + dokimi/scripts/ci_local.py | 2 +- dokimi/tests/test_noqa_reasons.py | 77 +++++++++++++++++++ 50 files changed, 161 insertions(+), 66 deletions(-) create mode 100644 dokimi/changelog.d/20261112_230000_napetrov_blind-except-gate.md create mode 100644 dokimi/tests/test_noqa_reasons.py diff --git a/AGENTS.md b/AGENTS.md index b79d98a20..3760f01ea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -45,6 +45,10 @@ also holds a PR to: constraint or the reason, and end with an issue or PR URL when a reader needs the backstory. A block of three or more `#` lines fails once the PR adds or edits a line in it, so shorten an old block when you touch it. +- No silent `except` (ruff BLE001, S110, S112): catch the exception you expect, or + log it with its traceback, or re-raise. A site that must catch everything says + what happens to the failure in `# noqa: BLE001 - ` + (`tests/test_noqa_reasons.py` fails a bare code). - User outcomes (`tests/outcomes/*.json`, checked by `pytest --outcomes`): a test marked `@pytest.mark.outcome(...)` is evidence for a promise in the docs; weaken or drop it only together with the doc and the contract. The same diff --git a/dokimi/changelog.d/20261112_230000_napetrov_blind-except-gate.md b/dokimi/changelog.d/20261112_230000_napetrov_blind-except-gate.md new file mode 100644 index 000000000..0caeb5815 --- /dev/null +++ b/dokimi/changelog.d/20261112_230000_napetrov_blind-except-gate.md @@ -0,0 +1,6 @@ +### Changed + +- A library id that cannot be resolved for an arm now logs a warning before the arm + falls back to the product name, and a failed MCP tool listing or GitHub issue fetch + is logged instead of dropped silently. JSON extraction from LLM replies catches only + parse errors, so a bug in it is no longer read as "no JSON here". diff --git a/dokimi/dokimi/commands/benchmark.py b/dokimi/dokimi/commands/benchmark.py index 9e89bd157..915a10d57 100644 --- a/dokimi/dokimi/commands/benchmark.py +++ b/dokimi/dokimi/commands/benchmark.py @@ -203,7 +203,7 @@ def _check_questions_from(questions_from: str, key: str) -> tuple[Path, list, st return qf, [], str(exc) try: return qf, load_questions_payload(qf), None - except Exception as exc: + except Exception as exc: # noqa: BLE001 - any load failure is reported as an invalid source return qf, [], f"questions source invalid: {qf} ({exc})" @@ -228,7 +228,7 @@ def _check_golden_set(questions_root: str, key: str, refuse: Callable[[str], Non bad = next((i for i, q in enumerate(cast(List[Any], golden_questions)) if not isinstance(q, dict)), None) if bad is not None: refuse(f"golden set item {bad} is not a dict: {golden}") - except Exception as exc: + except Exception as exc: # noqa: BLE001 - any load failure refuses the run, with its message refuse(f"golden set invalid: {golden} ({exc})") @@ -252,7 +252,7 @@ def _check_doc_source(plan: dict, entry: Any, refuse: Callable[[str], None], err warnings.append("context7 id not pinned; client will resolve by target name") except ValueError as exc: refuse(f"doc source invalid: {plan['doc_source']} ({exc})") - except Exception as exc: # the source cannot be reached or built: a failed check + except Exception as exc: # noqa: BLE001 - the source cannot be reached or built: a failed check errors.append(f"doc source check failed: {plan['doc_source']} ({exc})") @@ -440,7 +440,7 @@ def cmd_benchmark_batch(args: argparse.Namespace) -> None: registry=registry, ) results.append(r) - except Exception as exc: + except Exception as exc: # noqa: BLE001 - one failed library is recorded; --fail-fast stops msg = f"FAILED: {entry.key} โ€” {exc}" print(f"\nโŒ {msg}", file=sys.stderr) failed.append({"library": entry.key, "name": entry.name, "status": "failed", "error": str(exc)}) diff --git a/dokimi/dokimi/commands/evaluate.py b/dokimi/dokimi/commands/evaluate.py index 9ff7a1c60..9bf3c330b 100644 --- a/dokimi/dokimi/commands/evaluate.py +++ b/dokimi/dokimi/commands/evaluate.py @@ -76,7 +76,7 @@ def _run_ragas_eval(answers, output_path, args) -> None: raise # a bad input record is a refusal, not a RAGAS failure: cli.main reports it (exit 2) except ImportError as exc: sys.exit(f"RAGAS meta-evaluation was requested but cannot run: {exc}") - except Exception as exc: + except Exception as exc: # noqa: BLE001 - RAGAS is an optional extra pass; its failure is printed print(f"\nโš ๏ธ RAGAS evaluation failed: {exc}") diff --git a/dokimi/dokimi/commands/orchestrate.py b/dokimi/dokimi/commands/orchestrate.py index 7a5ecadc7..a3ae425d9 100644 --- a/dokimi/dokimi/commands/orchestrate.py +++ b/dokimi/dokimi/commands/orchestrate.py @@ -106,7 +106,7 @@ def cmd_evaluate(args: argparse.Namespace) -> None: print(f"\n๐Ÿ“Š View full report: cat {results['steps']['report']['path']}") - except Exception as e: + except Exception as e: # noqa: BLE001 - top-level: prints the traceback and exits 1 print(f"\nโŒ Pipeline failed: {e}", file=sys.stderr) import traceback traceback.print_exc() diff --git a/dokimi/dokimi/commands/subjects.py b/dokimi/dokimi/commands/subjects.py index c7e130256..c364aeb22 100644 --- a/dokimi/dokimi/commands/subjects.py +++ b/dokimi/dokimi/commands/subjects.py @@ -41,7 +41,7 @@ def cmd_subjects_list(args: argparse.Namespace) -> None: descriptor = load_subject(path) print(f"{descriptor.id}\t{descriptor.subject.kind}\t{path}") - except Exception as exc: + except Exception as exc: # noqa: BLE001 - one bad descriptor is listed as ERROR, the rest still print print(f"{path}\tERROR\t{exc}") diff --git a/dokimi/dokimi/dashboard/aggregator.py b/dokimi/dokimi/dashboard/aggregator.py index 59850bf9a..c4c876d51 100644 --- a/dokimi/dokimi/dashboard/aggregator.py +++ b/dokimi/dokimi/dashboard/aggregator.py @@ -122,7 +122,7 @@ def aggregate(self) -> DashboardData: def _load_snapshot(self, path: Path) -> Optional[ProductSnapshot]: try: data = json.loads(path.read_text(encoding="utf-8")) - except Exception as e: + except Exception as e: # noqa: BLE001 - an unreadable snapshot is logged and skipped logger.warning(f"Cannot read {path}: {e}") return None diff --git a/dokimi/dokimi/eval/answerer.py b/dokimi/dokimi/eval/answerer.py index 5db85fad4..17c824d4c 100644 --- a/dokimi/dokimi/eval/answerer.py +++ b/dokimi/dokimi/eval/answerer.py @@ -111,7 +111,7 @@ def __init__( try: self.reranker = SentenceTransformerReranker(threshold=rerank_threshold) logger.info(f"Semantic reranker initialized with threshold={rerank_threshold:.2f}") - except Exception as e: + except Exception as e: # noqa: BLE001 - logged; the lexical reranker is the documented fallback logger.warning(f"Semantic reranker failed ({e}); fallback to lexical reranker") self.reranker = SimpleReranker(threshold=rerank_threshold) else: @@ -164,7 +164,7 @@ def _process(idx: int, q: Dict) -> None: question=q, max_tokens=max_tokens_per_question ) - except Exception as e: + except Exception as e: # noqa: BLE001 - the failure is stored on the pair and reported failure = e pair = { "question_id": q_id, diff --git a/dokimi/dokimi/eval/arms_run.py b/dokimi/dokimi/eval/arms_run.py index 0ecf6b161..893c1c812 100644 --- a/dokimi/dokimi/eval/arms_run.py +++ b/dokimi/dokimi/eval/arms_run.py @@ -14,6 +14,7 @@ from __future__ import annotations import functools +import logging from collections.abc import Callable from dataclasses import dataclass, replace from typing import TYPE_CHECKING @@ -34,6 +35,8 @@ from dokimi.eval import Judge, JudgePanel from dokimi.eval.arm_runner import ArmRunner +logger = logging.getLogger(__name__) + @dataclass(frozen=True) class ArmsRunConfig: @@ -166,6 +169,8 @@ def resolve(arm): except ValueError: raise except Exception: + logger.warning("library id for %r not resolved; using the product name", product, + exc_info=True) arm.library_id = product return arm.library_id diff --git a/dokimi/dokimi/eval/cross_judge.py b/dokimi/dokimi/eval/cross_judge.py index 6eeae7fd3..47e574e5d 100644 --- a/dokimi/dokimi/eval/cross_judge.py +++ b/dokimi/dokimi/eval/cross_judge.py @@ -360,7 +360,7 @@ def one(job): return (qid, arm), float( scorer(library, question, answer, (ground_truth or {}).get(qid), context) ), None - except Exception as exc: # an unscored answer drops out of the pairing, it does not kill the run + except Exception as exc: # noqa: BLE001 - an unscored answer drops out of the pairing, it does not kill the run return (qid, arm), None, _reason(exc) if not stored_flags: diff --git a/dokimi/dokimi/eval/pairwise.py b/dokimi/dokimi/eval/pairwise.py index a3611cb11..54246e189 100644 --- a/dokimi/dokimi/eval/pairwise.py +++ b/dokimi/dokimi/eval/pairwise.py @@ -296,7 +296,7 @@ def one(job): arm, qid, question, answer, base = job try: return arm, qid, judge.compare(library, question, answer, base) - except Exception as exc: # one failed question must not lose the run + except Exception as exc: # noqa: BLE001 - one failed question must not lose the run return arm, qid, exc report = ArmPairwiseReport(judge=label, baseline_arm=baseline_arm) diff --git a/dokimi/dokimi/eval/panel.py b/dokimi/dokimi/eval/panel.py index e9d6741e3..208266c10 100644 --- a/dokimi/dokimi/eval/panel.py +++ b/dokimi/dokimi/eval/panel.py @@ -360,7 +360,7 @@ def send(text: str, *, draw: int, cache_if: Callable[[str], bool]) -> str: caps=capped.get("caps"), unread_claims=capped.get("unread_claims"), numeric_claims=claims if isinstance(claims := data.get("numeric_claims"), list) else None, ) - except Exception as exc: + except Exception as exc: # noqa: BLE001 - logged; the vote carries the error and drops out logger.warning(f"Judge '{config.role}' ({config.model}) failed: {exc}") return JudgeVote( role=config.role, model=config.model, provider=config.provider, diff --git a/dokimi/dokimi/eval/ragas_eval.py b/dokimi/dokimi/eval/ragas_eval.py index cf701f218..79e44c1ff 100644 --- a/dokimi/dokimi/eval/ragas_eval.py +++ b/dokimi/dokimi/eval/ragas_eval.py @@ -255,7 +255,7 @@ def _score(self, Dataset, ragas_evaluate, rows: List[Dict[str, Any]], metrics: l } summary = {n: round(sum(vals) / len(vals), 4) for n in names if (vals := df[n].dropna().tolist())} - except Exception as exc: + except Exception as exc: # noqa: BLE001 - logged; RAGAS is optional, the run keeps its own scores logger.error("RAGAS %s evaluation failed: %s", arm, exc) return {}, {} return scores, summary diff --git a/dokimi/dokimi/eval/reranker.py b/dokimi/dokimi/eval/reranker.py index 1c6dfdc93..0f77c864e 100644 --- a/dokimi/dokimi/eval/reranker.py +++ b/dokimi/dokimi/eval/reranker.py @@ -12,7 +12,7 @@ try: from sentence_transformers import SentenceTransformer, util SENTENCE_TRANSFORMERS_AVAILABLE = True -except Exception: +except Exception: # noqa: BLE001 - importing torch can raise more than ImportError SENTENCE_TRANSFORMERS_AVAILABLE = False diff --git a/dokimi/dokimi/eval/validity_scoring.py b/dokimi/dokimi/eval/validity_scoring.py index bdcabac8e..d30463089 100644 --- a/dokimi/dokimi/eval/validity_scoring.py +++ b/dokimi/dokimi/eval/validity_scoring.py @@ -137,7 +137,7 @@ def intact(pair: Dict[str, Any]) -> Tuple[tuple, Any]: for _ in range(INTACT_ATTEMPTS): try: return key, _call(scorer, pair, pair["intact"]) - except Exception as exc: # one bad pair must not lose the whole run + except Exception as exc: # noqa: BLE001 - one bad pair must not lose the whole run failure = exc return key, failure @@ -152,7 +152,7 @@ def degraded(pair: Dict[str, Any]) -> ValidityRow: if pair_scorer is not None: try: row.intact_score, row.degraded_score = pair_scorer(pair) - except Exception as exc: # one bad pair must not lose the whole run + except Exception as exc: # noqa: BLE001 - one bad pair must not lose the whole run row.error = f"{type(exc).__name__}: {exc}"[:200] return row base = intact_cache[(pair["base_id"], pair.get("arm", ""))] @@ -161,7 +161,7 @@ def degraded(pair: Dict[str, Any]) -> ValidityRow: raise base row.intact_score = base row.degraded_score = _call(scorer, pair, pair["degraded"]) - except Exception as exc: # one bad pair must not lose the whole run + except Exception as exc: # noqa: BLE001 - one bad pair must not lose the whole run row.error = f"{type(exc).__name__}: {exc}"[:200] return row diff --git a/dokimi/dokimi/harnesses/egress_proxy.py b/dokimi/dokimi/harnesses/egress_proxy.py index 145dfd931..5f48282c0 100644 --- a/dokimi/dokimi/harnesses/egress_proxy.py +++ b/dokimi/dokimi/harnesses/egress_proxy.py @@ -260,7 +260,7 @@ def _handle(self, client: socket.socket) -> None: client.sendall(b"HTTP/1.1 200 Connection Established\r\n\r\n") state["established"] = True _relay(client, remote) - except Exception as exc: # one bad tunnel must not take the proxy down + except Exception as exc: # noqa: BLE001 - one bad tunnel must not take the proxy down self._record(state["host"], state["port"], "error", f"{type(exc).__name__}: {exc}") if not state["established"]: # the client learns the route failed, not just a closed socket try: diff --git a/dokimi/dokimi/judged.py b/dokimi/dokimi/judged.py index 01fb7a9ef..27baf6ef5 100644 --- a/dokimi/dokimi/judged.py +++ b/dokimi/dokimi/judged.py @@ -29,7 +29,7 @@ def _accepts(parse: Callable[[str], Any]) -> Callable[[str], bool]: def parses(raw: str) -> bool: try: return parse(raw) is not None - except Exception: + except Exception: # noqa: BLE001 - a cache predicate: any parse failure means do not cache return False return parses diff --git a/dokimi/dokimi/llm.py b/dokimi/dokimi/llm.py index b2299a4d6..a3c9819da 100644 --- a/dokimi/dokimi/llm.py +++ b/dokimi/dokimi/llm.py @@ -223,7 +223,7 @@ def unsupported_sampling_params( from litellm import get_supported_openai_params supported = get_supported_openai_params(model=resolve_model(model, provider)) or [] - except Exception: + except Exception: # noqa: BLE001 - litellm raises many types for an unknown model; [] drops nothing return [] if not supported: return [] @@ -351,7 +351,7 @@ def _safe_completion_cost(resp: Any, model: str, litellm_model: Optional[str] = if cost == 0.0 and not _has_price_entry(litellm_model or model, model): raise ValueError("model is not in litellm's price map") return cost - except Exception as exc: + except Exception as exc: # noqa: BLE001 - logged once per model; cost is null, never a fake $0 if model not in _COST_UNKNOWN_LOGGED: _COST_UNKNOWN_LOGGED.add(model) logger.info("No litellm pricing for model %r; cost_usd will be null (%s)", model, exc) @@ -891,7 +891,7 @@ def ask_parsed(ask: Ask, prompt: str, parse: Callable[[str], Optional[T]], *, def parses(raw: str) -> bool: try: return parse(raw) is not None - except Exception: + except Exception: # noqa: BLE001 - a cache predicate: any parse failure means do not cache return False raw, error = "", "" @@ -900,7 +900,7 @@ def parses(raw: str) -> bool: raw = ask(asked, draw=draw * attempts + attempt, cache_if=parses) try: value = parse(raw) - except Exception as exc: + except Exception as exc: # noqa: BLE001 - the parse error is kept and re-asked, then raised error = str(exc) else: if value is not None: diff --git a/dokimi/dokimi/llm_json.py b/dokimi/dokimi/llm_json.py index 8ce439cfc..582fffee5 100644 --- a/dokimi/dokimi/llm_json.py +++ b/dokimi/dokimi/llm_json.py @@ -25,7 +25,7 @@ def extract_json_object(text: str) -> dict: result = json.loads(text) if isinstance(result, dict): return result - except Exception: + except (ValueError, RecursionError): # not JSON here: try the next form pass # Markdown fence fence = re.search(r"```(?:json)?\s*([\s\S]*?)```", text) @@ -34,7 +34,7 @@ def extract_json_object(text: str) -> dict: result = json.loads(fence.group(1).strip()) if isinstance(result, dict): return result - except Exception: + except (ValueError, RecursionError): pass # First { ... } block m = re.search(r"\{[\s\S]*\}", text) @@ -58,7 +58,7 @@ def extract_json_array(text: str) -> list: result = json.loads(text) if isinstance(result, list): return result - except Exception: + except (ValueError, RecursionError): # not JSON here: try the next form pass # Markdown fence fence = re.search(r"```(?:json)?\s*([\s\S]*?)```", text) @@ -67,7 +67,7 @@ def extract_json_array(text: str) -> list: result = json.loads(fence.group(1).strip()) if isinstance(result, list): return result - except Exception: + except (ValueError, RecursionError): pass # First [ ... ] block m = re.search(r"\[[\s\S]*\]", text) diff --git a/dokimi/dokimi/llm_pricing.py b/dokimi/dokimi/llm_pricing.py index b99f2179d..341604010 100644 --- a/dokimi/dokimi/llm_pricing.py +++ b/dokimi/dokimi/llm_pricing.py @@ -26,7 +26,7 @@ def list_cost_usd(model: str, provider: str, prompt_tokens: int, try: prompt, completion = cost_per_token(model=litellm_model, prompt_tokens=prompt_tokens, completion_tokens=completion_tokens) - except Exception: + except Exception: # noqa: BLE001 - litellm raises many types for an unpriced model; None = unknown return None if prompt + completion == 0.0 and not _has_price_entry(litellm_model, model): return None diff --git a/dokimi/dokimi/mcp/context7.py b/dokimi/dokimi/mcp/context7.py index 4710b4c2a..2d76d49ab 100644 --- a/dokimi/dokimi/mcp/context7.py +++ b/dokimi/dokimi/mcp/context7.py @@ -223,7 +223,7 @@ def check_connection(self, library_id: str, query: str = "overview") -> bool: # max_tokens=500 ) return len(docs) > 0 and len(docs[0]["content"]) > 0 - except Exception as e: + except Exception as e: # noqa: BLE001 - logged; a connection check answers False logger.error(f"Context7 connection check failed: {e}") return False diff --git a/dokimi/dokimi/mcp/local_markdown.py b/dokimi/dokimi/mcp/local_markdown.py index de99946ad..d4c9ff271 100644 --- a/dokimi/dokimi/mcp/local_markdown.py +++ b/dokimi/dokimi/mcp/local_markdown.py @@ -88,7 +88,7 @@ def get_library_docs( for fp in files: try: content = self._read_file(fp, char_limit) - except Exception as exc: + except Exception as exc: # noqa: BLE001 - logged; one unreadable file is skipped logger.debug(f"Skipping {fp}: {exc}") continue diff --git a/dokimi/dokimi/mcp/mcp_protocol.py b/dokimi/dokimi/mcp/mcp_protocol.py index 0353a1aee..0dc5b37ed 100644 --- a/dokimi/dokimi/mcp/mcp_protocol.py +++ b/dokimi/dokimi/mcp/mcp_protocol.py @@ -81,6 +81,7 @@ def resolve_library_id(self, library_name: str) -> str: tools = self._list_tool_names() resolve = self._pick_tool(tools, self.resolve_tool, _RESOLVE_TOOL_HINTS) except Exception: + logger.exception("listing MCP tools failed; resolving by name") resolve = None if not resolve: return library_name @@ -127,7 +128,7 @@ def check_connection(self) -> bool: try: self._list_tool_names() return True - except Exception: + except Exception: # noqa: BLE001 - a connection check answers False on any failure return False # โ”€โ”€ MCP session plumbing โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ diff --git a/dokimi/dokimi/mcp/okf_client.py b/dokimi/dokimi/mcp/okf_client.py index eafff9e8a..97742ee01 100644 --- a/dokimi/dokimi/mcp/okf_client.py +++ b/dokimi/dokimi/mcp/okf_client.py @@ -143,7 +143,7 @@ def get_library_docs( for fp in files: try: raw = fp.read_text(encoding=self.encoding) - except Exception as exc: + except Exception as exc: # noqa: BLE001 - logged; one unreadable file is skipped logger.debug("OKFClient: skipping %s: %s", fp, exc) continue diff --git a/dokimi/dokimi/mcp/url_client.py b/dokimi/dokimi/mcp/url_client.py index 433562de1..66e180292 100644 --- a/dokimi/dokimi/mcp/url_client.py +++ b/dokimi/dokimi/mcp/url_client.py @@ -125,7 +125,7 @@ def check_connection(self) -> bool: try: resp = httpx.head(self.url, timeout=self.timeout, follow_redirects=True) return 200 <= resp.status_code < 300 - except Exception as exc: + except Exception as exc: # noqa: BLE001 - logged; a connection check answers False logger.debug(f"URLClient.check_connection failed: {exc}") return False diff --git a/dokimi/dokimi/metrics/example_runner.py b/dokimi/dokimi/metrics/example_runner.py index 16e0d00f5..56b725a1f 100644 --- a/dokimi/dokimi/metrics/example_runner.py +++ b/dokimi/dokimi/metrics/example_runner.py @@ -237,7 +237,7 @@ def _run_docker(lang: str, code: str, policy: ExecutionPolicy, result = runner(cmd, timeout=policy.timeout + 30, env=process.Env.inherit()) except FileNotFoundError: return False, "docker backend selected but docker is not installed" - except Exception as exc: # noqa: BLE001 + except Exception as exc: # noqa: BLE001 - reported as the run's execution error return False, f"Execution error: {type(exc).__name__}: {exc}" if result.timed_out: diff --git a/dokimi/dokimi/novelty/atoms.py b/dokimi/dokimi/novelty/atoms.py index 8ac192067..aff0a4e9d 100644 --- a/dokimi/dokimi/novelty/atoms.py +++ b/dokimi/dokimi/novelty/atoms.py @@ -311,7 +311,7 @@ def _unread_at_cap(stop: SpendCapReached, batch: list[Span], parse: Callable[[st kept: list[Atom] = [] try: # all or nothing: a reply that fails part-way keeps no atoms, and its batch is unread _keep_atoms(parse(stop.reply), valid_ids, set(seen), kept) - except Exception: # not usable, as after any failed call + except Exception: # noqa: BLE001 - not usable, as after any failed call return batch atoms.extend(kept) seen.update(atom.id for atom in kept) @@ -435,7 +435,7 @@ def parse(reply: str) -> list: _stop_at_cap(spans, _unread_at_cap(exc, batch, parse, valid_ids, seen, atoms), pending, report, exc) break - except Exception as exc: + except Exception as exc: # noqa: BLE001 - re-asked in halves or recorded as the batch's error cut = isinstance(exc, OutputTruncated) if (unparseable or cut) and len(batch) > 1: # Unparseable, most often truncated: re-ask the two halves, in place, @@ -575,7 +575,7 @@ def verify_atoms( _VERIFY_PROMPT.format(statement=atom.statement, source=source), usable=_names_a_faithfulness_verdict, ).get("verdict", "")).strip() - except Exception: + except Exception: # noqa: BLE001 - an unverified atom is counted as unchecked verdict = "" atom.faithfulness = verdict if verdict in tally and verdict != "unchecked" else "unchecked" tally[atom.faithfulness] += 1 diff --git a/dokimi/dokimi/novelty/batching.py b/dokimi/dokimi/novelty/batching.py index 0169deef0..1bde07428 100644 --- a/dokimi/dokimi/novelty/batching.py +++ b/dokimi/dokimi/novelty/batching.py @@ -127,7 +127,7 @@ def read(chunk: list[T], reply: str) -> Any: raise ValueError("the model returned an empty reply") replied = True result.answered.append((chunk, read(chunk, reply))) - except Exception as exc: + except Exception as exc: # noqa: BLE001 - re-asked in halves or recorded as the chunk's error half = len(chunk) // 2 # A reply cut at the ceiling is re-asked smaller, like one that will not parse. if (replied or isinstance(exc, OutputTruncated)) and half >= max(min_size, 1): diff --git a/dokimi/dokimi/novelty/client.py b/dokimi/dokimi/novelty/client.py index d192fc5c5..8a9de63a7 100644 --- a/dokimi/dokimi/novelty/client.py +++ b/dokimi/dokimi/novelty/client.py @@ -294,7 +294,7 @@ def accepting(parse: Callable[[str], Any]) -> Accept: def accept(reply: str) -> bool: try: parse(reply) - except Exception: + except Exception: # noqa: BLE001 - an accept predicate: any parse failure rejects the reply return False return True diff --git a/dokimi/dokimi/novelty/measure.py b/dokimi/dokimi/novelty/measure.py index 8e8428e88..8a103d5b6 100644 --- a/dokimi/dokimi/novelty/measure.py +++ b/dokimi/dokimi/novelty/measure.py @@ -344,7 +344,7 @@ def one(job: tuple[Probe, str]) -> ProbeRun: ) except SpendCapReached as exc: # a call with no price: its paid reply is kept, ungraded return _unpriced_answer_run(probe, arm, exc, stops, ungraded) - except Exception as exc: # a provider failure is a missing datum, not a wrong answer + except Exception as exc: # noqa: BLE001 - a provider failure is a missing datum, not a wrong answer response, confidence = f"", None verdict = ERRORED else: @@ -438,7 +438,7 @@ def _grade(probe: Probe, response: str, judge: LLMClient) -> str: ) try: raw = judge.json_object(prompt, usable=_names_a_verdict).get("verdict") - except Exception: + except Exception: # noqa: BLE001 - a failed judge call is the ERRORED verdict, not a wrong one return ERRORED verdict = raw.strip() if isinstance(raw, str) else "" if not verdict: @@ -456,7 +456,7 @@ def _reply_verdict(reply: str | None) -> str | None: return None try: value = parse_json_object(reply) - except Exception: + except ValueError: # an unparseable reply names no verdict return None return value["verdict"] if _names_a_verdict(value) else None diff --git a/dokimi/dokimi/novelty/needs.py b/dokimi/dokimi/novelty/needs.py index 9c3e0e9ab..2fb16d21e 100644 --- a/dokimi/dokimi/novelty/needs.py +++ b/dokimi/dokimi/novelty/needs.py @@ -163,7 +163,7 @@ def build_need_graph(name: str, description: str, *, client: LLMClient) -> NeedG system=_GRAPH_SYSTEM, usable=_has_a_node, ) - except Exception: + except Exception: # noqa: BLE001 - a failed graph call keeps the graph built so far return graph for index, row in enumerate(data.get("nodes", []) or [], start=1): if not isinstance(row, dict) or not str(row.get("text", "")).strip(): diff --git a/dokimi/dokimi/novelty/probes.py b/dokimi/dokimi/novelty/probes.py index e0715991f..a646c0499 100644 --- a/dokimi/dokimi/novelty/probes.py +++ b/dokimi/dokimi/novelty/probes.py @@ -463,7 +463,7 @@ def _generate(client: LLMClient, template: str, lines: list[str]) -> list[str]: key = "questions" if "{questions}" in template else "statements" try: rows = client.json_array(template.format(**{key: numbered})) - except Exception: + except Exception: # noqa: BLE001 - a failed rewording call leaves the lines unworded rows = [] worded = [ # A generator that echoes the "1. " list numbering would otherwise add an anchor. diff --git a/dokimi/dokimi/novelty/retention.py b/dokimi/dokimi/novelty/retention.py index 7c6f331e2..967190b52 100644 --- a/dokimi/dokimi/novelty/retention.py +++ b/dokimi/dokimi/novelty/retention.py @@ -169,7 +169,7 @@ def _adjudicate(self, atom: Atom, text: str) -> str | None: _ADJUDICATE_PROMPT.format(statement=atom.statement, text=text[:12000]), accept=lambda raw: parse_verdict(raw) is not None, ) - except Exception: + except Exception: # noqa: BLE001 - a failed adjudication is no verdict (None), not a wrong one return None return parse_verdict(reply) diff --git a/dokimi/dokimi/personas/analyzer.py b/dokimi/dokimi/personas/analyzer.py index d5cee8f12..0aa0cf318 100644 --- a/dokimi/dokimi/personas/analyzer.py +++ b/dokimi/dokimi/personas/analyzer.py @@ -126,7 +126,7 @@ def _analyze_with_pygithub(self, client: "Github", repo_name: str) -> Dict[str, return analysis - except Exception as e: + except Exception as e: # noqa: BLE001 - logged; falls back to the gh CLI logger.error(f"PyGithub analysis error: {e}, falling back to gh CLI") return self._analyze_with_gh_cli(repo_name) @@ -165,7 +165,7 @@ def _analyze_with_gh_cli(self, repo_name: str) -> Dict[str, Any]: "topics": [] } - except Exception as e: + except Exception as e: # noqa: BLE001 - logged; an empty analysis logger.error(f"gh CLI analysis failed: {e}") return self._empty_analysis() @@ -217,7 +217,7 @@ def _get_readme(self, repo) -> str: try: readme = repo.get_readme() return readme.decoded_content.decode('utf-8') - except Exception: + except Exception: # noqa: BLE001 - a repo without a readable README has no README text return "" def _extract_use_cases(self, repo) -> List[str]: @@ -272,6 +272,7 @@ def _analyze_issues(self, repo, limit: int = 50) -> Dict[str, Any]: issues = repo.get_issues(state='all', labels=[label]) issues_to_analyze.extend(list(issues[:limit // len(question_labels)])) except Exception: + logger.warning("Issues for label %r not fetched", label, exc_info=True) continue # Extract patterns @@ -302,7 +303,7 @@ def _analyze_issues(self, repo, limit: int = 50) -> Dict[str, Any]: "sample_issues": sample_issues[:10] } - except Exception as e: + except Exception as e: # noqa: BLE001 - logged; an empty issue sample logger.error(f"Issue analysis error: {e}") return {"common_questions": [], "common_labels": [], "sample_issues": []} diff --git a/dokimi/dokimi/questions/chunk_gen.py b/dokimi/dokimi/questions/chunk_gen.py index 082e665e4..296b003b5 100644 --- a/dokimi/dokimi/questions/chunk_gen.py +++ b/dokimi/dokimi/questions/chunk_gen.py @@ -182,7 +182,7 @@ def generate( logger.info(f"Fetching docs for chunk-based generation: {doc_url}") try: raw = _fetch_url(doc_url) - except Exception as exc: + except Exception as exc: # noqa: BLE001 - logged; an empty result for that URL logger.error(f"Failed to fetch {doc_url}: {exc}") return ChunkQuestionResult(source_url=doc_url) @@ -220,7 +220,7 @@ def generate( qs = self._gen_from_chunk(library_name, chunk, doc_url, idx, count) all_questions.extend(qs) remaining -= len(qs) - except Exception as exc: + except Exception as exc: # noqa: BLE001 - logged; one failed chunk does not lose the rest logger.warning(f"Chunk {idx} failed: {exc}") return ChunkQuestionResult( diff --git a/dokimi/dokimi/questions/llm_gen.py b/dokimi/dokimi/questions/llm_gen.py index 734242f3d..fd99b0da4 100644 --- a/dokimi/dokimi/questions/llm_gen.py +++ b/dokimi/dokimi/questions/llm_gen.py @@ -271,7 +271,7 @@ def _generate_for_persona( } }) - except Exception as e: + except Exception as e: # noqa: BLE001 - logged; one failed persona x topic does not lose the rest logger.error( f"Failed to generate questions for {persona['name']} ร— {topic}: {e}" ) diff --git a/dokimi/dokimi/questions/panel_reviewer.py b/dokimi/dokimi/questions/panel_reviewer.py index 29e1c89bc..643fdb461 100644 --- a/dokimi/dokimi/questions/panel_reviewer.py +++ b/dokimi/dokimi/questions/panel_reviewer.py @@ -205,7 +205,7 @@ def _call_reviewer(self, reviewer: str, question: str, library_name: str) -> Rev flags=flags, primary_score=primary, ) - except Exception as exc: + except Exception as exc: # noqa: BLE001 - logged; the vote carries the error logger.warning(f"Reviewer '{reviewer}' failed on '{question[:50]}': {exc}") return ReviewerVote(reviewer=reviewer, scores={}, reasoning="", flags=[], error=str(exc)) diff --git a/dokimi/dokimi/questions/quality_analyzer.py b/dokimi/dokimi/questions/quality_analyzer.py index 2a7cfaa7d..bf5fab7bb 100644 --- a/dokimi/dokimi/questions/quality_analyzer.py +++ b/dokimi/dokimi/questions/quality_analyzer.py @@ -92,7 +92,7 @@ def classify_question(self, question: str, library_name: str) -> QuestionClassif raw = llm_call(prompt, model=self.model, provider=self.provider) from dokimi.llm import extract_json_object return _classification(question, extract_json_object(raw)) - except Exception as exc: + except Exception as exc: # noqa: BLE001 - logged; a default classification logger.warning(f"Failed to classify question: {exc}") return QuestionClassification( question=question, diff --git a/dokimi/dokimi/questions/ragas_seed.py b/dokimi/dokimi/questions/ragas_seed.py index 078c16ce5..225c42804 100644 --- a/dokimi/dokimi/questions/ragas_seed.py +++ b/dokimi/dokimi/questions/ragas_seed.py @@ -112,7 +112,7 @@ def _fetch_docs( max_tokens=max_tokens, ) return "\n\n".join(c["content"] for c in chunks) - except Exception as e: + except Exception as e: # noqa: BLE001 - logged; no seed docs logger.error("Failed to fetch docs for %s: %s", library_id, e) return "" @@ -141,7 +141,7 @@ def _extract_via_llm( # Clean and deduplicate topics = list(dict.fromkeys(str(t).strip() for t in topics if t)) return topics[:max_topics] - except Exception as e: + except Exception as e: # noqa: BLE001 - logged; falls back to the heuristic extractor logger.error("LLM topic extraction failed: %s โ€” falling back to heuristic", e) return self._extract_heuristic(docs, library_name, max_topics) diff --git a/dokimi/dokimi/questions/refiner.py b/dokimi/dokimi/questions/refiner.py index cd1062946..eb05d7af0 100644 --- a/dokimi/dokimi/questions/refiner.py +++ b/dokimi/dokimi/questions/refiner.py @@ -301,7 +301,7 @@ def fill( }) filled[level] = len(texts[:count]) logger.info(f" Generated {filled[level]}/{count} for {level}") - except Exception as exc: + except Exception as exc: # noqa: BLE001 - logged; that level is filled with 0 logger.error(f"Gap fill failed for {level}: {exc}") filled[level] = 0 diff --git a/dokimi/dokimi/questions/validator.py b/dokimi/dokimi/questions/validator.py index bdea05ee7..2974b9920 100644 --- a/dokimi/dokimi/questions/validator.py +++ b/dokimi/dokimi/questions/validator.py @@ -331,7 +331,7 @@ def _validate_question(self, library_name: str, question_text: str) -> Optional[ return score - except Exception as e: + except Exception as e: # noqa: BLE001 - logged; the score is marked unscored, not graded logger.error(f"Validation failed for question: {question_text[:50]}... โ€” {e}") return {"relevance": 100, "answerability": 100, "specificity": 100, "aggregate": 100, "unscored": True, diff --git a/dokimi/dokimi/repos/hits_judge.py b/dokimi/dokimi/repos/hits_judge.py index dcf3d547d..2d71b641f 100644 --- a/dokimi/dokimi/repos/hits_judge.py +++ b/dokimi/dokimi/repos/hits_judge.py @@ -381,7 +381,7 @@ def judge_pair(ask: Ask, pair: Pair) -> dict[str, Any]: return {**out, "error": f"no reply parsed: {exc}"} except BudgetSpent: raise - except Exception as exc: # a 5xx or 429 past the retries, an auth error: this pair is unjudged, the run goes on + except Exception as exc: # noqa: BLE001 - a 5xx or 429 past the retries, an auth error: this pair is unjudged, the run goes on return {**out, "error": f"{CALL_FAILED}: {type(exc).__name__}: {exc}"} return {**out, "label": label, "reason": reason} diff --git a/dokimi/dokimi/repos/l0/branches.py b/dokimi/dokimi/repos/l0/branches.py index 760e5fc2b..10f798747 100644 --- a/dokimi/dokimi/repos/l0/branches.py +++ b/dokimi/dokimi/repos/l0/branches.py @@ -108,7 +108,7 @@ def _load(scan: Scan, path: str) -> Any: text = scan.text(path) try: return yaml.load(text, Loader=_Loader) # nosec B506 - a SafeLoader subclass - except Exception: # PyYAML's constructors raise more than YAMLError: a bad date, tag value, depth + except Exception: # noqa: BLE001 - PyYAML's constructors raise more than YAMLError: a bad date, tag value, depth return None diff --git a/dokimi/dokimi/repos/l0/versions.py b/dokimi/dokimi/repos/l0/versions.py index 181591cbb..bd62c743d 100644 --- a/dokimi/dokimi/repos/l0/versions.py +++ b/dokimi/dokimi/repos/l0/versions.py @@ -213,7 +213,7 @@ def workflow_versions(scan: Scan, path: str, unknown: list[str], seen: dict[str, text = scan.text(path) # outside the try: a failed git read (SourceError is a ValueError) is no YAML error try: doc = yaml.safe_load(text) - except Exception: # PyYAML's constructors raise more than YAMLError: a bad date, tag value, depth + except Exception: # noqa: BLE001 - PyYAML's constructors raise more than YAMLError: a bad date, tag value, depth unknown.append(f"{path} is not valid YAML") return set() jobs = doc.get("jobs") if isinstance(doc, dict) else None diff --git a/dokimi/dokimi/repos/l0/workflows.py b/dokimi/dokimi/repos/l0/workflows.py index b56a414c5..2a5f66458 100644 --- a/dokimi/dokimi/repos/l0/workflows.py +++ b/dokimi/dokimi/repos/l0/workflows.py @@ -63,7 +63,7 @@ def _action_users(scan: Scan) -> tuple[dict[str, str], dict[str, str]]: for path in paths: try: doc = yaml.safe_load(scan.text(path)) - except Exception: # PyYAML's constructors raise more than YAMLError: a bad date, tag value, depth + except Exception: # noqa: BLE001, S112 - PyYAML's constructors raise more than YAMLError: a bad date, tag value, depth continue for value in _uses(doc, path in flows): # `.github/workflows/action.yml` is a workflow name, at, ref = value.strip().partition("@") diff --git a/dokimi/dokimi/tool_preference.py b/dokimi/dokimi/tool_preference.py index ff766184b..e0b1e4a30 100644 --- a/dokimi/dokimi/tool_preference.py +++ b/dokimi/dokimi/tool_preference.py @@ -331,7 +331,7 @@ def one(_: int) -> None: res.update(choice=classify(item, out.get("called") or []), text_preview=(out.get("text") or "")[:200], truncated=bool(out.get("truncated"))) - except Exception as exc: # recorded, not raised + except Exception as exc: # noqa: BLE001 - recorded, not raised res["error"] = f"{type(exc).__name__}: {exc}"[:300] if hasattr(exc, "usage"): billed, usage = True, exc.usage diff --git a/dokimi/dokimi/treatments/arms.py b/dokimi/dokimi/treatments/arms.py index 103e784c8..8fd3a00be 100644 --- a/dokimi/dokimi/treatments/arms.py +++ b/dokimi/dokimi/treatments/arms.py @@ -67,7 +67,7 @@ def _default_reranker(threshold: float): if SENTENCE_TRANSFORMERS_AVAILABLE: try: return SentenceTransformerReranker(threshold=threshold) - except Exception as exc: # pragma: no cover - env dependent + except Exception as exc: # noqa: BLE001 - pragma: no cover - env dependent logger.warning("Semantic reranker unavailable (%s); using lexical", exc) return SimpleReranker(threshold=threshold) diff --git a/dokimi/pyproject.toml b/dokimi/pyproject.toml index 64868be4e..266ff0ddd 100644 --- a/dokimi/pyproject.toml +++ b/dokimi/pyproject.toml @@ -208,6 +208,7 @@ select = [ "E711", "E712", "E713", "E714", "E721", "E722", # None/bool/type compares, bare except "A", # shadowed builtins and stdlib modules "ASYNC", # blocking calls in async code + "BLE001", "S110", "S112", # blind except, except-pass/continue: each kept site says why in its noqa "DTZ", # naive datetimes "EXE", # shebang vs executable bit "G", # logging call arguments diff --git a/dokimi/scripts/ci_local.py b/dokimi/scripts/ci_local.py index 1ef46845a..1dc4d6bd7 100755 --- a/dokimi/scripts/ci_local.py +++ b/dokimi/scripts/ci_local.py @@ -244,7 +244,7 @@ def network_ok() -> bool: try: urllib.request.urlopen("https://pypi.org/simple/pip/", timeout=8) # nosec B310 return True - except Exception: + except Exception: # noqa: BLE001 - a reachability probe answers False on any failure return False diff --git a/dokimi/tests/test_noqa_reasons.py b/dokimi/tests/test_noqa_reasons.py new file mode 100644 index 000000000..0d3ae9406 --- /dev/null +++ b/dokimi/tests/test_noqa_reasons.py @@ -0,0 +1,77 @@ +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +"""Every kept blind ``except`` says why in its ``noqa``. + +ruff's BLE001, S110 and S112 fail an ``except Exception`` that neither re-raises nor +logs the traceback, and an ``except`` whose body is only ``pass`` or ``continue``. +A site that must catch everything keeps a ``# noqa: BLE001 - `` naming what +happens to the failure (logged, recorded on the row, re-asked, answered False). ruff +accepts a bare ``# noqa: BLE001``; this test does not, so the gate cannot be passed +by adding the code alone. +""" + +from __future__ import annotations + +import io +import re +import tokenize +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +CODES = {"BLE001", "S110", "S112"} +# ruff's extend-exclude in pyproject.toml, plus the virtualenvs a checkout may hold. +SKIP = ("benchmarks/", "data/", "tests/fixtures/task_formats/", ".venv/", "venv/", "build/") +_NOQA = re.compile(r"#\s*noqa:\s*(?P[A-Z]+[0-9]+(?:\s*,\s*[A-Z]+[0-9]+)*)(?P.*)$") +_REASON = re.compile(r"^\s*(?:[-โ€”:(]\s*)?(?P.*?)\)?\s*$") + + +def unexplained(source: str) -> list[int]: + """Lines of *source* whose ``noqa`` names a blind-except code but gives no reason.""" + lines = [] + for tok in tokenize.generate_tokens(io.StringIO(source).readline): + if tok.type != tokenize.COMMENT: + continue + m = _NOQA.search(tok.string) + if not m or not CODES & {c.strip() for c in m.group("codes").split(",")}: + continue + reason = _REASON.match(m.group("rest")) + if not reason or len(reason.group("text").split()) < 2: + lines.append(tok.start[0]) + return lines + + +def _python_files() -> list[Path]: + out = [] + for path in sorted(ROOT.rglob("*.py")): + rel = path.relative_to(ROOT).as_posix() + if not rel.startswith(SKIP): + out.append(path) + return out + + +def test_every_blind_except_noqa_has_a_reason() -> None: + bad = [f"{p.relative_to(ROOT).as_posix()}:{line}" + for p in _python_files() + for line in unexplained(p.read_text(encoding="utf-8"))] + assert not bad, "a # noqa for BLE001/S110/S112 needs a reason after it: " + ", ".join(bad) + + +def test_a_bare_code_fails() -> None: + src = "try:\n x()\nexcept Exception: # noqa: BLE001\n pass\n" + assert unexplained(src) == [3] + + +def test_a_code_with_one_word_fails() -> None: + assert unexplained("try:\n x()\nexcept Exception: # noqa: BLE001 - fine\n pass\n") == [3] + + +def test_a_reason_passes_in_each_separator_style() -> None: + for comment in ("# noqa: BLE001 - one bad cell must not end the sweep", + "# noqa: BLE001 โ€” a bad frontmatter keeps its dir name", + "# noqa: BLE001, S110 (logged and skipped)"): + assert unexplained(f"try:\n x()\nexcept Exception: {comment}\n pass\n") == [] + + +def test_other_codes_are_not_checked() -> None: + assert unexplained("import os # noqa: F401\n") == [] From b4f2479ca9ed97ca04e58b895f292062cd16d959 Mon Sep 17 00:00:00 2001 From: "Petrov, Nikolay A" Date: Thu, 12 Nov 2026 23:25:06 -0800 Subject: [PATCH 02/13] test_noqa_reasons: match NOQA case-insensitively, strip doubled separators Signed-off-by: Petrov, Nikolay A --- dokimi/tests/test_noqa_reasons.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dokimi/tests/test_noqa_reasons.py b/dokimi/tests/test_noqa_reasons.py index 0d3ae9406..592df1459 100644 --- a/dokimi/tests/test_noqa_reasons.py +++ b/dokimi/tests/test_noqa_reasons.py @@ -22,8 +22,8 @@ CODES = {"BLE001", "S110", "S112"} # ruff's extend-exclude in pyproject.toml, plus the virtualenvs a checkout may hold. SKIP = ("benchmarks/", "data/", "tests/fixtures/task_formats/", ".venv/", "venv/", "build/") -_NOQA = re.compile(r"#\s*noqa:\s*(?P[A-Z]+[0-9]+(?:\s*,\s*[A-Z]+[0-9]+)*)(?P.*)$") -_REASON = re.compile(r"^\s*(?:[-โ€”:(]\s*)?(?P.*?)\)?\s*$") +_NOQA = re.compile(r"#\s*noqa:\s*(?P[A-Z]+[0-9]+(?:\s*,\s*[A-Z]+[0-9]+)*)(?P.*)$", re.I) +_REASON = re.compile(r"^\s*(?:[-โ€”:(]+\s*)?(?P.*?)\)?\s*$") def unexplained(source: str) -> list[int]: @@ -75,3 +75,7 @@ def test_a_reason_passes_in_each_separator_style() -> None: def test_other_codes_are_not_checked() -> None: assert unexplained("import os # noqa: F401\n") == [] + + +def test_upper_case_and_doubled_dashes_are_still_checked() -> None: + assert unexplained("try:\n x()\nexcept Exception: # NOQA: BLE001 -- ok\n pass\n") == [3] From 3616f06edf374882ccdd3afd34d2c1ca28735c98 Mon Sep 17 00:00:00 2001 From: Alexander Rybkin Date: Fri, 13 Nov 2026 23:31:51 -0800 Subject: [PATCH 03/13] test(noqa): check gated codes in a space-separated noqa list ruff reads `# noqa: E722 BLE001 S110` as three codes, but the reason gate split codes on commas only, so a gated code after a space-separated one was never checked and a bare except passed without a reason. Split the code list on commas and whitespace, as ruff does. Signed-off-by: Alexander Rybkin --- dokimi/tests/test_noqa_reasons.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/dokimi/tests/test_noqa_reasons.py b/dokimi/tests/test_noqa_reasons.py index 592df1459..80f51f4fb 100644 --- a/dokimi/tests/test_noqa_reasons.py +++ b/dokimi/tests/test_noqa_reasons.py @@ -22,7 +22,7 @@ CODES = {"BLE001", "S110", "S112"} # ruff's extend-exclude in pyproject.toml, plus the virtualenvs a checkout may hold. SKIP = ("benchmarks/", "data/", "tests/fixtures/task_formats/", ".venv/", "venv/", "build/") -_NOQA = re.compile(r"#\s*noqa:\s*(?P[A-Z]+[0-9]+(?:\s*,\s*[A-Z]+[0-9]+)*)(?P.*)$", re.I) +_NOQA = re.compile(r"#\s*noqa:\s*(?P[A-Z]+[0-9]+(?:[\s,]+[A-Z]+[0-9]+)*)(?P.*)$", re.I) _REASON = re.compile(r"^\s*(?:[-โ€”:(]+\s*)?(?P.*?)\)?\s*$") @@ -33,7 +33,7 @@ def unexplained(source: str) -> list[int]: if tok.type != tokenize.COMMENT: continue m = _NOQA.search(tok.string) - if not m or not CODES & {c.strip() for c in m.group("codes").split(",")}: + if not m or not CODES & set(re.split(r"[\s,]+", m.group("codes"))): continue reason = _REASON.match(m.group("rest")) if not reason or len(reason.group("text").split()) < 2: @@ -79,3 +79,8 @@ def test_other_codes_are_not_checked() -> None: def test_upper_case_and_doubled_dashes_are_still_checked() -> None: assert unexplained("try:\n x()\nexcept Exception: # NOQA: BLE001 -- ok\n pass\n") == [3] + + +def test_a_code_after_a_space_separated_one_is_still_checked() -> None: + assert unexplained("try:\n x()\nexcept: # noqa: E722 BLE001 S110\n pass\n") == [3] + assert unexplained("try:\n x()\nexcept: # noqa: E722 BLE001 S110 - logged by the caller\n pass\n") == [] From 391f5755b49c84af3763150619bf410d790c4c6a Mon Sep 17 00:00:00 2001 From: Alexander Rybkin Date: Fri, 13 Nov 2026 23:38:14 -0800 Subject: [PATCH 04/13] test(noqa): require a reason after a file-level ruff/flake8 noqa `# ruff: noqa: BLE001, S110` silences the rules for the whole file, and the reason gate only matched a line-level `# noqa:`, so a file-level directive let every blind except in the file through without a reason. Signed-off-by: Alexander Rybkin --- dokimi/tests/test_noqa_reasons.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/dokimi/tests/test_noqa_reasons.py b/dokimi/tests/test_noqa_reasons.py index 80f51f4fb..9403383b1 100644 --- a/dokimi/tests/test_noqa_reasons.py +++ b/dokimi/tests/test_noqa_reasons.py @@ -22,7 +22,7 @@ CODES = {"BLE001", "S110", "S112"} # ruff's extend-exclude in pyproject.toml, plus the virtualenvs a checkout may hold. SKIP = ("benchmarks/", "data/", "tests/fixtures/task_formats/", ".venv/", "venv/", "build/") -_NOQA = re.compile(r"#\s*noqa:\s*(?P[A-Z]+[0-9]+(?:[\s,]+[A-Z]+[0-9]+)*)(?P.*)$", re.I) +_NOQA = re.compile(r"#\s*(?:(?:ruff|flake8)\s*:\s*)?noqa:\s*(?P[A-Z]+[0-9]+(?:[\s,]+[A-Z]+[0-9]+)*)(?P.*)$", re.I) _REASON = re.compile(r"^\s*(?:[-โ€”:(]+\s*)?(?P.*?)\)?\s*$") @@ -84,3 +84,10 @@ def test_upper_case_and_doubled_dashes_are_still_checked() -> None: def test_a_code_after_a_space_separated_one_is_still_checked() -> None: assert unexplained("try:\n x()\nexcept: # noqa: E722 BLE001 S110\n pass\n") == [3] assert unexplained("try:\n x()\nexcept: # noqa: E722 BLE001 S110 - logged by the caller\n pass\n") == [] + + +def test_a_file_level_noqa_needs_a_reason_too() -> None: + src = "try:\n x()\nexcept Exception:\n pass\n" + assert unexplained("# ruff: noqa: BLE001, S110\n" + src) == [1] + assert unexplained("# flake8: noqa: BLE001\n" + src) == [1] + assert unexplained("# ruff: noqa: BLE001 - every probe failure is logged upstream\n" + src) == [] From 77ce4500fed49c18891a6341af36cf875387a585 Mon Sep 17 00:00:00 2001 From: Nikolay Petrov Date: Sat, 14 Nov 2026 11:24:58 -0800 Subject: [PATCH 05/13] Address review: pin the RecursionError clause of the direct JSON parse A reply with no prose before its nesting reaches the direct json.loads parse. Dropping RecursionError from that clause left all 58 fuzz cases green. Thread: PRRT_kwDOVBg7g863UN0k Signed-off-by: Nikolay Petrov --- dokimi/tests/test_fuzz_targets.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/dokimi/tests/test_fuzz_targets.py b/dokimi/tests/test_fuzz_targets.py index e4637f5c8..059eb7a04 100644 --- a/dokimi/tests/test_fuzz_targets.py +++ b/dokimi/tests/test_fuzz_targets.py @@ -74,6 +74,15 @@ def test_deep_nesting_is_a_parse_error_not_a_crash() -> None: assert isinstance(got, kind) +@pytest.mark.parametrize("extract_name", ["extract_json_object", "extract_json_array"]) +def test_a_bare_deep_reply_is_a_parse_error_not_a_crash(extract_name: str) -> None: + # No prose before the nesting: the direct json.loads parse is the one that recurses. + from dokimi import llm_json + + with pytest.raises(ValueError): + getattr(llm_json, extract_name)("[" * 100000) + + def test_a_date_that_is_not_real_is_invalid_frontmatter() -> None: # PyYAML raises a plain ValueError for 2026-02-30: the lenient reader reads no metadata, # and the strict one raises the YAML error its docstring names. From c9452c72faa30a84399db4cb495ec4a03efe017d Mon Sep 17 00:00:00 2001 From: Nikolay Petrov Date: Sat, 14 Nov 2026 11:24:58 -0800 Subject: [PATCH 06/13] Address review: pin the three fallback log calls with tests Each new logger call (arms resolver, MCP tool listing, persona label fetch) gets a caplog test; deleting the call fails the test. Thread: PRRT_kwDOVBg7g863fUNI Signed-off-by: Nikolay Petrov --- dokimi/tests/test_arms_run_config.py | 15 +++++++++++++++ dokimi/tests/test_mcp_protocol.py | 13 +++++++++++++ dokimi/tests/test_personas_analyzer.py | 10 ++++++++++ 3 files changed, 38 insertions(+) diff --git a/dokimi/tests/test_arms_run_config.py b/dokimi/tests/test_arms_run_config.py index 6d4301ffb..e77ba735e 100644 --- a/dokimi/tests/test_arms_run_config.py +++ b/dokimi/tests/test_arms_run_config.py @@ -152,3 +152,18 @@ def test_check_matrix_names_the_first_cell_with_an_unsupported_harness(): with pytest.raises(ValueError, match="^matrix cell 'odd': matrix cells only support"): check_matrix(ArmsRunConfig(product="demo"), cells) + + +def test_an_unresolved_doc_library_id_is_logged_and_the_name_is_used(monkeypatch, caplog): + from dokimi.eval import arms_run + from dokimi.mcp import factory + from dokimi.treatments.arms import DocTreatment + + def down(*args, **kwargs): + raise RuntimeError("server down") + + monkeypatch.setattr(factory, "resolve_library_id_for_product", down) + with caplog.at_level("WARNING", logger="dokimi.eval.arms_run"): + lib_id = arms_run.resolve_doc_library_id([DocTreatment(object(), name="docs", top_k=3)], "react") + assert lib_id == "react" + assert [r.exc_info is not None for r in caplog.records if "not resolved" in r.getMessage()] == [True] diff --git a/dokimi/tests/test_mcp_protocol.py b/dokimi/tests/test_mcp_protocol.py index 8a3ff3566..eb11a4bdb 100644 --- a/dokimi/tests/test_mcp_protocol.py +++ b/dokimi/tests/test_mcp_protocol.py @@ -165,3 +165,16 @@ def test_http_transport_works_across_mcp_versions(monkeypatch, http_attrs, strea _fake_mcp(monkeypatch, http_attrs, streams) client = MCPProtocolClient(transport="http", url="http://x/mcp") assert client._list_tool_names() == ["get-library-docs"] + + +def test_a_failed_tool_list_is_logged_and_the_name_is_used(caplog): + client = MCPProtocolClient(transport="http", url="http://127.0.0.1:9") + + def down(): + raise RuntimeError("server down") + + client._list_tool_names = down + with caplog.at_level("ERROR", logger="dokimi.mcp.mcp_protocol"): + assert client.resolve_library_id("react") == "react" + assert [r.exc_info is not None for r in caplog.records + if "listing MCP tools failed" in r.getMessage()] == [True] diff --git a/dokimi/tests/test_personas_analyzer.py b/dokimi/tests/test_personas_analyzer.py index 5114d09d6..23fb3e9a5 100644 --- a/dokimi/tests/test_personas_analyzer.py +++ b/dokimi/tests/test_personas_analyzer.py @@ -309,3 +309,13 @@ def test_create_minimal_analysis(): assert analysis["issues_analysis"] == {"common_questions": [], "common_labels": [], "sample_issues": []} assert analysis["api_patterns"] == [] assert analysis["topics"] == [] + + +def test_a_failed_label_fetch_is_logged_and_skipped(caplog): + analyzer = PersonaAnalyzer() + repo = Mock() + repo.get_issues.side_effect = RuntimeError("rate limited") + with caplog.at_level("WARNING", logger="dokimi.personas.analyzer"): + analyzer._analyze_issues(repo) + assert [r.exc_info is not None for r in caplog.records + if "not fetched" in r.getMessage()] == [True] * 4 From 7874a574df75c45e70036fa174bb15a1f175cf5a Mon Sep 17 00:00:00 2001 From: Nikolay Petrov Date: Sat, 14 Nov 2026 11:24:58 -0800 Subject: [PATCH 07/13] Address review: keep the coverage pragma; gate scans git-tracked files The reason on the reranker handler swallowed "pragma: no cover", so the line left coverage's exclusion. The pragma now comes first, the noqa reason after. The noqa gate lists the files git tracks or does not ignore, as ruff does, so mutants/, .tox/ and dist/ copies no longer fail it. Thread: PRRT_kwDOVBg7g863fUND Thread: PRRT_kwDOVBg7g863S32b Thread: PRRT_kwDOVBg7g863fUNL Thread: IC_kwDOVBg7g88AAAABmcksyQ Signed-off-by: Nikolay Petrov --- dokimi/dokimi/treatments/arms.py | 2 +- dokimi/tests/test_noqa_reasons.py | 37 +++++++++++++++++++++++++------ 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/dokimi/dokimi/treatments/arms.py b/dokimi/dokimi/treatments/arms.py index 8fd3a00be..f6e5a7d7f 100644 --- a/dokimi/dokimi/treatments/arms.py +++ b/dokimi/dokimi/treatments/arms.py @@ -67,7 +67,7 @@ def _default_reranker(threshold: float): if SENTENCE_TRANSFORMERS_AVAILABLE: try: return SentenceTransformerReranker(threshold=threshold) - except Exception as exc: # noqa: BLE001 - pragma: no cover - env dependent + except Exception as exc: # pragma: no cover # noqa: BLE001 - env dependent logger.warning("Semantic reranker unavailable (%s); using lexical", exc) return SimpleReranker(threshold=threshold) diff --git a/dokimi/tests/test_noqa_reasons.py b/dokimi/tests/test_noqa_reasons.py index 9403383b1..a19974c57 100644 --- a/dokimi/tests/test_noqa_reasons.py +++ b/dokimi/tests/test_noqa_reasons.py @@ -15,13 +15,14 @@ import io import re +import subprocess import tokenize from pathlib import Path ROOT = Path(__file__).resolve().parents[1] CODES = {"BLE001", "S110", "S112"} # ruff's extend-exclude in pyproject.toml, plus the virtualenvs a checkout may hold. -SKIP = ("benchmarks/", "data/", "tests/fixtures/task_formats/", ".venv/", "venv/", "build/") +SKIP = ("benchmarks/", "data/", "tests/fixtures/task_formats/", ".venv/", "venv/", "build/", ".tox/", "dist/") _NOQA = re.compile(r"#\s*(?:(?:ruff|flake8)\s*:\s*)?noqa:\s*(?P[A-Z]+[0-9]+(?:[\s,]+[A-Z]+[0-9]+)*)(?P.*)$", re.I) _REASON = re.compile(r"^\s*(?:[-โ€”:(]+\s*)?(?P.*?)\)?\s*$") @@ -42,12 +43,22 @@ def unexplained(source: str) -> list[int]: def _python_files() -> list[Path]: - out = [] - for path in sorted(ROOT.rglob("*.py")): - rel = path.relative_to(ROOT).as_posix() - if not rel.startswith(SKIP): - out.append(path) - return out + # The files ruff checks: what git does not hide (.gitignore, so mutants/ is out too). + listed = subprocess.run( + ["git", "ls-files", "--cached", "--others", "--exclude-standard", "-z", "--", "*.py"], + cwd=ROOT, capture_output=True, text=True, check=True).stdout + return [ROOT / rel for rel in sorted(listed.split("\0")) + if rel and not rel.startswith(SKIP)] + + +def test_python_files_follow_gitignore_and_skip_tox(tmp_path: Path, monkeypatch) -> None: + subprocess.run(["git", "init", "-q", str(tmp_path)], check=True) + (tmp_path / ".gitignore").write_text("mutants/\n", encoding="utf-8") + for rel in ("a.py", "mutants/scratch.py", ".tox/py311/x.py", "pkg/b.py"): + (tmp_path / rel).parent.mkdir(parents=True, exist_ok=True) + (tmp_path / rel).write_text("x = 1\n", encoding="utf-8") + monkeypatch.setitem(globals(), "ROOT", tmp_path) + assert [p.relative_to(tmp_path).as_posix() for p in _python_files()] == ["a.py", "pkg/b.py"] def test_every_blind_except_noqa_has_a_reason() -> None: @@ -91,3 +102,15 @@ def test_a_file_level_noqa_needs_a_reason_too() -> None: assert unexplained("# ruff: noqa: BLE001, S110\n" + src) == [1] assert unexplained("# flake8: noqa: BLE001\n" + src) == [1] assert unexplained("# ruff: noqa: BLE001 - every probe failure is logged upstream\n" + src) == [] + + +# coverage.py's default exclude pattern: the pragma must start with a `#` of its own. +_COVERAGE_PRAGMA = re.compile(r"#\s*(pragma|PRAGMA)[:\s]?\s*(no|NO)\s*(cover|COVER)") + + +def test_a_reason_does_not_hide_a_coverage_pragma() -> None: + bad = [f"{p.relative_to(ROOT).as_posix()}:{n}" + for p in _python_files() if p.resolve() != Path(__file__).resolve() + for n, line in enumerate(p.read_text(encoding="utf-8").splitlines(), 1) + if "noqa" in line and "pragma: no cover" in line and not _COVERAGE_PRAGMA.search(line)] + assert not bad, "a noqa reason swallowed a coverage pragma: " + ", ".join(bad) From c2cc374d5ad4b285e6fa045899ca3ade0c991d92 Mon Sep 17 00:00:00 2001 From: Nikolay Petrov Date: Sat, 14 Nov 2026 11:24:58 -0800 Subject: [PATCH 08/13] Address review: drop the stale BLE001 note; BLE001 out of lint.external The deferred-rules comment still listed BLE001 as not enabled, and lint.external hid stale BLE001 noqa comments from RUF100. Thread: PRR_kwDOVBg7g88AAAABX5yHKQ#1 Thread: IC_kwDOVBg7g88AAAABmcksyQ Signed-off-by: Nikolay Petrov --- dokimi/pyproject.toml | 30 +++--------------------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/dokimi/pyproject.toml b/dokimi/pyproject.toml index 72e33f82d..f460bd45e 100644 --- a/dokimi/pyproject.toml +++ b/dokimi/pyproject.toml @@ -173,32 +173,8 @@ extend-exclude = ["benchmarks", "data", "tests/fixtures/task_formats"] # The built-in review graders run on the task image's python3 too, 3.8 or newer (contract ยง4.5). "dokimi/repos/graders/*.py" = "py38" -# A correctness gate: rules that flag code that misbehaves (a swallowed -# KeyboardInterrupt, a naive datetime, a file handle never closed, an assert -# that never runs), not style. A family is taken whole where its rules are of -# that kind and its few style rules are cheap or ignored below; elsewhere the -# rules are listed one by one, so a ruff upgrade cannot add style rules to it. -# A whole family does gain rules with ruff releases (0.16.0 stabilized LOG004 -# and PLE0304, preview-only in 0.12), so ruff is pinned to one version here, in -# requirements-ci.txt (the tools lock CI installs) and in .pre-commit-config.yaml, -# and Renovate bumps all three in one PR that runs the new rules. -# -# Not enabled yet, with their violation counts at ruff 0.17.0. These three are -# autofixes across most files: they land in a PR of their own while few PRs -# are open, and per-file-target-version keeps UP from rewriting the py310 -# files above for 3.11: -# UP (pyupgrade) 1399, of which PEP 585/604 annotations 1185, imports 162 -# I (isort) 156 -# Hand fixes deferred because an open PR rewrites the same lines (#29, one -# process runner for the spawn sites): -# PLW1509 (preexec_fn with threads) 1, PLW1510 (subprocess.run without -# check=) 15 -# Bug-prone, each site needs a look: -# PLW2901 (loop variable reassigned) 20, RUF043 (pytest.raises match= with -# unescaped regex characters) 24, ISC004 (implicit concatenation in a -# collection, i.e. a missing comma) 67, BLE001 (blind except) 64, -# PT011 (pytest.raises too broad) 53, RUF012 (mutable class attribute) 8: -# constants and test stubs, but Tool.parameters is also set per instance +# A correctness gate, not style: ruff is pinned in requirements-ci.txt and .pre-commit-config.yaml. +# Not enabled (counts at ruff 0.17.0): UP 1399, I 156, PLW1509 1, PLW1510 15, PLW2901 20, RUF043 24, ISC004 67, PT011 53, RUF012 8. [tool.ruff.lint] select = [ "F", # pyflakes @@ -229,7 +205,7 @@ select = [ "RUF068", "RUF071", "RUF100", "RUF101", "RUF102", "RUF103", "RUF104", "RUF200", ] # noqa codes of rules not selected here; RUF100 leaves them alone. -external = ["ARG001", "BLE001", "E402", "E731", "N802", "N818", "S310"] +external = ["ARG001", "E402", "E731", "N802", "N818", "S310"] ignore = [ "A002", # argument named like a builtin: keyword APIs mirror subprocess's input= "G004", # f-string in a logging call: style, 108 sites From cbf05389f3ea0c33f63b9eb8a46053540c5ead55 Mon Sep 17 00:00:00 2001 From: Nikolay Petrov Date: Sat, 14 Nov 2026 11:24:58 -0800 Subject: [PATCH 09/13] Address review: the noqa file-level forms are already gated Thread: PRRT_kwDOVBg7g863S2fa: already fixed in 391f5755b4 Thread: PRRT_kwDOVBg7g863S319: already fixed in 391f5755b4 Signed-off-by: Nikolay Petrov From ea5e48209b4246479a6e01a268f6202223ea3911 Mon Sep 17 00:00:00 2001 From: Nikolay Petrov Date: Sat, 14 Nov 2026 11:44:41 -0800 Subject: [PATCH 10/13] Address review: the gate skips tracked files deleted from the working tree git ls-files --cached lists a file staged and then deleted on disk, and reading it raised FileNotFoundError in a local run. Thread: PRRT_kwDOVBg7g863fUNL Thread: PRRT_kwDOVBg7g863S32b Signed-off-by: Nikolay Petrov --- dokimi/tests/test_noqa_reasons.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dokimi/tests/test_noqa_reasons.py b/dokimi/tests/test_noqa_reasons.py index a19974c57..38ff1f53e 100644 --- a/dokimi/tests/test_noqa_reasons.py +++ b/dokimi/tests/test_noqa_reasons.py @@ -48,7 +48,7 @@ def _python_files() -> list[Path]: ["git", "ls-files", "--cached", "--others", "--exclude-standard", "-z", "--", "*.py"], cwd=ROOT, capture_output=True, text=True, check=True).stdout return [ROOT / rel for rel in sorted(listed.split("\0")) - if rel and not rel.startswith(SKIP)] + if rel and not rel.startswith(SKIP) and (ROOT / rel).is_file()] def test_python_files_follow_gitignore_and_skip_tox(tmp_path: Path, monkeypatch) -> None: @@ -57,6 +57,9 @@ def test_python_files_follow_gitignore_and_skip_tox(tmp_path: Path, monkeypatch) for rel in ("a.py", "mutants/scratch.py", ".tox/py311/x.py", "pkg/b.py"): (tmp_path / rel).parent.mkdir(parents=True, exist_ok=True) (tmp_path / rel).write_text("x = 1\n", encoding="utf-8") + (tmp_path / "gone.py").write_text("x = 1\n", encoding="utf-8") + subprocess.run(["git", "-C", str(tmp_path), "add", "gone.py"], check=True) + (tmp_path / "gone.py").unlink() # staged, then deleted in the working tree monkeypatch.setitem(globals(), "ROOT", tmp_path) assert [p.relative_to(tmp_path).as_posix() for p in _python_files()] == ["a.py", "pkg/b.py"] From 2a6b12211154303d20fff31f419b87e2f9ad1901 Mon Sep 17 00:00:00 2001 From: Nikolay Petrov Date: Sat, 14 Nov 2026 12:18:51 -0800 Subject: [PATCH 11/13] Address review: a coverage pragma is not a noqa reason Thread: PRRT_kwDOVBg7g863iWL2 Signed-off-by: Nikolay Petrov --- dokimi/tests/test_noqa_reasons.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/dokimi/tests/test_noqa_reasons.py b/dokimi/tests/test_noqa_reasons.py index 38ff1f53e..fa54bd4a1 100644 --- a/dokimi/tests/test_noqa_reasons.py +++ b/dokimi/tests/test_noqa_reasons.py @@ -25,6 +25,8 @@ SKIP = ("benchmarks/", "data/", "tests/fixtures/task_formats/", ".venv/", "venv/", "build/", ".tox/", "dist/") _NOQA = re.compile(r"#\s*(?:(?:ruff|flake8)\s*:\s*)?noqa:\s*(?P[A-Z]+[0-9]+(?:[\s,]+[A-Z]+[0-9]+)*)(?P.*)$", re.I) _REASON = re.compile(r"^\s*(?:[-โ€”:(]+\s*)?(?P.*?)\)?\s*$") +# coverage.py's default exclude pattern: the pragma must start with a `#` of its own. +_COVERAGE_PRAGMA = re.compile(r"#\s*(pragma|PRAGMA)[:\s]?\s*(no|NO)\s*(cover|COVER)") def unexplained(source: str) -> list[int]: @@ -36,7 +38,7 @@ def unexplained(source: str) -> list[int]: m = _NOQA.search(tok.string) if not m or not CODES & set(re.split(r"[\s,]+", m.group("codes"))): continue - reason = _REASON.match(m.group("rest")) + reason = _REASON.match(_COVERAGE_PRAGMA.sub("", m.group("rest"))) if not reason or len(reason.group("text").split()) < 2: lines.append(tok.start[0]) return lines @@ -100,6 +102,12 @@ def test_a_code_after_a_space_separated_one_is_still_checked() -> None: assert unexplained("try:\n x()\nexcept: # noqa: E722 BLE001 S110 - logged by the caller\n pass\n") == [] +def test_a_coverage_pragma_is_not_a_reason() -> None: + src = "try:\n x()\nexcept Exception: # noqa: BLE001, S110 # pragma: no cover\n pass\n" + assert unexplained(src) == [3] + assert unexplained(src.replace("# pragma: no cover", "# pragma: no cover - logged upstream")) == [] + + def test_a_file_level_noqa_needs_a_reason_too() -> None: src = "try:\n x()\nexcept Exception:\n pass\n" assert unexplained("# ruff: noqa: BLE001, S110\n" + src) == [1] @@ -107,10 +115,6 @@ def test_a_file_level_noqa_needs_a_reason_too() -> None: assert unexplained("# ruff: noqa: BLE001 - every probe failure is logged upstream\n" + src) == [] -# coverage.py's default exclude pattern: the pragma must start with a `#` of its own. -_COVERAGE_PRAGMA = re.compile(r"#\s*(pragma|PRAGMA)[:\s]?\s*(no|NO)\s*(cover|COVER)") - - def test_a_reason_does_not_hide_a_coverage_pragma() -> None: bad = [f"{p.relative_to(ROOT).as_posix()}:{n}" for p in _python_files() if p.resolve() != Path(__file__).resolve() From dbcb55a489d2b91a76696de6ede3b735f7cbeb15 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Sat, 14 Nov 2026 20:37:36 +0000 Subject: [PATCH 12/13] Pin the markdown-fence deep-nesting clauses in a test Signed-off-by: Anton Volkov --- dokimi/tests/test_fuzz_targets.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dokimi/tests/test_fuzz_targets.py b/dokimi/tests/test_fuzz_targets.py index 059eb7a04..a1e1e3626 100644 --- a/dokimi/tests/test_fuzz_targets.py +++ b/dokimi/tests/test_fuzz_targets.py @@ -74,13 +74,14 @@ def test_deep_nesting_is_a_parse_error_not_a_crash() -> None: assert isinstance(got, kind) +@pytest.mark.parametrize("text", ["[" * 100000, "```json\n" + "[" * 100000 + "\n```"]) @pytest.mark.parametrize("extract_name", ["extract_json_object", "extract_json_array"]) -def test_a_bare_deep_reply_is_a_parse_error_not_a_crash(extract_name: str) -> None: - # No prose before the nesting: the direct json.loads parse is the one that recurses. +def test_a_bare_deep_reply_is_a_parse_error_not_a_crash(extract_name: str, text: str) -> None: + # No prose before the nesting, bare or fenced: the direct and fence parses are the ones that recurse. from dokimi import llm_json with pytest.raises(ValueError): - getattr(llm_json, extract_name)("[" * 100000) + getattr(llm_json, extract_name)(text) def test_a_date_that_is_not_real_is_invalid_frontmatter() -> None: From 48dfc28b6ec0ae48636635e51196c43e0b46c2ca Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Sat, 14 Nov 2026 20:37:53 +0000 Subject: [PATCH 13/13] Say in the noqa reasons that the skip is logged at debug Signed-off-by: Anton Volkov --- dokimi/dokimi/mcp/local_markdown.py | 2 +- dokimi/dokimi/mcp/okf_client.py | 2 +- dokimi/dokimi/mcp/url_client.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dokimi/dokimi/mcp/local_markdown.py b/dokimi/dokimi/mcp/local_markdown.py index d4c9ff271..3a57882c0 100644 --- a/dokimi/dokimi/mcp/local_markdown.py +++ b/dokimi/dokimi/mcp/local_markdown.py @@ -88,7 +88,7 @@ def get_library_docs( for fp in files: try: content = self._read_file(fp, char_limit) - except Exception as exc: # noqa: BLE001 - logged; one unreadable file is skipped + except Exception as exc: # noqa: BLE001 - logged at debug; one unreadable file is skipped logger.debug(f"Skipping {fp}: {exc}") continue diff --git a/dokimi/dokimi/mcp/okf_client.py b/dokimi/dokimi/mcp/okf_client.py index 97742ee01..8cce37fb8 100644 --- a/dokimi/dokimi/mcp/okf_client.py +++ b/dokimi/dokimi/mcp/okf_client.py @@ -143,7 +143,7 @@ def get_library_docs( for fp in files: try: raw = fp.read_text(encoding=self.encoding) - except Exception as exc: # noqa: BLE001 - logged; one unreadable file is skipped + except Exception as exc: # noqa: BLE001 - logged at debug; one unreadable file is skipped logger.debug("OKFClient: skipping %s: %s", fp, exc) continue diff --git a/dokimi/dokimi/mcp/url_client.py b/dokimi/dokimi/mcp/url_client.py index c1e7e1992..a26e6e4d7 100644 --- a/dokimi/dokimi/mcp/url_client.py +++ b/dokimi/dokimi/mcp/url_client.py @@ -125,7 +125,7 @@ def check_connection(self) -> bool: try: resp = httpx.head(self.url, timeout=self.timeout, follow_redirects=True) return 200 <= resp.status_code < 300 - except Exception as exc: # noqa: BLE001 - logged; a connection check answers False + except Exception as exc: # noqa: BLE001 - logged at debug; a connection check answers False logger.debug(f"URLClient.check_connection failed: {exc}") return False