Skip to content

Commit 7c4ec52

Browse files
Keep trivial instances and aliases during expansion (#19543)
This weirdly looking change consistently shows 1% performance improvement on my machine (Python 3.12, compiled). The key here is to _not_ create new objects for trivial instances and aliases (i.e. those with no `.args`). The problem however is that some callers modify expanded type aliases _in place_ (for better error locations). I updated couple places discovered by tests, but there may be more. I think we should go ahead, and then fix bugs exposed by this using following strategy: * Always use original aliases (not theirs expansions) as error locations (see change in `semanal.py`). * If above is not possible (see unpacked tuple change), then call `t.copy_modified()` followed by `t.set_line()` manually. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 07d4a1b commit 7c4ec52

File tree

5 files changed

+35
-41
lines changed

5 files changed

+35
-41
lines changed

mypy/expandtype.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,7 @@ def visit_erased_type(self, t: ErasedType) -> Type:
210210

211211
def visit_instance(self, t: Instance) -> Type:
212212
if len(t.args) == 0:
213-
# TODO: Why do we need to create a copy here?
214-
return t.copy_modified()
213+
return t
215214

216215
args = self.expand_type_tuple_with_unpack(t.args)
217216

@@ -525,6 +524,8 @@ def visit_type_type(self, t: TypeType) -> Type:
525524
def visit_type_alias_type(self, t: TypeAliasType) -> Type:
526525
# Target of the type alias cannot contain type variables (not bound by the type
527526
# alias itself), so we just expand the arguments.
527+
if len(t.args) == 0:
528+
return t
528529
args = self.expand_type_list_with_unpack(t.args)
529530
# TODO: normalize if target is Tuple, and args are [*tuple[X, ...]]?
530531
return t.copy_modified(args=args)

mypy/meet.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,11 @@ def meet_types(s: Type, t: Type) -> ProperType:
116116
def narrow_declared_type(declared: Type, narrowed: Type) -> Type:
117117
"""Return the declared type narrowed down to another type."""
118118
# TODO: check infinite recursion for aliases here.
119-
if isinstance(narrowed, TypeGuardedType): # type: ignore[misc]
120-
# A type guard forces the new type even if it doesn't overlap the old.
119+
if isinstance(narrowed, TypeGuardedType):
120+
# A type guard forces the new type even if it doesn't overlap the old...
121+
if is_proper_subtype(declared, narrowed.type_guard, ignore_promotions=True):
122+
# ...unless it is a proper supertype of declared type.
123+
return declared
121124
return narrowed.type_guard
122125

123126
original_declared = declared
@@ -308,9 +311,7 @@ def is_overlapping_types(
308311
positives), for example: None only overlaps with explicitly optional types, Any
309312
doesn't overlap with anything except object, we don't ignore positional argument names.
310313
"""
311-
if isinstance(left, TypeGuardedType) or isinstance( # type: ignore[misc]
312-
right, TypeGuardedType
313-
):
314+
if isinstance(left, TypeGuardedType) or isinstance(right, TypeGuardedType):
314315
# A type guard forces the new type even if it doesn't overlap the old.
315316
return True
316317

mypy/plugins/proper_plugin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ def is_special_target(right: ProperType) -> bool:
107107
"mypy.types.DeletedType",
108108
"mypy.types.RequiredType",
109109
"mypy.types.ReadOnlyType",
110+
"mypy.types.TypeGuardedType",
110111
):
111112
# Special case: these are not valid targets for a type alias and thus safe.
112113
# TODO: introduce a SyntheticType base to simplify this?

mypy/semanal.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,12 +1046,12 @@ def remove_unpack_kwargs(self, defn: FuncDef, typ: CallableType) -> CallableType
10461046
last_type = typ.arg_types[-1]
10471047
if not isinstance(last_type, UnpackType):
10481048
return typ
1049-
last_type = get_proper_type(last_type.type)
1050-
if not isinstance(last_type, TypedDictType):
1049+
p_last_type = get_proper_type(last_type.type)
1050+
if not isinstance(p_last_type, TypedDictType):
10511051
self.fail("Unpack item in ** argument must be a TypedDict", last_type)
10521052
new_arg_types = typ.arg_types[:-1] + [AnyType(TypeOfAny.from_error)]
10531053
return typ.copy_modified(arg_types=new_arg_types)
1054-
overlap = set(typ.arg_names) & set(last_type.items)
1054+
overlap = set(typ.arg_names) & set(p_last_type.items)
10551055
# It is OK for TypedDict to have a key named 'kwargs'.
10561056
overlap.discard(typ.arg_names[-1])
10571057
if overlap:
@@ -1060,7 +1060,7 @@ def remove_unpack_kwargs(self, defn: FuncDef, typ: CallableType) -> CallableType
10601060
new_arg_types = typ.arg_types[:-1] + [AnyType(TypeOfAny.from_error)]
10611061
return typ.copy_modified(arg_types=new_arg_types)
10621062
# OK, everything looks right now, mark the callable type as using unpack.
1063-
new_arg_types = typ.arg_types[:-1] + [last_type]
1063+
new_arg_types = typ.arg_types[:-1] + [p_last_type]
10641064
return typ.copy_modified(arg_types=new_arg_types, unpack_kwargs=True)
10651065

10661066
def prepare_method_signature(self, func: FuncDef, info: TypeInfo, has_self_type: bool) -> None:

mypy/types.py

Lines changed: 21 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -355,11 +355,7 @@ def _expand_once(self) -> Type:
355355
):
356356
mapping[tvar.id] = sub
357357

358-
new_tp = self.alias.target.accept(InstantiateAliasVisitor(mapping))
359-
new_tp.accept(LocationSetter(self.line, self.column))
360-
new_tp.line = self.line
361-
new_tp.column = self.column
362-
return new_tp
358+
return self.alias.target.accept(InstantiateAliasVisitor(mapping))
363359

364360
def _partial_expansion(self, nothing_args: bool = False) -> tuple[ProperType, bool]:
365361
# Private method mostly for debugging and testing.
@@ -3214,7 +3210,8 @@ def get_proper_type(typ: Type | None) -> ProperType | None:
32143210
"""
32153211
if typ is None:
32163212
return None
3217-
if isinstance(typ, TypeGuardedType): # type: ignore[misc]
3213+
# TODO: this is an ugly hack, remove.
3214+
if isinstance(typ, TypeGuardedType):
32183215
typ = typ.type_guard
32193216
while isinstance(typ, TypeAliasType):
32203217
typ = typ._expand_once()
@@ -3238,9 +3235,7 @@ def get_proper_types(
32383235
if isinstance(types, list):
32393236
typelist = types
32403237
# Optimize for the common case so that we don't need to allocate anything
3241-
if not any(
3242-
isinstance(t, (TypeAliasType, TypeGuardedType)) for t in typelist # type: ignore[misc]
3243-
):
3238+
if not any(isinstance(t, (TypeAliasType, TypeGuardedType)) for t in typelist):
32443239
return cast("list[ProperType]", typelist)
32453240
return [get_proper_type(t) for t in typelist]
32463241
else:
@@ -3260,7 +3255,6 @@ def get_proper_types(
32603255
TypeTranslator as TypeTranslator,
32613256
TypeVisitor as TypeVisitor,
32623257
)
3263-
from mypy.typetraverser import TypeTraverserVisitor
32643258

32653259

32663260
class TypeStrVisitor(SyntheticTypeVisitor[str]):
@@ -3598,23 +3592,6 @@ def is_named_instance(t: Type, fullnames: str | tuple[str, ...]) -> TypeGuard[In
35983592
return isinstance(t, Instance) and t.type.fullname in fullnames
35993593

36003594

3601-
class LocationSetter(TypeTraverserVisitor):
3602-
# TODO: Should we update locations of other Type subclasses?
3603-
def __init__(self, line: int, column: int) -> None:
3604-
self.line = line
3605-
self.column = column
3606-
3607-
def visit_instance(self, typ: Instance) -> None:
3608-
typ.line = self.line
3609-
typ.column = self.column
3610-
super().visit_instance(typ)
3611-
3612-
def visit_type_alias_type(self, typ: TypeAliasType) -> None:
3613-
typ.line = self.line
3614-
typ.column = self.column
3615-
super().visit_type_alias_type(typ)
3616-
3617-
36183595
class HasTypeVars(BoolTypeQuery):
36193596
"""Visitor for querying whether a type has a type variable component."""
36203597

@@ -3709,8 +3686,8 @@ def flatten_nested_unions(
37093686

37103687
flat_items: list[Type] = []
37113688
for t in typelist:
3712-
if handle_type_alias_type:
3713-
if not handle_recursive and isinstance(t, TypeAliasType) and t.is_recursive:
3689+
if handle_type_alias_type and isinstance(t, TypeAliasType):
3690+
if not handle_recursive and t.is_recursive:
37143691
tp: Type = t
37153692
else:
37163693
tp = get_proper_type(t)
@@ -3757,7 +3734,21 @@ def flatten_nested_tuples(types: Iterable[Type]) -> list[Type]:
37573734
if not isinstance(p_type, TupleType):
37583735
res.append(typ)
37593736
continue
3760-
res.extend(flatten_nested_tuples(p_type.items))
3737+
if isinstance(typ.type, TypeAliasType):
3738+
items = []
3739+
for item in p_type.items:
3740+
if (
3741+
isinstance(item, ProperType)
3742+
and isinstance(item, Instance)
3743+
or isinstance(item, TypeAliasType)
3744+
):
3745+
if len(item.args) == 0:
3746+
item = item.copy_modified()
3747+
item.set_line(typ)
3748+
items.append(item)
3749+
else:
3750+
items = p_type.items
3751+
res.extend(flatten_nested_tuples(items))
37613752
return res
37623753

37633754

0 commit comments

Comments
 (0)