Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions mypy/checker_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ class TypeCheckerSharedApi(CheckerPluginInterface):
module_refs: set[str]
scope: CheckerScope
checking_missing_await: bool
allow_constructor_cache: bool

@property
@abstractmethod
Expand Down
6 changes: 6 additions & 0 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions mypy/semanal_infer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)],
Expand All @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions mypy/server/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -1025,10 +1025,10 @@ 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)
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:
Expand Down
35 changes: 30 additions & 5 deletions mypy/typeops.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -167,7 +177,12 @@ 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 checker_state.type_checker:
builtins_type = checker_state.type_checker.named_type("builtins.type")
else:
builtins_type = named_type("builtins.type")

fallback = info.metaclass_type or builtins_type
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we keep it lazy (only call named_type if info.metaclass_type is None)?

if init_index < new_index:
method: FuncBase | Decorator = init_method.node
is_new = False
Expand All @@ -189,7 +204,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
Expand All @@ -199,12 +217,17 @@ 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:
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:
Expand Down Expand Up @@ -865,8 +888,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(
Expand Down Expand Up @@ -1254,6 +1277,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], [])

Expand Down