Skip to content

Commit 25b899c

Browse files
committed
Honor return type of __new__
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.
1 parent 2ca892b commit 25b899c

11 files changed

+184
-40
lines changed

mypy/checker.py

+25
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,10 @@ def check_func_def(self, defn: FuncItem, typ: CallableType, name: Optional[str])
801801
self.fail(message_registry.MUST_HAVE_NONE_RETURN_TYPE.format(fdef.name()),
802802
item)
803803

804+
# Check validity of __new__ signature
805+
if fdef.info and fdef.name() == '__new__':
806+
self.check___new___signature(fdef, typ)
807+
804808
self.check_for_missing_annotations(fdef)
805809
if self.options.disallow_any_unimported:
806810
if fdef.type and isinstance(fdef.type, CallableType):
@@ -1005,6 +1009,27 @@ def is_unannotated_any(t: Type) -> bool:
10051009
if any(is_unannotated_any(t) for t in fdef.type.arg_types):
10061010
self.fail(message_registry.ARGUMENT_TYPE_EXPECTED, fdef)
10071011

1012+
def check___new___signature(self, fdef: FuncDef, typ: CallableType) -> None:
1013+
self_type = fill_typevars_with_any(fdef.info)
1014+
bound_type = bind_self(typ, self_type, is_classmethod=True)
1015+
# Check that __new__ (after binding cls) returns an instance
1016+
# type (or any)
1017+
if not isinstance(bound_type.ret_type, (AnyType, Instance, TupleType)):
1018+
self.fail(
1019+
message_registry.NON_INSTANCE_NEW_TYPE.format(
1020+
self.msg.format(bound_type.ret_type)),
1021+
fdef)
1022+
else:
1023+
# And that it returns a subtype of the class
1024+
self.check_subtype(
1025+
bound_type.ret_type,
1026+
self_type,
1027+
fdef,
1028+
message_registry.INVALID_NEW_TYPE,
1029+
'returns',
1030+
'but must return a subtype of'
1031+
)
1032+
10081033
def is_trivial_body(self, block: Block) -> bool:
10091034
"""Returns 'true' if the given body is "trivial" -- if it contains just a "pass",
10101035
"..." (ellipsis), or "raise NotImplementedError()". A trivial body may also

mypy/checkmember.py

+18-7
Original file line numberDiff line numberDiff line change
@@ -787,8 +787,10 @@ def type_object_type(info: TypeInfo, builtin_type: Callable[[str], Instance]) ->
787787
fallback = info.metaclass_type or builtin_type('builtins.type')
788788
if init_index < new_index:
789789
method = init_method.node # type: Union[FuncBase, Decorator]
790+
is_new = False
790791
elif init_index > new_index:
791792
method = new_method.node
793+
is_new = True
792794
else:
793795
if init_method.node.info.fullname() == 'builtins.object':
794796
# Both are defined by object. But if we've got a bogus
@@ -807,14 +809,15 @@ def type_object_type(info: TypeInfo, builtin_type: Callable[[str], Instance]) ->
807809
# is the right thing, but __new__ caused problems with
808810
# typeshed (#5647).
809811
method = init_method.node
812+
is_new = False
810813
# Construct callable type based on signature of __init__. Adjust
811814
# return type and insert type arguments.
812815
if isinstance(method, FuncBase):
813816
t = function_type(method, fallback)
814817
else:
815818
assert isinstance(method.type, FunctionLike) # is_valid_constructor() ensures this
816819
t = method.type
817-
return type_object_type_from_function(t, info, method.info, fallback)
820+
return type_object_type_from_function(t, info, method.info, fallback, is_new)
818821

819822

820823
def is_valid_constructor(n: Optional[SymbolNode]) -> bool:
@@ -833,7 +836,8 @@ def is_valid_constructor(n: Optional[SymbolNode]) -> bool:
833836
def type_object_type_from_function(signature: FunctionLike,
834837
info: TypeInfo,
835838
def_info: TypeInfo,
836-
fallback: Instance) -> FunctionLike:
839+
fallback: Instance,
840+
is_new: bool) -> FunctionLike:
837841
# The __init__ method might come from a generic superclass
838842
# (init_or_new.info) with type variables that do not map
839843
# identically to the type variables of the class being constructed
@@ -843,7 +847,7 @@ def type_object_type_from_function(signature: FunctionLike,
843847
# class B(A[List[T]], Generic[T]): pass
844848
#
845849
# We need to first map B's __init__ to the type (List[T]) -> None.
846-
signature = bind_self(signature)
850+
signature = bind_self(signature, original_type=fill_typevars(info), is_classmethod=is_new)
847851
signature = cast(FunctionLike,
848852
map_type_from_supertype(signature, info, def_info))
849853
special_sig = None # type: Optional[str]
@@ -852,25 +856,32 @@ def type_object_type_from_function(signature: FunctionLike,
852856
special_sig = 'dict'
853857

854858
if isinstance(signature, CallableType):
855-
return class_callable(signature, info, fallback, special_sig)
859+
return class_callable(signature, info, fallback, special_sig, is_new)
856860
else:
857861
# Overloaded __init__/__new__.
858862
assert isinstance(signature, Overloaded)
859863
items = [] # type: List[CallableType]
860864
for item in signature.items():
861-
items.append(class_callable(item, info, fallback, special_sig))
865+
items.append(class_callable(item, info, fallback, special_sig, is_new))
862866
return Overloaded(items)
863867

864868

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

877+
is_new = True
878+
if is_new and isinstance(init_type.ret_type, (Instance, TupleType)):
879+
ret_type = init_type.ret_type # type: Type
880+
else:
881+
ret_type = fill_typevars(info)
882+
872883
callable_type = init_type.copy_modified(
873-
ret_type=fill_typevars(info), fallback=type_type, name=None, variables=variables,
884+
ret_type=ret_type, fallback=type_type, name=None, variables=variables,
874885
special_sig=special_sig)
875886
c = callable_type.with_name(info.name())
876887
return c

mypy/interpreted_plugin.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ class InterpretedPlugin:
2222
that proxies to this interpreted version.
2323
"""
2424

