Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for PEP 698 - override decorator #14609

Merged
merged 5 commits into from
May 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions docs/source/class_basics.rst
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading