diff --git a/mypy/checker.py b/mypy/checker.py index 8e4af18c17c3..109fc24043bd 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -59,6 +59,7 @@ from mypy.meet import is_overlapping_types from mypy.options import Options from mypy.plugin import Plugin, CheckerPluginInterface +from mypy.sharedparse import BINARY_MAGIC_METHODS from mypy import experiments @@ -2179,8 +2180,11 @@ def check_return_stmt(self, s: ReturnStmt) -> None: if isinstance(typ, AnyType): # (Unless you asked to be warned in that case, and the # function is not declared to return Any) - if (self.options.warn_return_any and not self.current_node_deferred and - not is_proper_subtype(AnyType(TypeOfAny.special_form), return_type)): + if (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))): self.msg.incorrectly_returning_any(return_type, s) return @@ -3232,6 +3236,10 @@ def remove_optional(typ: Type) -> Type: return typ +def is_literal_not_implemented(n: Expression) -> bool: + return isinstance(n, NameExpr) and n.fullname == 'builtins.NotImplemented' + + def builtin_item_type(tp: Type) -> Optional[Type]: """Get the item type of a builtin container. diff --git a/mypy/sharedparse.py b/mypy/sharedparse.py index 1b3e5a3a9460..91015b6d693f 100644 --- a/mypy/sharedparse.py +++ b/mypy/sharedparse.py @@ -16,7 +16,6 @@ "__delitem__", "__divmod__", "__div__", - "__divmod__", "__enter__", "__exit__", "__eq__", @@ -92,6 +91,52 @@ MAGIC_METHODS_POS_ARGS_ONLY = MAGIC_METHODS - MAGIC_METHODS_ALLOWING_KWARGS +BINARY_MAGIC_METHODS = { + "__add__", + "__and__", + "__cmp__", + "__divmod__", + "__div__", + "__eq__", + "__floordiv__", + "__ge__", + "__gt__", + "__iadd__", + "__iand__", + "__idiv__", + "__ifloordiv__", + "__ilshift__", + "__imod__", + "__imul__", + "__ior__", + "__ipow__", + "__irshift__", + "__isub__", + "__ixor__", + "__le__", + "__lshift__", + "__lt__", + "__mod__", + "__mul__", + "__or__", + "__pow__", + "__radd__", + "__rand__", + "__rdiv__", + "__rfloordiv__", + "__rlshift__", + "__rmod__", + "__rmul__", + "__ror__", + "__rpow__", + "__rrshift__", + "__rshift__", + "__rsub__", + "__rxor__", + "__sub__", + "__xor__", +} + def special_function_elide_names(name: str) -> bool: return name in MAGIC_METHODS_POS_ARGS_ONLY diff --git a/test-data/unit/check-warnings.test b/test-data/unit/check-warnings.test index 4acbfe69df80..d812ed83efa8 100644 --- a/test-data/unit/check-warnings.test +++ b/test-data/unit/check-warnings.test @@ -143,6 +143,21 @@ def f() -> int: return g() [out] main:4: warning: 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: warning: 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 new file mode 100644 index 000000000000..e619a6c5ad85 --- /dev/null +++ b/test-data/unit/fixtures/notimplemented.pyi @@ -0,0 +1,13 @@ +# builtins stub used in NotImplemented related cases. +from typing import Any, cast + + +class object: + def __init__(self) -> None: pass + +class type: pass +class function: pass +class bool: pass +class int: pass +class str: pass +NotImplemented = cast(Any, None)