Skip to content

Commit d5872a8

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 4b2d720 commit d5872a8

File tree

2 files changed

+136
-11
lines changed

2 files changed

+136
-11
lines changed

pyflakes/checker.py

+58-11
Original file line numberDiff line numberDiff line change
@@ -820,9 +820,42 @@ def on_conditional_branch():
820820
self.report(messages.UndefinedName, node, name)
821821

822822
def handleChildren(self, tree, omit=None):
823+
"""Handle all children recursively, but may be flattened."""
823824
for node in iter_child_nodes(tree, omit=omit):
824825
self.handleNode(node, tree)
825826

827+
def handleChildrenNested(self, node):
828+
"""Handle all children recursively."""
829+
self.handleChildren(node)
830+
831+
def _iter_flattened(self, tree, omit, _fields_order=_FieldsOrder()):
832+
"""
833+
Yield child nodes of *node* and their children, with handler.
834+
835+
The value yielded is a tuple of the node, its parent and its handler.
836+
The handler may be False to indicate that no handler and no recursion
837+
is required as the node is part of a flattened list.
838+
"""
839+
_may_flatten = (self.handleChildren,
840+
self.handleChildrenFlattened)
841+
842+
nodes = [(tree, None)]
843+
for node, parent in nodes:
844+
# Skip the root of the tree, which has parent None
845+
handler = self.getNodeHandler(node.__class__) if parent else False
846+
if handler and handler not in _may_flatten:
847+
yield node, parent, handler
848+
else:
849+
nodes[:] += ((child, node)
850+
for child in iter_child_nodes(node,
851+
omit,
852+
_fields_order))
853+
854+
def handleChildrenFlattened(self, tree, omit=None):
855+
"""Handle all children recursively as a flat list where possible."""
856+
for node, parent, handler in self._iter_flattened(tree, omit=omit):
857+
self.handleNode(node, parent, handler)
858+
826859
def isLiteralTupleUnpacking(self, node):
827860
if isinstance(node, ast.Assign):
828861
for child in node.targets + [node.value]:
@@ -852,7 +885,12 @@ def getDocstring(self, node):
852885

853886
return (node.s, doctest_lineno)
854887

855-
def handleNode(self, node, parent):
888+
def handleNode(self, node, parent, handler=None):
889+
"""
890+
Handle a single node, invoking its handler, which may recurse.
891+
892+
If handler is None, the default handler is used.
893+
"""
856894
if node is None:
857895
return
858896
if self.offset and getattr(node, 'lineno', None) is not None:
@@ -863,11 +901,18 @@ def handleNode(self, node, parent):
863901
if self.futuresAllowed and not (isinstance(node, ast.ImportFrom) or
864902
self.isDocstring(node)):
865903
self.futuresAllowed = False
866-
self.nodeDepth += 1
867-
node.depth = self.nodeDepth
904+
905+
node.depth = self.nodeDepth + 1
868906
node.parent = parent
869-
try:
907+
908+
if handler is False:
909+
return
910+
911+
if not handler:
870912
handler = self.getNodeHandler(node.__class__)
913+
914+
self.nodeDepth += 1
915+
try:
871916
handler(node)
872917
finally:
873918
self.nodeDepth -= 1
@@ -964,21 +1009,22 @@ def ignore(self, node):
9641009
pass
9651010

9661011
# "stmt" type nodes
967-
DELETE = PRINT = FOR = ASYNCFOR = WHILE = IF = WITH = WITHITEM = \
968-
ASYNCWITH = ASYNCWITHITEM = TRYFINALLY = EXEC = \
969-
EXPR = ASSIGN = handleChildren
1012+
DELETE = PRINT = EXEC = EXPR = handleChildrenFlattened
1013+
ASSIGN = TRYFINALLY = handleChildren
1014+
FOR = ASYNCFOR = WHILE = IF = WITH = ASYNCWITH = handleChildren
1015+
WITHITEM = ASYNCWITHITEM = handleChildrenFlattened
9701016

9711017
PASS = ignore
9721018

9731019
# "expr" type nodes
9741020
BOOLOP = BINOP = UNARYOP = IFEXP = SET = \
9751021
COMPARE = CALL = REPR = ATTRIBUTE = SUBSCRIPT = \
976-
STARRED = NAMECONSTANT = handleChildren
1022+
STARRED = NAMECONSTANT = handleChildrenFlattened
9771023

9781024
NUM = STR = BYTES = ELLIPSIS = ignore
9791025

9801026
# "slice" type nodes
981-
SLICE = EXTSLICE = INDEX = handleChildren
1027+
SLICE = EXTSLICE = INDEX = handleChildrenFlattened
9821028

