-
Notifications
You must be signed in to change notification settings - Fork 3
/
walkers.py
211 lines (162 loc) · 7.84 KB
/
walkers.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# -*- coding: utf-8; -*-
"""AST walkers.
These have a state stack and a node collector, and accept also a `list` of statements.
Otherwise they work like `ast.NodeVisitor` and `ast.NodeTransformer`.
Basic usage summary::
def kittify(mytree):
class Kittifier(ASTTransformer):
def transform(self, tree):
if type(tree) is ast.Constant:
self.collect(tree.value)
tree.value = "meow!" if self.state.meows % 2 == 0 else "miaow!"
self.state.meows += 1
return self.generic_visit(tree) # recurse
w = Kittifier(meows=0) # set the initial state here
mytree = w.visit(mytree) # it's basically an ast.NodeTransformer
print(w.collected) # collected values, in the order visited
return mytree
def getmeows(mytree):
class MeowCollector(ASTVisitor):
def examine(self, tree):
if type(tree) is ast.Constant and tree.value in ("meow!", "miaow!"):
self.collect(tree)
self.generic_visit(tree)
w = MeowCollector()
w.visit(mytree)
print(w.collected)
return w.collected
For an example of `withstate`, see `mcpyrate.astfixers`.
"""
__all__ = ["ASTVisitor", "ASTTransformer"]
from abc import ABCMeta, abstractmethod
from ast import NodeVisitor, NodeTransformer, iter_child_nodes
from .bunch import Bunch
from . import utils
class BaseASTWalker:
"""AST walker base class, providing a state stack and a node collector."""
def __init__(self, **bindings):
"""Bindings are loaded into the initial `self.state` as attributes."""
self.reset(**bindings)
def reset(self, **bindings):
"""Clear everything. Load new bindings into a blank `self.state`."""
self._stack = [Bunch(**bindings)]
self._subtree_overrides = {}
self.collected = []
def _setstate(self, newstate):
self._stack[-1] = newstate
return newstate
def _getstate(self):
return self._stack[-1]
state = property(fget=_getstate, fset=_setstate, doc="The current state. Mutable. Can be rebound to replace it.")
def withstate(self, tree, **bindings):
"""Arrange to visit a subtree with a temporarily replaced, updated state.
`tree` can be an AST node or a statement suite (`list` of AST nodes).
It is identified by `id(tree)` at enter time. Bindings update a copy
of `self.state`.
If several `withstate` calls are made for the same `tree`, the last one
overrides.
Generally speaking:
`withstate(subtree, ...)` should be used if you then intend to
`visit(subtree)`, which recurses into that node (or suite) only.
`generic_withstate(tree, ...)` should be used if you then intend to
`generic_visit(tree)`, which recurses into the children of `tree`.
(It is possible to mix and match, but think through what you're doing.)
"""
newstate = self.state.copy()
newstate.update(**bindings)
# Due to how `ast.NodeTransformer.generic_visit` works, `visit` is
# never called for a statement suite, but only separately for each
# individual statement in it. So if we should set `newstate` for a
# suite, do it for each statement in it.
if isinstance(tree, list):
for elt in tree:
self._subtree_overrides[id(elt)] = newstate
else:
self._subtree_overrides[id(tree)] = newstate
def generic_withstate(self, tree, **bindings):
"""Like `withstate`, but set up the new state for all children of `tree`.
The same state instance is shared between the child nodes.
If several `generic_withstate` calls are made for the same `tree`, the
last one overrides (assuming the list of children has not changed in between).
The silly name is because this relates to `withstate` as `generic_visit`
relates to `visit`.
Generally speaking:
`generic_withstate(tree, ...)` should be used if you then intend to
`generic_visit(tree)`, which recurses into the children of `tree`.
`withstate(subtree, ...)` should be used if you then intend to
`visit(subtree)`, which recurses into that node (or suite) only.
(It is possible to mix and match, but think through what you're doing.)
"""
newstate = self.state.copy()
newstate.update(**bindings)
for node in iter_child_nodes(tree):
self._subtree_overrides[id(node)] = newstate
def collect(self, value):
"""Collect a value. The values are placed in the list `self.collected`."""
self.collected.append(value)
return value
class ASTVisitor(BaseASTWalker, NodeVisitor, metaclass=ABCMeta):
"""AST visitor, like `ast.NodeVisitor`, but with the features of `BaseASTWalker`.
(Because `return tree`, or `return self.generic_visit(tree)`, is too much
to remember when not editing.)
If you want to edit the tree, use `ASTTransformer` instead.
"""
def visit(self, tree):
"""Start visiting `tree`. **Do not override this method; see `examine` instead.**"""
newstate = self._subtree_overrides.pop(id(tree), False)
if newstate:
self._stack.append(newstate)
try:
if isinstance(tree, list):
for elt in tree:
self.visit(elt)
return
return self.examine(tree)
finally:
if newstate:
self._stack.pop()
@abstractmethod
def examine(self, tree):
"""Examine one node. **Abstract method, override this.**
There is only one `examine` method. To detect node type, use `type(tree)`.
This method must recurse explicitly where needed. Use:
- `self.generic_visit(tree)` to visit all children of `tree`.
- `self.visit(tree.something)` to selectively visit only some children.
Visiting a statement suite with `self.visit` is also ok; it is treated
like a `generic_visit`.
As in `ast.NodeVisitor`:
- Return value of `examine` is forwarded by `visit`.
- `generic_visit` always returns `None`.
"""
class ASTTransformer(BaseASTWalker, NodeTransformer, metaclass=ABCMeta):
"""AST transformer, like `ast.NodeTransformer`, but with the features of `BaseASTWalker`.
If you only want to examine the tree, not edit it, consider `ASTVisitor` instead.
"""
def visit(self, tree):
"""Start transforming `tree`. **Do not override this method; see `transform` instead.**"""
newstate = self._subtree_overrides.pop(id(tree), False)
if newstate:
self._stack.append(newstate)
try:
if isinstance(tree, list):
new_tree = utils.flatten(self.visit(elt) for elt in tree)
if not new_tree:
new_tree = [] # preserve the type of `tree`; an empty list shouldn't turn into `None`
tree[:] = new_tree
return tree
return self.transform(tree)
finally:
if newstate:
self._stack.pop()
@abstractmethod
def transform(self, tree):
"""Transform one node. **Abstract method, override this.**
There is only one `transform` method. To detect node type, use `type(tree)`.
This method must recurse explicitly where needed. Use:
- `tree = self.generic_visit(tree)` to visit all children of `tree`.
- `tree.something = self.visit(tree.something)` to selectively visit
only some children. Visiting a statement suite with `self.visit`
is also ok.
Return value as in `ast.NodeTransformer`. If you don't want to make changes,
you must `return tree`. If you return `None`, the subtree is removed.
"""