Skip to content

Commit 12c0968

Browse files
committed
Add support for PEP 698 - override decorator
1 parent 786c7b0 commit 12c0968

File tree

9 files changed

+221
-9
lines changed

9 files changed

+221
-9
lines changed

docs/source/class_basics.rst

Lines changed: 25 additions & 0 deletions
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

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1759,25 +1759,35 @@ def expand_typevars(
17591759
else:
17601760
return [(defn, typ)]
17611761

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

17731780
def check_method_or_accessor_override_for_base(
17741781
self, defn: FuncDef | OverloadedFuncDef | Decorator, base: TypeInfo
1775-
) -> bool:
1782+
) -> bool | None:
17761783
"""Check if method definition is compatible with a base class.
17771784
1778-
Return True if the node was deferred because one of the corresponding
1785+
Return ``None`` if the node was deferred because one of the corresponding
17791786
superclass nodes is not ready.
1787+
1788+
Return ``True`` if an attribute with the method name was found in the base class.
17801789
"""
1790+
found_base_method = False
17811791
if base:
17821792
name = defn.name
17831793
base_attr = base.names.get(name)
@@ -1788,22 +1798,24 @@ def check_method_or_accessor_override_for_base(
17881798
# Second, final can't override anything writeable independently of types.
17891799
if defn.is_final:
17901800
self.check_if_final_var_override_writable(name, base_attr.node, defn)
1801+
found_base_method = True
17911802

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

18081820
def check_method_override_for_base_with_name(
18091821
self, defn: FuncDef | OverloadedFuncDef | Decorator, name: str, base: TypeInfo
@@ -4636,7 +4648,9 @@ def visit_decorator(self, e: Decorator) -> None:
46364648
self.check_incompatible_property_override(e)
46374649
# For overloaded functions we already checked override for overload as a whole.
46384650
if e.func.info and not e.func.is_dynamic() and not e.is_overload:
4639-
self.check_method_override(e)
4651+
found_base_method = self.check_method_override(e)
4652+
if e.func.is_explicit_override and found_base_method is False:
4653+
self.msg.no_overridable_method(e.func.name, e.func)
46404654

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

mypy/messages.py

Lines changed: 7 additions & 0 deletions
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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,7 @@ class FuncDef(FuncItem, SymbolNode, Statement):
752752
"is_mypy_only",
753753
# Present only when a function is decorated with @typing.datasclass_transform or similar
754754
"dataclass_transform_spec",
755+
"is_explicit_override",
755756
)
756757

757758
__match_args__ = ("name", "arguments", "type", "body")
@@ -780,6 +781,8 @@ def __init__(
780781
# Definitions that appear in if TYPE_CHECKING are marked with this flag.
781782
self.is_mypy_only = False
782783
self.dataclass_transform_spec: DataclassTransformSpec | None = None
784+
# Decorated with @override
785+
self.is_explicit_override = False
783786

784787
@property
785788
def name(self) -> str:

mypy/semanal.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@
243243
FINAL_TYPE_NAMES,
244244
NEVER_NAMES,
245245
OVERLOAD_NAMES,
246+
OVERRIDE_DECORATOR_NAMES,
246247
PROTOCOL_NAMES,
247248
REVEAL_TYPE_NAMES,
248249
TPDICT_NAMES,
@@ -1491,6 +1492,10 @@ def visit_decorator(self, dec: Decorator) -> None:
14911492
dec.func.is_class = True
14921493
dec.var.is_classmethod = True
14931494
self.check_decorated_function_is_method("classmethod", dec)
1495+
elif refers_to_fullname(d, OVERRIDE_DECORATOR_NAMES):
1496+
removed.append(i)
1497+
dec.func.is_explicit_override = True
1498+
self.check_decorated_function_is_method("override", dec)
14941499
elif refers_to_fullname(
14951500
d,
14961501
(

mypy/types.py

Lines changed: 2 additions & 0 deletions
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

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2725,3 +2725,157 @@ 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-full.pyi]
2756+
[builtins fixtures/tuple.pyi]
2757+
2758+
[case explicitOverrideStaticmethod]
2759+
from typing import override
2760+
2761+
class A:
2762+
@staticmethod
2763+
def f(x: int) -> str: pass
2764+
2765+
class B(A):
2766+
@staticmethod
2767+
@override
2768+
def f(x: int) -> str: pass
2769+
@override
2770+
@staticmethod
2771+
def g(x: int) -> str: pass # E: Method "g" is marked as an override, but no base method with this name was found
2772+
2773+
class C(A): # inverted order of decorators
2774+
@override
2775+
@staticmethod
2776+
def f(x: int) -> str: pass
2777+
@override
2778+
@staticmethod
2779+
def g(x: int) -> str: pass # E: Method "g" is marked as an override, but no base method with this name was found
2780+
2781+
class D(A):
2782+
@staticmethod
2783+
@override
2784+
def f(x: str) -> str: pass # E: Argument 1 of "f" is incompatible with supertype "A"; supertype defines the argument type as "int" \
2785+
# N: This violates the Liskov substitution principle \
2786+
# N: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
2787+
[typing fixtures/typing-full.pyi]
2788+
[builtins fixtures/callable.pyi]
2789+
2790+
[case explicitOverrideClassmethod]
2791+
from typing import override
2792+
2793+
class A:
2794+
@classmethod
2795+
def f(cls, x: int) -> str: pass
2796+
2797+
class B(A):
2798+
@classmethod
2799+
@override
2800+
def f(cls, x: int) -> str: pass
2801+
@override
2802+
@classmethod
2803+
def g(cls, x: int) -> str: pass # E: Method "g" is marked as an override, but no base method with this name was found
2804+
2805+
class C(A): # inverted order of decorators
2806+
@override
2807+
@classmethod
2808+
def f(cls, x: int) -> str: pass
2809+
@override
2810+
@classmethod
2811+
def g(cls, x: int) -> str: pass # E: Method "g" is marked as an override, but no base method with this name was found
2812+
2813+
class D(A):
2814+
@classmethod
2815+
@override
2816+
def f(cls, x: str) -> str: pass # E: Argument 1 of "f" is incompatible with supertype "A"; supertype defines the argument type as "int" \
2817+
# N: This violates the Liskov substitution principle \
2818+
# N: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
2819+
[typing fixtures/typing-full.pyi]
2820+
[builtins fixtures/callable.pyi]
2821+
2822+
[case explicitOverrideProperty]
2823+
from typing import override
2824+
2825+
class A:
2826+
@property
2827+
def f(self) -> str: pass
2828+
2829+
class B(A):
2830+
@property
2831+
@override
2832+
def f(self) -> str: pass
2833+
@override
2834+
@property
2835+
def g(self) -> str: pass # E: Method "g" is marked as an override, but no base method with this name was found
2836+
2837+
class C(A): # inverted order of decorators
2838+
@override
2839+
@property
2840+
def f(self) -> str: pass
2841+
@override
2842+
@property
2843+
def g(self) -> str: pass # E: Method "g" is marked as an override, but no base method with this name was found
2844+
2845+
class D(A):
2846+
@property
2847+
@override
2848+
def f(self) -> int: pass # E: Signature of "f" incompatible with supertype "A"
2849+
[builtins fixtures/property.pyi]
2850+
[typing fixtures/typing-full.pyi]
2851+
2852+
[case invalidExplicitOverride]
2853+
from typing import override
2854+
2855+
@override # E: "override" used with a non-method
2856+
def f(x: int) -> str: pass
2857+
2858+
@override # this should probably throw an error but the signature from typeshed should ensure this already
2859+
class A: pass
2860+
2861+
def g() -> None:
2862+
@override # E: "override" used with a non-method
2863+
def h(b: bool) -> int: pass
2864+
[typing fixtures/typing-full.pyi]
2865+
[builtins fixtures/tuple.pyi]
2866+
2867+
[case explicitOverrideSpecialMethods]
2868+
from typing import override
2869+
2870+
class A:
2871+
def __init__(self, a: int) -> None: pass
2872+
2873+
class B(A):
2874+
@override
2875+
def __init__(self, b: str) -> None: pass
2876+
2877+
class C:
2878+
@override
2879+
def __init__(self, a: int) -> None: pass
2880+
[typing fixtures/typing-full.pyi]
2881+
[builtins fixtures/tuple.pyi]

test-data/unit/fixtures/property.pyi

Lines changed: 1 addition & 0 deletions
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-full.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,4 @@ def dataclass_transform(
190190
field_specifiers: tuple[type[Any] | Callable[..., Any], ...] = ...,
191191
**kwargs: Any,
192192
) -> Callable[[T], T]: ...
193+
def override(__arg: T) -> T: ...

0 commit comments

Comments
 (0)