Skip to content

Allow PEP 604 type aliases to be used in runtime context. #11650

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

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2112,6 +2112,7 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
self.fail(message_registry.DEPENDENT_FINAL_IN_CLASS_BODY, s)

def check_type_alias_rvalue(self, s: AssignmentStmt) -> None:
assert s.type_alias is not 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?
Expand All @@ -2132,6 +2133,7 @@ def accept_items(e: Expression) -> None:
self.expr_checker.accept(e)

accept_items(s.rvalue)
s.type_alias.rtype = get_proper_type(alias_type)
self.store_type(s.lvalues[-1], alias_type)

def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type: bool = True,
Expand Down
7 changes: 7 additions & 0 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -3242,6 +3242,13 @@ class LongName(Generic[T]): ...
return type_object_type(tuple_fallback(item).type, self.named_type)
elif isinstance(item, AnyType):
return AnyType(TypeOfAny.from_another_any, source_any=item)
elif isinstance(item, UnionType):
if alias.rtype is not None:
return alias.rtype
elif self.chk.options.python_version[:2] >= (3, 10):
return self.named_type('types.UnionType')
else:
return self.named_type('builtins.object')
else:
if alias_definition:
return AnyType(TypeOfAny.special_form)
Expand Down
2 changes: 2 additions & 0 deletions mypy/fixup.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ def visit_var(self, v: Var) -> None:

def visit_type_alias(self, a: TypeAlias) -> None:
a.target.accept(self.type_fixer)
if a.rtype is not None:
a.rtype.accept(self.type_fixer)


class TypeFixer(TypeVisitor[None]):
Expand Down
14 changes: 10 additions & 4 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1099,7 +1099,7 @@ class AssignmentStmt(Statement):
"""

__slots__ = ('lvalues', 'rvalue', 'type', 'unanalyzed_type', 'new_syntax',
'is_alias_def', 'is_final_def')
'is_alias_def', 'type_alias', 'is_final_def')

lvalues: List[Lvalue]
# This is a TempNode if and only if no rvalue (x: t).
Expand All @@ -1112,6 +1112,7 @@ class AssignmentStmt(Statement):
new_syntax: bool
# Does this assignment define a type alias?
is_alias_def: bool
type_alias: Optional["TypeAlias"]
# Is this a final definition?
# Final attributes can't be re-assigned once set, and can't be overridden
# in a subclass. This flag is not set if an attempted declaration was found to
Expand All @@ -1129,6 +1130,7 @@ def __init__(self, lvalues: List[Lvalue], rvalue: Expression,
self.unanalyzed_type = type
self.new_syntax = new_syntax
self.is_alias_def = False
self.type_alias = None
self.is_final_def = False

def accept(self, visitor: StatementVisitor[T]) -> T:
Expand Down Expand Up @@ -2971,14 +2973,15 @@ def f(x: B[T]) -> T: ... # without T, Any would be used here
within functions that can't be looked up from the symbol table)
"""
__slots__ = ('target', '_fullname', 'alias_tvars', 'no_args', 'normalized',
'line', 'column', '_is_recursive', 'eager')
'line', 'column', '_is_recursive', 'eager', 'rtype')

def __init__(self, target: 'mypy.types.Type', fullname: str, line: int, column: int,
*,
alias_tvars: Optional[List[str]] = None,
no_args: bool = False,
normalized: bool = False,
eager: bool = False) -> None:
eager: bool = False,
rtype: 'Optional[mypy.types.Type]' = None) -> None:
self._fullname = fullname
self.target = target
if alias_tvars is None:
Expand All @@ -2990,6 +2993,7 @@ def __init__(self, target: 'mypy.types.Type', fullname: str, line: int, column:
# it is the cached value.
self._is_recursive: Optional[bool] = None
self.eager = eager
self.rtype = rtype
super().__init__(line, column)

@property
Expand All @@ -3010,6 +3014,7 @@ def serialize(self) -> JsonDict:
"normalized": self.normalized,
"line": self.line,
"column": self.column,
"rtype": self.rtype.serialize() if self.rtype is not None else None
}
return data

