ruff/crates/ty_python_semantic/resources/mdtest/override.md

14 KiB

typing.override

Basics

Decorating a method with typing.override decorator is an explicit indication to a type checker that the method is intended to override a method on a superclass. If the decorated method does not in fact override anything, a type checker should report a diagnostic on that method.

from typing_extensions import override, Callable, TypeVar

def lossy_decorator(fn: Callable) -> Callable: ...

class A:
    @override
    def __repr__(self): ...  # fine: overrides `object.__repr__`

class Parent:
    def foo(self): ...

    @property
    def my_property1(self) -> int: ...

    @property
    def my_property2(self) -> int: ...

    baz = None

    @classmethod
    def class_method1(cls) -> int: ...

    @staticmethod
    def static_method1() -> int: ...

    @classmethod
    def class_method2(cls) -> int: ...

    @staticmethod
    def static_method2() -> int: ...

    @lossy_decorator
    def decorated_1(self): ...

    @lossy_decorator
    def decorated_2(self): ...

    @lossy_decorator
    def decorated_3(self): ...

class Child(Parent):
    @override
    def foo(self): ...  # fine: overrides `Parent.foo`

    @property
    @override
    def my_property1(self) -> int: ...  # fine: overrides `Parent.my_property1`

    @override
    @property
    def my_property2(self) -> int: ...  # fine: overrides `Parent.my_property2`

    @override
    def baz(self): ...  # fine: overrides `Parent.baz`

    @classmethod
    @override
    def class_method1(cls) -> int: ...  # fine: overrides `Parent.class_method1`

    @staticmethod
    @override
    def static_method1() -> int: ...  # fine: overrides `Parent.static_method1`

    @override
    @classmethod
    def class_method2(cls) -> int: ...  # fine: overrides `Parent.class_method2`

    @override
    @staticmethod
    def static_method2() -> int: ...  # fine: overrides `Parent.static_method2`

    @override
    def decorated_1(self): ...  # fine: overrides `Parent.decorated_1`

    @override
    @lossy_decorator
    def decorated_2(self): ...  # fine: overrides `Parent.decorated_2`

    @lossy_decorator
    @override
    def decorated_3(self): ...  # fine: overrides `Parent.decorated_3`

class OtherChild(Parent): ...

class Grandchild(OtherChild):
    @override
    def foo(self): ...  # fine: overrides `Parent.foo`

    @override
    @property
    def my_property1(self) -> int: ...  # fine: overrides `Parent.my_property1`

    @override
    def baz(self): ...  # fine: overrides `Parent.baz`

    @classmethod
    @override
    def class_method1(cls) -> int: ...  # fine: overrides `Parent.class_method1`

    @staticmethod
    @override
    def static_method1() -> int: ...  # fine: overrides `Parent.static_method1`

    @override
    @classmethod
    def class_method2(cls) -> int: ...  # fine: overrides `Parent.class_method2`

    @override
    @staticmethod
    def static_method2() -> int: ...  # fine: overrides `Parent.static_method2`

    @override
    def decorated_1(self): ...  # fine: overrides `Parent.decorated_1`

    @override
    @lossy_decorator
    def decorated_2(self): ...  # fine: overrides `Parent.decorated_2`

    @lossy_decorator
    @override
    def decorated_3(self): ...  # fine: overrides `Parent.decorated_3`

class Invalid:
    @override
    def ___reprrr__(self): ...  # error: [invalid-explicit-override]

    @override
    @classmethod
    def foo(self): ...  # error: [invalid-explicit-override]

    @classmethod
    @override
    def bar(self): ...  # error: [invalid-explicit-override]

    @staticmethod
    @override
    def baz(): ...  # error: [invalid-explicit-override]

    @override
    @staticmethod
    def eggs(): ...  # error: [invalid-explicit-override]

    @property
    @override
    def bad_property1(self) -> int: ...  # error: [invalid-explicit-override]

    @override
    @property
    def bad_property2(self) -> int: ...  # error: [invalid-explicit-override]

    @property
    @override
    def bad_settable_property(self) -> int: ...  # error: [invalid-explicit-override]
    @bad_settable_property.setter
    def bad_settable_property(self, x: int) -> None: ...

    @lossy_decorator
    @override
    def lossy(self): ...  # TODO: should emit `invalid-explicit-override` here

    @override
    @lossy_decorator
    def lossy2(self): ...  # TODO: should emit `invalid-explicit-override` here

# TODO: all overrides in this class should cause us to emit *Liskov* violations,
# but not `@override` violations
class LiskovViolatingButNotOverrideViolating(Parent):
    @override
    @property
    def foo(self) -> int: ...

    @override
    def my_property1(self) -> int: ...

    @staticmethod
    @override
    def class_method1() -> int: ...

    @classmethod
    @override
    def static_method1(cls) -> int: ...

# Diagnostic edge case: `override` is very far away from the method definition in the source code:

T = TypeVar("T")

def identity(x: T) -> T: ...

class Foo:
    @override
    @identity
    @identity
    @identity
    @identity
    @identity
    @identity
    @identity
    @identity
    @identity
    @identity
    @identity
    @identity
    @identity
    @identity
    @identity
    @identity
    @identity
    @identity
    def bar(self): ...  # error: [invalid-explicit-override]

Possibly-unbound definitions

from typing_extensions import override

def coinflip() -> bool:
    return False

class Parent:
    if coinflip():
        def method1(self) -> None: ...
        def method2(self) -> None: ...

    if coinflip():
        def method3(self) -> None: ...
        def method4(self) -> None: ...
    else:
        def method3(self) -> None: ...
        def method4(self) -> None: ...

    def method5(self) -> None: ...
    def method6(self) -> None: ...

class Child(Parent):
    @override
    def method1(self) -> None: ...
    @override
    def method2(self) -> None: ...

    if coinflip():
        @override
        def method3(self) -> None: ...

    if coinflip():
        @override
        def method4(self) -> None: ...
    else:
        @override
        def method4(self) -> None: ...

    if coinflip():
        @override
        def method5(self) -> None: ...

    if coinflip():
        @override
        def method6(self) -> None: ...
    else:
        @override
        def method6(self) -> None: ...

    if coinflip():
        @override
        def method7(self) -> None: ...  # error: [invalid-explicit-override]

    if coinflip():
        @override
        def method8(self) -> None: ...  # error: [invalid-explicit-override]
    else:
        @override
        def method8(self) -> None: ...

Multiple reachable definitions, only one of which is decorated with @override

The diagnostic should point to the first definition decorated with @override, which may not necessarily be the first definition of the symbol overall:

runtime.py:

from typing_extensions import override, overload

def coinflip() -> bool:
    return True

class Foo:
    if coinflip():
        def method(self, x): ...
    elif coinflip():
        @overload
        def method(self, x: str) -> str: ...
        @overload
        def method(self, x: int) -> int: ...
        @override
        def method(self, x: str | int) -> str | int:  # error: [invalid-explicit-override]
            return x
    elif coinflip():
        @override
        def method(self, x): ...

