From b06d60a6428af7d5195ee1882f9d2c6f6bfd8324 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Wed, 19 Sep 2018 11:09:23 -0700 Subject: [PATCH 1/5] Fix type object signature when both __new__ and __init__ present Currently mypy will prefer __init__ to __new__ for determining the signature of a type object if there exists any __init__ other than object's. Instead, prefer the closest definition in the MRO, so that subclass __new__ can override parent __init__. Fixes #1435. --- mypy/checkmember.py | 32 ++++++++++++++++++-------- test-data/unit/check-classes.test | 38 ++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 10 deletions(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index f6a6651f66f3d..0fbe9ba742f7d 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -650,18 +650,28 @@ def type_object_type(info: TypeInfo, builtin_type: Callable[[str], Instance]) -> where ... are argument types for the __init__/__new__ method (without the self argument). Also, the fallback type will be 'type' instead of 'function'. """ + + # 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: # 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 + + init_index = info.mro.index(init_method.info) + new_index = info.mro.index(new_method.info) + + fallback = info.metaclass_type or builtin_type('builtins.type') + if init_index < new_index: + method = init_method + elif init_index > new_index: + method = new_method else: - fallback = info.metaclass_type or builtin_type('builtins.type') if init_method.info.fullname() == 'builtins.object': - # No non-default __init__ -> look at __new__ instead. - new_method = info.get_method('__new__') - if new_method and new_method.info.fullname() != 'builtins.object': - # Found one! Get signature from __new__. - return type_object_type_from_function(new_method, info, fallback) # 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: @@ -673,9 +683,13 @@ def type_object_type(info: TypeInfo, builtin_type: Callable[[str], Instance]) -> ret_type=any_type, fallback=builtin_type('builtins.function')) return class_callable(sig, info, fallback, None) - # Construct callable type based on signature of __init__. Adjust - # return type and insert type arguments. - return type_object_type_from_function(init_method, info, fallback) + + # Otherwise prefer __init__ in a tie. It isn't clear that this + # is the right thing, but __new__ caused a few problems. + method = init_method + # Construct callable type based on signature of __init__. Adjust + # return type and insert type arguments. + return type_object_type_from_function(method, info, fallback) def type_object_type_from_function(init_or_new: FuncBase, info: TypeInfo, diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 14ed21207a00d..4dc3343c9bbc5 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -1828,7 +1828,7 @@ class Num1: class Num2(Num1): @overload - def __add__(self, other: Num2) -> Num2: ... + def __add__(self, other: Num2) -> Num2: ... @overload def __add__(self, other: Num1) -> Num2: ... def __add__(self, other): pass @@ -5117,3 +5117,39 @@ class C: def x(self) -> int: pass [builtins fixtures/property.pyi] [out] + +[case testNewAndInit1] +class A: + def __init__(self, x: int) -> None: + pass + +class B(A): + def __new__(cls) -> 'B': + ... + +B() +reveal_type(B) # E: Revealed type is 'def () -> __main__.B' + +[case testNewAndInit2] +from typing import Any + +class A: + def __new__(cls, *args: Any) -> 'A': + ... + +class B(A): + def __init__(self, x: int) -> None: + pass + +reveal_type(B) # E: Revealed type is 'def (x: builtins.int) -> __main__.B' + +[case testNewAndInit3] +from typing import Any + +class A: + def __new__(cls, *args: Any) -> 'A': + ... + def __init__(self, x: int) -> None: + pass + +reveal_type(A) # E: Revealed type is 'def (x: builtins.int) -> __main__.A' From 162e7e7fdf3090898086d49b21690769c7cbc5e9 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Wed, 19 Sep 2018 15:43:33 -0700 Subject: [PATCH 2/5] tweak the test --- test-data/unit/check-classes.test | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 4dc3343c9bbc5..6551e3e29e4f0 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -5124,11 +5124,10 @@ class A: pass class B(A): - def __new__(cls) -> 'B': - ... + def __new__(cls) -> int: + return 10 B() -reveal_type(B) # E: Revealed type is 'def () -> __main__.B' [case testNewAndInit2] from typing import Any From ef0cfc364066d56cd9c81edb088971b72bab5fe2 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Wed, 19 Sep 2018 15:47:40 -0700 Subject: [PATCH 3/5] add a comment --- mypy/checkmember.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 0fbe9ba742f7d..e38981b092fdd 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -685,8 +685,9 @@ def type_object_type(info: TypeInfo, builtin_type: Callable[[str], Instance]) -> return class_callable(sig, info, fallback, None) # Otherwise prefer __init__ in a tie. It isn't clear that this - # is the right thing, but __new__ caused a few problems. - method = init_method + # is the right thing, but __new__ caused problems with + # typeshed (#5647). + method = new_method # Construct callable type based on signature of __init__. Adjust # return type and insert type arguments. return type_object_type_from_function(method, info, fallback) From 39797b874b17e243beb5b647e3a9da8ebbb3b068 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Wed, 19 Sep 2018 16:03:06 -0700 Subject: [PATCH 4/5] oops --- mypy/checkmember.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index e38981b092fdd..3045f042cff96 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -687,7 +687,7 @@ 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 = new_method + method = init_method # Construct callable type based on signature of __init__. Adjust # return type and insert type arguments. return type_object_type_from_function(method, info, fallback) From 23ce6212e26ea165324bd8997770e30eb4eedc37 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Thu, 20 Sep 2018 13:12:00 -0700 Subject: [PATCH 5/5] oops --- test-data/unit/check-classes.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 44714689ad1a1..16b80dd8433da 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -5152,7 +5152,7 @@ class A: pass reveal_type(A) # E: Revealed type is 'def (x: builtins.int) -> __main__.A' -======= + [case testCyclicDecorator] import b [file a.py]