summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcpopa <devnull@localhost>2014-02-09 23:20:25 +0200
committercpopa <devnull@localhost>2014-02-09 23:20:25 +0200
commit06c9546e976a9344b7f9efa2ffa802183a0c6c86 (patch)
treef3edaa8628cf518011e33fe343d571e9b4ca79b2
parent9776d39734c50cdf0a4c267b6327048873486d33 (diff)
downloadpylint-06c9546e976a9344b7f9efa2ffa802183a0c6c86.tar.gz
Add abstract-class-instantiated warning.
-rw-r--r--checkers/base.py45
-rw-r--r--test/input/func_abstract_class_instantiated_py30.py43
-rw-r--r--test/messages/func_abstract_class_instantiated_py30.txt2
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