diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index d782acf50af5..16240666d5cd 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -966,25 +966,6 @@ def _has_direct_dataclass_transform_metaclass(info: TypeInfo) -> bool: ) -def _fail_not_dataclass(ctx: FunctionSigContext, t: Type, parent_t: Type) -> None: - t_name = format_type_bare(t, ctx.api.options) - if parent_t is t: - msg = ( - f'Argument 1 to "replace" has a variable type "{t_name}" not bound to a dataclass' - if isinstance(t, TypeVarType) - else f'Argument 1 to "replace" has incompatible type "{t_name}"; expected a dataclass' - ) - else: - pt_name = format_type_bare(parent_t, ctx.api.options) - msg = ( - f'Argument 1 to "replace" has type "{pt_name}" whose item "{t_name}" is not bound to a dataclass' - if isinstance(t, TypeVarType) - else f'Argument 1 to "replace" has incompatible type "{pt_name}" whose item "{t_name}" is not a dataclass' - ) - - ctx.api.fail(msg, ctx.context) - - def _get_expanded_dataclasses_fields( ctx: FunctionSigContext, typ: ProperType, display_typ: ProperType, parent_typ: ProperType ) -> list[CallableType] | None: @@ -993,9 +974,7 @@ def _get_expanded_dataclasses_fields( For generic classes, the field types are expanded. If the type contains Any or a non-dataclass, returns None; in the latter case, also reports an error. """ - if isinstance(typ, AnyType): - return None - elif isinstance(typ, UnionType): + if isinstance(typ, UnionType): ret: list[CallableType] | None = [] for item in typ.relevant_items(): item = get_proper_type(item) @@ -1012,14 +991,12 @@ def _get_expanded_dataclasses_fields( elif isinstance(typ, Instance): replace_sym = typ.type.get_method(_INTERNAL_REPLACE_SYM_NAME) if replace_sym is None: - _fail_not_dataclass(ctx, display_typ, parent_typ) return None replace_sig = replace_sym.type assert isinstance(replace_sig, ProperType) assert isinstance(replace_sig, CallableType) return [expand_type_by_instance(replace_sig, typ)] else: - _fail_not_dataclass(ctx, display_typ, parent_typ) return None diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index 7881dfbcf1bb..a80d0ae1936e 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -2106,6 +2106,8 @@ a2 = replace(a, x='42', q=42) # E: Argument "x" to "replace" of "A" has incompa a2 = replace(a, q='42') # E: Argument "q" to "replace" of "A" has incompatible type "str"; expected "int" reveal_type(a2) # N: Revealed type is "__main__.A" +[builtins fixtures/tuple.pyi] + [case testReplaceUnion] from typing import Generic, Union, TypeVar from dataclasses import dataclass, replace, InitVar @@ -2135,7 +2137,7 @@ _ = replace(a_or_b, x=42, y=True, z='42', init_var=42) # E: Argument "z" to "re _ = replace(a_or_b, x=42, y=True, w={}, init_var=42) # E: Argument "w" to "replace" of "Union[A[int], B]" has incompatible type "Dict[, ]"; expected _ = replace(a_or_b, y=42, init_var=42) # E: Argument "y" to "replace" of "Union[A[int], B]" has incompatible type "int"; expected "bool" -[builtins fixtures/dataclasses.pyi] +[builtins fixtures/tuple.pyi] [case testReplaceUnionOfTypeVar] from typing import Generic, Union, TypeVar @@ -2155,7 +2157,9 @@ TA = TypeVar('TA', bound=A) TB = TypeVar('TB', bound=B) def f(b_or_t: Union[TA, TB, int]) -> None: - a2 = replace(b_or_t) # E: Argument 1 to "replace" has type "Union[TA, TB, int]" whose item "TB" is not bound to a dataclass # E: Argument 1 to "replace" has incompatible type "Union[TA, TB, int]" whose item "int" is not a dataclass + a2 = replace(b_or_t) # E: Value of type variable "_DataclassT" of "replace" cannot be "Union[TA, TB, int]" + +[builtins fixtures/tuple.pyi] [case testReplaceTypeVarBoundNotDataclass] from dataclasses import dataclass, replace @@ -2167,16 +2171,18 @@ TNone = TypeVar('TNone', bound=None) TUnion = TypeVar('TUnion', bound=Union[str, int]) def f1(t: TInt) -> None: - _ = replace(t, x=42) # E: Argument 1 to "replace" has a variable type "TInt" not bound to a dataclass + _ = replace(t, x=42) # E: Value of type variable "_DataclassT" of "replace" cannot be "TInt" def f2(t: TAny) -> TAny: - return replace(t, x='spam') # E: Argument 1 to "replace" has a variable type "TAny" not bound to a dataclass + return replace(t, x='spam') # E: Value of type variable "_DataclassT" of "replace" cannot be "TAny" def f3(t: TNone) -> TNone: - return replace(t, x='spam') # E: Argument 1 to "replace" has a variable type "TNone" not bound to a dataclass + return replace(t, x='spam') # E: Value of type variable "_DataclassT" of "replace" cannot be "TNone" def f4(t: TUnion) -> TUnion: - return replace(t, x='spam') # E: Argument 1 to "replace" has incompatible type "TUnion" whose item "str" is not a dataclass # E: Argument 1 to "replace" has incompatible type "TUnion" whose item "int" is not a dataclass + return replace(t, x='spam') # E: Value of type variable "_DataclassT" of "replace" cannot be "TUnion" + +[builtins fixtures/tuple.pyi] [case testReplaceTypeVarBound] from dataclasses import dataclass, replace @@ -2201,6 +2207,8 @@ def f(t: TA) -> TA: f(A(x=42)) f(B(x=42)) +[builtins fixtures/tuple.pyi] + [case testReplaceAny] from dataclasses import replace from typing import Any @@ -2209,17 +2217,33 @@ a: Any a2 = replace(a) reveal_type(a2) # N: Revealed type is "Any" +[builtins fixtures/tuple.pyi] + [case testReplaceNotDataclass] from dataclasses import replace -replace(5) # E: Argument 1 to "replace" has incompatible type "int"; expected a dataclass +replace(5) # E: Value of type variable "_DataclassT" of "replace" cannot be "int" class C: pass -replace(C()) # E: Argument 1 to "replace" has incompatible type "C"; expected a dataclass +replace(C()) # E: Value of type variable "_DataclassT" of "replace" cannot be "C" -replace(None) # E: Argument 1 to "replace" has incompatible type "None"; expected a dataclass +replace(None) # E: Value of type variable "_DataclassT" of "replace" cannot be "None" + +[builtins fixtures/tuple.pyi] + +[case testReplaceIsDataclass] +from dataclasses import is_dataclass, replace + +def f(x: object) -> None: + # error before type-guard + y = replace(x) # E: Value of type variable "_DataclassT" of "replace" cannot be "object" + # no error after type-guard + if is_dataclass(x) and not isinstance(x, type): + y = replace(x) + +[builtins fixtures/tuple.pyi] [case testReplaceGeneric] from dataclasses import dataclass, replace, InitVar @@ -2238,6 +2262,8 @@ reveal_type(a2) # N: Revealed type is "__main__.A[builtins.int]" a2 = replace(a, x='42') # E: Argument "x" to "replace" of "A[int]" has incompatible type "str"; expected "int" reveal_type(a2) # N: Revealed type is "__main__.A[builtins.int]" +[builtins fixtures/tuple.pyi] + [case testPostInitCorrectSignature] from typing import Any, Generic, TypeVar, Callable, Self from dataclasses import dataclass, InitVar diff --git a/test-data/unit/lib-stub/_typeshed.pyi b/test-data/unit/lib-stub/_typeshed.pyi index 054ad0ec0c46..7ae427b3c7a6 100644 --- a/test-data/unit/lib-stub/_typeshed.pyi +++ b/test-data/unit/lib-stub/_typeshed.pyi @@ -1,4 +1,5 @@ -from typing import Protocol, TypeVar, Iterable +from dataclasses import Field +from typing import Any, ClassVar, Protocol, TypeVar, Iterable _KT = TypeVar("_KT") _VT_co = TypeVar("_VT_co", covariant=True) @@ -6,3 +7,6 @@ _VT_co = TypeVar("_VT_co", covariant=True) class SupportsKeysAndGetItem(Protocol[_KT, _VT_co]): def keys(self) -> Iterable[_KT]: pass def __getitem__(self, __key: _KT) -> _VT_co: pass + +class DataclassInstance(Protocol): + __dataclass_fields__: ClassVar[dict[str, Field[Any]]] diff --git a/test-data/unit/lib-stub/dataclasses.pyi b/test-data/unit/lib-stub/dataclasses.pyi index b2b48c2ae486..2d0910e52339 100644 --- a/test-data/unit/lib-stub/dataclasses.pyi +++ b/test-data/unit/lib-stub/dataclasses.pyi @@ -1,6 +1,9 @@ -from typing import Any, Callable, Generic, Mapping, Optional, TypeVar, overload, Type +from _typeshed import DataclassInstance +from typing import Any, Callable, Generic, Literal, Mapping, Optional, TypeVar, overload, Type +from typing_extensions import TypeGuard _T = TypeVar('_T') +_DataclassT = TypeVar("_DataclassT", bound=DataclassInstance) class InitVar(Generic[_T]): ... @@ -33,4 +36,12 @@ def field(*, class Field(Generic[_T]): pass -def replace(__obj: _T, **changes: Any) -> _T: ... +@overload +def is_dataclass(obj: DataclassInstance) -> Literal[True]: ... +@overload +def is_dataclass(obj: type) -> TypeGuard[type[DataclassInstance]]: ... +@overload +def is_dataclass(obj: object) -> TypeGuard[DataclassInstance | type[DataclassInstance]]: ... + + +def replace(__obj: _DataclassT, **changes: Any) -> _DataclassT: ...