diff options
author | Claudiu Popa <pcmanticore@gmail.com> | 2014-02-19 12:56:58 +0200 |
---|---|---|
committer | Claudiu Popa <pcmanticore@gmail.com> | 2014-02-19 12:56:58 +0200 |
commit | bd895c23c528ec3a7e1b917c97b349a2070b2ad2 (patch) | |
tree | 29ed45a11e2cd75e2ce0b2b3fc9c06b34cefae8b | |
parent | 9776d39734c50cdf0a4c267b6327048873486d33 (diff) | |
parent | d5ecbc1ae90c03505f904c48385137ddf16dbd83 (diff) | |
download | pylint-bd895c23c528ec3a7e1b917c97b349a2070b2ad2.tar.gz |
Merged in PCManticore/pylint/abc (pull request #90)
Add abstract-class-instantiated warning
-rw-r--r-- | ChangeLog | 4 | ||||
-rw-r--r-- | DEPENDS | 2 | ||||
-rw-r--r-- | __pkginfo__.py | 2 | ||||
-rw-r--r-- | checkers/base.py | 56 | ||||
-rw-r--r-- | debian/control | 2 | ||||
-rw-r--r-- | test/input/func_abstract_class_instantiated_py30.py | 43 | ||||
-rw-r--r-- | test/input/func_abstract_class_instantiated_py34.py | 55 | ||||
-rw-r--r-- | test/messages/func_abstract_class_instantiated_py30.txt | 2 | ||||
-rw-r--r-- | test/messages/func_abstract_class_instantiated_py34.txt | 3 |
9 files changed, 165 insertions, 4 deletions
@@ -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 @@ -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 |