Skip to content

Commit 7ea5ff6

Browse files
authored
Fix issues with type aliases and new style unions (#14181)
Fix aliases like this and other aliases involving new-style unions: ``` A = type[int] | str ``` Fixes #12392. Fixes #14158.
1 parent 4471c7e commit 7ea5ff6

11 files changed

+152
-28
lines changed

mypy/checker.py

+1-20
Original file line numberDiff line numberDiff line change
@@ -2668,26 +2668,7 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
26682668
self.msg.annotation_in_unchecked_function(context=s)
26692669

26702670
def check_type_alias_rvalue(self, s: AssignmentStmt) -> None:
2671-
if not (self.is_stub and isinstance(s.rvalue, OpExpr) and s.rvalue.op == "|"):
2672-
# We do this mostly for compatibility with old semantic analyzer.
2673-
# TODO: should we get rid of this?
2674-
alias_type = self.expr_checker.accept(s.rvalue)
2675-
else:
2676-
# Avoid type checking 'X | Y' in stubs, since there can be errors
2677-
# on older Python targets.
2678-
alias_type = AnyType(TypeOfAny.special_form)
2679-
2680-
def accept_items(e: Expression) -> None:
2681-
if isinstance(e, OpExpr) and e.op == "|":
2682-
accept_items(e.left)
2683-
accept_items(e.right)
2684-
else:
2685-
# Nested union types have been converted to type context
2686-
# in semantic analysis (such as in 'list[int | str]'),
2687-
# so we don't need to deal with them here.
2688-
self.expr_checker.accept(e)
2689-
2690-
accept_items(s.rvalue)
2671+
alias_type = self.expr_checker.accept(s.rvalue)
26912672
self.store_type(s.lvalues[-1], alias_type)
26922673

26932674
def check_assignment(

mypy/checkexpr.py

+3
Original file line numberDiff line numberDiff line change
@@ -2847,6 +2847,9 @@ def visit_ellipsis(self, e: EllipsisExpr) -> Type:
28472847

28482848
def visit_op_expr(self, e: OpExpr) -> Type:
28492849
"""Type check a binary operator expression."""
2850+
if e.analyzed:
2851+
# It's actually a type expression X | Y.
2852+
return self.accept(e.analyzed)
28502853
if e.op == "and" or e.op == "or":
28512854
return self.check_boolean_op(e, e)
28522855
if e.op == "*" and isinstance(e.left, ListExpr):

mypy/nodes.py

+19-4
Original file line numberDiff line numberDiff line change
@@ -1969,10 +1969,20 @@ def accept(self, visitor: ExpressionVisitor[T]) -> T:
19691969

19701970

19711971
class OpExpr(Expression):
1972-
"""Binary operation (other than . or [] or comparison operators,
1973-
which have specific nodes)."""
1972+
"""Binary operation.
19741973
1975-
__slots__ = ("op", "left", "right", "method_type", "right_always", "right_unreachable")
1974+
The dot (.), [] and comparison operators have more specific nodes.
1975+
"""
1976+
1977+
__slots__ = (
1978+
"op",
1979+
"left",
1980+
"right",
1981+
"method_type",
1982+
"right_always",
1983+
"right_unreachable",
1984+
"analyzed",
1985+
)
19761986

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

@@ -1985,15 +1995,20 @@ class OpExpr(Expression):
19851995
right_always: bool
19861996
# Per static analysis only: Is the right side unreachable?
19871997
right_unreachable: bool
1998+
# Used for expressions that represent a type "X | Y" in some contexts
1999+
analyzed: TypeAliasExpr | None
19882000

1989-
def __init__(self, op: str, left: Expression, right: Expression) -> None:
2001+
def __init__(
2002+
self, op: str, left: Expression, right: Expression, analyzed: TypeAliasExpr | None = None
2003+
) -> None:
19902004
super().__init__()
19912005
self.op = op
19922006
self.left = left
19932007
self.right = right
19942008
self.method_type = None
19952009
self.right_always = False
19962010
self.right_unreachable = False
2011+
self.analyzed = analyzed
19972012

19982013
def accept(self, visitor: ExpressionVisitor[T]) -> T:
19992014
return visitor.visit_op_expr(self)

mypy/semanal.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -3472,7 +3472,11 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
34723472
no_args=no_args,
34733473
eager=eager,
34743474
)
3475-
if isinstance(s.rvalue, (IndexExpr, CallExpr)): # CallExpr is for `void = type(None)`
3475+
if isinstance(s.rvalue, (IndexExpr, CallExpr, OpExpr)) and (
3476+
not isinstance(rvalue, OpExpr)
3477+
or (self.options.python_version >= (3, 10) or self.is_stub_file)
3478+
):
3479+
# Note: CallExpr is for "void = type(None)" and OpExpr is for "X | Y" union syntax.
34763480
s.rvalue.analyzed = TypeAliasExpr(alias_node)
34773481
s.rvalue.analyzed.line = s.line
34783482
# we use the column from resulting target, to get better location for errors

mypy/server/aststrip.py

+5
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
MypyFile,
5555
NameExpr,
5656
Node,
57+
OpExpr,
5758
OverloadedFuncDef,
5859
RefExpr,
5960
StarExpr,
@@ -222,6 +223,10 @@ def visit_index_expr(self, node: IndexExpr) -> None:
222223
node.analyzed = None # May have been an alias or type application.
223224
super().visit_index_expr(node)
224225

226+
def visit_op_expr(self, node: OpExpr) -> None:
227+
node.analyzed = None # May have been an alias
228+
super().visit_op_expr(node)
229+
225230
def strip_ref_expr(self, node: RefExpr) -> None:
226231
node.kind = None
227232
node.node = None

mypy/strconv.py

+2
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,8 @@ def visit_call_expr(self, o: mypy.nodes.CallExpr) -> str:
413413
return self.dump(a + extra, o)
414414

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

418420
def visit_comparison_expr(self, o: mypy.nodes.ComparisonExpr) -> str:

mypy/traverser.py

+2
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,8 @@ def visit_call_expr(self, o: CallExpr) -> None:
262262
def visit_op_expr(self, o: OpExpr) -> None:
263263
o.left.accept(self)
264264
o.right.accept(self)
265+
if o.analyzed is not None:
266+
o.analyzed.accept(self)
265267

266268
def visit_comparison_expr(self, o: ComparisonExpr) -> None:
267269
for operand in o.operands:

mypy/treetransform.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -519,7 +519,12 @@ def visit_call_expr(self, node: CallExpr) -> CallExpr:
519519
)
520520

