Skip to content

Commit 8eb44f5

Browse files
committed
Give classmethod first arg a reasonable type. Fixes #292
Give classmethod first arg a reasonable type. Fixes #292.
1 parent 30dfd06 commit 8eb44f5

File tree

8 files changed

+100
-12
lines changed

8 files changed

+100
-12
lines changed

mypy/checkexpr.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ def check_call(self, callee: Type, args: List[Node],
218218
"""
219219
arg_messages = arg_messages or self.msg
220220
if isinstance(callee, CallableType):
221-
if callee.is_type_obj() and callee.type_object().is_abstract:
221+
if callee.is_concrete_type_obj() and callee.type_object().is_abstract:
222222
type = callee.type_object()
223223
self.msg.cannot_instantiate_abstract_class(
224224
callee.type_object().name(), type.abstract_attributes,

mypy/checkmember.py

+18-4
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ def analyze_var(name: str, var: Var, itype: Instance, info: TypeInfo, node: Cont
187187
# methods: the former to the instance, the latter to the
188188
# class.
189189
functype = cast(FunctionLike, t)
190-
check_method_type(functype, itype, node, msg)
190+
check_method_type(functype, itype, var.is_classmethod, node, msg)
191191
signature = method_type(functype)
192192
if var.is_property:
193193
# A property cannot have an overloaded type => the cast
@@ -228,17 +228,29 @@ def lookup_member_var_or_accessor(info: TypeInfo, name: str,
228228
return None
229229

230230

231-
def check_method_type(functype: FunctionLike, itype: Instance,
231+
def check_method_type(functype: FunctionLike, itype: Instance, is_classmethod: bool,
232232
context: Context, msg: MessageBuilder) -> None:
233233
for item in functype.items():
234234
if not item.arg_types or item.arg_kinds[0] not in (ARG_POS, ARG_STAR):
235235
# No positional first (self) argument (*args is okay).
236236
msg.invalid_method_type(item, context)
237-
else:
237+
elif not is_classmethod:
238238
# Check that self argument has type 'Any' or valid instance type.
239239
selfarg = item.arg_types[0]
240240
if not subtypes.is_equivalent(selfarg, itype):
241241
msg.invalid_method_type(item, context)
242+
else:
243+
# Check that cls argument has type 'Any' or valid class type.
244+
# (This is sufficient for the current treatment of @classmethod,
245+
# but probably needs to be revisited when we implement Type[C]
246+
# or advanced variants of it like Type[<args>, C].)
247+
clsarg = item.arg_types[0]
248+
if isinstance(clsarg, CallableType) and clsarg.is_type_obj():
249+
if not subtypes.is_equivalent(clsarg.ret_type, itype):
250+
msg.invalid_class_method_type(item, context)
251+
else:
252+
if not subtypes.is_equivalent(clsarg, AnyType()):
253+
msg.invalid_class_method_type(item, context)
242254

243255

244256
def analyze_class_attribute_access(itype: Instance,
@@ -370,7 +382,9 @@ def class_callable(init_type: CallableType, info: TypeInfo, type_type: Instance)
370382
callable_type = init_type.copy_modified(
371383
ret_type=self_type(info), fallback=type_type, name=None, variables=variables)
372384
c = callable_type.with_name('"{}"'.format(info.name()))
373-
return convert_class_tvars_to_func_tvars(c, len(initvars))
385+
cc = convert_class_tvars_to_func_tvars(c, len(initvars))
386+
cc.is_classmethod_class = True
387+
return cc
374388

375389

376390
def convert_class_tvars_to_func_tvars(callable: CallableType,

mypy/messages.py

+3
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,9 @@ def cannot_determine_type_in_base(self, name: str, base: str, context: Context)
733733
def invalid_method_type(self, sig: CallableType, context: Context) -> None:
734734
self.fail('Invalid method type', context)
735735

736+
def invalid_class_method_type(self, sig: CallableType, context: Context) -> None:
737+
self.fail('Invalid class method type', context)
738+
736739
def incompatible_conditional_function_def(self, defn: FuncDef) -> None:
737740
self.fail('All conditional function variants must have identical '
738741
'signatures', defn)

mypy/semanal.py

+14-3
Original file line numberDiff line numberDiff line change
@@ -306,9 +306,10 @@ def prepare_method_signature(self, func: FuncDef) -> None:
306306
self.fail('Method must have at least one argument', func)
307307
elif func.type:
308308
sig = cast(FunctionLike, func.type)
309-
# TODO: A classmethod's first argument should be more
310-
# precisely typed than Any.
311-
leading_type = AnyType() if func.is_class else self_type(self.type)
309+
if func.is_class:
310+
leading_type = self.class_type(self.type)
311+
else:
312+
leading_type = self_type(self.type)
312313
func.type = replace_implicit_first_type(sig, leading_type)
313314

314315
def is_conditional_func(self, previous: Node, new: FuncDef) -> bool:
@@ -808,6 +809,16 @@ def analyze_metaclass(self, defn: ClassDef) -> None:
808809
def object_type(self) -> Instance:
809810
return self.named_type('__builtins__.object')
810811

812+
def class_type(self, info: TypeInfo) -> Type:
813+
# Construct a function type whose fallback is cls.
814+
from mypy import checkmember # To avoid import cycle.
815+
leading_type = checkmember.type_object_type(info, self.builtin_type)
816+
if isinstance(leading_type, Overloaded):
817+
# Overloaded __init__ is too complex to handle. Plus it's stubs only.
818+
return AnyType()
819+
else:
820+
return leading_type
821+
811822
def named_type(self, qualified_name: str, args: List[Type] = None) -> Instance:
812823
sym = self.lookup_qualified(qualified_name, None)
813824
return Instance(cast(TypeInfo, sym.node), args or [])

mypy/test/data/check-classes.test

+48
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,54 @@ class A:
763763
A().f = A.f # E: Cannot assign to a method
764764
[builtins fixtures/classmethod.py]
765765

766+
[case testClassMethodCalledInClassMethod]
767+
import typing
768+
class C:
769+
@classmethod
770+
def foo(cls) -> None: pass
771+
@classmethod
772+
def bar(cls) -> None:
773+
cls()
774+
cls(1) # E: Too many arguments for "C"
775+
cls.bar()
776+
cls.bar(1) # E: Too many arguments for "bar" of "C"
777+
cls.bozo() # E: "C" has no attribute "bozo"
778+
[builtins fixtures/classmethod.py]
779+
[out]
780+
main: note: In member "bar" of class "C":
781+
782+
[case testClassMethodCalledOnClass]
783+
import typing
784+
class C:
785+
@classmethod
786+
def foo(cls) -> None: pass
787+
C.foo()
788+
C.foo(1) # E: Too many arguments for "foo" of "C"
789+
C.bozo() # E: "C" has no attribute "bozo"
790+
[builtins fixtures/classmethod.py]
791+
792+
[case testClassMethodCalledOnInstance]
793+
import typing
794+
class C:
795+
@classmethod
796+
def foo(cls) -> None: pass
797+
C().foo()
798+
C().foo(1) # E: Too many arguments for "foo" of "C"
799+
C.bozo() # E: "C" has no attribute "bozo"
800+
[builtins fixtures/classmethod.py]
801+
802+
[case testClassMethodMayCallAbstractMethod]
803+
from abc import abstractmethod
804+
import typing
805+
class C:
806+
@classmethod
807+
def foo(cls) -> None:
808+
cls().bar()
809+
@abstractmethod
810+
def bar(self) -> None:
811+
pass
812+
[builtins fixtures/classmethod.py]
813+
766814

767815
-- Properties
768816
-- ----------

mypy/test/data/semanal-classes.test

+2-2
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,7 @@ MypyFile:1(
458458
Args(
459459
Var(cls)
460460
Var(z))
461-
def (cls: Any, z: builtins.int) -> builtins.str
461+
def (cls: def () -> __main__.A, z: builtins.int) -> builtins.str
462462
Class
463463
Block:3(
464464
PassStmt:3())))))
@@ -478,7 +478,7 @@ MypyFile:1(
478478
f
479479
Args(
480480
Var(cls))
481-
def (cls: Any) -> builtins.str
481+
def (cls: def () -> __main__.A) -> builtins.str
482482
Class
483483
Block:3(
484484
PassStmt:3())))))

mypy/typeanal.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ def visit_type_var(self, t: TypeVarType) -> Type:
179179
def visit_callable_type(self, t: CallableType) -> Type:
180180
return t.copy_modified(arg_types=self.anal_array(t.arg_types),
181181
ret_type=t.ret_type.accept(self),
182-
fallback=self.builtin_type('builtins.function'),
182+
fallback=t.fallback or self.builtin_type('builtins.function'),
183183
variables=self.anal_var_defs(t.variables),
184184
bound_vars=self.anal_bound_vars(t.bound_vars))
185185

mypy/types.py

+13-1
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,9 @@ class FunctionLike(Type):
231231
@abstractmethod
232232
def is_type_obj(self) -> bool: pass
233233

234+
def is_concrete_type_obj(self) -> bool:
235+
return self.is_type_obj()
236+
234237
@abstractmethod
235238
def type_object(self) -> mypy.nodes.TypeInfo: pass
236239

@@ -278,6 +281,8 @@ class CallableType(FunctionLike):
278281

279282
# Is this Callable[..., t] (with literal '...')?
280283
is_ellipsis_args = False
284+
# Is this callable constructed for the benefit of a classmethod's 'cls' argument?
285+
is_classmethod_class = False
281286
# Was this type implicitly generated instead of explicitly specified by the user?
282287
implicit = False
283288

@@ -292,7 +297,9 @@ def __init__(self, arg_types: List[Type],
292297
bound_vars: List[Tuple[int, Type]] = None,
293298
line: int = -1,
294299
is_ellipsis_args: bool = False,
295-
implicit=False) -> None:
300+
implicit=False,
301+
is_classmethod_class=False,
302+
) -> None:
296303
if variables is None:
297304
variables = []
298305
if not bound_vars:
@@ -338,11 +345,16 @@ def copy_modified(self,
338345
line=line if line is not _dummy else self.line,
339346
is_ellipsis_args=(
340347
is_ellipsis_args if is_ellipsis_args is not _dummy else self.is_ellipsis_args),
348+
implicit=self.implicit,
349+
is_classmethod_class=self.is_classmethod_class,
341350
)
342351

343352
def is_type_obj(self) -> bool:
344353
return self.fallback.type.fullname() == 'builtins.type'
345354

355+
def is_concrete_type_obj(self) -> bool:
356+
return self.is_type_obj() and self.is_classmethod_class
357+
346358
def type_object(self) -> mypy.nodes.TypeInfo:
347359
assert self.is_type_obj()
348360
ret = self.ret_type

0 commit comments

Comments
 (0)