Skip to content

Commit 889a1d0

Browse files
authored
Fix not-callable for attributes that alias NamedTuple (#5537)
1 parent 566a377 commit 889a1d0

File tree

5 files changed

+55
-20
lines changed

5 files changed

+55
-20
lines changed

ChangeLog

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ Release date: TBA
5959
* The ``PyLinter`` class will now be initialized with a ``TextReporter``
6060
as its reporter if none is provided.
6161

62+
* Fix false positive ``not-callable`` with attributes that alias ``NamedTuple``
63+
64+
Partially closes #1730
65+
6266
* Fatal errors now emit a score of 0.0 regardless of whether the linted module
6367
contained any statements
6468

doc/whatsnew/2.13.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ Other Changes
8383
* The ``PyLinter`` class will now be initialized with a ``TextReporter``
8484
as its reporter if none is provided.
8585

86+
* Fix false positive ``not-callable`` with attributes that alias ``NamedTuple``
87+
88+
Partially closes #1730
89+
8690
* The ``testutils`` for unittests now accept ``end_lineno`` and ``end_column``. Tests
8791
without these will trigger a ``DeprecationWarning``.
8892

pylint/checkers/typecheck.py

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1293,24 +1293,8 @@ def visit_call(self, node: nodes.Call) -> None:
12931293
the inferred function's definition
12941294
"""
12951295
called = safe_infer(node.func)
1296-
# only function, generator and object defining __call__ are allowed
1297-
# Ignore instances of descriptors since astroid cannot properly handle them
1298-
# yet
1299-
if called and not called.callable():
1300-
if isinstance(called, astroid.Instance) and (
1301-
not has_known_bases(called)
1302-
or (
1303-
called.parent is not None
1304-
and isinstance(called.scope(), nodes.ClassDef)
1305-
and "__get__" in called.locals
1306-
)
1307-
):
1308-
# Don't emit if we can't make sure this object is callable.
1309-
pass
1310-
else:
1311-
self.add_message("not-callable", node=node, args=node.func.as_string())
1312-
else:
1313-
self._check_uninferable_call(node)
1296+
1297+
self._check_not_callable(node, called)
13141298

13151299
try:
13161300
called, implicit_args, callable_name = _determine_callable(called)
@@ -1570,6 +1554,37 @@ def _check_invalid_sequence_index(self, subscript: nodes.Subscript):
15701554
self.add_message("invalid-sequence-index", node=subscript)
15711555
return None
15721556

1557+
def _check_not_callable(
1558+
self, node: nodes.Call, inferred_call: Optional[nodes.NodeNG]
1559+
) -> None:
1560+
"""Checks to see if the not-callable message should be emitted
1561+
1562+
Only functions, generators and objects defining __call__ are "callable"
1563+
We ignore instances of descriptors since astroid cannot properly handle them yet
1564+
"""
1565+
# Handle uninferable calls
1566+
if not inferred_call or inferred_call.callable():
1567+
self._check_uninferable_call(node)
1568+
return
1569+
1570+
if not isinstance(inferred_call, astroid.Instance):
1571+
self.add_message("not-callable", node=node, args=node.func.as_string())
1572+
return
1573+
1574+
# Don't emit if we can't make sure this object is callable.
1575+
if not has_known_bases(inferred_call):
1576+
return
1577+
1578+
if inferred_call.parent and isinstance(inferred_call.scope(), nodes.ClassDef):
1579+
# Ignore descriptor instances
1580+
if "__get__" in inferred_call.locals:
1581+
return
1582+
# NamedTuple instances are callable
1583+
if inferred_call.qname() == "typing.NamedTuple":
1584+
return
1585+
1586+
self.add_message("not-callable", node=node, args=node.func.as_string())
1587+
15731588
@check_messages("invalid-sequence-index")
15741589
def visit_extslice(self, node: nodes.ExtSlice) -> None:
15751590
if not node.parent or not hasattr(node.parent, "value"):

tests/functional/n/not_callable.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,13 +136,25 @@ def value(self):
136136

137137
CLASS_WITH_PROP = ClassWithProperty().value() # [not-callable]
138138

139-
# Test typing.Namedtuple not callable
139+
# Test typing.Namedtuple is callable
140140
# See: https://github.com/PyCQA/pylint/issues/1295
141141
import typing
142142

143143
Named = typing.NamedTuple("Named", [("foo", int), ("bar", int)])
144144
named = Named(1, 2)
145145

146+
147+
# NamedTuple is callable, even if it aliased to a attribute
148+
# See https://github.com/PyCQA/pylint/issues/1730
149+
class TestNamedTuple:
150+
def __init__(self, field: str) -> None:
151+
self.my_tuple = typing.NamedTuple("Tuple", [(field, int)])
152+
self.item: self.my_tuple
153+
154+
def set_item(self, item: int) -> None:
155+
self.item = self.my_tuple(item)
156+
157+
146158
# Test descriptor call
147159
def func():
148160
pass

tests/functional/n/not_callable.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ not-callable:32:12:32:17::INT is not callable:UNDEFINED
77
not-callable:67:0:67:13::PROP.test is not callable:UNDEFINED
88
not-callable:68:0:68:13::PROP.custom is not callable:UNDEFINED
99
not-callable:137:18:137:45::ClassWithProperty().value is not callable:UNDEFINED
10-
not-callable:190:0:190:16::get_number(10) is not callable:UNDEFINED
10+
not-callable:202:0:202:16::get_number(10) is not callable:UNDEFINED

0 commit comments

Comments
 (0)