Skip to content

Commit

Permalink
Add ignored-parents option to design checker
Browse files Browse the repository at this point in the history
This allows users to specify classes to ignore while counting parent
classes.

Partially closes pylint-dev#3057
  • Loading branch information
Rebecca Turner committed Jul 27, 2021
1 parent e04de25 commit d1f9a75
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 4 deletions.
2 changes: 2 additions & 0 deletions CONTRIBUTORS.txt
Expand Up @@ -521,3 +521,5 @@ contributors:
* Yilei Yang: contributor

* Marcin Kurczewski (rr-): contributor

* Rebecca Turner (9999years): contributor
5 changes: 5 additions & 0 deletions ChangeLog
Expand Up @@ -14,6 +14,11 @@ What's New in Pylint 2.9.6?
===========================
Release date: TBA

* Added ``ignored-parents`` option to the design checker to ignore classes
while counting ancestors.

Partially closes #3057

..
Put bug fixes that should not wait for a new minor version here

Expand Down
47 changes: 43 additions & 4 deletions pylint/checkers/design_analysis.py
Expand Up @@ -25,6 +25,7 @@

import re
from collections import defaultdict
from typing import FrozenSet, List, Set, cast

import astroid
from astroid import nodes
Expand Down Expand Up @@ -237,6 +238,35 @@ def _count_methods_in_class(node):
return all_methods


def _get_parents(
node: nodes.ClassDef, ignored_parents: FrozenSet[str]
) -> Set[nodes.ClassDef]:
r"""Get parents of ``node``, excluding ancestors of ``ignored_parents``.
If we have the following inheritance diagram:
F
/
D E
\/
B C
\/
A # class A(B, C): ...
And ``ignored_parents`` is ``{"E"}``, then this function will return
``{A, B, C, D}`` -- both ``E`` and its ancestors are excluded.
"""
parents: Set[nodes.ClassDef] = set()
to_explore = cast(List[nodes.ClassDef], list(node.ancestors(recurs=False)))
while to_explore:
parent = to_explore.pop()
if parent.qname() in ignored_parents:
continue
parents.add(parent)
to_explore.extend(parent.ancestors(recurs=False)) # type: ignore
return parents


class MisdesignChecker(BaseChecker):
"""checks for sign of poor/misdesign:
* number of methods, attributes, local variables...
Expand Down Expand Up @@ -307,6 +337,15 @@ class MisdesignChecker(BaseChecker):
"help": "Maximum number of parents for a class (see R0901).",
},
),
(
"ignored-parents",
{
"default": (),
"type": "csv",
"metavar": "<comma separated list of class names>",
"help": "List of qualified class names to ignore when countint class parents (see R0901)",
},
),
(
"max-attributes",
{
Expand Down Expand Up @@ -379,11 +418,11 @@ def _ignored_argument_names(self):
)
def visit_classdef(self, node: nodes.ClassDef):
"""check size of inheritance hierarchy and number of instance attributes"""
nb_parents = sum(
1
for ancestor in node.ancestors()
if ancestor.qname() not in STDLIB_CLASSES_IGNORE_ANCESTOR
parents = _get_parents(
node, STDLIB_CLASSES_IGNORE_ANCESTOR.union(self.config.ignored_parents)
)
print(parents)
nb_parents = len(parents)
if nb_parents > self.config.max_parents:
self.add_message(
"too-many-ancestors",
Expand Down
3 changes: 3 additions & 0 deletions pylintrc
Expand Up @@ -324,6 +324,9 @@ max-statements=100
# Maximum number of parents for a class (see R0901).
max-parents=7

# List of qualified class names to ignore when counting class parents (see R0901).
ignored-parents=

# Maximum number of attributes for a class (see R0902).
max-attributes=11

Expand Down
44 changes: 44 additions & 0 deletions tests/checkers/unittest_design.py
@@ -0,0 +1,44 @@
# Copyright (c) 2021 Rebecca Turner <rturner@starry.com>

# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE

from pathlib import Path

import astroid

from pylint.checkers import design_analysis
from pylint.testutils import CheckerTestCase, set_config

REGR_DATA_DIR = str(Path(__file__).parent / ".." / "regrtest_data")


class TestDesignChecker(CheckerTestCase):

CHECKER_CLASS = design_analysis.MisdesignChecker

@set_config(
ignored_parents=(".Dddd",),
max_parents=1,
)
def test_too_many_ancestors_ignored_parents_are_skipped(self):
"""Make sure that classes listed in ``ignored-parents`` aren't counted
by the too-many-ancestors message.
"""

node = astroid.extract_node(
"""
class Aaaa(object):
pass
class Bbbb(Aaaa):
pass
class Cccc(Bbbb):
pass
class Dddd(Cccc):
pass
class Eeee(Dddd):
pass
"""
)
with self.assertNoMessages():
self.checker.visit_classdef(node)

0 comments on commit d1f9a75

Please sign in to comment.