diff options
author | cpopa <devnull@localhost> | 2014-02-09 23:20:25 +0200 |
---|---|---|
committer | cpopa <devnull@localhost> | 2014-02-09 23:20:25 +0200 |
commit | 06c9546e976a9344b7f9efa2ffa802183a0c6c86 (patch) | |
tree | f3edaa8628cf518011e33fe343d571e9b4ca79b2 | |
parent | 9776d39734c50cdf0a4c267b6327048873486d33 (diff) | |
download | pylint-06c9546e976a9344b7f9efa2ffa802183a0c6c86.tar.gz |
Add abstract-class-instantiated warning.
-rw-r--r-- | checkers/base.py | 45 | ||||
-rw-r--r-- | test/input/func_abstract_class_instantiated_py30.py | 43 | ||||
-rw-r--r-- | test/messages/func_abstract_class_instantiated_py30.txt | 2 |
3 files changed, 89 insertions, 1 deletions
diff --git a/checkers/base.py b/checkers/base.py index 55e5f36..216fc99 100644 --- a/checkers/base.py +++ b/checkers/base.py @@ -19,7 +19,7 @@ import sys import astroid from logilab.common.ureports import Table -from astroid import are_exclusive +from astroid import are_exclusive, InferenceError import astroid.bases from pylint.interfaces import IAstroidChecker @@ -101,6 +101,8 @@ if sys.version_info < (3, 0): PROPERTY_CLASSES = set(('__builtin__.property', 'abc.abstractproperty')) else: PROPERTY_CLASSES = set(('builtins.property', 'abc.abstractproperty')) +ABC_METHODS = set(('abc.abstractproperty', 'abc.abstractmethod', + 'abc.abstractclassmethod', 'abc.abstractstaticmethod')) def _determine_function_name_type(node): """Determine the name type whose regex the a function's name should match. @@ -130,6 +132,18 @@ def _determine_function_name_type(node): return 'attr' return 'method' +def decorated_with_abc(func): + """ Determine if the `func` node is decorated + with `abc` decorators (abstractmethod et co.) + """ + if func.decorators: + for node in func.decorators.nodes: + try: + infered = next(node.infer()) + except InferenceError: + continue + if infered and infered.qname() in ABC_METHODS: + return True def report_by_type_stats(sect, stats, old_stats): """make a report of @@ -229,6 +243,11 @@ class BasicErrorChecker(_BasicChecker): 'duplicate-argument-name', 'Duplicate argument names in function definitions are syntax' ' errors.'), + 'E0110': ('Abstract class with abstract methods instantiated', + 'abstract-class-instantiated', + 'Used when an abstract class with `abc.ABCMeta` as metaclass ' + 'has abstract methods and is instantiated.', + {'minversion': (3, 0)}), 'W0120': ('Else clause on loop without a break statement', 'useless-else-on-loop', 'Loops should only have an else clause if they can exit early ' @@ -314,6 +333,30 @@ class BasicErrorChecker(_BasicChecker): (node.operand.op == node.op)): self.add_message('nonexistent-operator', node=node, args=node.op*2) + @check_messages('abstract-class-instantiated') + def visit_callfunc(self, node): + """ Check instantiating abstract class with + abc.ABCMeta as metaclass. + """ + if not isinstance(node.func, astroid.Name): + return + try: + infered = node.func.infer().next() + except astroid.InferenceError: + return + if not isinstance(infered, astroid.Class): + return + # __init__ was called + metaclass = infered.metaclass() + if metaclass is None: + return + if (metaclass.name == 'ABCMeta' and + metaclass.parent and + metaclass.parent.name == 'abc' and + any(decorated_with_abc(meth) + for meth in infered.mymethods())): + self.add_message('abstract-class-instantiated', node=node) + def _check_else_on_loop(self, node): """Check that any loop with an else clause has a break statement.""" if node.orelse and not _loop_exits_early(node): diff --git a/test/input/func_abstract_class_instantiated_py30.py b/test/input/func_abstract_class_instantiated_py30.py new file mode 100644 index 0000000..4bcfd3f --- /dev/null +++ b/test/input/func_abstract_class_instantiated_py30.py @@ -0,0 +1,43 @@ +"""Check that instantiating a class with +`abc.ABCMeta` as metaclass fails if it defines +abstract methods. +""" + +# pylint: disable=too-few-public-methods, missing-docstring, abstract-class-not-used + +__revision__ = 0 + +import abc +
+class GoodClass(object, metaclass=abc.ABCMeta):
+ pass
+
+class SecondGoodClass(object, metaclass=abc.ABCMeta):
+ def test(self):
+ """ do nothing. """
+ pass
+
+class ThirdGoodClass(object, metaclass=abc.ABCMeta):
+ """ This should not raise the warning. """
+ def test(self):
+ raise NotImplementedError()
+
+class BadClass(object, metaclass=abc.ABCMeta):
+ @abc.abstractmethod
+ def test(self):
+ """ do nothing. """
+ pass
+
+class SecondBadClass(object, metaclass=abc.ABCMeta):
+ @property
+ @abc.abstractmethod
+ def test(self):
+ """ do nothing. """
+
+def main():
+ """ do nothing """
+ GoodClass()
+ SecondGoodClass()
+ ThirdGoodClass()
+ BadClass()
+ SecondBadClass()
diff --git a/test/messages/func_abstract_class_instantiated_py30.txt b/test/messages/func_abstract_class_instantiated_py30.txt new file mode 100644 index 0000000..28dc11e --- /dev/null +++ b/test/messages/func_abstract_class_instantiated_py30.txt @@ -0,0 +1,2 @@ +E: 42:main: Abstract class with abstract methods instantiated
+E: 43:main: Abstract class with abstract methods instantiated
\ No newline at end of file |