8000 Add strict_override_decorator option (PEP 698) · python/mypy@0827dcd · GitHub
[go: up one dir, main page]

Skip to content

Commit 0827dcd

Browse files
committed
Add strict_override_decorator option (PEP 698)
1 parent 2d8ad8e commit 0827dcd

File tree

8 files changed

+147
-1
lines changed

8 files changed

+147
-1
lines changed

docs/source/class_basics.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,12 @@ show an error:
233233
def g(self, y: str) -> None: # Error: no corresponding base method found
234234
...
235235
236+
.. note::
237+
238+
Use ``--strict-override-decorator`` or
239+
:confval:`strict_override_decorator = True <strict_override_decorator>` to require
240+
methods overrides use the ``@override`` decorator. Emit an error if it is missing.
241+
236242
You can also override a statically typed method with a dynamically
237243
typed one. This allows dynamically typed code to override methods
238244
defined in library classes without worrying about their type

docs/source/config_file.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,13 @@ section of the command line docs.
706706

707707
Make arguments prepended via ``Concatenate`` be truly positional-only.
708708

709+
.. confval:: strict_override_decorator
710+
711+
:type: boolean
712+
:default: False
713+
714+
Require ``override`` decorator if method is overriding a base class method.
715+
709716
.. confval:: strict_equality
710717

711718
:type: boolean

mypy/checker.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,12 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
644644
found_base_method = self.check_method_override(defn)
645645
if defn.is_explicit_override and found_base_method is False:
646646
self.msg.no_overridable_method(defn.name, defn)
647+
elif (
648+
found_base_method
649+
and self.options.strict_override_decorator
650+
and not defn.is_explicit_override
651+
):
652+
self.msg.override_decorator_missing(defn.name, defn.impl or defn)
647653
self.check_inplace_operator_method(defn)
648654
if not defn.is_property:
649655
self.check_overlapping_overloads(defn)
@@ -970,7 +976,13 @@ def _visit_func_def(self, defn: FuncDef) -> None:
970976
# overload, the legality of the override has already
971977
# been typechecked, and decorated methods will be
972978
# checked when the decorator is.
973-
self.check_method_override(defn)
979+
found_base_method = self.check_method_override(defn)
980+
if (
981+
found_base_method
982+
and self.options.strict_override_decorator
983+
and defn.name not in ("__init__", "__new__")
984+
):
985+
self.msg.override_decorator_missing(defn.name, defn)
974986
self.check_inplace_operator_method(defn)
975987
if defn.original_def:
976988
# Override previous definition.
@@ -4735,6 +4747,12 @@ def visit_decorator(self, e: Decorator) -> None:
47354747
found_base_method = self.check_method_override(e)
47364748
if e.func.is_explicit_override and found_base_method is False:
47374749
self.msg.no_overridable_method(e.func.name, e.func)
4750+
elif (
4751+
found_base_method
4752+
and self.options.strict_override_decorator
4753+
and not e.func.is_explicit_override
4754+
):
4755+
self.msg.override_decorator_missing(e.func.name, e.func)
47384756

47394757
if e.func.info and e.func.name in ("__init__", "__new__"):
47404758
if e.type and not isinstance(get_proper_type(e.type), (FunctionLike, AnyType)):

mypy/main.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,14 @@ def add_invertible_flag(
833833
group=strictness_group,
F438 834834
)
835835

836+
add_invertible_flag(
837+
"--strict-override-decorator",
838+
default=False,
839+
strict_flag=False,
840+
help="Require override decorator if method is overriding a base class method.",
841+
group=strictness_group,
842+
)
843+
836844
strict_help = "Strict mode; enables the following flags: {}".format(
837845
", ".join(strict_flag_names)
838846
)

mypy/messages.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1521,6 +1521,13 @@ def no_overridable_method(self, name: str, context: Context) -> None:
15211521
context,
15221522
)
15231523

1524+
def override_decorator_missing(self, name: str, context: Context) -> None:
1525+
self.fail(
1526+
f'Method "{name}" is not marked as override '
1527+
"but is overriding a method in a base class",
1528+
context,
1529+
)
1530+
15241531
def final_cant_override_writable(self, name: str, ctx: Context) -> None:
15251532
self.fail(f'Cannot override writable attribute "{name}" with a final one', ctx)
15261533

mypy/options.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ class BuildType:
5151
"strict_concatenate",
5252
"strict_equality",
5353
"strict_optional",
54+
"strict_override_decorator",
5455
"warn_no_return",
5556
"warn_return_any",
5657
"warn_unreachable",
@@ -203,6 +204,9 @@ def __init__(self) -> None:
203204
# Make arguments prepended via Concatenate be truly positional-only.
204205
self.strict_concatenate = False
205206

207+
# Require override decorator. Strict mode for PEP 698.
208+
self.strict_override_decorator = False
209+
206210
# Report an error for any branches inferred to be unreachable as a result of
207211
# type analysis.
208212
self.warn_unreachable = False

test-data/unit/check-functions.test

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3007,3 +3007,75 @@ class C(A):
30073007
def f(self, y: int | str) -> str: pass
30083008
[typing fixtures/typing-full.pyi]
30093009
[builtins fixtures/tuple.pyi]
3010+
3011+
[case requireExplicitOverrideMethod]
3012+
# flags: --strict-override-decorator --python-version 3.12
3013+
from typing import override
3014+
3015+
class A:
3016+
def f(self, x: int) -> str: pass
3017+
3018+
class B(A):
3019+
@override
3020+
def f(self, y: int) -> str: pass
3021+
3022+
class C(A):
3023+
def f(self, y: int) -> str: pass # E: Method "f" is not marked as override but is overriding a method in a base class
3024+
[typing fixtures/typing-override.pyi]
3025+
3026+
[case requireExplicitOverrideSpecialMethod]
3027+
# flags: --strict-override-decorator --python-version 3.12
3028+
from typing import Self, override
3029+
3030+
# Don't require override decorator for __init__ and __new__
3031+
# See: https://github.com/python/typing/issues/1376
3032+
class A:
3033+
def __init__(self) -> None: pass
3034+
def __new__(cls) -> Self: pass
3035+
[typing fixtures/typing-override.pyi]
3036+
3037+
[case requireExplicitOverrideProperty]
3038+
# flags: --strict-override-decorator --python-version 3.12
3039+
from typing import override
3040+
3041+
class A:
3042+
@property
3043+
def prop(self) -> int: pass
3044+
3045+
class B(A):
3046+
@override
3047+
@property
3048+
def prop(self) -> int: pass
3049+
3050+
class C(A):
3051+
@property
3052+
def prop(self) -> int: pass # E: Method "prop" is not marked as override but is overriding a method in a base class
3053+
[typing fixtures/typing-override.pyi]
3054+
[builtins fixtures/property.pyi]
3055+
3056+
[case requireExplicitOverrideOverload]
3057+
# flags: --strict-override-decorator --python-version 3.12
3058+
from typing import overload, override
3059+
3060+
class A:
3061+
@overload
3062+
def f(self, x: int) -> str: ...
3063+
@overload
3064+
def f(self, x: str) -> str: ...
3065+
def f(self, x): pass
3066+
3067+
class B(A):
3068+
@overload
3069+
def f(self, y: int) -> str: ...
3070+
@overload
3071+
def f(self, y: str) -> str: ...
3072+
@override
3073+
def f(self, y): pass
3074+
3075+
class C(A):
3076+
@overload
3077+
def f(self, y: int) -> str: ...
3078+
@overload
3079+
def f(self, y: str) -> str: ...
3080+
def f(self, y): pass # E: Method "f" is not marked as override but is overriding a method in a base class
3081+
[typing fixtures/typing-override.pyi]
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
TypeVar = 0
2+
Generic = 0
3+
Any = 0
4+
overload = 0
5+
Type = 0
6+
Literal = 0
7+
Optional = 0
8+
Self = 0
9+
Tuple = 0
10+
ClassVar = 0
11+
12+
T = TypeVar('T')
13+
T_co = TypeVar('T_co', covariant=True)
14+
KT = TypeVar('KT')
15+
16+
class Iterable(Generic[T_co]): pass
17+
class Iterator(Iterable[T_co]): pass
18+
class Sequence(Iterable[T_co]): pass
19+
class Mapping(Iterable[KT], Generic[KT, T_co]):
20+
def keys(self) -> Iterable[T]: pass # Approximate return type
21+
def __getitem__(self, key: T) -> T_co: pass
22+
23+
24+
def override(__arg: T) -> T: ...

0 commit comments

Comments
 (0)
0