Skip to content

Commit

Permalink
Honor return type of __new__
Browse files Browse the repository at this point in the history
This basically follows the approach Jukka laid out in #1020 four years ago:
 * If the return type is Any, ignore that and keep the class type as
   the return type
 * Otherwise respect `__new__`'s return type
 * Produce an error if the return type is not a subtype of the class.

The main motivation for me in implementing this is to support
overloading `__new__` in order to select type variable arguments,
which will be useful for subprocess.Popen.

Fixes #1020.
  • Loading branch information
msullivan committed Jul 10, 2019
1 parent b279f4c commit c475994
Show file tree
Hide file tree
Showing 11 changed files with 184 additions and 40 deletions.
25 changes: 25 additions & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,10 @@ def check_func_def(self, defn: FuncItem, typ: CallableType, name: Optional[str])
self.fail(message_registry.MUST_HAVE_NONE_RETURN_TYPE.format(fdef.name()),
item)

# Check validity of __new__ signature
if fdef.info and fdef.name() == '__new__':
self.check___new___signature(fdef, typ)

self.check_for_missing_annotations(fdef)
if self.options.disallow_any_unimported:
if fdef.type and isinstance(fdef.type, CallableType):
Expand Down Expand Up @@ -1014,6 +1018,27 @@ def is_unannotated_any(t: Type) -> bool:
if any(is_unannotated_any(t) for t in fdef.type.arg_types):
self.fail(message_registry.ARGUMENT_TYPE_EXPECTED, fdef)

def check___new___signature(self, fdef: FuncDef, typ: CallableType) -> None:
self_type = fill_typevars_with_any(fdef.info)
bound_type = bind_self(typ, self_type, is_classmethod=True)
# Check that __new__ (after binding cls) returns an instance
# type (or any)
if not isinstance(bound_type.ret_type, (AnyType, Instance, TupleType)):
self.fail(
message_registry.NON_INSTANCE_NEW_TYPE.format(
self.msg.format(bound_type.ret_type)),
fdef)
else:
# And that it returns a subtype of the class
self.check_subtype(
bound_type.ret_type,
self_type,
fdef,
message_registry.INVALID_NEW_TYPE,
'returns',
'but must return a subtype of'
)

def is_trivial_body(self, block: Block) -> bool:
"""Returns 'true' if the given body is "trivial" -- if it contains just a "pass",
"..." (ellipsis), or "raise NotImplementedError()". A trivial body may also
Expand Down
25 changes: 18 additions & 7 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -787,8 +787,10 @@ def type_object_type(info: TypeInfo, builtin_type: Callable[[str], Instance]) ->
fallback = info.metaclass_type or builtin_type('builtins.type')
if init_index < new_index:
method = init_method.node # type: Union[FuncBase, Decorator]
is_new = False
elif init_index > new_index:
method = new_method.node
is_new = True
else:
if init_method.node.info.fullname() == 'builtins.object':
# Both are defined by object. But if we've got a bogus
Expand All @@ -807,14 +809,15 @@ def type_object_type(info: TypeInfo, builtin_type: Callable[[str], Instance]) ->
# is the right thing, but __new__ caused problems with
# typeshed (#5647).
method = init_method.node
is_new = False
# Construct callable type based on signature of __init__. Adjust
# return type and insert type arguments.
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)
return type_object_type_from_function(t, info, method.info, fallback, is_new)


def is_valid_constructor(n: Optional[SymbolNode]) -> bool:
Expand All @@ -833,7 +836,8 @@ def is_valid_constructor(n: Optional[SymbolNode]) -> bool:
def type_object_type_from_function(signature: FunctionLike,
info: TypeInfo,
def_info: TypeInfo,
fallback: Instance) -> FunctionLike:
fallback: Instance,
is_new: bool) -> FunctionLike:
# 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 @@ -843,7 +847,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 = bind_self(signature, original_type=fill_typevars(info), is_classmethod=is_new)
signature = cast(FunctionLike,
map_type_from_supertype(signature, info, def_info))
special_sig = None # type: Optional[str]
Expand All @@ -852,25 +856,32 @@ def type_object_type_from_function(signature: FunctionLike,
special_sig = 'dict'

if isinstance(signature, CallableType):
return class_callable(signature, info, fallback, special_sig)
return class_callable(signature, info, fallback, special_sig, is_new)
else:
# Overloaded __init__/__new__.
assert isinstance(signature, Overloaded)
items = [] # type: List[CallableType]
for item in signature.items():
items.append(class_callable(item, info, fallback, special_sig))
items.append(class_callable(item, info, fallback, special_sig, is_new))
return Overloaded(items)


def class_callable(init_type: CallableType, info: TypeInfo, type_type: Instance,
special_sig: Optional[str]) -> CallableType:
special_sig: Optional[str],
is_new: bool = False) -> CallableType:
"""Create a type object type based on the signature of __init__."""
variables = [] # type: List[TypeVarDef]
variables.extend(info.defn.type_vars)
variables.extend(init_type.variables)

is_new = True
if is_new and isinstance(init_type.ret_type, (Instance, TupleType)):
ret_type = init_type.ret_type # type: Type
else:
ret_type = fill_typevars(info)

callable_type = init_type.copy_modified(
ret_type=fill_typevars(info), fallback=type_type, name=None, variables=variables,
ret_type=ret_type, fallback=type_type, name=None, variables=variables,
special_sig=special_sig)
c = callable_type.with_name(info.name())
return c
Expand Down
3 changes: 2 additions & 1 deletion mypy/interpreted_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ class InterpretedPlugin:
that proxies to this interpreted version.
"""

