Skip to content

Commit b4651a2

Browse files
feat: more ast helpers: __str__ and find_body (#136)
* feat: implement __str__ * feat: add find_body helper * docs: describe str and find_body * refactor: try to explain is_equivalent better Also formatting.
1 parent cee580e commit b4651a2

File tree

3 files changed

+120
-12
lines changed

3 files changed

+120
-12
lines changed

docs/python.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,25 @@ assert add(a, b) == 300
122122
In contrast to the regex-based helpers, these helpers need to be run in Python, not
123123
JavaScript
124124

125+
### Formatting output
126+
127+
`str` returns a string that would parse to the same AST as the node. For example:
128+
129+
```python
130+
function_str = """
131+
def foo():
132+
# will not be in the output
133+
x = 1
134+
135+
"""
136+
output_str = """
137+
def foo():
138+
x = 1"""
139+
str(Node(function_str)) == output_str # True
140+
```
141+
142+
The output and source string compile to the same AST, but the output is indented with 4 spaces. Comments and trailing whitespace are removed.
143+
125144
### Finding nodes
126145

127146
`find_` functions search the current scope and return one of the following:
@@ -147,6 +166,15 @@ When the variable is out of scope, `find_variable` returns an `None` node (i.e.
147166
Node('def foo():\n x = "1"').find_variable("x") == Node() # True
148167
```
149168

169+
#### `find_body`
170+
171+
```python
172+
func_str = """
173+
def foo():
174+
x = 1"""
175+
Node(func_str).find_function("foo").find_body().is_equivalent("x = 1") # True
176+
```
177+
150178
#### `find_class`
151179

152180
```python

python/py_helpers.py

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ def __repr__(self):
4141
return "Node:\nNone"
4242
return "Node:\n" + ast.dump(self.tree, indent=2)
4343

44+
def __str__(self):
45+
if self.tree == None:
46+
return "# no ast"
47+
return ast.unparse(self.tree)
48+
4449
def _has_body(self):
4550
return bool(getattr(self.tree, "body", False))
4651

@@ -57,6 +62,13 @@ def find_function(self, func):
5762
return Node(node)
5863
return Node()
5964

65+
def find_body(self):
66+
if not isinstance(self.tree, ast.AST):
67+
return Node()
68+
if not hasattr(self.tree, "body"):
69+
return Node()
70+
return Node(ast.Module(self.tree.body, []))
71+
6072
# "has" functions return a boolean indicating whether whatever is being
6173
# searched for exists. In this case, it returns True if the variable exists.
6274

@@ -99,12 +111,18 @@ def value_is_call(self, name):
99111
return call.func.id == name
100112
return False
101113

102-
# Takes an string and checks if is equivalent to the node's AST. This
103-
# is a loose comparison that tries to find out if the code is essentially
104-
# the same. For example, the string "True" is not represented by the same
105-
# AST as the test in "if True:" (the string could be wrapped in Module,
106-
# Interactive or Expression, depending on the parse mode and the test is
107-
# just a Constant), but they are equivalent.
114+
# Loosely compares the code in target_str with the code represented by the
115+
# Node's AST. If the two codes are semantically equivalent (i.e. the same if
116+
# you ignore formatting and context) then this returns True, otherwise
117+
# False.
118+
#
119+
# Ignoring context means that the following comparison is True despite the
120+
# fact that the AST of `cond_node` is `Constant(value=True)` and `True`
121+
# compiles to `Module(body=[Expr(value=Constant(value=True))],
122+
# type_ignores=[])`:
123+
#
124+
# node = Node("if True:\n pass") cond_node =
125+
# node.find_ifs()[0].find_conditions()[0] cond_node.is_equivalent("True")
108126

109127
def is_equivalent(self, target_str):
110128
# Setting the tree to None is used to represent missing elements. Such
@@ -131,9 +149,7 @@ def find_ifs(self):
131149
return self._find_all(ast.If)
132150

133151
def _find_all(self, ast_type):
134-
return [
135-
Node(node) for node in self.tree.body if isinstance(node, ast_type)
136-
]
152+
return [Node(node) for node in self.tree.body if isinstance(node, ast_type)]
137153

138154
def find_conditions(self):
139155
def _find_conditions(tree):
@@ -162,6 +178,4 @@ def _find_if_bodies(tree):
162178

163179
return [tree.body] + [tree.orelse]
164180

165-
return [
166-
Node(ast.Module(body, [])) for body in _find_if_bodies(self.tree)
167-
]
181+
return [Node(ast.Module(body, [])) for body in _find_if_bodies(self.tree)]

python/py_helpers.test.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,37 @@ def test_not_has_function(self):
223223

224224
self.assertFalse(node.has_function("bar"))
225225

226+
def test_find_body(self):
227+
func_str = """def foo():
228+
x = 1
229+
print(x)
230+
"""
231+
node = Node(func_str)
232+
233+
self.assertTrue(
234+
node.find_function("foo").find_body().is_equivalent("x = 1\nprint(x)")
235+
)
236+
self.assertEqual("x = 1\nprint(x)", str(node.find_function("foo").find_body()))
237+
238+
def test_find_body_with_class(self):
239+
class_str = """
240+
class Foo:
241+
def __init__(self):
242+
self.x = 1
243+
"""
244+
node = Node(class_str)
245+
246+
self.assertTrue(
247+
node.find_class("Foo")
248+
.find_body()
249+
.is_equivalent("def __init__(self):\n self.x = 1")
250+
)
251+
252+
def test_find_body_without_body(self):
253+
node = Node("x = 1")
254+
255+
self.assertEqual(node.find_variable("x").find_body(), Node())
256+
226257

227258
class TestEquivalenceHelpers(unittest.TestCase):
228259
def test_is_equivalent(self):
@@ -478,6 +509,41 @@ def test_len(self):
478509

479510
self.assertEqual(len(node.find_ifs()), 2)
480511

512+
def test_str(self):
513+
func_str = """def foo():
514+
pass
515+
"""
516+
# Note: the indentation and whitespace is not preserved.
517+
expected = """def foo():
518+
pass"""
519+
520+
self.assertEqual(expected, str(Node(func_str)))
521+
522+
def test_none_str(self):
523+
self.assertEqual("# no ast", str(Node()))
524+
525+
def test_str_with_comments(self):
526+
func_str = """def foo():
527+
# comment
528+
pass
529+
530+
531+
"""
532+
# Note: comments are discarded
533+
expected = """def foo():
534+
pass"""
535+
536+
self.assertEqual(expected, str(Node(func_str)))
537+
538+
539+
def test_repr(self):
540+
func_str = """def foo():
541+
pass
542+
"""
543+
node = Node(func_str)
544+
545+
self.assertEqual(repr(node), "Node:\n" + ast.dump(node.tree, indent=2))
546+
481547

482548
if __name__ == "__main__":
483549
unittest.main()

0 commit comments

Comments
 (0)