Skip to content

Commit 02e9023

Browse files
Fix unsupported-binary-operation on classes that overload or (#6664)
* Use consistent location for NotFoundError Throughout this module we're using ``astroid.NotFoundError`` as an alias for ``astroid.exceptions.NotFoundError``, it seems best to be consistent. Closes #4951 Co-authored-by: Jacob Walls <[email protected]>
1 parent 8d8e518 commit 02e9023

File tree

5 files changed

+108
-7
lines changed

5 files changed

+108
-7
lines changed

doc/whatsnew/2/2.15/index.rst

+4
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ Extensions
2727
False positives fixed
2828
=====================
2929

30+
* Don't report ``unsupported-binary-operation`` on Python <= 3.9 when using the ``|`` operator
31+
with types, if one has a metaclass that overloads ``__or__`` or ``__ror__`` as appropriate.
32+
33+
Closes #4951
3034

3135
False negatives fixed
3236
=====================

pylint/checkers/typecheck.py

+49-6
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
from re import Pattern
2121
from typing import TYPE_CHECKING, Any, Union
2222

23+
import astroid
2324
import astroid.exceptions
25+
import astroid.helpers
2426
from astroid import bases, nodes
2527

2628
from pylint.checkers import BaseChecker, utils
@@ -1903,14 +1905,55 @@ def _detect_unsupported_alternative_union_syntax(self, node: nodes.BinOp) -> Non
19031905
if not allowed_nested_syntax:
19041906
self._check_unsupported_alternative_union_syntax(node)
19051907

1908+
def _includes_version_compatible_overload(self, attrs: list):
1909+
"""Check if a set of overloads of an operator includes one that
1910+
can be relied upon for our configured Python version.
1911+
1912+
If we are running under a Python 3.10+ runtime but configured for
1913+
pre-3.10 compatibility then Astroid will have inferred the
1914+
existence of __or__ / __ror__ on builtins.type, but these aren't
1915+
available in the configured version of Python.
1916+
"""
1917+
is_py310_builtin = all(
1918+
isinstance(attr, (nodes.FunctionDef, astroid.BoundMethod))
1919+
and attr.parent.qname() == "builtins.type"
1920+
for attr in attrs
1921+
)
1922+
return not is_py310_builtin or self._py310_plus
1923+
19061924
def _check_unsupported_alternative_union_syntax(self, node: nodes.BinOp) -> None:
1907-
"""Check if left or right node is of type `type`."""
1925+
"""Check if left or right node is of type `type`.
1926+
1927+
If either is, and doesn't support an or operator via a metaclass,
1928+
infer that this is a mistaken attempt to use alternative union
1929+
syntax when not supported.
1930+
"""
19081931
msg = "unsupported operand type(s) for |"
1909-
for n in (node.left, node.right):
1910-
n = astroid.helpers.object_type(n)
1911-
if isinstance(n, nodes.ClassDef) and is_classdef_type(n):
1912-
self.add_message("unsupported-binary-operation", args=msg, node=node)
1913-
break
1932+
left_obj = astroid.helpers.object_type(node.left)
1933+
right_obj = astroid.helpers.object_type(node.right)
1934+
left_is_type = False
1935+
right_is_type = False
1936+
1937+
if isinstance(left_obj, nodes.ClassDef) and is_classdef_type(left_obj):
1938+
try:
1939+
attrs = left_obj.getattr("__or__")
1940+
if self._includes_version_compatible_overload(attrs):
1941+
return
1942+
left_is_type = True
1943+
except astroid.NotFoundError:
1944+
left_is_type = True
1945+
1946+
if isinstance(right_obj, nodes.ClassDef) and is_classdef_type(right_obj):
1947+
try:
1948+
attrs = right_obj.getattr("__ror__")
1949+
if self._includes_version_compatible_overload(attrs):
1950+
return
1951+
right_is_type = True
1952+
except astroid.NotFoundError:
1953+
right_is_type = True
1954+
1955+
if left_is_type or right_is_type:
1956+
self.add_message("unsupported-binary-operation", args=msg, node=node)
19141957

19151958
# TODO: This check was disabled (by adding the leading underscore)
19161959
# due to false positives several years ago - can we re-enable it?

tests/functional/a/alternative/alternative_union_syntax.py

+20
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,23 @@ class CustomDataClass3:
8282
@dataclasses.dataclass
8383
class CustomDataClass4:
8484
my_var: int | str
85+
86+
class ForwardMetaclass(type):
87+
def __or__(cls, other):
88+
return True
89+
90+
class ReverseMetaclass(type):
91+
def __ror__(cls, other):
92+
return True
93+
94+
class WithForward(metaclass=ForwardMetaclass):
95+
pass
96+
97+
class WithReverse(metaclass=ReverseMetaclass):
98+
pass
99+
100+
class DefaultMetaclass:
101+
pass
102+
103+
class_list = [WithForward | DefaultMetaclass]
104+
class_list_reversed = [WithReverse | DefaultMetaclass]

tests/functional/a/alternative/alternative_union_syntax_error.py

+34-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
For Python 3.7 - 3.9: Everything should fail.
55
Testing only 3.8/3.9 to support TypedDict.
66
"""
7-
# 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
7+
# 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
88
import dataclasses
99
import typing
1010
from dataclasses import dataclass
@@ -87,3 +87,36 @@ class CustomDataClass3:
8787
@dataclasses.dataclass
8888
class CustomDataClass4:
8989
my_var: int | str # [unsupported-binary-operation]
90+
91+
# Not an error if the metaclass implements __or__
92+
93+
class ForwardMetaclass(type):
94+
def __or__(cls, other):
95+
return True
96+
97+
class ReverseMetaclass(type):
98+
def __ror__(cls, other):
99+
return True
100+
101+
class WithForward(metaclass=ForwardMetaclass):
102+
pass
103+
104+
class WithReverse(metaclass=ReverseMetaclass):
105+
pass
106+
107+
class DefaultMetaclass:
108+
pass
109+
110+
class_list = [WithForward | DefaultMetaclass]
111+
class_list_reversed_invalid = [WithReverse | DefaultMetaclass] # [unsupported-binary-operation]
112+
class_list_reversed_valid = [DefaultMetaclass | WithReverse]
113+
114+
115+
# Pathological cases
116+
class HorribleMetaclass(type):
117+
__or__ = lambda x: x
118+
119+
class WithHorrible(metaclass=HorribleMetaclass):
120+
pass
121+
122+
class_list = [WithHorrible | DefaultMetaclass]

tests/functional/a/alternative/alternative_union_syntax_error.txt

+1
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ unsupported-binary-operation:76:12:76:21:CustomDataClass:unsupported operand typ
2222
unsupported-binary-operation:80:12:80:21:CustomDataClass2:unsupported operand type(s) for |:UNDEFINED
2323
unsupported-binary-operation:84:12:84:21:CustomDataClass3:unsupported operand type(s) for |:UNDEFINED
2424
unsupported-binary-operation:89:12:89:21:CustomDataClass4:unsupported operand type(s) for |:UNDEFINED
25+
unsupported-binary-operation:111:31:111:61::unsupported operand type(s) for |:UNDEFINED

0 commit comments

Comments
 (0)