Skip to content

Commit ea032f4

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 changes from always being the ast depth, which varied between Python version due to ast differences, to being the number of nested handlers within pyflakes.
1 parent 29914fc commit ea032f4

File tree

2 files changed

+136
-11
lines changed

2 files changed

+136
-11
lines changed

pyflakes/checker.py

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -734,9 +734,42 @@ 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, tree, omit, _fields_order=_FieldsOrder()):
746+
"""
747+
Yield child nodes of *node* and their children, with handler.
748+
749+
The value yielded is a tuple of the node, its parent and its handler.
750+
The handler may be False to indicate that no handler and no recursion
751+
is required as the node is part of a flattened list.
752+
"""
753+
_may_flatten = (self.handleChildren,
754+
self.handleChildrenFlattened)
755+
756+
nodes = [(tree, None)]
757+
for node, parent in nodes:
758+
# Skip the root of the tree, which has parent None
759+
handler = self.getNodeHandler(node.__class__) if parent else False
760+
if handler and handler not in _may_flatten:
761+
yield node, parent, handler
762+
else:
763+
nodes[:] += ((child, node)
764+
for child in iter_child_nodes(node,
765+
omit,
766+
_fields_order))
767+
768+
def handleChildrenFlattened(self, tree, omit=None):
769+
"""Handle all children recursively as a flat list where possible."""
770+
for node, parent, handler in self._iter_flattened(tree, omit=omit):
771+
self.handleNode(node, parent, handler)
772+
740773
def isLiteralTupleUnpacking(self, node):
741774
if isinstance(node, ast.Assign):
742775
for child in node.targets + [node.value]:
@@ -766,7 +799,12 @@ def getDocstring(self, node):
766799

767800
return (node.s, doctest_lineno)
768801

769-
def handleNode(self, node, parent):
802+
def handleNode(self, node, parent, handler=None):
803+
"""
804+
Handle a single node, invoking its handler, which may recurse.
805+
806+
If handler is None, the default handler is used.
807+
"""
770808
if node is None:
771809
return
772810
if self.offset and getattr(node, 'lineno', None) is not None:
@@ -777,11 +815,18 @@ def handleNode(self, node, parent):
777815
if self.futuresAllowed and not (isinstance(node, ast.ImportFrom) or
778816
self.isDocstring(node)):
779817
self.futuresAllowed = False
780-
self.nodeDepth += 1
781-
node.depth = self.nodeDepth
818+
819+
node.depth = self.nodeDepth + 1
782820
node.parent = parent
783-
try:
821+
822+
if handler is False:
823+
return
824+
825+
if not handler:
784826
handler = self.getNodeHandler(node.__class__)
827+
828+
self.nodeDepth += 1
829+
try:
785830
handler(node)
786831
finally:
787832
self.nodeDepth -= 1
@@ -833,21 +878,22 @@ def ignore(self, node):
833878
pass
834879

835880
# "stmt" type nodes
836-
DELETE = PRINT = FOR = ASYNCFOR = WHILE = IF = WITH = WITHITEM = \
837-
ASYNCWITH = ASYNCWITHITEM = RAISE = TRYFINALLY = EXEC = \
838-
EXPR = ASSIGN = handleChildren
881+
DELETE = PRINT = EXEC = EXPR = RAISE = handleChildrenFlattened
882+
ASSIGN = TRYFINALLY = handleChildren
883+
FOR = ASYNCFOR = WHILE = IF = WITH = ASYNCWITH = handleChildren
884+
WITHITEM = ASYNCWITHITEM = handleChildrenFlattened
839885

840886
PASS = ignore
841887

842888
# "expr" type nodes
843889
BOOLOP = BINOP = UNARYOP = IFEXP = DICT = SET = \
844890
COMPARE = CALL = REPR = ATTRIBUTE = SUBSCRIPT = \
845-
STARRED = NAMECONSTANT = handleChildren
891+
STARRED = NAMECONSTANT = handleChildrenFlattened
846892

847893
NUM = STR = BYTES = ELLIPSIS = ignore
848894

849895
# "slice" type nodes
850-
SLICE = EXTSLICE = INDEX = handleChildren
896+
SLICE = EXTSLICE = INDEX = handleChildrenFlattened
851897

852898
# expression contexts are node instances too, though being constants
853899
LOAD = STORE = DEL = AUGLOAD = AUGSTORE = PARAM = ignore
@@ -859,7 +905,8 @@ def ignore(self, node):
859905
MATMULT = ignore
860906

861907
# additional node types
862-
COMPREHENSION = KEYWORD = FORMATTEDVALUE = handleChildren
908+
COMPREHENSION = handleChildren
909+
KEYWORD = FORMATTEDVALUE = handleChildrenFlattened
863910

864911
def ASSERT(self, node):
865912
if isinstance(node.test, ast.Tuple) and node.test.elts != []:
@@ -903,7 +950,7 @@ def GENERATOREXP(self, node):
903950
self.handleChildren(node)
904951
self.popScope()
905952

906-
LISTCOMP = handleChildren if PY2 else GENERATOREXP
953+
LISTCOMP = handleChildrenNested if PY2 else GENERATOREXP
907954

908955
DICTCOMP = SETCOMP = GENERATOREXP
909956

pyflakes/test/test_other.py

Lines changed: 78 additions & 0 deletions
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
"""
@@ -1374,6 +1425,13 @@ def test_ifexp(self):
13741425
self.flakes("a = foo if True else 'oink'", m.UndefinedName)
13751426
self.flakes("a = 'moo' if True else bar", m.UndefinedName)
13761427

1428+
def test_withStatement(self):
1429+
self.flakes('''
1430+
with open('foo'):
1431+
baz = 1
1432+
assert baz
1433+
''')
1434+
13771435
def test_withStatementNoNames(self):
13781436
"""
13791437
No warnings are emitted for using inside or after a nameless C{with}
@@ -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)