mirror of
https://github.com/kuhyx/WUT_Computer_Science.git
synced 2026-11-10 22:47:09 +01:00
PORR was the last course whose `run` did not work. Four measurements, not
guesses, explain why -- and none of them is the matrix size everyone assumes:
ProcessingType.PROCESSES 20.9s at n=2, 32.8s at n=5, 78.2s at n=10. That
is process-pool setup PER TEST, and it grows.
matrix_type poli3 densifies a sparse .mat into a 2.3 GB npz cache
(nemeth12: 723 MB) -- over the 2G ceiling before
any solving happens.
n=10000 800 MB matrix, and np.linalg.solve wants a copy.
n=5000 spd_5000.npz was a Git LFS pointer whose object
404s on GitHub, so it regenerates from scratch.
Remove those and what is left still exercises the whole Richardson
implementation at every size that fits: 18 tests, 28 seconds through the
harness, exit 0. The submitted parameter set is one documented environment
variable away. `main.py` now propagates pytest's status (verified: exit 5 when
nothing is collected) -- it discarded it before, so `run` could not tell a
passing suite from a failing one.
The rest of this commit is scripts/check_refactor_safety.py, which exists
because three regressions got past both ruff and a smoke run during this
campaign, and I would rather they could not get past the next one:
--signatures diffs two trees function by function, parameter by
parameter, default by default. This is what found the 22
EARIN functions whose defaults an annotation pass silently
dropped.
--logging a module with a __main__ guard that logs at INFO must call
basicConfig, or it prints nothing at all. Found two more
in PORR just now.
--print-keywords refuses to let an automated rewrite near print(x, end="")
or print(x, file=sys.stderr); losing those turns a
rendered board into one glyph per line, or silently moves
error output onto stdout.
All three pass on the whole archive. Also in here: 12 latent TypeErrors mypy
found in EARIN that ruff could not -- call sites still passing positionally to
parameters this campaign had made keyword-only, plus one caller of a function
whose argument I had removed. Every one would have raised at runtime.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0172dj34FDpao6wsix8ZuCzE
178 lines
6.7 KiB
Python
Executable File
178 lines
6.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Three checks that catch what ruff and a smoke run both miss.
|
|
|
|
Every one of these exists because it caught a real regression during the
|
|
`select = ["ALL"]` campaign on this archive:
|
|
|
|
1. **Signatures.** Annotating 110 EARIN functions silently dropped the DEFAULT
|
|
VALUES from 22 of them, so a call relying on a default would have raised
|
|
TypeError. Nothing flagged it: ruff does not look at defaults, and the
|
|
affected paths were not the ones a smoke run exercises.
|
|
2. **Logging.** Rewriting ``print()`` as ``logger.info()`` made programs
|
|
silent, because logging's last-resort handler only emits WARNING and above.
|
|
A module that logs and has a ``__main__`` guard must configure logging.
|
|
3. **print keywords.** ``print(x, end="")`` builds a line incrementally.
|
|
Dropping ``end=`` turned a rendered chess board into one glyph per line;
|
|
dropping ``file=sys.stderr`` would silently move error output to stdout.
|
|
|
|
Usage::
|
|
|
|
scripts/check_refactor_safety.py --logging --print-keywords <paths>
|
|
scripts/check_refactor_safety.py --signatures <before-tree> <after-tree>
|
|
|
|
Exit status is 1 if anything is reported.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import ast
|
|
import logging
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_MAIN_GUARD = "__name__ == '__main__'"
|
|
|
|
|
|
def _functions(source: str) -> dict[str, tuple[set[str], dict[str, str]]]:
|
|
"""Map each function name to its parameter names and their defaults."""
|
|
out: dict[str, tuple[set[str], dict[str, str]]] = {}
|
|
for node in ast.walk(ast.parse(source)):
|
|
if not isinstance(node, ast.FunctionDef):
|
|
continue
|
|
args = node.args
|
|
defaults: dict[str, str] = {}
|
|
# defaults cover posonlyargs and args together, tail-aligned.
|
|
ordered = args.posonlyargs + args.args
|
|
positional = ordered[len(ordered) - len(args.defaults) :]
|
|
for arg, value in zip(positional, args.defaults, strict=True):
|
|
defaults[arg.arg] = ast.unparse(value)
|
|
for arg, value in zip(args.kwonlyargs, args.kw_defaults, strict=True):
|
|
if value is not None:
|
|
defaults[arg.arg] = ast.unparse(value)
|
|
names = {a.arg for a in args.posonlyargs + args.args + args.kwonlyargs}
|
|
out[node.name] = (names, defaults)
|
|
return out
|
|
|
|
|
|
def check_signatures(before: Path, after: Path) -> int:
|
|
"""Report any parameter or default that changed between two trees."""
|
|
problems = 0
|
|
for old in sorted(before.rglob("*.py")):
|
|
new = after / old.relative_to(before)
|
|
if not new.exists():
|
|
continue
|
|
was = _functions(old.read_text(encoding="utf-8"))
|
|
now = _functions(new.read_text(encoding="utf-8"))
|
|
for name, (names, defaults) in was.items():
|
|
if name not in now:
|
|
logger.error("%s: %s disappeared", new, name)
|
|
problems += 1
|
|
continue
|
|
names_now, defaults_now = now[name]
|
|
missing = sorted(names - names_now)
|
|
if missing:
|
|
logger.error("%s: %s lost parameters %s", new, name, missing)
|
|
problems += 1
|
|
changed = {
|
|
key: (value, defaults_now.get(key))
|
|
for key, value in defaults.items()
|
|
if key in names_now and defaults_now.get(key) != value
|
|
}
|
|
if changed:
|
|
logger.error("%s: %s default(s) changed %s", new, name, changed)
|
|
problems += 1
|
|
return problems
|
|
|
|
|
|
def check_logging(paths: list[Path]) -> int:
|
|
"""Report modules that log but never configure logging."""
|
|
problems = 0
|
|
for path in paths:
|
|
source = path.read_text(encoding="utf-8", errors="ignore")
|
|
if "logger.info(" not in source and "logger.debug(" not in source:
|
|
continue
|
|
if "basicConfig" in source:
|
|
continue
|
|
tree = ast.parse(source)
|
|
runnable = any(
|
|
isinstance(node, ast.If) and ast.unparse(node.test) == _MAIN_GUARD
|
|
for node in tree.body
|
|
)
|
|
# Only a module that can be RUN is at fault: configuring logging is
|
|
# the entry point's job, not a library module's.
|
|
if runnable:
|
|
logger.error("%s: logs at INFO but never calls basicConfig", path)
|
|
problems += 1
|
|
return problems
|
|
|
|
|
|
def check_print_keywords(paths: list[Path]) -> int:
|
|
"""Report print() calls whose keywords an automated rewrite would lose."""
|
|
problems = 0
|
|
for path in paths:
|
|
tree = ast.parse(path.read_text(encoding="utf-8", errors="ignore"))
|
|
for node in ast.walk(tree):
|
|
if not (
|
|
isinstance(node, ast.Call)
|
|
and isinstance(node.func, ast.Name)
|
|
and node.func.id == "print"
|
|
and node.keywords
|
|
):
|
|
continue
|
|
keys = sorted(k.arg or "**" for k in node.keywords)
|
|
logger.error(
|
|
"%s:%d: print() with %s -- convert this one by hand",
|
|
path,
|
|
node.lineno,
|
|
", ".join(keys),
|
|
)
|
|
problems += 1
|
|
return problems
|
|
|
|
|
|
def _python_files(roots: list[str]) -> list[Path]:
|
|
"""Every .py under the given roots, skipping venvs and vendored trees."""
|
|
found: list[Path] = []
|
|
for root in roots:
|
|
base = Path(root)
|
|
candidates = [base] if base.is_file() else sorted(base.rglob("*.py"))
|
|
found.extend(
|
|
p for p in candidates if ".venv" not in p.parts and "sightpy" not in p.parts
|
|
)
|
|
return found
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
"""Run the requested checks and return 1 if any reported something."""
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("paths", nargs="+", help="files, directories, or two trees")
|
|
parser.add_argument("--signatures", action="store_true")
|
|
parser.add_argument("--logging", action="store_true")
|
|
parser.add_argument("--print-keywords", action="store_true")
|
|
args = parser.parse_args(argv)
|
|
logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout)
|
|
|
|
problems = 0
|
|
if args.signatures:
|
|
expected = 2
|
|
if len(args.paths) != expected:
|
|
logger.error("--signatures needs exactly two trees: before after")
|
|
return 1
|
|
problems += check_signatures(Path(args.paths[0]), Path(args.paths[1]))
|
|
if args.logging or args.print_keywords:
|
|
files = _python_files(args.paths)
|
|
if args.logging:
|
|
problems += check_logging(files)
|
|
if args.print_keywords:
|
|
problems += check_print_keywords(files)
|
|
|
|
logger.info("problems: %d", problems)
|
|
return 1 if problems else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|