Skip to content

Commit c1fb57d

Browse files
authored
Support for PEP 698 override decorator (#14609)
Closes #14072 This implements support for [PEP 698](https://peps.python.org/pep-0698/), which has recently been accepted for Python 3.12. However, this doesn't yet add the "strict mode" that is recommended in the PEP.
1 parent 16b5922 commit c1fb57d

File tree

10 files changed

+349
-10
lines changed

10 files changed

+349
-10
lines changed

docs/source/class_basics.rst

+25
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,31 @@ override has a compatible signature:
208208
subtype such as ``list[int]``. Similarly, you can vary argument types
209209
**contravariantly** -- subclasses can have more general argument types.
210210

211+
In order to ensure that your code remains correct when renaming methods,
212+
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
214+
method is then renamed while the overriding method is not, mypy will
215+
show an error:
216+
217+
.. code-block:: python
218+
219+
from typing import override
220+
221+
class Base:
222+
def f(self, x: int) -> None:
223+
...
224+
def g_renamed(self, y: str) -> None:
225+
...
226+
227+
class Derived1(Base):
228+
@override
229+
def f(self, x: int) -> None: # OK
230+
...
231+
232+
@override
233+
def g(self, y: str) -> None: # Error: no corresponding base method found
234+
...
235+
211236
You can also override a statically typed method with a dynamically
212237
typed one. This allows dynamically typed code to override methods
213238
defined in library classes without worrying about their type

mypy/checker.py

+26-10
Original file line numberDiff line numberDiff line change
@@ -641,7 +641,9 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
641641
if defn.impl:
642642
defn.impl.accept(self)
643643
if defn.info:
644-
self.check_method_override(defn)
644+
found_base_method = self.check_method_override(defn)
645+
if defn.is_explicit_override and found_base_method is False:
646+
self.msg.no_overridable_method(defn.name, defn)
645647
self.check_inplace_operator_method(defn)
646648
if not defn.is_property:
647649
self.check_overlapping_overloads(defn)
@@ -1807,25 +1809,35 @@ def expand_typevars(
18071809
else:
18081810
return [(defn, typ)]
18091811

1810-
def check_method_override(self, defn: FuncDef | OverloadedFuncDef | Decorator) -> None:
1812+
def check_method_override(self, defn: FuncDef | OverloadedFuncDef | Decorator) -> bool | None:
18111813
"""Check if function definition is compatible with base classes.
18121814
18131815
This may defer the method if a signature is not available in at least one base class.
1816+
Return ``None`` if that happens.
1817+
1818+
Return ``True`` if an attribute with the method name was found in the base class.
18141819
"""
18151820
# Check against definitions in base classes.
1821+
found_base_method = False
18161822
for base in defn.info.mro[1:]:
1817-
if self.check_method_or_accessor_override_for_base(defn, base):
1823+
result = self.check_method_or_accessor_override_for_base(defn, base)
1824+
if result is None:
18181825
# Node was deferred, we will have another attempt later.
1819-
return
1826+
return None
1827+
found_base_method |= result
1828+
return found_base_method
18201829

18211830
def check_method_or_accessor_override_for_base(
18221831
self, defn: FuncDef | OverloadedFuncDef | Decorator, base: TypeInfo
1823-
) -> bool:
1832+
) -> bool | None:
18241833
"""Check if method definition is compatible with a base class.
18251834
1826-
Return True if the node was deferred because one of the corresponding
1835+
Return ``None`` if the node was deferred because one of the corresponding
18271836
superclass nodes is not ready.
1837+
1838+
Return ``True`` if an attribute with the method name was found in the base class.
18281839
"""
1840+
found_base_method = False
18291841
if base:
18301842
name = defn.name
18311843
base_attr = base.names.get(name)
@@ -1836,22 +1848,24 @@ def check_method_or_accessor_override_for_base(
18361848
# Second, final can't override anything writeable independently of types.
18371849
if defn.is_final:
18381850
self.check_if_final_var_override_writable(name, base_attr.node, defn)
1851+
found_base_method = True
18391852

18401853
# Check the type of override.
18411854
if name not in ("__init__", "__new__", "__init_subclass__"):
18421855
# Check method override
18431856
# (__init__, __new__, __init_subclass__ are special).
18441857
if self.check_method_override_for_base_with_name(defn, name, base):
1845-
return True
1858+
return None
18461859
if name in operators.inplace_operator_methods:
18471860
# Figure out the name of the corresponding operator method.
18481861
method = "__" + name[3:]
18491862
# An inplace operator method such as __iadd__ might not be
18501863
# always introduced safely if a base class defined __add__.
18511864
# TODO can't come up with an example where this is
18521865
# necessary; now it's "just in case"
1853-
return self.check_method_override_for_base_with_name(defn, method, base)
1854-
return False
1866+
if self.check_method_override_for_base_with_name(defn, method, base):
1867+
return None
1868+
return found_base_method
18551869

18561870
def check_method_override_for_base_with_name(
18571871
self, defn: FuncDef | OverloadedFuncDef | Decorator, name: str, base: TypeInfo
@@ -4715,7 +4729,9 @@ def visit_decorator(self, e: Decorator) -> None:
47154729
self.check_incompatible_property_override(e)
47164730
# For overloaded functions we already checked override for overload as a whole.
47174731
if e.func.info and not e.func.is_dynamic() and not e.is_overload:
4718-
self.check_method_override(e)
4732+
found_base_method = self.check_method_override(e)
4733+
if e.func.is_explicit_override and found_base_method is False:
4734+
self.msg.no_overridable_method(e.func.name, e.func)
47194735

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

mypy/messages.py

+7
Original file line numberDiff line numberDiff line change
@@ -1493,6 +1493,13 @@ def cant_assign_to_method(self, context: Context) -> None:
14931493
def cant_assign_to_classvar(self, name: str, context: Context) -> None:
14941494
self.fail(f'Cannot assign to class variable "{name}" via instance', context)
14951495

1496+
def no_overridable_method(self, name: str, context: Context) -> None:
1497+
self.fail(
1498+
f'Method "{name}" is marked as an override, '
1499+
"but no base method was found with this name",
1500+
context,
1501+
)
1502+
14961503
def final_cant_override_writable(self, name: str, ctx: Context) -> None:
14971504
self.fail(f'Cannot override writable attribute "{name}" with a final one', ctx)
14981505

mypy/nodes.py

+2
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,7 @@ class FuncBase(Node):
512512
"is_class", # Uses "@classmethod" (explicit or implicit)
513513
"is_static", # Uses "@staticmethod"
514514
"is_final", # Uses "@final"
515+
"is_explicit_override", # Uses "@override"
515516
"_fullname",
516517
)
517518

@@ -529,6 +530,7 @@ def __init__(self) -> None:
529530
self.is_class = False
530531
self.is_static = False
531532
self.is_final = False
533+
self.is_explicit_override = False
532534
# Name with module prefix
533535
self._fullname = ""
534536

mypy/semanal.py

+8
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@
245245
FINAL_TYPE_NAMES,
246246
NEVER_NAMES,
247247
OVERLOAD_NAMES,
248+
OVERRIDE_DECORATOR_NAMES,
248249
PROTOCOL_NAMES,
249250
REVEAL_TYPE_NAMES,
250251
TPDICT_NAMES,
@@ -1196,6 +1197,9 @@ def analyze_overload_sigs_and_impl(
11961197
types.append(callable)
11971198
if item.var.is_property:
11981199
self.fail("An overload can not be a property", item)
1200+
# If any item was decorated with `@override`, the whole overload
1201+
# becomes an explicit override.
1202+
defn.is_explicit_override |= item.func.is_explicit_override
11991203
elif isinstance(item, FuncDef):
12001204
if i == len(defn.items) - 1 and not self.is_stub_file:
12011205
impl = item
@@ -1495,6 +1499,10 @@ def visit_decorator(self, dec: Decorator) -> None:
14951499
dec.func.is_class = True
14961500
dec.var.is_classmethod = True
14971501
self.check_decorated_function_is_method("classmethod", dec)
1502+
elif refers_to_fullname(d, OVERRIDE_DECORATOR_NAMES):
1503+
removed.append(i)
1504+
dec.func.is_explicit_override = True
1505+
self.check_decorated_function_is_method("override", dec)
14981506
elif refers_to_fullname(
14991507
d,
15001508
(

mypy/types.py

+2
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@
156156
"typing.dataclass_transform",
157157
"typing_extensions.dataclass_transform",
158158
)
159+
# Supported @override decorator names.
160+
OVERRIDE_DECORATOR_NAMES: Final = ("typing.override", "typing_extensions.override")
159161

160162
# A placeholder used for Bogus[...] parameters
161163
_dummy: Final[Any] = object()

0 commit comments

Comments
 (0)