Skip to content

Commit 25cf74b

Browse files
authored
Fix crash on typevar with forward ref used in other module (#20334)
Fixes #20326 Type variables with forward references in upper bound are known to be problematic. Existing mechanisms to work with them implicitly assumed that they are used in the same module where they are defined, which is not necessarily the case for "old-style" type variables that can be imported. Note that the simplification I made in `semanal_typeargs.py` would be probably sufficient to fix this, but that would be papering over the real issue, so I am making a bit more principled fix.
1 parent 67df116 commit 25cf74b

File tree

5 files changed

+105
-9
lines changed

5 files changed

+105
-9
lines changed

mypy/plugins/proper_plugin.py

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

mypy/semanal.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4945,7 +4945,7 @@ def get_typevarlike_argument(
49454945
)
49464946
if analyzed is None:
49474947
# Type variables are special: we need to place them in the symbol table
4948-
# soon, even if upper bound is not ready yet. Otherwise avoiding
4948+
# soon, even if upper bound is not ready yet. Otherwise, avoiding
49494949
# a "deadlock" in this common pattern would be tricky:
49504950
# T = TypeVar('T', bound=Custom[Any])
49514951
# class Custom(Generic[T]):

mypy/semanal_typeargs.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -176,12 +176,12 @@ def validate_args(
176176
code=codes.VALID_TYPE,
177177
)
178178
continue
179+
if self.in_type_alias_expr and isinstance(arg, TypeVarType):
180+
# Type aliases are allowed to use unconstrained type variables
181+
# error will be checked at substitution point.
182+
continue
179183
if tvar.values:
180184
if isinstance(arg, TypeVarType):
181-
if self.in_type_alias_expr:
182-
# Type aliases are allowed to use unconstrained type variables
183-
# error will be checked at substitution point.
184-
continue
185185
arg_values = arg.values
186186
if not arg_values:
187187
is_error = True
@@ -205,10 +205,6 @@ def validate_args(
205205
and upper_bound.type.fullname == "builtins.object"
206206
)
207207
if not object_upper_bound and not is_subtype(arg, upper_bound):
208-
if self.in_type_alias_expr and isinstance(arg, TypeVarType):
209-
# Type aliases are allowed to use unconstrained type variables
210-
# error will be checked at substitution point.
211-
continue
212208
is_error = True
213209
self.fail(
214210
message_registry.INVALID_TYPEVAR_ARG_BOUND.format(

mypy/typeanal.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,15 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
346346
if hook is not None:
347347
return hook(AnalyzeTypeContext(t, t, self))
348348
tvar_def = self.tvar_scope.get_binding(sym)
349+
if tvar_def is not None:
350+
# We need to cover special-case explained in get_typevarlike_argument() here,
351+
# since otherwise the deferral will not be triggered if the type variable is
352+
# used in a different module. Using isinstance() should be safe for this purpose.
353+
tvar_params = [tvar_def.upper_bound, tvar_def.default]
354+
if isinstance(tvar_def, TypeVarType):
355+
tvar_params += tvar_def.values
356+
if any(isinstance(tp, PlaceholderType) for tp in tvar_params):
357+
self.api.defer()
349358
if isinstance(sym.node, ParamSpecExpr):
350359
if tvar_def is None:
351360
if self.allow_unbound_tvars:

test-data/unit/check-type-aliases.test

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1351,3 +1351,93 @@ reveal_type(D(x="asdf")) # E: No overload variant of "dict" matches argument ty
13511351
# N: def __init__(self, arg: Iterable[tuple[str, int]], **kwargs: int) -> dict[str, int] \
13521352
# N: Revealed type is "Any"
13531353
[builtins fixtures/dict.pyi]
1354+
1355+
[case testTypeAliasesInCyclicImport1]
1356+
import p.aliases
1357+
1358+
[file p/__init__.py]
1359+
[file p/aliases.py]
1360+
from typing_extensions import TypeAlias
1361+
from .defs import C, Alias1
1362+
1363+
Alias2: TypeAlias = Alias1[C]
1364+
1365+
[file p/defs.py]
1366+
from typing import TypeVar
1367+
from typing_extensions import TypeAlias
1368+
import p.aliases
1369+
1370+
C = TypeVar("C", bound="SomeClass")
1371+
Alias1: TypeAlias = C
1372+
1373+
class SomeClass:
1374+
pass
1375+
[builtins fixtures/tuple.pyi]
1376+
[typing fixtures/typing-full.pyi]
1377+
1378+
[case testTypeAliasesInCyclicImport2]
1379+
import p.aliases
1380+
1381+
[file p/__init__.py]
1382+
[file p/aliases.py]
1383+
from typing_extensions import TypeAlias
1384+
from .defs import C, Alias1
1385+
1386+
Alias2: TypeAlias = Alias1[C]
1387+
1388+
[file p/defs.py]
1389+
from typing import TypeVar, Union
1390+
from typing_extensions import TypeAlias
1391+
import p.aliases
1392+
1393+
C = TypeVar("C", bound="SomeClass")
1394+
Alias1: TypeAlias = Union[C, int]
1395+
1396+
class SomeClass:
1397+
pass
1398+
[builtins fixtures/tuple.pyi]
1399+
1400+
[case testTypeAliasesInCyclicImport3]
1401+
import p.aliases
1402+
1403+
[file p/__init__.py]
1404+
[file p/aliases.py]
1405+
from typing_extensions import TypeAlias
1406+
from .defs import C, Alias1
1407+
1408+
Alias2: TypeAlias = Alias1[C]
1409+
1410+
[file p/defs.py]
1411+
from typing import TypeVar
1412+
from typing_extensions import TypeAlias
1413+
import p.aliases
1414+
1415+
C = TypeVar("C", bound="list[SomeClass]")
1416+
Alias1: TypeAlias = C
1417+
1418+
class SomeClass:
1419+
pass
1420+
[builtins fixtures/tuple.pyi]
1421+
[typing fixtures/typing-full.pyi]
1422+
1423+
[case testTypeAliasesInCyclicImport4]
1424+
import p.aliases
1425+
1426+
[file p/__init__.py]
1427+
[file p/aliases.py]
1428+
from typing_extensions import TypeAlias
1429+
from .defs import C, Alias1
1430+
1431+
Alias2: TypeAlias = Alias1[C]
1432+
1433+
[file p/defs.py]
1434+
from typing import TypeVar, Union
1435+
from typing_extensions import TypeAlias
1436+
import p.aliases
1437+
1438+
C = TypeVar("C", bound="list[SomeClass]")
1439+
Alias1: TypeAlias = Union[C, int]
1440+
1441+
class SomeClass:
1442+
pass
1443+
[builtins fixtures/tuple.pyi]

0 commit comments

Comments
 (0)