summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcpopa <devnull@localhost>2014-07-02 16:07:05 +0300
committercpopa <devnull@localhost>2014-07-02 16:07:05 +0300
commit82255ff411d37b1a47fe98029e7f4e9d6efa85a8 (patch)
tree21b1cf7f84605bb9663036f9c30ad9780b28a752
parenta71c0b48285566da852d5db35cf5698bcad48119 (diff)
downloadpylint-82255ff411d37b1a47fe98029e7f4e9d6efa85a8.tar.gz
Emit 'not-callable' when calling properties. Closes issue #268.
-rw-r--r--ChangeLog2
-rw-r--r--checkers/typecheck.py75
-rw-r--r--test/input/func_typecheck_non_callable_call.py36
-rw-r--r--test/messages/func_typecheck_non_callable_call.txt2
4 files changed, 112 insertions, 3 deletions
diff --git a/ChangeLog b/ChangeLog
index 6e3bce0..fdc27e1 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -18,6 +18,8 @@ ChangeLog for Pylint
* Issue attribute-defined-outside-init for all cases, not just
for the last assignment. Closes issue #262.
+ * Emit 'not-callable' when calling properties. Closes issue #268.
+
2014-04-30 -- 1.2.1
* Restore the ability to specify the init-hook option via the
configuration file, which was accidentally broken in 1.2.0.
diff --git a/checkers/typecheck.py b/checkers/typecheck.py
index 25f7612..c017805 100644
--- a/checkers/typecheck.py
+++ b/checkers/typecheck.py
@@ -21,6 +21,7 @@ import shlex
import astroid
from astroid import InferenceError, NotFoundError, YES, Instance
+from astroid.bases import BUILTINS
from pylint.interfaces import IAstroidChecker
from pylint.checkers import BaseChecker
@@ -293,7 +294,72 @@ accessed. Python regular expressions are accepted.'}
else:
self.add_message('assignment-from-none', node=node)
- @check_messages(*(MSGS.keys()))
+ def _check_uninferable_callfunc(self, node):
+ """
+ Check that the given uninferable CallFunc node does not
+ call an actual function.
+ """
+ if not isinstance(node.func, astroid.Getattr):
+ return
+
+ # Look for properties. First, obtain
+ # the lhs of the Getattr node and search the attribute
+ # there. If that attribute is a property or a subclass of properties,
+ # then most likely it's not callable.
+
+ # TODO: since astroid doesn't understand descriptors very well
+ # we will not handle them here, right now.
+
+ expr = node.func.expr
+ klass = safe_infer(expr)
+ if (klass is None or klass is astroid.YES or
+ not isinstance(klass, astroid.Instance)):
+ return
+
+ try:
+ attrs = klass._proxied.getattr(node.func.attrname)
+ except astroid.NotFoundError:
+ return
+
+ stop_checking = False
+ for attr in attrs:
+ if attr is astroid.YES:
+ continue
+ if stop_checking:
+ break
+ if not isinstance(attr, astroid.Function):
+ continue
+
+ # Decorated, see if it is decorated with a property
+ if not attr.decorators:
+ continue
+ for decorator in attr.decorators.nodes:
+ if not isinstance(decorator, astroid.Name):
+ continue
+ try:
+ for infered in decorator.infer():
+ property_like = False
+ if isinstance(infered, astroid.Class):
+ if (infered.root().name == BUILTINS and
+ infered.name == 'property'):
+ property_like = True
+ else:
+ for ancestor in infered.ancestors():
+ if (ancestor.name == 'property' and
+ ancestor.root().name == BUILTINS):
+ property_like = True
+ break
+ if property_like:
+ self.add_message('not-callable', node=node,
+ args=node.func.as_string())
+ stop_checking = True
+ break
+ except InferenceError:
+ pass
+ if stop_checking:
+ break
+
+ @check_messages(*(list(MSGS.keys())))
def visit_callfunc(self, node):
"""check that called functions/methods are inferred to callable objects,
and that the arguments passed to the function match the parameters in
@@ -315,12 +381,15 @@ accessed. Python regular expressions are accepted.'}
called = safe_infer(node.func)
# only function, generator and object defining __call__ are allowed
if called is not None and not called.callable():
- self.add_message('not-callable', node=node, args=node.func.as_string())
+ self.add_message('not-callable', node=node,
+ args=node.func.as_string())
+
+ self._check_uninferable_callfunc(node)
try:
called, implicit_args, callable_name = _determine_callable(called)
except ValueError:
- # Any error occurred during determining the function type, most of
+ # Any error occurred during determining the function type, most of
# those errors are handled by different warnings.
return
num_positional_args += implicit_args
diff --git a/test/input/func_typecheck_non_callable_call.py b/test/input/func_typecheck_non_callable_call.py
index 8d8a6c2..dc3e783 100644
--- a/test/input/func_typecheck_non_callable_call.py
+++ b/test/input/func_typecheck_non_callable_call.py
@@ -35,3 +35,39 @@ TUPLE = ()
INCORRECT = TUPLE()
INT = 1
INCORRECT = INT()
+
+# Test calling properties. Pylint can detect when using only the
+# getter, but it doesn't infer properly when having a getter
+# and a setter.
+class MyProperty(property):
+ """ test subclasses """
+
+class PropertyTest:
+ """ class """
+
+ def __init__(self):
+ self.attr = 4
+
+ @property
+ def test(self):
+ """ Get the attribute """
+ return self.attr
+
+ @test.setter
+ def test(self, value):
+ """ Set the attribute """
+ self.attr = value
+
+ @MyProperty
+ def custom(self):
+ """ Get the attribute """
+ return self.attr
+
+ @custom.setter
+ def custom(self, value):
+ """ Set the attribute """
+ self.attr = value
+
+PROP = PropertyTest()
+PROP.test(40)
+PROP.custom()
diff --git a/test/messages/func_typecheck_non_callable_call.txt b/test/messages/func_typecheck_non_callable_call.txt
index 0218074..8baa237 100644
--- a/test/messages/func_typecheck_non_callable_call.txt
+++ b/test/messages/func_typecheck_non_callable_call.txt
@@ -4,3 +4,5 @@ E: 31: LIST is not callable
E: 33: DICT is not callable
E: 35: TUPLE is not callable
E: 37: INT is not callable
+E: 72: PROP.test is not callable
+E: 73: PROP.custom is not callable \ No newline at end of file