diff options
-rw-r--r-- | ChangeLog | 2 | ||||
-rw-r--r-- | checkers/typecheck.py | 75 | ||||
-rw-r--r-- | test/input/func_typecheck_non_callable_call.py | 36 | ||||
-rw-r--r-- | test/messages/func_typecheck_non_callable_call.txt | 2 |
4 files changed, 112 insertions, 3 deletions
@@ -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 |