Skip to content

Mark abstract and protocol classes as not-instantiatable explicitly #6310

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all 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
6 changes: 2 additions & 4 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1808,12 +1808,10 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type
# Special case: only non-abstract non-protocol classes can be assigned to
# variables with explicit type Type[A], where A is protocol or abstract.
if (isinstance(rvalue_type, CallableType) and rvalue_type.is_type_obj() and
(rvalue_type.type_object().is_abstract or
rvalue_type.type_object().is_protocol) and
rvalue_type.type_object().is_not_instantiatable and
isinstance(lvalue_type, TypeType) and
isinstance(lvalue_type.item, Instance) and
(lvalue_type.item.type.is_abstract or
lvalue_type.item.type.is_protocol)):
lvalue_type.item.type.is_not_instantiatable):
self.msg.concrete_only_assign(lvalue_type, rvalue)
return
if rvalue_type and infer_lvalue_type and not isinstance(lvalue_type, PartialType):
Expand Down
28 changes: 14 additions & 14 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -746,19 +746,20 @@ def check_callable_call(self,
# An Enum() call that failed SemanticAnalyzerPass2.check_enum_call().
return callee.ret_type, callee

if (callee.is_type_obj() and callee.type_object().is_abstract
if (callee.is_type_obj() and callee.type_object().is_not_instantiatable
# Exception for Type[...]
and not callee.from_type_type
and not callee.type_object().fallback_to_any):
and not callee.from_type_type):
type = callee.type_object()
self.msg.cannot_instantiate_abstract_class(
callee.type_object().name(), type.abstract_attributes,
context)
elif (callee.is_type_obj() and callee.type_object().is_protocol
# Exception for Type[...]
and not callee.from_type_type):
self.chk.fail(message_registry.CANNOT_INSTANTIATE_PROTOCOL
.format(callee.type_object().name()), context)
if type.is_abstract and not callee.type_object().fallback_to_any:
self.msg.cannot_instantiate_abstract_class(
callee.type_object().name(), type.abstract_attributes,
context)
elif type.is_protocol:
self.chk.fail(message_registry.CANNOT_INSTANTIATE_PROTOCOL
.format(type.name()), context)
elif not (type.is_abstract or type.is_protocol):
# Other reason (e.g. marked as not-instantiatable by a plugin)
self.chk.fail(message_registry.CANNOT_INSTANTIATE.format(type.name()), context)

formal_to_actual = map_actuals_to_formals(
arg_kinds, arg_names,
Expand Down Expand Up @@ -1249,10 +1250,9 @@ def check_arg(self, caller_type: Type, original_caller_type: Type,
messages.deleted_as_rvalue(caller_type, context)
# Only non-abstract non-protocol class can be given where Type[...] is expected...
elif (isinstance(caller_type, CallableType) and isinstance(callee_type, TypeType) and
caller_type.is_type_obj() and
(caller_type.type_object().is_abstract or caller_type.type_object().is_protocol) and
caller_type.is_type_obj() and caller_type.type_object().is_not_instantiatable and
isinstance(callee_type.item, Instance) and
(callee_type.item.type.is_abstract or callee_type.item.type.is_protocol)):
callee_type.item.type.is_not_instantiatable):
self.msg.concrete_only_call(callee_type, context)
elif not is_subtype(caller_type, callee_type):
if self.chk.should_suppress_optional_error([caller_type, callee_type]):
Expand Down
1 change: 1 addition & 0 deletions mypy/message_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
DESCRIPTOR_SET_NOT_CALLABLE = "{}.__set__ is not callable" # type: Final
DESCRIPTOR_GET_NOT_CALLABLE = "{}.__get__ is not callable" # type: Final
MODULE_LEVEL_GETATTRIBUTE = '__getattribute__ is not valid at the module level' # type: Final
CANNOT_INSTANTIATE = 'Class "{}" is not allowed to be instantiated' # type: Final

# Generic
GENERIC_INSTANCE_VAR_CLASS_ACCESS = \
Expand Down
3 changes: 3 additions & 0 deletions mypy/newsemanal/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,7 @@ def analyze_class(self, defn: ClassDef) -> None:
if self.analyze_namedtuple_classdef(defn):
return

defn.info.is_not_instantiatable = is_protocol
defn.info.is_protocol = is_protocol
self.analyze_metaclass(defn)
defn.info.runtime_protocol = False
Expand Down Expand Up @@ -988,12 +989,14 @@ def calculate_abstract_status(self, typ: TypeInfo) -> None:
if isinstance(func, Decorator):
fdef = func.func
if fdef.is_abstract and name not in concrete:
typ.is_not_instantiatable = True
typ.is_abstract = True
abstract.append(name)
if base is typ:
abstract_in_this_class.append(name)
elif isinstance(node, Var):
if node.is_abstract_var and name not in concrete:
typ.is_not_instantiatable = True
typ.is_abstract = True
abstract.append(name)
if base is typ:
Expand Down
3 changes: 3 additions & 0 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2167,6 +2167,7 @@ class is generic then it will be a type constructor of higher kind.
metaclass_type = None # type: Optional[mypy.types.Instance]

names = None # type: SymbolTable # Names defined directly in this type
is_not_instantiatable = False # Is it allowed to instantiate the class?
is_abstract = False # Does the class have any abstract attributes?
is_protocol = False # Is this a protocol class?
runtime_protocol = False # Does this protocol support isinstance checks?
Expand Down Expand Up @@ -2261,6 +2262,7 @@ class is generic then it will be a type constructor of higher kind.
FLAGS = [
'is_abstract', 'is_enum', 'fallback_to_any', 'is_named_tuple',
'is_newtype', 'is_protocol', 'runtime_protocol', 'is_final',
'is_not_instantiatable',
] # type: Final[List[str]]

def __init__(self, names: 'SymbolTable', defn: ClassDef, module_name: str) -> None:
Expand All @@ -2274,6 +2276,7 @@ def __init__(self, names: 'SymbolTable', defn: ClassDef, module_name: str) -> No
self.mro = []
self._fullname = defn.fullname
self.is_abstract = False
self.is_not_instantiatable = False
self.abstract_attributes = []
self.assuming = []
self.assuming_proper = []
Expand Down
3 changes: 3 additions & 0 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,7 @@ def analyze_class(self, defn: ClassDef) -> None:
return
self.setup_class_def_analysis(defn)
self.analyze_base_classes(defn)
defn.info.is_not_instantiatable = is_protocol
defn.info.is_protocol = is_protocol
self.analyze_metaclass(defn)
defn.info.runtime_protocol = False
Expand Down Expand Up @@ -959,12 +960,14 @@ def calculate_abstract_status(self, typ: TypeInfo) -> None:
if isinstance(func, Decorator):
fdef = func.func
if fdef.is_abstract and name not in concrete:
typ.is_not_instantiatable = True
typ.is_abstract = True
abstract.append(name)
if base is typ:
abstract_in_this_class.append(name)
elif isinstance(node, Var):
if node.is_abstract_var and name not in concrete:
typ.is_not_instantiatable = True
typ.is_abstract = True
abstract.append(name)
if base is typ:
Expand Down
17 changes: 17 additions & 0 deletions test-data/unit/check-custom-plugin.test
Original file line number Diff line number Diff line change
Expand Up @@ -536,3 +536,20 @@ func(1, 2, [3, 4], *[5, 6, 7], **{'a': 1}) # E: [[0, 0, 0, 2], [4]]
[file mypy.ini]
[[mypy]
plugins=<ROOT>/test-data/unit/plugins/arg_kinds.py

[case testNotInstantiatable]
# flags: --config-file tmp/mypy.ini
def static_only(cls):
return cls

@static_only
class CannotBeInstantiated:
ONE = 1

ins = CannotBeInstantiated()
[file mypy.ini]
[[mypy]
plugins=<ROOT>/test-data/unit/plugins/staticonly.py

[out]
main:9: error: Class "CannotBeInstantiated" is not allowed to be instantiated
15 changes: 15 additions & 0 deletions test-data/unit/plugins/staticonly.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from mypy.plugin import Plugin

class MyPlugin(Plugin):
def get_class_decorator_hook(self, fullname):
if fullname == '__main__.static_only':
return static_only_hook
assert fullname is not None
return None

def static_only_hook(ctx):
typeinfo = ctx.cls.info
typeinfo.is_not_instantiatable = True

def plugin(version):
return MyPlugin