@@ -942,6 +942,114 @@ class C:
942942reveal_type(C) # N: Revealed type is "def (x: Any, y: Any, z: Any) -> __main__.C"
943943[builtins fixtures/list.pyi]
944944
945+ [case testAttrsUsingClassmethodConverter]
946+ import attr
947+
948+ class MyClass:
949+ @classmethod
950+ def my_class_method(cls, value: int) -> str:
951+ return "..."
952+
953+ @attr.s
954+ class Foo:
955+ bar: str = attr.ib(converter=MyClass.my_class_method)
956+
957+ # The classmethod's `cls` argument is dropped, so __init__ takes the int.
958+ reveal_type(Foo) # N: Revealed type is "def (bar: builtins.int) -> __main__.Foo"
959+ reveal_type(Foo(1).bar) # N: Revealed type is "builtins.str"
960+ [builtins fixtures/classmethod.pyi]
961+
962+ [case testAttrsUsingStaticmethodConverter]
963+ import attr
964+
965+ class MyClass:
966+ @staticmethod
967+ def my_static_method(value: bytes) -> str:
968+ return "..."
969+
970+ @attr.s
971+ class Foo:
972+ bar: str = attr.ib(converter=MyClass.my_static_method)
973+
974+ reveal_type(Foo) # N: Revealed type is "def (bar: builtins.bytes) -> __main__.Foo"
975+ reveal_type(Foo(b"x").bar) # N: Revealed type is "builtins.str"
976+ [builtins fixtures/classmethod.pyi]
977+
978+ [case testAttrsUsingDecoratedClassmethodConverterIsUnsupported]
979+ # A classmethod/staticmethod wrapped in another decorator may have its signature
980+ # changed by that decorator, so we can't trust the underlying function type and
981+ # treat it as unsupported instead of inferring a wrong __init__ type.
982+ import attr
983+ from typing import Any, Callable, TypeVar
984+
985+ F = TypeVar("F", bound=Callable[..., Any])
986+
987+ def deco(f: F) -> F:
988+ return f
989+
990+ class MyClass:
991+ @classmethod
992+ @deco
993+ def my_class_method(cls, value: int) -> str:
994+ return "..."
995+
996+ @attr.s
997+ class Foo:
998+ bar: str = attr.ib(converter=MyClass.my_class_method) # E: Unsupported converter, only named functions, types and lambdas are currently supported
999+ [builtins fixtures/classmethod.pyi]
1000+
1001+ [case testAttrsUsingUnannotatedClassmethodConverter]
1002+ import attr
1003+
1004+ class MyClass:
1005+ @classmethod
1006+ def my_class_method(cls, value):
1007+ return value
1008+
1009+ @staticmethod
1010+ def my_static_method(value):
1011+ return value
1012+
1013+ @attr.s
1014+ class Foo:
1015+ a: str = attr.ib(converter=MyClass.my_class_method)
1016+ b: str = attr.ib(converter=MyClass.my_static_method)
1017+
1018+ # Unannotated converters make the corresponding __init__ argument Any.
1019+ reveal_type(Foo) # N: Revealed type is "def (a: Any, b: Any) -> __main__.Foo"
1020+ [builtins fixtures/classmethod.pyi]
1021+
1022+ [case testAttrsUsingClassmethodPropertyConverterIsUnsupported]
1023+ # The exotic @classmethod @property combo is stripped to an empty decorators list,
1024+ # but yields a no-argument getter, so the converter type can't be determined.
1025+ import attr
1026+
1027+ class M:
1028+ @classmethod # E: Only instance methods can be decorated with @property
1029+ @property
1030+ def cp(cls) -> str:
1031+ return "..."
1032+
1033+ @attr.s
1034+ class Foo:
1035+ x: str = attr.ib(converter=M.cp) # E: Cannot determine __init__ type from converter \
1036+ # E: Argument "converter" has incompatible type "Callable[[], str]"; expected "Callable[[Any], Never] | None"
1037+ [builtins fixtures/property.pyi]
1038+
1039+ [case testAttrsUsingPropertyConverterIsUnsupported]
1040+ # A property is stripped to an empty decorators list but is neither classmethod
1041+ # nor staticmethod, so it must not be accepted as a converter.
1042+ import attr
1043+
1044+ class M:
1045+ @property
1046+ def p(self): ...
1047+
1048+ @attr.s
1049+ class Foo:
1050+ x: str = attr.ib(converter=M.p) # E: Unsupported converter, only named functions, types and lambdas are currently supported
1051+ [builtins fixtures/property.pyi]
1052+
9451053[case testAttrsUsingConverterAndSubclass]
9461054import attr
9471055
0 commit comments