521521
def visit_op_expr(self, node: OpExpr) -> OpExpr:
522-
new = OpExpr(node.op, self.expr(node.left), self.expr(node.right))
522+
new = OpExpr(
523+
node.op,
524+
self.expr(node.left),
525+
self.expr(node.right),
526+
cast(Optional[TypeAliasExpr], self.optional_expr(node.analyzed)),
527+
)
523528
new.method_type = self.optional_type(node.method_type)
524529
return new
525530

test-data/unit/check-type-aliases.test

+11
Original file line numberDiff line numberDiff line change
@@ -948,6 +948,17 @@ c.SpecialExplicit = 4
948948
[builtins fixtures/tuple.pyi]
949949
[typing fixtures/typing-medium.pyi]
950950

951+
[case testNewStyleUnionInTypeAliasWithMalformedInstance]
952+
# flags: --python-version 3.10
953+
from typing import List
954+
955+
A = List[int, str] | int # E: "list" expects 1 type argument, but 2 given
956+
B = int | list[int, str] # E: "list" expects 1 type argument, but 2 given
957+
a: A
958+
b: B
959+
reveal_type(a) # N: Revealed type is "Union[builtins.list[Any], builtins.int]"
960+
reveal_type(b) # N: Revealed type is "Union[builtins.int, builtins.list[Any]]"
961+
951962
[case testValidTypeAliasValues]
952963
from typing import TypeVar, Generic, List
953964