def __new__(cls, *args: Any, **kwargs: Any) -> 'mypy.plugin.Plugin':
# ... mypy doesn't like these shenanigans so we have to type ignore it!
def __new__(cls, *args: Any, **kwargs: Any) -> 'mypy.plugin.Plugin': # type: ignore
from mypy.plugin import WrapperPlugin
plugin = object.__new__(cls)
plugin.__init__(*args, **kwargs)
Expand Down
2 changes: 2 additions & 0 deletions mypy/message_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
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
NON_INSTANCE_NEW_TYPE = '"__new__" must return a class instance (got {})' # type: Final
INVALID_NEW_TYPE = 'Incompatible return type for "__new__"' # 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
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/check-class-namedtuple.test
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,7 @@ class XMethBad(NamedTuple):
class MagicalFields(NamedTuple):
x: int
def __slots__(self) -> None: pass # E: Cannot overwrite NamedTuple attribute "__slots__"
def __new__(cls) -> None: pass # E: Cannot overwrite NamedTuple attribute "__new__"
def __new__(cls) -> MagicalFields: pass # E: Cannot overwrite NamedTuple attribute "__new__"
def _source(self) -> int: pass # E: Cannot overwrite NamedTuple attribute "_source"
__annotations__ = {'x': float} # E: NamedTuple field name cannot start with an underscore: __annotations__ \
# E: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]" \
Expand Down
114 changes: 108 additions & 6 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -343,12 +343,12 @@ main:6: error: Return type "A" of "f" incompatible with return type "None" in su

[case testOverride__new__WithDifferentSignature]
class A:
def __new__(cls, x: int) -> str:
return ''
def __new__(cls, x: int) -> A:
pass

class B(A):
def __new__(cls) -> int:
return 1
def __new__(cls) -> B:
pass

[case testOverride__new__AndCallObject]
from typing import TypeVar, Generic
Expand Down Expand Up @@ -5359,8 +5359,8 @@ class A:
pass

class B(A):
def __new__(cls) -> int:
return 10
def __new__(cls) -> B:
pass

B()

Expand Down Expand Up @@ -5925,3 +5925,105 @@ class B:

class C(A, B): pass
[out]

[case testNewReturnType1]
class A:
def __new__(cls) -> B:
pass

class B(A): pass

reveal_type(A()) # N: Revealed type is '__main__.B'
reveal_type(B()) # N: Revealed type is '__main__.B'

[case testNewReturnType2]
from typing import Any

# make sure that __new__ method that return Any are ignored when
# determining the return type
class A:
def __new__(cls):
pass

class B:
def __new__(cls) -> Any:
pass

reveal_type(A()) # N: Revealed type is '__main__.A'
reveal_type(B()) # N: Revealed type is '__main__.B'

[case testNewReturnType3]

# Check for invalid __new__ typing

class A:
def __new__(cls) -> int: # E: Incompatible return type for "__new__" (returns "int", but must return a subtype of "A")
pass

reveal_type(A()) # N: Revealed type is 'builtins.int'

[case testNewReturnType4]
from typing import TypeVar, Type

# Check for __new__ using type vars

TX = TypeVar('TX', bound='X')
class X:
def __new__(lol: Type[TX], x: int) -> TX:
pass
class Y(X): pass

reveal_type(X(20)) # N: Revealed type is '__main__.X*'
reveal_type(Y(20)) # N: Revealed type is '__main__.Y*'

[case testNewReturnType5]
from typing import Any, TypeVar, Generic, overload

T = TypeVar('T')
class O(Generic[T]):
@overload
def __new__(cls) -> O[int]:
pass
@overload
def __new__(cls, x: int) -> O[str]:
pass
def __new__(cls, x: int = 0) -> O[Any]:
pass

reveal_type(O()) # N: Revealed type is '__main__.O[builtins.int]'
reveal_type(O(10)) # N: Revealed type is '__main__.O[builtins.str]'

[case testNewReturnType6]
from typing import Tuple, Optional

# Check for some cases that aren't allowed

class X:
def __new__(cls) -> Optional[Y]: # E: "__new__" must return a class instance (got "Optional[Y]")
pass
class Y:
def __new__(cls) -> Optional[int]: # E: "__new__" must return a class instance (got "Optional[int]")
pass


[case testNewReturnType7]
from typing import NamedTuple

# ... test __new__ returning tuple type
class A:
def __new__(cls) -> 'B':
pass

N = NamedTuple('N', [('x', int)])
class B(A, N): pass

reveal_type(A()) # N: Revealed type is 'Tuple[builtins.int, fallback=__main__.B]'

[case testNewReturnType8]
from typing import TypeVar, Any

# test type var from a different argument
TX = TypeVar('TX', bound='X')
class X:
def __new__(cls, x: TX) -> TX: # E: "__new__" must return a class instance (got "TX")
pass
Loading

0 comments on commit c475994

Please sign in to comment.