summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClaudiu Popa <pcmanticore@gmail.com>2014-02-19 12:56:58 +0200
committerClaudiu Popa <pcmanticore@gmail.com>2014-02-19 12:56:58 +0200
commitbd895c23c528ec3a7e1b917c97b349a2070b2ad2 (patch)
tree29ed45a11e2cd75e2ce0b2b3fc9c06b34cefae8b
parent9776d39734c50cdf0a4c267b6327048873486d33 (diff)
parentd5ecbc1ae90c03505f904c48385137ddf16dbd83 (diff)
downloadpylint-bd895c23c528ec3a7e1b917c97b349a2070b2ad2.tar.gz
Merged in PCManticore/pylint/abc (pull request #90)
Add abstract-class-instantiated warning
-rw-r--r--ChangeLog4
-rw-r--r--DEPENDS2
-rw-r--r--__pkginfo__.py2
-rw-r--r--checkers/base.py56
-rw-r--r--debian/control2
-rw-r--r--test/input/func_abstract_class_instantiated_py30.py43
-rw-r--r--test/input/func_abstract_class_instantiated_py34.py55
-rw-r--r--test/messages/func_abstract_class_instantiated_py30.txt2
-rw-r--r--test/messages/func_abstract_class_instantiated_py34.txt3
9 files changed, 165 insertions, 4 deletions
diff --git a/ChangeLog b/ChangeLog
index e20d609..1095347 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -23,6 +23,10 @@ ChangeLog for Pylint
* Emit 'undefined-all-variable' if a package's __all__
variable contains a missing submodule (closes #126).
+ * Add a new warning 'abstract-class-instantiated' for checking
+ that abstract classes created with `abc` module and
+ with abstract methods are instantied.
+
2013-12-22 -- 1.1.0
* Add new check for use of deprecated pragma directives "pylint:disable-msg"
or "pylint:enable-msg" (I0022, deprecated-pragma) which was previously
diff --git a/DEPENDS b/DEPENDS
index 630cec8..e171ef3 100644
--- a/DEPENDS
+++ b/DEPENDS
@@ -1,3 +1,3 @@
python-logilab-common (>= 0.19.0)
-python-astroid
+python-astroid (>= 1.0.1)
python-tk
diff --git a/__pkginfo__.py b/__pkginfo__.py
index eff50aa..8d4d65e 100644
--- a/__pkginfo__.py
+++ b/__pkginfo__.py
@@ -21,7 +21,7 @@ modname = distname = 'pylint'
numversion = (1, 1, 0)
version = '.'.join([str(num) for num in numversion])
-install_requires = ['logilab-common >= 0.53.0', 'astroid >= 0.24.3']
+install_requires = ['logilab-common >= 0.53.0', 'astroid >= 1.0.1']
license = 'GPL'
description = "python code static checker"
diff --git a/checkers/base.py b/checkers/base.py
index 55e5f36..6795457 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,25 @@ 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 = node.infer().next()
+ except InferenceError:
+ continue
+ if infered and infered.qname() in ABC_METHODS:
+ return True
+
+def has_abstract_methods(node):
+ """ Determine if the given `node` has
+ abstract methods, defined with `abc` module.
+ """
+ return any(decorated_with_abc(meth)
+ for meth in node.mymethods())
def report_by_type_stats(sect, stats, old_stats):
"""make a report of
@@ -229,6 +250,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 +340,34 @@ 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.
+ """
+ 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:
+ # Python 3.4 has `abc.ABC`, which won't be detected
+ # by ClassNode.metaclass()
+ for ancestor in infered.ancestors():
+ if (ancestor.qname() == 'abc.ABC' and
+ has_abstract_methods(infered)):
+
+ self.add_message('abstract-class-instantiated', node=node)
+ break
+ return
+ if (metaclass.qname() == 'abc.ABCMeta' and
+ has_abstract_methods(infered)):
+
+ 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/debian/control b/debian/control
index ecc265b..313445a 100644
--- a/debian/control
+++ b/debian/control
@@ -19,7 +19,7 @@ Architecture: all
Depends: ${python:Depends},
${misc:Depends},
python-logilab-common (>= 0.53.0),
- python-astroid
+ python-astroid (>= 1.0.1)
Suggests: python-tk
XB-Python-Version: ${python:Versions}
Description: python code static checker and UML diagram generator
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/input/func_abstract_class_instantiated_py34.py b/test/input/func_abstract_class_instantiated_py34.py
new file mode 100644
index 0000000..1814f34
--- /dev/null
+++ b/test/input/func_abstract_class_instantiated_py34.py
@@ -0,0 +1,55 @@
+"""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, no-init
+
+__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 FourthGoodClass(abc.ABC):
+ """ Neither this. """
+ def test(self):
+ pass
+
+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. """
+
+class ThirdBadClass(abc.ABC):
+ @abc.abstractmethod
+ def test(self):
+ pass
+
+def main():
+ """ do nothing """
+ GoodClass()
+ SecondGoodClass()
+ ThirdGoodClass()
+ FourthGoodClass()
+ BadClass()
+ SecondBadClass()
+ ThirdBadClass()
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
diff --git a/test/messages/func_abstract_class_instantiated_py34.txt b/test/messages/func_abstract_class_instantiated_py34.txt
new file mode 100644
index 0000000..310f8ea
--- /dev/null
+++ b/test/messages/func_abstract_class_instantiated_py34.txt
@@ -0,0 +1,3 @@
+E: 53:main: Abstract class with abstract methods instantiated
+E: 54:main: Abstract class with abstract methods instantiated
+E: 55:main: Abstract class with abstract methods instantiated \ No newline at end of file