Skip to content

Commit

Permalink
Fix unsupported-binary-operation on classes that overload or (#6664)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
timmartin and jacobtylerwalls authored Jun 23, 2022
1 parent 8d8e518 commit 02e9023
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 7 deletions.
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 @@ -1903,14 +1905,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

0 comments on commit 02e9023

Please sign in to comment.