diff --git a/ChangeLog b/ChangeLog index 3584a63100..62dd0f1ae6 100644 --- a/ChangeLog +++ b/ChangeLog @@ -2,13 +2,24 @@ Pylint's ChangeLog ------------------ -What's New in Pylint 2.8.2? +What's New in Pylint 2.8.3? =========================== Release date: 2021-04-26 .. Put new features and bugfixes here and also in 'doc/whatsnew/2.9.rst' +* Fix false-positive ``too-many-ancestors`` when inheriting from builtin classes, + especially from the ``collections.abc`` module + + Closes #4166 + Closes #4415 + + +What's New in Pylint 2.8.2? +=========================== +Release date: 2021-04-26 + * Keep ``__pkginfo__.numversion`` a tuple to avoid breaking pylint-django. Closes #4405 @@ -22,9 +33,6 @@ What's New in Pylint 2.8.1? =========================== Release date: 2021-04-25 -.. - Put new features and bugfixes here and also in 'doc/whatsnew/2.9.rst' - * Add numversion back (temporarily) in ``__pkginfo__`` because it broke Pylama and revert the unnecessary ``pylint.version`` breaking change. diff --git a/doc/whatsnew/2.9.rst b/doc/whatsnew/2.9.rst index 1fdc1a837a..68daffacbc 100644 --- a/doc/whatsnew/2.9.rst +++ b/doc/whatsnew/2.9.rst @@ -16,3 +16,6 @@ Other Changes ============= * Pylint's tags are now the standard form ``vX.Y.Z`` and not ``pylint-X.Y.Z`` anymore. + +* Fix false-positive ``too-many-ancestors`` when inheriting from builtin classes, + especially from the ``collections.abc`` module diff --git a/pylint/checkers/design_analysis.py b/pylint/checkers/design_analysis.py index da51cf70ca..5fea8035c0 100644 --- a/pylint/checkers/design_analysis.py +++ b/pylint/checkers/design_analysis.py @@ -26,6 +26,7 @@ from collections import defaultdict import astroid +from astroid import nodes from pylint import utils from pylint.checkers import BaseChecker @@ -97,6 +98,87 @@ TYPING_NAMEDTUPLE = "typing.NamedTuple" TYPING_TYPEDDICT = "typing.TypedDict" +# Set of stdlib classes to ignore when calculating number of ancestors +STDLIB_CLASSES_IGNORE_ANCESTOR = frozenset( + ( + "builtins.object", + "builtins.tuple", + "builtins.dict", + "builtins.list", + "builtins.set", + "bulitins.frozenset", + "collections.ChainMap", + "collections.Counter", + "collections.OrderedDict", + "collections.UserDict", + "collections.UserList", + "collections.UserString", + "collections.defaultdict", + "collections.deque", + "collections.namedtuple", + "_collections_abc.Awaitable", + "_collections_abc.Coroutine", + "_collections_abc.AsyncIterable", + "_collections_abc.AsyncIterator", + "_collections_abc.AsyncGenerator", + "_collections_abc.Hashable", + "_collections_abc.Iterable", + "_collections_abc.Iterator", + "_collections_abc.Generator", + "_collections_abc.Reversible", + "_collections_abc.Sized", + "_collections_abc.Container", + "_collections_abc.Collection", + "_collections_abc.Set", + "_collections_abc.MutableSet", + "_collections_abc.Mapping", + "_collections_abc.MutableMapping", + "_collections_abc.MappingView", + "_collections_abc.KeysView", + "_collections_abc.ItemsView", + "_collections_abc.ValuesView", + "_collections_abc.Sequence", + "_collections_abc.MutableSequence", + "_collections_abc.ByteString", + "typing.Tuple", + "typing.List", + "typing.Dict", + "typing.Set", + "typing.FrozenSet", + "typing.Deque", + "typing.DefaultDict", + "typing.OrderedDict", + "typing.Counter", + "typing.ChainMap", + "typing.Awaitable", + "typing.Coroutine", + "typing.AsyncIterable", + "typing.AsyncIterator", + "typing.AsyncGenerator", + "typing.Iterable", + "typing.Iterator", + "typing.Generator", + "typing.Reversible", + "typing.Container", + "typing.Collection", + "typing.AbstractSet", + "typing.MutableSet", + "typing.Mapping", + "typing.MutableMapping", + "typing.Sequence", + "typing.MutableSequence", + "typing.ByteString", + "typing.MappingView", + "typing.KeysView", + "typing.ItemsView", + "typing.ValuesView", + "typing.ContextManager", + "typing.AsyncContextManger", + "typing.Hashable", + "typing.Sized", + ) +) + def _is_exempt_from_public_methods(node: astroid.ClassDef) -> bool: """Check if a class is exempt from too-few-public-methods""" @@ -294,9 +376,13 @@ def _ignored_argument_names(self): "too-few-public-methods", "too-many-public-methods", ) - def visit_classdef(self, node): + def visit_classdef(self, node: nodes.ClassDef): """check size of inheritance hierarchy and number of instance attributes""" - nb_parents = len(list(node.ancestors())) + nb_parents = sum( + 1 + for ancestor in node.ancestors() + if ancestor.qname() not in STDLIB_CLASSES_IGNORE_ANCESTOR + ) if nb_parents > self.config.max_parents: self.add_message( "too-many-ancestors", diff --git a/tests/functional/t/too/too_many_ancestors.py b/tests/functional/t/too/too_many_ancestors.py index a0ae18a026..58b91ec3d8 100644 --- a/tests/functional/t/too/too_many_ancestors.py +++ b/tests/functional/t/too/too_many_ancestors.py @@ -1,4 +1,5 @@ # pylint: disable=missing-docstring, too-few-public-methods, useless-object-inheritance +from collections.abc import MutableSequence class Aaaa(object): pass @@ -22,3 +23,23 @@ class Iiii(Aaaa, Bbbb, Cccc, Dddd, Eeee, Ffff, Gggg, Hhhh): # [too-many-ancestor class Jjjj(Iiii): # [too-many-ancestors] pass + + +# https://github.com/PyCQA/pylint/issues/4166 +# https://github.com/PyCQA/pylint/issues/4415 +class ItemSequence(MutableSequence): + """Minimal MutableSequence.""" + def __getitem__(self, key): + return key + + def __setitem__(self, key, value): + _ = key, value + + def __delitem__(self, key): + _ = key + + def insert(self, index, value): + _ = index, value + + def __len__(self): + return 1 diff --git a/tests/functional/t/too/too_many_ancestors.txt b/tests/functional/t/too/too_many_ancestors.txt index 97c9543683..d913ce4efa 100644 --- a/tests/functional/t/too/too_many_ancestors.txt +++ b/tests/functional/t/too/too_many_ancestors.txt @@ -1,2 +1,2 @@ -too-many-ancestors:20:0:Iiii:Too many ancestors (9/7) -too-many-ancestors:23:0:Jjjj:Too many ancestors (10/7) +too-many-ancestors:21:0:Iiii:Too many ancestors (8/7) +too-many-ancestors:24:0:Jjjj:Too many ancestors (9/7)