Skip to content

Commit a8d2f13

Browse files
authored
Cache type_object_type() (#19514)
This gives almost 4% performance boost (Python 3.12, compiled). Note there is an old bug in `type_object_type()`, we treat not ready types as `Any` without deferring, I disable caching in this case. Unfortunately, using this in fine-grained mode is tricky, essentially I have three options: * Use some horrible hacks to invalidate cache when needed * Add (expensive) class target dependency from `__init__`/`__new__` * Only allow constructor caching during initial load, but disable it in fine-grained increments I decided to choose the last option. I think it has the best balance complexity/benefits.
1 parent c6b40df commit a8d2f13

File tree

6 files changed

+55
-8
lines changed

6 files changed

+55
-8
lines changed

mypy/checker.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,7 @@ def __init__(
397397
self.is_stub = tree.is_stub
398398
self.is_typeshed_stub = tree.is_typeshed_file(options)
399399
self.inferred_attribute_types = None
400+
self.allow_constructor_cache = True
400401

401402
# If True, process function definitions. If False, don't. This is used
402403
# for processing module top levels in fine-grained incremental mode.
@@ -500,12 +501,16 @@ def check_first_pass(self) -> None:
500501
)
501502

502503
def check_second_pass(
503-
self, todo: Sequence[DeferredNode | FineGrainedDeferredNode] | None = None
504+
self,
505+
todo: Sequence[DeferredNode | FineGrainedDeferredNode] | None = None,
506+
*,
507+
allow_constructor_cache: bool = True,
504508
) -> bool:
505509
"""Run second or following pass of type checking.
506510
507511
This goes through deferred nodes, returning True if there were any.
508512
"""
513+
self.allow_constructor_cache = allow_constructor_cache
509514
self.recurse_into_functions = True
510515
with state.strict_optional_set(self.options.strict_optional), checker_state.set(self):
511516
if not todo and not self.deferred_nodes:

mypy/checker_shared.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ class TypeCheckerSharedApi(CheckerPluginInterface):
137137
module_refs: set[str]
138138
scope: CheckerScope
139139
checking_missing_await: bool
140+
allow_constructor_cache: bool
140141

141142
@property
142143
@abstractmethod

mypy/nodes.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3022,6 +3022,7 @@ class is generic then it will be a type constructor of higher kind.
30223022
"dataclass_transform_spec",
30233023
"is_type_check_only",
30243024
"deprecated",
3025+
"type_object_type",
30253026
)
30263027

30273028
_fullname: str # Fully qualified name
@@ -3178,6 +3179,10 @@ class is generic then it will be a type constructor of higher kind.
31783179
# The type's deprecation message (in case it is deprecated)
31793180
deprecated: str | None
31803181

