Skip to content

Commit 35acb1b

Browse files
authored
Use native ExpandTypeVisitor for expanding type aliases as well (#15209)
This used to be a source of code duplication, and also using the "native" visitor will allow us to implement new features for type aliases easily (e.g. variadic type aliases, that I am going to do in next PR).
1 parent c54c226 commit 35acb1b

File tree

3 files changed

+65
-96
lines changed

3 files changed

+65
-96
lines changed

mypy/expandtype.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
NoneType,
2020
Overloaded,
2121
Parameters,
22+
ParamSpecFlavor,
2223
ParamSpecType,
2324
PartialType,
2425
ProperType,
@@ -36,7 +37,6 @@
3637
UninhabitedType,
3738
UnionType,
3839
UnpackType,
39-
expand_param_spec,
4040
flatten_nested_unions,
4141
get_proper_type,
4242
)
@@ -247,7 +247,33 @@ def visit_param_spec(self, t: ParamSpecType) -> Type:
247247
# TODO: why does this case even happen? Instances aren't plural.
248248
return repl
249249
elif isinstance(repl, (ParamSpecType, Parameters, CallableType)):
250-
return expand_param_spec(t, repl)
250+
if isinstance(repl, ParamSpecType):
251+
return repl.copy_modified(
252+
flavor=t.flavor,
253+
prefix=t.prefix.copy_modified(
254+
arg_types=t.prefix.arg_types + repl.prefix.arg_types,
255+
arg_kinds=t.prefix.arg_kinds + repl.prefix.arg_kinds,
256+
arg_names=t.prefix.arg_names + repl.prefix.arg_names,
257+
),
258+
)
259+
else:
260+
# if the paramspec is *P.args or **P.kwargs:
261+
if t.flavor != ParamSpecFlavor.BARE:
262+
assert isinstance(repl, CallableType), "Should not be able to get here."
263+
# Is this always the right thing to do?
264+
param_spec = repl.param_spec()
265+
if param_spec:
266+
return param_spec.with_flavor(t.flavor)
267+
else:
268+
return repl
269+
else:
270+
return Parameters(
271+
t.prefix.arg_types + repl.arg_types,
272+
t.prefix.arg_kinds + repl.arg_kinds,
273+
t.prefix.arg_names + repl.arg_names,
274+
variables=[*t.prefix.variables, *repl.variables],
275+
)
276+
251277
else:
252278
# TODO: should this branch be removed? better not to fail silently
253279
return repl

mypy/types.py

