Skip to content

Commit 5a0fa55

Browse files
authored
Narrow type variable bounds in binder (#19183)
Fixes #5720 Fixes #8556 Fixes #9778 Fixes #10003 Fixes #10817 Fixes #11163 Fixes #11664 Fixes #12882 Fixes #13426 Fixes #13462 Fixes #14941 Fixes #15151 Fixes #19166 This handles a (surprisingly) common edge case. The charges in `bind_self()` and `bind_self_fast()` are tricky. I got a few "Redundant cast" errors there, which seemed good, but then I realized that attribute access etc. on a type variable go through slow `PyObject` paths, so I am actually forcing `CallableType` instead of `F(bound=CallableType)` there, since these are performance-critical functions.
1 parent 29d8f06 commit 5a0fa55

File tree

12 files changed

+161
-30
lines changed

12 files changed

+161
-30
lines changed

mypy/checkmember.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1484,19 +1484,20 @@ def bind_self_fast(method: F, original_type: Type | None = None) -> F:
14841484
items = [bind_self_fast(c, original_type) for c in method.items]
14851485
return cast(F, Overloaded(items))
14861486
assert isinstance(method, CallableType)
1487-
if not method.arg_types:
1487+
func: CallableType = method
1488+
if not func.arg_types:
14881489
# Invalid method, return something.
1489-
return cast(F, method)
1490-
if method.arg_kinds[0] in (ARG_STAR, ARG_STAR2):
1490+
return method
1491+
if func.arg_kinds[0] in (ARG_STAR, ARG_STAR2):
14911492
# See typeops.py for details.
1492-
return cast(F, method)
1493+
return method
14931494
original_type = get_proper_type(original_type)
14941495
if isinstance(original_type, CallableType) and original_type.is_type_obj():
14951496
original_type = TypeType.make_normalized(original_type.ret_type)
1496-
res = method.copy_modified(
1497-
arg_types=method.arg_types[1:],
1498-
arg_kinds=method.arg_kinds[1:],
1499-
arg_names=method.arg_names[1:],
1497+
res = func.copy_modified(
1498+
arg_types=func.arg_types[1:],
1499+
arg_kinds=func.arg_kinds[1:],
1500+
arg_names=func.arg_names[1:],
15001501
bound_args=[original_type],
15011502
)
15021503
return cast(F, res)

mypy/expandtype.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ def freshen_function_type_vars(callee: F) -> F:
122122
"""Substitute fresh type variables for generic function type variables."""
123123
if isinstance(callee, CallableType):
124124
if not callee.is_generic():
125-
return cast(F, callee)
125+
return callee
126126
tvs = []
127127
tvmap: dict[TypeVarId, Type] = {}
128128
for v in callee.variables:

mypy/join.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,9 @@ def visit_erased_type(self, t: ErasedType) -> ProperType:
298298

299299
def visit_type_var(self, t: TypeVarType) -> ProperType:
300300
if isinstance(self.s, TypeVarType) and self.s.id == t.id:
301-
return self.s
301+
if self.s.upper_bound == t.upper_bound:
302+
return self.s
303+
return self.s.copy_modified(upper_bound=join_types(self.s.upper_bound, t.upper_bound))
302304
else:
303305
return self.default(self.s)
304306

mypy/meet.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
find_unpack_in_list,
5151
get_proper_type,
5252
get_proper_types,
53+
has_type_vars,
5354
is_named_instance,
5455
split_with_prefix_and_suffix,
5556
)
@@ -149,6 +150,14 @@ def narrow_declared_type(declared: Type, narrowed: Type) -> Type:
149150
return make_simplified_union(
150151
[narrow_declared_type(declared, x) for x in narrowed.relevant_items()]
151152
)
153+
elif (
154+
isinstance(declared, TypeVarType)
155+
and not has_type_vars(original_narrowed)
156+
and is_subtype(original_narrowed, declared.upper_bound)
157+
):
158+
# We put this branch early to get T(bound=Union[A, B]) instead of
159+
# Union[T(bound=A), T(bound=B)] that will be confusing for users.
160+
return declared.copy_modified(upper_bound=original_narrowed)
152161
elif not is_overlapping_types(declared, narrowed, prohibit_none_typevar_overlap=True):
153162
if state.strict_optional:
154163
return UninhabitedType()
@@ -777,7 +786,9 @@ def visit_erased_type(self, t: ErasedType) -> ProperType:
777786

778787
def visit_type_var(self, t: TypeVarType) -> ProperType:
779788
if isinstance(self.s, TypeVarType) and self.s.id == t.id:
780-
return self.s
789+
if self.s.upper_bound == t.upper_bound:
790+
return self.s
791+
return self.s.copy_modified(upper_bound=self.meet(self.s.upper_bound, t.upper_bound))
781792
else:
782793
return self.default(self.s)
783794

mypy/subtypes.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -632,7 +632,14 @@ def visit_instance(self, left: Instance) -> bool:
632632
def visit_type_var(self, left: TypeVarType) -> bool:
633633
right = self.right
634634
if isinstance(right, TypeVarType) and left.id == right.id:
635-
return True
635+
# Fast path for most common case.
636+
if left.upper_bound == right.upper_bound:
637+
return True
638+
# Corner case for self-types in classes generic in type vars
639+
# with value restrictions.
640+
if left.id.is_self():
641+
return True
642+
return self._is_subtype(left.upper_bound, right.upper_bound)
636643
if left.values and self._is_subtype(UnionType.make_union(left.values), right):
637644
return True
638645
return self._is_subtype(left.upper_bound, self.right)

mypy/typeops.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -415,10 +415,10 @@ class B(A): pass
415415
]
416416
return cast(F, Overloaded(items))
417417
assert isinstance(method, CallableType)
418-
func = method
418+
func: CallableType = method
419419
if not func.arg_types:
420420
# Invalid method, return something.
421-
return cast(F, func)
421+
return method
422422
if func.arg_kinds[0] in (ARG_STAR, ARG_STAR2):
423423
# The signature is of the form 'def foo(*args, ...)'.
424424
# In this case we shouldn't drop the first arg,
@@ -427,7 +427,7 @@ class B(A): pass
427427

