From 1175b6e9110d2319a3cb4d4e8481b774b6a3cbb9 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 3 Feb 2021 20:34:58 +0100 Subject: Add check for alternative union syntax - PEP 604 --- ChangeLog | 4 + pylint/checkers/typecheck.py | 35 +++++++++ pylint/checkers/utils.py | 10 +++ tests/functional/a/alternative_union_syntax.py | 79 ++++++++++++++++++++ tests/functional/a/alternative_union_syntax.rc | 2 + .../functional/a/alternative_union_syntax_error.py | 84 +++++++++++++++++++++ .../functional/a/alternative_union_syntax_error.rc | 3 + .../a/alternative_union_syntax_error.txt | 20 +++++ .../functional/a/alternative_union_syntax_py37.py | 85 ++++++++++++++++++++++ .../functional/a/alternative_union_syntax_py37.rc | 3 + .../functional/a/alternative_union_syntax_py37.txt | 9 +++ 11 files changed, 334 insertions(+) create mode 100644 tests/functional/a/alternative_union_syntax.py create mode 100644 tests/functional/a/alternative_union_syntax.rc create mode 100644 tests/functional/a/alternative_union_syntax_error.py create mode 100644 tests/functional/a/alternative_union_syntax_error.rc create mode 100644 tests/functional/a/alternative_union_syntax_error.txt create mode 100644 tests/functional/a/alternative_union_syntax_py37.py create mode 100644 tests/functional/a/alternative_union_syntax_py37.rc create mode 100644 tests/functional/a/alternative_union_syntax_py37.txt diff --git a/ChangeLog b/ChangeLog index 0b50cb0e3..5a4ad73d6 100644 --- a/ChangeLog +++ b/ChangeLog @@ -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 diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index 2b39293ec..055292a60 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -75,11 +75,13 @@ from pylint.checkers.utils import ( 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, @@ -88,6 +90,7 @@ from pylint.checkers.utils import ( supports_membership_test, supports_setitem, ) +from pylint.constants import PY310_PLUS from pylint.interfaces import INFERENCE, IAstroidChecker from pylint.utils import get_global_option @@ -1629,6 +1632,38 @@ accessed. Python regular expressions are accepted.", # 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.""" diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index f9944b20d..404a5f0d6 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -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 diff --git a/tests/functional/a/alternative_union_syntax.py b/tests/functional/a/alternative_union_syntax.py new file mode 100644 index 000000000..175e5a9ba --- /dev/null +++ b/tests/functional/a/alternative_union_syntax.py @@ -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 diff --git a/tests/functional/a/alternative_union_syntax.rc b/tests/functional/a/alternative_union_syntax.rc new file mode 100644 index 000000000..68a8c8ef1 --- /dev/null +++ b/tests/functional/a/alternative_union_syntax.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.10 diff --git a/tests/functional/a/alternative_union_syntax_error.py b/tests/functional/a/alternative_union_syntax_error.py new file mode 100644 index 000000000..54f9f8f52 --- /dev/null +++ b/tests/functional/a/alternative_union_syntax_error.py @@ -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] diff --git a/tests/functional/a/alternative_union_syntax_error.rc b/tests/functional/a/alternative_union_syntax_error.rc new file mode 100644 index 000000000..776b99f68 --- /dev/null +++ b/tests/functional/a/alternative_union_syntax_error.rc @@ -0,0 +1,3 @@ +[testoptions] +min_pyver=3.8 +max_pyver=3.10 diff --git a/tests/functional/a/alternative_union_syntax_error.txt b/tests/functional/a/alternative_union_syntax_error.txt new file mode 100644 index 000000000..2149b3285 --- /dev/null +++ b/tests/functional/a/alternative_union_syntax_error.txt @@ -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 | diff --git a/tests/functional/a/alternative_union_syntax_py37.py b/tests/functional/a/alternative_union_syntax_py37.py new file mode 100644 index 000000000..79bc28fc4 --- /dev/null +++ b/tests/functional/a/alternative_union_syntax_py37.py @@ -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 diff --git a/tests/functional/a/alternative_union_syntax_py37.rc b/tests/functional/a/alternative_union_syntax_py37.rc new file mode 100644 index 000000000..776b99f68 --- /dev/null +++ b/tests/functional/a/alternative_union_syntax_py37.rc @@ -0,0 +1,3 @@ +[testoptions] +min_pyver=3.8 +max_pyver=3.10 diff --git a/tests/functional/a/alternative_union_syntax_py37.txt b/tests/functional/a/alternative_union_syntax_py37.txt new file mode 100644 index 000000000..c7172808e --- /dev/null +++ b/tests/functional/a/alternative_union_syntax_py37.txt @@ -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 | -- cgit v1.2.1