diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index fd5d627eb2..a6f0c973ce 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 diff --git a/ChangeLog b/ChangeLog index be49bc1131..c6acdcc373 100644 --- a/ChangeLog +++ b/ChangeLog @@ -68,6 +68,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 479d3c0520..d0a02cf3a6 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 1126ebd531..a6c9736c08 100644 --- a/doc/whatsnew/2.12.rst +++ b/doc/whatsnew/2.12.rst @@ -86,6 +86,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 f9d1e15dd2..9aaead7c4f 100644 --- a/pylint/checkers/async.py +++ b/pylint/checkers/async.py @@ -44,6 +44,7 @@ def open(self): 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 @@ def visit_asyncwith(self, node: nodes.AsyncWith) -> None: # 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 0b15861b02..2ab66fd0b0 100644 --- a/pylint/checkers/classes.py +++ b/pylint/checkers/classes.py @@ -808,6 +808,9 @@ def __init__(self, linter=None): 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 @@ def _check_unused_private_attributes(self, node: nodes.ClassDef) -> None: 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 8fa80ee214..549cd7f4c2 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -442,7 +442,14 @@ def _missing_member_hint(owner, attrname, distance_threshold, max_choices): } -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() @@ -780,15 +787,25 @@ class TypeChecker(BaseChecker): "no-member and other checks for the rest of the inferred objects.", }, ), + ( + "mixin-class-rgx", + { + "default": ".*[Mm]ixin", + "type": "regexp", + "metavar": "", + "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": "", - "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 @@ class should be ignored. A mixin class is detected if its name ends with \ 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 @@ def visit_attribute(self, node: nodes.Attribute) -> None: 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 6c9f112982..8243bc3220 100644 --- a/pylint/utils/utils.py +++ b/pylint/utils/utils.py @@ -54,7 +54,10 @@ 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"] diff --git a/pylintrc b/pylintrc index b4a4e9facd..e5cbc12209 100644 --- a/pylintrc +++ b/pylintrc @@ -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 0000000000..1f6ea21e6c --- /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 0000000000..4ca300e4ab --- /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 0000000000..eb39a1185a --- /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