Expand All @@ -3026,8 +3031,9 @@ def deserialize(cls, data: JsonDict) -> 'TypeAlias':
normalized = data['normalized']
line = data['line']
column = data['column']
rtype = mypy.types.deserialize_type(data['rtype']) if data['rtype'] is not None else None
return cls(target, fullname, line, column, alias_tvars=alias_tvars,
no_args=no_args, normalized=normalized)
no_args=no_args, normalized=normalized, rtype=rtype)


class PlaceholderNode(SymbolNode):
Expand Down
1 change: 1 addition & 0 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -2733,6 +2733,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
alias_tvars=alias_tvars,
no_args=no_args,
eager=eager)
s.type_alias = alias_node
if isinstance(s.rvalue, (IndexExpr, CallExpr)): # CallExpr is for `void = type(None)`
s.rvalue.analyzed = TypeAliasExpr(alias_node)
s.rvalue.analyzed.line = s.line
Expand Down
1 change: 1 addition & 0 deletions mypyc/test-data/fixtures/ir.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# These builtins stubs are used implicitly in AST to IR generation
# test cases.

import types
from typing import (
TypeVar, Generic, List, Iterator, Iterable, Dict, Optional, Tuple, Any, Set,
overload, Mapping, Union, Callable, Sequence,
Expand Down
44 changes: 44 additions & 0 deletions test-data/unit/check-union-or-syntax.test
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,47 @@ def f(x: Union[int, str, None]) -> None:
else:
reveal_type(x) # N: Revealed type is "None"
[builtins fixtures/isinstance.pyi]

[case testUnionOrSyntaxMetaclassOverride]
# flags: --python-version 3.10
import types

class Meta(type):
def __or__(self, other) -> types.UnionType: pass

class F(metaclass=Meta): pass

reveal_type(F | F) # N: Revealed type is "types.UnionType"
Copy link
Member

Choose a reason for hiding this comment

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

I haven't personally used this trick yet, so I am thinking 🤔
Should not this be just F? Or do we want this special treatment of types.UnionType?

Copy link
Contributor Author

@esoma esoma Dec 3, 2021

Choose a reason for hiding this comment

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

I think there is confusion over Union and UnionType here (and imo, it is confusing and that's because unions are treated so specially). UnionType is the runtime type of a Union object:

X = str | int
print(type(X)) # <class 'types.UnionType'>

So, the type of X is always UnionType (it's worth noting that in previous version of mypy this was just builtins.object).


This is perhaps easier to grasp given:

class A: ...
reveal_type(A | A)  # types.UnionType
a: A | A
reveal_type(a) # Union[test2.A, test2.A]

Now, the second case not reducing is almost certainly a bug, but I think that is unrelated to this specific issue (it's also present in master) since it doesn't matter if its a TypeAlias or not (just if it was a PEP 604 union or not).

UF = F | F
reveal_type(UF) # N: Revealed type is "types.UnionType"
[builtins fixtures/tuple.pyi]

[case testUnionOrSyntaxMetaclassOverrideType]
# flags: --python-version 3.10
class U(type): pass

class Meta(type):
def __or__(self, other) -> U: pass # type: ignore

class F(metaclass=Meta): pass

reveal_type(F | F) # N: Revealed type is "__main__.U"
UF = F | F
reveal_type(UF) # N: Revealed type is "__main__.U"

[case testUnionOrSyntaxMetaclassOverrideUnion]
# flags: --python-version 3.10
from typing import TypeVar, Union, Type

T = TypeVar('T')
U = TypeVar('U')

class Meta(type):
def __or__(self: Type[T], other: Type[U]) -> Union[T, U]: pass

class A(metaclass=Meta): pass
class B(metaclass=Meta): pass

reveal_type(A | B) # N: Revealed type is "Union[__main__.A, __main__.B*]"
C = A | B
reveal_type(C) # N: Revealed type is "Union[__main__.A, __main__.B*]"
1 change: 1 addition & 0 deletions test-data/unit/fixtures/callable.pyi
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import types
from typing import Generic, Tuple, TypeVar, Union

T = TypeVar('T')
Expand Down
2 changes: 2 additions & 0 deletions test-data/unit/lib-stub/types.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ class ModuleType:
if sys.version_info >= (3, 10):
class Union:
def __or__(self, x) -> Union: ...

class UnionType: ...