Skip to content

Commit a2f3780

Browse files
committed
dataclasses.replace: fall through to typeshed sig
1 parent 7f65cc7 commit a2f3780

File tree

4 files changed

+51
-36
lines changed

4 files changed

+51
-36
lines changed

mypy/plugins/dataclasses.py

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -966,25 +966,6 @@ def _has_direct_dataclass_transform_metaclass(info: TypeInfo) -> bool:
966966
)
967967

968968

969-
def _fail_not_dataclass(ctx: FunctionSigContext, t: Type, parent_t: Type) -> None:
970-
t_name = format_type_bare(t, ctx.api.options)
971-
if parent_t is t:
972-
msg = (
973-
f'Argument 1 to "replace" has a variable type "{t_name}" not bound to a dataclass'
974-
if isinstance(t, TypeVarType)
975-
else f'Argument 1 to "replace" has incompatible type "{t_name}"; expected a dataclass'
976-
)
977-
else:
978-
pt_name = format_type_bare(parent_t, ctx.api.options)
979-
msg = (
980-
f'Argument 1 to "replace" has type "{pt_name}" whose item "{t_name}" is not bound to a dataclass'
981-
if isinstance(t, TypeVarType)
982-
else f'Argument 1 to "replace" has incompatible type "{pt_name}" whose item "{t_name}" is not a dataclass'
983-
)
984-
985-
ctx.api.fail(msg, ctx.context)
986-
987-
988969
def _get_expanded_dataclasses_fields(
989970
ctx: FunctionSigContext, typ: ProperType, display_typ: ProperType, parent_typ: ProperType
990971
) -> list[CallableType] | None:
@@ -993,9 +974,7 @@ def _get_expanded_dataclasses_fields(
993974
For generic classes, the field types are expanded.
994975
If the type contains Any or a non-dataclass, returns None; in the latter case, also reports an error.
995976
"""
996-
if isinstance(typ, AnyType):
997-
return None
998-
elif isinstance(typ, UnionType):
977+
if isinstance(typ, UnionType):
999978
ret: list[CallableType] | None = []
1000979
for item in typ.relevant_items():
1001980
item = get_proper_type(item)
@@ -1012,14 +991,12 @@ def _get_expanded_dataclasses_fields(
1012991
elif isinstance(typ, Instance):
1013992
replace_sym = typ.type.get_method(_INTERNAL_REPLACE_SYM_NAME)
1014993
if replace_sym is None:
1015-
_fail_not_dataclass(ctx, display_typ, parent_typ)
1016994
return None
1017995
replace_sig = replace_sym.type
1018996
assert isinstance(replace_sig, ProperType)
1019997
assert isinstance(replace_sig, CallableType)
1020998
return [expand_type_by_instance(replace_sig, typ)]
1021999
else:
1022-
_fail_not_dataclass(ctx, display_typ, parent_typ)
10231000
return None
10241001

10251002

test-data/unit/check-dataclasses.test

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2106,6 +2106,8 @@ a2 = replace(a, x='42', q=42) # E: Argument "x" to "replace" of "A" has incompa
21062106
a2 = replace(a, q='42') # E: Argument "q" to "replace" of "A" has incompatible type "str"; expected "int"
21072107
reveal_type(a2) # N: Revealed type is "__main__.A"
21082108

2109+
[builtins fixtures/tuple.pyi]
2110+
21092111
[case testReplaceUnion]
21102112
from typing import Generic, Union, TypeVar
21112113
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
21352137
_ = 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[<nothing>, <nothing>]"; expected <nothing>
21362138
_ = 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"
21372139

2138-
[builtins fixtures/dataclasses.pyi]
2140+
[builtins fixtures/tuple.pyi]
21392141

21402142
[case testReplaceUnionOfTypeVar]
21412143
from typing import Generic, Union, TypeVar
@@ -2155,7 +2157,9 @@ TA = TypeVar('TA', bound=A)
21552157
TB = TypeVar('TB', bound=B)
21562158

21572159
def f(b_or_t: Union[TA, TB, int]) -> None:
2158-
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
2160+
a2 = replace(b_or_t) # E: Value of type variable "_DataclassT" of "replace" cannot be "Union[TA, TB, int]"
2161+
2162+
[builtins fixtures/tuple.pyi]
21592163

21602164
[case testReplaceTypeVarBoundNotDataclass]
21612165
from dataclasses import dataclass, replace
@@ -2167,16 +2171,18 @@ TNone = TypeVar('TNone', bound=None)
21672171
TUnion = TypeVar('TUnion', bound=Union[str, int])
21682172

21692173
def f1(t: TInt) -> None:
2170-
_ = replace(t, x=42) # E: Argument 1 to "replace" has a variable type "TInt" not bound to a dataclass
2174+
_ = replace(t, x=42) # E: Value of type variable "_DataclassT" of "replace" cannot be "TInt"
21712175

21722176
def f2(t: TAny) -> TAny:
2173-
return replace(t, x='spam') # E: Argument 1 to "replace" has a variable type "TAny" not bound to a dataclass
2177+
return replace(t, x='spam') # E: Value of type variable "_DataclassT" of "replace" cannot be "TAny"
21742178

21752179
def f3(t: TNone) -> TNone:
2176-
return replace(t, x='spam') # E: Argument 1 to "replace" has a variable type "TNone" not bound to a dataclass
2180+
return replace(t, x='spam') # E: Value of type variable "_DataclassT" of "replace" cannot be "TNone"
21772181

21782182
def f4(t: TUnion) -> TUnion:
2179-
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
2183+
return replace(t, x='spam') # E: Value of type variable "_DataclassT" of "replace" cannot be "TUnion"
2184+
2185+
[builtins fixtures/tuple.pyi]
21802186

21812187
[case testReplaceTypeVarBound]
21822188
from dataclasses import dataclass, replace
@@ -2201,6 +2207,8 @@ def f(t: TA) -> TA:
22012207
f(A(x=42))
22022208
f(B(x=42))
22032209

2210+
[builtins fixtures/tuple.pyi]
2211+
22042212
[case testReplaceAny]
22052213
from dataclasses import replace
22062214
from typing import Any
@@ -2209,17 +2217,30 @@ a: Any
22092217
a2 = replace(a)
22102218
reveal_type(a2) # N: Revealed type is "Any"
22112219

2220+
[builtins fixtures/tuple.pyi]
2221+
22122222
[case testReplaceNotDataclass]
22132223
from dataclasses import replace
22142224

2215-
replace(5) # E: Argument 1 to "replace" has incompatible type "int"; expected a dataclass
2225+
replace(5) # E: Value of type variable "_DataclassT" of "replace" cannot be "int"
22162226

22172227
class C:
22182228
pass
22192229

2220-
replace(C()) # E: Argument 1 to "replace" has incompatible type "C"; expected a dataclass
2230+
replace(C()) # E: Value of type variable "_DataclassT" of "replace" cannot be "C"
22212231

2222-
replace(None) # E: Argument 1 to "replace" has incompatible type "None"; expected a dataclass
2232+
replace(None) # E: Value of type variable "_DataclassT" of "replace" cannot be "None"
2233+
2234+
[builtins fixtures/tuple.pyi]
2235+
2236+
[case testReplaceIsDataclass]
2237+
from dataclasses import is_dataclass, replace
2238+
2239+
def f(x: object) -> None:
2240+
if is_dataclass(x) and not isinstance(x, type):
2241+
y = replace(x)
2242+
2243+
[builtins fixtures/tuple.pyi]
22232244

22242245
[case testReplaceGeneric]
22252246
from dataclasses import dataclass, replace, InitVar
@@ -2238,6 +2259,8 @@ reveal_type(a2) # N: Revealed type is "__main__.A[builtins.int]"
22382259
a2 = replace(a, x='42') # E: Argument "x" to "replace" of "A[int]" has incompatible type "str"; expected "int"
22392260
reveal_type(a2) # N: Revealed type is "__main__.A[builtins.int]"
22402261

2262+
[builtins fixtures/tuple.pyi]
2263+
22412264
[case testPostInitCorrectSignature]
22422265
from typing import Any, Generic, TypeVar, Callable, Self
22432266
from dataclasses import dataclass, InitVar

test-data/unit/lib-stub/_typeshed.pyi

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
from typing import Protocol, TypeVar, Iterable
1+
from dataclasses import Field
2+
from typing import Any, ClassVar, Protocol, TypeVar, Iterable
23

34
_KT = TypeVar("_KT")
45
_VT_co = TypeVar("_VT_co", covariant=True)
56

67
class SupportsKeysAndGetItem(Protocol[_KT, _VT_co]):
78
def keys(self) -> Iterable[_KT]: pass
89
def __getitem__(self, __key: _KT) -> _VT_co: pass
10+
11+
class DataclassInstance(Protocol):
12+
__dataclass_fields__: ClassVar[dict[str, Field[Any]]]

test-data/unit/lib-stub/dataclasses.pyi

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
from typing import Any, Callable, Generic, Mapping, Optional, TypeVar, overload, Type
1+
from _typeshed import DataclassInstance
2+
from typing import Any, Callable, Generic, Literal, Mapping, Optional, TypeVar, overload, Type
3+
from typing_extensions import TypeGuard
24

35
_T = TypeVar('_T')
6+
_DataclassT = TypeVar("_DataclassT", bound=DataclassInstance)
47

58
class InitVar(Generic[_T]):
69
...
@@ -33,4 +36,12 @@ def field(*,
3336

3437
class Field(Generic[_T]): pass
3538

36-
def replace(__obj: _T, **changes: Any) -> _T: ...
39+
@overload
40+
def is_dataclass(obj: DataclassInstance) -> Literal[True]: ...
41+
@overload
42+
def is_dataclass(obj: type) -> TypeGuard[type[DataclassInstance]]: ...
43+
@overload
44+
def is_dataclass(obj: object) -> TypeGuard[DataclassInstance | type[DataclassInstance]]: ...
45+
46+
47+
def replace(__obj: _DataclassT, **changes: Any) -> _DataclassT: ...

0 commit comments

Comments
 (0)