Skip to content

Commit

Permalink
Correctly support self types in callable ClassVar (#14115)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ilevkivskyi authored Nov 17, 2022
1 parent 823667d commit 401798f
Show file tree
Hide file tree
Showing 3 changed files with 36 additions and 12 deletions.
17 changes: 10 additions & 7 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
class_callable,
erase_to_bound,
function_type,
get_type_vars,
make_simplified_union,
supported_self_type,
tuple_fallback,
Expand Down Expand Up @@ -68,7 +69,6 @@
TypeVarType,
UnionType,
get_proper_type,
has_type_vars,
)
from mypy.typetraverser import TypeTraverserVisitor

Expand Down Expand Up @@ -767,6 +767,9 @@ def analyze_var(
# and similarly for B1 when checking against B
dispatched_type = meet.meet_types(mx.original_type, itype)
signature = freshen_all_functions_type_vars(functype)
bound = get_proper_type(expand_self_type(var, signature, mx.original_type))
assert isinstance(bound, FunctionLike)
signature = bound
signature = check_self_arg(
signature, dispatched_type, var.is_classmethod, mx.context, name, mx.msg
)
Expand Down Expand Up @@ -960,11 +963,11 @@ def analyze_class_attribute_access(
# C.x # Error, ambiguous access
# C[int].x # Also an error, since C[int] is same as C at runtime
# Exception is Self type wrapped in ClassVar, that is safe.
if node.node.info.self_type is not None and node.node.is_classvar:
exclude = node.node.info.self_type.id
else:
exclude = None
if isinstance(t, TypeVarType) and t.id != exclude or has_type_vars(t, exclude):
def_vars = set(node.node.info.defn.type_vars)
if not node.node.is_classvar and node.node.info.self_type:
def_vars.add(node.node.info.self_type)
typ_vars = set(get_type_vars(t))
if def_vars & typ_vars:
# Exception: access on Type[...], including first argument of class methods is OK.
if not isinstance(get_proper_type(mx.original_type), TypeType) or node.implicit:
if node.node.is_classvar:
Expand All @@ -978,7 +981,7 @@ def analyze_class_attribute_access(
# C.x -> Any
# C[int].x -> int
t = get_proper_type(expand_self_type(node.node, t, itype))
t = erase_typevars(expand_type_by_instance(t, isuper))
t = erase_typevars(expand_type_by_instance(t, isuper), {tv.id for tv in def_vars})

is_classmethod = (is_decorated and cast(Decorator, node.node).func.is_class) or (
isinstance(node.node, FuncBase) and node.node.is_class
Expand Down
9 changes: 4 additions & 5 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -3236,12 +3236,11 @@ def replace_alias_tvars(


class HasTypeVars(TypeQuery[bool]):
def __init__(self, exclude: TypeVarId | None = None) -> None:
def __init__(self) -> None:
super().__init__(any)
self.exclude = exclude

def visit_type_var(self, t: TypeVarType) -> bool:
return t.id != self.exclude
return True

def visit_type_var_tuple(self, t: TypeVarTupleType) -> bool:
return True
Expand All @@ -3250,9 +3249,9 @@ def visit_param_spec(self, t: ParamSpecType) -> bool:
return True


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


class HasRecursiveType(TypeQuery[bool]):
Expand Down
22 changes: 22 additions & 0 deletions test-data/unit/check-selftype.test
Original file line number Diff line number Diff line change
Expand Up @@ -1750,3 +1750,25 @@ from typing import Self, final
class C:
def meth(self) -> Self:
return C() # OK for final classes

[case testTypingSelfCallableClassVar]
from typing import Self, ClassVar, Callable, TypeVar

class C:
f: ClassVar[Callable[[Self], Self]]
class D(C): ...

reveal_type(D.f) # N: Revealed type is "def (__main__.D) -> __main__.D"
reveal_type(D().f) # N: Revealed type is "def () -> __main__.D"

[case testSelfTypeCallableClassVarOldStyle]
from typing import ClassVar, Callable, TypeVar

T = TypeVar("T")
class C:
f: ClassVar[Callable[[T], T]]

class D(C): ...

reveal_type(D.f) # N: Revealed type is "def [T] (T`-1) -> T`-1"
reveal_type(D().f) # N: Revealed type is "def () -> __main__.D"

0 comments on commit 401798f

Please sign in to comment.