diff --git a/docs/source/class_basics.rst b/docs/source/class_basics.rst index 82bbf00b830dd..aec9e386d55e8 100644 --- a/docs/source/class_basics.rst +++ b/docs/source/class_basics.rst @@ -233,6 +233,12 @@ show an error: def g(self, y: str) -> None: # Error: no corresponding base method found ... +.. note:: + + Use ``--strict-override-decorator`` or + :confval:`strict_override_decorator = True ` to require + methods overrides use the ``@override`` decorator. Emit an error if it is missing. + You can also override a statically typed method with a dynamically typed one. This allows dynamically typed code to override methods defined in library classes without worrying about their type diff --git a/docs/source/config_file.rst b/docs/source/config_file.rst index 9e79ff99937b6..37861ec4d750d 100644 --- a/docs/source/config_file.rst +++ b/docs/source/config_file.rst @@ -714,6 +714,13 @@ section of the command line docs. Prohibit equality checks, identity checks, and container checks between non-overlapping types. +.. confval:: strict_override_decorator + + :type: boolean + :default: False + + Require ``override`` decorator if method is overriding a base class method. + .. confval:: strict :type: boolean diff --git a/mypy/checker.py b/mypy/checker.py index cdce42ddaaa1f..ae69c2f311ce6 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -645,6 +645,12 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: found_base_method = self.check_method_override(defn) if defn.is_explicit_override and found_base_method is False: self.msg.no_overridable_method(defn.name, defn) + elif ( + found_base_method + and self.options.strict_override_decorator + and not defn.is_explicit_override + ): + self.msg.override_decorator_missing(defn.name, defn.impl or defn) self.check_inplace_operator_method(defn) if not defn.is_property: self.check_overlapping_overloads(defn) @@ -971,7 +977,13 @@ def _visit_func_def(self, defn: FuncDef) -> None: # overload, the legality of the override has already # been typechecked, and decorated methods will be # checked when the decorator is. - self.check_method_override(defn) + found_base_method = self.check_method_override(defn) + if ( + found_base_method + and self.options.strict_override_decorator + and defn.name not in ("__init__", "__new__") + ): + self.msg.override_decorator_missing(defn.name, defn) self.check_inplace_operator_method(defn) if defn.original_def: # Override previous definition. @@ -4742,6 +4754,12 @@ def visit_decorator(self, e: Decorator) -> None: found_base_method = self.check_method_override(e) if e.func.is_explicit_override and found_base_method is False: self.msg.no_overridable_method(e.func.name, e.func) + elif ( + found_base_method + and self.options.strict_override_decorator + and not e.func.is_explicit_override + ): + self.msg.override_decorator_missing(e.func.name, e.func) if e.func.info and e.func.name in ("__init__", "__new__"): if e.type and not isinstance(get_proper_type(e.type), (FunctionLike, AnyType)): diff --git a/mypy/main.py b/mypy/main.py index 22ff3e32a718a..c2c768f54dbb9 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -825,6 +825,14 @@ def add_invertible_flag( group=strictness_group, ) + add_invertible_flag( + "--strict-override-decorator", + default=False, + strict_flag=False, + help="Require override decorator if method is overriding a base class method.", + group=strictness_group, + ) + add_invertible_flag( "--extra-checks", default=False, diff --git a/mypy/messages.py b/mypy/messages.py index ea7923c597782..e253d32f5af2c 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -1524,6 +1524,13 @@ def no_overridable_method(self, name: str, context: Context) -> None: context, ) + def override_decorator_missing(self, name: str, context: Context) -> None: + self.fail( + f'Method "{name}" is not marked as override ' + "but is overriding a method in a base class", + context, + ) + def final_cant_override_writable(self, name: str, ctx: Context) -> None: self.fail(f'Cannot override writable attribute "{name}" with a final one', ctx) diff --git a/mypy/options.py b/mypy/options.py index e1d731c1124c5..61f48ddc8d22b 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -52,6 +52,7 @@ class BuildType: "strict_concatenate", "strict_equality", "strict_optional", + "strict_override_decorator", "warn_no_return", "warn_return_any", "warn_unreachable", @@ -201,6 +202,9 @@ def __init__(self) -> None: # This makes 1 == '1', 1 in ['1'], and 1 is '1' errors. self.strict_equality = False + # Require override decorator. Strict mode for PEP 698. + self.strict_override_decorator = False + # Deprecated, use extra_checks instead. self.strict_concatenate = False diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index b5d540b105e33..fafabb04e24ad 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -3007,3 +3007,75 @@ class C(A): def f(self, y: int | str) -> str: pass [typing fixtures/typing-full.pyi] [builtins fixtures/tuple.pyi] + +[case requireExplicitOverrideMethod] +# flags: --strict-override-decorator --python-version 3.12 +from typing import override + +class A: + def f(self, x: int) -> str: pass + +class B(A): + @override + def f(self, y: int) -> str: pass + +class C(A): + def f(self, y: int) -> str: pass # E: Method "f" is not marked as override but is overriding a method in a base class +[typing fixtures/typing-override.pyi] + +[case requireExplicitOverrideSpecialMethod] +# flags: --strict-override-decorator --python-version 3.12 +from typing import Self, override + +# Don't require override decorator for __init__ and __new__ +# See: https://github.com/python/typing/issues/1376 +class A: + def __init__(self) -> None: pass + def __new__(cls) -> Self: pass +[typing fixtures/typing-override.pyi] + +[case requireExplicitOverrideProperty] +# flags: --strict-override-decorator --python-version 3.12 +from typing import override + +class A: + @property + def prop(self) -> int: pass + +class B(A): + @override + @property + def prop(self) -> int: pass + +class C(A): + @property + def prop(self) -> int: pass # E: Method "prop" is not marked as override but is overriding a method in a base class +[typing fixtures/typing-override.pyi] +[builtins fixtures/property.pyi] + +[case requireExplicitOverrideOverload] +# flags: --strict-override-decorator --python-version 3.12 +from typing import overload, override + +class A: + @overload + def f(self, x: int) -> str: ... + @overload + def f(self, x: str) -> str: ... + def f(self, x): pass + +class B(A): + @overload + def f(self, y: int) -> str: ... + @overload + def f(self, y: str) -> str: ... + @override + def f(self, y): pass + +class C(A): + @overload + def f(self, y: int) -> str: ... + @overload + def f(self, y: str) -> str: ... + def f(self, y): pass # E: Method "f" is not marked as override but is overriding a method in a base class +[typing fixtures/typing-override.pyi] diff --git a/test-data/unit/fixtures/typing-override.pyi b/test-data/unit/fixtures/typing-override.pyi new file mode 100644 index 0000000000000..b56be5cd819cc --- /dev/null +++ b/test-data/unit/fixtures/typing-override.pyi @@ -0,0 +1,24 @@ +TypeVar = 0 +Generic = 0 +Any = 0 +overload = 0 +Type = 0 +Literal = 0 +Optional = 0 +Self = 0 +Tuple = 0 +ClassVar = 0 + +T = TypeVar('T') +T_co = TypeVar('T_co', covariant=True) +KT = TypeVar('KT') + +class Iterable(Generic[T_co]): pass +class Iterator(Iterable[T_co]): pass +class Sequence(Iterable[T_co]): pass +class Mapping(Iterable[KT], Generic[KT, T_co]): + def keys(self) -> Iterable[T]: pass # Approximate return type + def __getitem__(self, key: T) -> T_co: pass + + +def override(__arg: T) -> T: ...