test-data/unit/fine-grained.test

+31
Original file line numberDiff line numberDiff line change
@@ -10277,3 +10277,34 @@ A = str
1027710277
m.py:5: error: Invalid statement in TypedDict definition; expected "field_name: field_type"
1027810278
==
1027910279
m.py:5: error: Invalid statement in TypedDict definition; expected "field_name: field_type"
10280+
10281+
[case testTypeAliasWithNewStyleUnionChangedToVariable]
10282+
# flags: --python-version 3.10
10283+
import a
10284+
10285+
[file a.py]
10286+
from b import C, D
10287+
A = C | D
10288+
a: A
10289+
reveal_type(a)
10290+
10291+
[file b.py]
10292+
C = int
10293+
D = str
10294+
10295+
[file b.py.2]
10296+
C = "x"
10297+
D = "y"
10298+
10299+
[file b.py.3]
10300+
C = str
10301+
D = int
10302+
[out]
10303+
a.py:4: note: Revealed type is "Union[builtins.int, builtins.str]"
10304+
==
10305+
a.py:2: error: Unsupported left operand type for | ("str")
10306+
a.py:3: error: Variable "a.A" is not valid as a type
10307+
a.py:3: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
10308+
a.py:4: note: Revealed type is "A?"
10309+
==
10310+
a.py:4: note: Revealed type is "Union[builtins.str, builtins.int]"

test-data/unit/pythoneval.test

+67-2
Original file line numberDiff line numberDiff line change
@@ -1663,7 +1663,7 @@ _testNarrowTypeForDictKeys.py:16: note: Revealed type is "Union[builtins.str, No
16631663

16641664
[case testTypeAliasWithNewStyleUnion]
16651665
# flags: --python-version 3.10
1666-
from typing import Literal, Type, TypeAlias
1666+
from typing import Literal, Type, TypeAlias, TypeVar
16671667

16681668
Foo = Literal[1, 2]
16691669
reveal_type(Foo)
@@ -1682,15 +1682,44 @@ Opt4 = float | None
16821682

16831683
A = Type[int] | str
16841684
B: TypeAlias = Type[int] | str
1685+
C = type[int] | str
1686+
1687+
D = type[int] | str
1688+
x: D
1689+
reveal_type(x)
1690+
E: TypeAlias = type[int] | str
1691+
y: E
1692+
reveal_type(y)
1693+
F = list[type[int] | str]
1694+
1695+
T = TypeVar("T", int, str)
1696+
def foo(x: T) -> T:
1697+
A = type[int] | str
1698+
a: A
1699+
return x
16851700
[out]
16861701
_testTypeAliasWithNewStyleUnion.py:5: note: Revealed type is "typing._SpecialForm"
1702+
_testTypeAliasWithNewStyleUnion.py:25: note: Revealed type is "Union[Type[builtins.int], builtins.str]"
1703+
_testTypeAliasWithNewStyleUnion.py:28: note: Revealed type is "Union[Type[builtins.int], builtins.str]"
16871704

16881705
[case testTypeAliasWithNewStyleUnionInStub]
16891706
# flags: --python-version 3.7
16901707
import m
1708+
a: m.A
1709+
reveal_type(a)
1710+
b: m.B
1711+
reveal_type(b)
1712+
c: m.C
1713+
reveal_type(c)
1714+
d: m.D
1715+
reveal_type(d)
1716+
e: m.E
1717+
reveal_type(e)
1718+
f: m.F
1719+
reveal_type(f)
16911720

16921721
[file m.pyi]
1693-
from typing import Type
1722+
from typing import Type, Callable
16941723
from typing_extensions import Literal, TypeAlias
16951724

