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
cdce8p authored Jul 13, 2023
1 parent 8a5d8f0 commit dfea43f
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
@@ -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:

@@ -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
39 changes: 39 additions & 0 deletions docs/source/error_code_list2.rst
Original file line number Diff line number Diff line change
@@ -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
@@ -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)
@@ -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.
@@ -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
@@ -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)):
6 changes: 6 additions & 0 deletions mypy/errorcodes.py
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 10 additions & 0 deletions mypy/messages.py
Original file line number Diff line number Diff line change
@@ -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)

182 changes: 164 additions & 18 deletions test-data/unit/check-functions.test
Original file line number Diff line number Diff line change
@@ -2759,8 +2759,7 @@ class E(D): pass
class F(E):
@override
def f(self, x: int) -> str: pass
[typing fixtures/typing-full.pyi]
[builtins fixtures/tuple.pyi]
[typing fixtures/typing-override.pyi]

[case explicitOverrideStaticmethod]
# flags: --python-version 3.12
@@ -2792,8 +2791,8 @@ class D(A):
def f(x: str) -> str: pass # E: Argument 1 of "f" is incompatible with supertype "A"; supertype defines the argument type as "int" \
# N: This violates the Liskov substitution principle \
# N: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
[typing fixtures/typing-full.pyi]
[builtins fixtures/callable.pyi]
[typing fixtures/typing-override.pyi]
[builtins fixtures/staticmethod.pyi]

[case explicitOverrideClassmethod]
# flags: --python-version 3.12
@@ -2825,8 +2824,8 @@ class D(A):
def f(cls, x: str) -> str: pass # E: Argument 1 of "f" is incompatible with supertype "A"; supertype defines the argument type as "int" \
# N: This violates the Liskov substitution principle \
# N: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
[typing fixtures/typing-full.pyi]
[builtins fixtures/callable.pyi]
[typing fixtures/typing-override.pyi]
[builtins fixtures/classmethod.pyi]

[case explicitOverrideProperty]
# flags: --python-version 3.12
@@ -2860,8 +2859,8 @@ class D(A):
# N: str \
# N: Subclass: \
# N: int
[typing fixtures/typing-override.pyi]
[builtins fixtures/property.pyi]
[typing fixtures/typing-full.pyi]

[case explicitOverrideSettableProperty]
# flags: --python-version 3.12
@@ -2898,8 +2897,8 @@ class D(A):

@f.setter
def f(self, value: int) -> None: pass
[typing fixtures/typing-override.pyi]
[builtins fixtures/property.pyi]
[typing fixtures/typing-full.pyi]

[case invalidExplicitOverride]
# flags: --python-version 3.12
@@ -2914,8 +2913,7 @@ class A: pass
def g() -> None:
@override # E: "override" used with a non-method
def h(b: bool) -> int: pass
[typing fixtures/typing-full.pyi]
[builtins fixtures/tuple.pyi]
[typing fixtures/typing-override.pyi]

[case explicitOverrideSpecialMethods]
# flags: --python-version 3.12
@@ -2931,8 +2929,7 @@ class B(A):
class C:
@override
def __init__(self, a: int) -> None: pass
[typing fixtures/typing-full.pyi]
[builtins fixtures/tuple.pyi]
[typing fixtures/typing-override.pyi]

[case explicitOverrideFromExtensions]
from typing_extensions import override
@@ -2943,7 +2940,6 @@ class A:
class B(A):
@override
def f2(self, x: int) -> str: pass # E: Method "f2" is marked as an override, but no base method was found with this name
[typing fixtures/typing-full.pyi]
[builtins fixtures/tuple.pyi]

[case explicitOverrideOverloads]
@@ -2960,8 +2956,7 @@ class B(A):
def f2(self, x: str) -> str: pass
@override
def f2(self, x: int | str) -> str: pass
[typing fixtures/typing-full.pyi]
[builtins fixtures/tuple.pyi]
[typing fixtures/typing-override.pyi]

