Skip to content

Commit 9f455bd

Browse files
authored
Generalize class/static method and property alias support (#19297)
Fixes #6700 This is another followup for the `checkmember` work. Currently, we only support non-instance method aliasing in few very specific cases. I am making this support general.
1 parent ae778cc commit 9f455bd

File tree

4 files changed

+106
-13
lines changed

4 files changed

+106
-13
lines changed

mypy/checker.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4400,9 +4400,9 @@ def set_inferred_type(self, var: Var, lvalue: Lvalue, type: Type) -> None:
44004400
refers to the variable (lvalue). If var is None, do nothing.
44014401
"""
44024402
if var and not self.current_node_deferred:
4403-
# TODO: should we also set 'is_ready = True' here?
44044403
var.type = type
44054404
var.is_inferred = True
4405+
var.is_ready = True
44064406
if var not in self.var_decl_frames:
44074407
# Used for the hack to improve optional type inference in conditionals
44084408
self.var_decl_frames[var] = {frame.id for frame in self.binder.frames}
@@ -4412,9 +4412,23 @@ def set_inferred_type(self, var: Var, lvalue: Lvalue, type: Type) -> None:
44124412
self.inferred_attribute_types[lvalue.def_var] = type
44134413
self.store_type(lvalue, type)
44144414
p_type = get_proper_type(type)
4415-
if isinstance(p_type, CallableType) and is_node_static(p_type.definition):
4416-
# TODO: handle aliases to class methods (similarly).
4417-
var.is_staticmethod = True
4415+
definition = None
4416+
if isinstance(p_type, CallableType):
4417+
definition = p_type.definition
4418+
elif isinstance(p_type, Overloaded):
4419+
# Randomly select first item, if items are different, there will
4420+
# be an error during semantic analysis.
4421+
definition = p_type.items[0].definition
4422+
if definition:
4423+
if is_node_static(definition):
4424+
var.is_staticmethod = True
4425+
elif is_classmethod_node(definition):
4426+
var.is_classmethod = True
4427+
elif is_property(definition):
4428+
var.is_property = True
4429+
if isinstance(p_type, Overloaded):
4430+
# TODO: in theory we can have a property with a deleter only.
4431+
var.is_settable_property = True
44184432

44194433
def set_inference_error_fallback_type(self, var: Var, lvalue: Lvalue, type: Type) -> None:
44204434
"""Store best known type for variable if type inference failed.
@@ -8531,15 +8545,21 @@ def visit_type_alias_type(self, t: TypeAliasType) -> Type:
85318545
return t.copy_modified(args=[a.accept(self) for a in t.args])
85328546

85338547

8548+
def is_classmethod_node(node: Node | None) -> bool | None:
8549+
"""Find out if a node describes a classmethod."""
8550+
if isinstance(node, FuncDef):
8551+
return node.is_class
8552+
if isinstance(node, Var):
8553+
return node.is_classmethod
8554+
return None
8555+
8556+
85348557
def is_node_static(node: Node | None) -> bool | None:
85358558
"""Find out if a node describes a static function method."""
8536-
85378559
if isinstance(node, FuncDef):
85388560
return node.is_static
8539-
85408561
if isinstance(node, Var):
85418562
return node.is_staticmethod
8542-
85438563
return None
85448564

85458565

@@ -8786,6 +8806,8 @@ def is_static(func: FuncBase | Decorator) -> bool:
87868806

87878807

87888808
def is_property(defn: SymbolNode) -> bool:
8809+
if isinstance(defn, FuncDef):
8810+
return defn.is_property
87898811
if isinstance(defn, Decorator):
87908812
return defn.func.is_property
87918813
if isinstance(defn, OverloadedFuncDef):

mypy/checkmember.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -371,8 +371,6 @@ def analyze_instance_member_access(
371371
signature, mx.self_type, method.is_class, mx.context, name, mx.msg
372372
)
373373
signature = bind_self(signature, mx.self_type, is_classmethod=method.is_class)
374-
# TODO: should we skip these steps for static methods as well?
375-
# Since generic static methods should not be allowed.
376374
typ = map_instance_to_supertype(typ, method.info)
377375
member_type = expand_type_by_instance(signature, typ)
378376
freeze_all_type_vars(member_type)
@@ -1224,8 +1222,11 @@ def analyze_class_attribute_access(
12241222
# C[int].x -> int
12251223
t = erase_typevars(expand_type_by_instance(t, isuper), {tv.id for tv in def_vars})
12261224

1227-
is_classmethod = (is_decorated and cast(Decorator, node.node).func.is_class) or (
1228-
isinstance(node.node, SYMBOL_FUNCBASE_TYPES) and node.node.is_class
1225+
is_classmethod = (
1226+
(is_decorated and cast(Decorator, node.node).func.is_class)
1227+
or (isinstance(node.node, SYMBOL_FUNCBASE_TYPES) and node.node.is_class)
1228+
or isinstance(node.node, Var)
1229+
and node.node.is_classmethod
12291230
)
12301231
is_staticmethod = (is_decorated and cast(Decorator, node.node).func.is_static) or (
12311232
isinstance(node.node, SYMBOL_FUNCBASE_TYPES) and node.node.is_static
@@ -1237,7 +1238,12 @@ def analyze_class_attribute_access(
12371238
is_trivial_self = node.node.func.is_trivial_self and not node.node.decorators
12381239
elif isinstance(node.node, (FuncDef, OverloadedFuncDef)):
12391240
is_trivial_self = node.node.is_trivial_self
1240-
if isinstance(t, FunctionLike) and is_classmethod and not is_trivial_self:
1241+
if (
1242+
isinstance(t, FunctionLike)
1243+
and is_classmethod
1244+
and not is_trivial_self
1245+
and not t.bound()
1246+
):
12411247
t = check_self_arg(t, mx.self_type, False, mx.context, name, mx.msg)
12421248
t = add_class_tvars(
12431249
t,
@@ -1406,7 +1412,7 @@ class B(A[str]): pass
14061412
tvars = original_vars if original_vars is not None else []
14071413
if not mx.preserve_type_var_ids:
14081414
t = freshen_all_functions_type_vars(t)
1409-
if is_classmethod:
1415+
if is_classmethod and not t.is_bound:
14101416
if is_trivial_self:
14111417
t = bind_self_fast(t, mx.self_type)
14121418
else:

test-data/unit/check-callable.test

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,54 @@ class C(B):
587587
class B: ...
588588
[builtins fixtures/classmethod.pyi]
589589

590+
[case testClassMethodAliasInClass]
591+
from typing import overload
592+
593+
class C:
594+
@classmethod
595+
def foo(cls) -> int: ...
596+
597+
bar = foo
598+
599+
@overload
600+
@classmethod
601+
def foo2(cls, x: int) -> int: ...
602+
@overload
603+
@classmethod
604+
def foo2(cls, x: str) -> str: ...
605+
@classmethod
606+
def foo2(cls, x):
607+
...
608+
609+
bar2 = foo2
610+
611+
reveal_type(C.bar) # N: Revealed type is "def () -> builtins.int"
612+
reveal_type(C().bar) # N: Revealed type is "def () -> builtins.int"
613+
reveal_type(C.bar2) # N: Revealed type is "Overload(def (x: builtins.int) -> builtins.int, def (x: builtins.str) -> builtins.str)"
614+
reveal_type(C().bar2) # N: Revealed type is "Overload(def (x: builtins.int) -> builtins.int, def (x: builtins.str) -> builtins.str)"
615+
[builtins fixtures/classmethod.pyi]
616+
617+
[case testPropertyAliasInClassBody]
618+
class A:
619+
@property
620+
def f(self) -> int: ...
621+
622+
g = f
623+
624+
@property
625+
def f2(self) -> int: ...
626+
@f2.setter
627+
def f2(self, val: int) -> None: ...
628+
629+
g2 = f2
630+
631+
reveal_type(A().g) # N: Revealed type is "builtins.int"
632+
reveal_type(A().g2) # N: Revealed type is "builtins.int"
633+
A().g = 1 # E: Property "g" defined in "A" is read-only
634+
A().g2 = 1
635+
A().g2 = "no" # E: Incompatible types in assignment (expression has type "str", variable has type "int")
636+
[builtins fixtures/property.pyi]
637+
590638
[case testCallableUnionCallback]
591639
from typing import Union, Callable, TypeVar
592640

test-data/unit/check-classes.test

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4588,6 +4588,23 @@ reveal_type(a.a) # N: Revealed type is "def (a: builtins.int)"
45884588
reveal_type(a.c) # N: Revealed type is "def (a: builtins.int)"
45894589
[builtins fixtures/staticmethod.pyi]
45904590

4591+
[case testClassStaticMethodIndirectOverloaded]
4592+
from typing import overload
4593+
class A:
4594+
@overload
4595+
@staticmethod
4596+
def a(x: int) -> int: ...
4597+
@overload
4598+
@staticmethod
4599+
def a(x: str) -> str: ...
4600+
@staticmethod
4601+
def a(x):
4602+
...
4603+
c = a
4604+
reveal_type(A.c) # N: Revealed type is "Overload(def (x: builtins.int) -> builtins.int, def (x: builtins.str) -> builtins.str)"
4605+
reveal_type(A().c) # N: Revealed type is "Overload(def (x: builtins.int) -> builtins.int, def (x: builtins.str) -> builtins.str)"
4606+
[builtins fixtures/staticmethod.pyi]
4607+
45914608
[case testClassStaticMethodSubclassing]
45924609
class A:
45934610
@staticmethod

0 commit comments

Comments
 (0)