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], [])
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: