Skip to content

Commit 72967fd

Browse files
ilevkivskyigvanrossum
authored andcommitted
Allow instantiation of Type[A], if A is abstract (#2853)
Fixes #1843 (It was also necessary to fix few minor things to make this work correctly) The rules are simple, assuming we have: ```python class A: @AbstractMethod def m(self) -> None: pass class C(A): def m(self) -> None: ... ``` then ```python def fun(cls: Type[A]): cls() # OK fun(A) # Error fun(C) # OK ``` The same applies to variables: ```python var: Type[A] var() # OK var = A # Error var = C # OK ``` Also there is an option for people who want to pass abstract classes around: type aliases, they work as before. For non-abstract ``A``, ``Type[A]`` also works as before. My intuition why you opened #1843 is when someone writes annotation ``Type[A]`` with an abstract ``A``, then most probably one wants a class object that _implements_ a certain protocol, not just inherits from ``A``. NOTE: As discussed in python/peps#224 this behaviour is good for both protocols and usual ABCs.
1 parent e674e25 commit 72967fd

File tree

6 files changed

+132
-6
lines changed

6 files changed

+132
-6
lines changed

mypy/checker.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,8 @@ def is_implicit_any(t: Type) -> bool:
656656
self.fail(msg, defn)
657657
if note:
658658
self.note(note, defn)
659+
if defn.is_class and isinstance(arg_type, CallableType):
660+
arg_type.is_classmethod_class = True
659661
elif isinstance(arg_type, TypeVarType):
660662
# Refuse covariant parameter type variables
661663
# TODO: check recursively for inner type variables
@@ -1208,6 +1210,16 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type
12081210
else:
12091211
rvalue_type = self.check_simple_assignment(lvalue_type, rvalue, lvalue)
12101212

1213+
# Special case: only non-abstract classes can be assigned to variables
1214+
# with explicit type Type[A].
1215+
if (isinstance(rvalue_type, CallableType) and rvalue_type.is_type_obj() and
1216+
rvalue_type.type_object().is_abstract and
1217+
isinstance(lvalue_type, TypeType) and
1218+
isinstance(lvalue_type.item, Instance) and
1219+
lvalue_type.item.type.is_abstract):
1220+
self.fail("Can only assign non-abstract classes"
1221+
" to a variable of type '{}'".format(lvalue_type), rvalue)
1222+
return
12111223
if rvalue_type and infer_lvalue_type:
12121224
self.binder.assign_type(lvalue,
12131225
rvalue_type,

mypy/checkexpr.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,9 @@ def check_call(self, callee: Type, args: List[Expression],
354354
"""
355355
arg_messages = arg_messages or self.msg
356356
if isinstance(callee, CallableType):
357-
if callee.is_concrete_type_obj() and callee.type_object().is_abstract:
357+
if (callee.is_type_obj() and callee.type_object().is_abstract
358+
# Exceptions for Type[...] and classmethod first argument
359+
and not callee.from_type_type and not callee.is_classmethod_class):
358360
type = callee.type_object()
359361
self.msg.cannot_instantiate_abstract_class(
360362
callee.type_object().name(), type.abstract_attributes,
@@ -440,7 +442,10 @@ def analyze_type_type_callee(self, item: Type, context: Context) -> Type:
440442
if isinstance(item, AnyType):
441443
return AnyType()
442444
if isinstance(item, Instance):
443-
return type_object_type(item.type, self.named_type)
445+
res = type_object_type(item.type, self.named_type)
446+
if isinstance(res, CallableType):
447+
res = res.copy_modified(from_type_type=True)
448+
return res
444449
if isinstance(item, UnionType):
445450
return UnionType([self.analyze_type_type_callee(item, context)
446451
for item in item.items], item.line)
@@ -838,6 +843,14 @@ def check_arg(self, caller_type: Type, original_caller_type: Type,
838843
"""Check the type of a single argument in a call."""
839844
if isinstance(caller_type, DeletedType):
840845
messages.deleted_as_rvalue(caller_type, context)
846+
# Only non-abstract class can be given where Type[...] is expected...
847+
elif (isinstance(caller_type, CallableType) and isinstance(callee_type, TypeType) and
848+
caller_type.is_type_obj() and caller_type.type_object().is_abstract and
849+
isinstance(callee_type.item, Instance) and callee_type.item.type.is_abstract and
850+
# ...except for classmethod first argument
851+
not caller_type.is_classmethod_class):
852+
messages.fail("Only non-abstract class can be given where '{}' is expected"
853+
.format(callee_type), context)
841854
elif not is_subtype(caller_type, callee_type):
842855
if self.chk.should_suppress_optional_error([caller_type, callee_type]):
843856
return

mypy/checkmember.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,6 @@ def class_callable(init_type: CallableType, info: TypeInfo, type_type: Instance,
535535
ret_type=fill_typevars(info), fallback=type_type, name=None, variables=variables,
536536
special_sig=special_sig)
537537
c = callable_type.with_name('"{}"'.format(info.name()))
538-
c.is_classmethod_class = True
539538
return c
540539

541540

mypy/meet.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,12 @@ class C(A, B): ...
104104
elif isinstance(t, TypeType) or isinstance(s, TypeType):
105105
# If exactly only one of t or s is a TypeType, check if one of them
106106
# is an `object` or a `type` and otherwise assume no overlap.
107+
one = t if isinstance(t, TypeType) else s
107108
other = s if isinstance(t, TypeType) else t
108109
if isinstance(other, Instance):
109110
return other.type.fullname() in {'builtins.object', 'builtins.type'}
110111
else:
111-
return False
112+
return isinstance(other, CallableType) and is_subtype(other, one)
112113
if experiments.STRICT_OPTIONAL:
113114
if isinstance(t, NoneTyp) != isinstance(s, NoneTyp):
114115
# NoneTyp does not overlap with other non-Union types under strict Optional checking

mypy/types.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,8 @@ class CallableType(FunctionLike):
537537
# Defined for signatures that require special handling (currently only value is 'dict'
538538
# for a signature similar to 'dict')
539539
special_sig = None # type: Optional[str]
540+
# Was this callable generated by analyzing Type[...] instantiation?
541+
from_type_type = False # type: bool
540542

541543
def __init__(self,
542544
arg_types: List[Type],
@@ -553,6 +555,7 @@ def __init__(self,
553555
implicit: bool = False,
554556
is_classmethod_class: bool = False,
555557
special_sig: Optional[str] = None,
558+
from_type_type: bool = False,
556559
) -> None:
557560
if variables is None:
558561
variables = []
@@ -571,7 +574,9 @@ def __init__(self,
571574
self.variables = variables
572575
self.is_ellipsis_args = is_ellipsis_args
573576
self.implicit = implicit
577+
self.is_classmethod_class = is_classmethod_class
574578
self.special_sig = special_sig
579+
self.from_type_type = from_type_type
575580
super().__init__(line, column)
576581

577582
def copy_modified(self,
@@ -586,7 +591,8 @@ def copy_modified(self,
586591
line: int = _dummy,
587592
column: int = _dummy,
588593
is_ellipsis_args: bool = _dummy,
589-
special_sig: Optional[str] = _dummy) -> 'CallableType':
594+
special_sig: Optional[str] = _dummy,
595+
from_type_type: bool = _dummy) -> 'CallableType':
590596
return CallableType(
591597
arg_types=arg_types if arg_types is not _dummy else self.arg_types,
592598
arg_kinds=arg_kinds if arg_kinds is not _dummy else self.arg_kinds,
@@ -603,6 +609,7 @@ def copy_modified(self,
603609
implicit=self.implicit,
604610
is_classmethod_class=self.is_classmethod_class,
605611
special_sig=special_sig if special_sig is not _dummy else self.special_sig,
612+
from_type_type=from_type_type if from_type_type is not _dummy else self.from_type_type,
606613
)
607614

608615
def is_type_obj(self) -> bool:
@@ -617,7 +624,10 @@ def type_object(self) -> mypy.nodes.TypeInfo:
617624
ret = self.ret_type
618625
if isinstance(ret, TupleType):
619626
ret = ret.fallback
620-
return cast(Instance, ret).type
627+
if isinstance(ret, TypeVarType):
628+
ret = ret.upper_bound
629+
assert isinstance(ret, Instance)
630+
return ret.type
621631

622632
def accept(self, visitor: 'TypeVisitor[T]') -> T:
623633
return visitor.visit_callable_type(self)

test-data/unit/check-abstract.test

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,97 @@ class B(A): pass
157157
B()# E: Cannot instantiate abstract class 'B' with abstract attributes 'f' and 'g'
158158
[out]
159159

160+
[case testInstantiationAbstractsInTypeForFunctions]
161+
from typing import Type
162+
from abc import abstractmethod
163+
164+
class A:
165+
@abstractmethod
166+
def m(self) -> None: pass
167+
class B(A): pass
168+
class C(B):
169+
def m(self) -> None:
170+
pass
171+
172+
def f(cls: Type[A]) -> A:
173+
return cls() # OK
174+
def g() -> A:
175+
return A() # E: Cannot instantiate abstract class 'A' with abstract attribute 'm'
176+
177+
f(A) # E: Only non-abstract class can be given where 'Type[__main__.A]' is expected
178+
f(B) # E: Only non-abstract class can be given where 'Type[__main__.A]' is expected
179+
f(C) # OK
180+
x: Type[B]
181+
f(x) # OK
182+
[out]
183+
184+
[case testInstantiationAbstractsInTypeForAliases]
185+
from typing import Type
186+
from abc import abstractmethod
187+
188+
class A:
189+
@abstractmethod
190+
def m(self) -> None: pass
191+
class B(A): pass
192+
class C(B):
193+
def m(self) -> None:
194+
pass
195+
196+
def f(cls: Type[A]) -> A:
197+
return cls() # OK
198+
199+
Alias = A
200+
GoodAlias = C
201+
Alias() # E: Cannot instantiate abstract class 'A' with abstract attribute 'm'
202+
GoodAlias()
203+
f(Alias) # E: Only non-abstract class can be given where 'Type[__main__.A]' is expected
204+
f(GoodAlias)
205+
[out]
206+
207+
[case testInstantiationAbstractsInTypeForVariables]
208+
from typing import Type
209+
from abc import abstractmethod
210+
211+
class A:
212+
@abstractmethod
213+
def m(self) -> None: pass
214+
class B(A): pass
215+
class C(B):
216+
def m(self) -> None:
217+
pass
218+
219+
var: Type[A]
220+
var()
221+
var = A # E: Can only assign non-abstract classes to a variable of type 'Type[__main__.A]'
222+
var = B # E: Can only assign non-abstract classes to a variable of type 'Type[__main__.A]'
223+
var = C # OK
224+
225+
var_old = None # type: Type[A] # Old syntax for variable annotations
226+
var_old()
227+
var_old = A # E: Can only assign non-abstract classes to a variable of type 'Type[__main__.A]'
228+
var_old = B # E: Can only assign non-abstract classes to a variable of type 'Type[__main__.A]'
229+
var_old = C # OK
230+
[out]
231+
232+
[case testInstantiationAbstractsInTypeForClassMethods]
233+
from typing import Type
234+
from abc import abstractmethod
235+
236+
class Logger:
237+
@staticmethod
238+
def log(a: Type[C]):
239+
pass
240+
class C:
241+
@classmethod
242+
def action(cls) -> None:
243+
cls() #OK for classmethods
244+
Logger.log(cls) #OK for classmethods
245+
@abstractmethod
246+
def m(self) -> None:
247+
pass
248+
[builtins fixtures/classmethod.pyi]
249+
[out]
250+
160251
[case testInstantiatingClassWithInheritedAbstractMethodAndSuppression]
161252
from abc import abstractmethod, ABCMeta
162253
import typing

0 commit comments

Comments
 (0)