diff --git a/misc/typeshed_patches/0001-Stop-subclassing_NotImplemented-from-Any.patch b/misc/typeshed_patches/0001-Stop-subclassing_NotImplemented-from-Any.patch new file mode 100644 index 000000000000..ee6f082502a4 --- /dev/null +++ b/misc/typeshed_patches/0001-Stop-subclassing_NotImplemented-from-Any.patch @@ -0,0 +1,25 @@ +From 9b584b626fc18881055a3f48080fe3afcf9bb583 Mon Sep 17 00:00:00 2001 +From: Christoph Tyralla +Date: Sat, 25 Oct 2025 09:39:03 +0200 +Subject: [PATCH] modify typeshed instead of hacking `calculate_mro` + +--- + mypy/typeshed/stdlib/builtins.pyi | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/mypy/typeshed/stdlib/builtins.pyi b/mypy/typeshed/stdlib/builtins.pyi +index ddf81db18..933a06640 100644 +--- a/mypy/typeshed/stdlib/builtins.pyi ++++ b/mypy/typeshed/stdlib/builtins.pyi +@@ -1269,7 +1269,7 @@ class property: + + @final + @type_check_only +-class _NotImplementedType(Any): ++class _NotImplementedType: + __call__: None + + NotImplemented: _NotImplementedType +-- +2.45.1.windows.1 + diff --git a/mypy/checker.py b/mypy/checker.py index b6a9bb3b22cd..1a324e02ec73 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -178,6 +178,7 @@ coerce_to_literal, custom_special_method, erase_def_to_union_or_bound, + erase_notimplemented, erase_to_bound, erase_to_union_or_bound, false_only, @@ -4895,6 +4896,7 @@ def infer_context_dependent( return typ def check_return_stmt(self, s: ReturnStmt) -> None: + defn = self.scope.current_function() if defn is not None: if defn.is_generator: @@ -4942,17 +4944,11 @@ def check_return_stmt(self, s: ReturnStmt) -> None: s.expr, return_type, allow_none_return=allow_none_func_call ) ) - # Treat NotImplemented as having type Any, consistent with its - # definition in typeshed prior to python/typeshed#4222. - if ( - isinstance(typ, Instance) - and typ.type.fullname == "builtins._NotImplementedType" - ): - typ = AnyType(TypeOfAny.special_form) if defn.is_async_generator: self.fail(message_registry.RETURN_IN_ASYNC_GENERATOR, s) return + # Returning a value of type Any is always fine. if isinstance(typ, AnyType): # (Unless you asked to be warned in that case, and the @@ -4961,10 +4957,6 @@ def check_return_stmt(self, s: ReturnStmt) -> None: self.options.warn_return_any and not self.current_node_deferred and not is_proper_subtype(AnyType(TypeOfAny.special_form), return_type) - and not ( - defn.name in BINARY_MAGIC_METHODS - and is_literal_not_implemented(s.expr) - ) and not ( isinstance(return_type, Instance) and return_type.type.fullname == "builtins.object" @@ -4983,9 +4975,12 @@ def check_return_stmt(self, s: ReturnStmt) -> None: return self.fail(message_registry.NO_RETURN_VALUE_EXPECTED, s) else: + typ_: Type = typ + if defn.name in BINARY_MAGIC_METHODS or defn.name == "__subclasshook__": + typ_ = erase_notimplemented(typ) self.check_subtype( subtype_label="got", - subtype=typ, + subtype=typ_, supertype_label="expected", supertype=return_type, context=s.expr, @@ -5098,22 +5093,15 @@ def type_check_raise(self, e: Expression, s: RaiseStmt, optional: bool = False) # where we allow `raise e from None`. expected_type_items.append(NoneType()) - self.check_subtype( - typ, UnionType.make_union(expected_type_items), s, message_registry.INVALID_EXCEPTION - ) + message = message_registry.INVALID_EXCEPTION + if isinstance(typ, Instance) and typ.type.fullname == "builtins._NotImplementedType": + message = message.with_additional_msg('; did you mean "NotImplementedError"?') + self.check_subtype(typ, UnionType.make_union(expected_type_items), s, message) if isinstance(typ, FunctionLike): # https://github.com/python/mypy/issues/11089 self.expr_checker.check_call(typ, [], [], e) - if isinstance(typ, Instance) and typ.type.fullname == "builtins._NotImplementedType": - self.fail( - message_registry.INVALID_EXCEPTION.with_additional_msg( - '; did you mean "NotImplementedError"?' - ), - s, - ) - def visit_try_stmt(self, s: TryStmt) -> None: """Type check a try statement.""" diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 3eb54579a050..242c1ed8cb46 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -132,6 +132,7 @@ from mypy.typeops import ( callable_type, custom_special_method, + erase_notimplemented, erase_to_union_or_bound, false_only, fixup_partial_type, @@ -3554,7 +3555,7 @@ def visit_op_expr(self, e: OpExpr) -> Type: else: assert_never(use_reverse) e.method_type = method_type - return result + return erase_notimplemented(result) else: raise RuntimeError(f"Unknown operator {e.op}") @@ -3705,7 +3706,7 @@ def visit_comparison_expr(self, e: ComparisonExpr) -> Type: result = join.join_types(result, sub_result) assert result is not None - return result + return erase_notimplemented(result) def find_partial_type_ref_fast_path(self, expr: Expression) -> Type | None: """If expression has a partial generic type, return it without additional checks. @@ -4228,15 +4229,16 @@ def check_op( # callable types. results_final = make_simplified_union(all_results) inferred_final = self.combine_function_signatures(get_proper_types(all_inferred)) - return results_final, inferred_final + return erase_notimplemented(results_final), inferred_final else: - return self.check_method_call_by_name( + result, inferred = self.check_method_call_by_name( method=method, base_type=base_type, args=[arg], arg_kinds=[ARG_POS], context=context, ) + return erase_notimplemented(result), inferred def check_boolean_op(self, e: OpExpr) -> Type: """Type check a boolean operation ('and' or 'or').""" diff --git a/mypy/mro.py b/mypy/mro.py index f34f3fa0c46d..d5d448a73b31 100644 --- a/mypy/mro.py +++ b/mypy/mro.py @@ -17,6 +17,7 @@ def calculate_mro(info: TypeInfo, obj_type: Callable[[], Instance] | None = None info.mro = mro # The property of falling back to Any is inherited. info.fallback_to_any = any(baseinfo.fallback_to_any for baseinfo in info.mro) + type_state.reset_all_subtype_caches_for(info) diff --git a/mypy/typeops.py b/mypy/typeops.py index 341c96c08931..c25f6e5b1d75 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -999,6 +999,21 @@ def is_singleton_type(typ: Type) -> bool: return typ.is_singleton_type() +def is_notimplemented(t: ProperType) -> bool: + return isinstance(t, Instance) and t.type.fullname == "builtins._NotImplementedType" + + +def erase_notimplemented(t: Type) -> Type: + t = get_proper_type(t) + if is_notimplemented(t): + return AnyType(TypeOfAny.special_form) + if isinstance(t, UnionType): + return UnionType.make_union( + [i for i in t.items if not is_notimplemented(get_proper_type(i))] + ) + return t + + def try_expanding_sum_type_to_union(typ: Type, target_fullname: str) -> Type: """Attempts to recursively expand any enum Instances with the given target_fullname into a Union of all of its component LiteralTypes. diff --git a/mypy/typeshed/stdlib/builtins.pyi b/mypy/typeshed/stdlib/builtins.pyi index ddf81db181bf..933a066404cc 100644 --- a/mypy/typeshed/stdlib/builtins.pyi +++ b/mypy/typeshed/stdlib/builtins.pyi @@ -1269,7 +1269,7 @@ class property: @final @type_check_only -class _NotImplementedType(Any): +class _NotImplementedType: __call__: None NotImplemented: _NotImplementedType diff --git a/test-data/unit/check-overloading.test b/test-data/unit/check-overloading.test index be55a182b87b..1fe9c689f6e0 100644 --- a/test-data/unit/check-overloading.test +++ b/test-data/unit/check-overloading.test @@ -6852,3 +6852,96 @@ if isinstance(headers, dict): reveal_type(headers) # N: Revealed type is "Union[__main__.Headers, typing.Iterable[tuple[builtins.bytes, builtins.bytes]]]" [builtins fixtures/isinstancelist.pyi] + +[case testReturnNotImplementedInBinaryMagicMethods] +# flags: --warn-return-any +from typing import Union + +class A: + def __add__(self, other: object) -> int: + return NotImplemented + def __radd__(self, other: object) -> Union[int, NotImplementedType]: + return NotImplemented + def __sub__(self, other: object) -> Union[int, NotImplementedType]: + return 1 + def __isub__(self, other: object) -> int: + x: Union[int, NotImplementedType] + return x + def __mul__(self, other: object) -> Union[int, NotImplementedType]: + x: Union[int, NotImplementedType] + return x +[builtins fixtures/notimplemented.pyi] + +[case testReturnNotImplementedABCSubclassHookMethod] +# flags: --warn-return-any +class A: + @classmethod + def __subclasshook__(cls, t: type[object], /) -> bool: + return NotImplemented +[builtins fixtures/notimplemented.pyi] + +[case testReturnNotImplementedInNormalMethods] +# flags: --warn-return-any +from typing import Union + +class A: + def f(self) -> bool: return NotImplemented # E: Incompatible return value type (got "_NotImplementedType", expected "bool") + def g(self) -> NotImplementedType: return True # E: Incompatible return value type (got "bool", expected "_NotImplementedType") + def h(self) -> NotImplementedType: return NotImplemented + def i(self) -> Union[bool, NotImplementedType]: return NotImplemented + def j(self) -> Union[bool, NotImplementedType]: return True +[builtins fixtures/notimplemented.pyi] + +[case testNotImplementedReturnedFromBinaryMagicMethod] +# flags: --warn-return-any +from typing import Union + +class A: + def __add__(self, x: A) -> Union[int, NotImplementedType]: ... + def __sub__(self, x: A) -> NotImplementedType: ... + def __imul__(self, x: A) -> Union[A, NotImplementedType]: ... + def __itruediv__(self, x: A) -> Union[A, NotImplementedType]: ... + def __ifloordiv__(self, x: A) -> Union[int, NotImplementedType]: ... + def __eq__(self, x: object) -> Union[bool, NotImplementedType]: ... + def __le__(self, x: int) -> Union[bool, NotImplementedType]: ... + def __lt__(self, x: int) -> NotImplementedType: ... + def __and__(self, x: object) -> NotImplementedType: ... +class B(A): + def __radd__(self, x: A) -> Union[int, NotImplementedType]: ... + def __rsub__(self, x: A) -> NotImplementedType: ... + def __itruediv__(self, x: A) -> Union[A, NotImplementedType]: ... + def __ror__(self, x: object) -> NotImplementedType: ... + +a: A +b: B + +reveal_type(a.__add__(a)) # N: Revealed type is "Union[builtins.int, builtins._NotImplementedType]" +reveal_type(a.__sub__(a)) # N: Revealed type is "builtins._NotImplementedType" +reveal_type(a.__imul__(a)) # N: Revealed type is "Union[__main__.A, builtins._NotImplementedType]" +reveal_type(a.__eq__(a)) # N: Revealed type is "Union[builtins.bool, builtins._NotImplementedType]" +reveal_type(a.__le__(1)) # N: Revealed type is "Union[builtins.bool, builtins._NotImplementedType]" + +reveal_type(a + a) # N: Revealed type is "builtins.int" +reveal_type(a - a) # N: Revealed type is "Any" +reveal_type(a + b) # N: Revealed type is "builtins.int" +reveal_type(a - b) # N: Revealed type is "Any" +def f1(a: A) -> None: + a += a # E: Incompatible types in assignment (expression has type "int", variable has type "A") +def f2(a: A) -> None: + a -= a + reveal_type(a) # N: Revealed type is "__main__.A" +def f3(a: A) -> None: + a *= a + reveal_type(a) # N: Revealed type is "__main__.A" +def f4(a: A) -> None: + a /= a + reveal_type(a) # N: Revealed type is "__main__.A" +def f5(a: A) -> None: + a //= a # E: Result type of // incompatible in assignment +reveal_type(a == a) # N: Revealed type is "builtins.bool" +reveal_type(a == 1) # N: Revealed type is "builtins.bool" +reveal_type(a <= 1) # N: Revealed type is "builtins.bool" +reveal_type(a < 1) # N: Revealed type is "Any" +reveal_type(a and int()) # N: Revealed type is "Union[__main__.A, builtins.int]" +reveal_type(int() or a) # N: Revealed type is "Union[builtins.int, __main__.A]" +[builtins fixtures/notimplemented.pyi] diff --git a/test-data/unit/check-warnings.test b/test-data/unit/check-warnings.test index a2d201fa301d..69b485d62344 100644 --- a/test-data/unit/check-warnings.test +++ b/test-data/unit/check-warnings.test @@ -178,21 +178,6 @@ def f() -> int: return g() [out] main:4: error: Returning Any from function declared to return "int" -[case testReturnAnyForNotImplementedInBinaryMagicMethods] -# flags: --warn-return-any -class A: - def __eq__(self, other: object) -> bool: return NotImplemented -[builtins fixtures/notimplemented.pyi] -[out] - -[case testReturnAnyForNotImplementedInNormalMethods] -# flags: --warn-return-any -class A: - def some(self) -> bool: return NotImplemented -[builtins fixtures/notimplemented.pyi] -[out] -main:3: error: Returning Any from function declared to return "bool" - [case testReturnAnyFromTypedFunctionWithSpecificFormatting] # flags: --warn-return-any from typing import Any, Tuple diff --git a/test-data/unit/fixtures/notimplemented.pyi b/test-data/unit/fixtures/notimplemented.pyi index 92edf84a7fd1..6d509abbb2c4 100644 --- a/test-data/unit/fixtures/notimplemented.pyi +++ b/test-data/unit/fixtures/notimplemented.pyi @@ -9,10 +9,15 @@ class function: pass class bool: pass class int: pass class str: pass +class tuple: pass class dict: pass +class classmethod: pass +class ellipsis: pass -class _NotImplementedType(Any): +class _NotImplementedType: __call__: NotImplemented # type: ignore NotImplemented: _NotImplementedType +NotImplementedType = _NotImplementedType + class BaseException: pass