From 955c7bd29cfb9d743ff5d304d6698147156ba91b Mon Sep 17 00:00:00 2001 From: Philippe Prados Date: Tue, 27 Aug 2019 08:19:47 +0200 Subject: [PATCH 01/10] Accept __or__ operator in PyType_Type to generate Union type (see corresponding pull_request in cpython). Then, it's possible to use str | int in place of Union[str,int]. --- docs/source/cheat_sheet.rst | 5 +- docs/source/cheat_sheet_py3.rst | 3 +- docs/source/generics.rst | 3 +- docs/source/getting_started.rst | 5 +- docs/source/kinds_of_types.rst | 12 ++-- docs/source/literal_types.rst | 4 +- docs/source/more_types.rst | 14 ++--- .../source/type_inference_and_annotations.rst | 6 +- mypy/fastparse.py | 20 +++++- mypy/test/testcheck.py | 1 + test-data/unit/check-union-or-syntax.test | 62 +++++++++++++++++++ 11 files changed, 109 insertions(+), 26 deletions(-) create mode 100644 test-data/unit/check-union-or-syntax.test diff --git a/docs/source/cheat_sheet.rst b/docs/source/cheat_sheet.rst index 0007f33bfcd4..bf6fd3f18d91 100644 --- a/docs/source/cheat_sheet.rst +++ b/docs/source/cheat_sheet.rst @@ -103,7 +103,7 @@ Functions i += 1 # There's an alternative syntax for functions with many arguments - def send_email(address, # type: Union[str, List[str]] + def send_email(address, # type: str | List[str] sender, # type: str cc, # type: Optional[List[str]] bcc, # type: Optional[List[str]] @@ -126,7 +126,8 @@ When you're puzzled or when things are complicated reveal_type(1) # -> Revealed type is 'builtins.int' # Use Union when something could be one of a few types - x = [3, 5, "test", "fun"] # type: List[Union[int, str]] + # Python 3.8+ (or List[Union[int, str]]) + x = [3, 5, "test", "fun"] # type: List[int | str] # Use Any if you don't know the type of something or it's too # dynamic to write a type for diff --git a/docs/source/cheat_sheet_py3.rst b/docs/source/cheat_sheet_py3.rst index 3e75d1a9367e..e3bbd5c3896b 100644 --- a/docs/source/cheat_sheet_py3.rst +++ b/docs/source/cheat_sheet_py3.rst @@ -143,7 +143,8 @@ When you're puzzled or when things are complicated reveal_type(1) # -> Revealed type is 'builtins.int' # Use Union when something could be one of a few types - x: List[Union[int, str]] = [3, 5, "test", "fun"] + # Python 3.8+ (or List[Union[int,str]]) + x: List[int | str]] = [3, 5, "test", "fun"] # Use Any if you don't know the type of something or it's too # dynamic to write a type for diff --git a/docs/source/generics.rst b/docs/source/generics.rst index 817466d2469a..52bccdc7749b 100644 --- a/docs/source/generics.rst +++ b/docs/source/generics.rst @@ -443,7 +443,8 @@ will reject this function: .. code-block:: python - def union_concat(x: Union[str, bytes], y: Union[str, bytes]) -> Union[str, bytes]: + def union_concat(x: str | bytes, y: str | bytes) -> str | bytes: + # or def union_concat(x: Union[str, bytes], y: Union[str, bytes]) -> Union[str, bytes]: return x + y # Error: can't concatenate str and bytes Another interesting special case is calling ``concat()`` with a diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index 08e614c73984..875f9e570b25 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -211,14 +211,15 @@ ints or strings, but no other types. You can express this using the :py:data:`~t from typing import Union - def normalize_id(user_id: Union[int, str]) -> str: + def normalize_id(user_id: int | str) -> str: + # or def normalize_id(user_id: Union[int, str]) -> str: if isinstance(user_id, int): return 'user-{}'.format(100000 + user_id) else: return user_id Similarly, suppose that you want the function to accept only strings or ``None``. You can -again use :py:data:`~typing.Union` and use ``Union[str, None]`` -- or alternatively, use the type +again use :py:data:`~typing.Union` and use ``str | None`` (or ``Union[str, None]``) -- or alternatively, use the type ``Optional[str]``. These two types are identical and interchangeable: ``Optional[str]`` is just a shorthand or *alias* for ``Union[str, None]``. It exists mostly as a convenience to help function signatures look a little cleaner: diff --git a/docs/source/kinds_of_types.rst b/docs/source/kinds_of_types.rst index 263534b59573..5bd73bca0642 100644 --- a/docs/source/kinds_of_types.rst +++ b/docs/source/kinds_of_types.rst @@ -208,8 +208,8 @@ Python functions often accept values of two or more different types. You can use :ref:`overloading ` to represent this, but union types are often more convenient. -Use the ``Union[T1, ..., Tn]`` type constructor to construct a union -type. For example, if an argument has type ``Union[int, str]``, both +Use the ``T1|...|Tn`` (or ``Union[T1, ..., Tn]``) type constructor to construct a union +type. For example, if an argument has type ``int|str`` (or ``Union[int, str]``), both integers and strings are valid argument values. You can use an :py:func:`isinstance` check to narrow down a union type to a @@ -219,7 +219,7 @@ more specific type: from typing import Union - def f(x: Union[int, str]) -> None: + def f(x: int | str) -> None: x + 1 # Error: str + int is not valid if isinstance(x, int): # Here type of x is int. @@ -248,7 +248,7 @@ Optional types and the None type You can use the :py:data:`~typing.Optional` type modifier to define a type variant that allows ``None``, such as ``Optional[int]`` (``Optional[X]`` is -the preferred shorthand for ``Union[X, None]``): +the preferred shorthand for ``X | None`` or ``Union[X, None]``): .. code-block:: python @@ -560,7 +560,7 @@ In certain situations, type names may end up being long and painful to type: .. code-block:: python - def f() -> Union[List[Dict[Tuple[int, str], Set[int]]], Tuple[str, List[str]]]: + def f() -> List[Dict[Tuple[int, str], Set[int]]] | Tuple[str, List[str]]: ... When cases like this arise, you can define a type alias by simply @@ -568,7 +568,7 @@ assigning the type to a variable: .. code-block:: python - AliasType = Union[List[Dict[Tuple[int, str], Set[int]]], Tuple[str, List[str]]] + AliasType = List[Dict[Tuple[int, str], Set[int]]] | Tuple[str, List[str]] # Now we can use AliasType in place of the full name: diff --git a/docs/source/literal_types.rst b/docs/source/literal_types.rst index 71c60caab549..9ef54033a1fe 100644 --- a/docs/source/literal_types.rst +++ b/docs/source/literal_types.rst @@ -30,9 +30,9 @@ precise type signature for this function using ``Literal[...]`` and overloads: # provides a regular bool: @overload - def fetch_data(raw: bool) -> Union[bytes, str]: ... + def fetch_data(raw: bool) -> bytes | str: ... - def fetch_data(raw: bool) -> Union[bytes, str]: + def fetch_data(raw: bool) -> bytes | str: # Implementation is omitted ... diff --git a/docs/source/more_types.rst b/docs/source/more_types.rst index 3a962553e68a..06c0962301b2 100644 --- a/docs/source/more_types.rst +++ b/docs/source/more_types.rst @@ -203,7 +203,7 @@ Our first attempt at writing this function might look like this: def mouse_event(x1: int, y1: int, x2: Optional[int] = None, - y2: Optional[int] = None) -> Union[ClickEvent, DragEvent]: + y2: Optional[int] = None) -> ClickEvent | DragEvent: if x2 is None and y2 is None: return ClickEvent(x1, y1) elif x2 is not None and y2 is not None: @@ -247,7 +247,7 @@ to more accurately describe the function's behavior: def mouse_event(x1: int, y1: int, x2: Optional[int] = None, - y2: Optional[int] = None) -> Union[ClickEvent, DragEvent]: + y2: Optional[int] = None) -> ClickEvent | DragEvent: if x2 is None and y2 is None: return ClickEvent(x1, y1) elif x2 is not None and y2 is not None: @@ -281,7 +281,7 @@ return type by using overloads like so: @overload def __getitem__(self, index: slice) -> Sequence[T]: ... - def __getitem__(self, index: Union[int, slice]) -> Union[T, Sequence[T]]: + def __getitem__(self, index: int | slice) -> T | Sequence[T]: if isinstance(index, int): # Return a T here elif isinstance(index, slice): @@ -378,7 +378,7 @@ matching variant returns: .. code-block:: python - some_list: Union[List[int], List[str]] + some_list: List[int] | List[str] # output3 is of type 'Union[float, str]' output3 = summarize(some_list) @@ -466,7 +466,7 @@ the following unsafe overload definition: @overload def unsafe_func(x: object) -> str: ... - def unsafe_func(x: object) -> Union[int, str]: + def unsafe_func(x: object) -> int | str: if isinstance(x, int): return 42 else: @@ -532,8 +532,8 @@ Type checking the implementation The body of an implementation is type-checked against the type hints provided on the implementation. For example, in the ``MyList`` example up above, the code in the body is checked with -argument list ``index: Union[int, slice]`` and a return type of -``Union[T, Sequence[T]]``. If there are no annotations on the +argument list ``index: int | slice`` and a return type of +``T | Sequence[T]``. If there are no annotations on the implementation, then the body is not type checked. If you want to force mypy to check the body anyways, use the :option:`--check-untyped-defs ` flag (:ref:`more details here `). diff --git a/docs/source/type_inference_and_annotations.rst b/docs/source/type_inference_and_annotations.rst index 16e24b2c7045..dfd893187a40 100644 --- a/docs/source/type_inference_and_annotations.rst +++ b/docs/source/type_inference_and_annotations.rst @@ -30,7 +30,7 @@ variable type annotation: from typing import Union - x: Union[int, str] = 1 + x: int | str = 1 Without the type annotation, the type of ``x`` would be just ``int``. We use an annotation to give it a more general type ``Union[int, str]`` (this @@ -42,7 +42,7 @@ type: .. code-block:: python - x: Union[int, str] = 1.1 # Error! + x: int | str = 1.1 # Error! The variable annotation syntax is available starting from Python 3.6. In earlier Python versions, you can use a special comment after an @@ -50,7 +50,7 @@ assignment statement to declare the type of a variable: .. code-block:: python - x = 1 # type: Union[int, str] + x = 1 # type: int | str We'll use both syntax variants in examples. The syntax variants are mostly interchangeable, but the variable annotation syntax allows diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 3319cd648957..1a66987f5d0d 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -31,8 +31,8 @@ ) from mypy.types import ( Type, CallableType, AnyType, UnboundType, TupleType, TypeList, EllipsisType, CallableArgument, - TypeOfAny, Instance, RawExpressionType, ProperType -) + TypeOfAny, Instance, RawExpressionType, ProperType, + UnionType) from mypy import defaults from mypy import message_registry, errorcodes as codes from mypy.errors import Errors @@ -1422,6 +1422,22 @@ def _extract_argument_name(self, n: ast3.expr) -> Optional[str]: def visit_Name(self, n: Name) -> Type: return UnboundType(n.id, line=self.line, column=self.convert_column(n.col_offset)) + def visit_BinOp(self, n: ast3.BinOp) -> Type: + if not isinstance(n.op, ast3.BitOr): + # invalid_type + return RawExpressionType( + None, + 'typing.Any', + line=self.line, + column=getattr(n, 'col_offset', -1) + ) + + left = self.visit(n.left) + right = self.visit(n.right) + return UnionType([left, right], + line=self.line, + column=self.convert_column(n.col_offset)) + def visit_NameConstant(self, n: NameConstant) -> Type: if isinstance(n.value, bool): return RawExpressionType(n.value, 'builtins.bool', line=self.line) diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py index f266a474a59a..35f73a5c75bc 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -25,6 +25,7 @@ # List of files that contain test case descriptions. typecheck_files = [ 'check-basic.test', + 'check-union-or-syntax.test', 'check-callable.test', 'check-classes.test', 'check-statements.test', diff --git a/test-data/unit/check-union-or-syntax.test b/test-data/unit/check-union-or-syntax.test new file mode 100644 index 000000000000..65a6a14f7508 --- /dev/null +++ b/test-data/unit/check-union-or-syntax.test @@ -0,0 +1,62 @@ +-- Type checking of union types with '|' syntax + +[case testUnionOrSyntaxWithTwoBuiltinsTypes] +# flags: --python-version 3.9 +def f(x: int | str) -> int | str: + reveal_type(x) # N: Revealed type is 'Union[builtins.int, builtins.str]' + z: int | str = 0 + reveal_type(z) # N: Revealed type is 'Union[builtins.int, builtins.str]' + return x +reveal_type(f) # N: Revealed type is 'def (x: Union[builtins.int, builtins.str]) -> Union[builtins.int, builtins.str]' + +[case testUnionOrSyntaxWithThreeBuiltinsTypes] +# flags: --python-version 3.9 +def f(x: int | str | float) -> int | str | float: + reveal_type(x) # N: Revealed type is 'Union[builtins.int, builtins.str, builtins.float]' + z: int | str | float = 0 + reveal_type(z) # N: Revealed type is 'Union[builtins.int, builtins.str, builtins.float]' + return x + +reveal_type(f) # N: Revealed type is 'def (x: Union[builtins.int, builtins.str, builtins.float]) -> Union[builtins.int, builtins.str, builtins.float]' + +[case testUnionOrSyntaxWithTwoTypes] +# flags: --python-version 3.9 +class A: pass +class B: pass +def f(x: A | B) -> A | B: + reveal_type(x) # N: Revealed type is 'Union[__main__.A, __main__.B]' + z: A | B = A() + reveal_type(z) # N: Revealed type is 'Union[__main__.A, __main__.B]' + return x +reveal_type(f) # N: Revealed type is 'def (x: Union[__main__.A, __main__.B]) -> Union[__main__.A, __main__.B]' + +[case testUnionOrSyntaxWithThreeTypes] +# flags: --python-version 3.9 +class A: pass +class B: pass +class C: pass +def f(x: A | B | C) -> A | B | C: + reveal_type(x) # N: Revealed type is 'Union[__main__.A, __main__.B, __main__.C]' + z: A | B | C = A() + reveal_type(z) # N: Revealed type is 'Union[__main__.A, __main__.B, __main__.C]' + return x +reveal_type(f) # N: Revealed type is 'def (x: Union[__main__.A, __main__.B, __main__.C]) -> Union[__main__.A, __main__.B, __main__.C]' + +[case testUnionOrSyntaxWithLiteral] +# flags: --python-version 3.9 +from typing_extensions import Literal +reveal_type(Literal[4] | str) # N: Revealed type is 'Any' + +[case testUnionOrSyntaxWithBadOperator] +# flags: --python-version 3.9 +x: 1 + 2 # E: Invalid type comment or annotation + +[case testUnionOrSyntaxWithBadOperands] +# flags: --python-version 3.9 +x: int | 42 # E: Invalid type: try using Literal[42] instead? +y: 42 | int # E: Invalid type: try using Literal[42] instead? +z: str | 42 | int # E: Invalid type: try using Literal[42] instead? + +[case testUnionOrSyntaxInComment] +# flags: --python-version 3.9 +x = 1 # type: int | str From 88cb5fe93db6c0da4728ef993eab674ead108cf6 Mon Sep 17 00:00:00 2001 From: Philippe Prados Date: Thu, 31 Oct 2019 14:16:50 +0100 Subject: [PATCH 02/10] Compatible PEP604 --- docs/source/cheat_sheet.rst | 2 +- docs/source/cheat_sheet_py3.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/cheat_sheet.rst b/docs/source/cheat_sheet.rst index bf6fd3f18d91..6ba6e612b789 100644 --- a/docs/source/cheat_sheet.rst +++ b/docs/source/cheat_sheet.rst @@ -126,7 +126,7 @@ When you're puzzled or when things are complicated reveal_type(1) # -> Revealed type is 'builtins.int' # Use Union when something could be one of a few types - # Python 3.8+ (or List[Union[int, str]]) + # Python 3.9+ (or List[Union[int, str]]) x = [3, 5, "test", "fun"] # type: List[int | str] # Use Any if you don't know the type of something or it's too diff --git a/docs/source/cheat_sheet_py3.rst b/docs/source/cheat_sheet_py3.rst index e3bbd5c3896b..c8603d380568 100644 --- a/docs/source/cheat_sheet_py3.rst +++ b/docs/source/cheat_sheet_py3.rst @@ -143,7 +143,7 @@ When you're puzzled or when things are complicated reveal_type(1) # -> Revealed type is 'builtins.int' # Use Union when something could be one of a few types - # Python 3.8+ (or List[Union[int,str]]) + # Python 3.9+ (or List[Union[int,str]]) x: List[int | str]] = [3, 5, "test", "fun"] # Use Any if you don't know the type of something or it's too From b9afd7a50e81b05b9956d48ab03fa746a11e35f9 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Tue, 27 Oct 2020 00:23:04 +0100 Subject: [PATCH 03/10] Update mypy/fastparse.py Co-authored-by: Shantanu <12621235+hauntsaninja@users.noreply.github.com> --- mypy/fastparse.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 1a66987f5d0d..88efc64d4178 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -1424,13 +1424,7 @@ def visit_Name(self, n: Name) -> Type: def visit_BinOp(self, n: ast3.BinOp) -> Type: if not isinstance(n.op, ast3.BitOr): - # invalid_type - return RawExpressionType( - None, - 'typing.Any', - line=self.line, - column=getattr(n, 'col_offset', -1) - ) + return self.invalid_type(n) left = self.visit(n.left) right = self.visit(n.right) From 584abd292ab912994d3f82e74df43b708a661482 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Tue, 27 Oct 2020 00:24:43 +0100 Subject: [PATCH 04/10] Fix tests --- test-data/unit/check-union-or-syntax.test | 1 + 1 file changed, 1 insertion(+) diff --git a/test-data/unit/check-union-or-syntax.test b/test-data/unit/check-union-or-syntax.test index 65a6a14f7508..aa04c1bd360e 100644 --- a/test-data/unit/check-union-or-syntax.test +++ b/test-data/unit/check-union-or-syntax.test @@ -46,6 +46,7 @@ reveal_type(f) # N: Revealed type is 'def (x: Union[__main__.A, __main__.B, __m # flags: --python-version 3.9 from typing_extensions import Literal reveal_type(Literal[4] | str) # N: Revealed type is 'Any' +[builtins fixtures/tuple.pyi] [case testUnionOrSyntaxWithBadOperator] # flags: --python-version 3.9 From 3be27436558f2d987d92943e4538472d775b48cc Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Tue, 27 Oct 2020 00:53:34 +0100 Subject: [PATCH 05/10] Remove documentation changes --- docs/source/cheat_sheet.rst | 5 ++--- docs/source/cheat_sheet_py3.rst | 3 +-- docs/source/generics.rst | 3 +-- docs/source/getting_started.rst | 5 ++--- docs/source/kinds_of_types.rst | 12 ++++++------ docs/source/literal_types.rst | 4 ++-- docs/source/more_types.rst | 14 +++++++------- docs/source/type_inference_and_annotations.rst | 6 +++--- 8 files changed, 24 insertions(+), 28 deletions(-) diff --git a/docs/source/cheat_sheet.rst b/docs/source/cheat_sheet.rst index 6ba6e612b789..0007f33bfcd4 100644 --- a/docs/source/cheat_sheet.rst +++ b/docs/source/cheat_sheet.rst @@ -103,7 +103,7 @@ Functions i += 1 # There's an alternative syntax for functions with many arguments - def send_email(address, # type: str | List[str] + def send_email(address, # type: Union[str, List[str]] sender, # type: str cc, # type: Optional[List[str]] bcc, # type: Optional[List[str]] @@ -126,8 +126,7 @@ When you're puzzled or when things are complicated reveal_type(1) # -> Revealed type is 'builtins.int' # Use Union when something could be one of a few types - # Python 3.9+ (or List[Union[int, str]]) - x = [3, 5, "test", "fun"] # type: List[int | str] + x = [3, 5, "test", "fun"] # type: List[Union[int, str]] # Use Any if you don't know the type of something or it's too # dynamic to write a type for diff --git a/docs/source/cheat_sheet_py3.rst b/docs/source/cheat_sheet_py3.rst index c8603d380568..3e75d1a9367e 100644 --- a/docs/source/cheat_sheet_py3.rst +++ b/docs/source/cheat_sheet_py3.rst @@ -143,8 +143,7 @@ When you're puzzled or when things are complicated reveal_type(1) # -> Revealed type is 'builtins.int' # Use Union when something could be one of a few types - # Python 3.9+ (or List[Union[int,str]]) - x: List[int | str]] = [3, 5, "test", "fun"] + x: List[Union[int, str]] = [3, 5, "test", "fun"] # Use Any if you don't know the type of something or it's too # dynamic to write a type for diff --git a/docs/source/generics.rst b/docs/source/generics.rst index 52bccdc7749b..817466d2469a 100644 --- a/docs/source/generics.rst +++ b/docs/source/generics.rst @@ -443,8 +443,7 @@ will reject this function: .. code-block:: python - def union_concat(x: str | bytes, y: str | bytes) -> str | bytes: - # or def union_concat(x: Union[str, bytes], y: Union[str, bytes]) -> Union[str, bytes]: + def union_concat(x: Union[str, bytes], y: Union[str, bytes]) -> Union[str, bytes]: return x + y # Error: can't concatenate str and bytes Another interesting special case is calling ``concat()`` with a diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index 875f9e570b25..08e614c73984 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -211,15 +211,14 @@ ints or strings, but no other types. You can express this using the :py:data:`~t from typing import Union - def normalize_id(user_id: int | str) -> str: - # or def normalize_id(user_id: Union[int, str]) -> str: + def normalize_id(user_id: Union[int, str]) -> str: if isinstance(user_id, int): return 'user-{}'.format(100000 + user_id) else: return user_id Similarly, suppose that you want the function to accept only strings or ``None``. You can -again use :py:data:`~typing.Union` and use ``str | None`` (or ``Union[str, None]``) -- or alternatively, use the type +again use :py:data:`~typing.Union` and use ``Union[str, None]`` -- or alternatively, use the type ``Optional[str]``. These two types are identical and interchangeable: ``Optional[str]`` is just a shorthand or *alias* for ``Union[str, None]``. It exists mostly as a convenience to help function signatures look a little cleaner: diff --git a/docs/source/kinds_of_types.rst b/docs/source/kinds_of_types.rst index 5bd73bca0642..263534b59573 100644 --- a/docs/source/kinds_of_types.rst +++ b/docs/source/kinds_of_types.rst @@ -208,8 +208,8 @@ Python functions often accept values of two or more different types. You can use :ref:`overloading ` to represent this, but union types are often more convenient. -Use the ``T1|...|Tn`` (or ``Union[T1, ..., Tn]``) type constructor to construct a union -type. For example, if an argument has type ``int|str`` (or ``Union[int, str]``), both +Use the ``Union[T1, ..., Tn]`` type constructor to construct a union +type. For example, if an argument has type ``Union[int, str]``, both integers and strings are valid argument values. You can use an :py:func:`isinstance` check to narrow down a union type to a @@ -219,7 +219,7 @@ more specific type: from typing import Union - def f(x: int | str) -> None: + def f(x: Union[int, str]) -> None: x + 1 # Error: str + int is not valid if isinstance(x, int): # Here type of x is int. @@ -248,7 +248,7 @@ Optional types and the None type You can use the :py:data:`~typing.Optional` type modifier to define a type variant that allows ``None``, such as ``Optional[int]`` (``Optional[X]`` is -the preferred shorthand for ``X | None`` or ``Union[X, None]``): +the preferred shorthand for ``Union[X, None]``): .. code-block:: python @@ -560,7 +560,7 @@ In certain situations, type names may end up being long and painful to type: .. code-block:: python - def f() -> List[Dict[Tuple[int, str], Set[int]]] | Tuple[str, List[str]]: + def f() -> Union[List[Dict[Tuple[int, str], Set[int]]], Tuple[str, List[str]]]: ... When cases like this arise, you can define a type alias by simply @@ -568,7 +568,7 @@ assigning the type to a variable: .. code-block:: python - AliasType = List[Dict[Tuple[int, str], Set[int]]] | Tuple[str, List[str]] + AliasType = Union[List[Dict[Tuple[int, str], Set[int]]], Tuple[str, List[str]]] # Now we can use AliasType in place of the full name: diff --git a/docs/source/literal_types.rst b/docs/source/literal_types.rst index 9ef54033a1fe..71c60caab549 100644 --- a/docs/source/literal_types.rst +++ b/docs/source/literal_types.rst @@ -30,9 +30,9 @@ precise type signature for this function using ``Literal[...]`` and overloads: # provides a regular bool: @overload - def fetch_data(raw: bool) -> bytes | str: ... + def fetch_data(raw: bool) -> Union[bytes, str]: ... - def fetch_data(raw: bool) -> bytes | str: + def fetch_data(raw: bool) -> Union[bytes, str]: # Implementation is omitted ... diff --git a/docs/source/more_types.rst b/docs/source/more_types.rst index 06c0962301b2..3a962553e68a 100644 --- a/docs/source/more_types.rst +++ b/docs/source/more_types.rst @@ -203,7 +203,7 @@ Our first attempt at writing this function might look like this: def mouse_event(x1: int, y1: int, x2: Optional[int] = None, - y2: Optional[int] = None) -> ClickEvent | DragEvent: + y2: Optional[int] = None) -> Union[ClickEvent, DragEvent]: if x2 is None and y2 is None: return ClickEvent(x1, y1) elif x2 is not None and y2 is not None: @@ -247,7 +247,7 @@ to more accurately describe the function's behavior: def mouse_event(x1: int, y1: int, x2: Optional[int] = None, - y2: Optional[int] = None) -> ClickEvent | DragEvent: + y2: Optional[int] = None) -> Union[ClickEvent, DragEvent]: if x2 is None and y2 is None: return ClickEvent(x1, y1) elif x2 is not None and y2 is not None: @@ -281,7 +281,7 @@ return type by using overloads like so: @overload def __getitem__(self, index: slice) -> Sequence[T]: ... - def __getitem__(self, index: int | slice) -> T | Sequence[T]: + def __getitem__(self, index: Union[int, slice]) -> Union[T, Sequence[T]]: if isinstance(index, int): # Return a T here elif isinstance(index, slice): @@ -378,7 +378,7 @@ matching variant returns: .. code-block:: python - some_list: List[int] | List[str] + some_list: Union[List[int], List[str]] # output3 is of type 'Union[float, str]' output3 = summarize(some_list) @@ -466,7 +466,7 @@ the following unsafe overload definition: @overload def unsafe_func(x: object) -> str: ... - def unsafe_func(x: object) -> int | str: + def unsafe_func(x: object) -> Union[int, str]: if isinstance(x, int): return 42 else: @@ -532,8 +532,8 @@ Type checking the implementation The body of an implementation is type-checked against the type hints provided on the implementation. For example, in the ``MyList`` example up above, the code in the body is checked with -argument list ``index: int | slice`` and a return type of -``T | Sequence[T]``. If there are no annotations on the +argument list ``index: Union[int, slice]`` and a return type of +``Union[T, Sequence[T]]``. If there are no annotations on the implementation, then the body is not type checked. If you want to force mypy to check the body anyways, use the :option:`--check-untyped-defs ` flag (:ref:`more details here `). diff --git a/docs/source/type_inference_and_annotations.rst b/docs/source/type_inference_and_annotations.rst index dfd893187a40..16e24b2c7045 100644 --- a/docs/source/type_inference_and_annotations.rst +++ b/docs/source/type_inference_and_annotations.rst @@ -30,7 +30,7 @@ variable type annotation: from typing import Union - x: int | str = 1 + x: Union[int, str] = 1 Without the type annotation, the type of ``x`` would be just ``int``. We use an annotation to give it a more general type ``Union[int, str]`` (this @@ -42,7 +42,7 @@ type: .. code-block:: python - x: int | str = 1.1 # Error! + x: Union[int, str] = 1.1 # Error! The variable annotation syntax is available starting from Python 3.6. In earlier Python versions, you can use a special comment after an @@ -50,7 +50,7 @@ assignment statement to declare the type of a variable: .. code-block:: python - x = 1 # type: int | str + x = 1 # type: Union[int, str] We'll use both syntax variants in examples. The syntax variants are mostly interchangeable, but the variable annotation syntax allows From 61d4c6c26139ee9226c78bb15d6a1d06734899f4 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Sun, 1 Nov 2020 01:49:19 +0100 Subject: [PATCH 06/10] Added future flag check --- mypy/fastparse.py | 3 +- mypy/typeanal.py | 5 ++++ mypy/types.py | 4 ++- test-data/unit/check-union-or-syntax.test | 34 +++++++++++++++++------ 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 88efc64d4178..7e38c8c965be 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -1430,7 +1430,8 @@ def visit_BinOp(self, n: ast3.BinOp) -> Type: right = self.visit(n.right) return UnionType([left, right], line=self.line, - column=self.convert_column(n.col_offset)) + column=self.convert_column(n.col_offset), + is_binary_op=True) def visit_NameConstant(self, n: NameConstant) -> Type: if isinstance(n.value, bool): diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 7a7408d351e1..27d8ee78d69c 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -605,6 +605,11 @@ def visit_star_type(self, t: StarType) -> Type: return StarType(self.anal_type(t.type), t.line) def visit_union_type(self, t: UnionType) -> Type: + if (t.is_binary_op is True + and self.api.is_stub_file is False + and self.options.python_version < (3, 10) + and self.api.is_future_flag_set('annotations') is False): + self.fail("Alternative syntax for unions requires Python 3.10 or newer", t) return UnionType(self.anal_array(t.items), t.line) def visit_partial_type(self, t: PartialType) -> Type: diff --git a/mypy/types.py b/mypy/types.py index a2651a01b37a..3d52bce70627 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1724,11 +1724,13 @@ class UnionType(ProperType): __slots__ = ('items',) - def __init__(self, items: Sequence[Type], line: int = -1, column: int = -1) -> None: + def __init__(self, items: Sequence[Type], line: int = -1, column: int = -1, + is_binary_op: bool = False) -> None: super().__init__(line, column) self.items = flatten_nested_unions(items) self.can_be_true = any(item.can_be_true for item in items) self.can_be_false = any(item.can_be_false for item in items) + self.is_binary_op = is_binary_op def __hash__(self) -> int: return hash(frozenset(self.items)) diff --git a/test-data/unit/check-union-or-syntax.test b/test-data/unit/check-union-or-syntax.test index aa04c1bd360e..745c35f29210 100644 --- a/test-data/unit/check-union-or-syntax.test +++ b/test-data/unit/check-union-or-syntax.test @@ -1,16 +1,18 @@ -- Type checking of union types with '|' syntax [case testUnionOrSyntaxWithTwoBuiltinsTypes] -# flags: --python-version 3.9 +# flags: --python-version 3.10 +from __future__ import annotations def f(x: int | str) -> int | str: reveal_type(x) # N: Revealed type is 'Union[builtins.int, builtins.str]' z: int | str = 0 reveal_type(z) # N: Revealed type is 'Union[builtins.int, builtins.str]' return x reveal_type(f) # N: Revealed type is 'def (x: Union[builtins.int, builtins.str]) -> Union[builtins.int, builtins.str]' +[builtins fixtures/tuple.pyi] [case testUnionOrSyntaxWithThreeBuiltinsTypes] -# flags: --python-version 3.9 +# flags: --python-version 3.10 def f(x: int | str | float) -> int | str | float: reveal_type(x) # N: Revealed type is 'Union[builtins.int, builtins.str, builtins.float]' z: int | str | float = 0 @@ -20,7 +22,7 @@ def f(x: int | str | float) -> int | str | float: reveal_type(f) # N: Revealed type is 'def (x: Union[builtins.int, builtins.str, builtins.float]) -> Union[builtins.int, builtins.str, builtins.float]' [case testUnionOrSyntaxWithTwoTypes] -# flags: --python-version 3.9 +# flags: --python-version 3.10 class A: pass class B: pass def f(x: A | B) -> A | B: @@ -31,7 +33,7 @@ def f(x: A | B) -> A | B: reveal_type(f) # N: Revealed type is 'def (x: Union[__main__.A, __main__.B]) -> Union[__main__.A, __main__.B]' [case testUnionOrSyntaxWithThreeTypes] -# flags: --python-version 3.9 +# flags: --python-version 3.10 class A: pass class B: pass class C: pass @@ -43,21 +45,37 @@ def f(x: A | B | C) -> A | B | C: reveal_type(f) # N: Revealed type is 'def (x: Union[__main__.A, __main__.B, __main__.C]) -> Union[__main__.A, __main__.B, __main__.C]' [case testUnionOrSyntaxWithLiteral] -# flags: --python-version 3.9 +# flags: --python-version 3.10 from typing_extensions import Literal reveal_type(Literal[4] | str) # N: Revealed type is 'Any' [builtins fixtures/tuple.pyi] [case testUnionOrSyntaxWithBadOperator] -# flags: --python-version 3.9 +# flags: --python-version 3.10 x: 1 + 2 # E: Invalid type comment or annotation [case testUnionOrSyntaxWithBadOperands] -# flags: --python-version 3.9 +# flags: --python-version 3.10 x: int | 42 # E: Invalid type: try using Literal[42] instead? y: 42 | int # E: Invalid type: try using Literal[42] instead? z: str | 42 | int # E: Invalid type: try using Literal[42] instead? [case testUnionOrSyntaxInComment] -# flags: --python-version 3.9 +# flags: --python-version 3.10 x = 1 # type: int | str + +[case testUnionOrSyntaxFutureImport] +# flags: --python-version 3.7 +from __future__ import annotations +x: int | None +[builtins fixtures/tuple.pyi] + +[case testUnionOrSyntaxMissingFutureImport] +# flags: --python-version 3.9 +x: int | None # E: Alternative syntax for unions requires Python 3.10 or newer + +[case testUnionOrSyntaxInStubFile] +# flags: --python-version 3.6 +from lib import x +[file lib.pyi] +x: int | None From c429e45d595255832e282db1606ec5323c1cf065 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Sun, 1 Nov 2020 11:52:17 +0100 Subject: [PATCH 07/10] Review changes + mypyc fix --- mypy/fastparse.py | 2 +- mypy/semanal.py | 8 ++++++-- mypy/semanal_shared.py | 5 +++++ mypy/typeanal.py | 4 ++-- mypy/types.py | 6 +++--- test-data/unit/check-union-or-syntax.test | 2 +- 6 files changed, 18 insertions(+), 9 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 7e38c8c965be..70ce37560704 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -1431,7 +1431,7 @@ def visit_BinOp(self, n: ast3.BinOp) -> Type: return UnionType([left, right], line=self.line, column=self.convert_column(n.col_offset), - is_binary_op=True) + uses_pep604_syntax=True) def visit_NameConstant(self, n: NameConstant) -> Type: if isinstance(n.value, bool): diff --git a/mypy/semanal.py b/mypy/semanal.py index cf02e967242c..9d000df04da1 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -206,7 +206,7 @@ class SemanticAnalyzer(NodeVisitor[None], patches = None # type: List[Tuple[int, Callable[[], None]]] loop_depth = 0 # Depth of breakable loops cur_mod_id = '' # Current module id (or None) (phase 2) - is_stub_file = False # Are we analyzing a stub file? + _is_stub_file = False # Are we analyzing a stub file? _is_typeshed_stub_file = False # Are we analyzing a typeshed stub file? imports = None # type: Set[str] # Imported modules (during phase 2 analysis) # Note: some imports (and therefore dependencies) might @@ -280,6 +280,10 @@ def __init__(self, # mypyc doesn't properly handle implementing an abstractproperty # with a regular attribute so we make them properties + @property + def is_stub_file(self) -> bool: + return self._is_stub_file + @property def is_typeshed_stub_file(self) -> bool: return self._is_typeshed_stub_file @@ -507,7 +511,7 @@ def file_context(self, self.cur_mod_node = file_node self.cur_mod_id = file_node.fullname scope.enter_file(self.cur_mod_id) - self.is_stub_file = file_node.path.lower().endswith('.pyi') + self._is_stub_file = file_node.path.lower().endswith('.pyi') self._is_typeshed_stub_file = is_typeshed_file(file_node.path) self.globals = file_node.names self.tvar_scope = TypeVarLikeScope() diff --git a/mypy/semanal_shared.py b/mypy/semanal_shared.py index ac7dd7cfc26f..87a5d28b4c2c 100644 --- a/mypy/semanal_shared.py +++ b/mypy/semanal_shared.py @@ -78,6 +78,11 @@ def is_future_flag_set(self, flag: str) -> bool: """Is the specific __future__ feature imported""" raise NotImplementedError + @property + @abstractmethod + def is_stub_file(self) -> bool: + raise NotImplementedError + @trait class SemanticAnalyzerInterface(SemanticAnalyzerCoreInterface): diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 27d8ee78d69c..7c43e834d248 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -605,11 +605,11 @@ def visit_star_type(self, t: StarType) -> Type: return StarType(self.anal_type(t.type), t.line) def visit_union_type(self, t: UnionType) -> Type: - if (t.is_binary_op is True + if (t.uses_pep604_syntax is True and self.api.is_stub_file is False and self.options.python_version < (3, 10) and self.api.is_future_flag_set('annotations') is False): - self.fail("Alternative syntax for unions requires Python 3.10 or newer", t) + self.fail("X | Y syntax for unions requires Python 3.10", t) return UnionType(self.anal_array(t.items), t.line) def visit_partial_type(self, t: PartialType) -> Type: diff --git a/mypy/types.py b/mypy/types.py index 3d52bce70627..70fc76dc3af5 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1722,15 +1722,15 @@ def serialize(self) -> JsonDict: class UnionType(ProperType): """The union type Union[T1, ..., Tn] (at least one type argument).""" - __slots__ = ('items',) + __slots__ = ('items', 'uses_pep604_syntax') def __init__(self, items: Sequence[Type], line: int = -1, column: int = -1, - is_binary_op: bool = False) -> None: + uses_pep604_syntax: bool = False) -> None: super().__init__(line, column) self.items = flatten_nested_unions(items) self.can_be_true = any(item.can_be_true for item in items) self.can_be_false = any(item.can_be_false for item in items) - self.is_binary_op = is_binary_op + self.uses_pep604_syntax = uses_pep604_syntax def __hash__(self) -> int: return hash(frozenset(self.items)) diff --git a/test-data/unit/check-union-or-syntax.test b/test-data/unit/check-union-or-syntax.test index 745c35f29210..49f031931bc5 100644 --- a/test-data/unit/check-union-or-syntax.test +++ b/test-data/unit/check-union-or-syntax.test @@ -72,7 +72,7 @@ x: int | None [case testUnionOrSyntaxMissingFutureImport] # flags: --python-version 3.9 -x: int | None # E: Alternative syntax for unions requires Python 3.10 or newer +x: int | None # E: X | Y syntax for unions requires Python 3.10 [case testUnionOrSyntaxInStubFile] # flags: --python-version 3.6 From 533202eabb3c256969890799da54ea3dcb3829b7 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Sun, 1 Nov 2020 13:20:57 +0100 Subject: [PATCH 08/10] Fixed check for type comments --- mypy/fastparse.py | 19 ++++++++++++++----- mypy/typeanal.py | 4 +++- mypy/types.py | 11 ++++++++--- test-data/unit/check-union-or-syntax.test | 2 +- 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 70ce37560704..5729bec7a5f5 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -32,7 +32,7 @@ from mypy.types import ( Type, CallableType, AnyType, UnboundType, TupleType, TypeList, EllipsisType, CallableArgument, TypeOfAny, Instance, RawExpressionType, ProperType, - UnionType) + UnionType, Pep604Syntax) from mypy import defaults from mypy import message_registry, errorcodes as codes from mypy.errors import Errors @@ -241,7 +241,8 @@ def parse_type_comment(type_comment: str, converted = TypeConverter(errors, line=line, override_column=column, - assume_str_is_unicode=assume_str_is_unicode).visit(typ.body) + assume_str_is_unicode=assume_str_is_unicode + ).visit(typ.body, is_type_comment=True) return ignored, converted @@ -1318,7 +1319,13 @@ def visit(self, node: ast3.expr) -> ProperType: ... @overload def visit(self, node: Optional[AST]) -> Optional[ProperType]: ... - def visit(self, node: Optional[AST]) -> Optional[ProperType]: + @overload + def visit(self, node: ast3.expr, is_type_comment: bool) -> ProperType: ... + + @overload + def visit(self, node: Optional[AST], is_type_comment: bool) -> Optional[ProperType]: ... + + def visit(self, node: Optional[AST], is_type_comment: bool = False) -> Optional[ProperType]: """Modified visit -- keep track of the stack of nodes""" if node is None: return None @@ -1327,6 +1334,8 @@ def visit(self, node: Optional[AST]) -> Optional[ProperType]: method = 'visit_' + node.__class__.__name__ visitor = getattr(self, method, None) if visitor is not None: + if visitor == self.visit_BinOp: + return visitor(node, is_type_comment) return visitor(node) else: return self.invalid_type(node) @@ -1422,7 +1431,7 @@ def _extract_argument_name(self, n: ast3.expr) -> Optional[str]: def visit_Name(self, n: Name) -> Type: return UnboundType(n.id, line=self.line, column=self.convert_column(n.col_offset)) - def visit_BinOp(self, n: ast3.BinOp) -> Type: + def visit_BinOp(self, n: ast3.BinOp, is_type_comment: bool = False) -> Type: if not isinstance(n.op, ast3.BitOr): return self.invalid_type(n) @@ -1431,7 +1440,7 @@ def visit_BinOp(self, n: ast3.BinOp) -> Type: return UnionType([left, right], line=self.line, column=self.convert_column(n.col_offset), - uses_pep604_syntax=True) + pep604_syntax=Pep604Syntax(True, is_type_comment)) def visit_NameConstant(self, n: NameConstant) -> Type: if isinstance(n.value, bool): diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 7c43e834d248..a8c857611c3c 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -605,7 +605,9 @@ def visit_star_type(self, t: StarType) -> Type: return StarType(self.anal_type(t.type), t.line) def visit_union_type(self, t: UnionType) -> Type: - if (t.uses_pep604_syntax is True + if (t.pep604_syntax is not None + and t.pep604_syntax.uses_pep604_syntax is True + and t.pep604_syntax.is_type_comment is False and self.api.is_stub_file is False and self.options.python_version < (3, 10) and self.api.is_future_flag_set('annotations') is False): diff --git a/mypy/types.py b/mypy/types.py index 70fc76dc3af5..9703c346e155 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1719,18 +1719,23 @@ def serialize(self) -> JsonDict: assert False, "Synthetic types don't serialize" +Pep604Syntax = NamedTuple('Pep604Syntax', [ + ('uses_pep604_syntax', bool), + ('is_type_comment', bool)]) + + class UnionType(ProperType): """The union type Union[T1, ..., Tn] (at least one type argument).""" - __slots__ = ('items', 'uses_pep604_syntax') + __slots__ = ('items', 'pep604_syntax') def __init__(self, items: Sequence[Type], line: int = -1, column: int = -1, - uses_pep604_syntax: bool = False) -> None: + pep604_syntax: Optional[Pep604Syntax] = None) -> None: super().__init__(line, column) self.items = flatten_nested_unions(items) self.can_be_true = any(item.can_be_true for item in items) self.can_be_false = any(item.can_be_false for item in items) - self.uses_pep604_syntax = uses_pep604_syntax + self.pep604_syntax = pep604_syntax def __hash__(self) -> int: return hash(frozenset(self.items)) diff --git a/test-data/unit/check-union-or-syntax.test b/test-data/unit/check-union-or-syntax.test index 49f031931bc5..48f6bc597f8b 100644 --- a/test-data/unit/check-union-or-syntax.test +++ b/test-data/unit/check-union-or-syntax.test @@ -61,7 +61,7 @@ y: 42 | int # E: Invalid type: try using Literal[42] instead? z: str | 42 | int # E: Invalid type: try using Literal[42] instead? [case testUnionOrSyntaxInComment] -# flags: --python-version 3.10 +# flags: --python-version 3.6 x = 1 # type: int | str [case testUnionOrSyntaxFutureImport] From ba7562021697f3c650e5409ad0cf66d7aa4568b5 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Mon, 2 Nov 2020 04:32:32 +0100 Subject: [PATCH 09/10] Review changes * Added tests * Rename/refactor variables * Fixed issue with type strings e.g. "int | str" --- mypy/fastparse.py | 27 ++++++++---------- mypy/typeanal.py | 5 ++-- mypy/types.py | 14 ++++------ test-data/unit/check-union-or-syntax.test | 34 ++++++++++++++++++++++- 4 files changed, 53 insertions(+), 27 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 5729bec7a5f5..6fb2e339a5a2 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -31,8 +31,8 @@ ) from mypy.types import ( Type, CallableType, AnyType, UnboundType, TupleType, TypeList, EllipsisType, CallableArgument, - TypeOfAny, Instance, RawExpressionType, ProperType, - UnionType, Pep604Syntax) + TypeOfAny, Instance, RawExpressionType, ProperType, UnionType, +) from mypy import defaults from mypy import message_registry, errorcodes as codes from mypy.errors import Errors @@ -241,8 +241,8 @@ def parse_type_comment(type_comment: str, converted = TypeConverter(errors, line=line, override_column=column, - assume_str_is_unicode=assume_str_is_unicode - ).visit(typ.body, is_type_comment=True) + assume_str_is_unicode=assume_str_is_unicode, + is_type_comment=True).visit(typ.body) return ignored, converted @@ -269,6 +269,8 @@ def parse_type_string(expr_string: str, expr_fallback_name: str, node.original_str_expr = expr_string node.original_str_fallback = expr_fallback_name return node + elif isinstance(node, UnionType): + return node else: return RawExpressionType(expr_string, expr_fallback_name, line, column) except (SyntaxError, ValueError): @@ -1277,12 +1279,14 @@ def __init__(self, line: int = -1, override_column: int = -1, assume_str_is_unicode: bool = True, + is_type_comment: bool = False, ) -> None: self.errors = errors self.line = line self.override_column = override_column self.node_stack = [] # type: List[AST] self.assume_str_is_unicode = assume_str_is_unicode + self.is_type_comment = is_type_comment def convert_column(self, column: int) -> int: """Apply column override if defined; otherwise return column. @@ -1319,13 +1323,7 @@ def visit(self, node: ast3.expr) -> ProperType: ... @overload def visit(self, node: Optional[AST]) -> Optional[ProperType]: ... - @overload - def visit(self, node: ast3.expr, is_type_comment: bool) -> ProperType: ... - - @overload - def visit(self, node: Optional[AST], is_type_comment: bool) -> Optional[ProperType]: ... - - def visit(self, node: Optional[AST], is_type_comment: bool = False) -> Optional[ProperType]: + def visit(self, node: Optional[AST]) -> Optional[ProperType]: """Modified visit -- keep track of the stack of nodes""" if node is None: return None @@ -1334,8 +1332,6 @@ def visit(self, node: Optional[AST], is_type_comment: bool = False) -> Optional[ method = 'visit_' + node.__class__.__name__ visitor = getattr(self, method, None) if visitor is not None: - if visitor == self.visit_BinOp: - return visitor(node, is_type_comment) return visitor(node) else: return self.invalid_type(node) @@ -1431,7 +1427,7 @@ def _extract_argument_name(self, n: ast3.expr) -> Optional[str]: def visit_Name(self, n: Name) -> Type: return UnboundType(n.id, line=self.line, column=self.convert_column(n.col_offset)) - def visit_BinOp(self, n: ast3.BinOp, is_type_comment: bool = False) -> Type: + def visit_BinOp(self, n: ast3.BinOp) -> Type: if not isinstance(n.op, ast3.BitOr): return self.invalid_type(n) @@ -1440,7 +1436,8 @@ def visit_BinOp(self, n: ast3.BinOp, is_type_comment: bool = False) -> Type: return UnionType([left, right], line=self.line, column=self.convert_column(n.col_offset), - pep604_syntax=Pep604Syntax(True, is_type_comment)) + is_evaluated=(not self.is_type_comment), + uses_pep604_syntax=True) def visit_NameConstant(self, n: NameConstant) -> Type: if isinstance(n.value, bool): diff --git a/mypy/typeanal.py b/mypy/typeanal.py index a8c857611c3c..4cce0984c21f 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -605,9 +605,8 @@ def visit_star_type(self, t: StarType) -> Type: return StarType(self.anal_type(t.type), t.line) def visit_union_type(self, t: UnionType) -> Type: - if (t.pep604_syntax is not None - and t.pep604_syntax.uses_pep604_syntax is True - and t.pep604_syntax.is_type_comment is False + if (t.uses_pep604_syntax is True + and t.is_evaluated is True and self.api.is_stub_file is False and self.options.python_version < (3, 10) and self.api.is_future_flag_set('annotations') is False): diff --git a/mypy/types.py b/mypy/types.py index 9703c346e155..10def3826120 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1719,23 +1719,21 @@ def serialize(self) -> JsonDict: assert False, "Synthetic types don't serialize" -Pep604Syntax = NamedTuple('Pep604Syntax', [ - ('uses_pep604_syntax', bool), - ('is_type_comment', bool)]) - - class UnionType(ProperType): """The union type Union[T1, ..., Tn] (at least one type argument).""" - __slots__ = ('items', 'pep604_syntax') + __slots__ = ('items', 'is_evaluated', 'uses_pep604_syntax') def __init__(self, items: Sequence[Type], line: int = -1, column: int = -1, - pep604_syntax: Optional[Pep604Syntax] = None) -> None: + is_evaluated: bool = True, uses_pep604_syntax: bool = False) -> None: super().__init__(line, column) self.items = flatten_nested_unions(items) self.can_be_true = any(item.can_be_true for item in items) self.can_be_false = any(item.can_be_false for item in items) - self.pep604_syntax = pep604_syntax + # is_evaluated should be set to false for type comments and string literals + self.is_evaluated = is_evaluated + # uses_pep604_syntax is True if Union uses OR syntax (X | Y) + self.uses_pep604_syntax = uses_pep604_syntax def __hash__(self) -> int: return hash(frozenset(self.items)) diff --git a/test-data/unit/check-union-or-syntax.test b/test-data/unit/check-union-or-syntax.test index 48f6bc597f8b..fe231af0d53d 100644 --- a/test-data/unit/check-union-or-syntax.test +++ b/test-data/unit/check-union-or-syntax.test @@ -11,6 +11,7 @@ def f(x: int | str) -> int | str: reveal_type(f) # N: Revealed type is 'def (x: Union[builtins.int, builtins.str]) -> Union[builtins.int, builtins.str]' [builtins fixtures/tuple.pyi] + [case testUnionOrSyntaxWithThreeBuiltinsTypes] # flags: --python-version 3.10 def f(x: int | str | float) -> int | str | float: @@ -18,9 +19,9 @@ def f(x: int | str | float) -> int | str | float: z: int | str | float = 0 reveal_type(z) # N: Revealed type is 'Union[builtins.int, builtins.str, builtins.float]' return x - reveal_type(f) # N: Revealed type is 'def (x: Union[builtins.int, builtins.str, builtins.float]) -> Union[builtins.int, builtins.str, builtins.float]' + [case testUnionOrSyntaxWithTwoTypes] # flags: --python-version 3.10 class A: pass @@ -32,6 +33,7 @@ def f(x: A | B) -> A | B: return x reveal_type(f) # N: Revealed type is 'def (x: Union[__main__.A, __main__.B]) -> Union[__main__.A, __main__.B]' + [case testUnionOrSyntaxWithThreeTypes] # flags: --python-version 3.10 class A: pass @@ -44,36 +46,66 @@ def f(x: A | B | C) -> A | B | C: return x reveal_type(f) # N: Revealed type is 'def (x: Union[__main__.A, __main__.B, __main__.C]) -> Union[__main__.A, __main__.B, __main__.C]' + [case testUnionOrSyntaxWithLiteral] # flags: --python-version 3.10 from typing_extensions import Literal reveal_type(Literal[4] | str) # N: Revealed type is 'Any' [builtins fixtures/tuple.pyi] + [case testUnionOrSyntaxWithBadOperator] # flags: --python-version 3.10 x: 1 + 2 # E: Invalid type comment or annotation + [case testUnionOrSyntaxWithBadOperands] # flags: --python-version 3.10 x: int | 42 # E: Invalid type: try using Literal[42] instead? y: 42 | int # E: Invalid type: try using Literal[42] instead? z: str | 42 | int # E: Invalid type: try using Literal[42] instead? + +[case testUnionOrSyntaxWithGenerics] +# flags: --python-version 3.10 +from typing import List +x: List[int | str] +reveal_type(x) # N: Revealed type is 'builtins.list[Union[builtins.int, builtins.str]]' +[builtins fixtures/list.pyi] + + +[case testUnionOrSyntaxWithQuotedTypes] +# flags: --python-version 3.10 +from typing import Union +def f(x: 'Union[int, str, None]') -> 'Union[int, None]': + reveal_type(x) # N: Revealed type is 'Union[builtins.int, builtins.str, None]' + return 42 +reveal_type(f) # N: Revealed type is 'def (x: Union[builtins.int, builtins.str, None]) -> Union[builtins.int, None]' + +# flags: --python-version 3.10 +def g(x: "int | str | None") -> "int | None": + reveal_type(x) # N: Revealed type is 'Union[builtins.int, builtins.str, None]' + return 42 +reveal_type(g) # N: Revealed type is 'def (x: Union[builtins.int, builtins.str, None]) -> Union[builtins.int, None]' + + [case testUnionOrSyntaxInComment] # flags: --python-version 3.6 x = 1 # type: int | str + [case testUnionOrSyntaxFutureImport] # flags: --python-version 3.7 from __future__ import annotations x: int | None [builtins fixtures/tuple.pyi] + [case testUnionOrSyntaxMissingFutureImport] # flags: --python-version 3.9 x: int | None # E: X | Y syntax for unions requires Python 3.10 + [case testUnionOrSyntaxInStubFile] # flags: --python-version 3.6 from lib import x From f47186588a433922beb3e4ec79aa0953156ed68e Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Mon, 2 Nov 2020 22:26:27 +0100 Subject: [PATCH 10/10] Small changes + added docs --- docs/source/kinds_of_types.rst | 90 +++++++++++++++++++++++ mypy/fastparse.py | 8 +- test-data/unit/check-union-or-syntax.test | 26 ++++++- 3 files changed, 117 insertions(+), 7 deletions(-) diff --git a/docs/source/kinds_of_types.rst b/docs/source/kinds_of_types.rst index 263534b59573..facc5da5a64c 100644 --- a/docs/source/kinds_of_types.rst +++ b/docs/source/kinds_of_types.rst @@ -241,6 +241,96 @@ more specific type: since the caller may have to use :py:func:`isinstance` before doing anything interesting with the value. +.. _alternative_union_syntax: + +Alternative union syntax +------------------------ + +`PEP 604 `_ introduced an alternative way +for writing union types. Starting with **Python 3.10** it is possible to write +``Union[int, str]`` as ``int | str``. Any of the following options is possible + +.. code-block:: python + + from typing import List + + # Use as Union + t1: int | str # equivalent to Union[int, str] + + # Use as Optional + t2: int | None # equivalent to Optional[int] + + # Use in generics + t3: List[int | str] # equivalent to List[Union[int, str]] + + # Use in type aliases + T4 = int | None + x: T4 + + # Quoted variable annotations + t5: "int | str" + + # Quoted function annotations + def f(t6: "int | str") -> None: ... + + # Type comments + t6 = 42 # type: int | str + +It is possible to use most of these even for earlier versions. However there are some +limitations to be aware of. + +.. _alternative_union_syntax_stub_files: + +Stub files +"""""""""" + +All options are supported, regardless of the Python version the project uses. + +.. _alternative_union_syntax_37: + +Python 3.7 - 3.9 +"""""""""""""""" + +It is necessary to add ``from __future__ import annotations`` to delay the evaluation +of type annotations. Not using it would result in a ``TypeError``. +This does not apply for **type comments**, **quoted function** and **quoted variable** annotations, +as those also work for earlier versions, see :ref:`below `. + +.. warning:: + + Type aliases are **NOT** supported! Those result in a ``TypeError`` regardless + if the evaluation of type annotations is delayed. + + Dynamic evaluation of annotations is **NOT** possible (e.g. ``typing.get_type_hints`` and ``eval``). + See `note PEP 604 `_. + Use ``typing.Union`` or **Python 3.10** instead if you need those! + +.. code-block:: python + + from __future__ import annotations + + t1: int | None + + # Type aliases + T2 = int | None # TypeError! + +.. _alternative_union_syntax_older_version: + +Older versions +"""""""""""""" + ++------------------------------------------+-----------+-----------+-----------+ +| Python Version | 3.6 | 3.0 - 3.5 | 2.7 | ++==========================================+===========+===========+===========+ +| Type comments | yes | yes | yes | ++------------------------------------------+-----------+-----------+-----------+ +| Quoted function annotations | yes | yes | | ++------------------------------------------+-----------+-----------+-----------+ +| Quoted variable annotations | yes | | | ++------------------------------------------+-----------+-----------+-----------+ +| Everything else | | | | ++------------------------------------------+-----------+-----------+-----------+ + .. _strict_optional: Optional types and the None type diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 6fb2e339a5a2..82088f8e8128 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -242,7 +242,7 @@ def parse_type_comment(type_comment: str, line=line, override_column=column, assume_str_is_unicode=assume_str_is_unicode, - is_type_comment=True).visit(typ.body) + is_evaluated=False).visit(typ.body) return ignored, converted @@ -1279,14 +1279,14 @@ def __init__(self, line: int = -1, override_column: int = -1, assume_str_is_unicode: bool = True, - is_type_comment: bool = False, + is_evaluated: bool = True, ) -> None: self.errors = errors self.line = line self.override_column = override_column self.node_stack = [] # type: List[AST] self.assume_str_is_unicode = assume_str_is_unicode - self.is_type_comment = is_type_comment + self.is_evaluated = is_evaluated def convert_column(self, column: int) -> int: """Apply column override if defined; otherwise return column. @@ -1436,7 +1436,7 @@ def visit_BinOp(self, n: ast3.BinOp) -> Type: return UnionType([left, right], line=self.line, column=self.convert_column(n.col_offset), - is_evaluated=(not self.is_type_comment), + is_evaluated=self.is_evaluated, uses_pep604_syntax=True) def visit_NameConstant(self, n: NameConstant) -> Type: diff --git a/test-data/unit/check-union-or-syntax.test b/test-data/unit/check-union-or-syntax.test index fe231af0d53d..348811ee9d1f 100644 --- a/test-data/unit/check-union-or-syntax.test +++ b/test-data/unit/check-union-or-syntax.test @@ -74,21 +74,41 @@ reveal_type(x) # N: Revealed type is 'builtins.list[Union[builtins.int, builtin [builtins fixtures/list.pyi] -[case testUnionOrSyntaxWithQuotedTypes] -# flags: --python-version 3.10 +[case testUnionOrSyntaxWithQuotedFunctionTypes] +# flags: --python-version 3.4 from typing import Union def f(x: 'Union[int, str, None]') -> 'Union[int, None]': reveal_type(x) # N: Revealed type is 'Union[builtins.int, builtins.str, None]' return 42 reveal_type(f) # N: Revealed type is 'def (x: Union[builtins.int, builtins.str, None]) -> Union[builtins.int, None]' -# flags: --python-version 3.10 def g(x: "int | str | None") -> "int | None": reveal_type(x) # N: Revealed type is 'Union[builtins.int, builtins.str, None]' return 42 reveal_type(g) # N: Revealed type is 'def (x: Union[builtins.int, builtins.str, None]) -> Union[builtins.int, None]' +[case testUnionOrSyntaxWithQuotedVariableTypes] +# flags: --python-version 3.6 +y: "int | str" = 42 +reveal_type(y) # N: Revealed type is 'Union[builtins.int, builtins.str]' + + +[case testUnionOrSyntaxWithTypeAliasWorking] +# flags: --python-version 3.10 +from typing import Union +T = Union[int, str] +x: T +reveal_type(x) # N: Revealed type is 'Union[builtins.int, builtins.str]' + + +[case testUnionOrSyntaxWithTypeAliasNotAllowed] +# flags: --python-version 3.9 +from __future__ import annotations +T = int | str # E: Unsupported left operand type for | ("Type[int]") +[builtins fixtures/tuple.pyi] + + [case testUnionOrSyntaxInComment] # flags: --python-version 3.6 x = 1 # type: int | str