diff options
-rw-r--r-- | ChangeLog | 4 | ||||
-rw-r--r-- | doc/whatsnew/2.13.rst | 4 | ||||
-rw-r--r-- | pylint/checkers/typecheck.py | 51 | ||||
-rw-r--r-- | tests/functional/n/not_callable.py | 14 | ||||
-rw-r--r-- | tests/functional/n/not_callable.txt | 2 |
5 files changed, 55 insertions, 20 deletions
@@ -59,6 +59,10 @@ Release date: TBA * The ``PyLinter`` class will now be initialized with a ``TextReporter`` as its reporter if none is provided. +* Fix false positive ``not-callable`` with attributes that alias ``NamedTuple`` + + Partially closes #1730 + * Fatal errors now emit a score of 0.0 regardless of whether the linted module contained any statements diff --git a/doc/whatsnew/2.13.rst b/doc/whatsnew/2.13.rst index 72f071510..ef72d5855 100644 --- a/doc/whatsnew/2.13.rst +++ b/doc/whatsnew/2.13.rst @@ -83,6 +83,10 @@ Other Changes * The ``PyLinter`` class will now be initialized with a ``TextReporter`` as its reporter if none is provided. +* Fix false positive ``not-callable`` with attributes that alias ``NamedTuple`` + + Partially closes #1730 + * The ``testutils`` for unittests now accept ``end_lineno`` and ``end_column``. Tests without these will trigger a ``DeprecationWarning``. diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index df01cc2fe..c3e76228d 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -1293,24 +1293,8 @@ accessed. Python regular expressions are accepted.", the inferred function's definition """ called = safe_infer(node.func) - # only function, generator and object defining __call__ are allowed - # Ignore instances of descriptors since astroid cannot properly handle them - # yet - if called and not called.callable(): - if isinstance(called, astroid.Instance) and ( - not has_known_bases(called) - or ( - called.parent is not None - and isinstance(called.scope(), nodes.ClassDef) - and "__get__" in called.locals - ) - ): - # Don't emit if we can't make sure this object is callable. - pass - else: - self.add_message("not-callable", node=node, args=node.func.as_string()) - else: - self._check_uninferable_call(node) + + self._check_not_callable(node, called) try: called, implicit_args, callable_name = _determine_callable(called) @@ -1570,6 +1554,37 @@ accessed. Python regular expressions are accepted.", self.add_message("invalid-sequence-index", node=subscript) return None + def _check_not_callable( + self, node: nodes.Call, inferred_call: Optional[nodes.NodeNG] + ) -> None: + """Checks to see if the not-callable message should be emitted + + Only functions, generators and objects defining __call__ are "callable" + We ignore instances of descriptors since astroid cannot properly handle them yet + """ + # Handle uninferable calls + if not inferred_call or inferred_call.callable(): + self._check_uninferable_call(node) + return + + if not isinstance(inferred_call, astroid.Instance): + self.add_message("not-callable", node=node, args=node.func.as_string()) + return + + # Don't emit if we can't make sure this object is callable. + if not has_known_bases(inferred_call): + return + + if inferred_call.parent and isinstance(inferred_call.scope(), nodes.ClassDef): + # Ignore descriptor instances + if "__get__" in inferred_call.locals: + return + # NamedTuple instances are callable + if inferred_call.qname() == "typing.NamedTuple": + return + + self.add_message("not-callable", node=node, args=node.func.as_string()) + @check_messages("invalid-sequence-index") def visit_extslice(self, node: nodes.ExtSlice) -> None: if not node.parent or not hasattr(node.parent, "value"): diff --git a/tests/functional/n/not_callable.py b/tests/functional/n/not_callable.py index a7c59ae9c..31d364b88 100644 --- a/tests/functional/n/not_callable.py +++ b/tests/functional/n/not_callable.py @@ -136,13 +136,25 @@ class ClassWithProperty: CLASS_WITH_PROP = ClassWithProperty().value() # [not-callable] -# Test typing.Namedtuple not callable +# Test typing.Namedtuple is callable # See: https://github.com/PyCQA/pylint/issues/1295 import typing Named = typing.NamedTuple("Named", [("foo", int), ("bar", int)]) named = Named(1, 2) + +# NamedTuple is callable, even if it aliased to a attribute +# See https://github.com/PyCQA/pylint/issues/1730 +class TestNamedTuple: + def __init__(self, field: str) -> None: + self.my_tuple = typing.NamedTuple("Tuple", [(field, int)]) + self.item: self.my_tuple + + def set_item(self, item: int) -> None: + self.item = self.my_tuple(item) + + # Test descriptor call def func(): pass diff --git a/tests/functional/n/not_callable.txt b/tests/functional/n/not_callable.txt index f079bbfe8..e8a06b003 100644 --- a/tests/functional/n/not_callable.txt +++ b/tests/functional/n/not_callable.txt @@ -7,4 +7,4 @@ not-callable:32:12:32:17::INT is not callable:UNDEFINED not-callable:67:0:67:13::PROP.test is not callable:UNDEFINED not-callable:68:0:68:13::PROP.custom is not callable:UNDEFINED not-callable:137:18:137:45::ClassWithProperty().value is not callable:UNDEFINED -not-callable:190:0:190:16::get_number(10) is not callable:UNDEFINED +not-callable:202:0:202:16::get_number(10) is not callable:UNDEFINED |