stub.pyi`:

from typing_extensions import override, overload

def coinflip() -> bool:
    return True

class Foo:
    if coinflip():
        def method(self, x): ...
    elif coinflip():
        @overload
        @override
        def method(self, x: str) -> str: ...  # error: [invalid-explicit-override]
        @overload
        def method(self, x: int) -> int: ...

    if coinflip():
        def method2(self, x): ...
    elif coinflip():
        @overload
        @override
        def method2(self, x: str) -> str: ...
        @overload
        def method2(self, x: int) -> int: ...
    else:
       # TODO: not sure why this is being emitted on this line rather than on
       # the first overload in the `elif` block? Ideally it would be emitted
       # on the first reachable definition, but perhaps this is due to the way
       # name lookups are deferred in stub files...? -- AW
       @override
       def method2(self, x): ...  # error: [invalid-explicit-override]

Definitions in statically known branches

[environment]
python-version = "3.10"
import sys
from typing_extensions import override, overload

class Parent:
    if sys.version_info >= (3, 10):
        def foo(self) -> None: ...
        def foooo(self) -> None: ...
    else:
        def bar(self) -> None: ...
        def baz(self) -> None: ...
        def spam(self) -> None: ...

class Child(Parent):
    @override
    def foo(self) -> None: ...

    # The declaration on `Parent` is not reachable,
    # so this is an error
    @override
    def bar(self) -> None: ...  # error: [invalid-explicit-override]

    if sys.version_info >= (3, 10):
        @override
        def foooo(self) -> None: ...
        @override
        def baz(self) -> None: ...  # error: [invalid-explicit-override]
    else:
        # This doesn't override any reachable definitions,
        # but the subclass definition also isn't a reachable definition
        # from the end of the scope with the given configuration,
        # so it's not flagged
        @override
        def foooo(self) -> None: ...
        @override
        def spam(self) -> None: ...

Overloads

The typing spec states that for an overloaded method, @override should only be applied to the implementation function. However, we nonetheless respect the decorator in this situation, even though we also emit invalid-overload on these methods.

from typing_extensions import override, overload

class Spam:
    @overload
    def foo(self, x: str) -> str: ...
    @overload
    def foo(self, x: int) -> int: ...
    @override
    def foo(self, x: str | int) -> str | int:  # error: [invalid-explicit-override]
        return x

    @overload
    @override
    def bar(self, x: str) -> str: ...
    @overload
    @override
    def bar(self, x: int) -> int: ...
    @override
    # error: [invalid-overload] "`@override` decorator should be applied only to the overload implementation"
    # error: [invalid-overload] "`@override` decorator should be applied only to the overload implementation"
    # error: [invalid-explicit-override]
    def bar(self, x: str | int) -> str | int:
        return x

    @overload
    @override
    def baz(self, x: str) -> str: ...
    @overload
    def baz(self, x: int) -> int: ...
    # error: [invalid-overload] "`@override` decorator should be applied only to the overload implementation"
    # error: [invalid-explicit-override]
    def baz(self, x: str | int) -> str | int:
        return x

In a stub file, @override should always be applied to the first overload. Even if it isn't, we always emit invalid-explicit-override diagnostics on the first overload.

module.pyi:

from typing_extensions import override, overload

class Spam:
    @overload
    def foo(self, x: str) -> str: ...  # error: [invalid-explicit-override]
    @overload
    @override
    # error: [invalid-overload]  "`@override` decorator should be applied only to the first overload"
    def foo(self, x: int) -> int: ...

    @overload
    @override
    def bar(self, x: str) -> str: ...  # error: [invalid-explicit-override]
    @overload
    @override
    # error: [invalid-overload]  "`@override` decorator should be applied only to the first overload"
    def bar(self, x: int) -> int: ...

    @overload
    @override
    def baz(self, x: str) -> str: ...  # error: [invalid-explicit-override]
    @overload
    def baz(self, x: int) -> int: ...

Overloads in statically-known branches in stub files

[environment]
python-version = "3.10"
import sys
from typing_extensions import overload, override

class Foo:
    if sys.version_info >= (3, 10):
        @overload
        @override
        def method(self, x: int) -> int: ...  # error: [invalid-explicit-override]
    else:
        @overload
        def method(self, x: int) -> int: ...
    @overload
    def method(self, x: str) -> str: ...

    if sys.version_info >= (3, 10):
        @overload
        def method2(self, x: int) -> int: ...
    else:
        @overload
        @override
        def method2(self, x: int) -> int: ...
    @overload
    def method2(self, x: str) -> str: ...

Classes inheriting from Any

from typing_extensions import Any, override
from does_not_exist import SomethingUnknown  # error: [unresolved-import]

class Parent1(Any): ...
class Parent2(SomethingUnknown): ...

class Child1(Parent1):
    @override
    def bar(self): ...  # fine

class Child2(Parent2):
    @override
    def bar(self): ...  # fine

Override of a synthesized method

from typing_extensions import NamedTuple, TypedDict, override, Any, Self
from dataclasses import dataclass

@dataclass(order=True)
class ParentDataclass:
    x: int

class Child(ParentDataclass):
    @override
    def __lt__(self, other: ParentDataclass) -> bool: ...  # fine

class MyNamedTuple(NamedTuple):
    x: int

    @override
    # error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_asdict`"
    def _asdict(self, /) -> dict[str, Any]: ...

class MyNamedTupleParent(NamedTuple):
    x: int

class MyNamedTupleChild(MyNamedTupleParent):
    @override
    def _asdict(self, /) -> dict[str, Any]: ...  # fine

class MyTypedDict(TypedDict):
    x: int

    @override
    # TODO: it's invalid to define a method on a `TypedDict` class,
    # so we should emit a diagnostic here.
    # It shouldn't be an `invalid-explicit-override` diagnostic, however.
    def copy(self) -> Self: ...

class Grandparent(Any): ...

class Parent(Grandparent, NamedTuple):  # error: [invalid-named-tuple]
    x: int

class Child(Parent):
    @override
    def foo(self): ...  # fine because `Any` is in the MRO