Skip to content

Commit b778c87

Browse files
authored
Support decorated constructors (#6645)
Fixes #1706 Fixes #5398 Previously, decorated constructors either resulted in silent `Any` types for class objects (in case of a decorated `__init__()`) or incorrect signatures (in case of a decorated `__new__()`). This PR adds support for decorated constructors, implementation is straightforward.
1 parent bd01d46 commit b778c87

File tree

4 files changed

+96
-16
lines changed

4 files changed

+96
-16
lines changed

mypy/checker.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3124,6 +3124,10 @@ def visit_decorator(self, e: Decorator) -> None:
31243124
if e.func.info and not e.func.is_dynamic():
31253125
self.check_method_override(e)
31263126

3127+
if e.func.info and e.func.name() in ('__init__', '__new__'):
3128+
if e.type and not isinstance(e.type, (FunctionLike, AnyType)):
3129+
self.fail(message_registry.BAD_CONSTRUCTOR_TYPE, e)
3130+
31273131
def check_for_untyped_decorator(self,
31283132
func: FuncDef,
31293133
dec_type: Type,

mypy/checkmember.py

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Type checking of attribute access"""
22

3-
from typing import cast, Callable, List, Optional, TypeVar
3+
from typing import cast, Callable, List, Optional, TypeVar, Union
44

55
from mypy.types import (
66
Type, Instance, AnyType, TupleType, TypedDictType, CallableType, FunctionLike, TypeVarDef,
@@ -764,25 +764,32 @@ def type_object_type(info: TypeInfo, builtin_type: Callable[[str], Instance]) ->
764764

765765
# We take the type from whichever of __init__ and __new__ is first
766766
# in the MRO, preferring __init__ if there is a tie.
767-
init_method = info.get_method('__init__')
768-
new_method = info.get_method('__new__')
769-
if not init_method:
767+
init_method = info.get('__init__')
768+
new_method = info.get('__new__')
769+
if not init_method or not is_valid_constructor(init_method.node):
770770
# Must be an invalid class definition.
771771
return AnyType(TypeOfAny.from_error)
772772
# There *should* always be a __new__ method except the test stubs
773773
# lack it, so just copy init_method in that situation
774774
new_method = new_method or init_method
775+
if not is_valid_constructor(new_method.node):
776+
# Must be an invalid class definition.
777+
return AnyType(TypeOfAny.from_error)
778+
779+
# The two is_valid_constructor() checks ensure this.
780+
assert isinstance(new_method.node, (FuncBase, Decorator))
781+
assert isinstance(init_method.node, (FuncBase, Decorator))
775782

776-
init_index = info.mro.index(init_method.info)
777-
new_index = info.mro.index(new_method.info)
783+
init_index = info.mro.index(init_method.node.info)
784+
new_index = info.mro.index(new_method.node.info)
778785

779786
fallback = info.metaclass_type or builtin_type('builtins.type')
780787
if init_index < new_index:
781-
method = init_method
788+
method = init_method.node # type: Union[FuncBase, Decorator]
782789
elif init_index > new_index:
783-
method = new_method
790+
method = new_method.node
784791
else:
785-
if init_method.info.fullname() == 'builtins.object':
792+
if init_method.node.info.fullname() == 'builtins.object':
786793
# Both are defined by object. But if we've got a bogus
787794
# base class, we can't know for sure, so check for that.
788795
if info.fallback_to_any:
@@ -798,17 +805,34 @@ def type_object_type(info: TypeInfo, builtin_type: Callable[[str], Instance]) ->
798805
# Otherwise prefer __init__ in a tie. It isn't clear that this
799806
# is the right thing, but __new__ caused problems with
800807
# typeshed (#5647).
801-
method = init_method
808+
method = init_method.node
802809
# Construct callable type based on signature of __init__. Adjust
803810
# return type and insert type arguments.
804-
return type_object_type_from_function(method, info, fallback)
811+
if isinstance(method, FuncBase):
812+
t = function_type(method, fallback)
813+
else:
814+
assert isinstance(method.type, FunctionLike) # is_valid_constructor() ensures this
815+
t = method.type
816+
return type_object_type_from_function(t, info, method.info, fallback)
817+
805818

819+
def is_valid_constructor(n: Optional[SymbolNode]) -> bool:
820+
"""Does this node represents a valid constructor method?
806821
807-
def type_object_type_from_function(init_or_new: FuncBase,
822+
This includes normal functions, overloaded functions, and decorators
823+
that return a callable type.
824+
"""
825+
if isinstance(n, FuncBase):
826+
return True
827+
if isinstance(n, Decorator):
828+
return isinstance(n.type, FunctionLike)
829+
return False
830+
831+
832+
def type_object_type_from_function(signature: FunctionLike,
808833
info: TypeInfo,
834+
def_info: TypeInfo,
809835
fallback: Instance) -> FunctionLike:
810-
signature = bind_self(function_type(init_or_new, fallback))
811-
812836
# The __init__ method might come from a generic superclass
813837
# (init_or_new.info) with type variables that do not map
814838
# identically to the type variables of the class being constructed
@@ -818,10 +842,11 @@ def type_object_type_from_function(init_or_new: FuncBase,
818842
# class B(A[List[T]], Generic[T]): pass
819843
#
820844
# We need to first map B's __init__ to the type (List[T]) -> None.
845+
signature = bind_self(signature)
821846
signature = cast(FunctionLike,
822-
map_type_from_supertype(signature, info, init_or_new.info))
847+
map_type_from_supertype(signature, info, def_info))
823848
special_sig = None # type: Optional[str]
824-
if init_or_new.info.fullname() == 'builtins.dict':
849+
if def_info.fullname() == 'builtins.dict':
825850
# Special signature!
826851
special_sig = 'dict'
827852

mypy/message_registry.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
INVALID_SLICE_INDEX = 'Slice index must be an integer or None' # type: Final
5858
CANNOT_INFER_LAMBDA_TYPE = 'Cannot infer type of lambda' # type: Final
5959
CANNOT_ACCESS_INIT = 'Cannot access "__init__" directly' # type: Final
60+
BAD_CONSTRUCTOR_TYPE = 'Unsupported decorated constructor type' # type: Final
6061
CANNOT_ASSIGN_TO_METHOD = 'Cannot assign to a method' # type: Final
6162
CANNOT_ASSIGN_TO_TYPE = 'Cannot assign to a type' # type: Final
6263
INCONSISTENT_ABSTRACT_OVERLOAD = \

test-data/unit/check-classes.test

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5837,3 +5837,53 @@ def test() -> None:
58375837
return
58385838
reveal_type(x) # E: Revealed type is 'Union[Type[__main__.One], Type[__main__.Other]]'
58395839
[builtins fixtures/isinstancelist.pyi]
5840+
5841+
[case testAbstractInit]
5842+
from abc import abstractmethod, ABCMeta
5843+
class A(metaclass=ABCMeta):
5844+
@abstractmethod
5845+
def __init__(self, a: int) -> None:
5846+
pass
5847+
class B(A):
5848+
pass
5849+
class C(B):
5850+
def __init__(self, a: int) -> None:
5851+
self.c = a
5852+
a = A(1) # E: Cannot instantiate abstract class 'A' with abstract attribute '__init__'
5853+
A.c # E: "Type[A]" has no attribute "c"
5854+
b = B(2) # E: Cannot instantiate abstract class 'B' with abstract attribute '__init__'
5855+
B.c # E: "Type[B]" has no attribute "c"
5856+
c = C(3)
5857+
c.c
5858+
C.c
5859+
5860+
[case testDecoratedConstructors]
5861+
from typing import TypeVar, Callable, Any
5862+
5863+
F = TypeVar('F', bound=Callable[..., Any])
5864+
5865+
def dec(f: F) -> F: ...
5866+
5867+
class A:
5868+
@dec
5869+
def __init__(self, x: int) -> None: ...
5870+
5871+
class B:
5872+
@dec
5873+
def __new__(cls, x: int) -> B: ...
5874+
5875+
reveal_type(A) # E: Revealed type is 'def (x: builtins.int) -> __main__.A'
5876+
reveal_type(B) # E: Revealed type is 'def (x: builtins.int) -> __main__.B'
5877+
5878+
[case testDecoratedConstructorsBad]
5879+
from typing import Callable, Any
5880+
5881+
def dec(f: Callable[[Any, int], Any]) -> int: ...
5882+
5883+
class A:
5884+
@dec # E: Unsupported decorated constructor type
5885+
def __init__(self, x: int) -> None: ...
5886+
5887+
class B:
5888+
@dec # E: Unsupported decorated constructor type
5889+
def __new__(cls, x: int) -> B: ...

0 commit comments

Comments
 (0)