Repository navigation
Repair what the ratchet counts with NRA plans; type imports by when they run - #3170
Conversation
…hey run tools/repairs.py: a Repair declares the located measure it removes and writes Nominal Refactor Advisor operations for each occurrence it can remove, tallying every one it keeps under the reason it stays. ImportHoist repairs FunctionLocalImport: it moves a local import to its module's top unless a module-level import cycle runs through it, judged with every accepted move already added to the load graph. tools/module_layers.py: Import is a family whose members say whether they run at module load: LoadTimeImport, CallTimeImport, CheckerImport (under TYPE_CHECKING). Only a function body is call-time; decorators, defaults and annotations run where the def runs. Package.load_graph gives the edges that load their target. docs/layout/MODULE_LAYERS.md regenerated; it had drifted 13 units. NRA joins the dev extra as a direct git reference. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
|
debt-tools at 0f54df8, every finding: |
|
debt-src at 6951c22, every finding: |
…wner The debt-src and debt-tools growth on this branch, at its owners: - ImportHoist.stays was a seven-arm if-chain returning sentences that left counted by string (StringSubscript at repairs.py:130). The reasons are now a HoistVerdict family in declaration order: each KeptImport member owns its holds() and why, Moves is the last member, and left is Counter[type[Kept]]. Its locals become HoistedFile and LocalImport. - target(path, qualname=None) named two targets with None as the switch: file_target and function_target. - rebinds' match over node types (TypeSwitch, three arms) is a Rebinding NodeVisitor. - module_layers wrapped a def's statements in ast.Expression to iterate them (a type error); visit_scope takes each scope's own body. Its TYPE_CHECKING test matched the spelling (StringIdentity +2) and any local name spelled so; it asks TypingReference.is_type_checking. - TypingImports and TypingReference move from debt_ratchet to typing_names, so module_layers stays at 0.04 s to import instead of debt_ratchet's 0.95 s. Their Python 3.11 type_param guard is deleted: the package requires 3.14. The full src simulation and its tallies are byte-identical before and after; the generated MODULE_LAYERS.md is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q8rVa3cPyXzpCKyR8XvM8W
Moving TypingImports into typing_names brought three sites into a new file the ratchet counts from zero: None meaning "no typing declaration" from dict.get, a getattr of "__module__" by literal name, and is_any's typing.Any. Untyped is now the declared null value, raw_access_count asks for it by identity, and AnyReference holds is_any in debt_ratchet next to the measure that asks it. The unused typing_extensions import goes. StringSubscript and AnyName lines are identical on every tracked file except the moved Any site; MODULE_LAYERS.md is unchanged. uv.lock takes NRA 3fc2f084, which ships py.typed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q8rVa3cPyXzpCKyR8XvM8W
# Conflicts: # tools/debt_ratchet.py
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q8rVa3cPyXzpCKyR8XvM8W
# Conflicts: # tools/debt_ratchet.py
repairs.py imports debt_ratchet by its tools-directory name, as every tool imports its siblings; the two tests spelling tools.debt_ratchet loaded the same file as a second module, and the second registration of each measure family member failed collection (DuplicateRegistryKeyError on ReportExclude). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q8rVa3cPyXzpCKyR8XvM8W
|
Sent back at 4b5e52e (commander review, recorded in Core). debt-tools is red on real slop:
Also:
What's good: ImportGraph replaces the copied reachability loop, the visitor counts def-time loads (decorators and defaults), and ImportHoist checks each move against the load graph with every other accepted move in it. 🤖 Generated with Claude Code |
…d's identity dc242bb traded main's getattr(value, "__module__") probe for `self.value is Untyped`: a None check in a class costume, counted by OwnTypeTest as a test of our own class (debt-tools: grew OwnTypeTest:tools/typing_names.py +1). at_any_depth held the same probe as `is not Untyped`. Both now ask TypingImports.declares, derived from the typing_modules the class already declares; nothing tests Untyped any more. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q8rVa3cPyXzpCKyR8XvM8W
The ratchet says which line got worse; this makes it also say how to fix it, as a Nominal Refactor Advisor plan you can preview or apply.
What it adds
tools/repairs.pyRepair(aDeclaredFamily) declares the located measure it removes (repairs = FunctionLocalImport).--applygoes through NRA's source-revision guards, then runs ruff's import sorter on the touched files. Black runs only on files Black already accepted.ImportHoistmoves a function-local import to its module's top. It refuses when a module-level import cycle runs through the import. It judges with every accepted move already in the load graph, because two local imports that name each other each pass alone and deadlock together. Other reasons it keeps an import: it is third-party, the module binds the name another way, the function rebinds the name, or it is not one plain line.tools/module_layers.py:Importis now a family whose members say whether they run at module load:LoadTimeImportCallTimeImportCheckerImport, for imports underif TYPE_CHECKING:This also fixes a bug. The old visitor counted a function's decorators and default arguments as call-time, but those run when the
defruns. Only the body is call-time now.Package.load_graph()gives the edges that load their target.Package.unreacheduses the sameImportGraph.tools/typing_names.py:TypingImportsandTypingReferencemove out ofdebt_ratchet, somodule_layersresolvesif TYPE_CHECKING:by what the name is bound to, not by its spelling, without importing the ratchet (0.04 s vs 0.95 s). Moving them made the ratchet count their debt from zero, so it is removed instead of carried:Untypedis the declared value for "bound to no typing declaration", replacing theNonethatdict.getreturned.raw_access_countasksself.value is Untypedinstead ofgetattr(value, "__module__", None).is_anylives onAnyReferenceindebt_ratchet, beside AnyName, the one measure that asks.docs/layout/MODULE_LAYERS.md: regenerated on current main. The committed copy had drifted 13 units, because--check(debt-ratchet.yml:64) checks client imports and never compares the doc with the generator. The doc depends on the stack clients' branch heads, so a freshness gate would fail PRs whenever Toad pushes. The honest options are to regenerate it, or to stop committing a generated copy. This PR regenerates it; deleting it is a separate call.Dependency: NRA joins the dev extra as a direct git reference. CI installs with pip, which ignores uv sources, and a bare name would resolve on PyPI to whoever registers it.
Evidence
tests/test_repairs.py: on a fixture package, the hoist moves the leaf import, keeps the import whose cycle runs through a module-level import, and the applied tree imports cleanly.tests/test_module_layers_loads.py: covers module-level, decorator, default, TYPE_CHECKING, body and lambda-body imports. It fails on the old visitor (decorator and default came out call-time).tests/test_module_layers_clients.pypasses.module_layers.py --check: 0 missing modules, 0 missing names. Generator output is unchanged by the Import family (diffed against main's generator).ImportHoist --allsimulates 81 files changing. It leaves 160 imports for cycles, 42 that bind a name another way, 8 third-party, 4 that clash with another moved import, and 3 that are not one plain line. Move 225 function-local imports that nothing cycles back through to their module tops #3166 is that hoist applied.NodeVisitoralias carries a reasonednoqa, following the repo's idiom: NodeVisitor dispatches on those names.NRA:
uv.locktakes 3fc2f084, which shipspy.typed(OpenHCSDev/NominalRefactorAdvisor#21), so mypy checksrepairs.pyagainst NRA's types. The placement bug found on the way, OpenHCSDev/NominalRefactorAdvisor#20 (ensure_importstops atif TYPE_CHECKING:), is fixed in OpenHCSDev/NominalRefactorAdvisor#22. Until that lands, the ruff I001 step after apply repairs the order.🤖 Generated with Claude Code
https://claude.ai/code/session_01Q8rVa3cPyXzpCKyR8XvM8W