Skip to content

Commit

Permalink
Add strict_override_decorator option (PEP 698)
Browse files Browse the repository at this point in the history
  • Loading branch information
cdce8p committed Jul 5, 2023
1 parent 7d1a899 commit 7fef24f
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 1 deletion.
6 changes: 6 additions & 0 deletions docs/source/class_basics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <strict_override_decorator>` 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
Expand Down
7 changes: 7 additions & 0 deletions docs/source/config_file.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 19 additions & 1 deletion mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,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)
Expand Down Expand Up @@ -972,7 +978,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.
Expand Down Expand Up @@ -4743,6 +4755,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)):
Expand Down
8 changes: 8 additions & 0 deletions mypy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,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,
Expand Down
7 changes: 7 additions & 0 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -1525,6 +1525,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)

Expand Down
4 changes: 4 additions & 0 deletions mypy/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class BuildType:
"strict_concatenate",
"strict_equality",
"strict_optional",
"strict_override_decorator",
"warn_no_return",
"warn_return_any",
"warn_unreachable",
Expand Down Expand Up @@ -200,6 +201,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

Expand Down
72 changes: 72 additions & 0 deletions test-data/unit/check-functions.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
24 changes: 24 additions & 0 deletions test-data/unit/fixtures/typing-override.pyi
Original file line number Diff line number Diff line change
@@ -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: ...

0 comments on commit 7fef24f

Please sign in to comment.