25-
def __new__(cls, *args: Any, **kwargs: Any) -> 'mypy.plugin.Plugin':
25+
# ... mypy doesn't like these shenanigans so we have to type ignore it!
26+
def __new__(cls, *args: Any, **kwargs: Any) -> 'mypy.plugin.Plugin': # type: ignore
2627
from mypy.plugin import WrapperPlugin
2728
plugin = object.__new__(cls)
2829
plugin.__init__(*args, **kwargs)

mypy/message_registry.py

+2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
INVALID_SLICE_INDEX = 'Slice index must be an integer or None' # type: Final
5252
CANNOT_INFER_LAMBDA_TYPE = 'Cannot infer type of lambda' # type: Final
5353
CANNOT_ACCESS_INIT = 'Cannot access "__init__" directly' # type: Final
54+
NON_INSTANCE_NEW_TYPE = '"__new__" must return a class instance (got {})' # type: Final
55+
INVALID_NEW_TYPE = 'Incompatible return type for "__new__"' # type: Final
5456
BAD_CONSTRUCTOR_TYPE = 'Unsupported decorated constructor type' # type: Final
5557
CANNOT_ASSIGN_TO_METHOD = 'Cannot assign to a method' # type: Final
5658
CANNOT_ASSIGN_TO_TYPE = 'Cannot assign to a type' # type: Final

test-data/unit/check-class-namedtuple.test

+1-1
Original file line numberDiff line numberDiff line change
@@ -598,7 +598,7 @@ class XMethBad(NamedTuple):
598598
class MagicalFields(NamedTuple):
599599
x: int
600600
def __slots__(self) -> None: pass # E: Cannot overwrite NamedTuple attribute "__slots__"
601-
def __new__(cls) -> None: pass # E: Cannot overwrite NamedTuple attribute "__new__"
601+
def __new__(cls) -> MagicalFields: pass # E: Cannot overwrite NamedTuple attribute "__new__"
602602
def _source(self) -> int: pass # E: Cannot overwrite NamedTuple attribute "_source"
603603
__annotations__ = {'x': float} # E: NamedTuple field name cannot start with an underscore: __annotations__ \
604604
# E: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]" \

test-data/unit/check-classes.test

+108-6
Original file line numberDiff line numberDiff line change
@@ -343,12 +343,12 @@ main:6: error: Return type "A" of "f" incompatible with return type "None" in su
343343

344344
[case testOverride__new__WithDifferentSignature]
345345
class A:
346-
def __new__(cls, x: int) -> str:
347-
return ''
346+
def __new__(cls, x: int) -> A:
347+
pass
348348

