Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,8 @@ def is_implicit_any(t: Type) -> bool:
self.fail(msg, defn)
if note:
self.note(note, defn)
if defn.is_class and isinstance(arg_type, CallableType):
arg_type.is_classmethod_class = True
elif isinstance(arg_type, TypeVarType):
# Refuse covariant parameter type variables
# TODO: check recursively for inner type variables
Expand Down Expand Up @@ -1149,6 +1151,16 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type
else:
rvalue_type = self.check_simple_assignment(lvalue_type, rvalue, lvalue)

# Special case

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For what, exactly?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a detailed comment here.

if (
isinstance(rvalue_type, CallableType) and rvalue_type.is_type_obj() and

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not keen on this formatting.

rvalue_type.type_object().is_abstract and
isinstance(lvalue_type, TypeType) and
isinstance(lvalue_type.item, Instance) and lvalue_type.item.type.is_abstract
):
self.fail("Cannot only assign non-abstract classes"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Cannot only" -> "Cannot" (I presume).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, sorry, it should be "Can only" (this is probably a worst kind of mistakes).

" to a variable of type '{}'".format(lvalue_type), rvalue)
return
if rvalue_type and infer_lvalue_type:
self.binder.assign_type(lvalue,
rvalue_type,
Expand Down
15 changes: 13 additions & 2 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,9 @@ def check_call(self, callee: Type, args: List[Expression],
"""
arg_messages = arg_messages or self.msg
if isinstance(callee, CallableType):
if callee.is_concrete_type_obj() and callee.type_object().is_abstract:
if (callee.is_type_obj() and callee.type_object().is_abstract
# Exceptions for Type[...] and classmethod first argument
and not callee.from_type_type and not callee.is_classmethod_class):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe also except static methods?

I found a case in our codebase that goes basically like this:

class Logger:
    @staticmethod
    def log(a: Type[C]): ...
class C:
    @classmethod
    def action(cls) -> None:
        Logger.log(cls)  #E: Only non-abstract class can be given where 'Type[C]' is expected
    @abstractmethod
    ...

(I'm far from positive that it's because of this line though.)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code looks reasonable, I added a corresponding exception to check_arg.

type = callee.type_object()
self.msg.cannot_instantiate_abstract_class(
callee.type_object().name(), type.abstract_attributes,
Expand Down Expand Up @@ -437,7 +439,10 @@ def analyze_type_type_callee(self, item: Type, context: Context) -> Type:
if isinstance(item, AnyType):
return AnyType()
if isinstance(item, Instance):
return type_object_type(item.type, self.named_type)
res = type_object_type(item.type, self.named_type)
if isinstance(res, CallableType):
res = res.copy_modified(from_type_type=True)
return res
if isinstance(item, UnionType):
return UnionType([self.analyze_type_type_callee(item, context)
for item in item.items], item.line)
Expand Down Expand Up @@ -840,6 +845,12 @@ def check_arg(self, caller_type: Type, original_caller_type: Type,
messages.does_not_return_value(caller_type, context)
elif isinstance(caller_type, DeletedType):
messages.deleted_as_rvalue(caller_type, context)
# Only non-abstract class could be given where Type[...] is expected

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could -> can (again :-)

elif (isinstance(caller_type, CallableType) and isinstance(callee_type, TypeType) and
caller_type.is_type_obj() and caller_type.type_object().is_abstract and
isinstance(callee_type.item, Instance) and callee_type.item.type.is_abstract):
messages.fail("Only non-abstract class can be given where '{}' is expected"
.format(callee_type), context)
elif not is_subtype(caller_type, callee_type):
if self.chk.should_suppress_optional_error([caller_type, callee_type]):
return
Expand Down
1 change: 0 additions & 1 deletion mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,6 @@ def class_callable(init_type: CallableType, info: TypeInfo, type_type: Instance,
ret_type=fill_typevars(info), fallback=type_type, name=None, variables=variables,
special_sig=special_sig)
c = callable_type.with_name('"{}"'.format(info.name()))
c.is_classmethod_class = True
return c


Expand Down
3 changes: 2 additions & 1 deletion mypy/meet.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,12 @@ class C(A, B): ...
elif isinstance(t, TypeType) or isinstance(s, TypeType):
# If exactly only one of t or s is a TypeType, check if one of them
# is an `object` or a `type` and otherwise assume no overlap.
one = t if isinstance(t, TypeType) else s
other = s if isinstance(t, TypeType) else t
if isinstance(other, Instance):
return other.type.fullname() in {'builtins.object', 'builtins.type'}
else:
return False
return isinstance(other, CallableType) and is_subtype(other, one)
if experiments.STRICT_OPTIONAL:
if isinstance(t, NoneTyp) != isinstance(s, NoneTyp):
# NoneTyp does not overlap with other non-Union types under strict Optional checking
Expand Down
14 changes: 12 additions & 2 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,8 @@ class CallableType(FunctionLike):
# Defined for signatures that require special handling (currently only value is 'dict'
# for a signature similar to 'dict')
special_sig = None # type: Optional[str]
# Was this callable generated by analyzing Type[...] instantiation?
from_type_type = False # type: bool

def __init__(self,
arg_types: List[Type],
Expand All @@ -591,6 +593,7 @@ def __init__(self,
implicit: bool = False,
is_classmethod_class: bool = False,
special_sig: Optional[str] = None,
from_type_type: bool = False,
) -> None:
if variables is None:
variables = []
Expand All @@ -609,7 +612,9 @@ def __init__(self,
self.variables = variables
self.is_ellipsis_args = is_ellipsis_args
self.implicit = implicit
self.is_classmethod_class = is_classmethod_class
self.special_sig = special_sig
self.from_type_type = from_type_type
super().__init__(line, column)

def copy_modified(self,
Expand All @@ -624,7 +629,8 @@ def copy_modified(self,
line: int = _dummy,
column: int = _dummy,
is_ellipsis_args: bool = _dummy,
special_sig: Optional[str] = _dummy) -> 'CallableType':
special_sig: Optional[str] = _dummy,
from_type_type: bool = _dummy) -> 'CallableType':
return CallableType(
arg_types=arg_types if arg_types is not _dummy else self.arg_types,
arg_kinds=arg_kinds if arg_kinds is not _dummy else self.arg_kinds,
Expand All @@ -641,6 +647,7 @@ def copy_modified(self,
implicit=self.implicit,
is_classmethod_class=self.is_classmethod_class,
special_sig=special_sig if special_sig is not _dummy else self.special_sig,
from_type_type=from_type_type if from_type_type is not _dummy else self.from_type_type,
)

def is_type_obj(self) -> bool:
Expand All @@ -655,7 +662,10 @@ def type_object(self) -> mypy.nodes.TypeInfo:
ret = self.ret_type
if isinstance(ret, TupleType):
ret = ret.fallback
return cast(Instance, ret).type
if isinstance(ret, TypeVarType):
ret = ret.upper_bound
assert isinstance(ret, Instance)
return ret.type

def accept(self, visitor: 'TypeVisitor[T]') -> T:
return visitor.visit_callable_type(self)
Expand Down
37 changes: 37 additions & 0 deletions test-data/unit/check-abstract.test
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,43 @@ class B(A): pass
B()# E: Cannot instantiate abstract class 'B' with abstract attributes 'f' and 'g'
[out]

[case testInstantiationAbstractsInType]
# flags: --python-version 3.6

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I take it this is only because of the var: Type[A] below? If this behavior is supposed to work for all Python versions maybe you can rewrite that part of the test using a function? (Also it's a rather long test; maybe it would be better split up.)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since recently --python-version flag is not needed. Also I split the test into three smaller and added a test case for @classmethod.

from typing import Type
from abc import abstractmethod

class A:
@abstractmethod
def m(self) -> None: pass
class B(A): pass
class C(B):
def m(self) -> None:
pass

def f(cls: Type[A]) -> A:
return cls() # OK

def g() -> A:
return A() # E: Cannot instantiate abstract class 'A' with abstract attribute 'm'

f(A) # E: Only non-abstract class can be given where 'Type[__main__.A]' is expected
f(B) # E: Only non-abstract class can be given where 'Type[__main__.A]' is expected
f(C) # OK

Alias = A
GoodAlias = C
Alias() # E: Cannot instantiate abstract class 'A' with abstract attribute 'm'
GoodAlias()
f(Alias) # E: Only non-abstract class can be given where 'Type[__main__.A]' is expected
f(GoodAlias)

var: Type[A]
var()
var = A # E: Cannot only assign non-abstract classes to a variable of type 'Type[__main__.A]'
var = B # E: Cannot only assign non-abstract classes to a variable of type 'Type[__main__.A]'
var = C # OK
[out]

[case testInstantiatingClassWithInheritedAbstractMethodAndSuppression]
from abc import abstractmethod, ABCMeta
import typing
Expand Down