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

[PEP 747] Recognize TypeForm[T] type and values (#9773) #18690

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
4 changes: 3 additions & 1 deletion mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -7786,7 +7786,9 @@ def add_any_attribute_to_type(self, typ: Type, name: str) -> Type:
fallback = typ.fallback.copy_with_extra_attr(name, any_type)
return typ.copy_modified(fallback=fallback)
if isinstance(typ, TypeType) and isinstance(typ.item, Instance):
return TypeType.make_normalized(self.add_any_attribute_to_type(typ.item, name))
return TypeType.make_normalized(
self.add_any_attribute_to_type(typ.item, name), is_type_form=typ.is_type_form
)
if isinstance(typ, TypeVarType):
return typ.copy_modified(
upper_bound=self.add_any_attribute_to_type(typ.upper_bound, name),
Expand Down
19 changes: 19 additions & 0 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
LambdaExpr,
ListComprehension,
ListExpr,
MaybeTypeExpression,
MemberExpr,
MypyFile,
NamedTupleExpr,
Expand All @@ -96,6 +97,7 @@
TypeAliasExpr,
TypeApplication,
TypedDictExpr,
TypeFormExpr,
TypeInfo,
TypeVarExpr,
TypeVarTupleExpr,
Expand Down Expand Up @@ -4688,6 +4690,10 @@ def visit_cast_expr(self, expr: CastExpr) -> Type:
)
return target_type

def visit_type_form_expr(self, expr: TypeFormExpr) -> Type:
typ = expr.type
return TypeType.make_normalized(typ, line=typ.line, column=typ.column, is_type_form=True)

def visit_assert_type_expr(self, expr: AssertTypeExpr) -> Type:
source_type = self.accept(
expr.expr,
Expand Down Expand Up @@ -5932,6 +5938,7 @@ def accept(
old_is_callee = self.is_callee
self.is_callee = is_callee
try:
p_type_context = get_proper_type(type_context)
if allow_none_return and isinstance(node, CallExpr):
typ = self.visit_call_expr(node, allow_none_return=True)
elif allow_none_return and isinstance(node, YieldFromExpr):
Expand All @@ -5940,6 +5947,18 @@ def accept(
typ = self.visit_conditional_expr(node, allow_none_return=True)
elif allow_none_return and isinstance(node, AwaitExpr):
typ = self.visit_await_expr(node, allow_none_return=True)
elif (
isinstance(p_type_context, TypeType)
and p_type_context.is_type_form
and isinstance(node, MaybeTypeExpression)
and node.as_type is not None
):
typ = TypeType.make_normalized(
node.as_type,
line=node.as_type.line,
column=node.as_type.column,
is_type_form=True,
)
else:
typ = node.accept(self)
except Exception as err:
Expand Down
2 changes: 1 addition & 1 deletion mypy/copytype.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ def visit_overloaded(self, t: Overloaded) -> ProperType:

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

def visit_type_alias_type(self, t: TypeAliasType) -> ProperType:
assert False, "only ProperTypes supported"
Expand Down
4 changes: 3 additions & 1 deletion mypy/erasetype.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,9 @@ def visit_union_type(self, t: UnionType) -> ProperType:
return make_simplified_union(erased_items)

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

def visit_type_alias_type(self, t: TypeAliasType) -> ProperType:
raise RuntimeError("Type aliases should be expanded before accepting this visitor")
Expand Down
3 changes: 3 additions & 0 deletions mypy/evalexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ def visit_comparison_expr(self, o: mypy.nodes.ComparisonExpr) -> object:
def visit_cast_expr(self, o: mypy.nodes.CastExpr) -> object:
return o.expr.accept(self)

def visit_type_form_expr(self, o: mypy.nodes.TypeFormExpr) -> object:
return UNKNOWN

def visit_assert_type_expr(self, o: mypy.nodes.AssertTypeExpr) -> object:
return o.expr.accept(self)

Expand Down
2 changes: 1 addition & 1 deletion mypy/expandtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,7 @@ def visit_type_type(self, t: TypeType) -> Type:
# union of instances or Any). Sadly we can't report errors
# here yet.
item = t.item.accept(self)
return TypeType.make_normalized(item)
return TypeType.make_normalized(item, is_type_form=t.is_type_form)

def visit_type_alias_type(self, t: TypeAliasType) -> Type:
# Target of the type alias cannot contain type variables (not bound by the type
Expand Down
6 changes: 5 additions & 1 deletion mypy/join.py
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,11 @@ def visit_partial_type(self, t: PartialType) -> ProperType:

def visit_type_type(self, t: TypeType) -> ProperType:
if isinstance(self.s, TypeType):
return TypeType.make_normalized(join_types(t.item, self.s.item), line=t.line)
return TypeType.make_normalized(
join_types(t.item, self.s.item),
line=t.line,
is_type_form=self.s.is_type_form or t.is_type_form,
)
elif isinstance(self.s, Instance) and self.s.type.fullname == "builtins.type":
return self.s
else:
Expand Down
4 changes: 4 additions & 0 deletions mypy/literals.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
TypeAliasExpr,
TypeApplication,
TypedDictExpr,
TypeFormExpr,
TypeVarExpr,
TypeVarTupleExpr,
UnaryExpr,
Expand Down Expand Up @@ -244,6 +245,9 @@ def visit_slice_expr(self, e: SliceExpr) -> None:
def visit_cast_expr(self, e: CastExpr) -> None:
return None

def visit_type_form_expr(self, e: TypeFormExpr) -> None:
return None

def visit_assert_type_expr(self, e: AssertTypeExpr) -> None:
return None

Expand Down
18 changes: 16 additions & 2 deletions mypy/meet.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,24 @@ def narrow_declared_type(declared: Type, narrowed: Type) -> Type:
elif isinstance(narrowed, TypeVarType) and is_subtype(narrowed.upper_bound, declared):
return narrowed
elif isinstance(declared, TypeType) and isinstance(narrowed, TypeType):
return TypeType.make_normalized(narrow_declared_type(declared.item, narrowed.item))
return TypeType.make_normalized(
narrow_declared_type(declared.item, narrowed.item),
is_type_form=declared.is_type_form and narrowed.is_type_form,
)
elif (
isinstance(declared, TypeType)
and isinstance(narrowed, Instance)
and narrowed.type.is_metaclass()
):
if declared.is_type_form:
# The declared TypeForm[T] after narrowing must be a kind of
# type object at least as narrow as Type[T]
return narrow_declared_type(
TypeType.make_normalized(
declared.item, line=declared.line, column=declared.column, is_type_form=False
),
original_narrowed,
)
# We'd need intersection types, so give up.
return original_declared
elif isinstance(declared, Instance):
Expand Down Expand Up @@ -1074,7 +1086,9 @@ def visit_type_type(self, t: TypeType) -> ProperType:
if isinstance(self.s, TypeType):
typ = self.meet(t.item, self.s.item)
if not isinstance(typ, NoneType):
typ = TypeType.make_normalized(typ, line=t.line)
typ = TypeType.make_normalized(
typ, line=t.line, is_type_form=self.s.is_type_form and t.is_type_form
)
return typ
elif isinstance(self.s, Instance) and self.s.type.fullname == "builtins.type":
return t
Expand Down
5 changes: 4 additions & 1 deletion mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -2721,7 +2721,10 @@ def format_literal_value(typ: LiteralType) -> str:
elif isinstance(typ, UninhabitedType):
return "Never"
elif isinstance(typ, TypeType):
type_name = "type" if options.use_lowercase_names() else "Type"
if typ.is_type_form:
type_name = "TypeForm"
else:
type_name = "type" if options.use_lowercase_names() else "Type"
return f"{type_name}[{format(typ.item)}]"
elif isinstance(typ, FunctionLike):
func = typ
Expand Down
5 changes: 5 additions & 0 deletions mypy/mixedtraverser.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
TypeAliasStmt,
TypeApplication,
TypedDictExpr,
TypeFormExpr,
TypeVarExpr,
Var,
WithStmt,
Expand Down Expand Up @@ -107,6 +108,10 @@ def visit_cast_expr(self, o: CastExpr, /) -> None:
super().visit_cast_expr(o)
o.type.accept(self)

def visit_type_form_expr(self, o: TypeFormExpr, /) -> None:
super().visit_type_form_expr(o)
o.type.accept(self)

def visit_assert_type_expr(self, o: AssertTypeExpr, /) -> None:
super().visit_assert_type_expr(o)
o.type.accept(self)
Expand Down
47 changes: 44 additions & 3 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1716,15 +1716,19 @@ def accept(self, visitor: ExpressionVisitor[T]) -> T:
class StrExpr(Expression):
"""String literal"""

__slots__ = ("value",)
__slots__ = ("value", "as_type")

__match_args__ = ("value",)

value: str # '' by default
# If this value expression can also be parsed as a valid type expression,
# represents the type denoted by the type expression.
as_type: mypy.types.Type | None

def __init__(self, value: str) -> None:
super().__init__()
self.value = value
self.as_type = None

def accept(self, visitor: ExpressionVisitor[T]) -> T:
return visitor.visit_str_expr(self)
Expand Down Expand Up @@ -1875,15 +1879,20 @@ class NameExpr(RefExpr):
This refers to a local name, global name or a module.
"""

__slots__ = ("name", "is_special_form")
__slots__ = ("name", "is_special_form", "as_type")

__match_args__ = ("name", "node")

# If this value expression can also be parsed as a valid type expression,
# represents the type denoted by the type expression.
as_type: mypy.types.Type | None

def __init__(self, name: str) -> None:
super().__init__()
self.name = name # Name referred to
# Is this a l.h.s. of a special form assignment like typed dict or type variable?
self.is_special_form = False
self.as_type = None

def accept(self, visitor: ExpressionVisitor[T]) -> T:
return visitor.visit_name_expr(self)
Expand Down Expand Up @@ -2023,7 +2032,7 @@ class IndexExpr(Expression):
Also wraps type application such as List[int] as a special form.
"""

__slots__ = ("base", "index", "method_type", "analyzed")
__slots__ = ("base", "index", "method_type", "analyzed", "as_type")

__match_args__ = ("base", "index")

Expand All @@ -2034,13 +2043,17 @@ class IndexExpr(Expression):
# If not None, this is actually semantically a type application
# Class[type, ...] or a type alias initializer.
analyzed: TypeApplication | TypeAliasExpr | None
# If this value expression can also be parsed as a valid type expression,
# represents the type denoted by the type expression.
as_type: mypy.types.Type | None

def __init__(self, base: Expression, index: Expression) -> None:
super().__init__()
self.base = base
self.index = index
self.method_type = None
self.analyzed = None
self.as_type = None

def accept(self, visitor: ExpressionVisitor[T]) -> T:
return visitor.visit_index_expr(self)
Expand Down Expand Up @@ -2098,6 +2111,7 @@ class OpExpr(Expression):
"right_always",
"right_unreachable",
"analyzed",
"as_type",
)

__match_args__ = ("left", "op", "right")
Expand All @@ -2113,6 +2127,9 @@ class OpExpr(Expression):
right_unreachable: bool
# Used for expressions that represent a type "X | Y" in some contexts
analyzed: TypeAliasExpr | None
# If this value expression can also be parsed as a valid type expression,
# represents the type denoted by the type expression.
as_type: mypy.types.Type | None

def __init__(
self, op: str, left: Expression, right: Expression, analyzed: TypeAliasExpr | None = None
Expand All @@ -2125,11 +2142,18 @@ def __init__(
self.right_always = False
self.right_unreachable = False
self.analyzed = analyzed
self.as_type = None

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


# Expression subtypes that could represent the root of a valid type expression.
# Always contains an "as_type" attribute.
# TODO: Make this into a Protocol if mypyc is OK with that.
MaybeTypeExpression = (IndexExpr, NameExpr, OpExpr, StrExpr)


class ComparisonExpr(Expression):
"""Comparison expression (e.g. a < b > c < d)."""

Expand Down Expand Up @@ -2207,6 +2231,23 @@ def accept(self, visitor: ExpressionVisitor[T]) -> T:
return visitor.visit_cast_expr(self)


class TypeFormExpr(Expression):
"""TypeForm(type) expression."""

__slots__ = ("type",)

__match_args__ = ("type",)

type: mypy.types.Type

def __init__(self, typ: mypy.types.Type) -> None:
super().__init__()
self.type = typ

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


class AssertTypeExpr(Expression):
"""Represents a typing.assert_type(expr, type) call."""

Expand Down
3 changes: 2 additions & 1 deletion mypy/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ class BuildType:
PRECISE_TUPLE_TYPES: Final = "PreciseTupleTypes"
NEW_GENERIC_SYNTAX: Final = "NewGenericSyntax"
INLINE_TYPEDDICT: Final = "InlineTypedDict"
INCOMPLETE_FEATURES: Final = frozenset((PRECISE_TUPLE_TYPES, INLINE_TYPEDDICT))
TYPE_FORM: Final = "TypeForm"
INCOMPLETE_FEATURES: Final = frozenset((PRECISE_TUPLE_TYPES, INLINE_TYPEDDICT, TYPE_FORM))
COMPLETE_FEATURES: Final = frozenset((TYPE_VAR_TUPLE, UNPACK, NEW_GENERIC_SYNTAX))


Expand Down
Loading