428428
# In the case of **kwargs we should probably emit an error, but
429429
# for now we simply skip it, to avoid crashes down the line.
430-
return cast(F, func)
430+
return method
431431
self_param_type = get_proper_type(func.arg_types[0])
432432

433433
variables: Sequence[TypeVarLikeType]

mypy/types.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,11 @@ def __init__(self, type_guard: Type) -> None:
461461
def __repr__(self) -> str:
462462
return f"TypeGuard({self.type_guard})"
463463

464+
# This may hide some real bugs, but it is convenient for various "synthetic"
465+
# visitors, similar to RequiredType and ReadOnlyType below.
466+
def accept(self, visitor: TypeVisitor[T]) -> T:
467+
return self.type_guard.accept(visitor)
468+
464469

465470
class RequiredType(Type):
466471
"""Required[T] or NotRequired[T]. Only usable at top-level of a TypedDict definition."""

mypyc/test-data/run-classes.test

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2983,3 +2983,31 @@ class B(native.A):
29832983

29842984
b: B = B.make()
29852985
assert(B.count == 2)
2986+
2987+
[case testTypeVarNarrowing]
2988+
from typing import TypeVar
2989+
2990+
class B:
2991+
def __init__(self, x: int) -> None:
2992+
self.x = x
2993+
class C(B):
2994+
def __init__(self, x: int, y: str) -> None:
2995+
self.x = x
2996+
self.y = y
2997+
2998+
T = TypeVar("T", bound=B)
2999+
def f(x: T) -> T:
3000+
if isinstance(x, C):
3001+
print("C", x.y)
3002+
return x
3003+
print("B", x.x)
3004+
return x
3005+
3006+
[file driver.py]
3007+
from native import f, B, C
3008+
3009+
f(B(1))
3010+
f(C(1, "yes"))
3011+
[out]
3012+
B 1
3013+
C yes

test-data/unit/check-classes.test

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6891,24 +6891,21 @@ reveal_type(i.x) # N: Revealed type is "builtins.int"
68916891
[builtins fixtures/isinstancelist.pyi]
68926892

68936893
[case testIsInstanceTypeTypeVar]
6894-
from typing import Type, TypeVar, Generic
6894+
from typing import Type, TypeVar, Generic, ClassVar
68956895

68966896
class Base: ...
6897-
class Sub(Base): ...
6897+
class Sub(Base):
6898+
other: ClassVar[int]
68986899

68996900
T = TypeVar('T', bound=Base)
69006901

69016902
class C(Generic[T]):
69026903
def meth(self, cls: Type[T]) -> None:
69036904
if not issubclass(cls, Sub):
69046905
return
6905-
reveal_type(cls) # N: Revealed type is "type[__main__.Sub]"
6906-
def other(self, cls: Type[T]) -> None:
6907-
if not issubclass(cls, Sub):
6908-
return
6909-
reveal_type(cls) # N: Revealed type is "type[__main__.Sub]"
6910-
6911-
[builtins fixtures/isinstancelist.pyi]
6906+
reveal_type(cls) # N: Revealed type is "type[T`1]"
6907+
reveal_type(cls.other) # N: Revealed type is "builtins.int"
6908+
[builtins fixtures/isinstance.pyi]
69126909

69136910
[case testIsInstanceTypeSubclass]
69146911
from typing import Type, Optional
@@ -7602,7 +7599,7 @@ class C1:
76027599
class C2(Generic[TypeT]):
76037600
def method(self, other: TypeT) -> int:
76047601
if issubclass(other, Base):
7605-
reveal_type(other) # N: Revealed type is "type[__main__.Base]"
7602+
reveal_type(other) # N: Revealed type is "TypeT`1"
76067603
return other.field
76077604
return 0
76087605

test-data/unit/check-isinstance.test

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1821,19 +1821,23 @@ if issubclass(fm, Baz):
18211821
from typing import TypeVar
18221822

18231823
class A: pass
1824-
class B(A): pass
1824+
class B(A):
1825+
attr: int
18251826

18261827
T = TypeVar('T', bound=A)
18271828

