Skip to content

Commit ca4c79f

Browse files
committed
[PEP 747] Recognize TypeForm[T] type and values (#9773)
User must opt-in to use TypeForm with --enable-incomplete-feature=TypeForm In particular: * Recognize TypeForm[T] as a kind of type that can be used in a type expression * Recognize a type expression literal as a TypeForm value in: - assignments - function calls - return statements * Define the following relationships between TypeForm values: - is_subtype - join_types - meet_types * Recognize the TypeForm(...) expression * Alter isinstance(typx, type) to narrow TypeForm[T] to Type[T]
1 parent 52c7735 commit ca4c79f

35 files changed

+900
-62
lines changed

mypy/checker.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -7786,7 +7786,10 @@ def add_any_attribute_to_type(self, typ: Type, name: str) -> Type:
77867786
fallback = typ.fallback.copy_with_extra_attr(name, any_type)
77877787
return typ.copy_modified(fallback=fallback)
77887788
if isinstance(typ, TypeType) and isinstance(typ.item, Instance):
7789-
return TypeType.make_normalized(self.add_any_attribute_to_type(typ.item, name))
7789+
return TypeType.make_normalized(
7790+
self.add_any_attribute_to_type(typ.item, name),
7791+
is_type_form=typ.is_type_form,
7792+
)
77907793
if isinstance(typ, TypeVarType):
77917794
return typ.copy_modified(
77927795
upper_bound=self.add_any_attribute_to_type(typ.upper_bound, name),

mypy/checkexpr.py

+17
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
TypeAliasExpr,
9797
TypeApplication,
9898
TypedDictExpr,
99+
TypeFormExpr,
99100
TypeInfo,
100101
TypeVarExpr,
101102
TypeVarTupleExpr,
@@ -4688,6 +4689,10 @@ def visit_cast_expr(self, expr: CastExpr) -> Type:
46884689
)
46894690
return target_type
46904691

4692+
def visit_type_form_expr(self, expr: TypeFormExpr) -> Type:
4693+
typ = expr.type
4694+
return TypeType.make_normalized(typ, line=typ.line, column=typ.column, is_type_form=True)
4695+
46914696
def visit_assert_type_expr(self, expr: AssertTypeExpr) -> Type:
46924697
source_type = self.accept(
46934698
expr.expr,
@@ -5932,6 +5937,7 @@ def accept(
59325937
old_is_callee = self.is_callee
59335938
self.is_callee = is_callee
59345939
try:
5940+
p_type_context = get_proper_type(type_context)
59355941
if allow_none_return and isinstance(node, CallExpr):
59365942
typ = self.visit_call_expr(node, allow_none_return=True)
59375943
elif allow_none_return and isinstance(node, YieldFromExpr):
@@ -5940,6 +5946,17 @@ def accept(
59405946
typ = self.visit_conditional_expr(node, allow_none_return=True)
59415947
elif allow_none_return and isinstance(node, AwaitExpr):
59425948
typ = self.visit_await_expr(node, allow_none_return=True)
5949+
elif (
5950+
isinstance(p_type_context, TypeType) and
5951+
p_type_context.is_type_form and
5952+
node.as_type is not None
5953+
):
5954+
typ = TypeType.make_normalized(
5955+
node.as_type,
5956+
line=node.as_type.line,
5957+
column=node.as_type.column,
5958+
is_type_form=True,
5959+
)
59435960
else:
59445961
typ = node.accept(self)
59455962
except Exception as err:

mypy/copytype.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ def visit_overloaded(self, t: Overloaded) -> ProperType:
122122

123123
def visit_type_type(self, t: TypeType) -> ProperType:
124124
# Use cast since the type annotations in TypeType are imprecise.
125-
return self.copy_common(t, TypeType(cast(Any, t.item)))
125+
return self.copy_common(t, TypeType(cast(Any, t.item), is_type_form=t.is_type_form))
126126

127127
def visit_type_alias_type(self, t: TypeAliasType) -> ProperType:
128128
assert False, "only ProperTypes supported"

mypy/erasetype.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ def visit_union_type(self, t: UnionType) -> ProperType:
134134
return make_simplified_union(erased_items)
135135

136136
def visit_type_type(self, t: TypeType) -> ProperType:
137-
return TypeType.make_normalized(t.item.accept(self), line=t.line)
137+
return TypeType.make_normalized(t.item.accept(self), line=t.line, is_type_form=t.is_type_form)
138138

139139
def visit_type_alias_type(self, t: TypeAliasType) -> ProperType:
140140
raise RuntimeError("Type aliases should be expanded before accepting this visitor")

mypy/evalexpr.py

+3
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ def visit_comparison_expr(self, o: mypy.nodes.ComparisonExpr) -> object:
7575
def visit_cast_expr(self, o: mypy.nodes.CastExpr) -> object:
7676
return o.expr.accept(self)
7777

78+
def visit_type_form_expr(self, o: mypy.nodes.TypeFormExpr) -> object:
79+
return UNKNOWN
80+
7881
def visit_assert_type_expr(self, o: mypy.nodes.AssertTypeExpr) -> object:
7982
return o.expr.accept(self)
8083

mypy/expandtype.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -505,7 +505,7 @@ def visit_type_type(self, t: TypeType) -> Type:
505505
# union of instances or Any). Sadly we can't report errors
506506
# here yet.
507507
item = t.item.accept(self)
508-
return TypeType.make_normalized(item)
508+
return TypeType.make_normalized(item, is_type_form=t.is_type_form)
509509

510510
def visit_type_alias_type(self, t: TypeAliasType) -> Type:
511511
# Target of the type alias cannot contain type variables (not bound by the type

mypy/join.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -622,7 +622,11 @@ def visit_partial_type(self, t: PartialType) -> ProperType:
622622

623623
def visit_type_type(self, t: TypeType) -> ProperType:
624624
if isinstance(self.s, TypeType):
625-
return TypeType.make_normalized(join_types(t.item, self.s.item), line=t.line)
625+
return TypeType.make_normalized(
626+
join_types(t.item, self.s.item),
627+
line=t.line,
628+
is_type_form=self.s.is_type_form or t.is_type_form,
629+
)
626630
elif isinstance(self.s, Instance) and self.s.type.fullname == "builtins.type":
627631
return self.s
628632
else:

mypy/literals.py

+4
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
TypeAliasExpr,
4949
TypeApplication,
5050
TypedDictExpr,
51+
TypeFormExpr,
5152
TypeVarExpr,
5253
TypeVarTupleExpr,
5354
UnaryExpr,
@@ -244,6 +245,9 @@ def visit_slice_expr(self, e: SliceExpr) -> None:
244245
def visit_cast_expr(self, e: CastExpr) -> None:
245246
return None
246247

248+
def visit_type_form_expr(self, e: TypeFormExpr) -> None:
249+
return None
250+
247251
def visit_assert_type_expr(self, e: AssertTypeExpr) -> None:
248252
return None
249253

mypy/meet.py

+21-2
Original file line numberDiff line numberDiff line change
@@ -158,12 +158,27 @@ def narrow_declared_type(declared: Type, narrowed: Type) -> Type:
158158
elif isinstance(narrowed, TypeVarType) and is_subtype(narrowed.upper_bound, declared):
159159
return narrowed
160160
elif isinstance(declared, TypeType) and isinstance(narrowed, TypeType):
161-
return TypeType.make_normalized(narrow_declared_type(declared.item, narrowed.item))
161+
return TypeType.make_normalized(
162+
narrow_declared_type(declared.item, narrowed.item),
163+
is_type_form=declared.is_type_form and narrowed.is_type_form,
164+
)
162165
elif (
163166
isinstance(declared, TypeType)
164167
and isinstance(narrowed, Instance)
165168
and narrowed.type.is_metaclass()
166169
):
170+
if declared.is_type_form:
171+
# The declared TypeForm[T] after narrowing must be a kind of
172+
# type object at least as narrow as Type[T]
173+
return narrow_declared_type(
174+
TypeType.make_normalized(
175+
declared.item,
176+
line=declared.line,
177+
column=declared.column,
178+
is_type_form=False,
179+
),
180+
original_narrowed,
181+
)
167182
# We'd need intersection types, so give up.
168183
return original_declared
169184
elif isinstance(declared, Instance):
@@ -1074,7 +1089,11 @@ def visit_type_type(self, t: TypeType) -> ProperType:
10741089
if isinstance(self.s, TypeType):
10751090
typ = self.meet(t.item, self.s.item)
10761091
if not isinstance(typ, NoneType):
1077-
typ = TypeType.make_normalized(typ, line=t.line)
1092+
typ = TypeType.make_normalized(
1093+
typ,
1094+
line=t.line,
1095+
is_type_form=self.s.is_type_form and t.is_type_form,
1096+
)
10781097
return typ
10791098
elif isinstance(self.s, Instance) and self.s.type.fullname == "builtins.type":
10801099
return t

mypy/messages.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -2721,7 +2721,10 @@ def format_literal_value(typ: LiteralType) -> str:
27212721
elif isinstance(typ, UninhabitedType):
27222722
return "Never"
27232723
elif isinstance(typ, TypeType):
2724-
type_name = "type" if options.use_lowercase_names() else "Type"
2724+
if typ.is_type_form:
2725+
type_name = "TypeForm"
2726+
else:
2727+
type_name = "type" if options.use_lowercase_names() else "Type"
27252728
return f"{type_name}[{format(typ.item)}]"
27262729
elif isinstance(typ, FunctionLike):
27272730
func = typ

mypy/mixedtraverser.py

+5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
TypeAliasStmt,
1616
TypeApplication,
1717
TypedDictExpr,
18+
TypeFormExpr,
1819
TypeVarExpr,
1920
Var,
2021
WithStmt,
@@ -107,6 +108,10 @@ def visit_cast_expr(self, o: CastExpr, /) -> None:
107108
super().visit_cast_expr(o)
108109
o.type.accept(self)
109110

111+
def visit_type_form_expr(self, o: TypeFormExpr, /) -> None:
112+
super().visit_type_form_expr(o)
113+
o.type.accept(self)
114+
110115
def visit_assert_type_expr(self, o: AssertTypeExpr, /) -> None:
111116
super().visit_assert_type_expr(o)
112117
o.type.accept(self)

mypy/nodes.py

+30-1
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,19 @@ def accept(self, visitor: StatementVisitor[T]) -> T:
201201
class Expression(Node):
202202
"""An expression node."""
203203

204-
__slots__ = ()
204+
# NOTE: Cannot use __slots__ because some subclasses also inherit from
205+
# a different superclass with its own __slots__. A subclass in
206+
# Python is not allowed to have multiple superclasses that define
207+
# __slots__.
208+
#__slots__ = ('as_type',)
209+
210+
# If this value expression can also be parsed as a valid type expression,
211+
# represents the type denoted by the type expression.
212+
as_type: mypy.types.Type | None
213+
214+
def __init__(self, *args: Any, **kwargs: Any) -> None:
215+
super().__init__(*args, **kwargs)
216+
self.as_type = None
205217

206218
def accept(self, visitor: ExpressionVisitor[T]) -> T:
207219
raise RuntimeError("Not implemented", type(self))
@@ -2207,6 +2219,23 @@ def accept(self, visitor: ExpressionVisitor[T]) -> T:
22072219
return visitor.visit_cast_expr(self)
22082220

22092221

2222+
class TypeFormExpr(Expression):
2223+
"""TypeForm(type) expression."""
2224+
2225+
__slots__ = ("type",)
2226+
2227+
__match_args__ = ("type",)
2228+
2229+
type: mypy.types.Type
2230+
2231+
def __init__(self, typ: mypy.types.Type) -> None:
2232+
super().__init__()
2233+
self.type = typ
2234+
2235+
def accept(self, visitor: ExpressionVisitor[T]) -> T:
2236+
return visitor.visit_type_form_expr(self)
2237+
2238+
22102239
class AssertTypeExpr(Expression):
22112240
"""Represents a typing.assert_type(expr, type) call."""
22122241

mypy/options.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ class BuildType:
7979
PRECISE_TUPLE_TYPES: Final = "PreciseTupleTypes"
8080
NEW_GENERIC_SYNTAX: Final = "NewGenericSyntax"
8181
INLINE_TYPEDDICT: Final = "InlineTypedDict"
82-
INCOMPLETE_FEATURES: Final = frozenset((PRECISE_TUPLE_TYPES, INLINE_TYPEDDICT))
82+
TYPE_FORM: Final = "TypeForm"
83+
INCOMPLETE_FEATURES: Final = frozenset((PRECISE_TUPLE_TYPES, INLINE_TYPEDDICT, TYPE_FORM))
8384
COMPLETE_FEATURES: Final = frozenset((TYPE_VAR_TUPLE, UNPACK, NEW_GENERIC_SYNTAX))
8485

8586

mypy/semanal.py

+65-1
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@
172172
TypeAliasStmt,
173173
TypeApplication,
174174
TypedDictExpr,
175+
TypeFormExpr,
175176
TypeInfo,
176177
TypeParam,
177178
TypeVarExpr,
@@ -191,7 +192,7 @@
191192
type_aliases_source_versions,
192193
typing_extensions_aliases,
193194
)
194-
from mypy.options import Options
195+
from mypy.options import Options, TYPE_FORM
195196
from mypy.patterns import (
196197
AsPattern,
197198
ClassPattern,
@@ -3209,6 +3210,7 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
32093210
self.store_final_status(s)
32103211
self.check_classvar(s)
32113212
self.process_type_annotation(s)
3213+
self.analyze_rvalue_as_type_form(s)
32123214
self.apply_dynamic_class_hook(s)
32133215
if not s.type:
32143216
self.process_module_assignment(s.lvalues, s.rvalue, s)
@@ -3537,6 +3539,10 @@ def analyze_lvalues(self, s: AssignmentStmt) -> None:
35373539
has_explicit_value=has_explicit_value,
35383540
)
35393541

3542+
def analyze_rvalue_as_type_form(self, s: AssignmentStmt) -> None:
3543+
if TYPE_FORM in self.options.enable_incomplete_feature:
3544+
s.rvalue.as_type = self.try_parse_as_type_expression(s.rvalue)
3545+
35403546
def apply_dynamic_class_hook(self, s: AssignmentStmt) -> None:
35413547
if not isinstance(s.rvalue, CallExpr):
35423548
return
@@ -5271,6 +5277,8 @@ def visit_return_stmt(self, s: ReturnStmt) -> None:
52715277
self.fail('"return" not allowed in except* block', s, serious=True)
52725278
if s.expr:
52735279
s.expr.accept(self)
5280+
if TYPE_FORM in self.options.enable_incomplete_feature:
5281+
s.expr.as_type = self.try_parse_as_type_expression(s.expr)
52745282

52755283
def visit_raise_stmt(self, s: RaiseStmt) -> None:
52765284
self.statement = s
@@ -5791,10 +5799,33 @@ def visit_call_expr(self, expr: CallExpr) -> None:
57915799
with self.allow_unbound_tvars_set():
57925800
for a in expr.args:
57935801
a.accept(self)
5802+
elif refers_to_fullname(
5803+
expr.callee, ("typing.TypeForm", "typing_extensions.TypeForm")
5804+
):
5805+
# Special form TypeForm(...).
5806+
if not self.check_fixed_args(expr, 1, "TypeForm"):
5807+
return
5808+
# Translate first argument to an unanalyzed type.
5809+
try:
5810+
typ = self.expr_to_unanalyzed_type(expr.args[0])
5811+
except TypeTranslationError:
5812+
self.fail("TypeForm argument is not a type", expr)
5813+
# Suppress future error: "<typing special form>" not callable
5814+
expr.analyzed = CastExpr(expr.args[0], AnyType(TypeOfAny.from_error))
5815+
return
5816+
# Piggyback TypeFormExpr object to the CallExpr object; it takes
5817+
# precedence over the CallExpr semantics.
5818+
expr.analyzed = TypeFormExpr(typ)
5819+
expr.analyzed.line = expr.line
5820+
expr.analyzed.column = expr.column
5821+
expr.analyzed.accept(self)
57945822
else:
57955823
# Normal call expression.
5824+
calculate_type_forms = TYPE_FORM in self.options.enable_incomplete_feature
57965825
for a in expr.args:
57975826
a.accept(self)
5827+
if calculate_type_forms:
5828+
a.as_type = self.try_parse_as_type_expression(a)
57985829

57995830
if (
58005831
isinstance(expr.callee, MemberExpr)
@@ -6063,6 +6094,11 @@ def visit_cast_expr(self, expr: CastExpr) -> None:
60636094
if analyzed is not None:
60646095
expr.type = analyzed
60656096

6097+
def visit_type_form_expr(self, expr: TypeFormExpr) -> None:
6098+
analyzed = self.anal_type(expr.type)
6099+
if analyzed is not None:
6100+
expr.type = analyzed
6101+
60666102
def visit_assert_type_expr(self, expr: AssertTypeExpr) -> None:
60676103
expr.expr.accept(self)
60686104
analyzed = self.anal_type(expr.type)
@@ -7584,6 +7620,34 @@ def visit_pass_stmt(self, o: PassStmt, /) -> None:
75847620
def visit_singleton_pattern(self, o: SingletonPattern, /) -> None:
75857621
return None
75867622

7623+
def try_parse_as_type_expression(self, maybe_type_expr: Expression) -> Type|None:
7624+
"""Try to parse maybe_type_expr as a type expression.
7625+
If parsing fails return None and emit no errors."""
7626+
# Save SemanticAnalyzer state
7627+
original_errors = self.errors # altered by fail()
7628+
original_num_incomplete_refs = self.num_incomplete_refs # altered by record_incomplete_ref()
7629+
original_progress = self.progress # altered by defer()
7630+
original_deferred = self.deferred # altered by defer()
7631+
original_deferral_debug_context_len = len(self.deferral_debug_context) # altered by defer()
7632+
7633+
self.errors = Errors(Options())
7634+
try:
7635+
t = self.expr_to_analyzed_type(maybe_type_expr)
7636+
if self.errors.is_errors():
7637+
raise TypeTranslationError
7638+
if isinstance(t, (UnboundType, PlaceholderType)): # type: ignore[misc]
7639+
raise TypeTranslationError
7640+
except TypeTranslationError:
7641+
# Not a type expression. It must be a value expression.
7642+
t = None
7643+
finally:
7644+
# Restore SemanticAnalyzer state
7645+
self.errors = original_errors
7646+
self.num_incomplete_refs = original_num_incomplete_refs
7647+
self.progress = original_progress
7648+
self.deferred = original_deferred
7649+
del self.deferral_debug_context[original_deferral_debug_context_len:]
7650+
return t
75877651

75887652
def replace_implicit_first_type(sig: FunctionLike, new: Type) -> FunctionLike:
75897653
if isinstance(sig, CallableType):

mypy/server/astdiff.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,7 @@ def visit_partial_type(self, typ: PartialType) -> SnapshotItem:
508508
raise RuntimeError
509509

510510
def visit_type_type(self, typ: TypeType) -> SnapshotItem:
511-
return ("TypeType", snapshot_type(typ.item))
511+
return ("TypeType", snapshot_type(typ.item), typ.is_type_form)
512512

513513
def visit_type_alias_type(self, typ: TypeAliasType) -> SnapshotItem:
514514
assert typ.alias is not None

0 commit comments

Comments
 (0)