Skip to content

Commit a2eca34

Browse files
committed
[PEP 747] Recognize TypeForm[T] type and values (python#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 1a2c8e2 commit a2eca34

35 files changed

+901
-62
lines changed

mypy/checker.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -7459,7 +7459,10 @@ def add_any_attribute_to_type(self, typ: Type, name: str) -> Type:
74597459
fallback = typ.fallback.copy_with_extra_attr(name, any_type)
74607460
return typ.copy_modified(fallback=fallback)
74617461
if isinstance(typ, TypeType) and isinstance(typ.item, Instance):
7462-
return TypeType.make_normalized(self.add_any_attribute_to_type(typ.item, name))
7462+
return TypeType.make_normalized(
7463+
self.add_any_attribute_to_type(typ.item, name),
7464+
is_type_form=typ.is_type_form,
7465+
)
74637466
if isinstance(typ, TypeVarType):
74647467
return typ.copy_modified(
74657468
upper_bound=self.add_any_attribute_to_type(typ.upper_bound, name),

mypy/checkexpr.py

+16
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
TypeAliasExpr,
9191
TypeApplication,
9292
TypedDictExpr,
93+
TypeFormExpr,
9394
TypeInfo,
9495
TypeVarExpr,
9596
TypeVarTupleExpr,
@@ -4601,6 +4602,10 @@ def visit_cast_expr(self, expr: CastExpr) -> Type:
46014602
)
46024603
return target_type
46034604

4605+
def visit_type_form_expr(self, expr: TypeFormExpr) -> Type:
4606+
typ = expr.type
4607+
return TypeType.make_normalized(typ, line=typ.line, column=typ.column, is_type_form=True)
4608+
46044609
def visit_assert_type_expr(self, expr: AssertTypeExpr) -> Type:
46054610
source_type = self.accept(
46064611
expr.expr,
@@ -5839,6 +5844,17 @@ def accept(
58395844
typ = self.visit_conditional_expr(node, allow_none_return=True)
58405845
elif allow_none_return and isinstance(node, AwaitExpr):
58415846
typ = self.visit_await_expr(node, allow_none_return=True)
5847+
elif (
5848+
isinstance(type_context, TypeType) and
5849+
type_context.is_type_form and
5850+
node.as_type is not None
5851+
):
5852+
typ = TypeType.make_normalized(
5853+
node.as_type,
5854+
line=node.as_type.line,
5855+
column=node.as_type.column,
5856+
is_type_form=True,
5857+
)
58425858
else:
58435859
typ = node.accept(self)
58445860
except Exception as err:

mypy/copytype.py

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

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

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

mypy/erasetype.py

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

135135
def visit_type_type(self, t: TypeType) -> ProperType:
136-
return TypeType.make_normalized(t.item.accept(self), line=t.line)
136+
return TypeType.make_normalized(t.item.accept(self), line=t.line, is_type_form=t.is_type_form)
137137

138138
def visit_type_alias_type(self, t: TypeAliasType) -> ProperType:
139139
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
@@ -500,7 +500,7 @@ def visit_type_type(self, t: TypeType) -> Type:
500500
# union of instances or Any). Sadly we can't report errors
501501
# here yet.
502502
item = t.item.accept(self)
503-
return TypeType.make_normalized(item)
503+
return TypeType.make_normalized(item, is_type_form=t.is_type_form)
504504

505505
def visit_type_alias_type(self, t: TypeAliasType) -> Type:
506506
# 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
@@ -657,7 +657,11 @@ def visit_partial_type(self, t: PartialType) -> ProperType:
657657

658658
def visit_type_type(self, t: TypeType) -> ProperType:
659659
if isinstance(self.s, TypeType):
660-
return TypeType.make_normalized(join_types(t.item, self.s.item), line=t.line)
660+
return TypeType.make_normalized(
661+
join_types(t.item, self.s.item),
662+
line=t.line,
663+
is_type_form=self.s.is_type_form or t.is_type_form,
664+
)
661665
elif isinstance(self.s, Instance) and self.s.type.fullname == "builtins.type":
662666
return self.s
663667
else:

mypy/literals.py

+4
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
TypeAliasExpr,
4848
TypeApplication,
4949
TypedDictExpr,
50+
TypeFormExpr,
5051
TypeVarExpr,
5152
TypeVarTupleExpr,
5253
UnaryExpr,
@@ -230,6 +231,9 @@ def visit_slice_expr(self, e: SliceExpr) -> None:
230231
def visit_cast_expr(self, e: CastExpr) -> None:
231232
return None
232233

234+
def visit_type_form_expr(self, e: TypeFormExpr) -> None:
235+
return None
236+
233237
def visit_assert_type_expr(self, e: AssertTypeExpr) -> None:
234238
return None
235239

mypy/meet.py

+21-2
Original file line numberDiff line numberDiff line change
@@ -157,12 +157,27 @@ def narrow_declared_type(declared: Type, narrowed: Type) -> Type:
157157
elif isinstance(narrowed, TypeVarType) and is_subtype(narrowed.upper_bound, declared):
158158
return narrowed
159159
elif isinstance(declared, TypeType) and isinstance(narrowed, TypeType):
160-
return TypeType.make_normalized(narrow_declared_type(declared.item, narrowed.item))
160+
return TypeType.make_normalized(
161+
narrow_declared_type(declared.item, narrowed.item),
162+
is_type_form=declared.is_type_form and narrowed.is_type_form,
163+
)
161164
elif (
162165
isinstance(declared, TypeType)
163166
and isinstance(narrowed, Instance)
164167
and narrowed.type.is_metaclass()
165168
):
169+
if declared.is_type_form:
170+
# The declared TypeForm[T] after narrowing must be a kind of
171+
# type object at least as narrow as Type[T]
172+
return narrow_declared_type(
173+
TypeType.make_normalized(
174+
declared.item,
175+
line=declared.line,
176+
column=declared.column,
177+
is_type_form=False,
178+
),
179+
original_narrowed,
180+
)
166181
# We'd need intersection types, so give up.
167182
return original_declared
168183
elif isinstance(declared, Instance):
@@ -1039,7 +1054,11 @@ def visit_type_type(self, t: TypeType) -> ProperType:
10391054
if isinstance(self.s, TypeType):
10401055
typ = self.meet(t.item, self.s.item)
10411056
if not isinstance(typ, NoneType):
1042-
typ = TypeType.make_normalized(typ, line=t.line)
1057+
typ = TypeType.make_normalized(
1058+
typ,
1059+
line=t.line,
1060+
is_type_form=self.s.is_type_form and t.is_type_form,
1061+
)
10431062
return typ
10441063
elif isinstance(self.s, Instance) and self.s.type.fullname == "builtins.type":
10451064
return t

mypy/messages.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -2675,7 +2675,10 @@ def format_literal_value(typ: LiteralType) -> str:
26752675
elif isinstance(typ, UninhabitedType):
26762676
return "Never"
26772677
elif isinstance(typ, TypeType):
2678-
type_name = "type" if options.use_lowercase_names() else "Type"
2678+
if typ.is_type_form:
2679+
type_name = "TypeForm"
2680+
else:
2681+
type_name = "type" if options.use_lowercase_names() else "Type"
26792682
return f"{type_name}[{format(typ.item)}]"
26802683
elif isinstance(typ, FunctionLike):
26812684
func = typ

mypy/mixedtraverser.py

+5
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
TypeAliasExpr,
1414
TypeApplication,
1515
TypedDictExpr,
16+
TypeFormExpr,
1617
TypeVarExpr,
1718
Var,
1819
WithStmt,
@@ -96,6 +97,10 @@ def visit_cast_expr(self, o: CastExpr) -> None:
9697
super().visit_cast_expr(o)
9798
o.type.accept(self)
9899

100+
def visit_type_form_expr(self, o: TypeFormExpr) -> None:
101+
super().visit_type_form_expr(o)
102+
o.type.accept(self)
103+
99104
def visit_assert_type_expr(self, o: AssertTypeExpr) -> None:
100105
super().visit_assert_type_expr(o)
101106
o.type.accept(self)

mypy/nodes.py

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

215-
__slots__ = ()
215+
# NOTE: Cannot use __slots__ because some subclasses also inherit from
216+
# a different superclass with its own __slots__. A subclass in
217+
# Python is not allowed to have multiple superclasses that define
218+
# __slots__.
219+
#__slots__ = ('as_type',)
220+
221+
# If this value expression can also be parsed as a valid type expression,
222+
# represents the type denoted by the type expression.
223+
as_type: mypy.types.Type | None
224+
225+
def __init__(self, *args, **kwargs):
226+
super().__init__(*args, **kwargs)
227+
self.as_type = None
216228

217229
def accept(self, visitor: ExpressionVisitor[T]) -> T:
218230
raise RuntimeError("Not implemented", type(self))
@@ -2189,6 +2201,23 @@ def accept(self, visitor: ExpressionVisitor[T]) -> T:
21892201
return visitor.visit_cast_expr(self)
21902202

21912203

2204+
class TypeFormExpr(Expression):
2205+
"""TypeForm(type) expression."""
2206+
2207+
__slots__ = ("type",)
2208+
2209+
__match_args__ = ("type",)
2210+
2211+
type: mypy.types.Type
2212+
2213+
def __init__(self, typ: mypy.types.Type) -> None:
2214+
super().__init__()
2215+
self.type = typ
2216+
2217+
def accept(self, visitor: ExpressionVisitor[T]) -> T:
2218+
return visitor.visit_type_form_expr(self)
2219+
2220+
21922221
class AssertTypeExpr(Expression):
21932222
"""Represents a typing.assert_type(expr, type) call."""
21942223

mypy/options.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ class BuildType:
7575
PRECISE_TUPLE_TYPES: Final = "PreciseTupleTypes"
7676
NEW_GENERIC_SYNTAX: Final = "NewGenericSyntax"
7777
INLINE_TYPEDDICT: Final = "InlineTypedDict"
78-
INCOMPLETE_FEATURES: Final = frozenset((PRECISE_TUPLE_TYPES, INLINE_TYPEDDICT))
78+
TYPE_FORM: Final = "TypeForm"
79+
INCOMPLETE_FEATURES: Final = frozenset((PRECISE_TUPLE_TYPES, INLINE_TYPEDDICT, TYPE_FORM))
7980
COMPLETE_FEATURES: Final = frozenset((TYPE_VAR_TUPLE, UNPACK, NEW_GENERIC_SYNTAX))
8081

8182

mypy/semanal.py

+68-1
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@
167167
TypeAliasStmt,
168168
TypeApplication,
169169
TypedDictExpr,
170+
TypeFormExpr,
170171
TypeInfo,
171172
TypeParam,
172173
TypeVarExpr,
@@ -186,7 +187,7 @@
186187
type_aliases_source_versions,
187188
typing_extensions_aliases,
188189
)
189-
from mypy.options import Options
190+
from mypy.options import Options, TYPE_FORM
190191
from mypy.patterns import (
191192
AsPattern,
192193
ClassPattern,
@@ -3151,6 +3152,7 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
31513152
self.store_final_status(s)
31523153
self.check_classvar(s)
31533154
self.process_type_annotation(s)
3155+
self.analyze_rvalue_as_type_form(s)
31543156
self.apply_dynamic_class_hook(s)
31553157
if not s.type:
31563158
self.process_module_assignment(s.lvalues, s.rvalue, s)
@@ -3479,6 +3481,10 @@ def analyze_lvalues(self, s: AssignmentStmt) -> None:
34793481
has_explicit_value=has_explicit_value,
34803482
)
34813483

3484+
def analyze_rvalue_as_type_form(self, s: AssignmentStmt) -> None:
3485+
if TYPE_FORM in self.options.enable_incomplete_feature:
3486+
s.rvalue.as_type = self.try_parse_as_type_expression(s.rvalue)
3487+
34823488
def apply_dynamic_class_hook(self, s: AssignmentStmt) -> None:
34833489
if not isinstance(s.rvalue, CallExpr):
34843490
return
@@ -5161,6 +5167,8 @@ def visit_return_stmt(self, s: ReturnStmt) -> None:
51615167
self.fail('"return" outside function', s)
51625168
if s.expr:
51635169
s.expr.accept(self)
5170+
if TYPE_FORM in self.options.enable_incomplete_feature:
5171+
s.expr.as_type = self.try_parse_as_type_expression(s.expr)
51645172

51655173
def visit_raise_stmt(self, s: RaiseStmt) -> None:
51665174
self.statement = s
@@ -5674,10 +5682,33 @@ def visit_call_expr(self, expr: CallExpr) -> None:
56745682
with self.allow_unbound_tvars_set():
56755683
for a in expr.args:
56765684
a.accept(self)
5685+
elif refers_to_fullname(
5686+
expr.callee, ("typing.TypeForm", "typing_extensions.TypeForm")
5687+
):
5688+
# Special form TypeForm(...).
5689+
if not self.check_fixed_args(expr, 1, "TypeForm"):
5690+
return
5691+
# Translate first argument to an unanalyzed type.
5692+
try:
5693+
typ = self.expr_to_unanalyzed_type(expr.args[0])
5694+
except TypeTranslationError:
5695+
self.fail("TypeForm argument is not a type", expr)
5696+
# Suppress future error: "<typing special form>" not callable
5697+
expr.analyzed = CastExpr(expr.args[0], AnyType(TypeOfAny.from_error))
5698+
return
5699+
# Piggyback TypeFormExpr object to the CallExpr object; it takes
5700+
# precedence over the CallExpr semantics.
5701+
expr.analyzed = TypeFormExpr(typ)
5702+
expr.analyzed.line = expr.line
5703+
expr.analyzed.column = expr.column
5704+
expr.analyzed.accept(self)
56775705
else:
56785706
# Normal call expression.
5707+
calculate_type_forms = TYPE_FORM in self.options.enable_incomplete_feature
56795708
for a in expr.args:
56805709
a.accept(self)
5710+
if calculate_type_forms:
5711+
a.as_type = self.try_parse_as_type_expression(a)
56815712

56825713
if (
56835714
isinstance(expr.callee, MemberExpr)
@@ -5946,6 +5977,11 @@ def visit_cast_expr(self, expr: CastExpr) -> None:
59465977
if analyzed is not None:
59475978
expr.type = analyzed
59485979

5980+
def visit_type_form_expr(self, expr: TypeFormExpr) -> None:
5981+
analyzed = self.anal_type(expr.type)
5982+
if analyzed is not None:
5983+
expr.type = analyzed
5984+
59495985
def visit_assert_type_expr(self, expr: AssertTypeExpr) -> None:
59505986
expr.expr.accept(self)
59515987
analyzed = self.anal_type(expr.type)
@@ -7381,6 +7417,37 @@ def parse_dataclass_transform_field_specifiers(self, arg: Expression) -> tuple[s
73817417
names.append(specifier.fullname)
73827418
return tuple(names)
73837419

7420+
def try_parse_as_type_expression(self, maybe_type_expr: Expression) -> Type|None:
7421+
"""Try to parse maybe_type_expr as a type expression.
7422+
If parsing fails return None and emit no errors."""
7423+
# Save SemanticAnalyzer state
7424+
original_errors = self.errors # altered by fail()
7425+
original_num_incomplete_refs = self.num_incomplete_refs # altered by record_incomplete_ref()
7426+
original_progress = self.progress # altered by defer()
7427+
original_deferred = self.deferred # altered by defer()
7428+
original_deferral_debug_context_len = len(self.deferral_debug_context) # altered by defer()
7429+
7430+
self.errors = Errors(Options())
7431+
try:
7432+
t = self.expr_to_analyzed_type(maybe_type_expr)
7433+
if self.errors.is_errors():
7434+
raise TypeTranslationError
7435+
if isinstance(t, UnboundType):
7436+
raise TypeTranslationError
7437+
if isinstance(t, PlaceholderType):
7438+
raise TypeTranslationError
7439+
except TypeTranslationError:
7440+
# Not a type expression. It must be a value expression.
7441+
t = None
7442+
finally:
7443+
# Restore SemanticAnalyzer state
7444+
self.errors = original_errors
7445+
self.num_incomplete_refs = original_num_incomplete_refs
7446+
self.progress = original_progress
7447+
self.deferred = original_deferred
7448+
del self.deferral_debug_context[original_deferral_debug_context_len:]
7449+
return t
7450+
73847451

73857452
def replace_implicit_first_type(sig: FunctionLike, new: Type) -> FunctionLike:
73867453
if isinstance(sig, CallableType):

mypy/server/astdiff.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@ def visit_partial_type(self, typ: PartialType) -> SnapshotItem:
496496
raise RuntimeError
497497

498498
def visit_type_type(self, typ: TypeType) -> SnapshotItem:
499-
return ("TypeType", snapshot_type(typ.item))
499+
return ("TypeType", snapshot_type(typ.item), typ.is_type_form)
500500

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

mypy/server/astmerge.py

+4
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,10 @@ def visit_cast_expr(self, node: CastExpr) -> None:
290290
super().visit_cast_expr(node)
291291
self.fixup_type(node.type)
292292

293+
def visit_type_form_expr(self, node: TypeFormExpr) -> None:
294+
super().visit_type_form_expr(node)
295+
self.fixup_type(node.type)
296+
293297
def visit_assert_type_expr(self, node: AssertTypeExpr) -> None:
294298
super().visit_assert_type_expr(node)
295299
self.fixup_type(node.type)

0 commit comments

Comments
 (0)