18281829
def f(x: T) -> None:
18291830
if isinstance(x, B):
1830-
reveal_type(x) # N: Revealed type is "__main__.B"
1831+
reveal_type(x) # N: Revealed type is "T`-1"
1832+
reveal_type(x.attr) # N: Revealed type is "builtins.int"
18311833
else:
18321834
reveal_type(x) # N: Revealed type is "T`-1"
1835+
x.attr # E: "T" has no attribute "attr"
18331836
reveal_type(x) # N: Revealed type is "T`-1"
1837+
x.attr # E: "T" has no attribute "attr"
18341838
[builtins fixtures/isinstance.pyi]
18351839

1836-
[case testIsinstanceAndNegativeNarrowTypeVariableWithUnionBound]
1840+
[case testIsinstanceAndNegativeNarrowTypeVariableWithUnionBound1]
18371841
from typing import Union, TypeVar
18381842

18391843
class A:
@@ -1845,9 +1849,11 @@ T = TypeVar("T", bound=Union[A, B])
18451849

18461850
def f(x: T) -> T:
18471851
if isinstance(x, A):
1848-
reveal_type(x) # N: Revealed type is "__main__.A"
1852+
reveal_type(x) # N: Revealed type is "T`-1"
18491853
x.a
1850-
x.b # E: "A" has no attribute "b"
1854+
x.b # E: "T" has no attribute "b"
1855+
if bool():
1856+
return x
18511857
else:
18521858
reveal_type(x) # N: Revealed type is "T`-1"
18531859
x.a # E: "T" has no attribute "a"
@@ -1857,6 +1863,24 @@ def f(x: T) -> T:
18571863
return x
18581864
[builtins fixtures/isinstance.pyi]
18591865

1866+
[case testIsinstanceAndNegativeNarrowTypeVariableWithUnionBound2]
1867+
from typing import Union, TypeVar
1868+
1869+
class A:
1870+
a: int
1871+
class B:
1872+
b: int
1873+
1874+
T = TypeVar("T", bound=Union[A, B])
1875+
1876+
def f(x: T) -> T:
1877+
if isinstance(x, A):
1878+
return x
1879+
x.a # E: "T" has no attribute "a"
1880+
x.b # OK
1881+
return x
1882+
[builtins fixtures/isinstance.pyi]
1883+
18601884
[case testIsinstanceAndTypeType]
18611885
from typing import Type
18621886
def f(x: Type[int]) -> None:

test-data/unit/check-narrowing.test

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2424,3 +2424,42 @@ def f() -> None:
24242424
assert isinstance(x, int)
24252425
reveal_type(x) # N: Revealed type is "builtins.int"
24262426
[builtins fixtures/isinstance.pyi]
2427+
2428+
[case testNarrowTypeVarBoundType]
2429+
from typing import Type, TypeVar
2430+
2431+
class A: ...
2432+
class B(A):
2433+
other: int
2434+
2435+
T = TypeVar("T", bound=A)
2436+
def test(cls: Type[T]) -> T:
2437+
if issubclass(cls, B):
2438+
reveal_type(cls) # N: Revealed type is "type[T`-1]"
2439+
reveal_type(cls().other) # N: Revealed type is "builtins.int"
2440+
return cls()
2441+
return cls()
2442+
[builtins fixtures/isinstance.pyi]
2443+
2444+
[case testNarrowTypeVarBoundUnion]
2445+
from typing import TypeVar
2446+
2447+
class A:
2448+
x: int
2449+
class B:
2450+
x: str
2451+
2452+
T = TypeVar("T")
2453+
def test(x: T) -> T:
2454+
if not isinstance(x, (A, B)):
2455+
return x
2456+
reveal_type(x) # N: Revealed type is "T`-1"
2457+
reveal_type(x.x) # N: Revealed type is "Union[builtins.int, builtins.str]"
2458+
if isinstance(x, A):
2459+
reveal_type(x) # N: Revealed type is "T`-1"
2460+
reveal_type(x.x) # N: Revealed type is "builtins.int"
2461+
return x
2462+
reveal_type(x) # N: Revealed type is "T`-1"
2463+
reveal_type(x.x) # N: Revealed type is "builtins.str"
2464+
return x
2465+
[builtins fixtures/isinstance.pyi]

test-data/unit/check-typeguard.test

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,23 @@ def handle(model: Model) -> int:
778778
return 0
779779
[builtins fixtures/tuple.pyi]
780780

781+
[case testTypeGuardRestrictTypeVarUnion]
782+
from typing import Union, TypeVar
783+
from typing_extensions import TypeGuard
784+
785+
class A:
786+
x: int
787+
class B:
788+
x: str
789+
790+
def is_b(x: object) -> TypeGuard[B]: ...
791+
792+
T = TypeVar("T")
793+
def test(x: T) -> T:
794+
if isinstance(x, A) or is_b(x):
795+
reveal_type(x.x) # N: Revealed type is "Union[builtins.int, builtins.str]"
796+
return x
797+
[builtins fixtures/isinstance.pyi]
781798

782799
[case testOverloadedTypeGuardType]
783800
from __future__ import annotations

0 commit comments

Comments
 (0)