From 5e119d0062ca425d927c0f036a63d95bf0cad367 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 14 Jan 2025 23:38:47 +0100 Subject: [PATCH] Ignore dataclass.__replace__ LSP violations (#18464) Refining dataclass attributes with a narrower type has historically been accepted. Mypy shouldn't emit an LSP warning for the synthesized `__replace__` method added in Python 3.13 either. Users are instead encouraged to enable `--enable-error-code mutable-override` to highlight potential issues. Fixes #18216 --- mypy/checker.py | 15 +++++++++------ test-data/unit/check-classes.test | 14 ++++++++++++++ test-data/unit/check-dataclasses.test | 25 +++++++++++++++++++------ 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 06e31cddd068..47b08b683e36 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1980,12 +1980,15 @@ def check_method_override( Return a list of base classes which contain an attribute with the method name. """ # Check against definitions in base classes. - check_override_compatibility = defn.name not in ( - "__init__", - "__new__", - "__init_subclass__", - "__post_init__", - ) and (self.options.check_untyped_defs or not defn.is_dynamic()) + check_override_compatibility = ( + defn.name not in ("__init__", "__new__", "__init_subclass__", "__post_init__") + and (self.options.check_untyped_defs or not defn.is_dynamic()) + and ( + # don't check override for synthesized __replace__ methods from dataclasses + defn.name != "__replace__" + or defn.info.metadata.get("dataclass_tag") is None + ) + ) found_method_base_classes: list[TypeInfo] = [] for base in defn.info.mro[1:]: result = self.check_method_or_accessor_override_for_base( diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 618b2c7a40c9..d1c33c4729a9 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -687,6 +687,20 @@ class B(A): def h(cls) -> int: pass [builtins fixtures/classmethod.pyi] +[case testOverrideReplaceMethod] +# flags: --show-error-codes +from typing import Optional +from typing_extensions import Self +class A: + def __replace__(self, x: Optional[str]) -> Self: pass + +class B(A): + def __replace__(self, x: str) -> Self: pass # E: \ + # E: Argument 1 of "__replace__" is incompatible with supertype "A"; supertype defines the argument type as "Optional[str]" [override] \ + # 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 testAllowCovarianceInReadOnlyAttributes] from typing import Callable, TypeVar diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index 6de428109c72..2e7259e4de0a 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -2527,16 +2527,29 @@ Gen(2).__replace__(x="not an int") # E: Argument "x" to "__replace__" of "Gen" [builtins fixtures/tuple.pyi] [case testDunderReplaceCovariantOverride] -# flags: --python-version 3.13 +# flags: --python-version 3.13 --enable-error-code mutable-override from dataclasses import dataclass +from typing import Optional +from typing_extensions import dataclass_transform @dataclass class Base: - a: object + a: Optional[int] @dataclass -class Child(Base): # E: Argument 1 of "__replace__" is incompatible with supertype "Base"; supertype defines the argument type as "object" \ - # N: This violates the Liskov substitution principle \ - # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides - a: int +class Child(Base): + a: int # E: Covariant override of a mutable attribute (base class "Base" defined the type as "Optional[int]", expression has type "int") + +@dataclass +class Other(Base): + a: str # E: Incompatible types in assignment (expression has type "str", base class "Base" defined the type as "Optional[int]") + +@dataclass_transform(kw_only_default=True) +class DCMeta(type): ... + +class X(metaclass=DCMeta): + a: Optional[int] + +class Y(X): + a: int # E: Covariant override of a mutable attribute (base class "X" defined the type as "Optional[int]", expression has type "int") [builtins fixtures/tuple.pyi]