Skip to content

Commit 401798f

Browse files
authored
Correctly support self types in callable ClassVar (#14115)
Fixes #14108 This fixes both new and old style of working with self types. After all I fixed the new style by simply expanding self type, then `bind_self()` does its job, so effect on the instance will be the same. I had two options fixing this, other one (that I didn't go with) is making the callable generic in new style, if it appears in `ClassVar`. This however has two downsides: implementation is tricky, and this adds and edge case to an existing edge case. So instead I choose internal consistency within the new style, rather than similarity between old and new style.
1 parent 823667d commit 401798f

File tree

3 files changed

+36
-12
lines changed

3 files changed

+36
-12
lines changed

mypy/checkmember.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
class_callable,
4141
erase_to_bound,
4242
function_type,
43+
get_type_vars,
4344
make_simplified_union,
4445
supported_self_type,
4546
tuple_fallback,
@@ -68,7 +69,6 @@
6869
TypeVarType,
6970
UnionType,
7071
get_proper_type,
71-
has_type_vars,
7272
)
7373
from mypy.typetraverser import TypeTraverserVisitor
7474

@@ -767,6 +767,9 @@ def analyze_var(
767767
# and similarly for B1 when checking against B
768768
dispatched_type = meet.meet_types(mx.original_type, itype)
769769
signature = freshen_all_functions_type_vars(functype)
770+
bound = get_proper_type(expand_self_type(var, signature, mx.original_type))
771+
assert isinstance(bound, FunctionLike)
772+
signature = bound
770773
signature = check_self_arg(
771774
signature, dispatched_type, var.is_classmethod, mx.context, name, mx.msg
772775
)
@@ -960,11 +963,11 @@ def analyze_class_attribute_access(
960963
# C.x # Error, ambiguous access
961964
# C[int].x # Also an error, since C[int] is same as C at runtime
962965
# Exception is Self type wrapped in ClassVar, that is safe.
963-
if node.node.info.self_type is not None and node.node.is_classvar:
964-
exclude = node.node.info.self_type.id
965-
else:
966-
exclude = None
967-
if isinstance(t, TypeVarType) and t.id != exclude or has_type_vars(t, exclude):
966+
def_vars = set(node.node.info.defn.type_vars)
967+
if not node.node.is_classvar and node.node.info.self_type:
968+
def_vars.add(node.node.info.self_type)
969+
typ_vars = set(get_type_vars(t))
970+
if def_vars & typ_vars:
968971
# Exception: access on Type[...], including first argument of class methods is OK.
969972
if not isinstance(get_proper_type(mx.original_type), TypeType) or node.implicit:
970973
if node.node.is_classvar:
@@ -978,7 +981,7 @@ def analyze_class_attribute_access(
978981
# C.x -> Any
979982
# C[int].x -> int
980983
t = get_proper_type(expand_self_type(node.node, t, itype))
981-
t = erase_typevars(expand_type_by_instance(t, isuper))
984+
t = erase_typevars(expand_type_by_instance(t, isuper), {tv.id for tv in def_vars})
982985

983986
is_classmethod = (is_decorated and cast(Decorator, node.node).func.is_class) or (
984987
isinstance(node.node, FuncBase) and node.node.is_class

mypy/types.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3236,12 +3236,11 @@ def replace_alias_tvars(
32363236

32373237

32383238
class HasTypeVars(TypeQuery[bool]):
3239-
def __init__(self, exclude: TypeVarId | None = None) -> None:
3239+
def __init__(self) -> None:
32403240
super().__init__(any)
3241-
self.exclude = exclude
32423241

32433242
def visit_type_var(self, t: TypeVarType) -> bool:
3244-
return t.id != self.exclude
3243+
return True
32453244

32463245
def visit_type_var_tuple(self, t: TypeVarTupleType) -> bool:
32473246
return True
@@ -3250,9 +3249,9 @@ def visit_param_spec(self, t: ParamSpecType) -> bool:
32503249
return True
32513250

32523251

3253-
def has_type_vars(typ: Type, exclude: TypeVarId | None = None) -> bool:
3252+
def has_type_vars(typ: Type) -> bool:
32543253
"""Check if a type contains any type variables (recursively)."""
3255-
return typ.accept(HasTypeVars(exclude))
3254+
return typ.accept(HasTypeVars())
32563255

32573256

32583257
class HasRecursiveType(TypeQuery[bool]):

test-data/unit/check-selftype.test

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1750,3 +1750,25 @@ from typing import Self, final
17501750
class C:
17511751
def meth(self) -> Self:
17521752
return C() # OK for final classes
1753+
1754+
[case testTypingSelfCallableClassVar]
1755+
from typing import Self, ClassVar, Callable, TypeVar
1756+
1757+
class C:
1758+
f: ClassVar[Callable[[Self], Self]]
1759+
class D(C): ...
1760+
1761+
reveal_type(D.f) # N: Revealed type is "def (__main__.D) -> __main__.D"
1762+
reveal_type(D().f) # N: Revealed type is "def () -> __main__.D"
1763+
1764+
[case testSelfTypeCallableClassVarOldStyle]
1765+
from typing import ClassVar, Callable, TypeVar
1766+
1767+
T = TypeVar("T")
1768+
class C:
1769+
f: ClassVar[Callable[[T], T]]
1770+
1771+
class D(C): ...
1772+
1773+
reveal_type(D.f) # N: Revealed type is "def [T] (T`-1) -> T`-1"
1774+
reveal_type(D().f) # N: Revealed type is "def () -> __main__.D"

0 commit comments

Comments
 (0)