Skip to content

Commit db9efde

Browse files
committed
Add basic support for @classmethod. We ascribe the type Any to the first argument to classmethods; at some point this will need to be tightened up to a bespoke class type.
1 parent afc7025 commit db9efde

15 files changed

+220
-57
lines changed

mypy/checkmember.py

Lines changed: 52 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def analyse_member_access(name: str, typ: Type, node: Context, is_lvalue: bool,
4747
method = info.get_method(name)
4848
if method:
4949
if is_lvalue:
50-
msg.fail(messages.CANNOT_ASSIGN_TO_METHOD, node)
50+
msg.cant_assign_to_method(node)
5151
typ = map_instance_to_supertype(typ, method.info)
5252
return expand_type_by_instance(method_type(method), typ)
5353
else:
@@ -107,20 +107,26 @@ def analyse_member_var_access(name: str, itype: Instance, info: TypeInfo,
107107
itype = map_instance_to_supertype(itype, var.info)
108108
if var.type:
109109
t = expand_type_by_instance(var.type, itype)
110-
if (var.is_initialized_in_class and isinstance(t, FunctionLike)
111-
and not var.is_staticmethod):
112-
# Class-level function object becomes a bound method.
113-
functype = cast(FunctionLike, t)
114-
check_method_type(functype, itype, node, msg)
115-
signature = method_type(functype)
116-
if var.is_property:
117-
if is_lvalue:
110+
if var.is_initialized_in_class and isinstance(t, FunctionLike):
111+
if is_lvalue:
112+
if var.is_property:
118113
msg.read_only_property(name, info, node)
119-
# A property cannot have an overloaded type => the cast
120-
# is fine.
121-
return cast(Callable, signature).ret_type
122-
else:
123-
return signature
114+
else:
115+
msg.cant_assign_to_method(node)
116+
117+
if not var.is_staticmethod:
118+
# Class-level function objects and classmethods become bound
119+
# methods: the former to the instance, the latter to the
120+
# class.
121+
functype = cast(FunctionLike, t)
122+
check_method_type(functype, itype, node, msg)
123+
signature = method_type(functype)
124+
if var.is_property:
125+
# A property cannot have an overloaded type => the cast
126+
# is fine.
127+
return cast(Callable, signature).ret_type
128+
else:
129+
return signature
124130
return t
125131
else:
126132
if not var.is_ready:
@@ -166,38 +172,51 @@ def analyse_class_attribute_access(itype: Instance, name: str,
166172
context: Context, is_lvalue: bool,
167173
msg: MessageBuilder) -> Type:
168174
node = itype.type.get(name)
169-
if node:
170-
if is_lvalue and isinstance(node.node, FuncDef):
171-
msg.fail(messages.CANNOT_ASSIGN_TO_METHOD, context)
172-
if is_lvalue and isinstance(node.node, TypeInfo):
173-
msg.fail(messages.CANNOT_ASSIGN_TO_TYPE, context)
174-
t = node.type
175-
if t:
176-
return add_class_tvars(t, itype.type)
177-
elif isinstance(node.node, TypeInfo):
178-
# TODO add second argument
179-
return type_object_type(cast(TypeInfo, node.node), None)
180-
else:
181-
return function_type(cast(FuncBase, node.node))
182-
else:
175+
if not node:
183176
return None
184177

178+
is_decorated = isinstance(node.node, Decorator)
179+
is_method = is_decorated or isinstance(node.node, FuncDef)
180+
if is_lvalue:
181+
if is_method:
182+
msg.cant_assign_to_method(context)
183+
if isinstance(node.node, TypeInfo):
184+
msg.fail(messages.CANNOT_ASSIGN_TO_TYPE, context)
185+
186+
t = node.type
187+
if t:
188+
is_classmethod = is_decorated and cast(Decorator, node.node).func.is_class
189+
return add_class_tvars(t, itype.type, is_classmethod)
190+
191+
if isinstance(node.node, TypeInfo):
192+
# TODO add second argument
193+
return type_object_type(cast(TypeInfo, node.node), None)
194+
195+
return function_type(cast(FuncBase, node.node))
196+
185197

186-
def add_class_tvars(t: Type, info: TypeInfo) -> Type:
198+
def add_class_tvars(t: Type, info: TypeInfo, is_classmethod: bool) -> Type:
187199
if isinstance(t, Callable):
188200
vars = [TypeVarDef(n, i + 1, None)
189201
for i, n in enumerate(info.type_vars)]
190-
return Callable(t.arg_types,
191-
t.arg_kinds,
192-
t.arg_names,
202+
arg_types = t.arg_types
203+
arg_kinds = t.arg_kinds
204+
arg_names = t.arg_names
205+
if is_classmethod:
206+
arg_types = arg_types[1:]
207+
arg_kinds = arg_kinds[1:]
208+
arg_names = arg_names[1:]
209+
return Callable(arg_types,
210+
arg_kinds,
211+
arg_names,
193212
t.ret_type,
194213
t.is_type_obj(),
195214
t.name,
196215
vars + t.variables,
197216
t.bound_vars,
198217
t.line, None)
199218
elif isinstance(t, Overloaded):
200-
return Overloaded([cast(Callable, add_class_tvars(i, info))
219+
return Overloaded([cast(Callable, add_class_tvars(i, info, is_classmethod))
201220
for i in t.items()])
202221
return t
203222

mypy/messages.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,9 @@ def base_class_definitions_incompatible(self, name: str, base1: TypeInfo,
565565
'with definition in base class "{}"'.format(
566566
name, base1.name(), base2.name()), context)
567567

568+
def cant_assign_to_method(self, context: Context) -> None:
569+
self.fail(CANNOT_ASSIGN_TO_METHOD, context)
570+
568571
def read_only_property(self, name: str, type: TypeInfo,
569572
context: Context) -> None:
570573
self.fail('Property "{}" defined in "{}" is read-only'.format(

mypy/nodes.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ class FuncDef(FuncItem):
293293
is_conditional = False # Defined conditionally (within block)?
294294
is_abstract = False
295295
is_static = False
296+
is_class = False
296297
is_property = False
297298
original_def = None # type: FuncDef # Original conditional definition
298299

@@ -367,6 +368,7 @@ class Var(SymbolNode):
367368
# Is this initialized explicitly to a non-None value in class body?
368369
is_initialized_in_class = False
369370
is_staticmethod = False
371+
is_classmethod = False
370372
is_property = False
371373

372374
def __init__(self, name: str, type: 'mypy.types.Type' = None) -> None:

mypy/semanal.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
from mypy.types import (
6565
NoneTyp, Callable, Overloaded, Instance, Type, TypeVar, AnyType,
6666
FunctionLike, UnboundType, TypeList, ErrorType, TypeVarDef,
67-
replace_self_type, TupleType
67+
replace_leading_arg_type, TupleType
6868
)
6969
from mypy.nodes import function_type, implicit_module_attrs
7070
from mypy.typeanal import TypeAnalyser, TypeAnalyserPass3
@@ -176,8 +176,10 @@ def visit_func_def(self, defn: FuncDef) -> None:
176176
self.fail('Method must have at least one argument', defn)
177177
elif defn.type:
178178
sig = cast(FunctionLike, defn.type)
179-
defn.type = replace_implicit_self_type(
180-
sig, self_type(self.type))
179+
# TODO: A classmethod's first argument should be more
180+
# precisely typed than Any.
181+
leading_type = AnyType() if defn.is_class else self_type(self.type)
182+
defn.type = replace_implicit_first_type(sig, leading_type)
181183

182184
if self.is_func_scope() and (not defn.is_decorated and
183185
not defn.is_overload):
@@ -288,9 +290,10 @@ def analyse_function(self, defn: FuncItem) -> None:
288290
if init_:
289291
init_.lvalues[0].accept(self)
290292

291-
# The first argument of a method is like 'self', though the name could
292-
# be different.
293-
if is_method and defn.args:
293+
# The first argument of a non-static, non-class method is like 'self'
294+
# (though the name could be different), having the enclosing class's
295+
# instance type.
296+
if is_method and not defn.is_static and not defn.is_class and defn.args:
294297
defn.args[0].is_self = True
295298

296299
defn.body.accept(self)
@@ -933,6 +936,11 @@ def visit_decorator(self, dec: Decorator) -> None:
933936
dec.func.is_static = True
934937
dec.var.is_staticmethod = True
935938
self.check_decorated_function_is_method('staticmethod', dec)
939+
elif refers_to_fullname(d, 'builtins.classmethod'):
940+
removed.append(i)
941+
dec.func.is_class = True
942+
dec.var.is_classmethod = True
943+
self.check_decorated_function_is_method('classmethod', dec)
936944
elif refers_to_fullname(d, 'builtins.property'):
937945
removed.append(i)
938946
dec.func.is_property = True
@@ -1606,17 +1614,17 @@ def self_type(typ: TypeInfo) -> Instance:
16061614

16071615

16081616
@overload
1609-
def replace_implicit_self_type(sig: Callable, new: Type) -> Callable:
1617+
def replace_implicit_first_type(sig: Callable, new: Type) -> Callable:
16101618
# We can detect implicit self type by it having no representation.
16111619
if not sig.arg_types[0].repr:
1612-
return replace_self_type(sig, new)
1620+
return replace_leading_arg_type(sig, new)
16131621
else:
16141622
return sig
16151623

16161624
@overload
1617-
def replace_implicit_self_type(sig: FunctionLike, new: Type) -> FunctionLike:
1625+
def replace_implicit_first_type(sig: FunctionLike, new: Type) -> FunctionLike:
16181626
osig = cast(Overloaded, sig)
1619-
return Overloaded([replace_implicit_self_type(i, new)
1627+
return Overloaded([replace_implicit_first_type(i, new)
16201628
for i in osig.items()])
16211629

16221630

mypy/strconv.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ def visit_func_def(self, o):
108108
a.insert(-1, 'Abstract')
109109
if o.is_static:
110110
a.insert(-1, 'Static')
111+
if o.is_class:
112+
a.insert(-1, 'Class')
111113
if o.is_property:
112114
a.insert(-1, 'Property')
113115
return self.dump(a, o)

mypy/test/data/check-classes.test

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,52 @@ int.from_bytes(b'', '')
717717
int.from_bytes('', '') # E: Argument 1 to "from_bytes" of "int" has incompatible type "str"; expected "bytes"
718718
[builtins fixtures/staticmethod.py]
719719

720+
[case testAssignStaticMethodOnInstance]
721+
import typing
722+
class A:
723+
@staticmethod
724+
def f(x: int) -> None: pass
725+
A().f = A.f # E: Cannot assign to a method
726+
[builtins fixtures/staticmethod.py]
727+
728+
729+
-- Class methods
730+
-- -------------
731+
732+
733+
[case testSimpleClassMethod]
734+
import typing
735+
class A:
736+
@classmethod
737+
def f(cls, x: int) -> None: pass
738+
A.f(1)
739+
A().f(1)
740+
A.f('') # E: Argument 1 to "f" of "A" has incompatible type "str"; expected "int"
741+
A().f('') # E: Argument 1 to "f" of "A" has incompatible type "str"; expected "int"
742+
[builtins fixtures/classmethod.py]
743+
744+
[case testBuiltinClassMethod]
745+
import typing
746+
int.from_bytes(b'', '')
747+
int.from_bytes('', '') # E: Argument 1 to "from_bytes" of "int" has incompatible type "str"; expected "bytes"
748+
[builtins fixtures/classmethod.py]
749+
750+
[case testAssignClassMethodOnClass]
751+
import typing
752+
class A:
753+
@classmethod
754+
def f(cls, x: int) -> None: pass
755+
A.f = A.f # E: Cannot assign to a method
756+
[builtins fixtures/classmethod.py]
757+
758+
[case testAssignClassMethodOnInstance]
759+
import typing
760+
class A:
761+
@classmethod
762+
def f(cls, x: int) -> None: pass
763+
A().f = A.f # E: Cannot assign to a method
764+
[builtins fixtures/classmethod.py]
765+
720766

721767
-- Properties
722768
-- ----------
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import typing
2+
3+
class object:
4+
def __init__(self) -> None: pass
5+
6+
class type:
7+
def __init__(self, x) -> None: pass
8+
9+
classmethod = object() # Dummy definition.
10+
11+
class int:
12+
@classmethod
13+
def from_bytes(cls, bytes: bytes, byteorder: str) -> int: pass
14+
15+
class str: pass
16+
class bytes: pass

mypy/test/data/pythoneval.test

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,17 @@ print(A().f('34'))
421421
12
422422
34
423423

424+
[case testClassmethod]
425+
import typing
426+
class A:
427+
@classmethod
428+
def f(cls, x: str) -> int: return int(x)
429+
print(A.f('12'))
430+
print(A().f('34'))
431+
[out]
432+
12
433+
34
434+
424435
[case testIntMethods]
425436
import typing
426437
print(int.from_bytes(b'ab', 'big'))

mypy/test/data/semanal-classes.test

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,47 @@ MypyFile:1(
425425
Block:3(
426426
PassStmt:3())))))
427427

428+
[case testClassMethod]
429+
class A:
430+
@classmethod
431+
def f(cls, z: int) -> str: pass
432+
[builtins fixtures/classmethod.py]
433+
[out]
434+
MypyFile:1(
435+
ClassDef:1(
436+
A
437+
Decorator:2(
438+
Var(f)
439+
FuncDef:3(
440+
f
441+
Args(
442+
Var(cls)
443+
Var(z))
444+
def (cls: Any, z: builtins.int) -> builtins.str
445+
Class
446+
Block:3(
447+
PassStmt:3())))))
448+
449+
[case testClassMethodWithNoArgs]
450+
class A:
451+
@classmethod
452+
def f(cls) -> str: pass
453+
[builtins fixtures/classmethod.py]
454+
[out]
455+
MypyFile:1(
456+
ClassDef:1(
457+
A
458+
Decorator:2(
459+
Var(f)
460+
FuncDef:3(
461+
f
462+
Args(
463+
Var(cls))
464+
def (cls: Any) -> builtins.str
465+
Class
466+
Block:3(
467+
PassStmt:3())))))
468+
428469
[case testProperty]
429470
import typing
430471
class A:

mypy/test/data/semanal-errors.test

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,6 +1023,20 @@ main, line 2: 'staticmethod' used with a non-method
10231023
main: In function "g":
10241024
main, line 6: 'staticmethod' used with a non-method
10251025

1026+
[case testClassmethodAndNonMethod]
1027+
import typing
1028+
@classmethod
1029+
def f(): pass
1030+
class A:
1031+
def g(self):
1032+
@classmethod
1033+
def h(): pass
1034+
[builtins fixtures/classmethod.py]
1035+
[out]
1036+
main, line 2: 'classmethod' used with a non-method
1037+
main: In function "g":
1038+
main, line 6: 'classmethod' used with a non-method
1039+
10261040
[case testNonMethodProperty]
10271041
import typing
10281042
@property # E: 'property' used with a non-method

0 commit comments

Comments
 (0)