diff --git a/doc/whatsnew/2/2.15/index.rst b/doc/whatsnew/2/2.15/index.rst index 519232b22e..59469bb547 100644 --- a/doc/whatsnew/2/2.15/index.rst +++ b/doc/whatsnew/2/2.15/index.rst @@ -27,6 +27,10 @@ Extensions False positives fixed ===================== +* Don't report ``unsupported-binary-operation`` on Python <= 3.9 when using the ``|`` operator + with types, if one has a metaclass that overloads ``__or__`` or ``__ror__`` as appropriate. + + Closes #4951 False negatives fixed ===================== diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index 413a93c3bb..303796d889 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -20,7 +20,9 @@ from re import Pattern from typing import TYPE_CHECKING, Any, Union +import astroid import astroid.exceptions +import astroid.helpers from astroid import bases, nodes from pylint.checkers import BaseChecker, utils @@ -1901,14 +1903,55 @@ def _detect_unsupported_alternative_union_syntax(self, node: nodes.BinOp) -> Non if not allowed_nested_syntax: self._check_unsupported_alternative_union_syntax(node) + def _includes_version_compatible_overload(self, attrs: list): + """Check if a set of overloads of an operator includes one that + can be relied upon for our configured Python version. + + If we are running under a Python 3.10+ runtime but configured for + pre-3.10 compatibility then Astroid will have inferred the + existence of __or__ / __ror__ on builtins.type, but these aren't + available in the configured version of Python. + """ + is_py310_builtin = all( + isinstance(attr, (nodes.FunctionDef, astroid.BoundMethod)) + and attr.parent.qname() == "builtins.type" + for attr in attrs + ) + return not is_py310_builtin or self._py310_plus + def _check_unsupported_alternative_union_syntax(self, node: nodes.BinOp) -> None: - """Check if left or right node is of type `type`.""" + """Check if left or right node is of type `type`. + + If either is, and doesn't support an or operator via a metaclass, + infer that this is a mistaken attempt to use alternative union + syntax when not supported. + """ msg = "unsupported operand type(s) for |" - for n in (node.left, node.right): - n = astroid.helpers.object_type(n) - if isinstance(n, nodes.ClassDef) and is_classdef_type(n): - self.add_message("unsupported-binary-operation", args=msg, node=node) - break + left_obj = astroid.helpers.object_type(node.left) + right_obj = astroid.helpers.object_type(node.right) + left_is_type = False + right_is_type = False + + if isinstance(left_obj, nodes.ClassDef) and is_classdef_type(left_obj): + try: + attrs = left_obj.getattr("__or__") + if self._includes_version_compatible_overload(attrs): + return + left_is_type = True + except astroid.NotFoundError: + left_is_type = True + + if isinstance(right_obj, nodes.ClassDef) and is_classdef_type(right_obj): + try: + attrs = right_obj.getattr("__ror__") + if self._includes_version_compatible_overload(attrs): + return + right_is_type = True + except astroid.NotFoundError: + right_is_type = True + + if left_is_type or right_is_type: + self.add_message("unsupported-binary-operation", args=msg, node=node) # TODO: This check was disabled (by adding the leading underscore) # due to false positives several years ago - can we re-enable it? diff --git a/tests/functional/a/alternative/alternative_union_syntax.py b/tests/functional/a/alternative/alternative_union_syntax.py index 05753266da..4492323a25 100644 --- a/tests/functional/a/alternative/alternative_union_syntax.py +++ b/tests/functional/a/alternative/alternative_union_syntax.py @@ -82,3 +82,23 @@ class CustomDataClass3: @dataclasses.dataclass class CustomDataClass4: my_var: int | str + +class ForwardMetaclass(type): + def __or__(cls, other): + return True + +class ReverseMetaclass(type): + def __ror__(cls, other): + return True + +class WithForward(metaclass=ForwardMetaclass): + pass + +class WithReverse(metaclass=ReverseMetaclass): + pass + +class DefaultMetaclass: + pass + +class_list = [WithForward | DefaultMetaclass] +class_list_reversed = [WithReverse | DefaultMetaclass] diff --git a/tests/functional/a/alternative/alternative_union_syntax_error.py b/tests/functional/a/alternative/alternative_union_syntax_error.py index 6f74d675a9..3922e3c0d3 100644 --- a/tests/functional/a/alternative/alternative_union_syntax_error.py +++ b/tests/functional/a/alternative/alternative_union_syntax_error.py @@ -4,7 +4,7 @@ For Python 3.7 - 3.9: Everything should fail. Testing only 3.8/3.9 to support TypedDict. """ -# pylint: disable=missing-function-docstring,unused-argument,invalid-name,missing-class-docstring,inherit-non-class,too-few-public-methods,line-too-long,unnecessary-direct-lambda-call +# pylint: disable=missing-function-docstring,unused-argument,invalid-name,missing-class-docstring,inherit-non-class,too-few-public-methods,line-too-long,unnecessary-direct-lambda-call,unnecessary-lambda-assignment import dataclasses import typing from dataclasses import dataclass @@ -87,3 +87,36 @@ class CustomDataClass3: @dataclasses.dataclass class CustomDataClass4: my_var: int | str # [unsupported-binary-operation] + +# Not an error if the metaclass implements __or__ + +class ForwardMetaclass(type): + def __or__(cls, other): + return True + +class ReverseMetaclass(type): + def __ror__(cls, other): + return True + +class WithForward(metaclass=ForwardMetaclass): + pass + +class WithReverse(metaclass=ReverseMetaclass): + pass + +class DefaultMetaclass: + pass + +class_list = [WithForward | DefaultMetaclass] +class_list_reversed_invalid = [WithReverse | DefaultMetaclass] # [unsupported-binary-operation] +class_list_reversed_valid = [DefaultMetaclass | WithReverse] + + +# Pathological cases +class HorribleMetaclass(type): + __or__ = lambda x: x + +class WithHorrible(metaclass=HorribleMetaclass): + pass + +class_list = [WithHorrible | DefaultMetaclass] diff --git a/tests/functional/a/alternative/alternative_union_syntax_error.txt b/tests/functional/a/alternative/alternative_union_syntax_error.txt index 5be5653f3b..72b9c5e1f2 100644 --- a/tests/functional/a/alternative/alternative_union_syntax_error.txt +++ b/tests/functional/a/alternative/alternative_union_syntax_error.txt @@ -22,3 +22,4 @@ unsupported-binary-operation:76:12:76:21:CustomDataClass:unsupported operand typ unsupported-binary-operation:80:12:80:21:CustomDataClass2:unsupported operand type(s) for |:UNDEFINED unsupported-binary-operation:84:12:84:21:CustomDataClass3:unsupported operand type(s) for |:UNDEFINED unsupported-binary-operation:89:12:89:21:CustomDataClass4:unsupported operand type(s) for |:UNDEFINED +unsupported-binary-operation:111:31:111:61::unsupported operand type(s) for |:UNDEFINED