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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

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:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy