Skip to content

Commit 18cb026

Browse files
mbyrnepr2Pierre-SassoulasDanielNoord
committed
Add an exception for IndexError inside uninferable_final_decorator (#6532)
Co-authored-by: Pierre Sassoulas <[email protected]> Co-authored-by: Daniël van Noord <[email protected]>
1 parent 45cbae2 commit 18cb026

File tree

4 files changed

+55
-12
lines changed

4 files changed

+55
-12
lines changed

ChangeLog

+3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ What's New in Pylint 2.13.9?
2020
============================
2121
Release date: TBA
2222

23+
* Fix ``IndexError`` crash in ``uninferable_final_decorators`` method.
24+
25+
Relates to #6531
2326

2427

2528
What's New in Pylint 2.13.8?

doc/whatsnew/2.13.rst

+4
Original file line numberDiff line numberDiff line change
@@ -639,3 +639,7 @@ Other Changes
639639
``open``
640640

641641
Closes #6414
642+
643+
* Fix ``IndexError`` crash in ``uninferable_final_decorators`` method.
644+
645+
Relates to #6531

pylint/checkers/utils.py

+18-12
Original file line numberDiff line numberDiff line change
@@ -820,28 +820,34 @@ def uninferable_final_decorators(
820820
"""
821821
decorators = []
822822
for decorator in getattr(node, "nodes", []):
823+
import_nodes: tuple[nodes.Import | nodes.ImportFrom] | None = None
824+
825+
# Get the `Import` node. The decorator is of the form: @module.name
823826
if isinstance(decorator, nodes.Attribute):
824-
try:
825-
import_node = decorator.expr.lookup(decorator.expr.name)[1][0]
826-
except AttributeError:
827-
continue
827+
inferred = safe_infer(decorator.expr)
828+
if isinstance(inferred, nodes.Module) and inferred.qname() == "typing":
829+
_, import_nodes = decorator.expr.lookup(decorator.expr.name)
830+
831+
# Get the `ImportFrom` node. The decorator is of the form: @name
828832
elif isinstance(decorator, nodes.Name):
829-
lookup_values = decorator.lookup(decorator.name)
830-
if lookup_values[1]:
831-
import_node = lookup_values[1][0]
832-
else:
833-
continue # pragma: no cover # Covered on Python < 3.8
834-
else:
833+
_, import_nodes = decorator.lookup(decorator.name)
834+
835+
# The `final` decorator is expected to be found in the
836+
# import_nodes. Continue if we don't find any `Import` or `ImportFrom`
837+
# nodes for this decorator.
838+
if not import_nodes:
835839
continue
840+
import_node = import_nodes[0]
836841

837842
if not isinstance(import_node, (astroid.Import, astroid.ImportFrom)):
838843
continue
839844

840845
import_names = dict(import_node.names)
841846

842-
# from typing import final
847+
# Check if the import is of the form: `from typing import final`
843848
is_from_import = ("final" in import_names) and import_node.modname == "typing"
844-
# import typing
849+
850+
# Check if the import is of the form: `import typing`
845851
is_import = ("typing" in import_names) and getattr(
846852
decorator, "attrname", None
847853
) == "final"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""Regression test for https://github.com/PyCQA/pylint/issues/6531."""
2+
3+
# pylint: disable=missing-docstring, redefined-outer-name
4+
5+
import pytest
6+
7+
8+
class Wallet:
9+
def __init__(self):
10+
self.balance = 0
11+
12+
def add_cash(self, earned):
13+
self.balance += earned
14+
15+
def spend_cash(self, spent):
16+
self.balance -= spent
17+
18+
@pytest.fixture
19+
def my_wallet():
20+
'''Returns a Wallet instance with a zero balance'''
21+
return Wallet()
22+
23+
@pytest.mark.parametrize("earned,spent,expected", [
24+
(30, 10, 20),
25+
(20, 2, 18),
26+
])
27+
def test_transactions(my_wallet, earned, spent, expected):
28+
my_wallet.add_cash(earned)
29+
my_wallet.spend_cash(spent)
30+
assert my_wallet.balance == expected

0 commit comments

Comments
 (0)