[case explicitOverrideNotOnOverloadsImplementation]
# flags: --python-version 3.12
@@ -2985,8 +2980,7 @@ class C(A):
@overload
def f(self, y: str) -> str: pass
def f(self, y: int | str) -> str: pass
[typing fixtures/typing-full.pyi]
[builtins fixtures/tuple.pyi]
[typing fixtures/typing-override.pyi]

[case explicitOverrideOnMultipleOverloads]
# flags: --python-version 3.12
@@ -3012,5 +3006,157 @@ class C(A):
def f(self, y: str) -> str: pass
@override
def f(self, y: int | str) -> str: pass
[typing fixtures/typing-full.pyi]
[typing fixtures/typing-override.pyi]

[case explicitOverrideCyclicDependency]
# flags: --python-version 3.12
import b
[file a.py]
from typing import override
import b
import c

class A(b.B):
@override # This is fine
@c.deco
def meth(self) -> int: ...
[file b.py]
import a
import c

class B:
@c.deco
def meth(self) -> int: ...
[file c.py]
from typing import TypeVar, Tuple, Callable
T = TypeVar('T')
def deco(f: Callable[..., T]) -> Callable[..., Tuple[T, int]]: ...
[builtins fixtures/tuple.pyi]
[typing fixtures/typing-override.pyi]

[case requireExplicitOverrideMethod]
# flags: --enable-error-code explicit-override --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 using @override but is overriding a method in class "__main__.A"

class D(B):
def f(self, y: int) -> str: pass # E: Method "f" is not using @override but is overriding a method in class "__main__.B"
[typing fixtures/typing-override.pyi]

[case requireExplicitOverrideSpecialMethod]
# flags: --enable-error-code explicit-override --python-version 3.12
from typing import Callable, Self, TypeVar, override, overload

T = TypeVar('T')
def some_decorator(f: Callable[..., T]) -> Callable[..., T]: ...

# 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

class B(A):
def __init__(self) -> None: pass
def __new__(cls) -> Self: pass

class C(A):
@some_decorator
def __init__(self) -> None: pass

@some_decorator
def __new__(cls) -> Self: pass

class D(A):
@overload
def __init__(self, x: int) -> None: ...
@overload
def __init__(self, x: str) -> None: ...
def __init__(self, x): pass

@overload
def __new__(cls, x: int) -> Self: pass
@overload
def __new__(cls, x: str) -> Self: pass
def __new__(cls, x): pass
[typing fixtures/typing-override.pyi]

[case requireExplicitOverrideProperty]
# flags: --enable-error-code explicit-override --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 using @override but is overriding a method in class "__main__.A"
[typing fixtures/typing-override.pyi]
[builtins fixtures/property.pyi]

[case requireExplicitOverrideOverload]
# flags: --enable-error-code explicit-override --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
@override
def f(self, y: int) -> str: ...
@overload
def f(self, y: str) -> str: ...
def f(self, y): pass

class D(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 using @override but is overriding a method in class "__main__.A"
[typing fixtures/typing-override.pyi]

[case requireExplicitOverrideMultipleInheritance]
# flags: --enable-error-code explicit-override --python-version 3.12
from typing import override

class A:
def f(self, x: int) -> str: pass
class B:
def f(self, y: int) -> str: pass

class C(A, B):
@override
def f(self, z: int) -> str: pass

class D(A, B):
def f(self, z: int) -> str: pass # E: Method "f" is not using @override but is overriding a method in class "__main__.A"
[typing fixtures/typing-override.pyi]
25 changes: 25 additions & 0 deletions test-data/unit/fixtures/typing-override.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
TypeVar = 0
Generic = 0
Any = 0
overload = 0
Type = 0
Literal = 0
Optional = 0
Self = 0
Tuple = 0
ClassVar = 0
Callable = 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 dfea43f

Please sign in to comment.