Cst rewrite
cst_rewrite
libcst write helpers + the import-index cache.
Everything that mutates Python source code lives here. Split into five concerns:
- class flatten (
_flatten_class_to_top_level,_flatten_class_child) - rename (
rename_name_in_module,rename_top_level_in_source) - delete (
delete_function_from_source,delete_source_if_empty_tests) - statement reorder (
reorder_module_statements+ ast helpers) Path(__file__).parents[N]depth patch (patch_file_dunder_depth)- import management — read-side analysis lives in
tests_ast; here we insert, dedupe, backfill, and synthesise missing imports, plus the project-wide import index that keeps backfill O(1) per lookup.
Hybrid I/O: we read with ast (cheap, well-tested for analysis) and
write with libcst (source-fidelity: comments, triple-quoted strings,
blank lines, quote style all preserved — what ast.unparse silently
loses).
backfill_import(module, mapping)
Insert from {mod} import {name} for each (name → mod) in mapping.
The new imports go at the canonical position — after every
from __future__ import … (or at the module top when none exists)
and before the first non-import statement. Names already imported in
module are skipped, so this is idempotent on already-correct sources.
Source code in packages/axm-audit/src/axm_audit/core/fix/cst_rewrite.py
backfill_missing_imports(source, target, project_path=None)
Copy imports from source into target for names target uses but doesn't define.
Falls back to scanning all test files under project_path if the
immediate source doesn't have the import — covers cases where the
original import was lost by an earlier move.
Hybrid: analyse with ast (cheap, well-tested), write with libcst so triple-quoted strings, blank lines, and comments in the target file are preserved byte-for-byte.
Source code in packages/axm-audit/src/axm_audit/core/fix/cst_rewrite.py
dedupe_imports(module)
delete_function(module, func_name)
Drop top-level function func_name from module.
Neighbouring statements (and their attached blank-line spacing) are
preserved by libcst's leading-lines semantics. In-memory counterpart
of :func:delete_function_from_source.
Source code in packages/axm-audit/src/axm_audit/core/fix/cst_rewrite.py
delete_function_from_source(source, func_name)
Remove a top-level FunctionDef from source, preserving formatting.
Source code in packages/axm-audit/src/axm_audit/core/fix/cst_rewrite.py
delete_source_if_empty_tests(source)
git rm the source if no test_* funcs/classes remain.
Source code in packages/axm-audit/src/axm_audit/core/fix/cst_rewrite.py
flatten_class(module, class_name)
Flatten the class_name class into top-level functions.
In-memory variant of :func:_flatten_class_to_top_level. Class-level
pytest marks are propagated onto each promoted method; method-level
decorators are preserved verbatim; the class docstring is dropped.
Source code in packages/axm-audit/src/axm_audit/core/fix/cst_rewrite.py
invalidate_import_index(project_path)
Drop the cached import index for project_path.
patch_file_depth(module, depth_delta=0)
Rewrite Path(__file__).parents[N] literals by depth_delta.
In-memory variant of :func:patch_file_dunder_depth that targets the
subscript form only — the chained .parent.parent form is left for
the file-level helper. Identity transform when depth_delta is 0 or
the pattern is absent.
Source code in packages/axm-audit/src/axm_audit/core/fix/cst_rewrite.py
patch_file_dunder_depth(file, depth_delta)
Rewrite Path(__file__).parents[N] / .parent.parent... after a move.
When a file is relocated by depth_delta directory levels
(depth_delta = target_depth - source_depth; negative if moved
closer to project root, positive if moved deeper), any constant of
the form Path(__file__).parents[N] or
Path(__file__).parent.parent... will resolve to a different
ancestor unless N is adjusted. We compute the new N so the
constant continues to resolve to the same directory it did before
the move:
N_new = N_old + depth_delta
Reasoning: a file at depth D has parents[N] at depth
D - N - 1 from project root. Moving the file to depth D'
means parents[N'] is at D' - N' - 1. For these to be equal,
N' = N + (D' - D) = N + depth_delta.
Two surface forms supported (in order of preference, since some files mix them):
- Subscript:
Path(__file__).parents[N](with optional.resolve()). N is decremented bydepth_delta. - Chained:
Path(__file__).parent.parent[.parent]*(with optional.resolve()). The number of.parentaccessors is reduced bydepth_delta.
If the resulting N would be <= 0, we leave the constant alone
and emit a warning — the file was moved too close to root for the
resolution to be expressible, indicating the relocate is suspect.
Source code in packages/axm-audit/src/axm_audit/core/fix/cst_rewrite.py
rename_function(module, old_name, new_name)
Rename top-level function old_name to new_name across module.
Updates the FunctionDef itself, any Name reference, and any
string-literal argument (e.g. pytest.mark.parametrize("old", …))
that matches old_name. In-memory counterpart of
:func:rename_name_in_module.
Source code in packages/axm-audit/src/axm_audit/core/fix/cst_rewrite.py
rename_name_in_module(path, old_to_new)
Rename every occurrence of name X across module path (def + refs).
Renames at three sites simultaneously
- the
cst.FunctionDef/cst.ClassDefdefinition itself, - every
cst.Namereference in the module body, - marker-argument string literals like
@pytest.mark.usefixtures("X")so usefixtures still resolves after the rename.
Preserves formatting via libcst. Unlike
rename_top_level_in_source (which only renames the def header
— needed for cross-file move collisions), this rewrites references
too — needed when source helpers get renamed to avoid colliding with
target's same-named helpers.
Source code in packages/axm-audit/src/axm_audit/core/fix/cst_rewrite.py
| Python | |
|---|---|
407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 | |
rename_top_level_in_source(source, old_to_new)
Rename top-level FunctionDef / ClassDef in source, preserving formatting.
Workaround for axm-anvil's rename= parameter, which validates
target absence under the ORIGINAL name before applying the rename —
so it cannot resolve cross-file collisions on its own. By renaming in
source first, we hand anvil a clean conflict-free move.
Source code in packages/axm-audit/src/axm_audit/core/fix/cst_rewrite.py
reorder_module_statements(path)
Reorder a module's top-level statements so definitions precede uses.
After SPLIT/MERGE/FLATTEN, axm-anvil can leave statements in an order
that breaks Python's module-execution semantics:
* _skip_no_tools = pytest.mark.skipif(_tools_available()) before
def _tools_available() → NameError at import.
* @_skip_no_tools decorator on a class, before the assign that
defines _skip_no_tools → NameError.
Strategy: stable topological sort. Imports stay first (they have no intra-module deps). For the rest, each statement is placed after the last statement that defines a name it references at module-execution time. References inside function bodies do NOT count — they're deferred. Order is preserved within independent groups.
Implementation note: we parse twice — once with libcst (the source of truth for formatting; what we'll write back) and once with ast (for cheap defines/references analysis). The libcst statements are reordered by index, not rebuilt, so triple-quoted strings, comments, and blank-line spacing all survive intact.
Idempotent.
Source code in packages/axm-audit/src/axm_audit/core/fix/cst_rewrite.py
resolve_import_for_symbol(project_path, symbol)
Return the import statement that brings symbol into scope, or None.
Builds (and caches in _PROJECT_IMPORT_INDEX_CACHE) a project-wide
index of top-level FunctionDef / AsyncFunctionDef / ClassDef
definitions across every .py file under project_path. Drop the
cache via :func:invalidate_import_index after mutating the file
tree so the next call rebuilds.