Skip to content

Commit

Permalink
Add support for PEP 698 - override decorator
Browse files Browse the repository at this point in the history
  • Loading branch information
tmke8 committed Feb 4, 2023
1 parent f527656 commit c616e54
Show file tree
Hide file tree
Showing 9 changed files with 218 additions and 9 deletions.
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
32 changes: 23 additions & 9 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1760,25 +1760,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 @@ -1789,22 +1799,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 @@ -4638,7 +4650,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 @@ -1407,6 +1407,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 with this name was found",
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
3 changes: 3 additions & 0 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,7 @@ class FuncDef(FuncItem, SymbolNode, Statement):
"deco_line",
"is_trivial_body",
"is_mypy_only",
"is_explicit_override",
)

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

@property
def name(self) -> str:
Expand Down
5 changes: 5 additions & 0 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@
FINAL_TYPE_NAMES,
NEVER_NAMES,
OVERLOAD_NAMES,
OVERRIDE_DECORATOR_NAMES,
PROTOCOL_NAMES,
REVEAL_TYPE_NAMES,
TPDICT_NAMES,
Expand Down Expand Up @@ -1490,6 +1491,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 @@ -157,6 +157,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
151 changes: 151 additions & 0 deletions test-data/unit/check-functions.test
Original file line number Diff line number Diff line change
Expand Up @@ -2725,3 +2725,154 @@ TS = TypeVar("TS", bound=str)
f: Callable[[Sequence[TI]], None]
g: Callable[[Union[Sequence[TI], Sequence[TS]]], None]
f = g

[case explicitOverride]
from typing import override

class A:
def f(self, x: int) -> str: pass
@override
def g(self, x: int) -> str: pass # E: Method "g" is marked as an override, but no base method with this name was found

class B(A):
@override
def f(self, x: int) -> str: pass
@override
def g(self, x: int) -> str: pass

class C(A):
@override
def f(self, x: str) -> str: pass # E: Argument 1 of "f" is incompatible with supertype "A"; supertype defines the argument type as "int" \
# N: This violates the Liskov substitution principle \
# N: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
def g(self, x: int) -> str: pass

class D(A): pass
class E(D): pass
class F(E):
@override
def f(self, x: int) -> str: pass
[typing fixtures/typing-medium.pyi]

[case explicitOverrideStaticmethod]
from typing import override

class A:
@staticmethod
def f(x: int) -> str: pass

class B(A):
@staticmethod
@override
def f(x: int) -> str: pass
@override
@staticmethod
def g(x: int) -> str: pass # E: Method "g" is marked as an override, but no base method with this name was found

class C(A): # inverted order of decorators
@override
@staticmethod
def f(x: int) -> str: pass
@override
@staticmethod
def g(x: int) -> str: pass # E: Method "g" is marked as an override, but no base method with this name was found

class D(A):
@staticmethod
@override
def f(x: str) -> str: pass # E: Argument 1 of "f" is incompatible with supertype "A"; supertype defines the argument type as "int" \
# N: This violates the Liskov substitution principle \
# N: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
[typing fixtures/typing-medium.pyi]
[builtins fixtures/callable.pyi]

[case explicitOverrideClassmethod]
from typing import override

class A:
@classmethod
def f(cls, x: int) -> str: pass

class B(A):
@classmethod
@override
def f(cls, x: int) -> str: pass
@override
@classmethod
def g(cls, x: int) -> str: pass # E: Method "g" is marked as an override, but no base method with this name was found

class C(A): # inverted order of decorators
@override
@classmethod
def f(cls, x: int) -> str: pass
@override
@classmethod
def g(cls, x: int) -> str: pass # E: Method "g" is marked as an override, but no base method with this name was found

class D(A):
@classmethod
@override
def f(cls, x: str) -> str: pass # E: Argument 1 of "f" is incompatible with supertype "A"; supertype defines the argument type as "int" \
# N: This violates the Liskov substitution principle \
# N: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
[typing fixtures/typing-medium.pyi]
[builtins fixtures/callable.pyi]

[case explicitOverrideProperty]
from typing import override

class A:
@property
def f(self) -> str: pass

class B(A):
@property
@override
def f(self) -> str: pass
@override
@property
def g(self) -> str: pass # E: Method "g" is marked as an override, but no base method with this name was found

class C(A): # inverted order of decorators
@override
@property
def f(self) -> str: pass
@override
@property
def g(self) -> str: pass # E: Method "g" is marked as an override, but no base method with this name was found

class D(A):
@property
@override
def f(self) -> int: pass # E: Signature of "f" incompatible with supertype "A"
[builtins fixtures/property.pyi]
[typing fixtures/typing-medium.pyi]

[case invalidExplicitOverride]
from typing import override

@override # E: "override" used with a non-method
def f(x: int) -> str: pass

@override # this should probably throw an error but the signature from typeshed should ensure this already
class A: pass

def g() -> None:
@override # E: "override" used with a non-method
def h(b: bool) -> int: pass
[typing fixtures/typing-medium.pyi]

[case explicitOverrideSpecialMethods]
from typing import override

class A:
def __init__(self, a: int) -> None: pass

class B(A):
@override
def __init__(self, b: str) -> None: pass

class C:
@override
def __init__(self, a: int) -> None: pass
[typing fixtures/typing-medium.pyi]
1 change: 1 addition & 0 deletions test-data/unit/fixtures/property.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class classmethod: pass
class list: pass
class dict: pass
class int: pass
class float: pass
class str: pass
class bytes: pass
class bool: pass
Expand Down
1 change: 1 addition & 0 deletions test-data/unit/fixtures/typing-medium.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,4 @@ class _SpecialForm: pass
TYPE_CHECKING = 1

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

0 comments on commit c616e54

Please sign in to comment.