Skip to content

Commit 8c7106e

Browse files
Log RecursionErrors out as warnings during node transformation (#2385)
* Trap RecursionError in `visit_attribute` Co-authored-by: Marc Mueller <[email protected]>
1 parent 6519a0e commit 8c7106e

File tree

6 files changed

+237
-4
lines changed

6 files changed

+237
-4
lines changed

ChangeLog

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,15 @@ Release date: TBA
2727
* Include modname in AST warnings. Useful for ``invalid escape sequence`` warnings
2828
with Python 3.12.
2929

30+
* ``RecursionError`` is now trapped and logged out as ``UserWarning`` during astroid node transformations with instructions about raising the system recursion limit.
31+
32+
Closes pylint-dev/pylint#8842
33+
3034
* Suppress ``SyntaxWarning`` for invalid escape sequences on Python 3.12 when parsing modules.
3135

3236
Closes pylint-dev/pylint#9322
3337

3438

35-
3639
What's New in astroid 3.0.3?
3740
============================
3841
Release date: 2024-02-04

astroid/nodes/as_string.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from __future__ import annotations
88

9+
import warnings
910
from collections.abc import Iterator
1011
from typing import TYPE_CHECKING
1112

@@ -363,7 +364,15 @@ def visit_generatorexp(self, node) -> str:
363364

364365
def visit_attribute(self, node) -> str:
365366
"""return an astroid.Getattr node as string"""
366-
left = self._precedence_parens(node, node.expr)
367+
try:
368+
left = self._precedence_parens(node, node.expr)
369+
except RecursionError:
370+
warnings.warn(
371+
"Recursion limit exhausted; defaulting to adding parentheses.",
372+
UserWarning,
373+
stacklevel=2,
374+
)
375+
left = f"({node.expr.accept(self)})"
367376
if left.isdigit():
368377
left = f"({left})"
369378
return f"{left}.{node.attrname}"

astroid/transforms.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from __future__ import annotations
66

7+
import warnings
78
from collections import defaultdict
89
from collections.abc import Callable
910
from typing import TYPE_CHECKING, List, Optional, Tuple, TypeVar, Union, cast, overload
@@ -110,7 +111,18 @@ def _visit_generic(self, node: _Vistables) -> _VisitReturns:
110111
if not node or isinstance(node, str):
111112
return node
112113

113-
return self._visit(node)
114+
try:
115+
return self._visit(node)
116+
except RecursionError:
117+
# Returning the node untransformed is better than giving up.
118+
warnings.warn(
119+
f"Astroid was unable to transform {node}.\n"
120+
"Some functionality will be missing unless the system recursion limit is lifted.\n"
121+
"From pylint, try: --init-hook='import sys; sys.setrecursionlimit(2000)' or higher.",
122+
UserWarning,
123+
stacklevel=0,
124+
)
125+
return node
114126

115127
def register_transform(
116128
self,

tests/test_nodes.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
transforms,
3030
util,
3131
)
32-
from astroid.const import PY310_PLUS, PY312_PLUS, Context
32+
from astroid.const import IS_PYPY, PY310_PLUS, PY312_PLUS, Context
3333
from astroid.context import InferenceContext
3434
from astroid.exceptions import (
3535
AstroidBuildingError,
@@ -47,6 +47,7 @@
4747
Tuple,
4848
)
4949
from astroid.nodes.scoped_nodes import ClassDef, FunctionDef, GeneratorExp, Module
50+
from tests.testdata.python3.recursion_error import LONG_CHAINED_METHOD_CALL
5051

5152
from . import resources
5253

@@ -279,6 +280,20 @@ def test_as_string_unknown() -> None:
279280
assert nodes.Unknown().as_string() == "Unknown.Unknown()"
280281
assert nodes.Unknown(lineno=1, col_offset=0).as_string() == "Unknown.Unknown()"
281282

283+
@staticmethod
284+
@pytest.mark.skipif(
285+
IS_PYPY,
286+
reason="Test requires manipulating the recursion limit, which cannot "
287+
"be undone in a finally block without polluting other tests on PyPy.",
288+
)
289+
def test_recursion_error_trapped() -> None:
290+
with pytest.warns(UserWarning, match="unable to transform"):
291+
ast = abuilder.string_build(LONG_CHAINED_METHOD_CALL)
292+
293+
attribute = ast.body[1].value.func
294+
with pytest.raises(UserWarning):
295+
attribute.as_string()
296+
282297

283298
@pytest.mark.skipif(not PY312_PLUS, reason="Uses 3.12 type param nodes")
284299
class AsStringTypeParamNodes(unittest.TestCase):

tests/test_transforms.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,20 @@
55
from __future__ import annotations
66

77
import contextlib
8+
import sys
89
import time
910
import unittest
1011
from collections.abc import Callable, Iterator
1112

13+
import pytest
14+
1215
from astroid import MANAGER, builder, nodes, parse, transforms
16+
from astroid.brain.brain_dataclasses import _looks_like_dataclass_field_call
17+
from astroid.const import IS_PYPY
1318
from astroid.manager import AstroidManager
1419
from astroid.nodes.node_classes import Call, Compare, Const, Name
1520
from astroid.nodes.scoped_nodes import FunctionDef, Module
21+
from tests.testdata.python3.recursion_error import LONG_CHAINED_METHOD_CALL
1622

1723

1824
@contextlib.contextmanager
@@ -258,3 +264,21 @@ def transform_class(cls):
258264
import UserDict
259265
"""
260266
)
267+
268+
def test_transform_aborted_if_recursion_limited(self):
269+
def transform_call(node: Call) -> Const:
270+
return node
271+
272+
self.transformer.register_transform(
273+
nodes.Call, transform_call, _looks_like_dataclass_field_call
274+
)
275+
276+
original_limit = sys.getrecursionlimit()
277+
sys.setrecursionlimit(500 if IS_PYPY else 1000)
278+
279+
try:
280+
with pytest.warns(UserWarning) as records:
281+
self.parse_transform(LONG_CHAINED_METHOD_CALL)
282+
assert "sys.setrecursionlimit" in records[0].message.args[0]
283+
finally:
284+
sys.setrecursionlimit(original_limit)
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
LONG_CHAINED_METHOD_CALL = """
2+
from a import b
3+
4+
(
5+
b.builder('name')
6+
.add('name', value='value')
7+
.add('name', value='value')
8+
.add('name', value='value')
9+
.add('name', value='value')
10+
.add('name', value='value')
11+
.add('name', value='value')
12+
.add('name', value='value')
13+
.add('name', value='value')
14+
.add('name', value='value')
15+
.add('name', value='value')
16+
.add('name', value='value')
17+
.add('name', value='value')
18+
.add('name', value='value')
19+
.add('name', value='value')
20+
.add('name', value='value')
21+
.add('name', value='value')
22+
.add('name', value='value')
23+
.add('name', value='value')
24+
.add('name', value='value')
25+
.add('name', value='value')
26+
.add('name', value='value')
27+
.add('name', value='value')
28+
.add('name', value='value')
29+
.add('name', value='value')
30+
.add('name', value='value')
31+
.add('name', value='value')
32+
.add('name', value='value')
33+
.add('name', value='value')
34+
.add('name', value='value')
35+
.add('name', value='value')
36+
.add('name', value='value')
37+
.add('name', value='value')
38+
.add('name', value='value')
39+
.add('name', value='value')
40+
.add('name', value='value')
41+
.add('name', value='value')
42+
.add('name', value='value')
43+
.add('name', value='value')
44+
.add('name', value='value')
45+
.add('name', value='value')
46+
.add('name', value='value')
47+
.add('name', value='value')
48+
.add('name', value='value')
49+
.add('name', value='value')
50+
.add('name', value='value')
51+
.add('name', value='value')
52+
.add('name', value='value')
53+
.add('name', value='value')
54+
.add('name', value='value')
55+
.add('name', value='value')
56+
.add('name', value='value')
57+
.add('name', value='value')
58+
.add('name', value='value')
59+
.add('name', value='value')
60+
.add('name', value='value')
61+
.add('name', value='value')
62+
.add('name', value='value')
63+
.add('name', value='value')
64+
.add('name', value='value')
65+
.add('name', value='value')
66+
.add('name', value='value')
67+
.add('name', value='value')
68+
.add('name', value='value')
69+
.add('name', value='value')
70+
.add('name', value='value')
71+
.add('name', value='value')
72+
.add('name', value='value')
73+
.add('name', value='value')
74+
.add('name', value='value')
75+
.add('name', value='value')
76+
.add('name', value='value')
77+
.add('name', value='value')
78+
.add('name', value='value')
79+
.add('name', value='value')
80+
.add('name', value='value')
81+
.add('name', value='value')
82+
.add('name', value='value')
83+
.add('name', value='value')
84+
.add('name', value='value')
85+
.add('name', value='value')
86+
.add('name', value='value')
87+
.add('name', value='value')
88+
.add('name', value='value')
89+
.add('name', value='value')
90+
.add('name', value='value')
91+
.add('name', value='value')
92+
.add('name', value='value')
93+
.add('name', value='value')
94+
.add('name', value='value')
95+
.add('name', value='value')
96+
.add('name', value='value')
97+
.add('name', value='value')
98+
.add('name', value='value')
99+
.add('name', value='value')
100+
.add('name', value='value')
101+
.add('name', value='value')
102+
.add('name', value='value')
103+
.add('name', value='value')
104+
.add('name', value='value')
105+
.add('name', value='value')
106+
.add('name', value='value')
107+
.add('name', value='value')
108+
.add('name', value='value')
109+
.add('name', value='value')
110+
.add('name', value='value')
111+
.add('name', value='value')
112+
.add('name', value='value')
113+
.add('name', value='value')
114+
.add('name', value='value')
115+
.add('name', value='value')
116+
.add('name', value='value')
117+
.add('name', value='value')
118+
.add('name', value='value')
119+
.add('name', value='value')
120+
.add('name', value='value')
121+
.add('name', value='value')
122+
.add('name', value='value')
123+
.add('name', value='value')
124+
.add('name', value='value')
125+
.add('name', value='value')
126+
.add('name', value='value')
127+
.add('name', value='value')
128+
.add('name', value='value')
129+
.add('name', value='value')
130+
.add('name', value='value')
131+
.add('name', value='value')
132+
.add('name', value='value')
133+
.add('name', value='value')
134+
.add('name', value='value')
135+
.add('name', value='value')
136+
.add('name', value='value')
137+
.add('name', value='value')
138+
.add('name', value='value')
139+
.add('name', value='value')
140+
.add('name', value='value')
141+
.add('name', value='value')
142+
.add('name', value='value')
143+
.add('name', value='value')
144+
.add('name', value='value')
145+
.add('name', value='value')
146+
.add('name', value='value')
147+
.add('name', value='value')
148+
.add('name', value='value')
149+
.add('name', value='value')
150+
.add('name', value='value')
151+
.add('name', value='value')
152+
.add('name', value='value')
153+
.add('name', value='value')
154+
.add('name', value='value')
155+
.add('name', value='value')
156+
.add('name', value='value')
157+
.add('name', value='value')
158+
.add('name', value='value')
159+
.add('name', value='value')
160+
.add('name', value='value')
161+
.add('name', value='value')
162+
.add('name', value='value')
163+
.add('name', value='value')
164+
.add('name', value='value')
165+
.add('name', value='value')
166+
.add('name', value='value')
167+
.add('name', value='value')
168+
.add('name', value='value')
169+
.Build()
170+
)"""

0 commit comments

Comments
 (0)