16961725
Foo = Literal[1, 2]
@@ -1710,8 +1739,27 @@ Opt4 = float | None
17101739

17111740
A = Type[int] | str
17121741
B: TypeAlias = Type[int] | str
1742+
C = type[int] | str
1743+
reveal_type(C)
1744+
D: TypeAlias = type[int] | str
1745+
E = str | type[int]
1746+
F: TypeAlias = str | type[int]
1747+
G = list[type[int] | str]
1748+
H = list[str | type[int]]
1749+
1750+
CU1 = int | Callable[[], str | bool]
1751+
CU2: TypeAlias = int | Callable[[], str | bool]
1752+
CU3 = int | Callable[[str | bool], str]
1753+
CU4: TypeAlias = int | Callable[[str | bool], str]
17131754
[out]
17141755
m.pyi:5: note: Revealed type is "typing._SpecialForm"
1756+
m.pyi:22: note: Revealed type is "typing._SpecialForm"
1757+
_testTypeAliasWithNewStyleUnionInStub.py:4: note: Revealed type is "Union[Type[builtins.int], builtins.str]"
1758+
_testTypeAliasWithNewStyleUnionInStub.py:6: note: Revealed type is "Union[Type[builtins.int], builtins.str]"
1759+
_testTypeAliasWithNewStyleUnionInStub.py:8: note: Revealed type is "Union[Type[builtins.int], builtins.str]"
1760+
_testTypeAliasWithNewStyleUnionInStub.py:10: note: Revealed type is "Union[Type[builtins.int], builtins.str]"
1761+
_testTypeAliasWithNewStyleUnionInStub.py:12: note: Revealed type is "Union[builtins.str, Type[builtins.int]]"
1762+
_testTypeAliasWithNewStyleUnionInStub.py:14: note: Revealed type is "Union[builtins.str, Type[builtins.int]]"
17151763

17161764
[case testEnumNameWorkCorrectlyOn311]
17171765
# flags: --python-version 3.11
@@ -1736,6 +1784,23 @@ _testEnumNameWorkCorrectlyOn311.py:13: note: Revealed type is "Literal['X']?"
17361784
_testEnumNameWorkCorrectlyOn311.py:14: note: Revealed type is "builtins.int"
17371785
_testEnumNameWorkCorrectlyOn311.py:15: note: Revealed type is "builtins.int"
17381786

1787+
[case testTypeAliasNotSupportedWithNewStyleUnion]
1788+
# flags: --python-version 3.9
1789+
from typing_extensions import TypeAlias
1790+
A = type[int] | str
1791+
B = str | type[int]
1792+
C = str | int
1793+
D: TypeAlias = str | int
1794+
[out]
1795+
_testTypeAliasNotSupportedWithNewStyleUnion.py:3: error: Invalid type alias: expression is not a valid type
1796+
_testTypeAliasNotSupportedWithNewStyleUnion.py:3: error: Value of type "Type[type]" is not indexable
1797+
_testTypeAliasNotSupportedWithNewStyleUnion.py:4: error: Invalid type alias: expression is not a valid type
1798+
_testTypeAliasNotSupportedWithNewStyleUnion.py:4: error: Value of type "Type[type]" is not indexable
1799+
_testTypeAliasNotSupportedWithNewStyleUnion.py:5: error: Invalid type alias: expression is not a valid type
1800+
_testTypeAliasNotSupportedWithNewStyleUnion.py:5: error: Unsupported left operand type for | ("Type[str]")
1801+
_testTypeAliasNotSupportedWithNewStyleUnion.py:6: error: Invalid type alias: expression is not a valid type
1802+
_testTypeAliasNotSupportedWithNewStyleUnion.py:6: error: Unsupported left operand type for | ("Type[str]")
1803+
17391804
[case testTypedDictUnionGetFull]
17401805
from typing import Dict
17411806
from typing_extensions import TypedDict

0 commit comments

Comments
 (0)