Lines changed: 24 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -313,9 +313,14 @@ def _expand_once(self) -> Type:
313313
# as their target.
314314
assert isinstance(self.alias.target, Instance) # type: ignore[misc]
315315
return self.alias.target.copy_modified(args=self.args)
316-
return replace_alias_tvars(
317-
self.alias.target, self.alias.alias_tvars, self.args, self.line, self.column
316+
replacer = InstantiateAliasVisitor(
317+
{v.id: s for (v, s) in zip(self.alias.alias_tvars, self.args)}
318318
)
319+
new_tp = self.alias.target.accept(replacer)
320+
new_tp.accept(LocationSetter(self.line, self.column))
321+
new_tp.line = self.line
322+
new_tp.column = self.column
323+
return new_tp
319324

320325
def _partial_expansion(self, nothing_args: bool = False) -> tuple[ProperType, bool]:
321326
# Private method mostly for debugging and testing.
@@ -3243,49 +3248,6 @@ def is_named_instance(t: Type, fullnames: str | tuple[str, ...]) -> TypeGuard[In
32433248
return isinstance(t, Instance) and t.type.fullname in fullnames
32443249

32453250

3246-
class InstantiateAliasVisitor(TrivialSyntheticTypeTranslator):
3247-
def __init__(self, vars: list[TypeVarLikeType], subs: list[Type]) -> None:
3248-
self.replacements = {v.id: s for (v, s) in zip(vars, subs)}
3249-
3250-
def visit_type_alias_type(self, typ: TypeAliasType) -> Type:
3251-
return typ.copy_modified(args=[t.accept(self) for t in typ.args])
3252-
3253-
def visit_type_var(self, typ: TypeVarType) -> Type:
3254-
if typ.id in self.replacements:
3255-
return self.replacements[typ.id]
3256-
return typ
3257-
3258-
def visit_callable_type(self, t: CallableType) -> Type:
3259-
param_spec = t.param_spec()
3260-
if param_spec is not None:
3261-
# TODO: this branch duplicates the one in expand_type(), find a way to reuse it
3262-
# without import cycle types <-> typeanal <-> expandtype.
3263-
repl = get_proper_type(self.replacements.get(param_spec.id))
3264-
if isinstance(repl, (CallableType, Parameters)):
3265-
prefix = param_spec.prefix
3266-
t = t.expand_param_spec(repl, no_prefix=True)
3267-
return t.copy_modified(
3268-
arg_types=[t.accept(self) for t in prefix.arg_types] + t.arg_types,
3269-
arg_kinds=prefix.arg_kinds + t.arg_kinds,
3270-
arg_names=prefix.arg_names + t.arg_names,
3271-
ret_type=t.ret_type.accept(self),
3272-
type_guard=(t.type_guard.accept(self) if t.type_guard is not None else None),
3273-
)
3274-
return super().visit_callable_type(t)
3275-
3276-
def visit_param_spec(self, typ: ParamSpecType) -> Type:
3277-
if typ.id in self.replacements:
3278-
repl = get_proper_type(self.replacements[typ.id])
3279-
# TODO: all the TODOs from same logic in expand_type() apply here.
3280-
if isinstance(repl, Instance):
3281-
return repl
3282-
elif isinstance(repl, (ParamSpecType, Parameters, CallableType)):
3283-
return expand_param_spec(typ, repl)
3284-
else:
3285-
return repl
3286-
return typ
3287-
3288-
32893251
class LocationSetter(TypeTraverserVisitor):
32903252
# TODO: Should we update locations of other Type subclasses?
32913253
def __init__(self, line: int, column: int) -> None:
@@ -3298,20 +3260,6 @@ def visit_instance(self, typ: Instance) -> None:
32983260
super().visit_instance(typ)
32993261

33003262

3301-
def replace_alias_tvars(
3302-
tp: Type, vars: list[TypeVarLikeType], subs: list[Type], newline: int, newcolumn: int
3303-
) -> Type:
3304-
"""Replace type variables in a generic type alias tp with substitutions subs
3305-
resetting context. Length of subs should be already checked.
3306-
"""
3307-
replacer = InstantiateAliasVisitor(vars, subs)
3308-
new_tp = tp.accept(replacer)
3309-
new_tp.accept(LocationSetter(newline, newcolumn))
3310-
new_tp.line = newline
3311-
new_tp.column = newcolumn
3312-
return new_tp
3313-
3314-
33153263
class HasTypeVars(BoolTypeQuery):
33163264
def __init__(self) -> None:
33173265
super().__init__(ANY_STRATEGY)
@@ -3408,36 +3356,20 @@ def callable_with_ellipsis(any_type: AnyType, ret_type: Type, fallback: Instance
34083356
)
34093357

34103358

3411-
def expand_param_spec(
3412-
t: ParamSpecType, repl: ParamSpecType | Parameters | CallableType
3413-
) -> ProperType:
3414-
"""This is shared part of the logic w.r.t. ParamSpec instantiation.
3415-
3416-
It is shared between type aliases and proper types, that currently use somewhat different
3417-
logic for instantiation."""
3418-
if isinstance(repl, ParamSpecType):
3419-
return repl.copy_modified(
3420-
flavor=t.flavor,
3421-
prefix=t.prefix.copy_modified(
3422-
arg_types=t.prefix.arg_types + repl.prefix.arg_types,
3423-
arg_kinds=t.prefix.arg_kinds + repl.prefix.arg_kinds,
3424-
arg_names=t.prefix.arg_names + repl.prefix.arg_names,
3425-
),
3426-
)
3427-
else:
3428-
# if the paramspec is *P.args or **P.kwargs:
3429-
if t.flavor != ParamSpecFlavor.BARE:
3430-
assert isinstance(repl, CallableType), "Should not be able to get here."
3431-
# Is this always the right thing to do?
3432-
param_spec = repl.param_spec()
3433-
if param_spec:
3434-
return param_spec.with_flavor(t.flavor)
3435-
else:
3436-
return repl
3437-
else:
3438-
return Parameters(
3439-
t.prefix.arg_types + repl.arg_types,
3440-
t.prefix.arg_kinds + repl.arg_kinds,
3441-
t.prefix.arg_names + repl.arg_names,
3442-
variables=[*t.prefix.variables, *repl.variables],
3443-
)
3359+
# This cyclic import is unfortunate, but to avoid it we would need to move away all uses
3360+
# of get_proper_type() from types.py. Majority of them have been removed, but few remaining
3361+
# are quite tricky to get rid of, but ultimately we want to do it at some point.
3362+
from mypy.expandtype import ExpandTypeVisitor
3363+
3364+
3365+
class InstantiateAliasVisitor(ExpandTypeVisitor):
3366+
def visit_union_type(self, t: UnionType) -> Type:
3367+
# Unlike regular expand_type(), we don't do any simplification for unions,
3368+
# not even removing strict duplicates. There are three reasons for this:
3369+
# * get_proper_type() is a very hot function, even slightest slow down will
3370+
# cause a perf regression
3371+
# * We want to preserve this historical behaviour, to avoid possible
3372+
# regressions
3373+
# * Simplifying unions may (indirectly) call get_proper_type(), causing
3374+
# infinite recursion.
3375+
return TypeTranslator.visit_union_type(self, t)

test-data/unit/check-typeguard.test

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -604,11 +604,11 @@ from typing_extensions import TypeGuard
604604
class Z:
605605
def typeguard1(self, *, x: object) -> TypeGuard[int]: # line 4
606606
...
607-
607+
608608
@staticmethod
609609
def typeguard2(x: object) -> TypeGuard[int]:
610610
...
611-
611+
612612
@staticmethod # line 11
613613
def typeguard3(*, x: object) -> TypeGuard[int]:
614614
...
@@ -688,3 +688,14 @@ if typeguard(x=x, y="42"):
688688
if typeguard(y="42", x=x):
689689
reveal_type(x) # N: Revealed type is "builtins.str"
690690
[builtins fixtures/tuple.pyi]
691+
692+
[case testGenericAliasWithTypeGuard]
693+
from typing import Callable, List, TypeVar
694+
from typing_extensions import TypeGuard, TypeAlias
695+
696+
A = Callable[[object], TypeGuard[List[T]]]
697+
def foo(x: object) -> TypeGuard[List[str]]: ...
698+
699+
def test(f: A[T]) -> T: ...
700+
reveal_type(test(foo)) # N: Revealed type is "builtins.str"
701+
[builtins fixtures/list.pyi]

0 commit comments

Comments
 (0)