diff options
author | Daniƫl van Noord <13665637+DanielNoord@users.noreply.github.com> | 2021-10-25 09:27:47 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-10-25 09:27:47 +0200 |
commit | 8cfce138e9519f63e969e89359e86e140b2f0f13 (patch) | |
tree | eff12c3d285d53b7a6e4cf387f265ca029c72b51 | |
parent | ed3449fee063d91f050c6b733030d3b3d7ad719f (diff) | |
download | pylint-git-8cfce138e9519f63e969e89359e86e140b2f0f13.tar.gz |
Add ``mixin-class-rgx`` option (#5203)
Co-authored-by: Alpha <alpha@pokesplash.net>
Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
-rw-r--r-- | CONTRIBUTORS.txt | 2 | ||||
-rw-r--r-- | ChangeLog | 2 | ||||
-rw-r--r-- | doc/faq.rst | 5 | ||||
-rw-r--r-- | doc/whatsnew/2.12.rst | 2 | ||||
-rw-r--r-- | pylint/checkers/async.py | 10 | ||||
-rw-r--r-- | pylint/checkers/classes.py | 5 | ||||
-rw-r--r-- | pylint/checkers/typecheck.py | 29 | ||||
-rw-r--r-- | pylint/utils/utils.py | 5 | ||||
-rw-r--r-- | pylintrc | 7 | ||||
-rw-r--r-- | tests/functional/m/mixin_class_rgx.py | 60 | ||||
-rw-r--r-- | tests/functional/m/mixin_class_rgx.rc | 3 | ||||
-rw-r--r-- | tests/functional/m/mixin_class_rgx.txt | 3 |
12 files changed, 118 insertions, 15 deletions
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 545691405..881d53db6 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -390,6 +390,8 @@ contributors: * Ram Rachum (cool-RR) +* D. Alphus (Alphadelta14): contributor + * Pieter Engelbrecht * Ethan Leba: contributor @@ -74,6 +74,8 @@ Release date: TBA Closes #4981 +* Support configuring mixin class pattern via ``mixin-class-rgx`` + * Added new checker ``use-implicit-booleaness-not-comparison``: Emitted when collection literal comparison is being used to check for emptiness. diff --git a/doc/faq.rst b/doc/faq.rst index 479d3c052..d0a02cf3a 100644 --- a/doc/faq.rst +++ b/doc/faq.rst @@ -238,8 +238,9 @@ methods is doing nothing but raising NotImplementedError. ------------------------------------------------------------------------------- To do so you have to set the ignore-mixin-members option to -"yes" (this is the default value) and to name your mixin class with -a name which ends with "mixin" (whatever case). +"yes" (this is the default value) and name your mixin class with +a name which ends with "Mixin" or "mixin" (default) or change the +default value by changing the mixin-class-rgx option. 6. Troubleshooting diff --git a/doc/whatsnew/2.12.rst b/doc/whatsnew/2.12.rst index 8b5b5f0c2..af5b33a43 100644 --- a/doc/whatsnew/2.12.rst +++ b/doc/whatsnew/2.12.rst @@ -90,6 +90,8 @@ Other Changes Closes #4426 +* Support configuring mixin class pattern via ``mixin-class-rgx`` + * Normalize the input to the ``ignore-paths`` option to allow both Posix and Windows paths diff --git a/pylint/checkers/async.py b/pylint/checkers/async.py index f9d1e15dd..9aaead7c4 100644 --- a/pylint/checkers/async.py +++ b/pylint/checkers/async.py @@ -44,6 +44,7 @@ class AsyncChecker(checkers.BaseChecker): self._ignore_mixin_members = utils.get_global_option( self, "ignore-mixin-members" ) + self._mixin_class_rgx = utils.get_global_option(self, "mixin-class-rgx") self._async_generators = ["contextlib.asynccontextmanager"] @checker_utils.check_messages("yield-inside-async-function") @@ -81,10 +82,11 @@ class AsyncChecker(checkers.BaseChecker): # just skip it. if not checker_utils.has_known_bases(inferred): continue - # Just ignore mixin classes. - if self._ignore_mixin_members: - if inferred.name[-5:].lower() == "mixin": - continue + # Ignore mixin classes if they match the rgx option. + if self._ignore_mixin_members and self._mixin_class_rgx.match( + inferred.name + ): + continue else: continue self.add_message( diff --git a/pylint/checkers/classes.py b/pylint/checkers/classes.py index 0b15861b0..2ab66fd0b 100644 --- a/pylint/checkers/classes.py +++ b/pylint/checkers/classes.py @@ -808,6 +808,9 @@ a metaclass class method.", self._first_attrs = [] self._meth_could_be_func = None + def open(self) -> None: + self._mixin_class_rgx = get_global_option(self, "mixin-class-rgx") + @astroid.decorators.cachedproperty def _dummy_rgx(self): return get_global_option(self, "dummy-variables-rgx", default=None) @@ -1029,7 +1032,7 @@ a metaclass class method.", def _check_attribute_defined_outside_init(self, cnode: nodes.ClassDef) -> None: # check access to existent members on non metaclass classes - if self._ignore_mixin and cnode.name[-5:].lower() == "mixin": + if self._ignore_mixin and self._mixin_class_rgx.match(cnode.name): # We are in a mixin class. No need to try to figure out if # something is missing, since it is most likely that it will # miss. diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index 8fa80ee21..549cd7f4c 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -442,7 +442,14 @@ SEQUENCE_TYPES = { } -def _emit_no_member(node, owner, owner_name, ignored_mixins=True, ignored_none=True): +def _emit_no_member( + node, + owner, + owner_name, + mixin_class_rgx: Pattern[str], + ignored_mixins=True, + ignored_none=True, +): """Try to see if no-member should be emitted for the given owner. The following cases are ignored: @@ -462,7 +469,7 @@ def _emit_no_member(node, owner, owner_name, ignored_mixins=True, ignored_none=T return False if is_super(owner) or getattr(owner, "type", None) == "metaclass": return False - if owner_name and ignored_mixins and owner_name[-5:].lower() == "mixin": + if owner_name and ignored_mixins and mixin_class_rgx.match(owner_name): return False if isinstance(owner, nodes.FunctionDef) and ( owner.decorators or owner.is_abstract() @@ -781,14 +788,24 @@ class TypeChecker(BaseChecker): }, ), ( + "mixin-class-rgx", + { + "default": ".*[Mm]ixin", + "type": "regexp", + "metavar": "<regexp>", + "help": "Regex pattern to define which classes are considered mixins " + "ignore-mixin-members is set to 'yes'", + }, + ), + ( "ignore-mixin-members", { "default": True, "type": "yn", "metavar": "<y_or_n>", - "help": 'Tells whether missing members accessed in mixin \ -class should be ignored. A mixin class is detected if its name ends with \ -"mixin" (case insensitive).', + "help": "Tells whether missing members accessed in mixin " + "class should be ignored. A class is considered mixin if its name matches " + "the mixin-class-rgx option.", }, ), ( @@ -898,6 +915,7 @@ accessed. Python regular expressions are accepted.", def open(self) -> None: py_version = get_global_option(self, "py-version") self._py310_plus = py_version >= (3, 10) + self._mixin_class_rgx = get_global_option(self, "mixin-class-rgx") @astroid.decorators.cachedproperty def _suggestion_mode(self): @@ -1040,6 +1058,7 @@ accessed. Python regular expressions are accepted.", node, owner, name, + self._mixin_class_rgx, ignored_mixins=self.config.ignore_mixin_members, ignored_none=self.config.ignore_none, ): diff --git a/pylint/utils/utils.py b/pylint/utils/utils.py index 6c9f11298..8243bc322 100644 --- a/pylint/utils/utils.py +++ b/pylint/utils/utils.py @@ -54,7 +54,10 @@ GLOBAL_OPTION_BOOL = Literal[ GLOBAL_OPTION_INT = Literal["max-line-length", "docstring-min-length"] GLOBAL_OPTION_LIST = Literal["ignored-modules"] GLOBAL_OPTION_PATTERN = Literal[ - "no-docstring-rgx", "dummy-variables-rgx", "ignored-argument-names" + "no-docstring-rgx", + "dummy-variables-rgx", + "ignored-argument-names", + "mixin-class-rgx", ] GLOBAL_OPTION_PATTERN_LIST = Literal["ignore-paths"] GLOBAL_OPTION_TUPLE_INT = Literal["py-version"] @@ -270,10 +270,13 @@ property-classes=abc.abstractproperty [TYPECHECK] -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). +# Tells whether missing members accessed in mixin class should be ignored. +# A class is considered mixin if its name matches the mixin-class-rgx option. ignore-mixin-members=yes +# Regex pattern to define which classes are considered mixins. +mixin-class-rgx=.*MixIn + # List of module names for which member attributes should not be checked # (useful for modules/projects where namespaces are manipulated during runtime # and thus existing member attributes cannot be deduced by static analysis) diff --git a/tests/functional/m/mixin_class_rgx.py b/tests/functional/m/mixin_class_rgx.py new file mode 100644 index 000000000..1f6ea21e6 --- /dev/null +++ b/tests/functional/m/mixin_class_rgx.py @@ -0,0 +1,60 @@ +"""Tests for the mixin-class-rgx option""" +# pylint: disable=too-few-public-methods + + +# Tests for not-async-context-manager + + +class AsyncManagerMixedin: + """Class that does not match the option pattern""" + + def __aenter__(self): + pass + + +class AsyncManagerMixin: + """Class that does match the option pattern""" + + def __aenter__(self): + pass + + +async def check_not_async_context_manager(): + """Function calling the classes for not-async-context-manager""" + async with AsyncManagerMixedin: # [not-async-context-manager] + pass + async with AsyncManagerMixin(): + pass + + +# Tests for attribute-defined-outside-init + + +class OutsideInitMixedin: + """Class that does not match the option pattern""" + + def set_attribute(self): + """Set an attribute outside of __init__""" + self.attr = 1 # [attribute-defined-outside-init] + + +class OutsideInitMixin: + """Class that does match the option pattern""" + + def set_attribute(self): + """Set an attribute outside of __init__""" + self.attr = 1 + + +# Tests for no-member + + +class NoMemberMixedin: + """Class that does not match the option pattern""" + +MY_CLASS = OutsideInitMixedin().method() # [no-member] + +class NoMemberMixin: + """Class that does match the option pattern""" + +MY_OTHER_CLASS = NoMemberMixin().method() diff --git a/tests/functional/m/mixin_class_rgx.rc b/tests/functional/m/mixin_class_rgx.rc new file mode 100644 index 000000000..4ca300e4a --- /dev/null +++ b/tests/functional/m/mixin_class_rgx.rc @@ -0,0 +1,3 @@ +[TYPECHECK] +ignore-mixin-members=yes +mixin-class-rgx=.*[Mm]ixin diff --git a/tests/functional/m/mixin_class_rgx.txt b/tests/functional/m/mixin_class_rgx.txt new file mode 100644 index 000000000..eb39a1185 --- /dev/null +++ b/tests/functional/m/mixin_class_rgx.txt @@ -0,0 +1,3 @@ +not-async-context-manager:24:4:check_not_async_context_manager:Async context manager 'AsyncManagerMixedin' doesn't implement __aenter__ and __aexit__.:HIGH +attribute-defined-outside-init:38:8:OutsideInitMixedin.set_attribute:Attribute 'attr' defined outside __init__:HIGH +no-member:55:11::Instance of 'OutsideInitMixedin' has no 'method' member:INFERENCE |