Skip to content

Commit

Permalink
Add check for alternative union syntax - PEP 604
Browse files Browse the repository at this point in the history
  • Loading branch information
cdce8p authored and Pierre-Sassoulas committed Feb 15, 2021
1 parent b6edb60 commit 1175b6e
Show file tree
Hide file tree
Showing 11 changed files with 334 additions and 0 deletions.
4 changes: 4 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ Pylint's ChangeLog

Closes #3320

* Check alternative union syntax - PEP 604

Closes #4065

What's New in Pylint 2.6.1?
===========================
Release date: TBA
Expand Down
35 changes: 35 additions & 0 deletions pylint/checkers/typecheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,13 @@
decorated_with_property,
has_known_bases,
is_builtin_object,
is_classdef_type,
is_comprehension,
is_inside_abstract_class,
is_iterable,
is_mapping,
is_overload_stub,
is_postponed_evaluation_enabled,
is_super,
node_ignores_exception,
safe_infer,
Expand All @@ -88,6 +90,7 @@
supports_membership_test,
supports_setitem,
)
from pylint.constants import PY310_PLUS
from pylint.interfaces import INFERENCE, IAstroidChecker
from pylint.utils import get_global_option

Expand Down Expand Up @@ -1629,6 +1632,38 @@ def visit_unaryop(self, node):
# Let the error customize its output.
self.add_message("invalid-unary-operand-type", args=str(error), node=node)

@check_messages("unsupported-binary-operation")
def visit_binop(self, node: astroid.BinOp):
# Test alternative Union syntax PEP 604 - int | None
msg = "unsupported operand type(s) for |"
if node.op == "|" and (
not is_postponed_evaluation_enabled(node)
and isinstance(
node.parent, (astroid.AnnAssign, astroid.Arguments, astroid.FunctionDef)
)
or not PY310_PLUS
and isinstance(
node.parent,
(
astroid.Assign,
astroid.Call,
astroid.Keyword,
astroid.Dict,
astroid.Tuple,
astroid.Set,
astroid.List,
),
)
):
for n in (node.left, node.right):
n = helpers.object_type(n)
if isinstance(n, astroid.ClassDef):
if is_classdef_type(n):
self.add_message(
"unsupported-binary-operation", args=msg, node=node
)
break

