From e818d935710f3a6f785ff6a083c5393fc03527db Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 8 Apr 2019 01:39:29 +0100 Subject: [PATCH 1/4] Support decorated constructors --- mypy/checkmember.py | 52 +++++++++++++++++++++---------- test-data/unit/check-classes.test | 37 ++++++++++++++++++++++ 2 files changed, 73 insertions(+), 16 deletions(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 2117194e7fba..03a8585ee695 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -1,6 +1,6 @@ """Type checking of attribute access""" -from typing import cast, Callable, List, Optional, TypeVar +from typing import cast, Callable, List, Optional, TypeVar, Union from mypy.types import ( Type, Instance, AnyType, TupleType, TypedDictType, CallableType, FunctionLike, TypeVarDef, @@ -763,25 +763,32 @@ def type_object_type(info: TypeInfo, builtin_type: Callable[[str], Instance]) -> # We take the type from whichever of __init__ and __new__ is first # in the MRO, preferring __init__ if there is a tie. - init_method = info.get_method('__init__') - new_method = info.get_method('__new__') - if not init_method: + init_method = info.get('__init__') + new_method = info.get('__new__') + if not init_method or not is_valid_constructor(init_method.node): # Must be an invalid class definition. return AnyType(TypeOfAny.from_error) # There *should* always be a __new__ method except the test stubs # lack it, so just copy init_method in that situation new_method = new_method or init_method + if not is_valid_constructor(new_method.node): + # Must be an invalid class definition. + return AnyType(TypeOfAny.from_error) - init_index = info.mro.index(init_method.info) - new_index = info.mro.index(new_method.info) + # The two is_valid_constructor() checks ensure this. + assert isinstance(new_method.node, (FuncBase, Decorator)) + assert isinstance(init_method.node, (FuncBase, Decorator)) + + init_index = info.mro.index(init_method.node.info) + new_index = info.mro.index(new_method.node.info) fallback = info.metaclass_type or builtin_type('builtins.type') if init_index < new_index: - method = init_method + method = init_method.node # type: Union[FuncBase, Decorator] elif init_index > new_index: - method = new_method + method = new_method.node else: - if init_method.info.fullname() == 'builtins.object': + if init_method.node.info.fullname() == 'builtins.object': # Both are defined by object. But if we've got a bogus # base class, we can't know for sure, so check for that. if info.fallback_to_any: @@ -797,17 +804,30 @@ def type_object_type(info: TypeInfo, builtin_type: Callable[[str], Instance]) -> # Otherwise prefer __init__ in a tie. It isn't clear that this # is the right thing, but __new__ caused problems with # typeshed (#5647). - method = init_method + method = init_method.node # Construct callable type based on signature of __init__. Adjust # return type and insert type arguments. - return type_object_type_from_function(method, info, fallback) + if isinstance(method, FuncBase): + t = function_type(method, fallback) + else: + assert isinstance(method.type, FunctionLike) # is_valid_constructor() ensures this + t = method.type + signature = bind_self(t) + return type_object_type_from_function(signature, info, method.info, fallback) -def type_object_type_from_function(init_or_new: FuncBase, +def is_valid_constructor(n: Optional[SymbolNode]) -> bool: + if isinstance(n, FuncBase): + return True + if isinstance(n, Decorator): + return isinstance(n.type, FunctionLike) + return False + + +def type_object_type_from_function(signature: FunctionLike, info: TypeInfo, + def_info: TypeInfo, fallback: Instance) -> FunctionLike: - signature = bind_self(function_type(init_or_new, fallback)) - # The __init__ method might come from a generic superclass # (init_or_new.info) with type variables that do not map # identically to the type variables of the class being constructed @@ -818,9 +838,9 @@ def type_object_type_from_function(init_or_new: FuncBase, # # We need to first map B's __init__ to the type (List[T]) -> None. signature = cast(FunctionLike, - map_type_from_supertype(signature, info, init_or_new.info)) + map_type_from_supertype(signature, info, def_info)) special_sig = None # type: Optional[str] - if init_or_new.info.fullname() == 'builtins.dict': + if def_info.fullname() == 'builtins.dict': # Special signature! special_sig = 'dict' diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 8710a7ee6b3c..02e2f21ec569 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -5812,3 +5812,40 @@ def test() -> None: return reveal_type(x) # E: Revealed type is 'Union[Type[__main__.One], Type[__main__.Other]]' [builtins fixtures/isinstancelist.pyi] + +[case testAbstractInit] +from abc import abstractmethod, ABCMeta +class A(metaclass=ABCMeta): + @abstractmethod + def __init__(self, a: int) -> None: + pass +class B(A): + pass +class C(B): + def __init__(self, a: int) -> None: + self.c = a +a = A(1) # E: Cannot instantiate abstract class 'A' with abstract attribute '__init__' +A.c # E: "Type[A]" has no attribute "c" +b = B(2) # E: Cannot instantiate abstract class 'B' with abstract attribute '__init__' +B.c # E: "Type[B]" has no attribute "c" +c = C(3) +c.c +C.c + +[case testDecoratedConstructors] +from typing import TypeVar, Callable, Any + +F = TypeVar('F', bound=Callable[..., Any]) + +def dec(f: F) -> F: ... + +class A: + @dec + def __init__(self, x: int) -> None: ... + +class B: + @dec + def __new__(cls, x: int) -> B: ... + +reveal_type(A) # E: Revealed type is 'def (x: builtins.int) -> __main__.A' +reveal_type(B) # E: Revealed type is 'def (x: builtins.int) -> __main__.B' From 312cef3a3fb480de45bf3d9184cce407278fc304 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 8 Apr 2019 09:03:46 +0100 Subject: [PATCH 2/4] Add a doctring --- mypy/checkmember.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 03a8585ee695..0b8dc03644fa 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -817,6 +817,11 @@ def type_object_type(info: TypeInfo, builtin_type: Callable[[str], Instance]) -> def is_valid_constructor(n: Optional[SymbolNode]) -> bool: + """Does this node represents a valid constructor method? + + This includes nomral functions, overloaded functions, and decorators + that return a callable type. + """ if isinstance(n, FuncBase): return True if isinstance(n, Decorator): From 2176fdd867619c8cbd44a783125241de6e4df66c Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 8 Apr 2019 09:21:43 +0100 Subject: [PATCH 3/4] Flag bad decorators for constructors --- mypy/checker.py | 4 ++++ mypy/message_registry.py | 1 + test-data/unit/check-classes.test | 13 +++++++++++++ 3 files changed, 18 insertions(+) diff --git a/mypy/checker.py b/mypy/checker.py index 83dc2ead9035..83362dbcb8d1 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3088,6 +3088,10 @@ def visit_decorator(self, e: Decorator) -> None: if e.func.info and not e.func.is_dynamic(): self.check_method_override(e) + if e.func.info and e.func.name() in ('__init__', '__new__'): + if e.type and not isinstance(e.type, (FunctionLike, AnyType)): + self.fail(message_registry.BAD_CONSTRUCTOR_TYPE, e) + def check_for_untyped_decorator(self, func: FuncDef, dec_type: Type, diff --git a/mypy/message_registry.py b/mypy/message_registry.py index 7279ea20dca5..020cea73f1c1 100644 --- a/mypy/message_registry.py +++ b/mypy/message_registry.py @@ -52,6 +52,7 @@ INVALID_SLICE_INDEX = 'Slice index must be an integer or None' # type: Final CANNOT_INFER_LAMBDA_TYPE = 'Cannot infer type of lambda' # type: Final CANNOT_ACCESS_INIT = 'Cannot access "__init__" directly' # type: Final +BAD_CONSTRUCTOR_TYPE = 'Unsupported decorated constructor type' # type: Final CANNOT_ASSIGN_TO_METHOD = 'Cannot assign to a method' # type: Final CANNOT_ASSIGN_TO_TYPE = 'Cannot assign to a type' # type: Final INCONSISTENT_ABSTRACT_OVERLOAD = \ diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 02e2f21ec569..2f782adf18ee 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -5849,3 +5849,16 @@ class B: reveal_type(A) # E: Revealed type is 'def (x: builtins.int) -> __main__.A' reveal_type(B) # E: Revealed type is 'def (x: builtins.int) -> __main__.B' + +[case testDecoratedConstructorsBad] +from typing import Callable, Any + +def dec(f: Callable[[Any, int], Any]) -> int: ... + +class A: + @dec # E: Unsupported decorated constructor type + def __init__(self, x: int) -> None: ... + +class B: + @dec # E: Unsupported decorated constructor type + def __new__(cls, x: int) -> B: ... From e9402629725ec5c6c9a338c3c8fd52feb68d4b67 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 26 Apr 2019 18:16:43 -0700 Subject: [PATCH 4/4] Address CR --- mypy/checkmember.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index aac1a2c4ae8b..e3ac94030e8a 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -813,14 +813,13 @@ def type_object_type(info: TypeInfo, builtin_type: Callable[[str], Instance]) -> else: assert isinstance(method.type, FunctionLike) # is_valid_constructor() ensures this t = method.type - signature = bind_self(t) - return type_object_type_from_function(signature, info, method.info, fallback) + return type_object_type_from_function(t, info, method.info, fallback) def is_valid_constructor(n: Optional[SymbolNode]) -> bool: """Does this node represents a valid constructor method? - This includes nomral functions, overloaded functions, and decorators + This includes normal functions, overloaded functions, and decorators that return a callable type. """ if isinstance(n, FuncBase): @@ -843,6 +842,7 @@ def type_object_type_from_function(signature: FunctionLike, # class B(A[List[T]], Generic[T]): pass # # We need to first map B's __init__ to the type (List[T]) -> None. + signature = bind_self(signature) signature = cast(FunctionLike, map_type_from_supertype(signature, info, def_info)) special_sig = None # type: Optional[str]