From c1a622340037b27533085ece42478e85e2e7e395 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Tue, 16 Dec 2014 14:16:40 +0200 Subject: Proper abstract method lookup while checking for abstract-class-instantiated. This depends on the new Class.mro method, which will be added in astroid 1.3.3. Closes issue #401. --- ChangeLog | 3 ++ checkers/base.py | 33 +++++++++++++- test/functional/abstract_class_instantiated_py3.py | 50 ++++++++++++++++++++++ .../functional/abstract_class_instantiated_py3.txt | 7 +-- 4 files changed, 88 insertions(+), 5 deletions(-) diff --git a/ChangeLog b/ChangeLog index d25a775..6e13e04 100644 --- a/ChangeLog +++ b/ChangeLog @@ -11,6 +11,9 @@ ChangeLog for Pylint * Warn about using the input() or round() built-ins for Python 3. Closes issue #411. + * Proper abstract method lookup while checking for + abstract-class-instantiated. Closes issue #401. + 2014-11-23 -- 1.4.0 diff --git a/checkers/base.py b/checkers/base.py index 17e6713..60bc096 100644 --- a/checkers/base.py +++ b/checkers/base.py @@ -197,8 +197,37 @@ 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.methods()) + visited = set() + try: + mro = reversed(node.mro()) + except NotImplementedError: + # Old style class, it will not have a mro. + return False + for ancestor in mro: + for obj in ancestor.values(): + infered = obj + if isinstance(obj, astroid.AssName): + infered = safe_infer(obj) + if not infered: + continue + if not isinstance(infered, astroid.Function): + if obj.name in visited: + visited.remove(obj.name) + if isinstance(infered, astroid.Function): + # It's critical to use the original name, + # since after inferring, an object can be something + # else than expected, as in the case of the + # following assignment. + # + # class A: + # def keys(self): pass + # __iter__ = keys + abstract = _decorated_with_abc(infered) + if abstract: + visited.add(obj.name) + elif not abstract and obj.name in visited: + visited.remove(obj.name) + return len(visited) > 0 def report_by_type_stats(sect, stats, old_stats): """make a report of diff --git a/test/functional/abstract_class_instantiated_py3.py b/test/functional/abstract_class_instantiated_py3.py index 146666e..8d94733 100644 --- a/test/functional/abstract_class_instantiated_py3.py +++ b/test/functional/abstract_class_instantiated_py3.py @@ -10,6 +10,7 @@ abstract methods. __revision__ = 0 import abc +import weakref class GoodClass(object, metaclass=abc.ABCMeta): pass @@ -38,11 +39,60 @@ class ThirdBadClass(SecondBadClass): pass +class Structure(object, metaclass=abc.ABCMeta): + @abc.abstractmethod + def __iter__(self): + pass + @abc.abstractmethod + def __len__(self): + pass + @abc.abstractmethod + def __contains__(self): + pass + @abc.abstractmethod + def __hash__(self): + pass + +class Container(Structure): + def __contains__(self): + pass + +class Sizable(Structure): + def __len__(self): + pass + +class Hashable(Structure): + __hash__ = 42 + + +class Iterator(Structure): + def keys(self): # pylint: disable=no-self-use + return iter([1, 2, 3]) + + __iter__ = keys + +class AbstractSizable(Structure): + @abc.abstractmethod + def length(self): + pass + __len__ = length + +class NoMroAbstractMethods(Container, Iterator, Sizable, Hashable): + pass + +class BadMroAbstractMethods(Container, Iterator, AbstractSizable): + pass + def main(): """ do nothing """ GoodClass() SecondGoodClass() ThirdGoodClass() + weakref.WeakKeyDictionary() + weakref.WeakValueDictionary() + NoMroAbstractMethods() + + BadMroAbstractMethods() # [abstract-class-instantiated] BadClass() # [abstract-class-instantiated] SecondBadClass() # [abstract-class-instantiated] ThirdBadClass() # [abstract-class-instantiated] diff --git a/test/functional/abstract_class_instantiated_py3.txt b/test/functional/abstract_class_instantiated_py3.txt index 3c4f0c2..db5bef6 100644 --- a/test/functional/abstract_class_instantiated_py3.txt +++ b/test/functional/abstract_class_instantiated_py3.txt @@ -1,3 +1,4 @@ -abstract-class-instantiated:46:main:Abstract class with abstract methods instantiated -abstract-class-instantiated:47:main:Abstract class with abstract methods instantiated -abstract-class-instantiated:48:main:Abstract class with abstract methods instantiated \ No newline at end of file +abstract-class-instantiated:95:main:Abstract class with abstract methods instantiated +abstract-class-instantiated:96:main:Abstract class with abstract methods instantiated +abstract-class-instantiated:97:main:Abstract class with abstract methods instantiated +abstract-class-instantiated:98:main:Abstract class with abstract methods instantiated -- cgit v1.2.1