Skip to content

Commit 7471086

Browse files
Fix crash on Super.getattr for previously uninferable attributes (#1370)
Co-authored-by: Pierre Sassoulas <[email protected]>
1 parent 7cde999 commit 7471086

File tree

7 files changed

+194
-4
lines changed

7 files changed

+194
-4
lines changed

ChangeLog

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ Release date: TBA
99
* Fixed builtin inferenence on `property` calls not calling the `postinit` of the new node, which
1010
resulted in instance arguments missing on these nodes.
1111

12+
* Fixed a crash on ``Super.getattr`` when the attribute was previously uninferable due to a cache
13+
limit size. This limit can be hit when the inheritance pattern of a class (and therefore of the
14+
``__init__`` attribute) is very large.
15+
16+
Closes PyCQA/pylint#5679
17+
1218
What's New in astroid 2.9.4?
1319
============================
1420
Release date: TBA

astroid/nodes/node_ng.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,9 @@ def infer(self, context=None, **kwargs):
163163
limit = AstroidManager().max_inferable_values
164164
for i, result in enumerate(generator):
165165
if i >= limit or (context.nodes_inferred > context.max_inferred):
166-
yield util.Uninferable
166+
uninferable = util.Uninferable
167+
results.append(uninferable)
168+
yield uninferable
167169
break
168170
results.append(result)
169171
yield result

tests/resources.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@
1313

1414
import os
1515
import sys
16+
from pathlib import Path
1617
from typing import Optional
1718

1819
from astroid import builder
1920
from astroid.manager import AstroidManager
2021
from astroid.nodes.scoped_nodes import Module
2122

22-
DATA_DIR = os.path.join("testdata", "python3")
23-
RESOURCE_PATH = os.path.join(os.path.dirname(__file__), DATA_DIR, "data")
23+
DATA_DIR = Path("testdata") / "python3"
24+
RESOURCE_PATH = Path(__file__).parent / DATA_DIR / "data"
2425

2526

2627
def find(name: str) -> str:
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""This example is based on sqlalchemy.
2+
3+
See https://github.com/PyCQA/pylint/issues/5679
4+
"""
5+
from other_funcs import FromClause
6+
7+
from .nodes import roles
8+
9+
10+
class HasMemoized(object):
11+
...
12+
13+
14+
class Generative(HasMemoized):
15+
...
16+
17+
18+
class ColumnElement(
19+
roles.ColumnArgumentOrKeyRole,
20+
roles.BinaryElementRole,
21+
roles.OrderByRole,
22+
roles.ColumnsClauseRole,
23+
roles.LimitOffsetRole,
24+
roles.DMLColumnRole,
25+
roles.DDLConstraintColumnRole,
26+
roles.StatementRole,
27+
Generative,
28+
):
29+
...
30+
31+
32+
class FunctionElement(ColumnElement, FromClause):
33+
...
34+
35+
36+
class months_between(FunctionElement):
37+
def __init__(self):
38+
super().__init__()
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
class SQLRole(object):
2+
...
3+
4+
5+
class UsesInspection(object):
6+
...
7+
8+
9+
class AllowsLambdaRole(object):
10+
...
11+
12+
13+
class ColumnArgumentRole(SQLRole):
14+
...
15+
16+
17+
class ColumnArgumentOrKeyRole(ColumnArgumentRole):
18+
...
19+
20+
21+
class ColumnListRole(SQLRole):
22+
...
23+
24+
25+
class ColumnsClauseRole(AllowsLambdaRole, UsesInspection, ColumnListRole):
26+
...
27+
28+
29+
class LimitOffsetRole(SQLRole):
30+
...
31+
32+
33+
class ByOfRole(ColumnListRole):
34+
...
35+
36+
37+
class OrderByRole(AllowsLambdaRole, ByOfRole):
38+
...
39+
40+
41+
class StructuralRole(SQLRole):
42+
...
43+
44+
45+
class ExpressionElementRole(SQLRole):
46+
...
47+
48+
49+
class BinaryElementRole(ExpressionElementRole):
50+
...
51+
52+
53+
class JoinTargetRole(AllowsLambdaRole, UsesInspection, StructuralRole):
54+
...
55+
56+
57+
class FromClauseRole(ColumnsClauseRole, JoinTargetRole):
58+
...
59+
60+
61+
class StrictFromClauseRole(FromClauseRole):
62+
...
63+
64+
65+
class AnonymizedFromClauseRole(StrictFromClauseRole):
66+
...
67+
68+
69+
class ReturnsRowsRole(SQLRole):
70+
...
71+
72+
73+
class StatementRole(SQLRole):
74+
...
75+
76+
77+
class DMLColumnRole(SQLRole):
78+
...
79+
80+
81+
class DDLConstraintColumnRole(SQLRole):
82+
...
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from operator import attrgetter
2+
3+
from .nodes import roles
4+
5+
6+
class HasCacheKey(object):
7+
...
8+
9+
10+
class HasMemoized(object):
11+
...
12+
13+
14+
class MemoizedHasCacheKey(HasCacheKey, HasMemoized):
15+
...
16+
17+
18+
class ClauseElement(MemoizedHasCacheKey):
19+
...
20+
21+
22+
class ReturnsRows(roles.ReturnsRowsRole, ClauseElement):
23+
...
24+
25+
26+
class Selectable(ReturnsRows):
27+
...
28+
29+
30+
class FromClause(roles.AnonymizedFromClauseRole, Selectable):
31+
c = property(attrgetter("columns"))

tests/unittest_regrtest.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@
2424

2525
import pytest
2626

27-
from astroid import MANAGER, Instance, nodes, parse, test_utils
27+
from astroid import MANAGER, Instance, bases, nodes, parse, test_utils
2828
from astroid.builder import AstroidBuilder, extract_node
2929
from astroid.const import PY38_PLUS
30+
from astroid.context import InferenceContext
3031
from astroid.exceptions import InferenceError
3132
from astroid.raw_building import build_module
33+
from astroid.util import Uninferable
3234

3335
from . import resources
3436

@@ -399,5 +401,33 @@ class Another(subclass):
399401
parse(code)
400402

401403

404+
def test_max_inferred_for_complicated_class_hierarchy() -> None:
405+
"""Regression test for a crash reported in https://github.com/PyCQA/pylint/issues/5679.
406+
407+
The class hierarchy of 'sqlalchemy' is so intricate that it becomes uninferable with
408+
the standard max_inferred of 100. We used to crash when this happened.
409+
"""
410+
# Create module and get relevant nodes
411+
module = resources.build_file(
412+
str(resources.RESOURCE_PATH / "max_inferable_limit_for_classes" / "main.py")
413+
)
414+
init_attr_node = module.body[-1].body[0].body[0].value.func
415+
init_object_node = module.body[-1].mro()[-1]["__init__"]
416+
super_node = next(init_attr_node.expr.infer())
417+
418+
# Arbitrarily limit the max number of infered nodes per context
419+
InferenceContext.max_inferred = -1
420+
context = InferenceContext()
421+
422+
# Try to infer 'object.__init__' > because of limit is impossible
423+
for inferred in bases._infer_stmts([init_object_node], context, frame=super):
424+
assert inferred == Uninferable
425+
426+
# Reset inference limit
427+
InferenceContext.max_inferred = 100
428+
# Check that we don't crash on a previously uninferable node
429+
assert super_node.getattr("__init__", context=context)[0] == Uninferable
430+
431+
402432
if __name__ == "__main__":
403433
unittest.main()

0 commit comments

Comments
 (0)