349349
class B(A):
350-
def __new__(cls) -> int:
351-
return 1
350+
def __new__(cls) -> B:
351+
pass
352352

353353
[case testOverride__new__AndCallObject]
354354
from typing import TypeVar, Generic
@@ -5363,8 +5363,8 @@ class A:
53635363
pass
53645364

53655365
class B(A):
5366-
def __new__(cls) -> int:
5367-
return 10
5366+
def __new__(cls) -> B:
5367+
pass
53685368

53695369
B()
53705370

@@ -5929,3 +5929,105 @@ class B:
59295929

59305930
class C(A, B): pass
59315931
[out]
5932+
5933+
[case testNewReturnType1]
5934+
class A:
5935+
def __new__(cls) -> B:
5936+
pass
5937+
5938+
class B(A): pass
5939+
5940+
reveal_type(A()) # N: Revealed type is '__main__.B'
5941+
reveal_type(B()) # N: Revealed type is '__main__.B'
5942+
5943+
[case testNewReturnType2]
5944+
from typing import Any
5945+
5946+
# make sure that __new__ method that return Any are ignored when
5947+
# determining the return type
5948+
class A:
5949+
def __new__(cls):
5950+
pass
5951+
5952+
class B:
5953+
def __new__(cls) -> Any:
5954+
pass
5955+
5956+
reveal_type(A()) # N: Revealed type is '__main__.A'
5957+
reveal_type(B()) # N: Revealed type is '__main__.B'
5958+
5959+
[case testNewReturnType3]
5960+
5961+
# Check for invalid __new__ typing
5962+
5963+
class A:
5964+
def __new__(cls) -> int: # E: Incompatible return type for "__new__" (returns "int", but must return a subtype of "A")
5965+
pass
5966+
5967+
reveal_type(A()) # N: Revealed type is 'builtins.int'
5968+
5969+
[case testNewReturnType4]
5970+
from typing import TypeVar, Type
5971+
5972+
# Check for __new__ using type vars
5973+
5974+
TX = TypeVar('TX', bound='X')
5975+
class X:
5976+
def __new__(lol: Type[TX], x: int) -> TX:
5977+
pass
5978+
class Y(X): pass
5979+
5980+
reveal_type(X(20)) # N: Revealed type is '__main__.X*'
5981+
reveal_type(Y(20)) # N: Revealed type is '__main__.Y*'
5982+
5983+
[case testNewReturnType5]
5984+
from typing import Any, TypeVar, Generic, overload
5985+
5986+
T = TypeVar('T')
5987+
class O(Generic[T]):
5988+
@overload
5989+
def __new__(cls) -> O[int]:
5990+
pass
5991+
@overload
5992+
def __new__(cls, x: int) -> O[str]:
5993+
pass
5994+
def __new__(cls, x: int = 0) -> O[Any]:
5995+
pass
5996+
5997+
reveal_type(O()) # N: Revealed type is '__main__.O[builtins.int]'
5998+
reveal_type(O(10)) # N: Revealed type is '__main__.O[builtins.str]'
5999+
6000+
[case testNewReturnType6]
6001+
from typing import Tuple, Optional
6002+
6003+
# Check for some cases that aren't allowed
6004+
6005+
class X:
6006+
def __new__(cls) -> Optional[Y]: # E: "__new__" must return a class instance (got "Optional[Y]")
6007+
pass
6008+
class Y:
6009+
def __new__(cls) -> Optional[int]: # E: "__new__" must return a class instance (got "Optional[int]")
6010+
pass
6011+
6012+
6013+
[case testNewReturnType7]
6014+
from typing import NamedTuple
6015+
6016+
# ... test __new__ returning tuple type
6017+
class A:
6018+
def __new__(cls) -> 'B':
6019+
pass
6020+
6021+
N = NamedTuple('N', [('x', int)])
6022+
class B(A, N): pass
6023+
6024+
reveal_type(A()) # N: Revealed type is 'Tuple[builtins.int, fallback=__main__.B]'
6025+
6026+
[case testNewReturnType8]
6027+
from typing import TypeVar, Any
6028+
6029+
# test type var from a different argument
6030+
TX = TypeVar('TX', bound='X')
6031+
class X:
6032+
def __new__(cls, x: TX) -> TX: # E: "__new__" must return a class instance (got "TX")
6033+
pass

0 commit comments

Comments
 (0)