Skip to content

Commit e3b323c

Browse files
committed
Flatten handlers
pyflakes has traditionally recursed with a handler for every level of the ast. The ast depth can become very large, especially for an expression containing many binary operators. Python has a maximum recursion limit, defaulting to a low number like 1000, which resulted in a RuntimeError for the ast of: x = 1 + 2 + 3 + ... + 1001 This change avoids recursing for nodes that do not have a specific handler. Checker.nodeDepth and node.depth change from always being the ast depth, which varied based on Python version due to ast differences, to being the number of nested handlers within pyflakes.
1 parent 29914fc commit e3b323c

File tree

2 files changed

+150
-11
lines changed

2 files changed

+150
-11
lines changed

pyflakes/checker.py

+72-11
Original file line numberDiff line numberDiff line change
@@ -734,9 +734,53 @@ def on_conditional_branch():
734734
self.report(messages.UndefinedName, node, name)
735735

736736
def handleChildren(self, tree, omit=None):
737+
"""Handle all children recursively, but may be flattened."""
737738
for node in iter_child_nodes(tree, omit=omit):
738739
self.handleNode(node, tree)
739740

741+
def handleChildrenNested(self, node):
742+
"""Handle all children recursively."""
743+
self.handleChildren(node)
744+
745+
def _iter_flattened(self, node, omit, _fields_order=_FieldsOrder()):
746+
"""
747+
Yield child nodes of *node* and their children, with a recurse flag.
748+
749+
The value yielded is a tuple of the node, its parent and a boolean flag
750+
to indicate whether the node needs to be recursed by the caller because
751+
all child nodes can not be processed as a flat list.
752+
753+
e.g. flag is True for lambda and comprehension nodes,
754+
as their children must be handled by the appropriate node handler.
755+
"""
756+
nodes = [(node, None)]
757+
while nodes:
758+
new_nodes = []
759+
for node, parent in nodes:
760+
yield_children = True
761+
if parent:
762+
handler = self.getNodeHandler(node.__class__)
763+
if handler in (self.handleChildren,
764+
self.handleChildrenFlattened,
765+
self.handleNode):
766+
handler = False
767+
else:
768+
yield_children = False
769+
yield node, parent, handler
770+
771+
if yield_children:
772+
new_nodes += list(
773+
(child, node)
774+
for child in iter_child_nodes(node,
775+
omit,
776+
_fields_order))
777+
778+
nodes[:] = new_nodes
779+
780+
def handleChildrenFlattened(self, tree, omit=None):
781+
for node, parent, handler in self._iter_flattened(tree, omit=omit):
782+
self.handleNode(node, parent, handler)
783+
740784
def isLiteralTupleUnpacking(self, node):
741785
if isinstance(node, ast.Assign):
742786
for child in node.targets + [node.value]:
@@ -766,7 +810,7 @@ def getDocstring(self, node):
766810

767811
return (node.s, doctest_lineno)
768812

769-
def handleNode(self, node, parent):
813+
def handleNode(self, node, parent, handler=None):
770814
if node is None:
771815
return
772816
if self.offset and getattr(node, 'lineno', None) is not None:
@@ -777,11 +821,18 @@ def handleNode(self, node, parent):
777821
if self.futuresAllowed and not (isinstance(node, ast.ImportFrom) or
778822
self.isDocstring(node)):
779823
self.futuresAllowed = False
780-
self.nodeDepth += 1
781-
node.depth = self.nodeDepth
824+
825+
node.depth = self.nodeDepth + 1
782826
node.parent = parent
783-
try:
827+
828+
if handler is False:
829+
return
830+
831+
if not handler:
784832
handler = self.getNodeHandler(node.__class__)
833+
834+
self.nodeDepth += 1
835+
try:
785836
handler(node)
786837
finally:
787838
self.nodeDepth -= 1
@@ -833,21 +884,30 @@ def ignore(self, node):
833884
pass
834885

835886
# "stmt" type nodes
836-
DELETE = PRINT = FOR = ASYNCFOR = WHILE = IF = WITH = WITHITEM = \
837-
ASYNCWITH = ASYNCWITHITEM = RAISE = TRYFINALLY = EXEC = \
838-
EXPR = ASSIGN = handleChildren
887+
888+
# many of these handleChildrenFlattened are wrong, and need tests
889+
# Anywhere a function/class can be defined probably caueses a failure,
890+
# due to the delayed processing.
891+
DELETE = PRINT = handleChildrenFlattened
892+
FOR = ASYNCFOR = handleChildren
893+
WHILE = IF = WITH = ASYNCWITH = handleChildren
894+
WITHITEM = ASYNCWITHITEM = handleChildrenFlattened
895+
RAISE = handleChildrenFlattened
896+
TRYFINALLY = handleChildren
897+
EXEC = EXPR = handleChildrenFlattened
898+
ASSIGN = handleChildren
839899

840900
PASS = ignore
841901

842902
# "expr" type nodes
843903
BOOLOP = BINOP = UNARYOP = IFEXP = DICT = SET = \
844904
COMPARE = CALL = REPR = ATTRIBUTE = SUBSCRIPT = \
845-
STARRED = NAMECONSTANT = handleChildren
905+
STARRED = NAMECONSTANT = handleChildrenFlattened
846906

847907
NUM = STR = BYTES = ELLIPSIS = ignore
848908

