Repository: python/mypy

Issue:
Custom dataclasses are not recognized as dataclasses
Feel free to skip to the bottom of the code.   The reset is custom dataclass specification.
```python
import dataclasses
from dataclasses import replace
from typing import (Any, Callable, ClassVar, Protocol, dataclass_transform, overload,
                    runtime_checkable)


@runtime_checkable
class DataclassInstance(Protocol):
    __dataclass_fields__: ClassVar[dict[str, dataclasses.Field[Any]]]


@overload
@dataclass_transform()
def dataclass(*, init: bool = True, repr_: bool = True, eq: bool = True,
              order: bool = False) -> Callable[[type[Any]], type[DataclassInstance]]:
    ...


@overload
@dataclass_transform()
def dataclass(cls: type[Any], /, *, init: bool = True, repr_: bool = True, eq: bool = True,
              order: bool = False) -> type[DataclassInstance]:
    ...


@dataclass_transform()
def dataclass(cls: type[Any] | None = None, /, *, init: bool = True, repr_: bool = True,
              eq: bool = True, order: bool = False
              ) -> type[DataclassInstance] | Callable[[type[Any]], type[DataclassInstance]]:
    if cls is None:
        def f(x: type[Any], /) -> type[DataclassInstance]:
            return dataclass(x, init=init, repr_=repr_, eq=eq, order=order)
        return f  # Type checking support partial is poor.
    return dataclasses.dataclass(init=init, repr=repr_, eq=eq, order=order, frozen=True)(cls)

@dataclass
class X:
    a: int

def f(x: X) -> X:
    return replace(x, a=1)  # [Error](error: Argument 1 to "replace" has incompatible type "X"; expected a dataclass
```

It appears that MyPy recognizes that something is a dataclass through some means other than the protocol.  See here for background: https://github.com/python/cpython/issues/102699


Discussion:
> It appears that MyPy recognizes that something is a dataclass through some means other than the protocol.

Yes, following https://github.com/python/mypy/pull/14849, mypy now infers a much more precise signature for `dataclasses.replace()` if it sees it being called on an instance of a class that it knows to have been constructed using `@dataclass`. But it looks like an unfortunate side effect is that it now _only_ accepts `@dataclass` instances being passed to `dataclasses.replace()`, whereas the runtime function `dataclasses.replace()` accepts anything with a `__dataclass_fields__` dictionary `ClassVar`.

@ikonst, would it be possible to just fallback to the signature typeshed gives if mypy sees an instance of a class that hasn't been constructed with `@dataclass` being passed to `dataclasses.replace()`? This issue is also causing one of typeshed's tests to fail: see the commented-out test in https://github.com/python/typeshed/pull/10559.

(It's not a huge issue for typeshed, though, we can just comment out the test for now and that's not the end of the world.)
(Although, It would be cool to have the typed replace function for custom dataclasses 😄)
> (Although, It would be cool to have the typed replace function for custom dataclasses 😄)

Well sure, but I'm guessing that would be significantly more complex :)
Yup, fair enough.  Thanks for taking a look at this so quickly.
BTW, we also have a false positive problem, assuming every `dataclass_transform`'d class has `__dataclass_fields__`. For example:
```python
import typing

_T = typing.TypeVar("_T")


@typing.dataclass_transform()
def create_model(cls: type[_T]) -> type[_T]:
    cls.__init__ = lambda *args, **kwargs: None  # type: ignore
    return cls


@create_model
class C:
    a: int


c = C(a=42)
typing.reveal_type(c.__dataclass_fields__)
```

Runtime:
```
    typing.reveal_type(c.__dataclass_fields__)
                       ^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'C' object has no attribute '__dataclass_fields__'
```

mypy:
```
file.py:18: note: Revealed type is "builtins.dict[builtins.str, Any]"
Success: no issues found in 1 source file
```

Resolving patch:
diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py
--- a/mypy/plugins/dataclasses.py
+++ b/mypy/plugins/dataclasses.py
@@ -375,9 +375,7 @@ def transform(self) -> bool:
             add_attribute_to_class(self._api, self._cls, "__match_args__", match_args_type)
 
         self._add_dataclass_fields_magic_attribute()
-
-        if self._spec is _TRANSFORM_SPEC_FOR_DATACLASSES:
-            self._add_internal_replace_method(attributes)
+        self._add_internal_replace_method(attributes)
         if "__post_init__" in info.names:
             self._add_internal_post_init_method(attributes)
 


Test patch: