diff --git a/mypy/checker.py b/mypy/checker.py index 7579c36a97d0..421755d36dcd 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -397,6 +397,7 @@ def __init__( self.is_stub = tree.is_stub self.is_typeshed_stub = tree.is_typeshed_file(options) self.inferred_attribute_types = None + self.allow_constructor_cache = True # If True, process function definitions. If False, don't. This is used # for processing module top levels in fine-grained incremental mode. @@ -500,12 +501,16 @@ def check_first_pass(self) -> None: ) def check_second_pass( - self, todo: Sequence[DeferredNode | FineGrainedDeferredNode] | None = None + self, + todo: Sequence[DeferredNode | FineGrainedDeferredNode] | None = None, + *, + allow_constructor_cache: bool = True, ) -> bool: """Run second or following pass of type checking. This goes through deferred nodes, returning True if there were any. """ + self.allow_constructor_cache = allow_constructor_cache self.recurse_into_functions = True with state.strict_optional_set(self.options.strict_optional), checker_state.set(self): if not todo and not self.deferred_nodes: diff --git a/mypy/checker_shared.py b/mypy/checker_shared.py index 65cec41d5202..7a5e9cb52c70 100644 --- a/mypy/checker_shared.py +++ b/mypy/checker_shared.py @@ -137,6 +137,7 @@ class TypeCheckerSharedApi(CheckerPluginInterface): module_refs: set[str] scope: CheckerScope checking_missing_await: bool + allow_constructor_cache: bool @property @abstractmethod diff --git a/mypy/nodes.py b/mypy/nodes.py index fc2656ce2130..921620866a06 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -3022,6 +3022,7 @@ class is generic then it will be a type constructor of higher kind. "dataclass_transform_spec", "is_type_check_only", "deprecated", + "type_object_type", ) _fullname: str # Fully qualified name @@ -3178,6 +3179,10 @@ class is generic then it will be a type constructor of higher kind. # The type's deprecation message (in case it is deprecated) deprecated: str | None + # Cached value of class constructor type, i.e. the type of class object when it + # appears in runtime context. + type_object_type: mypy.types.FunctionLike | None + FLAGS: Final = [ "is_abstract", "is_enum", @@ -3236,6 +3241,7 @@ def __init__(self, names: SymbolTable, defn: ClassDef, module_name: str) -> None self.dataclass_transform_spec = None self.is_type_check_only = False self.deprecated = None + self.type_object_type = None def add_type_vars(self) -> None: self.has_type_var_tuple_type = False diff --git a/mypy/semanal_infer.py b/mypy/semanal_infer.py index a146b56dc2d3..89a073cdad47 100644 --- a/mypy/semanal_infer.py +++ b/mypy/semanal_infer.py @@ -31,6 +31,7 @@ def infer_decorator_signature_if_simple( """ if dec.var.is_property: # Decorators are expected to have a callable type (it's a little odd). + # TODO: this may result in wrong type if @property is applied to decorated method. if dec.func.type is None: dec.var.type = CallableType( [AnyType(TypeOfAny.special_form)], @@ -47,6 +48,8 @@ def infer_decorator_signature_if_simple( for expr in dec.decorators: preserve_type = False if isinstance(expr, RefExpr) and isinstance(expr.node, FuncDef): + if expr.fullname == "typing.no_type_check": + return if expr.node.type and is_identity_signature(expr.node.type): preserve_type = True if not preserve_type: diff --git a/mypy/server/update.py b/mypy/server/update.py index ea336154ae56..839090ca45ac 100644 --- a/mypy/server/update.py +++ b/mypy/server/update.py @@ -1025,10 +1025,12 @@ def key(node: FineGrainedDeferredNode) -> int: # We seem to need additional passes in fine-grained incremental mode. checker.pass_num = 0 checker.last_pass = 3 - more = checker.check_second_pass(nodes) + # It is tricky to reliably invalidate constructor cache in fine-grained increments. + # See PR 19514 description for details. + more = checker.check_second_pass(nodes, allow_constructor_cache=False) while more: more = False - if graph[module_id].type_checker().check_second_pass(): + if graph[module_id].type_checker().check_second_pass(allow_constructor_cache=False): more = True if manager.options.export_types: diff --git a/mypy/typeops.py b/mypy/typeops.py index 9aa08b40a991..1c22a1711944 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -11,6 +11,7 @@ from collections.abc import Iterable, Sequence from typing import Any, Callable, TypeVar, cast +from mypy.checker_state import checker_state from mypy.copytype import copy_type from mypy.expandtype import expand_type, expand_type_by_instance from mypy.maptype import map_instance_to_supertype @@ -145,6 +146,15 @@ def type_object_type(info: TypeInfo, named_type: Callable[[str], Instance]) -> P where ... are argument types for the __init__/__new__ method (without the self argument). Also, the fallback type will be 'type' instead of 'function'. """ + allow_cache = ( + checker_state.type_checker is not None + and checker_state.type_checker.allow_constructor_cache + ) + + if info.type_object_type is not None: + if allow_cache: + return info.type_object_type + info.type_object_type = None # We take the type from whichever of __init__ and __new__ is first # in the MRO, preferring __init__ if there is a tie. @@ -167,7 +177,15 @@ def type_object_type(info: TypeInfo, named_type: Callable[[str], Instance]) -> P init_index = info.mro.index(init_method.node.info) new_index = info.mro.index(new_method.node.info) - fallback = info.metaclass_type or named_type("builtins.type") + if info.metaclass_type is not None: + fallback = info.metaclass_type + elif checker_state.type_checker: + # Prefer direct call when it is available. It is faster, and, + # unfortunately, some callers provide bogus callback. + fallback = checker_state.type_checker.named_type("builtins.type") + else: + fallback = named_type("builtins.type") + if init_index < new_index: method: FuncBase | Decorator = init_method.node is_new = False @@ -189,7 +207,10 @@ def type_object_type(info: TypeInfo, named_type: Callable[[str], Instance]) -> P is_bound=True, fallback=named_type("builtins.function"), ) - return class_callable(sig, info, fallback, None, is_new=False) + result: FunctionLike = class_callable(sig, info, fallback, None, is_new=False) + if allow_cache: + info.type_object_type = result + return result # Otherwise prefer __init__ in a tie. It isn't clear that this # is the right thing, but __new__ caused problems with @@ -199,12 +220,19 @@ def type_object_type(info: TypeInfo, named_type: Callable[[str], Instance]) -> P # Construct callable type based on signature of __init__. Adjust # return type and insert type arguments. if isinstance(method, FuncBase): + if isinstance(method, OverloadedFuncDef) and not method.type: + # Do not cache if the type is not ready. Same logic for decorators is + # achieved in early return above because is_valid_constructor() is False. + allow_cache = False t = function_type(method, fallback) else: assert isinstance(method.type, ProperType) assert isinstance(method.type, FunctionLike) # is_valid_constructor() ensures this t = method.type - return type_object_type_from_function(t, info, method.info, fallback, is_new) + result = type_object_type_from_function(t, info, method.info, fallback, is_new) + if allow_cache: + info.type_object_type = result + return result def is_valid_constructor(n: SymbolNode | None) -> bool: @@ -865,8 +893,8 @@ def function_type(func: FuncBase, fallback: Instance) -> FunctionLike: if isinstance(func, FuncItem): return callable_type(func, fallback) else: - # Broken overloads can have self.type set to None. - # TODO: should we instead always set the type in semantic analyzer? + # Either a broken overload, or decorated overload type is not ready. + # TODO: make sure the caller defers if possible. assert isinstance(func, OverloadedFuncDef) any_type = AnyType(TypeOfAny.from_error) dummy = CallableType( @@ -1254,6 +1282,8 @@ def get_protocol_member( if member == "__call__" and class_obj: # Special case: class objects always have __call__ that is just the constructor. + # TODO: this is wrong, it creates callables that are not recognized as type objects. + # Long-term, we should probably get rid of this callback argument altogether. def named_type(fullname: str) -> Instance: return Instance(left.type.mro[-1], [])