Skip to content

Commit

Permalink
Add error code "explicit-override" for strict @OverRide mode (PEP 698) (
Browse files Browse the repository at this point in the history
  • Loading branch information
cdce8p committed Jul 13, 2023
1 parent 8a5d8f0 commit dfea43f
Show file tree
Hide file tree
Showing 7 changed files with 291 additions and 29 deletions.
9 changes: 8 additions & 1 deletion docs/source/class_basics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,9 @@ override has a compatible signature:

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

Expand All @@ -233,6 +235,11 @@ show an error:
def g(self, y: str) -> None: # Error: no corresponding base method found
...
.. note::

Use :ref:`--enable-error-code explicit-override <code-explicit-override>` to require
that method 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
39 changes: 39 additions & 0 deletions docs/source/error_code_list2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -442,3 +442,42 @@ Example:
# The following will not generate an error on either
# Python 3.8, or Python 3.9
42 + "testing..." # type: ignore
.. _code-explicit-override:

Check that ``@override`` is used when overriding a base class method [explicit-override]
----------------------------------------------------------------------------------------

If you use :option:`--enable-error-code explicit-override <mypy --enable-error-code>`
mypy generates an error if you override a base class method without using the
``@override`` decorator. An error will not be emitted for overrides of ``__init__``
or ``__new__``. See `PEP 698 <https://peps.python.org/pep-0698/#strict-enforcement-per-project>`_.

.. note::

Starting with Python 3.12, the ``@override`` decorator can be imported from ``typing``.
To use it with older Python versions, import it from ``typing_extensions`` instead.

Example:

.. code-block:: python
# Use "mypy --enable-error-code explicit-override ..."
from typing import override
class Parent:
def f(self, x: int) -> None:
pass
def g(self, y: int) -> None:
pass
class Child(Parent):
def f(self, x: int) -> None: # Error: Missing @override decorator
pass
@override
def g(self, y: int) -> None:
pass
49 changes: 39 additions & 10 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -643,9 +643,14 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
if defn.impl:
defn.impl.accept(self)
if defn.info:
found_base_method = self.check_method_override(defn)
if defn.is_explicit_override and found_base_method is False:
found_method_base_classes = self.check_method_override(defn)
if (
defn.is_explicit_override
and not found_method_base_classes
and found_method_base_classes is not None
):
self.msg.no_overridable_method(defn.name, defn)
self.check_explicit_override_decorator(defn, found_method_base_classes, defn.impl)
self.check_inplace_operator_method(defn)
if not defn.is_property:
self.check_overlapping_overloads(defn)
Expand Down Expand Up @@ -972,7 +977,8 @@ 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_method_base_classes = self.check_method_override(defn)
self.check_explicit_override_decorator(defn, found_method_base_classes)
self.check_inplace_operator_method(defn)
if defn.original_def:
# Override previous definition.
Expand Down Expand Up @@ -1813,23 +1819,41 @@ def expand_typevars(
else:
return [(defn, typ)]

def check_method_override(self, defn: FuncDef | OverloadedFuncDef | Decorator) -> bool | None:
def check_explicit_override_decorator(
self,
defn: FuncDef | OverloadedFuncDef,
found_method_base_classes: list[TypeInfo] | None,
context: Context | None = None,
) -> None:
if (
found_method_base_classes
and not defn.is_explicit_override
and defn.name not in ("__init__", "__new__")
):
self.msg.explicit_override_decorator_missing(
defn.name, found_method_base_classes[0].fullname, context or defn
)

def check_method_override(
self, defn: FuncDef | OverloadedFuncDef | Decorator
) -> list[TypeInfo] | None:
"""Check if function definition is compatible with base classes.
This may defer the method if a signature is not available in at least one base class.
Return ``None`` if that happens.
Return ``True`` if an attribute with the method name was found in the base class.
Return a list of base classes which contain an attribute with the method name.
"""
# Check against definitions in base classes.
found_base_method = False
found_method_base_classes: list[TypeInfo] = []
for base in defn.info.mro[1:]:
result = self.check_method_or_accessor_override_for_base(defn, base)
if result is None:
# Node was deferred, we will have another attempt later.
return None
found_base_method |= result
return found_base_method
if result:
found_method_base_classes.append(base)
return found_method_base_classes

def check_method_or_accessor_override_for_base(
self, defn: FuncDef | OverloadedFuncDef | Decorator, base: TypeInfo
Expand Down Expand Up @@ -4739,9 +4763,14 @@ def visit_decorator(self, e: Decorator) -> None:
self.check_incompatible_property_override(e)
# For overloaded functions we already checked override for overload as a whole.
if e.func.info and not e.func.is_dynamic() and not e.is_overload:
found_base_method = self.check_method_override(e)
if e.func.is_explicit_override and found_base_method is False:
found_method_base_classes = self.check_method_override(e)
if (
e.func.is_explicit_override
and not found_method_base_classes
and found_method_base_classes is not None
):
self.msg.no_overridable_method(e.func.name, e.func)
self.check_explicit_override_decorator(e.func, found_method_base_classes)

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
6 changes: 6 additions & 0 deletions mypy/errorcodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,12 @@ def __hash__(self) -> int:
UNUSED_IGNORE: Final = ErrorCode(
"unused-ignore", "Ensure that all type ignores are used", "General", default_enabled=False
)
EXPLICIT_OVERRIDE_REQUIRED: Final = ErrorCode(
"explicit-override",
"Require @override decorator if method is overriding a base class method",
"General",
default_enabled=False,
)


# Syntax errors are often blocking.
Expand Down
10 changes: 10 additions & 0 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -1525,6 +1525,16 @@ def no_overridable_method(self, name: str, context: Context) -> None:
context,
)

def explicit_override_decorator_missing(
self, name: str, base_name: str, context: Context
) -> None:
self.fail(
f'Method "{name}" is not using @override '
f'but is overriding a method in class "{base_name}"',
context,
code=codes.EXPLICIT_OVERRIDE_REQUIRED,
)

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
Loading

0 comments on commit dfea43f

Please sign in to comment.