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
23 changes: 19 additions & 4 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1969,10 +1969,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 @@ -1985,15 +1995,20 @@ 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:
def __init__(
self, op: str, left: Expression, right: Expression, analyzed: TypeAliasExpr | None = None
) -> None:
super().__init__()
self.op = op
self.left = left
self.right = right
self.method_type = None
self.right_always = False
self.right_unreachable = False
self.analyzed = analyzed

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 @@ -3472,7 +3472,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
5 changes: 5 additions & 0 deletions mypy/server/aststrip.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
MypyFile,
NameExpr,
Node,
OpExpr,
OverloadedFuncDef,
RefExpr,
StarExpr,
Expand Down Expand Up @@ -222,6 +223,10 @@ def visit_index_expr(self, node: IndexExpr) -> None:
node.analyzed = None # May have been an alias or type application.
super().visit_index_expr(node)

def visit_op_expr(self, node: OpExpr) -> None:
node.analyzed = None # May have been an alias
super().visit_op_expr(node)

def strip_ref_expr(self, node: RefExpr) -> None:
node.kind = None
node.node = None
Expand Down
2 changes: 2 additions & 0 deletions mypy/strconv.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,8 @@ def visit_call_expr(self, o: mypy.nodes.CallExpr) -> str:
return self.dump(a + extra, o)

def visit_op_expr(self, o: mypy.nodes.OpExpr) -> str:
if o.analyzed:
return o.analyzed.accept(self)
return self.dump([o.op, o.left, o.right], o)

def visit_comparison_expr(self, o: mypy.nodes.ComparisonExpr) -> str:
Expand Down
2 changes: 2 additions & 0 deletions mypy/traverser.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,8 @@ def visit_call_expr(self, o: CallExpr) -> None:
def visit_op_expr(self, o: OpExpr) -> None:
o.left.accept(self)
o.right.accept(self)
if o.analyzed is not None:
o.analyzed.accept(self)

def visit_comparison_expr(self, o: ComparisonExpr) -> None:
for operand in o.operands:
Expand Down
7 changes: 6 additions & 1 deletion mypy/treetransform.py
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,12 @@ def visit_call_expr(self, node: CallExpr) -> CallExpr:
)

def visit_op_expr(self, node: OpExpr) -> OpExpr:
new = OpExpr(node.op, self.expr(node.left), self.expr(node.right))
new = OpExpr(
node.op,
self.expr(node.left),
self.expr(node.right),
cast(Optional[TypeAliasExpr], self.optional_expr(node.analyzed)),
)
new.method_type = self.optional_type(node.method_type)
return new

Expand Down
11 changes: 11 additions & 0 deletions test-data/unit/check-type-aliases.test
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,17 @@ c.SpecialExplicit = 4
[builtins fixtures/tuple.pyi]
[typing fixtures/typing-medium.pyi]

[case testNewStyleUnionInTypeAliasWithMalformedInstance]
# flags: --python-version 3.10
from typing import List

A = List[int, str] | int # E: "list" expects 1 type argument, but 2 given
B = int | list[int, str] # E: "list" expects 1 type argument, but 2 given
a: A
b: B
reveal_type(a) # N: Revealed type is "Union[builtins.list[Any], builtins.int]"
reveal_type(b) # N: Revealed type is "Union[builtins.int, builtins.list[Any]]"

[case testValidTypeAliasValues]
from typing import TypeVar, Generic, List

Expand Down
31 changes: 31 additions & 0 deletions test-data/unit/fine-grained.test
Original file line number Diff line number Diff line change
Expand Up @@ -10277,3 +10277,34 @@ A = str
m.py:5: error: Invalid statement in TypedDict definition; expected "field_name: field_type"
==
m.py:5: error: Invalid statement in TypedDict definition; expected "field_name: field_type"

[case testTypeAliasWithNewStyleUnionChangedToVariable]
# flags: --python-version 3.10
import a

[file a.py]
from b import C, D
A = C | D
a: A
reveal_type(a)

[file b.py]
C = int
D = str

[file b.py.2]
C = "x"
D = "y"

[file b.py.3]
C = str
D = int
[out]
a.py:4: note: Revealed type is "Union[builtins.int, builtins.str]"
==
a.py:2: error: Unsupported left operand type for | ("str")
a.py:3: error: Variable "a.A" is not valid as a type
a.py:3: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
a.py:4: note: Revealed type is "A?"
==
a.py:4: note: Revealed type is "Union[builtins.str, builtins.int]"
69 changes: 67 additions & 2 deletions test-data/unit/pythoneval.test
Original file line number Diff line number Diff line change
Expand Up @@ -1663,7 +1663,7 @@ _testNarrowTypeForDictKeys.py:16: note: Revealed type is "Union[builtins.str, No

[case testTypeAliasWithNewStyleUnion]
# flags: --python-version 3.10
from typing import Literal, Type, TypeAlias
from typing import Literal, Type, TypeAlias, TypeVar

Foo = Literal[1, 2]
reveal_type(Foo)
Expand All @@ -1682,15 +1682,44 @@ 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]

T = TypeVar("T", int, str)
def foo(x: T) -> T:
A = type[int] | str
a: A
return x
[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 +1739,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 @@ -1736,6 +1784,23 @@ _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]")

[case testTypedDictUnionGetFull]
from typing import Dict
from typing_extensions import TypedDict
Expand Down