Skip to content

Generalize class/static method and property alias support #19297

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 29 additions & 7 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -4400,9 +4400,9 @@ def set_inferred_type(self, var: Var, lvalue: Lvalue, type: Type) -> None:
refers to the variable (lvalue). If var is None, do nothing.
"""
if var and not self.current_node_deferred:
# TODO: should we also set 'is_ready = True' here?
var.type = type
var.is_inferred = True
var.is_ready = True
if var not in self.var_decl_frames:
# Used for the hack to improve optional type inference in conditionals
self.var_decl_frames[var] = {frame.id for frame in self.binder.frames}
Expand All @@ -4412,9 +4412,23 @@ def set_inferred_type(self, var: Var, lvalue: Lvalue, type: Type) -> None:
self.inferred_attribute_types[lvalue.def_var] = type
self.store_type(lvalue, type)
p_type = get_proper_type(type)
if isinstance(p_type, CallableType) and is_node_static(p_type.definition):
# TODO: handle aliases to class methods (similarly).
var.is_staticmethod = True
definition = None
if isinstance(p_type, CallableType):
definition = p_type.definition
elif isinstance(p_type, Overloaded):
# Randomly select first item, if items are different, there will
# be an error during semantic analysis.
definition = p_type.items[0].definition
if definition:
if is_node_static(definition):
var.is_staticmethod = True
elif is_classmethod_node(definition):
var.is_classmethod = True
elif is_property(definition):
var.is_property = True
if isinstance(p_type, Overloaded):
# TODO: in theory we can have a property with a deleter only.
var.is_settable_property = True

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


def is_classmethod_node(node: Node | None) -> bool | None:
"""Find out if a node describes a classmethod."""
if isinstance(node, FuncDef):
return node.is_class
if isinstance(node, Var):
return node.is_classmethod
return None


def is_node_static(node: Node | None) -> bool | None:
"""Find out if a node describes a static function method."""

if isinstance(node, FuncDef):
return node.is_static

if isinstance(node, Var):
return node.is_staticmethod

return None


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


def is_property(defn: SymbolNode) -> bool:
if isinstance(defn, FuncDef):
return defn.is_property
if isinstance(defn, Decorator):
return defn.func.is_property
if isinstance(defn, OverloadedFuncDef):
Expand Down
18 changes: 12 additions & 6 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,8 +371,6 @@ def analyze_instance_member_access(
signature, mx.self_type, method.is_class, mx.context, name, mx.msg
)
signature = bind_self(signature, mx.self_type, is_classmethod=method.is_class)
# TODO: should we skip these steps for static methods as well?
# Since generic static methods should not be allowed.
typ = map_instance_to_supertype(typ, method.info)
member_type = expand_type_by_instance(signature, typ)
freeze_all_type_vars(member_type)
Expand Down Expand Up @@ -1218,8 +1216,11 @@ def analyze_class_attribute_access(
# C[int].x -> int
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, SYMBOL_FUNCBASE_TYPES) and node.node.is_class
is_classmethod = (
(is_decorated and cast(Decorator, node.node).func.is_class)
or (isinstance(node.node, SYMBOL_FUNCBASE_TYPES) and node.node.is_class)
or isinstance(node.node, Var)
and node.node.is_classmethod
)
is_staticmethod = (is_decorated and cast(Decorator, node.node).func.is_static) or (
isinstance(node.node, SYMBOL_FUNCBASE_TYPES) and node.node.is_static
Expand All @@ -1231,7 +1232,12 @@ def analyze_class_attribute_access(
is_trivial_self = node.node.func.is_trivial_self and not node.node.decorators
elif isinstance(node.node, (FuncDef, OverloadedFuncDef)):
is_trivial_self = node.node.is_trivial_self
if isinstance(t, FunctionLike) and is_classmethod and not is_trivial_self:
if (
isinstance(t, FunctionLike)
and is_classmethod
and not is_trivial_self
and not t.bound()
):
t = check_self_arg(t, mx.self_type, False, mx.context, name, mx.msg)
t = add_class_tvars(
t,
Expand Down Expand Up @@ -1400,7 +1406,7 @@ class B(A[str]): pass
tvars = original_vars if original_vars is not None else []
if not mx.preserve_type_var_ids:
t = freshen_all_functions_type_vars(t)
if is_classmethod:
if is_classmethod and not t.is_bound:
if is_trivial_self:
t = bind_self_fast(t, mx.self_type)
else:
Expand Down
48 changes: 48 additions & 0 deletions test-data/unit/check-callable.test
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,54 @@ class C(B):
class B: ...
[builtins fixtures/classmethod.pyi]

[case testClassMethodAliasInClass]
from typing import overload

class C:
@classmethod
def foo(cls) -> int: ...

bar = foo

@overload
@classmethod
def foo2(cls, x: int) -> int: ...
@overload
@classmethod
def foo2(cls, x: str) -> str: ...
@classmethod
def foo2(cls, x):
...

bar2 = foo2
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth it to also test static methods? Did they already work?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static methods already worked, but not overloaded ones. I added a test specifically for overloaded static method.


reveal_type(C.bar) # N: Revealed type is "def () -> builtins.int"
reveal_type(C().bar) # N: Revealed type is "def () -> builtins.int"
reveal_type(C.bar2) # N: Revealed type is "Overload(def (x: builtins.int) -> builtins.int, def (x: builtins.str) -> builtins.str)"
reveal_type(C().bar2) # N: Revealed type is "Overload(def (x: builtins.int) -> builtins.int, def (x: builtins.str) -> builtins.str)"
[builtins fixtures/classmethod.pyi]

[case testPropertyAliasInClassBody]
class A:
@property
def f(self) -> int: ...

g = f

@property
def f2(self) -> int: ...
@f2.setter
def f2(self, val: int) -> None: ...

g2 = f2

reveal_type(A().g) # N: Revealed type is "builtins.int"
reveal_type(A().g2) # N: Revealed type is "builtins.int"
A().g = 1 # E: Property "g" defined in "A" is read-only
A().g2 = 1
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test assignment to A().g (should generate an error).

A().g2 = "no" # E: Incompatible types in assignment (expression has type "str", variable has type "int")
[builtins fixtures/property.pyi]

[case testCallableUnionCallback]
from typing import Union, Callable, TypeVar

Expand Down
17 changes: 17 additions & 0 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -4588,6 +4588,23 @@ reveal_type(a.a) # N: Revealed type is "def (a: builtins.int)"
reveal_type(a.c) # N: Revealed type is "def (a: builtins.int)"
[builtins fixtures/staticmethod.pyi]

[case testClassStaticMethodIndirectOverloaded]
from typing import overload
class A:
@overload
@staticmethod
def a(x: int) -> int: ...
@overload
@staticmethod
def a(x: str) -> str: ...
@staticmethod
def a(x):
...
c = a
reveal_type(A.c) # N: Revealed type is "Overload(def (x: builtins.int) -> builtins.int, def (x: builtins.str) -> builtins.str)"
reveal_type(A().c) # N: Revealed type is "Overload(def (x: builtins.int) -> builtins.int, def (x: builtins.str) -> builtins.str)"
[builtins fixtures/staticmethod.pyi]

[case testClassStaticMethodSubclassing]
class A:
@staticmethod
Expand Down