849909
# "slice" type nodes
850-
SLICE = EXTSLICE = INDEX = handleChildren
910+
SLICE = EXTSLICE = INDEX = handleChildrenFlattened
851911

852912
# expression contexts are node instances too, though being constants
853913
LOAD = STORE = DEL = AUGLOAD = AUGSTORE = PARAM = ignore
@@ -859,7 +919,8 @@ def ignore(self, node):
859919
MATMULT = ignore
860920

861921
# additional node types
862-
COMPREHENSION = KEYWORD = FORMATTEDVALUE = handleChildren
922+
COMPREHENSION = handleChildren
923+
KEYWORD = FORMATTEDVALUE = handleChildrenFlattened
863924

864925
def ASSERT(self, node):
865926
if isinstance(node.test, ast.Tuple) and node.test.elts != []:
@@ -903,7 +964,7 @@ def GENERATOREXP(self, node):
903964
self.handleChildren(node)
904965
self.popScope()
905966

906-
LISTCOMP = handleChildren if PY2 else GENERATOREXP
967+
LISTCOMP = handleChildrenNested if PY2 else GENERATOREXP
907968

908969
DICTCOMP = SETCOMP = GENERATOREXP
909970

pyflakes/test/test_other.py

+78
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
Tests for various Pyflakes behavior.
33
"""
44

5+
import sys
6+
57
from sys import version_info
68

79
from pyflakes import messages as m
@@ -1084,6 +1086,50 @@ def test_containment(self):
10841086
x not in y
10851087
''')
10861088

1089+
def test_flattened(self):
1090+
"""
1091+
Suppress warning when a defined name is used by a binop.
1092+
"""
1093+
self.flakes('''
1094+
w = 5
1095+
x = 10
1096+
y = 20
1097+
z = w + x + y
1098+
''')
1099+
1100+
self.flakes('''
1101+
a = 10
1102+
x = {}
1103+
y = {}
1104+
z = x + {a: a} + y
1105+
''')
1106+
1107+
def test_flattened_with_lambda(self):
1108+
"""
1109+
Suppress warning when a defined name is used in an expression
1110+
containing flattened and recursed nodes.
1111+
"""
1112+
self.flakes('''
1113+
a = 10
1114+
b = 10
1115+
l = True and (lambda x: a) or (lambda x: b)
1116+
''')
1117+
self.flakes('''
1118+
a = 10
1119+
l = []
1120+
l = l + (lambda x: a)
1121+
''')
1122+
1123+
def test_flattened_with_comprehension(self):
1124+
"""
1125+
Suppress warning when a defined name is used in an expression
1126+
containing flattened and recursed nodes.
1127+
"""
1128+
self.flakes('''
1129+
l = []
1130+
l = l + [x for x in range(10)]
1131+
''')
1132+
10871133
def test_loopControl(self):
10881134
"""
10891135
break and continue statements are supported.
@@ -1168,6 +1214,11 @@ def a():
11681214
b = 1
11691215
return locals()
11701216
''')
1217+
self.flakes('''
1218+
def a():
1219+
b = 1
1220+
return '{b}' % locals()
1221+
''')
11711222

11721223
def test_unusedVariableNoLocals(self):
11731224
"""
@@ -1565,6 +1616,13 @@ def test_withStatementUndefinedInExpression(self):
15651616
pass
15661617
''', m.UndefinedName)
15671618

1619+
def test_with_statement_order(self):
1620+
self.flakes('''
1621+
with open('foo'):
1622+
baz = 1
1623+
assert baz
1624+
''')
1625+
15681626
@skipIf(version_info < (2, 7), "Python >= 2.7 only")
15691627
def test_dictComprehension(self):
15701628
"""
@@ -1715,7 +1773,9 @@ def test_asyncFor(self):
17151773
async def read_data(db):
17161774
output = []
17171775
async for row in db.cursor():
1776+
foo = 1
17181777
output.append(row)
1778+
assert foo
17191779
return output
17201780
''')
17211781

@@ -1725,6 +1785,8 @@ def test_asyncWith(self):
17251785
async def commit(session, data):
17261786
async with session.transaction():
17271787
await session.update(data)
1788+
foo = 1
1789+
assert foo
17281790
''')
17291791

17301792
@skipIf(version_info < (3, 5), 'new in Python 3.5')
@@ -1733,7 +1795,9 @@ def test_asyncWithItem(self):
17331795
async def commit(session, data):
17341796
async with session.transaction() as trans:
17351797
await trans.begin()
1798+
foo = 1
17361799
...
1800+
assert foo
17371801
await trans.end()
17381802
''')
17391803

@@ -1743,3 +1807,17 @@ def test_matmul(self):
17431807
def foo(a, b):
17441808
return a @ b
17451809
''')
1810+
1811+
1812+
class TestMaximumRecursion(TestCase):
1813+
1814+
def setUp(self):
1815+
self._recursionlimit = sys.getrecursionlimit()
1816+
1817+
def test_flattened(self):
1818+
sys.setrecursionlimit(100)
1819+
s = 'x = ' + ' + '.join(str(n) for n in range(100))
1820+
self.flakes(s)
1821+
1822+
def tearDown(self):
1823+
sys.setrecursionlimit(self._recursionlimit)

0 commit comments

Comments
 (0)