diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 881d53db6e..28b2e332cc 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -562,3 +562,5 @@ contributors: * Youngsoo Sung: contributor * Arianna Yang: contributor + +* Mike Fiedler (miketheman): contributor diff --git a/ChangeLog b/ChangeLog index 541e6543fd..9f60f43d1d 100644 --- a/ChangeLog +++ b/ChangeLog @@ -107,6 +107,11 @@ Release date: TBA * Fix ``missing-function-docstring`` not being able to check ``__init__`` and other magic methods even if the ``no-docstring-rgx`` setting was set to do so +* Added configuration option ``exclude-too-few-public-methods`` to allow excluding + classes from the ``min-public-methods`` checker. + + Closes #3370 + What's New in Pylint 2.11.2? ============================ diff --git a/doc/whatsnew/2.12.rst b/doc/whatsnew/2.12.rst index 0573ddc508..52b5ba595f 100644 --- a/doc/whatsnew/2.12.rst +++ b/doc/whatsnew/2.12.rst @@ -43,6 +43,11 @@ New checkers Closes #4774 +* Added configuration option ``exclude-too-few-public-methods`` to allow excluding + classes from the ``min-public-methods`` checker. + + Closes #3370 + Removed checkers ================ diff --git a/pylint/checkers/design_analysis.py b/pylint/checkers/design_analysis.py index 3b5056a9ea..8486aa89eb 100644 --- a/pylint/checkers/design_analysis.py +++ b/pylint/checkers/design_analysis.py @@ -402,6 +402,16 @@ class MisdesignChecker(BaseChecker): "statement (see R0916).", }, ), + ( + "exclude-too-few-public-methods", + { + "default": [], + "type": "regexp_csv", + "metavar": "[,...]", + "help": "List of regular expressions of class ancestor names " + "to ignore when counting public methods (see R0903)", + }, + ), ) def __init__(self, linter=None): @@ -416,6 +426,9 @@ def open(self): self._returns = [] self._branches = defaultdict(int) self._stmts = [] + self._exclude_too_few_public_methods = utils.get_global_option( + self, "exclude-too-few-public-methods", default=[] + ) def _inc_all_stmts(self, amount): for i, _ in enumerate(self._stmts): @@ -472,6 +485,15 @@ def leave_classdef(self, node: nodes.ClassDef) -> None: args=(my_methods, self.config.max_public_methods), ) + # Stop here if the class is excluded via configuration. + if node.type == "class" and self._exclude_too_few_public_methods: + for ancestor in node.ancestors(): + if any( + pattern.match(ancestor.qname()) + for pattern in self._exclude_too_few_public_methods + ): + return + # Stop here for exception, metaclass, interface classes and other # classes for which we don't need to count the methods. if node.type != "class" or _is_exempt_from_public_methods(node): diff --git a/pylint/utils/utils.py b/pylint/utils/utils.py index 8243bc3220..25b0ad3f19 100644 --- a/pylint/utils/utils.py +++ b/pylint/utils/utils.py @@ -59,7 +59,7 @@ "ignored-argument-names", "mixin-class-rgx", ] -GLOBAL_OPTION_PATTERN_LIST = Literal["ignore-paths"] +GLOBAL_OPTION_PATTERN_LIST = Literal["exclude-too-few-public-methods", "ignore-paths"] GLOBAL_OPTION_TUPLE_INT = Literal["py-version"] GLOBAL_OPTION_NAMES = Union[ GLOBAL_OPTION_BOOL, diff --git a/pylintrc b/pylintrc index 7669da6e8c..8df15f7136 100644 --- a/pylintrc +++ b/pylintrc @@ -350,6 +350,9 @@ min-public-methods=2 # Maximum number of public methods for a class (see R0904). max-public-methods=25 +# List of regular expressions of class ancestor names to +# ignore when counting public methods (see R0903). +exclude-too-few-public-methods= [CLASSES] diff --git a/tests/checkers/unittest_design.py b/tests/checkers/unittest_design.py index 5c6f388161..441793a2ed 100644 --- a/tests/checkers/unittest_design.py +++ b/tests/checkers/unittest_design.py @@ -11,6 +11,7 @@ from pylint.checkers import design_analysis from pylint.testutils import CheckerTestCase, set_config +from pylint.utils.utils import get_global_option class TestDesignChecker(CheckerTestCase): @@ -42,3 +43,20 @@ class Eeee(Dddd): ) with self.assertNoMessages(): self.checker.visit_classdef(node) + + @set_config(exclude_too_few_public_methods="toml.*") + def test_exclude_too_few_methods_with_value(self) -> None: + """Test exclude-too-few-public-methods option with value""" + options = get_global_option(self.checker, "exclude-too-few-public-methods") + + assert any(i.match("toml") for i in options) + assert any(i.match("toml.*") for i in options) + assert any(i.match("toml.TomlEncoder") for i in options) + + def test_ignore_paths_with_no_value(self) -> None: + """Test exclude-too-few-public-methods option with no value. + Compare against actual list to see if validator works.""" + options = get_global_option(self.checker, "exclude-too-few-public-methods") + + # pylint: disable-next=use-implicit-booleaness-not-comparison + assert options == [] diff --git a/tests/functional/t/too/too_few_public_methods_excluded.py b/tests/functional/t/too/too_few_public_methods_excluded.py new file mode 100644 index 0000000000..35ba873ee5 --- /dev/null +++ b/tests/functional/t/too/too_few_public_methods_excluded.py @@ -0,0 +1,14 @@ +# pylint: disable=missing-docstring +from json import JSONEncoder + +class Control: # [too-few-public-methods] + ... + + +class MyJsonEncoder(JSONEncoder): + ... + +class InheritedInModule(Control): + """This class inherits from a class that doesn't have enough mehods, + and its parent is excluded via config, so it doesn't raise.""" + ... diff --git a/tests/functional/t/too/too_few_public_methods_excluded.rc b/tests/functional/t/too/too_few_public_methods_excluded.rc new file mode 100644 index 0000000000..00c025832c --- /dev/null +++ b/tests/functional/t/too/too_few_public_methods_excluded.rc @@ -0,0 +1,4 @@ +[testoptions] +min-public-methods=10 # to combat inherited methods + +exclude-too-few-public-methods=json.*,^.*Control$ diff --git a/tests/functional/t/too/too_few_public_methods_excluded.txt b/tests/functional/t/too/too_few_public_methods_excluded.txt new file mode 100644 index 0000000000..c88cde25f4 --- /dev/null +++ b/tests/functional/t/too/too_few_public_methods_excluded.txt @@ -0,0 +1 @@ +too-few-public-methods:4:0:Control:Too few public methods (0/10):HIGH