scripts.scan_imperative_accumulators
Reference › scripts › scan_imperative_accumulators
scripts/scan_imperative_accumulators.py inventories every IMPERATIVE_ACCUMULATOR site in the tree: loops that only accumulate into a variable. A human runs it to measure how much code a style rule affects; tests/scripts/test_scan_imperative_accumulators.py covers it. It does not duplicate the rule: _style_checker loads the live StyleChecker class from its hyphenated module path, and sites returns the line, function and variable of each accumulate-only loop in a parsed tree.
Arguments: one or more files or directories (roots) and --json. Directories are walked for *.py, and files that do not parse are skipped. The default output is a per-file count, largest first, followed by a total of sites across files. With --json it prints the findings as a list of objects with file, line, function and variable. It reads only and always exits 0.
At a glance
| Source | scripts/scan_imperative_accumulators.py |
| Lines of code | 107 |
| Classes | 0 |
| Functions | 3 |
| Imports (internal) | none |
| Imported by | none |
| Tests that import it | tests/scripts/test_scan_imperative_accumulators.py |
Class diagram
No classes; see Functions.
Dependency diagram
No internal imports and no importers.
API
Functions
| Function | Signature | Summary |
|---|---|---|
_style_checker |
_style_checker() -> Any |
The live StyleChecker class, loaded from the hyphenated module path. |
sites |
sites(checker: Any, tree: ast.Module) -> list[tuple[int, str, str]] |
(line, function, variable) for every accumulate-only loop in tree. |
main |
main(argv: list[str] | None = None) -> int |
Source
#!/usr/bin/env python3
"""Inventory every IMPERATIVE_ACCUMULATOR site in the tree.
The rule itself lives in ``scripts/code-review.py`` (StyleChecker); running the
full reviewer over the whole tree also runs ruff and mypy per file, which is
minutes of work when all that is wanted is the accumulator census. This reuses
the SAME atoms and facts so the two can never disagree -- it imports the rule's
own helpers rather than restating the pattern.
Usage:
python scripts/scan_imperative_accumulators.py src scripts
python scripts/scan_imperative_accumulators.py --json src
"""
from __future__ import annotations
import argparse
import ast
import importlib.util
import json
import sys
from collections import Counter
from pathlib import Path
from typing import Any
_REVIEWER = Path(__file__).with_name("code-review.py")
def _style_checker() -> Any:
"""The live StyleChecker class, loaded from the hyphenated module path."""
spec = importlib.util.spec_from_file_location("code_review_tool", _REVIEWER)
if spec is None or spec.loader is None:
raise SystemExit(f"cannot load {_REVIEWER}")
module = importlib.util.module_from_spec(spec)
sys.modules["code_review_tool"] = module
spec.loader.exec_module(module)
return module.StyleChecker
def sites(checker: Any, tree: ast.Module) -> list[tuple[int, str, str]]:
"""(line, function, variable) for every accumulate-only loop in ``tree``."""
scopes = [
n
for n in ast.walk(tree)
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
]
found: list[tuple[int, str, str]] = []
for scope in scopes:
inits = checker._accumulator_inits(scope)
if not inits:
continue
loops = [n for n in ast.walk(scope) if isinstance(n, (ast.For, ast.AsyncFor))]
for loop in loops:
if not loop.body or not all(
isinstance(stmt, ast.Expr)
and isinstance(stmt.value, ast.Call)
and isinstance(stmt.value.func, ast.Attribute)
and stmt.value.func.attr in checker._ACCUMULATOR_METHODS
and isinstance(stmt.value.func.value, ast.Name)
and stmt.value.func.value.id in inits
for stmt in loop.body
):
continue
found.extend(
(loop.lineno, scope.name, var)
for var in sorted(checker._loop_mutations(loop) & inits.keys())
)
return found
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("roots", nargs="+", help="directories or files to scan")
parser.add_argument("--json", action="store_true", help="machine-readable output")
args = parser.parse_args(argv)
checker = _style_checker()
files = [
f
for root in map(Path, args.roots)
for f in ([root] if root.is_file() else sorted(root.rglob("*.py")))
]
findings: list[dict[str, Any]] = []
for path in files:
try:
tree = ast.parse(path.read_text(encoding="utf-8"))
except SyntaxError, UnicodeDecodeError:
continue
findings.extend(
{"file": str(path), "line": line, "function": func, "var": var}
for line, func, var in sites(checker, tree)
)
if args.json:
print(json.dumps(findings, indent=2))
return 0
by_file = Counter(f["file"] for f in findings)
for path, count in by_file.most_common():
print(f"{count:4d} {path}")
print(f"\n{len(findings)} sites across {len(by_file)} files ({len(files)} scanned)")
return 0
if __name__ == "__main__":
sys.exit(main())