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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy