8000 Add error code "explicit-override" for strict @override mode (PEP 698… · python/mypy@dfea43f · GitHub
[go: up one dir, main page]

Skip to content

Commit dfea43f

Browse files
authored
Add error code "explicit-override" for strict @OverRide mode (PEP 698) (#15512)
Add the strict mode for [PEP 698](https://peps.python.org/pep-0698/#strict-enforcement-per-project). Closes: #14072
1 parent 8a5d8f0 commit dfea43f

File tree

7 files changed

+291
-29
lines changed

7 files changed

+291
-29
lines changed

docs/source/class_basics.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,9 @@ override has a compatible signature:
210210

211211
In order to ensure that your code remains correct when renaming methods,
212212
it can be helpful to explicitly mark a method as overriding a base
213-
method. This can be done with the ``@override`` decorator. If the base
213+
method. This can be done with the ``@override`` decorator. ``@override``
214+
can be imported from ``typing`` starting with Python 3.12 or from
215+
``typing_extensions`` for use with older Python versions. If the base
214216
method is then renamed while the overriding method is not, mypy will
215217
show an error:
216218

@@ -233,6 +235,11 @@ show an error:
233235
def g(self, y: str) -> None: # Error: no corresponding base method found
234236
...
235237
238+
.. note::
239+
240+
Use :ref:`--enable-error-code explicit-override <code-explicit-override>` to require
241+
that method overrides use the ``@override`` decorator. Emit an error if it is missing.
242+
236243
You can also override a statically typed method with a dynamically
237244
typed one. This allows dynamically typed code to override methods
238245
defined in library classes without worrying about their type

docs/source/error_code_list2.rst

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,3 +442,42 @@ Example:
442442
# The following will not generate an error on either
443443
# Python 3.8, or Python 3.9
444444
42 + "testing..." # type: ignore
445+
446+
.. _code-explicit-override:
447+
448+
Check that ``@override`` is used when overriding a base class method [explicit-override]
449+
----------------------------------------------------------------------------------------
450+
451+
If you use :option:`--enable-error-code explicit-override <mypy --enable-error-code>`
452+
mypy generates an error if you override a base class method without using the
453+
``@override`` decorator. An error will not be emitted for overrides of ``__init__``
454+
or ``__new__``. See `PEP 698 <https://peps.python.org/pep-0698/#strict-enforcement-per-project>`_.
455+
456+
.. note::
457+
458+
Starting with Python 3.12, the ``@override`` decorator can be imported from ``typing``.
459+
To use it with older Python versions, import it from ``typing_extensions`` instead.
460+
461+
Example:
462+
463+
.. code-block:: python
464+
465+
# Use "mypy --enable-error-code explicit-override ..."
466+
467+
from typing import override
468+
469+
class Parent:
470+
def f(self, x: int) -> None:
471+
pass
472+
473+
def g(self, y: int) -> None:
474+
pass
475+
476+
477+
class Child(Parent):
478+
def f(self, x: int) -> None: # Error: Missing @override decorator
479+
pass
480+
481+
@override
482+
def g(self, y: int) -> None:
483+
pass

mypy/checker.py

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -643,9 +643,14 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
643643
if defn.impl:
644644
defn.impl.accept(self)
645645
if defn.info:
646-
found_base_method = self.check_method_override(defn)
647-
if defn.is_explicit_override and found_base_method is False:
646+
found_method_base_classes = self.check_method_override(defn)
647+
if (
648+
defn.is_explicit_override
649+
and not found_method_base_classes
650+
and found_method_base_classes is not None
651+
):
648652
self.msg.no_overridable_method(defn.name, defn)
653+
self.check_explicit_override_decorator(defn, found_method_base_classes, defn.impl)
649654
self.check_inplace_operator_method(defn)
650655
if not defn.is_property:
651656
self.check_overlapping_overloads(defn)
@@ -972,7 +977,8 @@ def _visit_func_def(self, defn: FuncDef) -> None:
972977
# overload, the legality of the override has already
973978
# been typechecked, and decorated methods will be
974979
# checked when the decorator is.
975-
self.check_method_override(defn)
980+
found_method_base_classes = self.check_method_override(defn)
981+
self.check_explicit_override_decorator(defn, found_method_base_classes)
976982
self.check_inplace_operator_method(defn)
977983
if defn.original_def:
978984
# Override previous definition.
@@ -1813,23 +1819,41 @@ def expand_typevars(
18131819
else:
18141820
return [(defn, typ)]
18151821

1816-
def check_method_override(self, defn: FuncDef | OverloadedFuncDef | Decorator) -> bool | None:
1822+
def check_explicit_override_decorator(
1823+
self,
1824+
defn: FuncDef | OverloadedFuncDef,
1825+
found_method_base_classes: list[TypeInfo] | None,
1826+
context: Context | None = None,
1827+
) -> None:
1828+
if (
1829+
found_method_base_classes
1830+
and not defn.is_explicit_override
1831+
and defn.name not in ("__init__", "__new__")
1832+
):
1833+
self.msg.explicit_override_decorator_missing(
1834+
defn.name, found_method_base_classes[0].fullname, context or defn
1835+
)
1836+
1837+
def check_method_override(
1838+
self, defn: FuncDef | OverloadedFuncDef | Decorator
1839+
) -> list[TypeInfo] | None:
18171840
"""Check if function definition is compatible with base classes.
18181841
18191842
This may defer the method if a signature is not available in at least one base class.
18201843
Return ``None`` if that happens.
18211844
1822-
Return ``True`` if an attribute with the method name was found in the base class.
1845+
Return a list of base classes which contain an attribute with the method name.
18231846
"""
18241847
# Check against definitions in base classes.
1825-
found_base_method = False
1848+
found_method_base_classes: list[TypeInfo] = []
18261849
for base in defn.info.mro[1:]:
18271850
result = self.check_method_or_accessor_override_for_base(defn, base)
18281851
if result is None:
18291852
# Node was deferred, we will have another attempt later.
18301853
return None
1831-
found_base_method |= result
1832-
return found_base_method
1854+
if result:
1855+
found_method_base_classes.append(base)
1856+
return found_method_base_classes
18331857

18341858
def check_method_or_accessor_override_for_base(
18351859
self, defn: FuncDef | OverloadedFuncDef | Decorator, base: TypeInfo
@@ -4739,9 +4763,14 @@ def visit_decorator(self, e: Decorator) -> None:
47394763
self.check_incompatible_property_override(e)
47404764
# For overloaded functions we already checked override for overload as a whole.
47414765
if e.func.info and not e.func.is_dynamic() and not e.is_overload:
4742-
found_base_method = self.check_method_override(e)
4743-
if e.func.is_explicit_override and found_base_method is False:
4766+
found_method_base_classes = self.check_method_override(e)
4767+
if (
4768+
e.func.is_explicit_override
4769+
and not found_method_base_classes
4770+
and found_method_base_classes is not None
4771+
):
47444772
self.msg.no_overridable_method(e.func.name, e.func)
4773+
self.check_explicit_override_decorator(e.func, found_method_base_classes)
47454774

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

mypy/errorcodes.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,12 @@ def __hash__(self) -> int:
235235
UNUSED_IGNORE: Final = ErrorCode(
236236
"unused-ignore", "Ensure that all type ignores are used", "General", default_enabled=False
237237
)
238+
EXPLICIT_OVERRIDE_REQUIRED: Final = ErrorCode(
239+
"explicit-override",
240+
"Require @override decorator if method is overriding a base class method",
241+
"General",
242+
default_enabled=False,
243+
)
238244

239245

240246
# Syntax errors are often blocking.

mypy/messages.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1525,6 +1525,16 @@ def no_overridable_method(self, name: str, context: Context) -> None:
15251525
context,
15261526
)
15271527

1528+
def explicit_override_decorator_missing(
1529+
self, name: str, base_name: str, context: Context
1530+
) -> None:
1531+
self.fail(
1532+
f'Method "{name}" is not using @override '
1533+
f'but is overriding a method in class "{base_name}"',
1534+
context,
1535+
code=codes.EXPLICIT_OVERRIDE_REQUIRED,
1536+
)
1537+
15281538
def final_cant_override_writable(self, name: str, ctx: Context) -> None:
15291539
self.fail(f'Cannot override writable attribute "{name}" with a final one', ctx)
15301540

0 commit comments

Comments
 (0)
0