Skip to content

Commit c616e54

Browse files
committed
Add support for PEP 698 - override decorator
1 parent f527656 commit c616e54

File tree

9 files changed

+218
-9
lines changed

9 files changed

+218
-9
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

+23-9
Original file line numberDiff line numberDiff line change
@@ -1760,25 +1760,35 @@ def expand_typevars(
17601760
else:
17611761
return [(defn, typ)]
17621762

1763-
def check_method_override(self, defn: FuncDef | OverloadedFuncDef | Decorator) -> None:
1763+
def check_method_override(self, defn: FuncDef | OverloadedFuncDef | Decorator) -> bool | None:
17641764
"""Check if function definition is compatible with base classes.
17651765
17661766
This may defer the method if a signature is not available in at least one base class.
1767+
Return ``None`` if that happens.
1768+
1769+
Return ``True`` if an attribute with the method name was found in the base class.
17671770
"""
17681771
# Check against definitions in base classes.
1772+
found_base_method = False
17691773
for base in defn.info.mro[1:]:
1770-
if self.check_method_or_accessor_override_for_base(defn, base):
1774+
result = self.check_method_or_accessor_override_for_base(defn, base)
1775+
if result is None:
17711776
# Node was deferred, we will have another attempt later.
1772-
return
1777+
return None
1778+
found_base_method |= result
1779+
return found_base_method
17731780

17741781
def check_method_or_accessor_override_for_base(
17751782
self, defn: FuncDef | OverloadedFuncDef | Decorator, base: TypeInfo
1776-
) -> bool:
1783+
) -> bool | None:
17771784
"""Check if method definition is compatible with a base class.
17781785
1779-
Return True if the node was deferred because one of the corresponding
1786+
Return ``None`` if the node was deferred because one of the corresponding
17801787
superclass nodes is not ready.
1788+
1789+
Return ``True`` if an attribute with the method name was found in the base class.
17811790
"""
1791+
found_base_method = False
17821792
if base:
17831793
name = defn.name
17841794
base_attr = base.names.get(name)
@@ -1789,22 +1799,24 @@ def check_method_or_accessor_override_for_base(
17891799
# Second, final can't override anything writeable independently of types.
17901800
if defn.is_final:
17911801
self.check_if_final_var_override_writable(name, base_attr.node, defn)
1802+
found_base_method = True
17921803

17931804
# Check the type of override.
17941805
if name not in ("__init__", "__new__", "__init_subclass__"):
17951806
# Check method override
17961807
# (__init__, __new__, __init_subclass__ are special).
17971808
if self.check_method_override_for_base_with_name(defn, name, base):
1798-
return True
1809+
return None
17991810
if name in operators.inplace_operator_methods:
18001811
# Figure out the name of the corresponding operator method.
18011812
method = "__" + name[3:]
18021813
# An inplace operator method such as __iadd__ might not be
18031814
# always introduced safely if a base class defined __add__.
18041815
# TODO can't come up with an example where this is
18051816
# necessary; now it's "just in case"
1806-
return self.check_method_override_for_base_with_name(defn, method, base)
1807-
return False
1817+
if self.check_method_override_for_base_with_name(defn, method, base):
1818+
return None
1819+
return found_base_method
18081820

18091821
def check_method_override_for_base_with_name(
18101822
self, defn: FuncDef | OverloadedFuncDef | Decorator, name: str, base: TypeInfo
@@ -4638,7 +4650,9 @@ def visit_decorator(self, e: Decorator) -> None:
46384650
self.check_incompatible_property_override(e)
46394651
# For overloaded functions we already checked override for overload as a whole.
46404652
if e.func.info and not e.func.is_dynamic() and not e.is_overload:
4641-
self.check_method_override(e)
4653+
found_base_method = self.check_method_override(e)
4654+
if e.func.is_explicit_override and found_base_method is False:
4655+
self.msg.no_overridable_method(e.func.name, e.func)
46424656

46434657
if e.func.info and e.func.name in ("__init__", "__new__"):
46444658
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
@@ -1407,6 +1407,13 @@ def cant_assign_to_method(self, context: Context) -> None:
14071407
def cant_assign_to_classvar(self, name: str, context: Context) -> None:
14081408
self.fail(f'Cannot assign to class variable "{name}" via instance', context)
14091409

1410+
def no_overridable_method(self, name: str, context: Context) -> None:
1411+
self.fail(
1412+
f'Method "{name}" is marked as an override, '
1413+
"but no base method with this name was found",
1414+
context,
1415+
)
1416+
14101417
def final_cant_override_writable(self, name: str, ctx: Context) -> None:
14111418
self.fail(f'Cannot override writable attribute "{name}" with a final one', ctx)
14121419

mypy/nodes.py

+3
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,7 @@ class FuncDef(FuncItem, SymbolNode, Statement):
758758
"deco_line",
759759
"is_trivial_body",
760760
"is_mypy_only",
761+
"is_explicit_override",
761762
)
762763

763764
__match_args__ = ("name", "arguments", "type", "body")
@@ -785,6 +786,8 @@ def __init__(
785786
self.deco_line: int | None = None
786787
# Definitions that appear in if TYPE_CHECKING are marked with this flag.
787788
self.is_mypy_only = False
789+
# Decorated with @override
790+
self.is_explicit_override = False
788791

789792
@property
790793
def name(self) -> str:

mypy/semanal.py

+5
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@
241241
FINAL_TYPE_NAMES,
242242
NEVER_NAMES,
243243
OVERLOAD_NAMES,
244+
OVERRIDE_DECORATOR_NAMES,
244245
PROTOCOL_NAMES,
245246
REVEAL_TYPE_NAMES,
246247
TPDICT_NAMES,
@@ -1490,6 +1491,10 @@ def visit_decorator(self, dec: Decorator) -> None:
14901491
dec.func.is_class = True
14911492
dec.var.is_classmethod = True
14921493
self.check_decorated_function_is_method("classmethod", dec)
1494+
elif refers_to_fullname(d, OVERRIDE_DECORATOR_NAMES):
1495+
removed.append(i)
1496+
dec.func.is_explicit_override = True
1497+
self.check_decorated_function_is_method("override", dec)
14931498
elif refers_to_fullname(
14941499
d,
14951500
(

mypy/types.py

+2
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@
157157
"typing.dataclass_transform",
158158
"typing_extensions.dataclass_transform",
159159
)
160+
# Supported @override decorator names.
161+
OVERRIDE_DECORATOR_NAMES: Final = ("typing.override", "typing_extensions.override")
160162

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

test-data/unit/check-functions.test

+151
Original file line numberDiff line numberDiff line change
@@ -2725,3 +2725,154 @@ TS = TypeVar("TS", bound=str)
27252725
f: Callable[[Sequence[TI]], None]
27262726
g: Callable[[Union[Sequence[TI], Sequence[TS]]], None]
27272727
f = g
2728+
2729+
[case explicitOverride]
2730+
from typing import override
2731+
2732+
class A:
2733+
def f(self, x: int) -> str: pass
2734+
@override
2735+
def g(self, x: int) -> str: pass # E: Method "g" is marked as an override, but no base method with this name was found
2736+
2737+
class B(A):
2738+
@override
2739+
def f(self, x: int) -> str: pass
2740+
@override
2741+
def g(self, x: int) -> str: pass
2742+
2743+
class C(A):
2744+
@override
2745+
def f(self, x: str) -> str: pass # E: Argument 1 of "f" is incompatible with supertype "A"; supertype defines the argument type as "int" \
2746+
# N: This violates the Liskov substitution principle \
2747+
# N: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
2748+
def g(self, x: int) -> str: pass
2749+
2750+
class D(A): pass
2751+
class E(D): pass
2752+
class F(E):
2753+
@override
2754+
def f(self, x: int) -> str: pass
2755+
[typing fixtures/typing-medium.pyi]
2756+
2757+
[case explicitOverrideStaticmethod]
2758+
from typing import override
2759+
2760+
class A:
2761+
@staticmethod
2762+
def f(x: int) -> str: pass
2763+
2764+
class B(A):
2765+
@staticmethod
2766+
@override
2767+
def f(x: int) -> str: pass
2768+
@override
2769+
@staticmethod
2770+
def g(x: int) -> str: pass # E: Method "g" is marked as an override, but no base method with this name was found
2771+
2772+
class C(A): # inverted order of decorators
2773+
@override
2774+
@staticmethod
2775+
def f(x: int) -> str: pass
2776+
@override
2777+
@staticmethod
2778+
def g(x: int) -> str: pass # E: Method "g" is marked as an override, but no base method with this name was found
2779+
2780+
class D(A):
2781+
@staticmethod
2782+
@override
2783+
def f(x: str) -> str: pass # E: Argument 1 of "f" is incompatible with supertype "A"; supertype defines the argument type as "int" \
2784+
# N: This violates the Liskov substitution principle \
2785+
# N: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
2786+
[typing fixtures/typing-medium.pyi]
2787+
[builtins fixtures/callable.pyi]
2788+
2789+
[case explicitOverrideClassmethod]
2790+
from typing import override
2791+
2792+
class A:
2793+
@classmethod
2794+
def f(cls, x: int) -> str: pass
2795+
2796+
class B(A):
2797+
@classmethod
2798+
@override
2799+
def f(cls, x: int) -> str: pass
2800+
@override
2801+
@classmethod
2802+
def g(cls, x: int) -> str: pass # E: Method "g" is marked as an override, but no base method with this name was found
2803+
2804+
class C(A): # inverted order of decorators
2805+
@override
2806+
@classmethod
2807+
def f(cls, x: int) -> str: pass
2808+
@override
2809+
@classmethod
2810+
def g(cls, x: int) -> str: pass # E: Method "g" is marked as an override, but no base method with this name was found
2811+
2812+
class D(A):
2813+
@classmethod
2814+
@override
2815+
def f(cls, x: str) -> str: pass # E: Argument 1 of "f" is incompatible with supertype "A"; supertype defines the argument type as "int" \
2816+
# N: This violates the Liskov substitution principle \
2817+
# N: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
2818+
[typing fixtures/typing-medium.pyi]
2819+
[builtins fixtures/callable.pyi]
2820+
2821+
[case explicitOverrideProperty]
2822+
from typing import override
2823+
2824+
class A:
2825+
@property
2826+
def f(self) -> str: pass
2827+
2828+
class B(A):
2829+
@property
2830+
@override
2831+
def f(self) -> str: pass
2832+
@override
2833+
@property
2834+
def g(self) -> str: pass # E: Method "g" is marked as an override, but no base method with this name was found
2835+
2836+
class C(A): # inverted order of decorators
2837+
@override
2838+
@property
2839+
def f(self) -> str: pass
2840+
@override
2841+
@property
2842+
def g(self) -> str: pass # E: Method "g" is marked as an override, but no base method with this name was found
2843+
2844+
class D(A):
2845+
@property
2846+
@override
2847+
def f(self) -> int: pass # E: Signature of "f" incompatible with supertype "A"
2848+
[builtins fixtures/property.pyi]
2849+
[typing fixtures/typing-medium.pyi]
2850+
2851+
[case invalidExplicitOverride]
2852+
from typing import override
2853+
2854+
@override # E: "override" used with a non-method
2855+
def f(x: int) -> str: pass
2856+
2857+
@override # this should probably throw an error but the signature from typeshed should ensure this already
2858+
class A: pass
2859+
2860+
def g() -> None:
2861+
@override # E: "override" used with a non-method
2862+
def h(b: bool) -> int: pass
2863+
[typing fixtures/typing-medium.pyi]
2864+
2865+
[case explicitOverrideSpecialMethods]
2866+
from typing import override
2867+
2868+
class A:
2869+
def __init__(self, a: int) -> None: pass
2870+
2871+
class B(A):
2872+
@override
2873+
def __init__(self, b: str) -> None: pass
2874+
2875+
class C:
2876+
@override
2877+
def __init__(self, a: int) -> None: pass
2878+
[typing fixtures/typing-medium.pyi]

test-data/unit/fixtures/property.pyi

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class classmethod: pass
1616
class list: pass
1717
class dict: pass
1818
class int: pass
19+
class float: pass
1920
class str: pass
2021
class bytes: pass
2122
class bool: pass

test-data/unit/fixtures/typing-medium.pyi

+1
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,4 @@ class _SpecialForm: pass
7373
TYPE_CHECKING = 1
7474

7575
def dataclass_transform() -> Callable[[T], T]: ...
76+
def override(f: T) -> T: ...

0 commit comments

Comments
 (0)