@check_messages("unsupported-binary-operation")
def _visit_binop(self, node):
"""Detect TypeErrors for binary arithmetic operands."""
Expand Down
10 changes: 10 additions & 0 deletions pylint/checkers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1416,3 +1416,13 @@ def is_test_condition(
if isinstance(parent, astroid.Comprehension):
return node in parent.ifs
return is_call_of_name(parent, "bool") and parent.parent_of(node)


def is_classdef_type(node: astroid.ClassDef) -> bool:
"""Test if ClassDef node is Type."""
if node.name == "type":
return True
for base in node.bases:
if isinstance(base, astroid.Name) and base.name == "type":
return True
return False
79 changes: 79 additions & 0 deletions tests/functional/a/alternative_union_syntax.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Test PEP 604 - Alternative Union syntax"""
# pylint: disable=missing-function-docstring,unused-argument,invalid-name,missing-class-docstring,inherit-non-class,too-few-public-methods
import dataclasses
import typing
from dataclasses import dataclass
from typing import NamedTuple, TypedDict


Alias = str | list[int]
lst = [typing.Dict[str, int] | None,]

cast_var = 1
cast_var = typing.cast(str | int, cast_var)

T = typing.TypeVar("T", int | str, bool)

(lambda x: 2)(int | str)

var: str | int

def func(arg: int | str):
pass

def func2() -> int | str:
pass

class CustomCls(int):
pass

Alias2 = CustomCls | str

var2 = CustomCls(1) | int(2)


# Check typing.NamedTuple
CustomNamedTuple = typing.NamedTuple(
"CustomNamedTuple", [("my_var", int | str)])

class CustomNamedTuple2(NamedTuple):
my_var: int | str

class CustomNamedTuple3(typing.NamedTuple):
my_var: int | str


# Check typing.TypedDict
CustomTypedDict = TypedDict("CustomTypedDict", my_var=(int | str))

CustomTypedDict2 = TypedDict("CustomTypedDict2", {"my_var": int | str})

class CustomTypedDict3(TypedDict):
my_var: int | str

class CustomTypedDict4(typing.TypedDict):
my_var: int | str


# Check dataclasses
def my_decorator(*args, **kwargs):
def wraps(*args, **kwargs):
pass
return wraps

@dataclass
class CustomDataClass:
my_var: int | str

@dataclasses.dataclass
class CustomDataClass2:
my_var: int | str

@dataclass()
class CustomDataClass3:
my_var: int | str

@my_decorator
@dataclasses.dataclass
class CustomDataClass4:
my_var: int | str
2 changes: 2 additions & 0 deletions tests/functional/a/alternative_union_syntax.rc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[testoptions]
min_pyver=3.10
84 changes: 84 additions & 0 deletions tests/functional/a/alternative_union_syntax_error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Test PEP 604 - Alternative Union syntax
without postponed evaluation of annotations.
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
import dataclasses
import typing
from dataclasses import dataclass
from typing import NamedTuple, TypedDict


Alias = str | typing.List[int] # [unsupported-binary-operation]
lst = [typing.Dict[str, int] | None,] # [unsupported-binary-operation]

cast_var = 1
cast_var = typing.cast(str | int, cast_var) # [unsupported-binary-operation]

T = typing.TypeVar("T", int | str, bool) # [unsupported-binary-operation]

(lambda x: 2)(int | str) # [unsupported-binary-operation]

var: str | int # [unsupported-binary-operation]

def func(arg: int | str): # [unsupported-binary-operation]
pass

def func2() -> int | str: # [unsupported-binary-operation]
pass

class CustomCls(int):
pass

Alias2 = CustomCls | str # [unsupported-binary-operation]

var2 = CustomCls(1) | int(2)


# Check typing.NamedTuple
CustomNamedTuple = typing.NamedTuple(
"CustomNamedTuple", [("my_var", int | str)]) # [unsupported-binary-operation]

class CustomNamedTuple2(NamedTuple):
my_var: int | str # [unsupported-binary-operation]

class CustomNamedTuple3(typing.NamedTuple):
my_var: int | str # [unsupported-binary-operation]


# Check typing.TypedDict
CustomTypedDict = TypedDict("CustomTypedDict", my_var=int | str) # [unsupported-binary-operation]

CustomTypedDict2 = TypedDict("CustomTypedDict2", {"my_var": int | str}) # [unsupported-binary-operation]

class CustomTypedDict3(TypedDict):
my_var: int | str # [unsupported-binary-operation]

class CustomTypedDict4(typing.TypedDict):
my_var: int | str # [unsupported-binary-operation]


# Check dataclasses
def my_decorator(*args, **kwargs):
def wraps(*args, **kwargs):
pass
return wraps

@dataclass
class CustomDataClass:
my_var: int | str # [unsupported-binary-operation]

@dataclasses.dataclass
class CustomDataClass2:
my_var: int | str # [unsupported-binary-operation]

@dataclass()
class CustomDataClass3:
my_var: int | str # [unsupported-binary-operation]

@my_decorator
@dataclasses.dataclass
class CustomDataClass4:
my_var: int | str # [unsupported-binary-operation]
3 changes: 3 additions & 0 deletions tests/functional/a/alternative_union_syntax_error.rc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[testoptions]
min_pyver=3.8
max_pyver=3.10
20 changes: 20 additions & 0 deletions tests/functional/a/alternative_union_syntax_error.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
unsupported-binary-operation:14:8::unsupported operand type(s) for |
unsupported-binary-operation:15:7::unsupported operand type(s) for |
unsupported-binary-operation:18:23::unsupported operand type(s) for |
unsupported-binary-operation:20:24::unsupported operand type(s) for |
unsupported-binary-operation:22:14::unsupported operand type(s) for |
unsupported-binary-operation:24:5::unsupported operand type(s) for |
unsupported-binary-operation:26:14:func:unsupported operand type(s) for |
unsupported-binary-operation:29:15:func2:unsupported operand type(s) for |
unsupported-binary-operation:35:9::unsupported operand type(s) for |
unsupported-binary-operation:42:36::unsupported operand type(s) for |
unsupported-binary-operation:45:12:CustomNamedTuple2:unsupported operand type(s) for |
unsupported-binary-operation:48:12:CustomNamedTuple3:unsupported operand type(s) for |
unsupported-binary-operation:52:54::unsupported operand type(s) for |
unsupported-binary-operation:54:60::unsupported operand type(s) for |
unsupported-binary-operation:57:12:CustomTypedDict3:unsupported operand type(s) for |
unsupported-binary-operation:60:12:CustomTypedDict4:unsupported operand type(s) for |
unsupported-binary-operation:71:12:CustomDataClass:unsupported operand type(s) for |
unsupported-binary-operation:75:12:CustomDataClass2:unsupported operand type(s) for |
unsupported-binary-operation:79:12:CustomDataClass3:unsupported operand type(s) for |
unsupported-binary-operation:84:12:CustomDataClass4:unsupported operand type(s) for |
85 changes: 85 additions & 0 deletions tests/functional/a/alternative_union_syntax_py37.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Test PEP 604 - Alternative Union syntax
with postponed evaluation of annotations enabled.
For Python 3.7 - 3.9: Most things should work.
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
from __future__ import annotations
import dataclasses
import typing
from dataclasses import dataclass
from typing import NamedTuple, TypedDict


Alias = str | typing.List[int] # [unsupported-binary-operation]
lst = [typing.Dict[str, int] | None,] # [unsupported-binary-operation]

cast_var = 1
cast_var = typing.cast(str | int, cast_var) # [unsupported-binary-operation]

T = typing.TypeVar("T", int | str, bool) # [unsupported-binary-operation]

(lambda x: 2)(int | str) # [unsupported-binary-operation]

var: str | int

def func(arg: int | str):
pass

def func2() -> int | str:
pass

class CustomCls(int):
pass

Alias2 = CustomCls | str # [unsupported-binary-operation]

var2 = CustomCls(1) | int(2)


# Check typing.NamedTuple
CustomNamedTuple = typing.NamedTuple(
"CustomNamedTuple", [("my_var", int | str)]) # [unsupported-binary-operation]

class CustomNamedTuple2(NamedTuple):
my_var: int | str

class CustomNamedTuple3(typing.NamedTuple):
my_var: int | str


# Check typing.TypedDict
CustomTypedDict = TypedDict("CustomTypedDict", my_var=int | str) # [unsupported-binary-operation]

CustomTypedDict2 = TypedDict("CustomTypedDict2", {"my_var": int | str}) # [unsupported-binary-operation]

class CustomTypedDict3(TypedDict):
my_var: int | str

class CustomTypedDict4(typing.TypedDict):
my_var: int | str


# Check dataclasses
def my_decorator(*args, **kwargs):
def wraps(*args, **kwargs):
pass
return wraps

@dataclass
class CustomDataClass:
my_var: int | str

@dataclasses.dataclass
class CustomDataClass2:
my_var: int | str

@dataclass()
class CustomDataClass3:
my_var: int | str

@my_decorator
@dataclasses.dataclass
class CustomDataClass4:
my_var: int | str
3 changes: 3 additions & 0 deletions tests/functional/a/alternative_union_syntax_py37.rc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[testoptions]
min_pyver=3.8
max_pyver=3.10
9 changes: 9 additions & 0 deletions tests/functional/a/alternative_union_syntax_py37.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
unsupported-binary-operation:15:8::unsupported operand type(s) for |
unsupported-binary-operation:16:7::unsupported operand type(s) for |
unsupported-binary-operation:19:23::unsupported operand type(s) for |
unsupported-binary-operation:21:24::unsupported operand type(s) for |
unsupported-binary-operation:23:14::unsupported operand type(s) for |
unsupported-binary-operation:36:9::unsupported operand type(s) for |
unsupported-binary-operation:43:36::unsupported operand type(s) for |
unsupported-binary-operation:53:54::unsupported operand type(s) for |
unsupported-binary-operation:55:60::unsupported operand type(s) for |

0 comments on commit 1175b6e

Please sign in to comment.