Skip to content
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

Fix issues with type aliases and new style unions #14181

Merged
merged 16 commits into from
Nov 25, 2022
21 changes: 1 addition & 20 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2668,26 +2668,7 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
self.msg.annotation_in_unchecked_function(context=s)

def check_type_alias_rvalue(self, s: AssignmentStmt) -> None:
if not (self.is_stub and isinstance(s.rvalue, OpExpr) and s.rvalue.op == "|"):
# We do this mostly for compatibility with old semantic analyzer.
# TODO: should we get rid of this?
alias_type = self.expr_checker.accept(s.rvalue)
else:
# Avoid type checking 'X | Y' in stubs, since there can be errors
# on older Python targets.
alias_type = AnyType(TypeOfAny.special_form)

def accept_items(e: Expression) -> None:
if isinstance(e, OpExpr) and e.op == "|":
accept_items(e.left)
accept_items(e.right)
else:
# Nested union types have been converted to type context
# in semantic analysis (such as in 'list[int | str]'),
# so we don't need to deal with them here.
self.expr_checker.accept(e)

accept_items(s.rvalue)
alias_type = self.expr_checker.accept(s.rvalue)
self.store_type(s.lvalues[-1], alias_type)

def check_assignment(
Expand Down
3 changes: 3 additions & 0 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -2847,6 +2847,9 @@ def visit_ellipsis(self, e: EllipsisExpr) -> Type:

def visit_op_expr(self, e: OpExpr) -> Type:
"""Type check a binary operator expression."""
if e.analyzed:
# It's actually a type expression X | Y.
return self.accept(e.analyzed)
if e.op == "and" or e.op == "or":
return self.check_boolean_op(e, e)
if e.op == "*" and isinstance(e.left, ListExpr):
Expand Down
19 changes: 16 additions & 3 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1968,10 +1968,20 @@ def accept(self, visitor: ExpressionVisitor[T]) -> T:


class OpExpr(Expression):
"""Binary operation (other than . or [] or comparison operators,
which have specific nodes)."""
"""Binary operation.

__slots__ = ("op", "left", "right", "method_type", "right_always", "right_unreachable")
The dot (.), [] and comparison operators have more specific nodes.
"""

__slots__ = (
"op",
"left",
"right",
"method_type",
"right_always",
"right_unreachable",
"analyzed",
)

__match_args__ = ("left", "op", "right")

Expand All @@ -1984,6 +1994,8 @@ class OpExpr(Expression):
right_always: bool
# Per static analysis only: Is the right side unreachable?
right_unreachable: bool
# Used for expressions that represent a type "X | Y" in some contexts
analyzed: TypeAliasExpr | None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should now be handled in various visitors. Three cases come to my mind: First, treetransform.py this is needed so that this error will not re-appear in a test case like this

T = TypeVar("T", int, str)
def foo(x: T) -> T:
    A = type[int] | str
    return x

Second, aststrip.py, be sure that when you switch imported names from types to variables, you do get an error about missing __or__ on update. Third, semanal_typeargs.py (uses MixedTraverserVisitor), since we should not carry malformed instances around (with number of type args), they may cause crashes, add a test just in case with a malformed instance in | alias.

And in general adding in to the basic TraverserVisitor is a good idea. Maybe just grep for def visit_index_expr( and see where we use analyzed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good points! I'll fix these.


def __init__(self, op: str, left: Expression, right: Expression) -> None:
super().__init__()
Expand All @@ -1993,6 +2005,7 @@ def __init__(self, op: str, left: Expression, right: Expression) -> None:
self.method_type = None
self.right_always = False
self.right_unreachable = False
self.analyzed = None

def accept(self, visitor: ExpressionVisitor[T]) -> T:
return visitor.visit_op_expr(self)
Expand Down
6 changes: 5 additions & 1 deletion mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -3440,7 +3440,11 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
no_args=no_args,
eager=eager,
)
if isinstance(s.rvalue, (IndexExpr, CallExpr)): # CallExpr is for `void = type(None)`
if isinstance(s.rvalue, (IndexExpr, CallExpr, OpExpr)) and (
not isinstance(rvalue, OpExpr)
or (self.options.python_version >= (3, 10) or self.is_stub_file)
):
# Note: CallExpr is for "void = type(None)" and OpExpr is for "X | Y" union syntax.
s.rvalue.analyzed = TypeAliasExpr(alias_node)
s.rvalue.analyzed.line = s.line
# we use the column from resulting target, to get better location for errors
Expand Down
61 changes: 60 additions & 1 deletion test-data/unit/pythoneval.test
Original file line number Diff line number Diff line change
Expand Up @@ -1682,15 +1682,38 @@ Opt4 = float | None

A = Type[int] | str
B: TypeAlias = Type[int] | str
C = type[int] | str

D = type[int] | str
x: D
reveal_type(x)
E: TypeAlias = type[int] | str
y: E
reveal_type(y)
F = list[type[int] | str]
[out]
_testTypeAliasWithNewStyleUnion.py:5: note: Revealed type is "typing._SpecialForm"
_testTypeAliasWithNewStyleUnion.py:25: note: Revealed type is "Union[Type[builtins.int], builtins.str]"
_testTypeAliasWithNewStyleUnion.py:28: note: Revealed type is "Union[Type[builtins.int], builtins.str]"

[case testTypeAliasWithNewStyleUnionInStub]
# flags: --python-version 3.7
import m
a: m.A
reveal_type(a)
b: m.B
reveal_type(b)
c: m.C
reveal_type(c)
d: m.D
reveal_type(d)
e: m.E
reveal_type(e)
f: m.F
reveal_type(f)

[file m.pyi]
from typing import Type
from typing import Type, Callable
from typing_extensions import Literal, TypeAlias

Foo = Literal[1, 2]
Expand All @@ -1710,8 +1733,27 @@ Opt4 = float | None

A = Type[int] | str
B: TypeAlias = Type[int] | str
C = type[int] | str
reveal_type(C)
D: TypeAlias = type[int] | str
E = str | type[int]
F: TypeAlias = str | type[int]
G = list[type[int] | str]
H = list[str | type[int]]

CU1 = int | Callable[[], str | bool]
CU2: TypeAlias = int | Callable[[], str | bool]
CU3 = int | Callable[[str | bool], str]
CU4: TypeAlias = int | Callable[[str | bool], str]
[out]
m.pyi:5: note: Revealed type is "typing._SpecialForm"
m.pyi:22: note: Revealed type is "typing._SpecialForm"
_testTypeAliasWithNewStyleUnionInStub.py:4: note: Revealed type is "Union[Type[builtins.int], builtins.str]"
_testTypeAliasWithNewStyleUnionInStub.py:6: note: Revealed type is "Union[Type[builtins.int], builtins.str]"
_testTypeAliasWithNewStyleUnionInStub.py:8: note: Revealed type is "Union[Type[builtins.int], builtins.str]"
_testTypeAliasWithNewStyleUnionInStub.py:10: note: Revealed type is "Union[Type[builtins.int], builtins.str]"
_testTypeAliasWithNewStyleUnionInStub.py:12: note: Revealed type is "Union[builtins.str, Type[builtins.int]]"
_testTypeAliasWithNewStyleUnionInStub.py:14: note: Revealed type is "Union[builtins.str, Type[builtins.int]]"

[case testEnumNameWorkCorrectlyOn311]
# flags: --python-version 3.11
Expand All @@ -1735,3 +1777,20 @@ _testEnumNameWorkCorrectlyOn311.py:12: note: Revealed type is "Union[Literal[1]?
_testEnumNameWorkCorrectlyOn311.py:13: note: Revealed type is "Literal['X']?"
_testEnumNameWorkCorrectlyOn311.py:14: note: Revealed type is "builtins.int"
_testEnumNameWorkCorrectlyOn311.py:15: note: Revealed type is "builtins.int"

[case testTypeAliasNotSupportedWithNewStyleUnion]
# flags: --python-version 3.9
from typing_extensions import TypeAlias
A = type[int] | str
B = str | type[int]
C = str | int
D: TypeAlias = str | int
[out]
_testTypeAliasNotSupportedWithNewStyleUnion.py:3: error: Invalid type alias: expression is not a valid type
_testTypeAliasNotSupportedWithNewStyleUnion.py:3: error: Value of type "Type[type]" is not indexable
_testTypeAliasNotSupportedWithNewStyleUnion.py:4: error: Invalid type alias: expression is not a valid type
_testTypeAliasNotSupportedWithNewStyleUnion.py:4: error: Value of type "Type[type]" is not indexable
_testTypeAliasNotSupportedWithNewStyleUnion.py:5: error: Invalid type alias: expression is not a valid type
_testTypeAliasNotSupportedWithNewStyleUnion.py:5: error: Unsupported left operand type for | ("Type[str]")
_testTypeAliasNotSupportedWithNewStyleUnion.py:6: error: Invalid type alias: expression is not a valid type
_testTypeAliasNotSupportedWithNewStyleUnion.py:6: error: Unsupported left operand type for | ("Type[str]")