From e59c310cbd9f11684a4dd7fcbddc9f0903ef8c68 Mon Sep 17 00:00:00 2001 From: David Foster Date: Sun, 29 Sep 2024 14:11:00 -0400 Subject: [PATCH 01/20] [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] --- docs/source/error_code_list.rst | 59 ++ mypy/checker.py | 114 +++- mypy/checkexpr.py | 97 ++- mypy/copytype.py | 2 +- mypy/erasetype.py | 4 +- mypy/errorcodes.py | 5 + mypy/errors.py | 6 +- mypy/evalexpr.py | 3 + mypy/expandtype.py | 2 +- mypy/join.py | 6 +- mypy/literals.py | 4 + mypy/meet.py | 18 +- mypy/messages.py | 6 +- mypy/mixedtraverser.py | 5 + mypy/nodes.py | 49 +- mypy/options.py | 3 +- mypy/semanal.py | 155 ++++- mypy/server/astdiff.py | 2 +- mypy/server/astmerge.py | 5 + mypy/server/deps.py | 5 + mypy/server/subexpr.py | 5 + mypy/strconv.py | 3 + mypy/subtypes.py | 72 ++- mypy/traverser.py | 42 ++ mypy/treetransform.py | 4 + mypy/type_visitor.py | 4 +- mypy/typeanal.py | 23 +- mypy/typeops.py | 2 +- mypy/types.py | 53 +- mypy/typeshed/stdlib/typing.pyi | 1 + mypy/typeshed/stdlib/typing_extensions.pyi | 1 + mypy/visitor.py | 7 + mypyc/irbuild/visitor.py | 4 + test-data/unit/check-typeform.test | 702 +++++++++++++++++++++ test-data/unit/check-union-or-syntax.test | 3 +- test-data/unit/fixtures/dict.pyi | 4 +- test-data/unit/fixtures/tuple.pyi | 2 + test-data/unit/fixtures/typing-full.pyi | 1 + 38 files changed, 1415 insertions(+), 68 deletions(-) create mode 100644 test-data/unit/check-typeform.test diff --git a/docs/source/error_code_list.rst b/docs/source/error_code_list.rst index 6deed549c2f1..962091b3611e 100644 --- a/docs/source/error_code_list.rst +++ b/docs/source/error_code_list.rst @@ -1286,6 +1286,65 @@ type must be a subtype of the original type:: def g(x: object) -> TypeIs[str]: # OK ... +.. _code-maybe-unrecognized-str-typeform: + +String appears in a context which expects a TypeForm [maybe-unrecognized-str-typeform] +-------------------------------------------------------------------------------------- + +TypeForm literals may contain string annotations: + +.. code-block:: python + + typx1: TypeForm = str | None + typx2: TypeForm = 'str | None' # OK + typx3: TypeForm = 'str' | None # OK + +However TypeForm literals containing a string annotation can only be recognized +by mypy in the following locations: + +.. code-block:: python + + typx_var: TypeForm = 'str | None' # assignment r-value + + def func(typx_param: TypeForm) -> TypeForm: + return 'str | None' # returned expression + + func('str | None') # callable's argument + +If you try to use a string annotation in some other location +which expects a TypeForm, the string value will always be treated as a ``str`` +even if a ``TypeForm`` would be more appropriate and this note code +will be generated: + +.. code-block:: python + + # Note: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. [maybe-unrecognized-str-typeform] + # Error: List item 0 has incompatible type "str"; expected "TypeForm[Any]" [list-item] + list_of_typx: list[TypeForm] = ['str | None', float] + +Fix the note by surrounding the entire type with ``TypeForm(...)``: + +.. code-block:: python + + list_of_typx: list[TypeForm] = [TypeForm('str | None'), float] # OK + +Similarly, if you try to use a string literal in a location which expects a +TypeForm, this note code will be generated: + +.. code-block:: python + + dict_of_typx = {'str_or_none': TypeForm(str | None)} + # Note: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. [maybe-unrecognized-str-typeform] + list_of_typx: list[TypeForm] = [dict_of_typx['str_or_none']] + +Fix the note by adding ``# type: ignore[maybe-unrecognized-str-typeform]`` +to the line with the string literal: + +.. code-block:: python + + dict_of_typx = {'str_or_none': TypeForm(str | None)} + list_of_typx: list[TypeForm] = [dict_of_typx['str_or_none']] # type: ignore[maybe-unrecognized-str-typeform] + .. _code-misc: Miscellaneous checks [misc] diff --git a/mypy/checker.py b/mypy/checker.py index 7579c36a97d0..f61d5be508db 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -140,6 +140,7 @@ from mypy.scope import Scope from mypy.semanal import is_trivial_body, refers_to_fullname, set_callable_name from mypy.semanal_enum import ENUM_BASES, ENUM_SPECIAL_PROPS +from mypy.semanal_shared import SemanticAnalyzerCoreInterface from mypy.sharedparse import BINARY_MAGIC_METHODS from mypy.state import state from mypy.subtypes import ( @@ -309,6 +310,8 @@ class TypeChecker(NodeVisitor[None], TypeCheckerSharedApi): tscope: Scope scope: CheckerScope + # Innermost enclosing type + type: TypeInfo | None # Stack of function return types return_types: list[Type] # Flags; true for dynamically typed functions @@ -383,6 +386,7 @@ def __init__( self.scope = CheckerScope(tree) self.binder = ConditionalTypeBinder(options) self.globals = tree.names + self.type = None self.return_types = [] self.dynamic_funcs = [] self.partial_types = [] @@ -2552,7 +2556,11 @@ def visit_class_def(self, defn: ClassDef) -> None: for base in typ.mro[1:]: if base.is_final: self.fail(message_registry.CANNOT_INHERIT_FROM_FINAL.format(base.name), defn) - with self.tscope.class_scope(defn.info), self.enter_partial_types(is_class=True): + with ( + self.tscope.class_scope(defn.info), + self.enter_partial_types(is_class=True), + self.enter_class(defn.info), + ): old_binder = self.binder self.binder = ConditionalTypeBinder(self.options) with self.binder.top_frame_context(): @@ -2620,6 +2628,15 @@ def visit_class_def(self, defn: ClassDef) -> None: self.check_enum(defn) infer_class_variances(defn.info) + @contextmanager + def enter_class(self, type: TypeInfo) -> Iterator[None]: + original_type = self.type + self.type = type + try: + yield + finally: + self.type = original_type + def check_final_deletable(self, typ: TypeInfo) -> None: # These checks are only for mypyc. Only perform some checks that are easier # to implement here than in mypyc. @@ -7818,7 +7835,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), @@ -7955,6 +7974,97 @@ def visit_global_decl(self, o: GlobalDecl, /) -> None: return None +class TypeCheckerAsSemanticAnalyzer(SemanticAnalyzerCoreInterface): + """ + Adapts TypeChecker to the SemanticAnalyzerCoreInterface, + allowing most type expressions to be parsed during the TypeChecker pass. + + See ExpressionChecker.try_parse_as_type_expression() to understand how this + class is used. + """ + + _chk: TypeChecker + _names: dict[str, SymbolTableNode] + did_fail: bool + + def __init__(self, chk: TypeChecker, names: dict[str, SymbolTableNode]) -> None: + self._chk = chk + self._names = names + self.did_fail = False + + def lookup_qualified( + self, name: str, ctx: Context, suppress_errors: bool = False + ) -> SymbolTableNode | None: + sym = self._names.get(name) + # All names being looked up should have been previously gathered, + # even if the related SymbolTableNode does not refer to a valid SymbolNode + assert sym is not None, name + return sym + + def lookup_fully_qualified(self, fullname: str, /) -> SymbolTableNode: + ret = self.lookup_fully_qualified_or_none(fullname) + assert ret is not None, fullname + return ret + + def lookup_fully_qualified_or_none(self, fullname: str, /) -> SymbolTableNode | None: + try: + return self._chk.lookup_qualified(fullname) + except KeyError: + return None + + def fail( + self, + msg: str, + ctx: Context, + serious: bool = False, + *, + blocker: bool = False, + code: ErrorCode | None = None, + ) -> None: + self.did_fail = True + + def note(self, msg: str, ctx: Context, *, code: ErrorCode | None = None) -> None: + pass + + def incomplete_feature_enabled(self, feature: str, ctx: Context) -> bool: + if feature not in self._chk.options.enable_incomplete_feature: + self.fail("__ignored__", ctx) + return False + return True + + def record_incomplete_ref(self) -> None: + pass + + def defer(self, debug_context: Context | None = None, force_progress: bool = False) -> None: + pass + + def is_incomplete_namespace(self, fullname: str) -> bool: + return False + + @property + def final_iteration(self) -> bool: + return True + + def is_future_flag_set(self, flag: str) -> bool: + return self._chk.tree.is_future_flag_set(flag) + + @property + def is_stub_file(self) -> bool: + return self._chk.tree.is_stub + + def is_func_scope(self) -> bool: + # Return arbitrary value. + # + # This method is currently only used to decide whether to pair + # a fail() message with a note() message or not. Both of those + # message types are ignored. + return False + + @property + def type(self) -> TypeInfo | None: + return self._chk.type + + class CollectArgTypeVarTypes(TypeTraverserVisitor): """Collects the non-nested argument types in a set.""" diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 24f0c8c85d61..824d57c093bb 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -26,6 +26,7 @@ freshen_all_functions_type_vars, freshen_function_type_vars, ) +from mypy.exprtotype import TypeTranslationError, expr_to_unanalyzed_type from mypy.infer import ArgumentInferContext, infer_function_type_arguments, infer_type_arguments from mypy.literals import literal from mypy.maptype import map_instance_to_supertype @@ -42,6 +43,7 @@ LITERAL_TYPE, REVEAL_LOCALS, REVEAL_TYPE, + UNBOUND_IMPORTED, ArgKind, AssertTypeExpr, AssignmentExpr, @@ -67,11 +69,13 @@ LambdaExpr, ListComprehension, ListExpr, + MaybeTypeExpression, MemberExpr, MypyFile, NamedTupleExpr, NameExpr, NewTypeExpr, + NotParsed, OpExpr, OverloadedFuncDef, ParamSpecExpr, @@ -86,12 +90,14 @@ StrExpr, SuperExpr, SymbolNode, + SymbolTableNode, TempNode, TupleExpr, TypeAlias, TypeAliasExpr, TypeApplication, TypedDictExpr, + TypeFormExpr, TypeInfo, TypeVarExpr, TypeVarLikeExpr, @@ -100,6 +106,7 @@ Var, YieldExpr, YieldFromExpr, + get_member_expr_fullname, ) from mypy.options import PRECISE_TUPLE_TYPES from mypy.plugin import ( @@ -118,8 +125,14 @@ is_subtype, non_method_protocol_members, ) -from mypy.traverser import has_await_expression +from mypy.traverser import ( + all_name_and_member_expressions, + has_await_expression, + has_str_expression, +) +from mypy.tvar_scope import TypeVarLikeScope from mypy.typeanal import ( + TypeAnalyser, check_for_explicit_any, fix_instance, has_any_from_unimported_type, @@ -4712,6 +4725,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, @@ -5973,6 +5990,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): @@ -5981,6 +5999,17 @@ 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 (node_as_type := self.try_parse_as_type_expression(node)) 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: @@ -6302,6 +6331,72 @@ def has_abstract_type(self, caller_type: ProperType, callee_type: ProperType) -> and not self.chk.allow_abstract_call ) + def try_parse_as_type_expression(self, maybe_type_expr: Expression) -> Type | None: + """Try to parse a value Expression as a type expression. + If success then return the type that it spells. + If fails then return None. + + A value expression that is parsable as a type expression may be used + where a TypeForm is expected to represent the spelled type. + + Unlike SemanticAnalyzer.try_parse_as_type_expression() + (used in the earlier SemanticAnalyzer pass), this function can only + recognize type expressions which contain no string annotations.""" + if not isinstance(maybe_type_expr, MaybeTypeExpression): + return None + + # Check whether has already been parsed as a type expression + # by SemanticAnalyzer.try_parse_as_type_expression(), + # perhaps containing a string annotation + if ( + isinstance(maybe_type_expr, (StrExpr, IndexExpr, OpExpr)) + and maybe_type_expr.as_type != NotParsed.VALUE + ): + return maybe_type_expr.as_type + + # If is potentially a type expression containing a string annotation, + # don't try to parse it because there isn't enough information + # available to the TypeChecker pass to resolve string annotations + if has_str_expression(maybe_type_expr): + self.chk.note( + "TypeForm containing a string annotation cannot be recognized here. " + "Surround with TypeForm(...) to recognize.", + maybe_type_expr, + code=codes.MAYBE_UNRECOGNIZED_STR_TYPEFORM, + ) + return None + + # Collect symbols targeted by NameExprs and MemberExprs, + # to be looked up by TypeAnalyser when binding the + # UnboundTypes corresponding to those expressions. + (name_exprs, member_exprs) = all_name_and_member_expressions(maybe_type_expr) + sym_for_name = {e.name: SymbolTableNode(UNBOUND_IMPORTED, e.node) for e in name_exprs} | { + e_name: SymbolTableNode(UNBOUND_IMPORTED, e.node) + for e in member_exprs + if (e_name := get_member_expr_fullname(e)) is not None + } + + chk_sem = mypy.checker.TypeCheckerAsSemanticAnalyzer(self.chk, sym_for_name) + tpan = TypeAnalyser( + chk_sem, + TypeVarLikeScope(), # empty scope + self.plugin, + self.chk.options, + self.chk.tree, + self.chk.is_typeshed_stub, + ) + + try: + typ1 = expr_to_unanalyzed_type( + maybe_type_expr, self.chk.options, self.chk.is_typeshed_stub + ) + typ2 = typ1.accept(tpan) + if chk_sem.did_fail: + return None + return typ2 + except TypeTranslationError: + return None + def has_any_type(t: Type, ignore_in_type_obj: bool = False) -> bool: """Whether t contains an Any type""" diff --git a/mypy/copytype.py b/mypy/copytype.py index ecb1a89759b6..a890431a1772 100644 --- a/mypy/copytype.py +++ b/mypy/copytype.py @@ -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" diff --git a/mypy/erasetype.py b/mypy/erasetype.py index 6c47670d6687..e4d083765a00 100644 --- a/mypy/erasetype.py +++ b/mypy/erasetype.py @@ -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") diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index 8f85a6f6351a..125bdf5b2fe1 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -271,6 +271,11 @@ def __hash__(self) -> int: default_enabled=False, ) METACLASS: Final[ErrorCode] = ErrorCode("metaclass", "Ensure that metaclass is valid", "General") +MAYBE_UNRECOGNIZED_STR_TYPEFORM: Final[ErrorCode] = ErrorCode( + "maybe-unrecognized-str-typeform", + "Warn when a string is used where a TypeForm is expected but a string annotation cannot be recognized", + "General", +) # Syntax errors are often blocking. SYNTAX: Final[ErrorCode] = ErrorCode("syntax", "Report syntax errors", "General") diff --git a/mypy/errors.py b/mypy/errors.py index 5c135146bcb7..97f232681cb0 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -23,7 +23,11 @@ # Show error codes for some note-level messages (these usually appear alone # and not as a comment for a previous error-level message). -SHOW_NOTE_CODES: Final = {codes.ANNOTATION_UNCHECKED, codes.DEPRECATED} +SHOW_NOTE_CODES: Final = { + codes.ANNOTATION_UNCHECKED, + codes.DEPRECATED, + codes.MAYBE_UNRECOGNIZED_STR_TYPEFORM, +} # Do not add notes with links to error code docs to errors with these codes. # We can tweak this set as we get more experience about what is helpful and what is not. diff --git a/mypy/evalexpr.py b/mypy/evalexpr.py index e39c5840d47a..218d50e37ec3 100644 --- a/mypy/evalexpr.py +++ b/mypy/evalexpr.py @@ -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) diff --git a/mypy/expandtype.py b/mypy/expandtype.py index f704df3b010e..fa1b2c3a4466 100644 --- a/mypy/expandtype.py +++ b/mypy/expandtype.py @@ -520,7 +520,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 diff --git a/mypy/join.py b/mypy/join.py index 099df02680f0..0822ddbfd89a 100644 --- a/mypy/join.py +++ b/mypy/join.py @@ -637,7 +637,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: diff --git a/mypy/literals.py b/mypy/literals.py index 5b0c46f4bee8..fd17e0471440 100644 --- a/mypy/literals.py +++ b/mypy/literals.py @@ -48,6 +48,7 @@ TypeAliasExpr, TypeApplication, TypedDictExpr, + TypeFormExpr, TypeVarExpr, TypeVarTupleExpr, UnaryExpr, @@ -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 diff --git a/mypy/meet.py b/mypy/meet.py index 2e238be7765e..bb58341ca74f 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -182,12 +182,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): @@ -1100,7 +1112,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 diff --git a/mypy/messages.py b/mypy/messages.py index 44ed25a19517..dc23a74e4c11 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -2741,7 +2741,11 @@ def format_literal_value(typ: LiteralType) -> str: elif isinstance(typ, UninhabitedType): return "Never" elif isinstance(typ, TypeType): - return f"type[{format(typ.item)}]" + if typ.is_type_form: + type_name = "TypeForm" + else: + type_name = "type" + return f"{type_name}[{format(typ.item)}]" elif isinstance(typ, FunctionLike): func = typ if func.is_type_obj(): diff --git a/mypy/mixedtraverser.py b/mypy/mixedtraverser.py index 324e8a87c1bd..a4804e680e68 100644 --- a/mypy/mixedtraverser.py +++ b/mypy/mixedtraverser.py @@ -15,6 +15,7 @@ TypeAliasStmt, TypeApplication, TypedDictExpr, + TypeFormExpr, TypeVarExpr, Var, WithStmt, @@ -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) diff --git a/mypy/nodes.py b/mypy/nodes.py index fc2656ce2130..53c75250a018 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -21,6 +21,11 @@ from mypy.patterns import Pattern +@unique +class NotParsed(Enum): + VALUE = "NotParsed" + + class Context: """Base type for objects that are valid as error message locations.""" @@ -1768,15 +1773,20 @@ 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. + # None means "is not a type expression". + as_type: NotParsed | mypy.types.Type | None def __init__(self, value: str) -> None: super().__init__() self.value = value + self.as_type = NotParsed.VALUE def accept(self, visitor: ExpressionVisitor[T]) -> T: return visitor.visit_str_expr(self) @@ -2075,7 +2085,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") @@ -2086,6 +2096,10 @@ 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. + # None means "is not a type expression". + as_type: NotParsed | mypy.types.Type | None def __init__(self, base: Expression, index: Expression) -> None: super().__init__() @@ -2093,6 +2107,7 @@ def __init__(self, base: Expression, index: Expression) -> None: self.index = index self.method_type = None self.analyzed = None + self.as_type = NotParsed.VALUE def accept(self, visitor: ExpressionVisitor[T]) -> T: return visitor.visit_index_expr(self) @@ -2150,6 +2165,7 @@ class OpExpr(Expression): "right_always", "right_unreachable", "analyzed", + "as_type", ) __match_args__ = ("left", "op", "right") @@ -2165,6 +2181,10 @@ 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. + # None means "is not a type expression". + as_type: NotParsed | mypy.types.Type | None def __init__( self, op: str, left: Expression, right: Expression, analyzed: TypeAliasExpr | None = None @@ -2177,11 +2197,19 @@ def __init__( self.right_always = False self.right_unreachable = False self.analyzed = analyzed + self.as_type = NotParsed.VALUE 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. +# +# May have an "as_type" attribute to hold the type for a type expression parsed +# during the SemanticAnalyzer pass. +MaybeTypeExpression = (IndexExpr, MemberExpr, NameExpr, OpExpr, StrExpr) + + class ComparisonExpr(Expression): """Comparison expression (e.g. a < b > c < d).""" @@ -2259,6 +2287,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.""" diff --git a/mypy/options.py b/mypy/options.py index 4a89ef529c07..1013ff5ab17e 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -81,7 +81,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)) diff --git a/mypy/semanal.py b/mypy/semanal.py index 01b7f4989d80..9452be955dbe 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -50,10 +50,12 @@ from __future__ import annotations +import re +import warnings from collections.abc import Collection, Iterable, Iterator from contextlib import contextmanager from typing import Any, Callable, Final, TypeVar, cast -from typing_extensions import TypeAlias as _TypeAlias, TypeGuard +from typing_extensions import TypeAlias as _TypeAlias, TypeGuard, assert_never from mypy import errorcodes as codes, message_registry from mypy.constant_fold import constant_fold_expr @@ -136,6 +138,7 @@ ListExpr, Lvalue, MatchStmt, + MaybeTypeExpression, MemberExpr, MypyFile, NamedTupleExpr, @@ -172,6 +175,7 @@ TypeAliasStmt, TypeApplication, TypedDictExpr, + TypeFormExpr, TypeInfo, TypeParam, TypeVarExpr, @@ -190,7 +194,7 @@ type_aliases_source_versions, typing_extensions_aliases, ) -from mypy.options import Options +from mypy.options import TYPE_FORM, Options from mypy.patterns import ( AsPattern, ClassPattern, @@ -340,6 +344,15 @@ Tag: _TypeAlias = int +# Matches two words separated by whitespace, where each word lacks +# any symbols which have special meaning in a type expression. +# +# Any string literal matching this common pattern cannot be a valid +# type expression and can be ignored quickly when attempting to parse a +# string literal as a type expression. +_MULTIPLE_WORDS_NONTYPE_RE = re.compile(r'\s*[^\s.\'"|\[]+\s+[^\s.\'"|\[]') + + class SemanticAnalyzer( NodeVisitor[None], SemanticAnalyzerInterface, SemanticAnalyzerPluginInterface ): @@ -3265,6 +3278,7 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: self.store_final_status(s) self.check_classvar(s) self.process_type_annotation(s) + self.analyze_rvalue_as_type_form(s) self.apply_dynamic_class_hook(s) if not s.type: self.process_module_assignment(s.lvalues, s.rvalue, s) @@ -3600,6 +3614,10 @@ def analyze_lvalues(self, s: AssignmentStmt) -> None: has_explicit_value=has_explicit_value, ) + def analyze_rvalue_as_type_form(self, s: AssignmentStmt) -> None: + if TYPE_FORM in self.options.enable_incomplete_feature: + self.try_parse_as_type_expression(s.rvalue) + def apply_dynamic_class_hook(self, s: AssignmentStmt) -> None: if not isinstance(s.rvalue, CallExpr): return @@ -5328,6 +5346,8 @@ def visit_return_stmt(self, s: ReturnStmt) -> None: self.fail('"return" not allowed in except* block', s, serious=True) if s.expr: s.expr.accept(self) + if TYPE_FORM in self.options.enable_incomplete_feature: + self.try_parse_as_type_expression(s.expr) def visit_raise_stmt(self, s: RaiseStmt) -> None: self.statement = s @@ -5848,10 +5868,31 @@ def visit_call_expr(self, expr: CallExpr) -> None: with self.allow_unbound_tvars_set(): for a in expr.args: a.accept(self) + elif refers_to_fullname(expr.callee, ("typing.TypeForm", "typing_extensions.TypeForm")): + # Special form TypeForm(...). + if not self.check_fixed_args(expr, 1, "TypeForm"): + return + # Translate first argument to an unanalyzed type. + try: + typ = self.expr_to_unanalyzed_type(expr.args[0]) + except TypeTranslationError: + self.fail("TypeForm argument is not a type", expr) + # Suppress future error: "" not callable + expr.analyzed = CastExpr(expr.args[0], AnyType(TypeOfAny.from_error)) + return + # Piggyback TypeFormExpr object to the CallExpr object; it takes + # precedence over the CallExpr semantics. + expr.analyzed = TypeFormExpr(typ) + expr.analyzed.line = expr.line + expr.analyzed.column = expr.column + expr.analyzed.accept(self) else: # Normal call expression. + calculate_type_forms = TYPE_FORM in self.options.enable_incomplete_feature for a in expr.args: a.accept(self) + if calculate_type_forms: + self.try_parse_as_type_expression(a) if ( isinstance(expr.callee, MemberExpr) @@ -6098,6 +6139,11 @@ def visit_cast_expr(self, expr: CastExpr) -> None: if analyzed is not None: expr.type = analyzed + def visit_type_form_expr(self, expr: TypeFormExpr) -> None: + analyzed = self.anal_type(expr.type) + if analyzed is not None: + expr.type = analyzed + def visit_assert_type_expr(self, expr: AssertTypeExpr) -> None: expr.expr.accept(self) analyzed = self.anal_type(expr.type) @@ -7628,6 +7674,111 @@ def visit_pass_stmt(self, o: PassStmt, /) -> None: def visit_singleton_pattern(self, o: SingletonPattern, /) -> None: return None + def try_parse_as_type_expression(self, maybe_type_expr: Expression) -> None: + """Try to parse a value Expression as a type expression. + If success then annotate the Expression with the type that it spells. + If fails then emit no errors and take no further action. + + A value expression that is parsable as a type expression may be used + where a TypeForm is expected to represent the spelled type. + + Unlike ExpressionChecker.try_parse_as_type_expression() + (used in the later TypeChecker pass), this function can recognize + ALL kinds of type expressions, including type expressions containing + string annotations. + + If the provided Expression will be parsable later in + ExpressionChecker.try_parse_as_type_expression(), this function will + skip parsing the Expression to improve performance, because the later + function is called many fewer times (i.e. only lazily in a rare TypeForm + type context) than this function is called (i.e. eagerly for EVERY + expression in certain syntactic positions). + """ + + # Bail ASAP if the Expression matches a common pattern that cannot possibly + # be a valid type expression, because this function is called very frequently + if not isinstance(maybe_type_expr, MaybeTypeExpression): + return + # Check types in order from most common to least common, for best performance + if isinstance(maybe_type_expr, (NameExpr, MemberExpr)): + # Defer parsing to the later TypeChecker pass, + # and only lazily in contexts where a TypeForm is expected + return + elif isinstance(maybe_type_expr, StrExpr): + # Filter out string literals with common patterns that could not + # possibly be in a type expression + if _MULTIPLE_WORDS_NONTYPE_RE.match(maybe_type_expr.value): + # A common pattern in string literals containing a sentence. + # But cannot be a type expression. + maybe_type_expr.as_type = None + return + elif isinstance(maybe_type_expr, IndexExpr): + if isinstance(maybe_type_expr.base, NameExpr): + if isinstance(maybe_type_expr.base.node, Var): + # Leftmost part of IndexExpr refers to a Var. Not a valid type. + maybe_type_expr.as_type = None + return + elif isinstance(maybe_type_expr.base, MemberExpr): + next_leftmost = maybe_type_expr.base + while True: + leftmost = next_leftmost.expr + if not isinstance(leftmost, MemberExpr): + break + next_leftmost = leftmost + if isinstance(leftmost, NameExpr): + if isinstance(leftmost.node, Var): + # Leftmost part of IndexExpr refers to a Var. Not a valid type. + maybe_type_expr.as_type = None + return + else: + # Leftmost part of IndexExpr is not a NameExpr. Not a valid type. + maybe_type_expr.as_type = None + return + else: + # IndexExpr base is neither a NameExpr nor MemberExpr. Not a valid type. + maybe_type_expr.as_type = None + return + elif isinstance(maybe_type_expr, OpExpr): + if maybe_type_expr.op != "|": + # Binary operators other than '|' never spell a valid type + maybe_type_expr.as_type = None + return + else: + assert_never(maybe_type_expr) + + # Save SemanticAnalyzer state + original_errors = self.errors # altered by fail() + original_num_incomplete_refs = ( + self.num_incomplete_refs + ) # altered by record_incomplete_ref() + original_progress = self.progress # altered by defer() + original_deferred = self.deferred # altered by defer() + original_deferral_debug_context_len = len( + self.deferral_debug_context + ) # altered by defer() + + self.errors = Errors(Options()) + try: + # Ignore warnings that look like: + # :1: SyntaxWarning: invalid escape sequence '\(' + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=SyntaxWarning) + t = self.expr_to_analyzed_type(maybe_type_expr) + if self.errors.is_errors(): + t = None + except TypeTranslationError: + # Not a type expression + t = None + finally: + # Restore SemanticAnalyzer state + self.errors = original_errors + self.num_incomplete_refs = original_num_incomplete_refs + self.progress = original_progress + self.deferred = original_deferred + del self.deferral_debug_context[original_deferral_debug_context_len:] + + maybe_type_expr.as_type = t + def replace_implicit_first_type(sig: FunctionLike, new: Type) -> FunctionLike: if isinstance(sig, CallableType): diff --git a/mypy/server/astdiff.py b/mypy/server/astdiff.py index 16a0d882a8aa..3e4e593568fb 100644 --- a/mypy/server/astdiff.py +++ b/mypy/server/astdiff.py @@ -509,7 +509,7 @@ def visit_partial_type(self, typ: PartialType) -> SnapshotItem: raise RuntimeError def visit_type_type(self, typ: TypeType) -> SnapshotItem: - return ("TypeType", snapshot_type(typ.item)) + return ("TypeType", snapshot_type(typ.item), typ.is_type_form) def visit_type_alias_type(self, typ: TypeAliasType) -> SnapshotItem: assert typ.alias is not None diff --git a/mypy/server/astmerge.py b/mypy/server/astmerge.py index 33e2d2b799cb..197fa3d2498f 100644 --- a/mypy/server/astmerge.py +++ b/mypy/server/astmerge.py @@ -75,6 +75,7 @@ SymbolTable, TypeAlias, TypedDictExpr, + TypeFormExpr, TypeInfo, Var, ) @@ -291,6 +292,10 @@ def visit_cast_expr(self, node: CastExpr) -> None: super().visit_cast_expr(node) self.fixup_type(node.type) + def visit_type_form_expr(self, node: TypeFormExpr) -> None: + super().visit_type_form_expr(node) + self.fixup_type(node.type) + def visit_assert_type_expr(self, node: AssertTypeExpr) -> None: super().visit_assert_type_expr(node) self.fixup_type(node.type) diff --git a/mypy/server/deps.py b/mypy/server/deps.py index b994a214f67a..0191a477d346 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -125,6 +125,7 @@ class 'mod.Cls'. This can also refer to an attribute inherited from a TypeAliasExpr, TypeApplication, TypedDictExpr, + TypeFormExpr, TypeInfo, TypeVarExpr, UnaryExpr, @@ -763,6 +764,10 @@ def visit_cast_expr(self, e: CastExpr) -> None: super().visit_cast_expr(e) self.add_type_dependencies(e.type) + def visit_type_form_expr(self, e: TypeFormExpr) -> None: + super().visit_type_form_expr(e) + self.add_type_dependencies(e.type) + def visit_assert_type_expr(self, e: AssertTypeExpr) -> None: super().visit_assert_type_expr(e) self.add_type_dependencies(e.type) diff --git a/mypy/server/subexpr.py b/mypy/server/subexpr.py index c94db44445dc..013b936e8b7c 100644 --- a/mypy/server/subexpr.py +++ b/mypy/server/subexpr.py @@ -28,6 +28,7 @@ StarExpr, TupleExpr, TypeApplication, + TypeFormExpr, UnaryExpr, YieldExpr, YieldFromExpr, @@ -122,6 +123,10 @@ def visit_cast_expr(self, e: CastExpr) -> None: self.add(e) super().visit_cast_expr(e) + def visit_type_form_expr(self, e: TypeFormExpr) -> None: + self.add(e) + super().visit_type_form_expr(e) + def visit_assert_type_expr(self, e: AssertTypeExpr) -> None: self.add(e) super().visit_assert_type_expr(e) diff --git a/mypy/strconv.py b/mypy/strconv.py index 3e9d37586f72..128de0561856 100644 --- a/mypy/strconv.py +++ b/mypy/strconv.py @@ -464,6 +464,9 @@ def visit_comparison_expr(self, o: mypy.nodes.ComparisonExpr) -> str: def visit_cast_expr(self, o: mypy.nodes.CastExpr) -> str: return self.dump([o.expr, o.type], o) + def visit_type_form_expr(self, o: mypy.nodes.TypeFormExpr) -> str: + return self.dump([o.type], o) + def visit_assert_type_expr(self, o: mypy.nodes.AssertTypeExpr) -> str: return self.dump([o.expr, o.type], o) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 7da258a827f3..c02ff068560b 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -1108,38 +1108,50 @@ def visit_partial_type(self, left: PartialType) -> bool: def visit_type_type(self, left: TypeType) -> bool: right = self.right - if isinstance(right, TypeType): - return self._is_subtype(left.item, right.item) - if isinstance(right, Overloaded) and right.is_type_obj(): - # Same as in other direction: if it's a constructor callable, all - # items should belong to the same class' constructor, so it's enough - # to check one of them. - return self._is_subtype(left, right.items[0]) - if isinstance(right, CallableType): - if self.proper_subtype and not right.is_type_obj(): - # We can't accept `Type[X]` as a *proper* subtype of Callable[P, X] - # since this will break transitivity of subtyping. + if left.is_type_form: + if isinstance(right, TypeType): + if not right.is_type_form: + return False + return self._is_subtype(left.item, right.item) + if isinstance(right, Instance): + if right.type.fullname == "builtins.object": + return True return False - # This is unsound, we don't check the __init__ signature. - return self._is_subtype(left.item, right.ret_type) - if isinstance(right, Instance): - if right.type.fullname in ["builtins.object", "builtins.type"]: - # TODO: Strictly speaking, the type builtins.type is considered equivalent to - # Type[Any]. However, this would break the is_proper_subtype check in - # conditional_types for cases like isinstance(x, type) when the type - # of x is Type[int]. It's unclear what's the right way to address this. - return True - item = left.item - if isinstance(item, TypeVarType): - item = get_proper_type(item.upper_bound) - if isinstance(item, Instance): - if right.type.is_protocol and is_protocol_implementation( - item, right, proper_subtype=self.proper_subtype, class_obj=True - ): + return False + else: # not left.is_type_form + if isinstance(right, TypeType): + return self._is_subtype(left.item, right.item) + if isinstance(right, Overloaded) and right.is_type_obj(): + # Same as in other direction: if it's a constructor callable, all + # items should belong to the same class' constructor, so it's enough + # to check one of them. + return self._is_subtype(left, right.items[0]) + if isinstance(right, CallableType): + if self.proper_subtype and not right.is_type_obj(): + # We can't accept `Type[X]` as a *proper* subtype of Callable[P, X] + # since this will break transitivity of subtyping. + return False + # This is unsound, we don't check the __init__ signature. + return self._is_subtype(left.item, right.ret_type) + + if isinstance(right, Instance): + if right.type.fullname in ["builtins.object", "builtins.type"]: + # TODO: Strictly speaking, the type builtins.type is considered equivalent to + # Type[Any]. However, this would break the is_proper_subtype check in + # conditional_types for cases like isinstance(x, type) when the type + # of x is Type[int]. It's unclear what's the right way to address this. return True - metaclass = item.type.metaclass_type - return metaclass is not None and self._is_subtype(metaclass, right) - return False + item = left.item + if isinstance(item, TypeVarType): + item = get_proper_type(item.upper_bound) + if isinstance(item, Instance): + if right.type.is_protocol and is_protocol_implementation( + item, right, proper_subtype=self.proper_subtype, class_obj=True + ): + return True + metaclass = item.type.metaclass_type + return metaclass is not None and self._is_subtype(metaclass, right) + return False def visit_type_alias_type(self, left: TypeAliasType) -> bool: assert False, f"This should be never called, got {left}" diff --git a/mypy/traverser.py b/mypy/traverser.py index 7d7794822396..18bb0c4ce4c2 100644 --- a/mypy/traverser.py +++ b/mypy/traverser.py @@ -76,6 +76,7 @@ TypeAliasStmt, TypeApplication, TypedDictExpr, + TypeFormExpr, TypeVarExpr, TypeVarTupleExpr, UnaryExpr, @@ -289,6 +290,9 @@ def visit_slice_expr(self, o: SliceExpr, /) -> None: def visit_cast_expr(self, o: CastExpr, /) -> None: o.expr.accept(self) + def visit_type_form_expr(self, o: TypeFormExpr, /) -> None: + pass + def visit_assert_type_expr(self, o: AssertTypeExpr, /) -> None: o.expr.accept(self) @@ -737,6 +741,11 @@ def visit_cast_expr(self, o: CastExpr, /) -> None: return super().visit_cast_expr(o) + def visit_type_form_expr(self, o: TypeFormExpr, /) -> None: + if not self.visit(o): + return + super().visit_type_form_expr(o) + def visit_assert_type_expr(self, o: AssertTypeExpr, /) -> None: if not self.visit(o): return @@ -935,6 +944,39 @@ def has_return_statement(fdef: FuncBase) -> bool: return seeker.found +class NameAndMemberCollector(TraverserVisitor): + def __init__(self) -> None: + super().__init__() + self.name_exprs: list[NameExpr] = [] + self.member_exprs: list[MemberExpr] = [] + + def visit_name_expr(self, o: NameExpr, /) -> None: + self.name_exprs.append(o) + + def visit_member_expr(self, o: MemberExpr, /) -> None: + self.member_exprs.append(o) + + +def all_name_and_member_expressions(node: Expression) -> tuple[list[NameExpr], list[MemberExpr]]: + v = NameAndMemberCollector() + node.accept(v) + return (v.name_exprs, v.member_exprs) + + +class StringSeeker(TraverserVisitor): + def __init__(self) -> None: + self.found = False + + def visit_str_expr(self, o: StrExpr, /) -> None: + self.found = True + + +def has_str_expression(node: Expression) -> bool: + v = StringSeeker() + node.accept(v) + return v.found + + class FuncCollectorBase(TraverserVisitor): def __init__(self) -> None: self.inside_func = False diff --git a/mypy/treetransform.py b/mypy/treetransform.py index 0abf98a52336..f5af5fb777b5 100644 --- a/mypy/treetransform.py +++ b/mypy/treetransform.py @@ -83,6 +83,7 @@ TypeAliasExpr, TypeApplication, TypedDictExpr, + TypeFormExpr, TypeVarExpr, TypeVarTupleExpr, UnaryExpr, @@ -540,6 +541,9 @@ def visit_comparison_expr(self, node: ComparisonExpr) -> ComparisonExpr: def visit_cast_expr(self, node: CastExpr) -> CastExpr: return CastExpr(self.expr(node.expr), self.type(node.type)) + def visit_type_form_expr(self, node: TypeFormExpr) -> TypeFormExpr: + return TypeFormExpr(self.type(node.type)) + def visit_assert_type_expr(self, node: AssertTypeExpr) -> AssertTypeExpr: return AssertTypeExpr(self.expr(node.expr), self.type(node.type)) diff --git a/mypy/type_visitor.py b/mypy/type_visitor.py index ab1ec8b46fdd..ed4580140041 100644 --- a/mypy/type_visitor.py +++ b/mypy/type_visitor.py @@ -325,7 +325,9 @@ def visit_overloaded(self, t: Overloaded, /) -> Type: return Overloaded(items=items) def visit_type_type(self, t: TypeType, /) -> Type: - return TypeType.make_normalized(t.item.accept(self), line=t.line, column=t.column) + return TypeType.make_normalized( + t.item.accept(self), line=t.line, column=t.column, is_type_form=t.is_type_form + ) @abstractmethod def visit_type_alias_type(self, t: TypeAliasType, /) -> Type: diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 204d3061c734..34907e5c8f64 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -49,7 +49,7 @@ check_arg_kinds, check_arg_names, ) -from mypy.options import INLINE_TYPEDDICT, Options +from mypy.options import INLINE_TYPEDDICT, TYPE_FORM, Options from mypy.plugin import AnalyzeTypeContext, Plugin, TypeAnalyzerPluginInterface from mypy.semanal_shared import ( SemanticAnalyzerCoreInterface, @@ -665,6 +665,23 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ self.fail(f'{type_str} can\'t contain "{bad_item_name}"', t, code=codes.VALID_TYPE) item = AnyType(TypeOfAny.from_error) return TypeType.make_normalized(item, line=t.line, column=t.column) + elif fullname in ("typing_extensions.TypeForm", "typing.TypeForm"): + if TYPE_FORM not in self.options.enable_incomplete_feature: + self.fail( + "TypeForm is experimental," + " must be enabled with --enable-incomplete-feature=TypeForm", + t, + ) + if len(t.args) == 0: + any_type = self.get_omitted_any(t) + return TypeType(any_type, line=t.line, column=t.column, is_type_form=True) + if len(t.args) != 1: + type_str = "TypeForm[...]" + self.fail( + type_str + " must have exactly one type argument", t, code=codes.VALID_TYPE + ) + item = self.anal_type(t.args[0]) + return TypeType.make_normalized(item, line=t.line, column=t.column, is_type_form=True) elif fullname == "typing.ClassVar": if self.nesting_level > 0: self.fail( @@ -1400,7 +1417,9 @@ def visit_ellipsis_type(self, t: EllipsisType) -> Type: return AnyType(TypeOfAny.from_error) def visit_type_type(self, t: TypeType) -> Type: - return TypeType.make_normalized(self.anal_type(t.item), line=t.line) + return TypeType.make_normalized( + self.anal_type(t.item), line=t.line, is_type_form=t.is_type_form + ) def visit_placeholder_type(self, t: PlaceholderType) -> Type: n = ( diff --git a/mypy/typeops.py b/mypy/typeops.py index 9aa08b40a991..c6f74cc5b3a0 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -526,7 +526,7 @@ def erase_to_bound(t: Type) -> Type: return t.upper_bound if isinstance(t, TypeType): if isinstance(t.item, TypeVarType): - return TypeType.make_normalized(t.item.upper_bound) + return TypeType.make_normalized(t.item.upper_bound, is_type_form=t.is_type_form) return t diff --git a/mypy/types.py b/mypy/types.py index e9d299dbc8fc..5d0fb507fd1f 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -3076,11 +3076,14 @@ def serialize(self) -> JsonDict: class TypeType(ProperType): - """For types like Type[User]. + """For types like Type[User] or TypeForm[User | None]. - This annotates variables that are class objects, constrained by + Type[C] annotates variables that are class objects, constrained by the type argument. See PEP 484 for more details. + TypeForm[T] annotates variables that hold the result of evaluating + a type expression. See PEP 747 for more details. + We may encounter expressions whose values are specific classes; those are represented as callables (possibly overloaded) corresponding to the class's constructor's signature and returning @@ -3103,35 +3106,47 @@ class TypeType(ProperType): assumption). """ - __slots__ = ("item",) + __slots__ = ("item", "is_type_form") # This can't be everything, but it can be a class reference, # a generic class instance, a union, Any, a type variable... item: ProperType + # If True then this TypeType represents a TypeForm[T]. + # If False then this TypeType represents a Type[C]. + is_type_form: bool + def __init__( self, item: Bogus[Instance | AnyType | TypeVarType | TupleType | NoneType | CallableType], *, line: int = -1, column: int = -1, + is_type_form: bool = False, ) -> None: """To ensure Type[Union[A, B]] is always represented as Union[Type[A], Type[B]], item of type UnionType must be handled through make_normalized static method. """ super().__init__(line, column) self.item = item + self.is_type_form = is_type_form @staticmethod - def make_normalized(item: Type, *, line: int = -1, column: int = -1) -> ProperType: + def make_normalized( + item: Type, *, line: int = -1, column: int = -1, is_type_form: bool = False + ) -> ProperType: item = get_proper_type(item) - if isinstance(item, UnionType): - return UnionType.make_union( - [TypeType.make_normalized(union_item) for union_item in item.items], - line=line, - column=column, - ) - return TypeType(item, line=line, column=column) # type: ignore[arg-type] + if is_type_form: + # Don't convert TypeForm[X | Y] to (TypeForm[X] | TypeForm[Y]) + pass + else: + if isinstance(item, UnionType): + return UnionType.make_union( + [TypeType.make_normalized(union_item) for union_item in item.items], + line=line, + column=column, + ) + return TypeType(item, line=line, column=column, is_type_form=is_type_form) # type: ignore[arg-type] def accept(self, visitor: TypeVisitor[T]) -> T: return visitor.visit_type_type(self) @@ -3145,12 +3160,18 @@ def __eq__(self, other: object) -> bool: return self.item == other.item def serialize(self) -> JsonDict: - return {".class": "TypeType", "item": self.item.serialize()} + return { + ".class": "TypeType", + "item": self.item.serialize(), + "is_type_form": self.is_type_form, + } @classmethod def deserialize(cls, data: JsonDict) -> Type: assert data[".class"] == "TypeType" - return TypeType.make_normalized(deserialize_type(data["item"])) + return TypeType.make_normalized( + deserialize_type(data["item"]), is_type_form=data["is_type_form"] + ) class PlaceholderType(ProperType): @@ -3519,7 +3540,11 @@ def visit_ellipsis_type(self, t: EllipsisType, /) -> str: return "..." def visit_type_type(self, t: TypeType, /) -> str: - return f"type[{t.item.accept(self)}]" + if t.is_type_form: + type_name = "TypeForm" + else: + type_name = "type" + return f"{type_name}[{t.item.accept(self)}]" def visit_placeholder_type(self, t: PlaceholderType, /) -> str: return f"" diff --git a/mypy/typeshed/stdlib/typing.pyi b/mypy/typeshed/stdlib/typing.pyi index d296c8d92149..2caa6427ce03 100644 --- a/mypy/typeshed/stdlib/typing.pyi +++ b/mypy/typeshed/stdlib/typing.pyi @@ -239,6 +239,7 @@ Generic: _SpecialForm Protocol: _SpecialForm Callable: _SpecialForm Type: _SpecialForm +TypeForm: _SpecialForm NoReturn: _SpecialForm ClassVar: _SpecialForm diff --git a/mypy/typeshed/stdlib/typing_extensions.pyi b/mypy/typeshed/stdlib/typing_extensions.pyi index 3f7c25712081..3a775c97b526 100644 --- a/mypy/typeshed/stdlib/typing_extensions.pyi +++ b/mypy/typeshed/stdlib/typing_extensions.pyi @@ -56,6 +56,7 @@ from typing import ( # noqa: Y022,Y037,Y038,Y039,UP035 Tuple as Tuple, Type as Type, TypedDict as TypedDict, + TypeForm as TypeForm, TypeVar as _TypeVar, Union as Union, _Alias, diff --git a/mypy/visitor.py b/mypy/visitor.py index d1b2ca416410..e150788ec3c1 100644 --- a/mypy/visitor.py +++ b/mypy/visitor.py @@ -79,6 +79,10 @@ def visit_comparison_expr(self, o: mypy.nodes.ComparisonExpr, /) -> T: def visit_cast_expr(self, o: mypy.nodes.CastExpr, /) -> T: pass + @abstractmethod + def visit_type_form_expr(self, o: mypy.nodes.TypeFormExpr, /) -> T: + pass + @abstractmethod def visit_assert_type_expr(self, o: mypy.nodes.AssertTypeExpr, /) -> T: pass @@ -511,6 +515,9 @@ def visit_comparison_expr(self, o: mypy.nodes.ComparisonExpr, /) -> T: def visit_cast_expr(self, o: mypy.nodes.CastExpr, /) -> T: raise NotImplementedError() + def visit_type_form_expr(self, o: mypy.nodes.TypeFormExpr, /) -> T: + raise NotImplementedError() + def visit_assert_type_expr(self, o: mypy.nodes.AssertTypeExpr, /) -> T: raise NotImplementedError() diff --git a/mypyc/irbuild/visitor.py b/mypyc/irbuild/visitor.py index 05a033c3e6ad..dc81e95a2980 100644 --- a/mypyc/irbuild/visitor.py +++ b/mypyc/irbuild/visitor.py @@ -73,6 +73,7 @@ TypeAliasStmt, TypeApplication, TypedDictExpr, + TypeFormExpr, TypeVarExpr, TypeVarTupleExpr, UnaryExpr, @@ -387,6 +388,9 @@ def visit_var(self, o: Var) -> None: def visit_cast_expr(self, o: CastExpr) -> Value: assert False, "CastExpr should have been handled in CallExpr" + def visit_type_form_expr(self, o: TypeFormExpr) -> Value: + assert False, "TypeFormExpr should have been handled in CallExpr" + def visit_assert_type_expr(self, o: AssertTypeExpr) -> Value: assert False, "AssertTypeExpr should have been handled in CallExpr" diff --git a/test-data/unit/check-typeform.test b/test-data/unit/check-typeform.test new file mode 100644 index 000000000000..425aa2687586 --- /dev/null +++ b/test-data/unit/check-typeform.test @@ -0,0 +1,702 @@ +-- TypeForm Type + +[case testRecognizesUnparameterizedTypeFormInAnnotation] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import TypeForm +typx: TypeForm = str +reveal_type(typx) # N: Revealed type is "TypeForm[Any]" +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testRecognizesParameterizedTypeFormInAnnotation] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import TypeForm +typx: TypeForm[str] = str +reveal_type(typx) # N: Revealed type is "TypeForm[builtins.str]" +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + + +-- Type Expression Location: Assignment + +[case testCanAssignTypeExpressionToTypeFormVariable] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import TypeForm +typx: TypeForm[str] = str +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testCanAssignTypeExpressionToUnionTypeFormVariable] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import TypeForm +typx: TypeForm[str | None] = str | None +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testCannotAssignTypeExpressionToTypeFormVariableWithIncompatibleItemType] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import TypeForm +typx: TypeForm[str] = int # E: Incompatible types in assignment (expression has type "TypeForm[int]", variable has type "TypeForm[str]") +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testCanAssignValueExpressionToTypeFormVariableIfValueIsATypeForm1] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import TypeForm +typx1: TypeForm = str +typx2: TypeForm = typx1 # looks like a type expression: name +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testCanAssignValueExpressionToTypeFormVariableIfValueIsATypeForm2] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import TypeForm +def identity_tf(x: TypeForm) -> TypeForm: + return x +typx1: TypeForm = str +typx2: TypeForm = identity_tf(typx1) # does not look like a type expression +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testCannotAssignValueExpressionToTypeFormVariableIfValueIsNotATypeForm] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import TypeForm +val: int = 42 +typx: TypeForm = val # E: Incompatible types in assignment (expression has type "int", variable has type "TypeForm[Any]") +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testCanAssignNoneTypeExpressionToTypeFormVariable] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import TypeForm +typx: TypeForm = None +reveal_type(typx) # N: Revealed type is "TypeForm[Any]" +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testCanAssignTypeExpressionToTypeFormVariableDeclaredEarlier] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import Type, TypeForm +typ: Type +typ = int | None # E: Incompatible types in assignment (expression has type "object", variable has type "Type[Any]") +typx: TypeForm +typx = int | None +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testCanAssignTypeExpressionWithStringAnnotationToTypeFormVariable] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import TypeForm +typx: TypeForm[str | None] = 'str | None' +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + + +-- Type Expression Location: Function Parameter + +[case testCanPassTypeExpressionToTypeFormParameterInFunction] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import TypeForm +def is_type(typx: TypeForm) -> bool: + return isinstance(typx, type) +is_type(int | None) +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testCannotPassTypeExpressionToTypeParameter] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +def is_type(typ: type) -> bool: + return isinstance(typ, type) +is_type(int | None) # E: Argument 1 to "is_type" has incompatible type "object"; expected "type" +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testCanPassTypeExpressionToTypeFormParameterInMethod] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import TypeForm +class C: + def is_type(self, typx: TypeForm) -> bool: + return isinstance(typx, type) +C().is_type(int | None) +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testCanPassTypeExpressionToTypeFormParameterInOverload] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import overload, TypeForm +@overload +def is_type(typx: TypeForm) -> bool: ... +@overload +def is_type(typx: type) -> bool: ... +def is_type(typx): + return isinstance(typx, type) +is_type(int | None) +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testCanPassTypeExpressionToTypeFormParameterInDecorator] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import Callable, TypeForm, TypeVar +P = TypeVar('P') +R = TypeVar('R') +def expects_type(typx: TypeForm) -> Callable[[Callable[[P], R]], Callable[[P], R]]: + def wrap(func: Callable[[P], R]) -> Callable[[P], R]: + func.expected_type = typx # type: ignore[attr-defined] + return func + return wrap +@expects_type(int | None) +def sum_ints(x: int | None) -> int: + return (x or 0) +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testCanPassTypeExpressionToTypeFormVarargsParameter] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import Callable, ParamSpec, TypeForm, TypeVar +P = ParamSpec('P') +R = TypeVar('R') +def expects_types(*typxs: TypeForm) -> Callable[[Callable[P, R]], Callable[P, R]]: + def wrap(func: Callable[P, R]) -> Callable[P, R]: + func.expected_types = typxs # type: ignore[attr-defined] + return func + return wrap +@expects_types(int | None, int) +def sum_ints(x: int | None, y: int) -> tuple[int, int]: + return ((x or 0), y) +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testCanPassTypeExpressionWithStringAnnotationToTypeFormParameter] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import TypeForm +def is_type(typx: TypeForm) -> bool: + return isinstance(typx, type) +is_type('int | None') +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + + +-- Type Expression Location: Return Statement + +[case testCanReturnTypeExpressionInFunctionWithTypeFormReturnType] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import TypeForm +def maybe_int_type() -> TypeForm: + return int | None +reveal_type(maybe_int_type()) # N: Revealed type is "TypeForm[Any]" +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testCanReturnTypeExpressionWithStringAnnotationInFunctionWithTypeFormReturnType] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import TypeForm +def maybe_int_type() -> TypeForm: + return 'int | None' +reveal_type(maybe_int_type()) # N: Revealed type is "TypeForm[Any]" +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + + +-- Type Expression Location: Other + +-- In particular ensure that ExpressionChecker.try_parse_as_type_expression() in +-- the TypeChecker pass is able to parse types correctly even though it doesn't +-- have the same rich context as SemanticAnalyzer.try_parse_as_type_expression(). + +[case testTypeExpressionWithoutStringAnnotationRecognizedInOtherSyntacticLocations] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import Dict, List, TypeForm +list_of_typx: List[TypeForm] = [int | str] +dict_with_typx_keys: Dict[TypeForm, int] = { + int | str: 1, + str | None: 2, +} +dict_with_typx_keys[int | str] += 1 +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] + +[case testTypeExpressionWithStringAnnotationNotRecognizedInOtherSyntacticLocations] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import Dict, List, TypeForm +list_of_typx: List[TypeForm] = ['int | str'] # N: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. \ + # E: List item 0 has incompatible type "str"; expected "TypeForm[Any]" +dict_with_typx_keys: Dict[TypeForm, int] = { + 'int | str': 1, # N: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. \ + # E: Dict entry 0 has incompatible type "str": "int"; expected "TypeForm[Any]": "int" + 'str | None': 2, # N: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. \ + # E: Dict entry 1 has incompatible type "str": "int"; expected "TypeForm[Any]": "int" +} +dict_with_typx_keys['int | str'] += 1 # N: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. \ + # E: Invalid index type "str" for "Dict[TypeForm[Any], int]"; expected type "TypeForm[Any]" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] + +[case testValueExpressionWithStringInTypeFormContextEmitsConservativeWarning] +from typing import Any, Dict, List, TypeForm +types: Dict[str, TypeForm] = {'any': Any} +# Ensure warning can be ignored if does not apply. +list_of_typx1: List[TypeForm] = [types['any']] # N: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. +list_of_typx2: List[TypeForm] = [types['any']] # type: ignore[maybe-unrecognized-str-typeform] +# Ensure warning can be fixed using the suggested fix in the warning message. +list_of_typx3: List[TypeForm] = ['Any'] # N: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. \ + # E: List item 0 has incompatible type "str"; expected "TypeForm[Any]" +list_of_typx4: List[TypeForm] = [TypeForm('Any')] +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] + +[case testSelfRecognizedInOtherSyntacticLocations] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import List, Self, TypeForm +class C: + def foo(self) -> None: + list_of_typx1: List[TypeForm] = [Self] + typx1: TypeForm = Self + typx2: TypeForm = 'Self' +list_of_typx2: List[TypeForm] = [Self] # E: List item 0 has incompatible type "int"; expected "TypeForm[Any]" +typx3: TypeForm = Self # E: Incompatible types in assignment (expression has type "int", variable has type "TypeForm[Any]") +typx4: TypeForm = 'Self' # E: Incompatible types in assignment (expression has type "str", variable has type "TypeForm[Any]") +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testNameOrDottedNameRecognizedInOtherSyntacticLocations] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +import typing +from typing import List, TypeForm +list_of_typx: List[TypeForm] = [List | typing.Optional[str]] +typx: TypeForm = List | typing.Optional[str] +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testInvalidNameOrDottedNameRecognizedInOtherSyntacticLocations] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import List, TypeForm +list_of_typx1: List[TypeForm] = [NoSuchType] # E: Name "NoSuchType" is not defined +list_of_typx2: List[TypeForm] = [no_such_module.NoSuchType] # E: Name "no_such_module" is not defined +typx1: TypeForm = NoSuchType # E: Name "NoSuchType" is not defined +typx2: TypeForm = no_such_module.NoSuchType # E: Name "no_such_module" is not defined +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + + +-- Assignability (is_subtype) + +[case testTypeFormToTypeFormAssignability] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +# - TypeForm[T1] is assignable to TypeForm[T2] iff T1 is assignable to T2. +# - In particular TypeForm[Any] is assignable to TypeForm[Any]. +from typing import TypeForm +INT_OR_STR_TF: TypeForm[int | str] = int | str +INT_TF: TypeForm[int] = int +STR_TF: TypeForm[str] = str +OBJECT_TF: TypeForm[object] = object +ANY_TF: TypeForm = object +reveal_type(ANY_TF) # N: Revealed type is "TypeForm[Any]" +typx1: TypeForm[int | str] = INT_OR_STR_TF +typx2: TypeForm[int | str] = INT_TF +typx3: TypeForm[int | str] = STR_TF +typx4: TypeForm[int | str] = OBJECT_TF # E: Incompatible types in assignment (expression has type "TypeForm[object]", variable has type "TypeForm[Union[int, str]]") +typx5: TypeForm[int | str] = ANY_TF # no error +typx6: TypeForm[int] = INT_OR_STR_TF # E: Incompatible types in assignment (expression has type "TypeForm[Union[int, str]]", variable has type "TypeForm[int]") +typx7: TypeForm[int] = INT_TF +typx8: TypeForm[int] = STR_TF # E: Incompatible types in assignment (expression has type "TypeForm[str]", variable has type "TypeForm[int]") +typx9: TypeForm[int] = OBJECT_TF # E: Incompatible types in assignment (expression has type "TypeForm[object]", variable has type "TypeForm[int]") +typx10: TypeForm[int] = ANY_TF # no error +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testTypeToTypeFormAssignability] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +# - Type[C] is assignable to TypeForm[T] iff C is assignable to T. +# - In particular Type[Any] is assignable to TypeForm[Any]. +from typing import Type, TypeForm +INT_T: Type[int] = int +STR_T: Type[str] = str +OBJECT_T: Type[object] = object +ANY_T: Type = object +reveal_type(ANY_T) # N: Revealed type is "Type[Any]" +typx1: TypeForm[int | str] = INT_T +typx2: TypeForm[int | str] = STR_T +typx3: TypeForm[int | str] = OBJECT_T # E: Incompatible types in assignment (expression has type "Type[object]", variable has type "TypeForm[Union[int, str]]") +typx4: TypeForm[int | str] = ANY_T # no error +typx5: TypeForm[int] = INT_T +typx6: TypeForm[int] = STR_T # E: Incompatible types in assignment (expression has type "Type[str]", variable has type "TypeForm[int]") +typx7: TypeForm[int] = OBJECT_T # E: Incompatible types in assignment (expression has type "Type[object]", variable has type "TypeForm[int]") +typx8: TypeForm[int] = ANY_T # no error +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testTypeFormToTypeAssignability] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +# - TypeForm[T] is NOT assignable to Type[C]. +# - In particular TypeForm[Any] is NOT assignable to Type[Any]. +from typing import Type, TypeForm +INT_OR_STR_TF: TypeForm[int | str] = int | str +INT_TF: TypeForm[int] = int +STR_TF: TypeForm[str] = str +OBJECT_TF: TypeForm[object] = object +ANY_TF: TypeForm = object +reveal_type(ANY_TF) # N: Revealed type is "TypeForm[Any]" +typ1: Type[int] = INT_OR_STR_TF # E: Incompatible types in assignment (expression has type "TypeForm[Union[int, str]]", variable has type "Type[int]") +typ2: Type[int] = INT_TF # E: Incompatible types in assignment (expression has type "TypeForm[int]", variable has type "Type[int]") +typ3: Type[int] = STR_TF # E: Incompatible types in assignment (expression has type "TypeForm[str]", variable has type "Type[int]") +typ4: Type[int] = OBJECT_TF # E: Incompatible types in assignment (expression has type "TypeForm[object]", variable has type "Type[int]") +typ5: Type[int] = ANY_TF # E: Incompatible types in assignment (expression has type "TypeForm[Any]", variable has type "Type[int]") +typ6: Type[object] = INT_OR_STR_TF # E: Incompatible types in assignment (expression has type "TypeForm[Union[int, str]]", variable has type "Type[object]") +typ7: Type[object] = INT_TF # E: Incompatible types in assignment (expression has type "TypeForm[int]", variable has type "Type[object]") +typ8: Type[object] = STR_TF # E: Incompatible types in assignment (expression has type "TypeForm[str]", variable has type "Type[object]") +typ9: Type[object] = OBJECT_TF # E: Incompatible types in assignment (expression has type "TypeForm[object]", variable has type "Type[object]") +typ10: Type[object] = ANY_TF # E: Incompatible types in assignment (expression has type "TypeForm[Any]", variable has type "Type[object]") +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +# NOTE: This test doesn't involve TypeForm at all, but is still illustrative +# when compared with similarly structured TypeForm-related tests above. +[case testTypeToTypeAssignability] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +# - Type[C1] is assignable to Type[C2] iff C1 is assignable to C2. +# - In particular Type[Any] is assignable to Type[Any]. +from typing import Type +INT_T: Type[int] = int +STR_T: Type[str] = str +OBJECT_T: Type[object] = object +ANY_T: Type = object +reveal_type(ANY_T) # N: Revealed type is "Type[Any]" +typ1: Type[int] = INT_T +typ2: Type[int] = STR_T # E: Incompatible types in assignment (expression has type "Type[str]", variable has type "Type[int]") +typ3: Type[int] = OBJECT_T # E: Incompatible types in assignment (expression has type "Type[object]", variable has type "Type[int]") +typ4: Type[int] = ANY_T # no error +typ5: Type[object] = INT_T +typ6: Type[object] = STR_T +typ7: Type[object] = OBJECT_T +typ8: Type[object] = ANY_T # no error +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testTypeFormToObjectAssignability] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +# - TypeForm[T] is assignable to object and Any. +from typing import Any, TypeForm +INT_TF: TypeForm[int] = int +OBJECT_TF: TypeForm[object] = object +ANY_TF: TypeForm = object +reveal_type(ANY_TF) # N: Revealed type is "TypeForm[Any]" +obj1: object = INT_TF +obj2: object = OBJECT_TF +obj3: object = ANY_TF +any1: Any = INT_TF +any2: Any = OBJECT_TF +any3: Any = ANY_TF +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + + +-- Join (join_types) + +[case testTypeFormToTypeFormJoin] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +# - TypeForm[T1] join TypeForm[T2] == TypeForm[T1 join T2] +from typing import TypeForm +class AB: + pass +class A(AB): + pass +class B(AB): + pass +A_TF: TypeForm[A] = A +B_TF: TypeForm[B] = B +reveal_type([A_TF, B_TF][0]) # N: Revealed type is "TypeForm[__main__.AB]" +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testTypeToTypeFormJoin] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +# - TypeForm[T1] join Type[T2] == TypeForm[T1 join T2] +from typing import Type, TypeForm +class AB: + pass +class A(AB): + pass +class B(AB): + pass +A_T: Type[A] = A +B_TF: TypeForm[B] = B +reveal_type([A_T, B_TF][0]) # N: Revealed type is "TypeForm[__main__.AB]" +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testTypeFormToTypeJoin] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +# - TypeForm[T1] join Type[T2] == TypeForm[T1 join T2] +from typing import Type, TypeForm +class AB: + pass +class A(AB): + pass +class B(AB): + pass +A_TF: TypeForm[A] = A +B_T: Type[B] = B +reveal_type([A_TF, B_T][0]) # N: Revealed type is "TypeForm[__main__.AB]" +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +# NOTE: This test doesn't involve TypeForm at all, but is still illustrative +# when compared with similarly structured TypeForm-related tests above. +[case testTypeToTypeJoin] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +# - Type[T1] join Type[T2] == Type[T1 join T2] +from typing import Type, TypeForm +class AB: + pass +class A(AB): + pass +class B(AB): + pass +A_T: Type[A] = A +B_T: Type[B] = B +reveal_type([A_T, B_T][0]) # N: Revealed type is "Type[__main__.AB]" +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + + +-- Meet (meet_types) + +[case testTypeFormToTypeFormMeet] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +# - TypeForm[T1] meet TypeForm[T2] == TypeForm[T1 meet T2] +from typing import Callable, TypeForm, TypeVar +class AB: + pass +class A(AB): + pass +class B(AB): + pass +class C(AB): + pass +T = TypeVar('T') +def f(x: Callable[[T, T], None]) -> T: pass # type: ignore[empty-body] +def g(x: TypeForm[A | B], y: TypeForm[B | C]) -> None: pass +reveal_type(f(g)) # N: Revealed type is "TypeForm[__main__.B]" +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testTypeToTypeFormMeet] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +# - TypeForm[T1] meet Type[T2] == Type[T1 meet T2] +from typing import Callable, Type, TypeForm, TypeVar +class AB: + pass +class A(AB): + pass +class B(AB): + pass +class C(AB): + pass +T = TypeVar('T') +def f(x: Callable[[T, T], None]) -> T: pass # type: ignore[empty-body] +def g(x: Type[B], y: TypeForm[B | C]) -> None: pass +reveal_type(f(g)) # N: Revealed type is "Type[__main__.B]" +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testTypeFormToTypeMeet] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +# - TypeForm[T1] meet Type[T2] == Type[T1 meet T2] +from typing import Callable, Type, TypeForm, TypeVar +class AB: + pass +class A(AB): + pass +class B(AB): + pass +class C(AB): + pass +T = TypeVar('T') +def f(x: Callable[[T, T], None]) -> T: pass # type: ignore[empty-body] +def g(x: TypeForm[A | B], y: Type[B]) -> None: pass +reveal_type(f(g)) # N: Revealed type is "Type[__main__.B]" +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +# NOTE: This test doesn't involve TypeForm at all, but is still illustrative +# when compared with similarly structured TypeForm-related tests above. +[case testTypeToTypeMeet] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +# - Type[T1] meet Type[T2] == Type[T1 meet T2] +from typing import Callable, Type, TypedDict, TypeForm, TypeVar +class AB(TypedDict): + a: str + b: str +class BC(TypedDict): + b: str + c: str +T = TypeVar('T') +def f(x: Callable[[T, T], None]) -> T: pass # type: ignore[empty-body] +def g(x: Type[AB], y: Type[BC]) -> None: pass +reveal_type(f(g)) # N: Revealed type is "Type[TypedDict({'b': builtins.str, 'c': builtins.str, 'a': builtins.str})]" +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + + +-- TypeForm(...) Expression + +[case testTypeFormExpression] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import TypeForm +tf1 = TypeForm(int | str) +reveal_type(tf1) # N: Revealed type is "TypeForm[Union[builtins.int, builtins.str]]" +tf2 = TypeForm('int | str') +reveal_type(tf2) # N: Revealed type is "TypeForm[Union[builtins.int, builtins.str]]" +tf3: TypeForm = TypeForm(int | str) +reveal_type(tf3) # N: Revealed type is "TypeForm[Any]" +tf4: TypeForm = TypeForm(1) # E: Invalid type: try using Literal[1] instead? +tf5: TypeForm = TypeForm(int) | TypeForm(str) # E: Invalid self argument "TypeForm[int]" to attribute function "__or__" with type "Callable[[type, object], object]" \ + # E: Incompatible types in assignment (expression has type "object", variable has type "TypeForm[Any]") +tf6: TypeForm = TypeForm(TypeForm(int) | TypeForm(str)) # E: TypeForm argument is not a type +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + + +-- isinstance + +[case testTypeFormAndTypeIsinstance] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import TypeForm +typx: TypeForm[str] = str +if isinstance(typx, type): + reveal_type(typx) # N: Revealed type is "Type[builtins.str]" +else: + reveal_type(typx) # N: Revealed type is "TypeForm[builtins.str]" +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + + +-- Type Variables + +[case testLinkTypeFormToTypeFormWithTypeVariable] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import TypeForm, TypeVar +T = TypeVar('T') +def as_typeform(typx: TypeForm[T]) -> TypeForm[T]: + return typx +reveal_type(as_typeform(int | str)) # N: Revealed type is "TypeForm[Union[builtins.int, builtins.str]]" +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testLinkTypeFormToTypeWithTypeVariable] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import Type, TypeForm, TypeVar +T = TypeVar('T') +def as_type(typx: TypeForm[T]) -> Type[T] | None: + if isinstance(typx, type): + return typx + else: + return None +reveal_type(as_type(int | str)) # N: Revealed type is "Union[Type[builtins.int], Type[builtins.str], None]" +reveal_type(as_type(int)) # N: Revealed type is "Union[Type[builtins.int], None]" +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testLinkTypeFormToInstanceWithTypeVariable] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import TypeForm, TypeVar +T = TypeVar('T') +def as_instance(typx: TypeForm[T]) -> T | None: + if isinstance(typx, type): + return typx() + else: + return None +reveal_type(as_instance(int | str)) # N: Revealed type is "Union[builtins.int, builtins.str, None]" +reveal_type(as_instance(int)) # N: Revealed type is "Union[builtins.int, None]" +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testLinkTypeFormToTypeIsWithTypeVariable] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import TypeForm, TypeVar +from typing_extensions import TypeIs +T = TypeVar('T') +def isassignable(value: object, typx: TypeForm[T]) -> TypeIs[T]: + raise BaseException() +count: int | str = 1 +if isassignable(count, int): + reveal_type(count) # N: Revealed type is "builtins.int" +else: + reveal_type(count) # N: Revealed type is "builtins.str" +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testLinkTypeFormToTypeGuardWithTypeVariable] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import TypeForm, TypeVar +from typing_extensions import TypeGuard +T = TypeVar('T') +def isassignable(value: object, typx: TypeForm[T]) -> TypeGuard[T]: + raise BaseException() +count: int | str = 1 +if isassignable(count, int): + reveal_type(count) # N: Revealed type is "builtins.int" +else: + reveal_type(count) # N: Revealed type is "Union[builtins.int, builtins.str]" +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + + +-- Misc + +[case testTypeFormHasAllObjectAttributesAndMethods] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import TypeForm +typx: TypeForm[int | str] = int | str +print(typx.__class__) # OK +print(typx.__hash__()) # OK +obj: object = typx +[file builtins.py] +class object: + def __init__(self) -> None: pass + __class__: None + def __hash__(self) -> int: pass +def print(x): + raise BaseException() +class int: pass +class dict: pass +class str: pass +class type: pass +class tuple: pass +class ellipsis: pass +class BaseException: pass +class float: pass +[typing fixtures/typing-full.pyi] + +[case testDottedTypeFormsAreRecognized] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import TypeForm +import typing +class C1: + class C2: + pass +typx1: TypeForm[C1.C2] = C1.C2 # OK +typx2: TypeForm[typing.Any] = typing.Any # OK +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +-- mypy already refused to recognize TypeVars in value expressions before +-- the TypeForm feature was introduced. +[case testTypeVarTypeFormsAreOnlyRecognizedInStringAnnotation] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import Generic, List, TypeForm, TypeVar +E = TypeVar('E') +class Box(Generic[E]): + def foo(self, e: E) -> None: + list_of_typx: List[TypeForm] = [E] # E: "E" is a type variable and only valid in type context + typx1: TypeForm = E # E: "E" is a type variable and only valid in type context + typx2: TypeForm = 'E' +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testIncompleteTypeFormsAreNotRecognized] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import Optional, TypeForm +typx: TypeForm = Optional # E: Incompatible types in assignment (expression has type "int", variable has type "TypeForm[Any]") +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] diff --git a/test-data/unit/check-union-or-syntax.test b/test-data/unit/check-union-or-syntax.test index 35af44c62800..cf69920de8e3 100644 --- a/test-data/unit/check-union-or-syntax.test +++ b/test-data/unit/check-union-or-syntax.test @@ -106,8 +106,7 @@ b: X # E: Variable "__main__.X" is not valid as a type \ # flags: --python-version 3.9 from __future__ import annotations from typing import List -T = int | str # E: Invalid type alias: expression is not a valid type \ - # E: Unsupported left operand type for | ("type[int]") +T = int | str # E: Invalid type alias: expression is not a valid type class C(List[int | str]): # E: Type expected within [...] \ # E: Invalid base class "List" pass diff --git a/test-data/unit/fixtures/dict.pyi b/test-data/unit/fixtures/dict.pyi index ed2287511161..b87255f5486a 100644 --- a/test-data/unit/fixtures/dict.pyi +++ b/test-data/unit/fixtures/dict.pyi @@ -19,7 +19,9 @@ class object: def __init__(self) -> None: pass def __eq__(self, other: object) -> bool: pass -class type: pass +class type: + # Real implementation returns UnionType + def __or__(self, value: object, /) -> object: pass class dict(Mapping[KT, VT]): @overload diff --git a/test-data/unit/fixtures/tuple.pyi b/test-data/unit/fixtures/tuple.pyi index d01cd0034d26..f5e21aa228c0 100644 --- a/test-data/unit/fixtures/tuple.pyi +++ b/test-data/unit/fixtures/tuple.pyi @@ -13,6 +13,8 @@ class object: class type: def __init__(self, *a: object) -> None: pass def __call__(self, *a: object) -> object: pass + # Real implementation returns UnionType + def __or__(self, value: object, /) -> object: pass class tuple(Sequence[_Tco], Generic[_Tco]): def __new__(cls: Type[_T], iterable: Iterable[_Tco] = ...) -> _T: ... def __iter__(self) -> Iterator[_Tco]: pass diff --git a/test-data/unit/fixtures/typing-full.pyi b/test-data/unit/fixtures/typing-full.pyi index 8e0116aab1c2..a081952d3236 100644 --- a/test-data/unit/fixtures/typing-full.pyi +++ b/test-data/unit/fixtures/typing-full.pyi @@ -30,6 +30,7 @@ Protocol = 0 Tuple = 0 _promote = 0 Type = 0 +TypeForm = 0 no_type_check = 0 ClassVar = 0 Final = 0 From 9a1992a1027436702858037d500b834d776a249f Mon Sep 17 00:00:00 2001 From: David Foster Date: Thu, 24 Jul 2025 08:05:19 -0400 Subject: [PATCH 02/20] Fix multiple issues broken by upstream In particular: - Adjust error messages to use lowercased type names, which is now the default - Adjust error message to align with upstream stub changes - Fix multiple definition of TypeForm in typing_extensions.pyi, because definition was added upstream - Fix TypeType equality definition to recognize type forms - Fixes test: $ pytest -q -k testTypeFormToTypeAssignability --- mypy/types.py | 2 +- mypy/typeshed/stdlib/typing_extensions.pyi | 1 - test-data/unit/check-typeform.test | 55 +++++++++++----------- 3 files changed, 28 insertions(+), 30 deletions(-) diff --git a/mypy/types.py b/mypy/types.py index 5d0fb507fd1f..2c2dd9bf346d 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -3157,7 +3157,7 @@ def __hash__(self) -> int: def __eq__(self, other: object) -> bool: if not isinstance(other, TypeType): return NotImplemented - return self.item == other.item + return self.item == other.item and self.is_type_form == other.is_type_form def serialize(self) -> JsonDict: return { diff --git a/mypy/typeshed/stdlib/typing_extensions.pyi b/mypy/typeshed/stdlib/typing_extensions.pyi index 3a775c97b526..3f7c25712081 100644 --- a/mypy/typeshed/stdlib/typing_extensions.pyi +++ b/mypy/typeshed/stdlib/typing_extensions.pyi @@ -56,7 +56,6 @@ from typing import ( # noqa: Y022,Y037,Y038,Y039,UP035 Tuple as Tuple, Type as Type, TypedDict as TypedDict, - TypeForm as TypeForm, TypeVar as _TypeVar, Union as Union, _Alias, diff --git a/test-data/unit/check-typeform.test b/test-data/unit/check-typeform.test index 425aa2687586..72a19d91f12c 100644 --- a/test-data/unit/check-typeform.test +++ b/test-data/unit/check-typeform.test @@ -78,7 +78,7 @@ reveal_type(typx) # N: Revealed type is "TypeForm[Any]" # flags: --python-version 3.14 --enable-incomplete-feature=TypeForm from typing import Type, TypeForm typ: Type -typ = int | None # E: Incompatible types in assignment (expression has type "object", variable has type "Type[Any]") +typ = int | None # E: Incompatible types in assignment (expression has type "object", variable has type "type[Any]") typx: TypeForm typx = int | None [builtins fixtures/tuple.pyi] @@ -227,7 +227,7 @@ dict_with_typx_keys: Dict[TypeForm, int] = { # E: Dict entry 1 has incompatible type "str": "int"; expected "TypeForm[Any]": "int" } dict_with_typx_keys['int | str'] += 1 # N: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. \ - # E: Invalid index type "str" for "Dict[TypeForm[Any], int]"; expected type "TypeForm[Any]" + # E: Invalid index type "str" for "dict[TypeForm[Any], int]"; expected type "TypeForm[Any]" [builtins fixtures/dict.pyi] [typing fixtures/typing-full.pyi] @@ -313,14 +313,14 @@ INT_T: Type[int] = int STR_T: Type[str] = str OBJECT_T: Type[object] = object ANY_T: Type = object -reveal_type(ANY_T) # N: Revealed type is "Type[Any]" +reveal_type(ANY_T) # N: Revealed type is "type[Any]" typx1: TypeForm[int | str] = INT_T typx2: TypeForm[int | str] = STR_T -typx3: TypeForm[int | str] = OBJECT_T # E: Incompatible types in assignment (expression has type "Type[object]", variable has type "TypeForm[Union[int, str]]") +typx3: TypeForm[int | str] = OBJECT_T # E: Incompatible types in assignment (expression has type "type[object]", variable has type "TypeForm[Union[int, str]]") typx4: TypeForm[int | str] = ANY_T # no error typx5: TypeForm[int] = INT_T -typx6: TypeForm[int] = STR_T # E: Incompatible types in assignment (expression has type "Type[str]", variable has type "TypeForm[int]") -typx7: TypeForm[int] = OBJECT_T # E: Incompatible types in assignment (expression has type "Type[object]", variable has type "TypeForm[int]") +typx6: TypeForm[int] = STR_T # E: Incompatible types in assignment (expression has type "type[str]", variable has type "TypeForm[int]") +typx7: TypeForm[int] = OBJECT_T # E: Incompatible types in assignment (expression has type "type[object]", variable has type "TypeForm[int]") typx8: TypeForm[int] = ANY_T # no error [builtins fixtures/tuple.pyi] [typing fixtures/typing-full.pyi] @@ -336,16 +336,16 @@ STR_TF: TypeForm[str] = str OBJECT_TF: TypeForm[object] = object ANY_TF: TypeForm = object reveal_type(ANY_TF) # N: Revealed type is "TypeForm[Any]" -typ1: Type[int] = INT_OR_STR_TF # E: Incompatible types in assignment (expression has type "TypeForm[Union[int, str]]", variable has type "Type[int]") -typ2: Type[int] = INT_TF # E: Incompatible types in assignment (expression has type "TypeForm[int]", variable has type "Type[int]") -typ3: Type[int] = STR_TF # E: Incompatible types in assignment (expression has type "TypeForm[str]", variable has type "Type[int]") -typ4: Type[int] = OBJECT_TF # E: Incompatible types in assignment (expression has type "TypeForm[object]", variable has type "Type[int]") -typ5: Type[int] = ANY_TF # E: Incompatible types in assignment (expression has type "TypeForm[Any]", variable has type "Type[int]") -typ6: Type[object] = INT_OR_STR_TF # E: Incompatible types in assignment (expression has type "TypeForm[Union[int, str]]", variable has type "Type[object]") -typ7: Type[object] = INT_TF # E: Incompatible types in assignment (expression has type "TypeForm[int]", variable has type "Type[object]") -typ8: Type[object] = STR_TF # E: Incompatible types in assignment (expression has type "TypeForm[str]", variable has type "Type[object]") -typ9: Type[object] = OBJECT_TF # E: Incompatible types in assignment (expression has type "TypeForm[object]", variable has type "Type[object]") -typ10: Type[object] = ANY_TF # E: Incompatible types in assignment (expression has type "TypeForm[Any]", variable has type "Type[object]") +typ1: Type[int] = INT_OR_STR_TF # E: Incompatible types in assignment (expression has type "TypeForm[Union[int, str]]", variable has type "type[int]") +typ2: Type[int] = INT_TF # E: Incompatible types in assignment (expression has type "TypeForm[int]", variable has type "type[int]") +typ3: Type[int] = STR_TF # E: Incompatible types in assignment (expression has type "TypeForm[str]", variable has type "type[int]") +typ4: Type[int] = OBJECT_TF # E: Incompatible types in assignment (expression has type "TypeForm[object]", variable has type "type[int]") +typ5: Type[int] = ANY_TF # E: Incompatible types in assignment (expression has type "TypeForm[Any]", variable has type "type[int]") +typ6: Type[object] = INT_OR_STR_TF # E: Incompatible types in assignment (expression has type "TypeForm[Union[int, str]]", variable has type "type[object]") +typ7: Type[object] = INT_TF # E: Incompatible types in assignment (expression has type "TypeForm[int]", variable has type "type[object]") +typ8: Type[object] = STR_TF # E: Incompatible types in assignment (expression has type "TypeForm[str]", variable has type "type[object]") +typ9: Type[object] = OBJECT_TF # E: Incompatible types in assignment (expression has type "TypeForm[object]", variable has type "type[object]") +typ10: Type[object] = ANY_TF # E: Incompatible types in assignment (expression has type "TypeForm[Any]", variable has type "type[object]") [builtins fixtures/tuple.pyi] [typing fixtures/typing-full.pyi] @@ -360,10 +360,10 @@ INT_T: Type[int] = int STR_T: Type[str] = str OBJECT_T: Type[object] = object ANY_T: Type = object -reveal_type(ANY_T) # N: Revealed type is "Type[Any]" +reveal_type(ANY_T) # N: Revealed type is "type[Any]" typ1: Type[int] = INT_T -typ2: Type[int] = STR_T # E: Incompatible types in assignment (expression has type "Type[str]", variable has type "Type[int]") -typ3: Type[int] = OBJECT_T # E: Incompatible types in assignment (expression has type "Type[object]", variable has type "Type[int]") +typ2: Type[int] = STR_T # E: Incompatible types in assignment (expression has type "type[str]", variable has type "type[int]") +typ3: Type[int] = OBJECT_T # E: Incompatible types in assignment (expression has type "type[object]", variable has type "type[int]") typ4: Type[int] = ANY_T # no error typ5: Type[object] = INT_T typ6: Type[object] = STR_T @@ -454,7 +454,7 @@ class B(AB): pass A_T: Type[A] = A B_T: Type[B] = B -reveal_type([A_T, B_T][0]) # N: Revealed type is "Type[__main__.AB]" +reveal_type([A_T, B_T][0]) # N: Revealed type is "type[__main__.AB]" [builtins fixtures/tuple.pyi] [typing fixtures/typing-full.pyi] @@ -495,7 +495,7 @@ class C(AB): T = TypeVar('T') def f(x: Callable[[T, T], None]) -> T: pass # type: ignore[empty-body] def g(x: Type[B], y: TypeForm[B | C]) -> None: pass -reveal_type(f(g)) # N: Revealed type is "Type[__main__.B]" +reveal_type(f(g)) # N: Revealed type is "type[__main__.B]" [builtins fixtures/tuple.pyi] [typing fixtures/typing-full.pyi] @@ -514,7 +514,7 @@ class C(AB): T = TypeVar('T') def f(x: Callable[[T, T], None]) -> T: pass # type: ignore[empty-body] def g(x: TypeForm[A | B], y: Type[B]) -> None: pass -reveal_type(f(g)) # N: Revealed type is "Type[__main__.B]" +reveal_type(f(g)) # N: Revealed type is "type[__main__.B]" [builtins fixtures/tuple.pyi] [typing fixtures/typing-full.pyi] @@ -533,7 +533,7 @@ class BC(TypedDict): T = TypeVar('T') def f(x: Callable[[T, T], None]) -> T: pass # type: ignore[empty-body] def g(x: Type[AB], y: Type[BC]) -> None: pass -reveal_type(f(g)) # N: Revealed type is "Type[TypedDict({'b': builtins.str, 'c': builtins.str, 'a': builtins.str})]" +reveal_type(f(g)) # N: Revealed type is "type[TypedDict({'b': builtins.str, 'c': builtins.str, 'a': builtins.str})]" [builtins fixtures/tuple.pyi] [typing fixtures/typing-full.pyi] @@ -550,8 +550,7 @@ reveal_type(tf2) # N: Revealed type is "TypeForm[Union[builtins.int, builtins.s tf3: TypeForm = TypeForm(int | str) reveal_type(tf3) # N: Revealed type is "TypeForm[Any]" tf4: TypeForm = TypeForm(1) # E: Invalid type: try using Literal[1] instead? -tf5: TypeForm = TypeForm(int) | TypeForm(str) # E: Invalid self argument "TypeForm[int]" to attribute function "__or__" with type "Callable[[type, object], object]" \ - # E: Incompatible types in assignment (expression has type "object", variable has type "TypeForm[Any]") +tf5: TypeForm = TypeForm(int) | TypeForm(str) # E: Incompatible types in assignment (expression has type "object", variable has type "TypeForm[Any]") tf6: TypeForm = TypeForm(TypeForm(int) | TypeForm(str)) # E: TypeForm argument is not a type [builtins fixtures/tuple.pyi] [typing fixtures/typing-full.pyi] @@ -564,7 +563,7 @@ tf6: TypeForm = TypeForm(TypeForm(int) | TypeForm(str)) # E: TypeForm argument from typing import TypeForm typx: TypeForm[str] = str if isinstance(typx, type): - reveal_type(typx) # N: Revealed type is "Type[builtins.str]" + reveal_type(typx) # N: Revealed type is "type[builtins.str]" else: reveal_type(typx) # N: Revealed type is "TypeForm[builtins.str]" [builtins fixtures/tuple.pyi] @@ -592,8 +591,8 @@ def as_type(typx: TypeForm[T]) -> Type[T] | None: return typx else: return None -reveal_type(as_type(int | str)) # N: Revealed type is "Union[Type[builtins.int], Type[builtins.str], None]" -reveal_type(as_type(int)) # N: Revealed type is "Union[Type[builtins.int], None]" +reveal_type(as_type(int | str)) # N: Revealed type is "Union[type[builtins.int], type[builtins.str], None]" +reveal_type(as_type(int)) # N: Revealed type is "Union[type[builtins.int], None]" [builtins fixtures/tuple.pyi] [typing fixtures/typing-full.pyi] From 06fc9e47321eb1a186e3a172dec05bbefb0efd27 Mon Sep 17 00:00:00 2001 From: David Foster Date: Sun, 27 Jul 2025 18:23:15 -0400 Subject: [PATCH 03/20] Apply feedback: Change MAYBE_UNRECOGNIZED_STR_TYPEFORM from unaccompanied note to standalone error --- docs/source/error_code_list.rst | 12 ++++++------ mypy/checkexpr.py | 2 +- mypy/errorcodes.py | 2 +- mypy/errors.py | 1 - test-data/unit/check-typeform.test | 12 ++++++------ 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/docs/source/error_code_list.rst b/docs/source/error_code_list.rst index 962091b3611e..abd91822f3de 100644 --- a/docs/source/error_code_list.rst +++ b/docs/source/error_code_list.rst @@ -1313,31 +1313,31 @@ by mypy in the following locations: If you try to use a string annotation in some other location which expects a TypeForm, the string value will always be treated as a ``str`` -even if a ``TypeForm`` would be more appropriate and this note code +even if a ``TypeForm`` would be more appropriate and this error code will be generated: .. code-block:: python - # Note: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. [maybe-unrecognized-str-typeform] + # Error: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. [maybe-unrecognized-str-typeform] # Error: List item 0 has incompatible type "str"; expected "TypeForm[Any]" [list-item] list_of_typx: list[TypeForm] = ['str | None', float] -Fix the note by surrounding the entire type with ``TypeForm(...)``: +Fix the error by surrounding the entire type with ``TypeForm(...)``: .. code-block:: python list_of_typx: list[TypeForm] = [TypeForm('str | None'), float] # OK Similarly, if you try to use a string literal in a location which expects a -TypeForm, this note code will be generated: +TypeForm, this error code will be generated: .. code-block:: python dict_of_typx = {'str_or_none': TypeForm(str | None)} - # Note: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. [maybe-unrecognized-str-typeform] + # Error: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. [maybe-unrecognized-str-typeform] list_of_typx: list[TypeForm] = [dict_of_typx['str_or_none']] -Fix the note by adding ``# type: ignore[maybe-unrecognized-str-typeform]`` +Fix the error by adding ``# type: ignore[maybe-unrecognized-str-typeform]`` to the line with the string literal: .. code-block:: python diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 824d57c093bb..03682d6d8da7 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -6358,7 +6358,7 @@ def try_parse_as_type_expression(self, maybe_type_expr: Expression) -> Type | No # don't try to parse it because there isn't enough information # available to the TypeChecker pass to resolve string annotations if has_str_expression(maybe_type_expr): - self.chk.note( + self.chk.fail( "TypeForm containing a string annotation cannot be recognized here. " "Surround with TypeForm(...) to recognize.", maybe_type_expr, diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index 125bdf5b2fe1..f149d40ebc98 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -273,7 +273,7 @@ def __hash__(self) -> int: METACLASS: Final[ErrorCode] = ErrorCode("metaclass", "Ensure that metaclass is valid", "General") MAYBE_UNRECOGNIZED_STR_TYPEFORM: Final[ErrorCode] = ErrorCode( "maybe-unrecognized-str-typeform", - "Warn when a string is used where a TypeForm is expected but a string annotation cannot be recognized", + "Error when a string is used where a TypeForm is expected but a string annotation cannot be recognized", "General", ) diff --git a/mypy/errors.py b/mypy/errors.py index 97f232681cb0..9f65c004f506 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -26,7 +26,6 @@ SHOW_NOTE_CODES: Final = { codes.ANNOTATION_UNCHECKED, codes.DEPRECATED, - codes.MAYBE_UNRECOGNIZED_STR_TYPEFORM, } # Do not add notes with links to error code docs to errors with these codes. diff --git a/test-data/unit/check-typeform.test b/test-data/unit/check-typeform.test index 72a19d91f12c..0a1eb0470e46 100644 --- a/test-data/unit/check-typeform.test +++ b/test-data/unit/check-typeform.test @@ -218,15 +218,15 @@ dict_with_typx_keys[int | str] += 1 [case testTypeExpressionWithStringAnnotationNotRecognizedInOtherSyntacticLocations] # flags: --python-version 3.14 --enable-incomplete-feature=TypeForm from typing import Dict, List, TypeForm -list_of_typx: List[TypeForm] = ['int | str'] # N: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. \ +list_of_typx: List[TypeForm] = ['int | str'] # E: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. \ # E: List item 0 has incompatible type "str"; expected "TypeForm[Any]" dict_with_typx_keys: Dict[TypeForm, int] = { - 'int | str': 1, # N: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. \ + 'int | str': 1, # E: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. \ # E: Dict entry 0 has incompatible type "str": "int"; expected "TypeForm[Any]": "int" - 'str | None': 2, # N: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. \ + 'str | None': 2, # E: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. \ # E: Dict entry 1 has incompatible type "str": "int"; expected "TypeForm[Any]": "int" } -dict_with_typx_keys['int | str'] += 1 # N: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. \ +dict_with_typx_keys['int | str'] += 1 # E: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. \ # E: Invalid index type "str" for "dict[TypeForm[Any], int]"; expected type "TypeForm[Any]" [builtins fixtures/dict.pyi] [typing fixtures/typing-full.pyi] @@ -235,10 +235,10 @@ dict_with_typx_keys['int | str'] += 1 # N: TypeForm containing a string annotat from typing import Any, Dict, List, TypeForm types: Dict[str, TypeForm] = {'any': Any} # Ensure warning can be ignored if does not apply. -list_of_typx1: List[TypeForm] = [types['any']] # N: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. +list_of_typx1: List[TypeForm] = [types['any']] # E: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. list_of_typx2: List[TypeForm] = [types['any']] # type: ignore[maybe-unrecognized-str-typeform] # Ensure warning can be fixed using the suggested fix in the warning message. -list_of_typx3: List[TypeForm] = ['Any'] # N: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. \ +list_of_typx3: List[TypeForm] = ['Any'] # E: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. \ # E: List item 0 has incompatible type "str"; expected "TypeForm[Any]" list_of_typx4: List[TypeForm] = [TypeForm('Any')] [builtins fixtures/dict.pyi] From c7a5e1f669aa8eaa8a4db9ea2957eada0859f254 Mon Sep 17 00:00:00 2001 From: David Foster Date: Sun, 27 Jul 2025 19:04:44 -0400 Subject: [PATCH 04/20] Apply feedback: Refactor extract save/restore of SemanticAnalyzer state to a new context manager --- mypy/semanal.py | 57 +++++++++++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 9452be955dbe..c1237baa9d83 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -7746,39 +7746,50 @@ def try_parse_as_type_expression(self, maybe_type_expr: Expression) -> None: else: assert_never(maybe_type_expr) - # Save SemanticAnalyzer state - original_errors = self.errors # altered by fail() - original_num_incomplete_refs = ( - self.num_incomplete_refs - ) # altered by record_incomplete_ref() - original_progress = self.progress # altered by defer() - original_deferred = self.deferred # altered by defer() - original_deferral_debug_context_len = len( - self.deferral_debug_context - ) # altered by defer() + with self.isolated_error_analysis(): + try: + # Ignore warnings that look like: + # :1: SyntaxWarning: invalid escape sequence '\(' + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=SyntaxWarning) + t = self.expr_to_analyzed_type(maybe_type_expr) + if self.errors.is_errors(): + t = None + except TypeTranslationError: + # Not a type expression + t = None + + maybe_type_expr.as_type = t + + @contextmanager + def isolated_error_analysis(self) -> Iterator[None]: + """ + Context manager for performing error analysis that should not + affect the main SemanticAnalyzer state. + + Upon entering this context, `self.errors` will start empty. + Within this context, you can analyze expressions for errors. + Upon exiting this context, the original `self.errors` will be restored, + and any errors collected during the analysis will be discarded. + """ + # Save state + original_errors = self.errors + original_num_incomplete_refs = self.num_incomplete_refs + original_progress = self.progress + original_deferred = self.deferred + original_deferral_debug_context_len = len(self.deferral_debug_context) self.errors = Errors(Options()) try: - # Ignore warnings that look like: - # :1: SyntaxWarning: invalid escape sequence '\(' - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=SyntaxWarning) - t = self.expr_to_analyzed_type(maybe_type_expr) - if self.errors.is_errors(): - t = None - except TypeTranslationError: - # Not a type expression - t = None + yield finally: - # Restore SemanticAnalyzer state + # Restore state self.errors = original_errors self.num_incomplete_refs = original_num_incomplete_refs self.progress = original_progress self.deferred = original_deferred del self.deferral_debug_context[original_deferral_debug_context_len:] - maybe_type_expr.as_type = t - def replace_implicit_first_type(sig: FunctionLike, new: Type) -> FunctionLike: if isinstance(sig, CallableType): From 20df00161465740d91c913566ce56f0e38bd7438 Mon Sep 17 00:00:00 2001 From: David Foster Date: Sun, 27 Jul 2025 20:30:49 -0400 Subject: [PATCH 05/20] Apply feedback: Suppress SyntaxWarnings when parsing strings as types ...at the most-targeted location Specific warning: * SyntaxWarning: invalid escape sequence '\(' --- mypy/fastparse.py | 19 ++++++++++++------- mypy/semanal.py | 6 +----- test-data/unit/check-fastparse.test | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index bb71242182f1..e07cf49a0e0e 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -137,13 +137,18 @@ def ast3_parse( source: str | bytes, filename: str, mode: str, feature_version: int = PY_MINOR_VERSION ) -> AST: - return ast3.parse( - source, - filename, - mode, - type_comments=True, # This works the magic - feature_version=feature_version, - ) + # Ignore warnings that look like: + # :1: SyntaxWarning: invalid escape sequence '\.' + # because `source` could be anything, including literals like r'(re\.match)' + with warnings.catch_warnings(): + warnings.simplefilter("ignore", SyntaxWarning) + return ast3.parse( + source, + filename, + mode, + type_comments=True, # This works the magic + feature_version=feature_version, + ) NamedExpr = ast3.NamedExpr diff --git a/mypy/semanal.py b/mypy/semanal.py index c1237baa9d83..cb487da0ca4f 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -7748,11 +7748,7 @@ def try_parse_as_type_expression(self, maybe_type_expr: Expression) -> None: with self.isolated_error_analysis(): try: - # Ignore warnings that look like: - # :1: SyntaxWarning: invalid escape sequence '\(' - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=SyntaxWarning) - t = self.expr_to_analyzed_type(maybe_type_expr) + t = self.expr_to_analyzed_type(maybe_type_expr) if self.errors.is_errors(): t = None except TypeTranslationError: diff --git a/test-data/unit/check-fastparse.test b/test-data/unit/check-fastparse.test index 80d314333ddc..742e4ef9d129 100644 --- a/test-data/unit/check-fastparse.test +++ b/test-data/unit/check-fastparse.test @@ -324,3 +324,17 @@ class Bla: def call() -> str: pass [builtins fixtures/module.pyi] + +[case testInvalidEscapeSequenceWarningsSuppressed] +# Test that SyntaxWarnings for invalid escape sequences are suppressed +# when parsing potential type expressions containing regex patterns or +# similar strings. Callable arguments are always potential type expressions. +from typing import TypeForm + +def identity(typx: TypeForm) -> TypeForm: + return typx + +# This should not generate SyntaxWarning despite invalid escape sequence +identity(r"re\.match") # E: Argument 1 to "identity" has incompatible type "str"; expected "TypeForm[Any]" +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] From 07009a98a6211859f9457e38bb300cc564d26664 Mon Sep 17 00:00:00 2001 From: David Foster Date: Mon, 28 Jul 2025 09:25:38 -0400 Subject: [PATCH 06/20] Apply feedback: Add TypeForm profiling counters to SemanticAnalyzer and the --dump-build-stats option --- mypy/semanal.py | 13 +++++++++++++ mypy/semanal_main.py | 11 +++++++++++ 2 files changed, 24 insertions(+) diff --git a/mypy/semanal.py b/mypy/semanal.py index cb487da0ca4f..c7cc1612fbdc 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -510,6 +510,11 @@ def __init__( # Used to track edge case when return is still inside except* if it enters a loop self.return_stmt_inside_except_star_block: bool = False + # TypeForm profiling counters + self.type_expression_parse_count: int = 0 # Total try_parse_as_type_expression calls + self.type_expression_full_parse_success_count: int = 0 # Successful full parses + self.type_expression_full_parse_failure_count: int = 0 # Failed full parses + # mypyc doesn't properly handle implementing an abstractproperty # with a regular attribute so we make them properties @property @@ -7694,6 +7699,8 @@ def try_parse_as_type_expression(self, maybe_type_expr: Expression) -> None: type context) than this function is called (i.e. eagerly for EVERY expression in certain syntactic positions). """ + # Count every call to this method for profiling + self.type_expression_parse_count += 1 # Bail ASAP if the Expression matches a common pattern that cannot possibly # be a valid type expression, because this function is called very frequently @@ -7755,6 +7762,12 @@ def try_parse_as_type_expression(self, maybe_type_expr: Expression) -> None: # Not a type expression t = None + # Count full parse attempts for profiling + if t is not None: + self.type_expression_full_parse_success_count += 1 + else: + self.type_expression_full_parse_failure_count += 1 + maybe_type_expr.as_type = t @contextmanager diff --git a/mypy/semanal_main.py b/mypy/semanal_main.py index 7301e9f9b9b3..b2c43e6becb8 100644 --- a/mypy/semanal_main.py +++ b/mypy/semanal_main.py @@ -107,6 +107,17 @@ def semantic_analysis_for_scc(graph: Graph, scc: list[str], errors: Errors) -> N if "builtins" in scc: cleanup_builtin_scc(graph["builtins"]) + # Report TypeForm profiling stats + if len(scc) >= 1: + # Get manager from any state in the SCC (they all share the same manager) + manager = graph[scc[0]].manager + analyzer = manager.semantic_analyzer + manager.add_stats( + type_expression_parse_count=analyzer.type_expression_parse_count, + type_expression_full_parse_success_count=analyzer.type_expression_full_parse_success_count, + type_expression_full_parse_failure_count=analyzer.type_expression_full_parse_failure_count, + ) + def cleanup_builtin_scc(state: State) -> None: """Remove imported names from builtins namespace. From 0104ce5086f51e2c6be5d6c7ab7559a730ce90b2 Mon Sep 17 00:00:00 2001 From: David Foster Date: Tue, 29 Jul 2025 07:47:43 -0400 Subject: [PATCH 07/20] Increase efficiency of quick rejection heuristic from 85.8% -> 99.6% ...in SemanticAnalyzer.try_parse_as_type_expression() --- misc/analyze_typeform_stats.py | 87 ++++++++++++++++++++++++++++++++++ mypy/semanal.py | 63 +++++++++++++++++++++++- 2 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 misc/analyze_typeform_stats.py diff --git a/misc/analyze_typeform_stats.py b/misc/analyze_typeform_stats.py new file mode 100644 index 000000000000..05ddf6bab93d --- /dev/null +++ b/misc/analyze_typeform_stats.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +""" +Analyze TypeForm parsing efficiency from mypy build stats. + +Usage: + python3 analyze_typeform_stats.py '' + python3 -m mypy --dump-build-stats file.py 2>&1 | python3 analyze_typeform_stats.py + +Example output: + TypeForm Expression Parsing Statistics: + ================================================== + Total calls to SA.try_parse_as_type_expression: 14,555 + Quick rejections (no full parse): 14,255 + Full parses attempted: 300 + - Successful: 248 + - Failed: 52 + + Efficiency Metrics: + - Quick rejection rate: 97.9% + - Full parse rate: 2.1% + - Full parse success rate: 82.7% + - Overall success rate: 1.7% + + Performance Implications: + - Expensive failed full parses: 52 (0.4% of all calls) + +See also: + - mypy/semanal.py: SemanticAnalyzer.try_parse_as_type_expression() + - mypy/semanal.py: DEBUG_TYPE_EXPRESSION_FULL_PARSE_FAILURES +""" + +import re +import sys + + +def analyze_stats(output: str) -> None: + """Parse mypy stats output and calculate TypeForm parsing efficiency.""" + + # Extract the three counters + total_match = re.search(r'type_expression_parse_count:\s*(\d+)', output) + success_match = re.search(r'type_expression_full_parse_success_count:\s*(\d+)', output) + failure_match = re.search(r'type_expression_full_parse_failure_count:\s*(\d+)', output) + + if not (total_match and success_match and failure_match): + print("Error: Could not find all required counters in output") + return + + total = int(total_match.group(1)) + successes = int(success_match.group(1)) + failures = int(failure_match.group(1)) + + full_parses = successes + failures + + print(f"TypeForm Expression Parsing Statistics:") + print(f"="*50) + print(f"Total calls to SA.try_parse_as_type_expression: {total:,}") + print(f"Quick rejections (no full parse): {total - full_parses:,}") + print(f"Full parses attempted: {full_parses:,}") + print(f" - Successful: {successes:,}") + print(f" - Failed: {failures:,}") + if total > 0: + print() + print(f"Efficiency Metrics:") + print(f" - Quick rejection rate: {((total - full_parses) / total * 100):.1f}%") + print(f" - Full parse rate: {(full_parses / total * 100):.1f}%") + print(f" - Full parse success rate: {(successes / full_parses * 100):.1f}%") + print(f" - Overall success rate: {(successes / total * 100):.1f}%") + print() + print(f"Performance Implications:") + print(f" - Expensive failed full parses: {failures:,} ({(failures / total * 100):.1f}% of all calls)") + + +if __name__ == "__main__": + if len(sys.argv) == 1: + # Read from stdin + output = sys.stdin.read() + elif len(sys.argv) == 2: + # Read from command line argument + output = sys.argv[1] + else: + print("Usage: python3 analyze_typeform_stats.py [mypy_output_with_stats]") + print("Examples:") + print(" python3 -m mypy --dump-build-stats file.py 2>&1 | python3 analyze_typeform_stats.py") + print(" python3 analyze_typeform_stats.py 'output_string'") + sys.exit(1) + + analyze_stats(output) diff --git a/mypy/semanal.py b/mypy/semanal.py index c7cc1612fbdc..723ceadab32a 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -313,6 +313,13 @@ T = TypeVar("T") +# Whether to print diagnostic information for failed full parses +# in SemanticAnalyzer.try_parse_as_type_expression(). +# +# See also: misc/analyze_typeform_stats.py +DEBUG_TYPE_EXPRESSION_FULL_PARSE_FAILURES: Final = False + + FUTURE_IMPORTS: Final = { "__future__.nested_scopes": "nested_scopes", "__future__.generators": "generators", @@ -352,6 +359,13 @@ # string literal as a type expression. _MULTIPLE_WORDS_NONTYPE_RE = re.compile(r'\s*[^\s.\'"|\[]+\s+[^\s.\'"|\[]') +# Matches any valid Python identifier, including identifiers with Unicode characters. +# +# [^\d\W] = word character that is not a digit +# \w = word character +# \Z = match end of string; does not allow a trailing \n, unlike $ +_IDENTIFIER_RE = re.compile(r'^[^\d\W]\w*\Z', re.UNICODE) + class SemanticAnalyzer( NodeVisitor[None], SemanticAnalyzerInterface, SemanticAnalyzerPluginInterface @@ -7712,13 +7726,51 @@ def try_parse_as_type_expression(self, maybe_type_expr: Expression) -> None: # and only lazily in contexts where a TypeForm is expected return elif isinstance(maybe_type_expr, StrExpr): + str_value = maybe_type_expr.value # cache # Filter out string literals with common patterns that could not # possibly be in a type expression - if _MULTIPLE_WORDS_NONTYPE_RE.match(maybe_type_expr.value): + if _MULTIPLE_WORDS_NONTYPE_RE.match(str_value): # A common pattern in string literals containing a sentence. # But cannot be a type expression. maybe_type_expr.as_type = None return + # Filter out string literals which look like an identifier but + # cannot be a type expression, for a few common reasons + if _IDENTIFIER_RE.fullmatch(str_value): + sym = self.lookup(str_value, UnboundType(str_value), suppress_errors=True) + if sym is None: + # Does not refer to anything in the local symbol table + maybe_type_expr.as_type = None + return + else: # sym is not None + node = sym.node # cache + if isinstance(node, PlaceholderNode) and not node.becomes_typeinfo: + # Either: + # 1. f'Cannot resolve name "{t.name}" (possible cyclic definition)' + # 2. Reference to an unknown placeholder node. + maybe_type_expr.as_type = None + return + unbound_tvar_or_paramspec = ( + isinstance(node, (TypeVarExpr, TypeVarTupleExpr, ParamSpecExpr)) + and self.tvar_scope.get_binding(sym) is None + ) + if unbound_tvar_or_paramspec: + # Either: + # 1. unbound_tvar: 'Type variable "{}" is unbound' [codes.VALID_TYPE] + # 2. unbound_paramspec: f'ParamSpec "{name}" is unbound' [codes.VALID_TYPE] + maybe_type_expr.as_type = None + return + else: # does not look like an identifier + if '"' in str_value or "'" in str_value: + # Only valid inside a Literal[...] type + if '[' not in str_value: + # Cannot be a Literal[...] type + maybe_type_expr.as_type = None + return + elif str_value == '': + # Empty string is not a valid type + maybe_type_expr.as_type = None + return elif isinstance(maybe_type_expr, IndexExpr): if isinstance(maybe_type_expr.base, NameExpr): if isinstance(maybe_type_expr.base.node, Var): @@ -7762,6 +7814,15 @@ def try_parse_as_type_expression(self, maybe_type_expr: Expression) -> None: # Not a type expression t = None + if DEBUG_TYPE_EXPRESSION_FULL_PARSE_FAILURES and t is None: + original_flushed_files = set(self.errors.flushed_files) # save + try: + errors = self.errors.new_messages() # capture + finally: + self.errors.flushed_files = original_flushed_files # restore + + print(f'SA.try_parse_as_type_expression: Full parse failure: {maybe_type_expr}, errors={errors!r}') + # Count full parse attempts for profiling if t is not None: self.type_expression_full_parse_success_count += 1 From a958b45513d01fc74805902cec50812d3a2df94f Mon Sep 17 00:00:00 2001 From: David Foster Date: Wed, 30 Jul 2025 08:16:20 -0400 Subject: [PATCH 08/20] Apply feedback: Recognize assignment to union of TypeForm with non-TypeForm --- mypy/checkexpr.py | 21 ++++++++++++++- test-data/unit/check-typeform.test | 41 ++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 03682d6d8da7..59c507f2ceaa 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -6009,9 +6009,28 @@ def accept( line=node_as_type.line, column=node_as_type.column, is_type_form=True, + ) # r-value type, when interpreted as a type expression + elif ( + isinstance(p_type_context, UnionType) + and any([ + isinstance(item, TypeType) and item.is_type_form + for item in p_type_context.items + ]) + and (node_as_type := self.try_parse_as_type_expression(node)) is not None + ): + typ1 = TypeType.make_normalized( + node_as_type, + line=node_as_type.line, + column=node_as_type.column, + is_type_form=True, ) + if is_subtype(typ1, p_type_context): + typ = typ1 # r-value type, when interpreted as a type expression + else: + typ2 = node.accept(self) + typ = typ2 # r-value type, when interpreted as a value expression else: - typ = node.accept(self) + typ = node.accept(self) # r-value type, when interpreted as a value expression except Exception as err: report_internal_error( err, self.chk.errors.file, node.line, self.chk.errors, self.chk.options diff --git a/test-data/unit/check-typeform.test b/test-data/unit/check-typeform.test index 0a1eb0470e46..a8cce55b6589 100644 --- a/test-data/unit/check-typeform.test +++ b/test-data/unit/check-typeform.test @@ -278,6 +278,47 @@ typx2: TypeForm = no_such_module.NoSuchType # E: Name "no_such_module" is not d [typing fixtures/typing-full.pyi] +-- Type Expression Context: Union[TypeForm, ] + +[case testAcceptsTypeFormLiteralAssignedToUnionOfTypeFormAndNonStr] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import TypeForm +typx_or_int1: TypeForm[int | None] | int = int | None # No error; interpret as TypeForm +typx_or_int2: TypeForm[int | None] | int = str | None # E: Incompatible types in assignment (expression has type "object", variable has type "Union[TypeForm[Optional[int]], int]") +typx_or_int3: TypeForm[int | None] | int = 1 +typx_or_int4: TypeForm[int | None] | int = object() # E: Incompatible types in assignment (expression has type "object", variable has type "Union[TypeForm[Optional[int]], int]") +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testAcceptsTypeFormLiteralAssignedToUnionOfTypeFormAndStr] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import TypeForm +typx_or_str1: TypeForm[int | None] | str = 'int | None' +typx_or_str2: TypeForm[int | None] | str = 'str | None' # No error; interpret as str +typx_or_str3: TypeForm[int | None] | str = 'hello' +typx_or_str4: TypeForm[int | None] | str = object() # E: Incompatible types in assignment (expression has type "object", variable has type "Union[TypeForm[Optional[int]], str]") +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testValueExpressionWithStringInTypeFormUnionContextEmitsConservativeWarning1] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import List, TypeForm +list_of_typx1: List[TypeForm[int | None] | str] = ['int | None'] # E: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. +list_of_typx2: List[TypeForm[int | None] | str] = ['str | None'] # E: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testValueExpressionWithStringInTypeFormUnionContextEmitsConservativeWarning2] +# flags: --python-version 3.14 --enable-incomplete-feature=TypeForm +from typing import List, TypeForm +list_of_typx3: List[TypeForm[int | None] | int] = ['int | None'] # E: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. \ + # E: List item 0 has incompatible type "str"; expected "Union[TypeForm[Optional[int]], int]" +list_of_typx4: List[TypeForm[str | None] | int] = ['str | None'] # E: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. \ + # E: List item 0 has incompatible type "str"; expected "Union[TypeForm[Optional[str]], int]" +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + + -- Assignability (is_subtype) [case testTypeFormToTypeFormAssignability] From e4e55303dc81e88394b8e8fdf15b438ef297da06 Mon Sep 17 00:00:00 2001 From: David Foster Date: Wed, 30 Jul 2025 08:53:09 -0400 Subject: [PATCH 09/20] Add comment explaining safety of empty tvar scope in TypeAnalyser used by TA.try_parse_as_type_expression() --- mypy/checkexpr.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 59c507f2ceaa..75c27b95a0ff 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -6398,6 +6398,11 @@ def try_parse_as_type_expression(self, maybe_type_expr: Expression) -> Type | No chk_sem = mypy.checker.TypeCheckerAsSemanticAnalyzer(self.chk, sym_for_name) tpan = TypeAnalyser( chk_sem, + # NOTE: Will never need to lookup type vars in this scope because + # SemanticAnalyzer.try_parse_as_type_expression() will have + # already recognized any type var referenced in a NameExpr. + # String annotations (which may also reference type vars) + # can't be resolved in the TypeChecker pass anyway. TypeVarLikeScope(), # empty scope self.plugin, self.chk.options, From 8e130ba9e46750b68a3e313104d58465ee005c89 Mon Sep 17 00:00:00 2001 From: David Foster Date: Tue, 5 Aug 2025 09:44:21 -0400 Subject: [PATCH 10/20] Allow TypeAlias and PlaceholderNode to be stringified/printed --- mypy/strconv.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mypy/strconv.py b/mypy/strconv.py index 128de0561856..ac925a8a374b 100644 --- a/mypy/strconv.py +++ b/mypy/strconv.py @@ -213,6 +213,12 @@ def visit_nonlocal_decl(self, o: mypy.nodes.NonlocalDecl) -> str: def visit_decorator(self, o: mypy.nodes.Decorator) -> str: return self.dump([o.var, o.decorators, o.func], o) + def visit_type_alias(self, o: mypy.nodes.TypeAlias, /) -> T: + return self.dump([o.name, o.target, o.alias_tvars, o.no_args], o) + + def visit_placeholder_node(self, o: mypy.nodes.PlaceholderNode, /) -> T: + return self.dump([o.fullname], o) + # Statements def visit_block(self, o: mypy.nodes.Block) -> str: From 502e1a5a9d262125385262cc132d398210b19f33 Mon Sep 17 00:00:00 2001 From: David Foster Date: Tue, 5 Aug 2025 10:04:52 -0400 Subject: [PATCH 11/20] Apply feedback: Alter primitives.pyi fixture rather than tuple.pyi and dict.pyi --- test-data/unit/check-typeform.test | 104 ++++++++++++------------- test-data/unit/fixtures/dict.pyi | 4 +- test-data/unit/fixtures/primitives.pyi | 4 + test-data/unit/fixtures/tuple.pyi | 2 - 4 files changed, 57 insertions(+), 57 deletions(-) diff --git a/test-data/unit/check-typeform.test b/test-data/unit/check-typeform.test index a8cce55b6589..73f64aab21f6 100644 --- a/test-data/unit/check-typeform.test +++ b/test-data/unit/check-typeform.test @@ -5,7 +5,7 @@ from typing import TypeForm typx: TypeForm = str reveal_type(typx) # N: Revealed type is "TypeForm[Any]" -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testRecognizesParameterizedTypeFormInAnnotation] @@ -13,7 +13,7 @@ reveal_type(typx) # N: Revealed type is "TypeForm[Any]" from typing import TypeForm typx: TypeForm[str] = str reveal_type(typx) # N: Revealed type is "TypeForm[builtins.str]" -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] @@ -23,21 +23,21 @@ reveal_type(typx) # N: Revealed type is "TypeForm[builtins.str]" # flags: --python-version 3.14 --enable-incomplete-feature=TypeForm from typing import TypeForm typx: TypeForm[str] = str -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testCanAssignTypeExpressionToUnionTypeFormVariable] # flags: --python-version 3.14 --enable-incomplete-feature=TypeForm from typing import TypeForm typx: TypeForm[str | None] = str | None -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testCannotAssignTypeExpressionToTypeFormVariableWithIncompatibleItemType] # flags: --python-version 3.14 --enable-incomplete-feature=TypeForm from typing import TypeForm typx: TypeForm[str] = int # E: Incompatible types in assignment (expression has type "TypeForm[int]", variable has type "TypeForm[str]") -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testCanAssignValueExpressionToTypeFormVariableIfValueIsATypeForm1] @@ -45,7 +45,7 @@ typx: TypeForm[str] = int # E: Incompatible types in assignment (expression has from typing import TypeForm typx1: TypeForm = str typx2: TypeForm = typx1 # looks like a type expression: name -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testCanAssignValueExpressionToTypeFormVariableIfValueIsATypeForm2] @@ -55,7 +55,7 @@ def identity_tf(x: TypeForm) -> TypeForm: return x typx1: TypeForm = str typx2: TypeForm = identity_tf(typx1) # does not look like a type expression -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testCannotAssignValueExpressionToTypeFormVariableIfValueIsNotATypeForm] @@ -63,7 +63,7 @@ typx2: TypeForm = identity_tf(typx1) # does not look like a type expression from typing import TypeForm val: int = 42 typx: TypeForm = val # E: Incompatible types in assignment (expression has type "int", variable has type "TypeForm[Any]") -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testCanAssignNoneTypeExpressionToTypeFormVariable] @@ -71,7 +71,7 @@ typx: TypeForm = val # E: Incompatible types in assignment (expression has type from typing import TypeForm typx: TypeForm = None reveal_type(typx) # N: Revealed type is "TypeForm[Any]" -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testCanAssignTypeExpressionToTypeFormVariableDeclaredEarlier] @@ -81,14 +81,14 @@ typ: Type typ = int | None # E: Incompatible types in assignment (expression has type "object", variable has type "type[Any]") typx: TypeForm typx = int | None -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testCanAssignTypeExpressionWithStringAnnotationToTypeFormVariable] # flags: --python-version 3.14 --enable-incomplete-feature=TypeForm from typing import TypeForm typx: TypeForm[str | None] = 'str | None' -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] @@ -100,7 +100,7 @@ from typing import TypeForm def is_type(typx: TypeForm) -> bool: return isinstance(typx, type) is_type(int | None) -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testCannotPassTypeExpressionToTypeParameter] @@ -108,7 +108,7 @@ is_type(int | None) def is_type(typ: type) -> bool: return isinstance(typ, type) is_type(int | None) # E: Argument 1 to "is_type" has incompatible type "object"; expected "type" -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testCanPassTypeExpressionToTypeFormParameterInMethod] @@ -118,7 +118,7 @@ class C: def is_type(self, typx: TypeForm) -> bool: return isinstance(typx, type) C().is_type(int | None) -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testCanPassTypeExpressionToTypeFormParameterInOverload] @@ -131,7 +131,7 @@ def is_type(typx: type) -> bool: ... def is_type(typx): return isinstance(typx, type) is_type(int | None) -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testCanPassTypeExpressionToTypeFormParameterInDecorator] @@ -147,7 +147,7 @@ def expects_type(typx: TypeForm) -> Callable[[Callable[[P], R]], Callable[[P], R @expects_type(int | None) def sum_ints(x: int | None) -> int: return (x or 0) -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testCanPassTypeExpressionToTypeFormVarargsParameter] @@ -163,7 +163,7 @@ def expects_types(*typxs: TypeForm) -> Callable[[Callable[P, R]], Callable[P, R] @expects_types(int | None, int) def sum_ints(x: int | None, y: int) -> tuple[int, int]: return ((x or 0), y) -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testCanPassTypeExpressionWithStringAnnotationToTypeFormParameter] @@ -172,7 +172,7 @@ from typing import TypeForm def is_type(typx: TypeForm) -> bool: return isinstance(typx, type) is_type('int | None') -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] @@ -184,7 +184,7 @@ from typing import TypeForm def maybe_int_type() -> TypeForm: return int | None reveal_type(maybe_int_type()) # N: Revealed type is "TypeForm[Any]" -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testCanReturnTypeExpressionWithStringAnnotationInFunctionWithTypeFormReturnType] @@ -193,7 +193,7 @@ from typing import TypeForm def maybe_int_type() -> TypeForm: return 'int | None' reveal_type(maybe_int_type()) # N: Revealed type is "TypeForm[Any]" -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] @@ -211,8 +211,8 @@ dict_with_typx_keys: Dict[TypeForm, int] = { int | str: 1, str | None: 2, } -dict_with_typx_keys[int | str] += 1 -[builtins fixtures/dict.pyi] +dict_with_typx_keys[int | str] + 1 +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testTypeExpressionWithStringAnnotationNotRecognizedInOtherSyntacticLocations] @@ -255,7 +255,7 @@ class C: list_of_typx2: List[TypeForm] = [Self] # E: List item 0 has incompatible type "int"; expected "TypeForm[Any]" typx3: TypeForm = Self # E: Incompatible types in assignment (expression has type "int", variable has type "TypeForm[Any]") typx4: TypeForm = 'Self' # E: Incompatible types in assignment (expression has type "str", variable has type "TypeForm[Any]") -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testNameOrDottedNameRecognizedInOtherSyntacticLocations] @@ -264,7 +264,7 @@ import typing from typing import List, TypeForm list_of_typx: List[TypeForm] = [List | typing.Optional[str]] typx: TypeForm = List | typing.Optional[str] -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testInvalidNameOrDottedNameRecognizedInOtherSyntacticLocations] @@ -274,7 +274,7 @@ list_of_typx1: List[TypeForm] = [NoSuchType] # E: Name "NoSuchType" is not defi list_of_typx2: List[TypeForm] = [no_such_module.NoSuchType] # E: Name "no_such_module" is not defined typx1: TypeForm = NoSuchType # E: Name "NoSuchType" is not defined typx2: TypeForm = no_such_module.NoSuchType # E: Name "no_such_module" is not defined -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] @@ -287,7 +287,7 @@ typx_or_int1: TypeForm[int | None] | int = int | None # No error; interpret as typx_or_int2: TypeForm[int | None] | int = str | None # E: Incompatible types in assignment (expression has type "object", variable has type "Union[TypeForm[Optional[int]], int]") typx_or_int3: TypeForm[int | None] | int = 1 typx_or_int4: TypeForm[int | None] | int = object() # E: Incompatible types in assignment (expression has type "object", variable has type "Union[TypeForm[Optional[int]], int]") -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testAcceptsTypeFormLiteralAssignedToUnionOfTypeFormAndStr] @@ -297,7 +297,7 @@ typx_or_str1: TypeForm[int | None] | str = 'int | None' typx_or_str2: TypeForm[int | None] | str = 'str | None' # No error; interpret as str typx_or_str3: TypeForm[int | None] | str = 'hello' typx_or_str4: TypeForm[int | None] | str = object() # E: Incompatible types in assignment (expression has type "object", variable has type "Union[TypeForm[Optional[int]], str]") -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testValueExpressionWithStringInTypeFormUnionContextEmitsConservativeWarning1] @@ -305,7 +305,7 @@ typx_or_str4: TypeForm[int | None] | str = object() # E: Incompatible types in from typing import List, TypeForm list_of_typx1: List[TypeForm[int | None] | str] = ['int | None'] # E: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. list_of_typx2: List[TypeForm[int | None] | str] = ['str | None'] # E: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testValueExpressionWithStringInTypeFormUnionContextEmitsConservativeWarning2] @@ -315,7 +315,7 @@ list_of_typx3: List[TypeForm[int | None] | int] = ['int | None'] # E: TypeForm # E: List item 0 has incompatible type "str"; expected "Union[TypeForm[Optional[int]], int]" list_of_typx4: List[TypeForm[str | None] | int] = ['str | None'] # E: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. \ # E: List item 0 has incompatible type "str"; expected "Union[TypeForm[Optional[str]], int]" -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] @@ -342,7 +342,7 @@ typx7: TypeForm[int] = INT_TF typx8: TypeForm[int] = STR_TF # E: Incompatible types in assignment (expression has type "TypeForm[str]", variable has type "TypeForm[int]") typx9: TypeForm[int] = OBJECT_TF # E: Incompatible types in assignment (expression has type "TypeForm[object]", variable has type "TypeForm[int]") typx10: TypeForm[int] = ANY_TF # no error -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testTypeToTypeFormAssignability] @@ -363,7 +363,7 @@ typx5: TypeForm[int] = INT_T typx6: TypeForm[int] = STR_T # E: Incompatible types in assignment (expression has type "type[str]", variable has type "TypeForm[int]") typx7: TypeForm[int] = OBJECT_T # E: Incompatible types in assignment (expression has type "type[object]", variable has type "TypeForm[int]") typx8: TypeForm[int] = ANY_T # no error -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testTypeFormToTypeAssignability] @@ -387,7 +387,7 @@ typ7: Type[object] = INT_TF # E: Incompatible types in assignment (expression h typ8: Type[object] = STR_TF # E: Incompatible types in assignment (expression has type "TypeForm[str]", variable has type "type[object]") typ9: Type[object] = OBJECT_TF # E: Incompatible types in assignment (expression has type "TypeForm[object]", variable has type "type[object]") typ10: Type[object] = ANY_TF # E: Incompatible types in assignment (expression has type "TypeForm[Any]", variable has type "type[object]") -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] # NOTE: This test doesn't involve TypeForm at all, but is still illustrative @@ -410,7 +410,7 @@ typ5: Type[object] = INT_T typ6: Type[object] = STR_T typ7: Type[object] = OBJECT_T typ8: Type[object] = ANY_T # no error -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testTypeFormToObjectAssignability] @@ -427,7 +427,7 @@ obj3: object = ANY_TF any1: Any = INT_TF any2: Any = OBJECT_TF any3: Any = ANY_TF -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] @@ -446,7 +446,7 @@ class B(AB): A_TF: TypeForm[A] = A B_TF: TypeForm[B] = B reveal_type([A_TF, B_TF][0]) # N: Revealed type is "TypeForm[__main__.AB]" -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testTypeToTypeFormJoin] @@ -462,7 +462,7 @@ class B(AB): A_T: Type[A] = A B_TF: TypeForm[B] = B reveal_type([A_T, B_TF][0]) # N: Revealed type is "TypeForm[__main__.AB]" -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testTypeFormToTypeJoin] @@ -478,7 +478,7 @@ class B(AB): A_TF: TypeForm[A] = A B_T: Type[B] = B reveal_type([A_TF, B_T][0]) # N: Revealed type is "TypeForm[__main__.AB]" -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] # NOTE: This test doesn't involve TypeForm at all, but is still illustrative @@ -496,7 +496,7 @@ class B(AB): A_T: Type[A] = A B_T: Type[B] = B reveal_type([A_T, B_T][0]) # N: Revealed type is "type[__main__.AB]" -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] @@ -518,7 +518,7 @@ T = TypeVar('T') def f(x: Callable[[T, T], None]) -> T: pass # type: ignore[empty-body] def g(x: TypeForm[A | B], y: TypeForm[B | C]) -> None: pass reveal_type(f(g)) # N: Revealed type is "TypeForm[__main__.B]" -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testTypeToTypeFormMeet] @@ -537,7 +537,7 @@ T = TypeVar('T') def f(x: Callable[[T, T], None]) -> T: pass # type: ignore[empty-body] def g(x: Type[B], y: TypeForm[B | C]) -> None: pass reveal_type(f(g)) # N: Revealed type is "type[__main__.B]" -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testTypeFormToTypeMeet] @@ -556,7 +556,7 @@ T = TypeVar('T') def f(x: Callable[[T, T], None]) -> T: pass # type: ignore[empty-body] def g(x: TypeForm[A | B], y: Type[B]) -> None: pass reveal_type(f(g)) # N: Revealed type is "type[__main__.B]" -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] # NOTE: This test doesn't involve TypeForm at all, but is still illustrative @@ -575,7 +575,7 @@ T = TypeVar('T') def f(x: Callable[[T, T], None]) -> T: pass # type: ignore[empty-body] def g(x: Type[AB], y: Type[BC]) -> None: pass reveal_type(f(g)) # N: Revealed type is "type[TypedDict({'b': builtins.str, 'c': builtins.str, 'a': builtins.str})]" -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] @@ -593,7 +593,7 @@ reveal_type(tf3) # N: Revealed type is "TypeForm[Any]" tf4: TypeForm = TypeForm(1) # E: Invalid type: try using Literal[1] instead? tf5: TypeForm = TypeForm(int) | TypeForm(str) # E: Incompatible types in assignment (expression has type "object", variable has type "TypeForm[Any]") tf6: TypeForm = TypeForm(TypeForm(int) | TypeForm(str)) # E: TypeForm argument is not a type -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] @@ -607,7 +607,7 @@ if isinstance(typx, type): reveal_type(typx) # N: Revealed type is "type[builtins.str]" else: reveal_type(typx) # N: Revealed type is "TypeForm[builtins.str]" -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] @@ -620,7 +620,7 @@ T = TypeVar('T') def as_typeform(typx: TypeForm[T]) -> TypeForm[T]: return typx reveal_type(as_typeform(int | str)) # N: Revealed type is "TypeForm[Union[builtins.int, builtins.str]]" -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testLinkTypeFormToTypeWithTypeVariable] @@ -634,7 +634,7 @@ def as_type(typx: TypeForm[T]) -> Type[T] | None: return None reveal_type(as_type(int | str)) # N: Revealed type is "Union[type[builtins.int], type[builtins.str], None]" reveal_type(as_type(int)) # N: Revealed type is "Union[type[builtins.int], None]" -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testLinkTypeFormToInstanceWithTypeVariable] @@ -648,7 +648,7 @@ def as_instance(typx: TypeForm[T]) -> T | None: return None reveal_type(as_instance(int | str)) # N: Revealed type is "Union[builtins.int, builtins.str, None]" reveal_type(as_instance(int)) # N: Revealed type is "Union[builtins.int, None]" -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testLinkTypeFormToTypeIsWithTypeVariable] @@ -663,7 +663,7 @@ if isassignable(count, int): reveal_type(count) # N: Revealed type is "builtins.int" else: reveal_type(count) # N: Revealed type is "builtins.str" -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testLinkTypeFormToTypeGuardWithTypeVariable] @@ -678,7 +678,7 @@ if isassignable(count, int): reveal_type(count) # N: Revealed type is "builtins.int" else: reveal_type(count) # N: Revealed type is "Union[builtins.int, builtins.str]" -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] @@ -717,7 +717,7 @@ class C1: pass typx1: TypeForm[C1.C2] = C1.C2 # OK typx2: TypeForm[typing.Any] = typing.Any # OK -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] -- mypy already refused to recognize TypeVars in value expressions before @@ -731,12 +731,12 @@ class Box(Generic[E]): list_of_typx: List[TypeForm] = [E] # E: "E" is a type variable and only valid in type context typx1: TypeForm = E # E: "E" is a type variable and only valid in type context typx2: TypeForm = 'E' -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] [case testIncompleteTypeFormsAreNotRecognized] # flags: --python-version 3.14 --enable-incomplete-feature=TypeForm from typing import Optional, TypeForm typx: TypeForm = Optional # E: Incompatible types in assignment (expression has type "int", variable has type "TypeForm[Any]") -[builtins fixtures/tuple.pyi] +[builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] diff --git a/test-data/unit/fixtures/dict.pyi b/test-data/unit/fixtures/dict.pyi index b87255f5486a..ed2287511161 100644 --- a/test-data/unit/fixtures/dict.pyi +++ b/test-data/unit/fixtures/dict.pyi @@ -19,9 +19,7 @@ class object: def __init__(self) -> None: pass def __eq__(self, other: object) -> bool: pass -class type: - # Real implementation returns UnionType - def __or__(self, value: object, /) -> object: pass +class type: pass class dict(Mapping[KT, VT]): @overload diff --git a/test-data/unit/fixtures/primitives.pyi b/test-data/unit/fixtures/primitives.pyi index 2f8623c79b9f..98e604e9e81e 100644 --- a/test-data/unit/fixtures/primitives.pyi +++ b/test-data/unit/fixtures/primitives.pyi @@ -13,6 +13,8 @@ class object: class type: def __init__(self, x: object) -> None: pass + # Real implementation returns UnionType + def __or__(self, value: object, /) -> object: pass class int: # Note: this is a simplification of the actual signature @@ -72,3 +74,5 @@ class range(Sequence[int]): def __contains__(self, other: object) -> bool: pass def isinstance(x: object, t: Union[type, Tuple]) -> bool: pass + +class BaseException: pass diff --git a/test-data/unit/fixtures/tuple.pyi b/test-data/unit/fixtures/tuple.pyi index f5e21aa228c0..d01cd0034d26 100644 --- a/test-data/unit/fixtures/tuple.pyi +++ b/test-data/unit/fixtures/tuple.pyi @@ -13,8 +13,6 @@ class object: class type: def __init__(self, *a: object) -> None: pass def __call__(self, *a: object) -> object: pass - # Real implementation returns UnionType - def __or__(self, value: object, /) -> object: pass class tuple(Sequence[_Tco], Generic[_Tco]): def __new__(cls: Type[_T], iterable: Iterable[_Tco] = ...) -> _T: ... def __iter__(self) -> Iterator[_Tco]: pass From d8c59f50cf7fee24112b190e0268ce727a578b0e Mon Sep 17 00:00:00 2001 From: David Foster Date: Sat, 8 Mar 2025 13:48:07 -0500 Subject: [PATCH 12/20] NOMERGE: mypy_primer: Enable --enable-incomplete-feature=TypeForm when checking open source code --- mypy/options.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mypy/options.py b/mypy/options.py index 1013ff5ab17e..32dd69739e0f 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -336,7 +336,10 @@ def __init__(self) -> None: self.dump_type_stats = False self.dump_inference_stats = False self.dump_build_stats = False - self.enable_incomplete_feature: list[str] = [] + # FIXME: Temporarily TypeForm support by default so that mypy_primer + # can check how enabling it by default would affect typechecker + # for projects that are already trying to use TypeForm. + self.enable_incomplete_feature: list[str] = [TYPE_FORM] self.timing_stats: str | None = None self.line_checking_stats: str | None = None From 5a64c78cc7a686de31b44b01ef0f9de3c26574fc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 5 Aug 2025 23:56:36 +0000 Subject: [PATCH 13/20] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- misc/analyze_typeform_stats.py | 22 +++++++++++++--------- mypy/checkexpr.py | 10 ++++++---- mypy/errors.py | 5 +---- mypy/semanal.py | 11 ++++++----- 4 files changed, 26 insertions(+), 22 deletions(-) diff --git a/misc/analyze_typeform_stats.py b/misc/analyze_typeform_stats.py index 05ddf6bab93d..0a540610bc62 100644 --- a/misc/analyze_typeform_stats.py +++ b/misc/analyze_typeform_stats.py @@ -37,9 +37,9 @@ def analyze_stats(output: str) -> None: """Parse mypy stats output and calculate TypeForm parsing efficiency.""" # Extract the three counters - total_match = re.search(r'type_expression_parse_count:\s*(\d+)', output) - success_match = re.search(r'type_expression_full_parse_success_count:\s*(\d+)', output) - failure_match = re.search(r'type_expression_full_parse_failure_count:\s*(\d+)', output) + total_match = re.search(r"type_expression_parse_count:\s*(\d+)", output) + success_match = re.search(r"type_expression_full_parse_success_count:\s*(\d+)", output) + failure_match = re.search(r"type_expression_full_parse_failure_count:\s*(\d+)", output) if not (total_match and success_match and failure_match): print("Error: Could not find all required counters in output") @@ -51,8 +51,8 @@ def analyze_stats(output: str) -> None: full_parses = successes + failures - print(f"TypeForm Expression Parsing Statistics:") - print(f"="*50) + print("TypeForm Expression Parsing Statistics:") + print("=" * 50) print(f"Total calls to SA.try_parse_as_type_expression: {total:,}") print(f"Quick rejections (no full parse): {total - full_parses:,}") print(f"Full parses attempted: {full_parses:,}") @@ -60,14 +60,16 @@ def analyze_stats(output: str) -> None: print(f" - Failed: {failures:,}") if total > 0: print() - print(f"Efficiency Metrics:") + print("Efficiency Metrics:") print(f" - Quick rejection rate: {((total - full_parses) / total * 100):.1f}%") print(f" - Full parse rate: {(full_parses / total * 100):.1f}%") print(f" - Full parse success rate: {(successes / full_parses * 100):.1f}%") print(f" - Overall success rate: {(successes / total * 100):.1f}%") print() - print(f"Performance Implications:") - print(f" - Expensive failed full parses: {failures:,} ({(failures / total * 100):.1f}% of all calls)") + print("Performance Implications:") + print( + f" - Expensive failed full parses: {failures:,} ({(failures / total * 100):.1f}% of all calls)" + ) if __name__ == "__main__": @@ -80,7 +82,9 @@ def analyze_stats(output: str) -> None: else: print("Usage: python3 analyze_typeform_stats.py [mypy_output_with_stats]") print("Examples:") - print(" python3 -m mypy --dump-build-stats file.py 2>&1 | python3 analyze_typeform_stats.py") + print( + " python3 -m mypy --dump-build-stats file.py 2>&1 | python3 analyze_typeform_stats.py" + ) print(" python3 analyze_typeform_stats.py 'output_string'") sys.exit(1) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 14e940518bee..f2f63a1aee9f 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -6083,10 +6083,12 @@ def accept( ) # r-value type, when interpreted as a type expression elif ( isinstance(p_type_context, UnionType) - and any([ - isinstance(item, TypeType) and item.is_type_form - for item in p_type_context.items - ]) + and any( + [ + isinstance(item, TypeType) and item.is_type_form + for item in p_type_context.items + ] + ) and (node_as_type := self.try_parse_as_type_expression(node)) is not None ): typ1 = TypeType.make_normalized( diff --git a/mypy/errors.py b/mypy/errors.py index 5c508e4222ee..d75c1c62a1ed 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -23,10 +23,7 @@ # Show error codes for some note-level messages (these usually appear alone # and not as a comment for a previous error-level message). -SHOW_NOTE_CODES: Final = { - codes.ANNOTATION_UNCHECKED, - codes.DEPRECATED, -} +SHOW_NOTE_CODES: Final = {codes.ANNOTATION_UNCHECKED, codes.DEPRECATED} # Do not add notes with links to error code docs to errors with these codes. # We can tweak this set as we get more experience about what is helpful and what is not. diff --git a/mypy/semanal.py b/mypy/semanal.py index 5df2d0366dea..03b67e873a45 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -51,7 +51,6 @@ from __future__ import annotations import re -import warnings from collections.abc import Collection, Iterable, Iterator from contextlib import contextmanager from typing import Any, Callable, Final, TypeVar, cast @@ -364,7 +363,7 @@ # [^\d\W] = word character that is not a digit # \w = word character # \Z = match end of string; does not allow a trailing \n, unlike $ -_IDENTIFIER_RE = re.compile(r'^[^\d\W]\w*\Z', re.UNICODE) +_IDENTIFIER_RE = re.compile(r"^[^\d\W]\w*\Z", re.UNICODE) class SemanticAnalyzer( @@ -7770,11 +7769,11 @@ def try_parse_as_type_expression(self, maybe_type_expr: Expression) -> None: else: # does not look like an identifier if '"' in str_value or "'" in str_value: # Only valid inside a Literal[...] type - if '[' not in str_value: + if "[" not in str_value: # Cannot be a Literal[...] type maybe_type_expr.as_type = None return - elif str_value == '': + elif str_value == "": # Empty string is not a valid type maybe_type_expr.as_type = None return @@ -7828,7 +7827,9 @@ def try_parse_as_type_expression(self, maybe_type_expr: Expression) -> None: finally: self.errors.flushed_files = original_flushed_files # restore - print(f'SA.try_parse_as_type_expression: Full parse failure: {maybe_type_expr}, errors={errors!r}') + print( + f"SA.try_parse_as_type_expression: Full parse failure: {maybe_type_expr}, errors={errors!r}" + ) # Count full parse attempts for profiling if t is not None: From cc4fc2377d15a16d8ca62bbe6753ef0ef4309a5c Mon Sep 17 00:00:00 2001 From: David Foster Date: Tue, 5 Aug 2025 19:59:05 -0400 Subject: [PATCH 14/20] SQ -> Allow TypeAlias and PlaceholderNode to be stringified/printed --- mypy/strconv.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/strconv.py b/mypy/strconv.py index ac925a8a374b..6d3c59711e50 100644 --- a/mypy/strconv.py +++ b/mypy/strconv.py @@ -213,10 +213,10 @@ def visit_nonlocal_decl(self, o: mypy.nodes.NonlocalDecl) -> str: def visit_decorator(self, o: mypy.nodes.Decorator) -> str: return self.dump([o.var, o.decorators, o.func], o) - def visit_type_alias(self, o: mypy.nodes.TypeAlias, /) -> T: + def visit_type_alias(self, o: mypy.nodes.TypeAlias, /) -> str: return self.dump([o.name, o.target, o.alias_tvars, o.no_args], o) - def visit_placeholder_node(self, o: mypy.nodes.PlaceholderNode, /) -> T: + def visit_placeholder_node(self, o: mypy.nodes.PlaceholderNode, /) -> str: return self.dump([o.fullname], o) # Statements From f14b163f7146af6b790888717d737d6e6c5e4530 Mon Sep 17 00:00:00 2001 From: David Foster Date: Tue, 5 Aug 2025 20:17:53 -0400 Subject: [PATCH 15/20] SQ -> Merge branch 'master' into f/typeform4 -- Fix bad merge --- mypy/checkexpr.py | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index f2f63a1aee9f..e9913a6b394e 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -6050,26 +6050,7 @@ 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) - # Deeply nested generic calls can deteriorate performance dramatically. - # Although in most cases caching makes little difference, in worst case - # it avoids exponential complexity. - # We cannot use cache inside lambdas, because they skip immediate type - # context, and use enclosing one, see infer_lambda_type_using_context(). - # TODO: consider using cache for more expression kinds. - elif ( - isinstance(node, (CallExpr, ListExpr, TupleExpr, DictExpr, OpExpr)) - and not (self.in_lambda_expr or self.chk.current_node_deferred) - and not self.chk.options.disable_expression_cache - ): - if (node, type_context) in self.expr_cache: - binder_version, typ, messages, type_map = self.expr_cache[(node, type_context)] - if binder_version == self.chk.binder.version: - self.chk.store_types(type_map) - self.msg.add_errors(messages) - else: - typ = self.accept_maybe_cache(node, type_context=type_context) - else: - typ = self.accept_maybe_cache(node, type_context=type_context) + elif ( isinstance(p_type_context, TypeType) and p_type_context.is_type_form @@ -6102,6 +6083,26 @@ def accept( else: typ2 = node.accept(self) typ = typ2 # r-value type, when interpreted as a value expression + # Deeply nested generic calls can deteriorate performance dramatically. + # Although in most cases caching makes little difference, in worst case + # it avoids exponential complexity. + # We cannot use cache inside lambdas, because they skip immediate type + # context, and use enclosing one, see infer_lambda_type_using_context(). + # TODO: consider using cache for more expression kinds. + elif ( + isinstance(node, (CallExpr, ListExpr, TupleExpr, DictExpr, OpExpr)) + and not (self.in_lambda_expr or self.chk.current_node_deferred) + and not self.chk.options.disable_expression_cache + ): + if (node, type_context) in self.expr_cache: + binder_version, typ, messages, type_map = self.expr_cache[(node, type_context)] + if binder_version == self.chk.binder.version: + self.chk.store_types(type_map) + self.msg.add_errors(messages) + else: + typ = self.accept_maybe_cache(node, type_context=type_context) + else: + typ = self.accept_maybe_cache(node, type_context=type_context) else: typ = node.accept(self) # r-value type, when interpreted as a value expression except Exception as err: From f4cb3f96023852d34b098c2cb15b2d18ae2449b2 Mon Sep 17 00:00:00 2001 From: David Foster Date: Tue, 5 Aug 2025 20:26:11 -0400 Subject: [PATCH 16/20] Fix test: testUnionOrSyntaxWithinRuntimeContextNotAllowed --- test-data/unit/check-union-or-syntax.test | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test-data/unit/check-union-or-syntax.test b/test-data/unit/check-union-or-syntax.test index cf69920de8e3..35af44c62800 100644 --- a/test-data/unit/check-union-or-syntax.test +++ b/test-data/unit/check-union-or-syntax.test @@ -106,7 +106,8 @@ b: X # E: Variable "__main__.X" is not valid as a type \ # flags: --python-version 3.9 from __future__ import annotations from typing import List -T = int | str # E: Invalid type alias: expression is not a valid type +T = int | str # E: Invalid type alias: expression is not a valid type \ + # E: Unsupported left operand type for | ("type[int]") class C(List[int | str]): # E: Type expected within [...] \ # E: Invalid base class "List" pass From 53174d40c044fe2ab1e13a255c9844c45a93dd7f Mon Sep 17 00:00:00 2001 From: David Foster Date: Tue, 5 Aug 2025 20:33:30 -0400 Subject: [PATCH 17/20] Fix error: Never apply isinstance() to unexpanded types --- mypy/checkexpr.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index e9913a6b394e..5d437c57ae0f 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -6066,7 +6066,8 @@ def accept( isinstance(p_type_context, UnionType) and any( [ - isinstance(item, TypeType) and item.is_type_form + isinstance(p_item := get_proper_type(item), TypeType) and + p_item.is_type_form for item in p_type_context.items ] ) From 4cc1b18b4e7d5cb118e3654b46164167fb20e804 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 6 Aug 2025 00:35:15 +0000 Subject: [PATCH 18/20] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checkexpr.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 5d437c57ae0f..a8eeb41a1b47 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -6066,8 +6066,8 @@ def accept( isinstance(p_type_context, UnionType) and any( [ - isinstance(p_item := get_proper_type(item), TypeType) and - p_item.is_type_form + isinstance(p_item := get_proper_type(item), TypeType) + and p_item.is_type_form for item in p_type_context.items ] ) From 45d93798fef35ac90fd7c263a5e8313a70cf7c7f Mon Sep 17 00:00:00 2001 From: David Foster Date: Tue, 5 Aug 2025 20:55:52 -0400 Subject: [PATCH 19/20] Make ruff happy --- mypy/checkexpr.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index a8eeb41a1b47..8db03b3092fa 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -6065,11 +6065,9 @@ def accept( elif ( isinstance(p_type_context, UnionType) and any( - [ - isinstance(p_item := get_proper_type(item), TypeType) - and p_item.is_type_form - for item in p_type_context.items - ] + isinstance(p_item := get_proper_type(item), TypeType) + and p_item.is_type_form + for item in p_type_context.items ) and (node_as_type := self.try_parse_as_type_expression(node)) is not None ): From e91d02dfcf454e6a7646406b2b6cd77f27559d1e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 6 Aug 2025 00:58:01 +0000 Subject: [PATCH 20/20] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checkexpr.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 8db03b3092fa..8a670824629a 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -6065,8 +6065,7 @@ def accept( elif ( isinstance(p_type_context, UnionType) and any( - isinstance(p_item := get_proper_type(item), TypeType) - and p_item.is_type_form + isinstance(p_item := get_proper_type(item), TypeType) and p_item.is_type_form for item in p_type_context.items ) and (node_as_type := self.try_parse_as_type_expression(node)) is not None