9831029
# expression contexts are node instances too, though being constants
9841030
LOAD = STORE = DEL = AUGLOAD = AUGSTORE = PARAM = ignore
@@ -1003,7 +1049,8 @@ def RAISE(self, node):
10031049
self.report(messages.RaiseNotImplemented, node)
10041050

10051051
# additional node types
1006-
COMPREHENSION = KEYWORD = FORMATTEDVALUE = JOINEDSTR = handleChildren
1052+
COMPREHENSION = handleChildren
1053+
KEYWORD = FORMATTEDVALUE = JOINEDSTR = handleChildrenFlattened
10071054

10081055
def DICT(self, node):
10091056
# Complain if there are duplicate keys with different values
@@ -1083,7 +1130,7 @@ def GENERATOREXP(self, node):
10831130
self.handleChildren(node)
10841131
self.popScope()
10851132

1086-
LISTCOMP = handleChildren if PY2 else GENERATOREXP
1133+
LISTCOMP = handleChildrenNested if PY2 else GENERATOREXP
10871134

10881135
DICTCOMP = SETCOMP = GENERATOREXP
10891136

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
@@ -1100,6 +1102,50 @@ def test_containment(self):
11001102
x not in y
11011103
''')
11021104

1105+
def test_flattened(self):
1106+
"""
1107+
Suppress warning when a defined name is used by a binop.
1108+
"""
1109+
self.flakes('''
1110+
w = 5
1111+
x = 10
1112+
y = 20
1113+
z = w + x + y
1114+
''')
1115+
1116+
self.flakes('''
1117+
a = 10
1118+
x = {}
1119+
y = {}
1120+
z = x + {a: a} + y
1121+
''')
1122+
1123+
def test_flattened_with_lambda(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+
a = 10
1130+
b = 10
1131+
l = True and (lambda x: a) or (lambda x: b)
1132+
''')
1133+
self.flakes('''
1134+
a = 10
1135+
l = []
1136+
l = l + (lambda x: a)
1137+
''')
1138+
1139+
def test_flattened_with_comprehension(self):
1140+
"""
1141+
Suppress warning when a defined name is used in an expression
1142+
containing flattened and recursed nodes.
1143+
"""
1144+
self.flakes('''
1145+
l = []
1146+
l = l + [x for x in range(10)]
1147+
''')
1148+
11031149
def test_loopControl(self):
11041150
"""
11051151
break and continue statements are supported.
@@ -1184,6 +1230,11 @@ def a():
11841230
b = 1
11851231
return locals()
11861232
''')
1233+
self.flakes('''
1234+
def a():
1235+
b = 1
1236+
return '{b}' % locals()
1237+
''')
11871238

11881239
def test_unusedVariableNoLocals(self):
11891240
"""
@@ -1390,6 +1441,13 @@ def test_ifexp(self):
13901441
self.flakes("a = foo if True else 'oink'", m.UndefinedName)
13911442
self.flakes("a = 'moo' if True else bar", m.UndefinedName)
13921443

1444+
def test_withStatement(self):
1445+
self.flakes('''
1446+
with open('foo'):
1447+
baz = 1
1448+
assert baz
1449+
''')
1450+
13931451
def test_withStatementNoNames(self):
13941452
"""
13951453
No warnings are emitted for using inside or after a nameless C{with}
@@ -1743,7 +1801,9 @@ def test_asyncFor(self):
17431801
async def read_data(db):
17441802
output = []
17451803
async for row in db.cursor():
1804+
foo = 1
17461805
output.append(row)
1806+
assert foo
17471807
return output
17481808
''')
17491809

@@ -1810,6 +1870,8 @@ def test_asyncWith(self):
18101870
async def commit(session, data):
18111871
async with session.transaction():
18121872
await session.update(data)
1873+
foo = 1
1874+
assert foo
18131875
''')
18141876

18151877
@skipIf(version_info < (3, 5), 'new in Python 3.5')
@@ -1818,7 +1880,9 @@ def test_asyncWithItem(self):
18181880
async def commit(session, data):
18191881
async with session.transaction() as trans:
18201882
await trans.begin()
1883+
foo = 1
18211884
...
1885+
assert foo
18221886
await trans.end()
18231887
''')
18241888

@@ -1993,3 +2057,17 @@ def test_raise_notimplemented(self):
19932057
self.flakes('''
19942058
raise NotImplemented
19952059
''', m.RaiseNotImplemented)
2060+
2061+
2062+
class TestMaximumRecursion(TestCase):
2063+
2064+
def setUp(self):
2065+
self._recursionlimit = sys.getrecursionlimit()
2066+
2067+
def test_flattened(self):
2068+
sys.setrecursionlimit(100)
2069+
s = 'x = ' + ' + '.join(str(n) for n in range(100))
2070+
self.flakes(s)
2071+
2072+
def tearDown(self):
2073+
sys.setrecursionlimit(self._recursionlimit)

0 commit comments

Comments
 (0)