3182+
# Cached value of class constructor type, i.e. the type of class object when it
3183+
# appears in runtime context.
3184+
type_object_type: mypy.types.FunctionLike | None
3185+
31813186
FLAGS: Final = [
31823187
"is_abstract",
31833188
"is_enum",
@@ -3236,6 +3241,7 @@ def __init__(self, names: SymbolTable, defn: ClassDef, module_name: str) -> None
32363241
self.dataclass_transform_spec = None
32373242
self.is_type_check_only = False
32383243
self.deprecated = None
3244+
self.type_object_type = None
32393245

32403246
def add_type_vars(self) -> None:
32413247
self.has_type_var_tuple_type = False

mypy/semanal_infer.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def infer_decorator_signature_if_simple(
3131
"""
3232
if dec.var.is_property:
3333
# Decorators are expected to have a callable type (it's a little odd).
34+
# TODO: this may result in wrong type if @property is applied to decorated method.
3435
if dec.func.type is None:
3536
dec.var.type = CallableType(
3637
[AnyType(TypeOfAny.special_form)],
@@ -47,6 +48,8 @@ def infer_decorator_signature_if_simple(
4748
for expr in dec.decorators:
4849
preserve_type = False
4950
if isinstance(expr, RefExpr) and isinstance(expr.node, FuncDef):
51+
if expr.fullname == "typing.no_type_check":
52+
return
5053
if expr.node.type and is_identity_signature(expr.node.type):
5154
preserve_type = True
5255
if not preserve_type:

mypy/server/update.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,10 +1025,12 @@ def key(node: FineGrainedDeferredNode) -> int:
10251025
# We seem to need additional passes in fine-grained incremental mode.
10261026
checker.pass_num = 0
10271027
checker.last_pass = 3
1028-
more = checker.check_second_pass(nodes)
1028+
# It is tricky to reliably invalidate constructor cache in fine-grained increments.
1029+
# See PR 19514 description for details.
1030+
more = checker.check_second_pass(nodes, allow_constructor_cache=False)
10291031
while more:
10301032
more = False
1031-
if graph[module_id].type_checker().check_second_pass():
1033+
if graph[module_id].type_checker().check_second_pass(allow_constructor_cache=False):
10321034
more = True
10331035

10341036
if manager.options.export_types:

mypy/typeops.py

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from collections.abc import Iterable, Sequence
1212
from typing import Any, Callable, TypeVar, cast
1313

14+
from mypy.checker_state import checker_state
1415
from mypy.copytype import copy_type
1516
from mypy.expandtype import expand_type, expand_type_by_instance
1617
from mypy.maptype import map_instance_to_supertype
@@ -145,6 +146,15 @@ def type_object_type(info: TypeInfo, named_type: Callable[[str], Instance]) -> P
145146
where ... are argument types for the __init__/__new__ method (without the self
146147
argument). Also, the fallback type will be 'type' instead of 'function'.
147148
"""
149+
allow_cache = (
150+
checker_state.type_checker is not None
151+
and checker_state.type_checker.allow_constructor_cache
152+
)
153+
154+
if info.type_object_type is not None:
155+
if allow_cache:
156+
return info.type_object_type
157+
info.type_object_type = None
148158

149159
# We take the type from whichever of __init__ and __new__ is first
150160
# 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
167177
init_index = info.mro.index(init_method.node.info)
168178
new_index = info.mro.index(new_method.node.info)
169179

170-
fallback = info.metaclass_type or named_type("builtins.type")
180+
if info.metaclass_type is not None:
181+
fallback = info.metaclass_type
182+
elif checker_state.type_checker:
183+
# Prefer direct call when it is available. It is faster, and,
184+
# unfortunately, some callers provide bogus callback.
185+
fallback = checker_state.type_checker.named_type("builtins.type")
186+
else:
187+
fallback = named_type("builtins.type")
188+
171189
if init_index < new_index:
172190
method: FuncBase | Decorator = init_method.node
173191
is_new = False
@@ -189,7 +207,10 @@ def type_object_type(info: TypeInfo, named_type: Callable[[str], Instance]) -> P
189207
is_bound=True,
190208
fallback=named_type("builtins.function"),
191209
)
192-
return class_callable(sig, info, fallback, None, is_new=False)
210+
result: FunctionLike = class_callable(sig, info, fallback, None, is_new=False)
211+
if allow_cache:
212+
info.type_object_type = result
213+
return result
193214

194215
# Otherwise prefer __init__ in a tie. It isn't clear that this
195216
# 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
199220
# Construct callable type based on signature of __init__. Adjust
200221
# return type and insert type arguments.
201222
if isinstance(method, FuncBase):
223+
if isinstance(method, OverloadedFuncDef) and not method.type:
224+
# Do not cache if the type is not ready. Same logic for decorators is
225+
# achieved in early return above because is_valid_constructor() is False.
226+
allow_cache = False
202227
t = function_type(method, fallback)
203228
else:
204229
assert isinstance(method.type, ProperType)
205230
assert isinstance(method.type, FunctionLike) # is_valid_constructor() ensures this
206231
t = method.type
207-
return type_object_type_from_function(t, info, method.info, fallback, is_new)
232+
result = type_object_type_from_function(t, info, method.info, fallback, is_new)
233+
if allow_cache:
234+
info.type_object_type = result
235+
return result
208236

209237

210238
def is_valid_constructor(n: SymbolNode | None) -> bool:
@@ -865,8 +893,8 @@ def function_type(func: FuncBase, fallback: Instance) -> FunctionLike:
865893
if isinstance(func, FuncItem):
866894
return callable_type(func, fallback)
867895
else:
868-
# Broken overloads can have self.type set to None.
869-
# TODO: should we instead always set the type in semantic analyzer?
896+
# Either a broken overload, or decorated overload type is not ready.
897+
# TODO: make sure the caller defers if possible.
870898
assert isinstance(func, OverloadedFuncDef)
871899
any_type = AnyType(TypeOfAny.from_error)
872900
dummy = CallableType(
@@ -1254,6 +1282,8 @@ def get_protocol_member(
12541282
if member == "__call__" and class_obj:
12551283
# Special case: class objects always have __call__ that is just the constructor.
12561284

1285+
# TODO: this is wrong, it creates callables that are not recognized as type objects.
1286+
# Long-term, we should probably get rid of this callback argument altogether.
12571287
def named_type(fullname: str) -> Instance:
12581288
return Instance(left.type.mro[-1], [])
12591289

0 commit comments

Comments
 (0)