Skip to content

Support decorated constructors #6645

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

Merged
merged 5 commits into from
Apr 27, 2019
Merged
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
4 changes: 4 additions & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -3124,6 +3124,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,
Expand Down
57 changes: 41 additions & 16 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -764,25 +764,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)

# 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.info)
new_index = info.mro.index(new_method.info)
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:
Expand All @@ -798,17 +805,34 @@ 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
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?

def type_object_type_from_function(init_or_new: FuncBase,
This includes normal functions, overloaded functions, and decorators
that return a callable type.
"""
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
Expand All @@ -818,10 +842,11 @@ def type_object_type_from_function(init_or_new: FuncBase,
# 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, 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'

Expand Down
1 change: 1 addition & 0 deletions mypy/message_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,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 = \
Expand Down
50 changes: 50 additions & 0 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -5837,3 +5837,53 @@ 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'

[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: ...