Skip to content

Commit

Permalink
Support for PEP 698 override decorator (#14609)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
tmke8 committed May 12, 2023
1 parent 16b5922 commit c1fb57d
Show file tree
Hide file tree
Showing 10 changed files with 349 additions and 10 deletions.
25 changes: 25 additions & 0 deletions docs/source/class_basics.rst
Expand Up @@ -208,6 +208,31 @@ override has a compatible signature:
subtype such as ``list[int]``. Similarly, you can vary argument types
**contravariantly** -- subclasses can have more general argument types.

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 is then renamed while the overriding method is not, mypy will
show an error:

.. code-block:: python
from typing import override
class Base:
def f(self, x: int) -> None:
...
def g_renamed(self, y: str) -> None:
...
class Derived1(Base):
@override
def f(self, x: int) -> None: # OK
...
@override
def g(self, y: str) -> None: # Error: no corresponding base method found
...
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
36 changes: 26 additions & 10 deletions mypy/checker.py
Expand Up @@ -641,7 +641,9 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
if defn.impl:
defn.impl.accept(self)
if defn.info:
self.check_method_override(defn)
found_base_method = self.check_method_override(defn)
if defn.is_explicit_override and found_base_method is False:
self.msg.no_overridable_method(defn.name, defn)
self.check_inplace_operator_method(defn)
if not defn.is_property:
self.check_overlapping_overloads(defn)
Expand Down Expand Up @@ -1807,25 +1809,35 @@ def expand_typevars(
else:
return [(defn, typ)]

def check_method_override(self, defn: FuncDef | OverloadedFuncDef | Decorator) -> None:
def check_method_override(self, defn: FuncDef | OverloadedFuncDef | Decorator) -> bool | 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.
"""
# Check against definitions in base classes.
found_base_method = False
for base in defn.info.mro[1:]:
if self.check_method_or_accessor_override_for_base(defn, base):
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
return None
found_base_method |= result
return found_base_method

def check_method_or_accessor_override_for_base(
self, defn: FuncDef | OverloadedFuncDef | Decorator, base: TypeInfo
) -> bool:
) -> bool | None:
"""Check if method definition is compatible with a base class.
Return True if the node was deferred because one of the corresponding
Return ``None`` if the node was deferred because one of the corresponding
superclass nodes is not ready.
Return ``True`` if an attribute with the method name was found in the base class.
"""
found_base_method = False
if base:
name = defn.name
base_attr = base.names.get(name)
Expand All @@ -1836,22 +1848,24 @@ def check_method_or_accessor_override_for_base(
# Second, final can't override anything writeable independently of types.
if defn.is_final:
self.check_if_final_var_override_writable(name, base_attr.node, defn)
found_base_method = True

# Check the type of override.
if name not in ("__init__", "__new__", "__init_subclass__"):
# Check method override
# (__init__, __new__, __init_subclass__ are special).
if self.check_method_override_for_base_with_name(defn, name, base):
return True
return None
if name in operators.inplace_operator_methods:
# Figure out the name of the corresponding operator method.
method = "__" + name[3:]
# An inplace operator method such as __iadd__ might not be
# always introduced safely if a base class defined __add__.
# TODO can't come up with an example where this is
# necessary; now it's "just in case"
return self.check_method_override_for_base_with_name(defn, method, base)
return False
if self.check_method_override_for_base_with_name(defn, method, base):
return None
return found_base_method

def check_method_override_for_base_with_name(
self, defn: FuncDef | OverloadedFuncDef | Decorator, name: str, base: TypeInfo
Expand Down Expand Up @@ -4715,7 +4729,9 @@ 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:
self.check_method_override(e)
found_base_method = self.check_method_override(e)
if e.func.is_explicit_override and found_base_method is False:
self.msg.no_overridable_method(e.func.name, e.func)

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
7 changes: 7 additions & 0 deletions mypy/messages.py
Expand Up @@ -1493,6 +1493,13 @@ def cant_assign_to_method(self, context: Context) -> None:
def cant_assign_to_classvar(self, name: str, context: Context) -> None:
self.fail(f'Cannot assign to class variable "{name}" via instance', context)

def no_overridable_method(self, name: str, context: Context) -> None:
self.fail(
f'Method "{name}" is marked as an override, '
"but no base method was found with this name",
context,
)

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
2 changes: 2 additions & 0 deletions mypy/nodes.py
Expand Up @@ -512,6 +512,7 @@ class FuncBase(Node):
"is_class", # Uses "@classmethod" (explicit or implicit)
"is_static", # Uses "@staticmethod"
"is_final", # Uses "@final"
"is_explicit_override", # Uses "@override"
"_fullname",
)

Expand All @@ -529,6 +530,7 @@ def __init__(self) -> None:
self.is_class = False
self.is_static = False
self.is_final = False
self.is_explicit_override = False
# Name with module prefix
self._fullname = ""

Expand Down
8 changes: 8 additions & 0 deletions mypy/semanal.py
Expand Up @@ -245,6 +245,7 @@
FINAL_TYPE_NAMES,
NEVER_NAMES,
OVERLOAD_NAMES,
OVERRIDE_DECORATOR_NAMES,
PROTOCOL_NAMES,
REVEAL_TYPE_NAMES,
TPDICT_NAMES,
Expand Down Expand Up @@ -1196,6 +1197,9 @@ def analyze_overload_sigs_and_impl(
types.append(callable)
if item.var.is_property:
self.fail("An overload can not be a property", item)
# If any item was decorated with `@override`, the whole overload
# becomes an explicit override.
defn.is_explicit_override |= item.func.is_explicit_override
elif isinstance(item, FuncDef):
if i == len(defn.items) - 1 and not self.is_stub_file:
impl = item
Expand Down Expand Up @@ -1495,6 +1499,10 @@ def visit_decorator(self, dec: Decorator) -> None:
dec.func.is_class = True
dec.var.is_classmethod = True
self.check_decorated_function_is_method("classmethod", dec)
elif refers_to_fullname(d, OVERRIDE_DECORATOR_NAMES):
removed.append(i)
dec.func.is_explicit_override = True
self.check_decorated_function_is_method("override", dec)
elif refers_to_fullname(
d,
(
Expand Down
2 changes: 2 additions & 0 deletions mypy/types.py
Expand Up @@ -156,6 +156,8 @@
"typing.dataclass_transform",
"typing_extensions.dataclass_transform",
)
# Supported @override decorator names.
OVERRIDE_DECORATOR_NAMES: Final = ("typing.override", "typing_extensions.override")

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

0 comments on commit c1fb57d

Please sign in to comment.