Skip to content

Fix unsupported-binary-operation on classes that overload or #6664

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Jun 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
94c5cfd
Fix ``unsupported-binary-operation`` on classes that overload or
timmartin May 22, 2022
1af0924
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 22, 2022
f8b0166
Handle the case where the overload is from Python 3.10 runtime
timmartin May 28, 2022
02e7360
Merge branch 'main' into issue-4951
timmartin May 29, 2022
3f7687e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 29, 2022
262ef22
Update tests/functional/a/alternative/alternative_union_syntax_error.py
timmartin Jun 1, 2022
e2ecb8f
Defend against assigning an anonymous function to __or__
timmartin Jun 1, 2022
2d82246
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 1, 2022
4512b70
Merge branch 'main' into issue-4951
timmartin Jun 1, 2022
f8877c3
Move the release note to 2.15
timmartin Jun 1, 2022
d20a35a
Cope with multiple attributes inferred on Python 3.10
timmartin Jun 2, 2022
410cb7e
Apply code review feedback
timmartin Jun 15, 2022
414353c
Add a test to illustrate the second-level metaclass issue
timmartin Jun 15, 2022
7dc9fc7
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 15, 2022
efc75e7
Clean up the functional test and add an extra case
timmartin Jun 15, 2022
5d6a493
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 15, 2022
b46cd09
Fix some code review comments
timmartin Jun 22, 2022
433f850
Remove the second-level metaclass test case
timmartin Jun 22, 2022
8ec0215
Use consistent location for NotFoundError
timmartin Jun 22, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/whatsnew/2/2.15/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
=====================
Expand Down
55 changes: 49 additions & 6 deletions pylint/checkers/typecheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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?
Expand Down
20 changes: 20 additions & 0 deletions tests/functional/a/alternative/alternative_union_syntax.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
35 changes: 34 additions & 1 deletion tests/functional/a/alternative/alternative_union_syntax_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Original file line number Diff line number Diff line change
Expand Up @@ -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