Skip to content

Commit 4cbc995

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 434bbb0 commit 4cbc995

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
@@ -812,9 +812,42 @@ def on_conditional_branch():
812812
self.report(messages.UndefinedName, node, name)
813813

814814
def handleChildren(self, tree, omit=None):
815+
"""Handle all children recursively, but may be flattened."""
815816
for node in iter_child_nodes(tree, omit=omit):
816817
self.handleNode(node, tree)
817818

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

845878
return (node.s, doctest_lineno)
846879

847-
def handleNode(self, node, parent):
880+
def handleNode(self, node, parent, handler=None):
881+
"""
882+
Handle a single node, invoking its handler, which may recurse.
883+
884+
If handler is None, the default handler is used.
885+
"""
848886
if node is None:
849887
return
850888
if self.offset and getattr(node, 'lineno', None) is not None:
@@ -855,11 +893,18 @@ def handleNode(self, node, parent):
855893
if self.futuresAllowed and not (isinstance(node, ast.ImportFrom) or
856894
self.isDocstring(node)):
857895
self.futuresAllowed = False
858-
self.nodeDepth += 1
859-
node.depth = self.nodeDepth
896+
897+
node.depth = self.nodeDepth + 1
860898
node.parent = parent
861-
try:
899+
900+
if handler is False:
901+
return
902+
903+
if not handler:
862904
handler = self.getNodeHandler(node.__class__)
905+
906+
self.nodeDepth += 1
907+
try:
863908
handler(node)
864909
finally:
865910
self.nodeDepth -= 1
@@ -911,21 +956,22 @@ def ignore(self, node):
911956
pass
912957

913958
# "stmt" type nodes
914-
DELETE = PRINT = FOR = ASYNCFOR = WHILE = IF = WITH = WITHITEM = \
915-
ASYNCWITH = ASYNCWITHITEM = RAISE = TRYFINALLY = EXEC = \
916-
EXPR = ASSIGN = handleChildren
959+
DELETE = PRINT = EXEC = EXPR = RAISE = handleChildrenFlattened
960+
ASSIGN = TRYFINALLY = handleChildren
961+
FOR = ASYNCFOR = WHILE = IF = WITH = ASYNCWITH = handleChildren
962+
WITHITEM = ASYNCWITHITEM = handleChildrenFlattened
917963

918964
PASS = ignore
919965

920966
# "expr" type nodes
921967
BOOLOP = BINOP = UNARYOP = IFEXP = SET = \
922968
COMPARE = CALL = REPR = ATTRIBUTE = SUBSCRIPT = \
923-
STARRED = NAMECONSTANT = handleChildren
969+
STARRED = NAMECONSTANT = handleChildrenFlattened
924970

925971
NUM = STR = BYTES = ELLIPSIS = ignore
926972

927973
# "slice" type nodes
928-
SLICE = EXTSLICE = INDEX = handleChildren
974+
SLICE = EXTSLICE = INDEX = handleChildrenFlattened
929975

930976
# expression contexts are node instances too, though being constants
931977
LOAD = STORE = DEL = AUGLOAD = AUGSTORE = PARAM = ignore
@@ -937,7 +983,8 @@ def ignore(self, node):
937983
MATMULT = ignore
938984

939985
# additional node types
940-
COMPREHENSION = KEYWORD = FORMATTEDVALUE = JOINEDSTR = handleChildren
986+
COMPREHENSION = handleChildren
987+
KEYWORD = FORMATTEDVALUE = JOINEDSTR = handleChildrenFlattened
941988

942989
def DICT(self, node):
943990
# Complain if there are duplicate keys with different values
@@ -1017,7 +1064,7 @@ def GENERATOREXP(self, node):
10171064
self.handleChildren(node)
10181065
self.popScope()
10191066

1020-
LISTCOMP = handleChildren if PY2 else GENERATOREXP
1067+
LISTCOMP = handleChildrenNested if PY2 else GENERATOREXP
10211068

10221069
DICTCOMP = SETCOMP = GENERATOREXP
10231070

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
"""
@@ -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

@@ -1782,6 +1842,8 @@ def test_asyncWith(self):
17821842
async def commit(session, data):
17831843
async with session.transaction():
17841844
await session.update(data)
1845+
foo = 1
1846+
assert foo
17851847
''')
17861848

17871849
@skipIf(version_info < (3, 5), 'new in Python 3.5')
@@ -1790,7 +1852,9 @@ def test_asyncWithItem(self):
17901852
async def commit(session, data):
17911853
async with session.transaction() as trans:
17921854
await trans.begin()
1855+
foo = 1
17931856
...
1857+
assert foo
17941858
await trans.end()
17951859
''')
17961860

@@ -1808,3 +1872,17 @@ def test_formatstring(self):
18081872
mom = 'mom'
18091873
f'{hi} {mom}'
18101874
''')
1875+
1876+
1877+
class TestMaximumRecursion(TestCase):
1878+
1879+
def setUp(self):
1880+
self._recursionlimit = sys.getrecursionlimit()
1881+
1882+
def test_flattened(self):
1883+
sys.setrecursionlimit(100)
1884+
s = 'x = ' + ' + '.join(str(n) for n in range(100))
1885+
self.flakes(s)
1886+
1887+
def tearDown(self):
1888+
sys.setrecursionlimit(self._recursionlimit)

0 commit comments

Comments
 (0)