Python: search preconditions walk the LST with the generated visitor - #8748
Merged
jkschneider merged 1 commit intoSep 2, 2026
Merged
Conversation
knutwannheden
force-pushed
the
python-sdk-search-preconditions-walk-the-lst-by-reflection
branch
from
September 2, 2026 10:56
17b1d8f to
7d5e491
Compare
UsesType, UsesMethod and UsesImport reached every node through dir()/getattr over ~70 public attributes, at 36 us/node against the 6 us/node a PythonVisitor traversal costs. A file is gated once per sub-recipe, so a 25-recipe composite paid it 25 times.
knutwannheden
force-pushed
the
python-sdk-search-preconditions-walk-the-lst-by-reflection
branch
from
September 2, 2026 11:04
7d5e491 to
c33ff5d
Compare
jkschneider
deleted the
python-sdk-search-preconditions-walk-the-lst-by-reflection
branch
September 2, 2026 12:02
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three of the four Python search preconditions walked the LST by reflection —
dir(cur)plus agetattron each of ~70 public attributes, per node — where every other language uses its own visitor. Onpsf/requeststests/test_requests.py(3094 lines, 15044 LST nodes), per-node cost against aPythonVisitortraversal of the same tree:Those are the miss cases, which is what a gate mostly does. A hit is far cheaper still, because the visitor stops at the first matching node where the walk collected the whole tree first:
UsesImport("pytest")on the same file goes from 549 ms to 0.36 ms.A file is gated once per sub-recipe, so 25
ChangeImportsub-recipes over that one file — the shape oforg.openrewrite.python.migrate.ReplaceCollectionsAbcImports— go from 17.95s to 3.20s. None of that is the recipe's own work: the gate never matches, so the run is precondition time end to end._FindFirstis aPythonVisitorthat stops at the first node itsmatchesaccepts, and the three preconditions each supply one. It dispatches straight toaccept, since a search never rewrites and nothing reads the cursor the basevisitbuilds. Theseenset and thetry/exceptaroundgetattrgo with the walk;IsSourceFilenever used it.Do both traversals reach the same nodes
Reflection's rule — any attribute holding something with
idormarkers— is not the visitor's, so I compared the two node sets over 1499 stdlib files (1.38M visited nodes):Comprehension clauses, conditions and match-case patterns are reached through
visit_comprehension_clauseand friends rather than throughvisit, so their children are traversed either way, and none of the three is aTypedTree, aJ.MethodInvocationor an import node.J.ClassDeclaration.Kindis exposed only on.padding, which is why reflection missed it.Answers agreed on all 1881 (query, file) pairs checked: 33 queries × 37 files parsed without attribution, and 33 × 20 with ty-types attached.
UsesTypegets a sharper check, per FQN rather than per file, because a per-file comparison hides a node class whose type is also reachable from a sibling: every one of the 300 distinct fully-qualified names the old walk could match on across 14 ty-attributed files still matches. That check is why_TypeSearchreadsgetattr(tree, "type", None)—Expressiondeclares its owntypealongsideTypedTreerather than under it, so anisinstanceagainst either alone silently narrows the gate.The one behaviour change: depth
The visitor recurses ~5 Python frames per LST level against an interpreter limit of 2000, so it gives out around 350-400 levels of nesting where the iterative walk did not. That is lower than it sounds reachable — a 400-term
orchain does it, as doespydoc_data/topics.pyat 2295 levels.searchcatchesRecursionError, logs the path at WARNING and answers "no match".No file loses an edit to this, because every editor in the SDK is an equally recursive
PythonVisitorand gives out at the same depth:So the gate's answer changes only for a file the editor was going to fail on anyway, and only when the deep expression precedes the match. What changes is the diagnostic: an error naming a file no recipe could have changed becomes a WARNING naming a file that was skipped. Letting the
RecursionErrorpropagate instead would restore the error, on every sweep that touches such a file; the log line is the compromise, so the skip still leaves a trace.Tests
tests/recipes/test_search_traversal.pyparses one fixture that buries each match below a decorated function, a nested class and method, and then below an f-string comprehension, awith, or anexceptblock. It asserts the answer for a present and an absent type, method and import; replacing the descent withreturn treefails all three. A fourth test pins thatlist, attributed to the comprehension node itself, is found — it fails against anisinstance(tree, TypedTree)gate. A fifth builds a 3000-fragment concatenation — deterministic, not dependent on a stdlib version — and fails if either theRecursionErrorcatch or the warning goes.Full Python SDK suite: 2194 passed, 11 skipped, including the Java-peer RPC tests.