From 07816954f7da84713ad157b031b4a96b0bea5695 Mon Sep 17 00:00:00 2001 From: Andrey Lebedev Date: Sat, 2 Feb 2019 23:26:15 +0200 Subject: [PATCH] Mark abstract and protocol classes as not-instantiatable explicitly Only show error when not-instantiatable class is instantiated. This will allow plugins to allow instantiation of certain classes, even if they are abstract or specify a protocol. The particular problem that called for this solution: [mypy-zope](https://github.com/Shoobx/mypy-zope) plugin tries to implement zope interfaces as "protocols". Zope has the adaptation pattern that looks like instantiation of an interface: ```python adapter = IMyInterface(context) ``` since `IMyInterface` is marked as "protocol", mypy unconditionally rejects such syntax with the message ``` error: Cannot instantiate protocol class "IMyInterface" ``` The proposed change would allow zope plugin to mark `IMyInterface` as both "protocol" and "instantiatable", effectively disabling the error message. --- mypy/checker.py | 6 ++---- mypy/checkexpr.py | 28 ++++++++++++------------- mypy/message_registry.py | 1 + mypy/newsemanal/semanal.py | 3 +++ mypy/nodes.py | 3 +++ mypy/semanal.py | 3 +++ test-data/unit/check-custom-plugin.test | 17 +++++++++++++++ test-data/unit/plugins/staticonly.py | 15 +++++++++++++ 8 files changed, 58 insertions(+), 18 deletions(-) create mode 100644 test-data/unit/plugins/staticonly.py diff --git a/mypy/checker.py b/mypy/checker.py index 525726e42f05..995674da3dc2 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -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): diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index d57832e34acc..a505f8555d35 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -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, @@ -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]): diff --git a/mypy/message_registry.py b/mypy/message_registry.py index 7279ea20dca5..676b1399d0ff 100644 --- a/mypy/message_registry.py +++ b/mypy/message_registry.py @@ -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 = \ diff --git a/mypy/newsemanal/semanal.py b/mypy/newsemanal/semanal.py index bcb8be844c15..87aeddcfba55 100644 --- a/mypy/newsemanal/semanal.py +++ b/mypy/newsemanal/semanal.py @@ -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 @@ -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: diff --git a/mypy/nodes.py b/mypy/nodes.py index 66ca58714f8b..9c044add7bf2 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -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? @@ -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: @@ -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 = [] diff --git a/mypy/semanal.py b/mypy/semanal.py index cf5224419b37..0475a96a5471 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -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 @@ -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: diff --git a/test-data/unit/check-custom-plugin.test b/test-data/unit/check-custom-plugin.test index 5505d487ab7e..c6bc4f61b1a2 100644 --- a/test-data/unit/check-custom-plugin.test +++ b/test-data/unit/check-custom-plugin.test @@ -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=/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=/test-data/unit/plugins/staticonly.py + +[out] +main:9: error: Class "CannotBeInstantiated" is not allowed to be instantiated diff --git a/test-data/unit/plugins/staticonly.py b/test-data/unit/plugins/staticonly.py new file mode 100644 index 000000000000..f620c302f87f --- /dev/null +++ b/test-data/unit/plugins/staticonly.py @@ -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