Skip to content

Commit 57f8da3

Browse files
authored
Ensure type of classmethod is consistent regardless of whether cls is annotated. (#87)
For example ```py class C: @classmethod def f(cls) -> int: ... @classmethod def g(cls: type[C]) -> int: ... ``` The result of `GetMemberType` on each function was: - `f`: `classmethod[C, tuple[()], int]` - `g`: `classmethod[type[C], tuple[()], int]`
1 parent 38ad82b commit 57f8da3

2 files changed

Lines changed: 27 additions & 5 deletions

File tree

tests/test_type_eval.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,23 @@ def test_type_getattr_union_5():
228228
assert d == int | list[float] | TB
229229

230230

231+
def test_getmembertype_classmethod_01():
232+
class C:
233+
@classmethod
234+
def f(cls, x: int) -> int: ...
235+
@classmethod
236+
def g(cls: type[C], x: int) -> int: ...
237+
@classmethod
238+
def h(cls: type[Self], x: int) -> int: ...
239+
240+
d = eval_typing(GetMemberType[C, Literal["f"]])
241+
assert d == classmethod[C, tuple[Param[Literal["x"], int]], int]
242+
d = eval_typing(GetMemberType[C, Literal["g"]])
243+
assert d == classmethod[C, tuple[Param[Literal["x"], int]], int]
244+
d = eval_typing(GetMemberType[C, Literal["h"]])
245+
assert d == classmethod[Self, tuple[Param[Literal["x"], int]], int]
246+
247+
231248
def test_type_strings_1():
232249
d = eval_typing(Uppercase[Literal["foo"]])
233250
assert d == Literal["FOO"]
@@ -1642,7 +1659,7 @@ def test_new_protocol_with_methods_02():
16421659
],
16431660
Member[
16441661
Literal["class_method"],
1645-
classmethod[type[Self], tuple[Param[Literal["x"], int]], int],
1662+
classmethod[Self, tuple[Param[Literal["x"], int]], int],
16461663
Literal["ClassVar"],
16471664
],
16481665
Member[

typemap/type_eval/_eval_operators.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -599,9 +599,6 @@ def _callable_type_to_method(name, typ, ctx):
599599
# For __init_subclass__ generic on cls: T, keep type[T]
600600
cls_typ = type[cls] # type: ignore[name-defined]
601601
else:
602-
# An annoying thing to know is that for a member classmethod of C,
603-
# cls *should* be type[C], but if it was not explicitly annotated,
604-
# it will be C.
605602
cls_typ = type[typing.Self] # type: ignore[name-defined]
606603
cls_param = Param[typing.Literal["cls"], cls_typ, quals]
607604
typ = typing.Callable[[cls_param] + list(typing.get_args(params)), ret]
@@ -658,7 +655,15 @@ def _ann(x):
658655
if ann is empty:
659656
ann = receiver_type
660657
else:
661-
specified_receiver = ann
658+
if (
659+
isinstance(func, classmethod)
660+
and typing.get_origin(ann) is type
661+
and (receiver_args := typing.get_args(ann))
662+
):
663+
# The annotation for cls in a classmethod should be type[C]
664+
specified_receiver = receiver_args[0]
665+
else:
666+
specified_receiver = ann
662667

663668
quals = []
664669
if p.kind == inspect.Parameter.VAR_POSITIONAL:

0 commit comments

Comments
 (0)