From a688d17c7b33ca0cc6205a87dd350bd4927db27c Mon Sep 17 00:00:00 2001 From: STerliakov Date: Fri, 7 Nov 2025 18:33:55 +0100 Subject: [PATCH 1/2] Handle decorated overrides --- mypy/checker.py | 7 ++++- test-data/unit/check-classes.test | 52 +++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 9f8299e6805d..4a40ef5f9138 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2578,7 +2578,12 @@ def erase_override(t: Type) -> Type: continue if not is_subtype(original_arg_type, erase_override(override_arg_type)): context: Context = node - if isinstance(node, FuncDef) and not node.is_property: + if ( + isinstance(node, FuncDef) + and not node.is_property + and not node.is_decorated + ): + # If there's any decorator, we can no longer map arguments 1:1 reliably. arg_node = node.arguments[i + override.bound()] if arg_node.line != -1: context = arg_node diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index c0b1114db512..0e9d6357af1a 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -585,6 +585,58 @@ class B(A): @dec def f(self) -> int: pass +[case testOverrideWithDecoratorReturningCallable] +from typing import Any, Callable, TypeVar + +class Base: + def get(self, a: str) -> None: ... + +def dec(fn: Any) -> Callable[[Any, int], None]: ... + +class Derived(Base): + @dec + def get(self) -> None: ... # E: Argument 1 of "get" is incompatible with supertype "Base"; supertype defines the argument type as "str" \ + # N: This violates the Liskov substitution principle \ + # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides +[builtins fixtures/tuple.pyi] + +[case testOverrideWithDecoratorReturningCallable2] +# flags: --pretty +from typing import Any, Callable, TypeVar + +_C = TypeVar("_C", bound=Callable[..., Any]) + +def infer_signature(f: _C) -> Callable[[Any], _C]: ... + +class Base: + def get(self, a: str, b: str, c: str) -> None: ... + def post(self, a: str, b: str) -> None: ... + +# Third argument incompatible +def get(self, a: str, b: str, c: int) -> None: ... + +# Second argument incompatible - still should not map to **kwargs +def post(self, a: str, b: int) -> None: ... + +class Derived(Base): + @infer_signature(get) + def get(self, *args: Any, **kwargs: Any) -> None: ... + + @infer_signature(post) + def post(self, *args: Any, **kwargs: Any) -> None: ... +[builtins fixtures/tuple.pyi] +[out] +main:20: error: Argument 3 of "get" is incompatible with supertype "Base"; supertype defines the argument type as "str" + def get(self, *args: Any, **kwargs: Any) -> None: ... + ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +main:20: note: This violates the Liskov substitution principle +main:20: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides +main:23: error: Argument 2 of "post" is incompatible with supertype "Base"; supertype defines the argument type as "str" + def post(self, *args: Any, **kwargs: Any) -> None: ... + ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +main:23: note: This violates the Liskov substitution principle +main:23: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides + [case testOverrideWithDecoratorReturningInstance] def dec(f) -> str: pass From 93f53dad74b00d24d243515018324fb7f10ecd5b Mon Sep 17 00:00:00 2001 From: STerliakov Date: Fri, 7 Nov 2025 18:51:28 +0100 Subject: [PATCH 2/2] Allow trivial decorators --- mypy/checker.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 4a40ef5f9138..07f5c520de95 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2581,7 +2581,13 @@ def erase_override(t: Type) -> Type: if ( isinstance(node, FuncDef) and not node.is_property - and not node.is_decorated + and ( + not node.is_decorated # fast path + # allow trivial decorators like @classmethod and @override + or not (sym := node.info.get(node.name)) + or not isinstance(sym.node, Decorator) + or not sym.node.decorators + ) ): # If there's any decorator, we can no longer map arguments 1:1 reliably. arg_node = node.arguments[i + override.bound()]