Skip to content

Commit d6df8e8

Browse files
authored
dataclasses.replace: fall through to typeshed sig (#15962)
If the dataclasses plugin cannot determine a signature for `dataclasses.replace`, it should not report an error. The underlying typeshed signature will get a shot at verifying the type and reporting an error, and it would enable the following pattern (without typing `replace`'s kwargs, though)
1 parent 6c16143 commit d6df8e8

File tree

4 files changed

+55
-36
lines changed

4 files changed

+55
-36
lines changed

mypy/plugins/dataclasses.py

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

974974

975-
def _fail_not_dataclass(ctx: FunctionSigContext, t: Type, parent_t: Type) -> None:
976-
t_name = format_type_bare(t, ctx.api.options)
977-
if parent_t is t:
978-
msg = (
979-
f'Argument 1 to "replace" has a variable type "{t_name}" not bound to a dataclass'
980-
if isinstance(t, TypeVarType)
981-
else f'Argument 1 to "replace" has incompatible type "{t_name}"; expected a dataclass'
982-
)
983-
else:
984-
pt_name = format_type_bare(parent_t, ctx.api.options)
985-
msg = (
986-
f'Argument 1 to "replace" has type "{pt_name}" whose item "{t_name}" is not bound to a dataclass'
987-
if isinstance(t, TypeVarType)
988-
else f'Argument 1 to "replace" has incompatible type "{pt_name}" whose item "{t_name}" is not a dataclass'
989-
)
990-
991-
ctx.api.fail(msg, ctx.context)
992-
993-
994975
def _get_expanded_dataclasses_fields(
995976
ctx: FunctionSigContext, typ: ProperType, display_typ: ProperType, parent_typ: ProperType
996977
) -> list[CallableType] | None:
@@ -999,9 +980,7 @@ def _get_expanded_dataclasses_fields(
999980
For generic classes, the field types are expanded.
1000981
If the type contains Any or a non-dataclass, returns None; in the latter case, also reports an error.
1001982
"""
1002-
if isinstance(typ, AnyType):
1003-
return None
1004-
elif isinstance(typ, UnionType):
983+
if isinstance(typ, UnionType):
1005984
ret: list[CallableType] | None = []
1006985
for item in typ.relevant_items():
1007986
item = get_proper_type(item)
@@ -1018,14 +997,12 @@ def _get_expanded_dataclasses_fields(
1018997
elif isinstance(typ, Instance):
1019998
replace_sym = typ.type.get_method(_INTERNAL_REPLACE_SYM_NAME)
1020999
if replace_sym is None:
1021-
_fail_not_dataclass(ctx, display_typ, parent_typ)
10221000
return None
10231001
replace_sig = replace_sym.type
10241002
assert isinstance(replace_sig, ProperType)
10251003
assert isinstance(replace_sig, CallableType)
10261004
return [expand_type_by_instance(replace_sig, typ)]
10271005
else:
1028-
_fail_not_dataclass(ctx, display_typ, parent_typ)
10291006
return None
10301007

10311008

test-data/unit/check-dataclass-transform.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -853,7 +853,7 @@ class Person:
853853
name: str
854854

855855
p = Person('John')
856-
y = replace(p, name='Bob') # E: Argument 1 to "replace" has incompatible type "Person"; expected a dataclass
856+
y = replace(p, name='Bob')
857857

858858
[typing fixtures/typing-full.pyi]
859859
[builtins fixtures/dataclasses.pyi]

test-data/unit/check-dataclasses.test

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

2125+
[builtins fixtures/tuple.pyi]
2126+
21252127
[case testReplaceUnion]
21262128
from typing import Generic, Union, TypeVar
21272129
from dataclasses import dataclass, replace, InitVar
@@ -2151,7 +2153,7 @@ _ = replace(a_or_b, x=42, y=True, z='42', init_var=42) # E: Argument "z" to "re
21512153
_ = 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>
21522154
_ = 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"
21532155

2154-
[builtins fixtures/dataclasses.pyi]
2156+
[builtins fixtures/tuple.pyi]
21552157

21562158
[case testReplaceUnionOfTypeVar]
21572159
from typing import Generic, Union, TypeVar
@@ -2171,7 +2173,9 @@ TA = TypeVar('TA', bound=A)
21712173
TB = TypeVar('TB', bound=B)
21722174

21732175
def f(b_or_t: Union[TA, TB, int]) -> None:
2174-
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
2176+
a2 = replace(b_or_t) # E: Value of type variable "_DataclassT" of "replace" cannot be "Union[TA, TB, int]"
2177+
2178+
[builtins fixtures/tuple.pyi]
21752179

21762180
[case testReplaceTypeVarBoundNotDataclass]
21772181
from dataclasses import dataclass, replace
@@ -2183,16 +2187,18 @@ TNone = TypeVar('TNone', bound=None)
21832187
TUnion = TypeVar('TUnion', bound=Union[str, int])
21842188

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

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

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

21942198
def f4(t: TUnion) -> TUnion:
2195-
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
2199+
return replace(t, x='spam') # E: Value of type variable "_DataclassT" of "replace" cannot be "TUnion"
2200+
2201+
[builtins fixtures/tuple.pyi]
21962202

21972203
[case testReplaceTypeVarBound]
21982204
from dataclasses import dataclass, replace
@@ -2217,6 +2223,8 @@ def f(t: TA) -> TA:
22172223
f(A(x=42))
22182224
f(B(x=42))
22192225

2226+
[builtins fixtures/tuple.pyi]
2227+
22202228
[case testReplaceAny]
22212229
from dataclasses import replace
22222230
from typing import Any
@@ -2225,17 +2233,33 @@ a: Any
22252233
a2 = replace(a)
22262234
reveal_type(a2) # N: Revealed type is "Any"
22272235

2236+
[builtins fixtures/tuple.pyi]
2237+
22282238
[case testReplaceNotDataclass]
22292239
from dataclasses import replace
22302240

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

22332243
class C:
22342244
pass
22352245

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

2238-
replace(None) # E: Argument 1 to "replace" has incompatible type "None"; expected a dataclass
2248+
replace(None) # E: Value of type variable "_DataclassT" of "replace" cannot be "None"
2249+
2250+
[builtins fixtures/tuple.pyi]
2251+
2252+
[case testReplaceIsDataclass]
2253+
from dataclasses import is_dataclass, replace
2254+
2255+
def f(x: object) -> None:
2256+
_ = replace(x) # E: Value of type variable "_DataclassT" of "replace" cannot be "object"
2257+
if is_dataclass(x):
2258+
_ = replace(x) # E: Value of type variable "_DataclassT" of "replace" cannot be "Union[DataclassInstance, Type[DataclassInstance]]"
2259+
if not isinstance(x, type):
2260+
_ = replace(x)
2261+
2262+
[builtins fixtures/tuple.pyi]
22392263

22402264
[case testReplaceGeneric]
22412265
from dataclasses import dataclass, replace, InitVar
@@ -2254,6 +2278,8 @@ reveal_type(a2) # N: Revealed type is "__main__.A[builtins.int]"
22542278
a2 = replace(a, x='42') # E: Argument "x" to "replace" of "A[int]" has incompatible type "str"; expected "int"
22552279
reveal_type(a2) # N: Revealed type is "__main__.A[builtins.int]"
22562280

2281+
[builtins fixtures/tuple.pyi]
2282+
22572283
[case testPostInitCorrectSignature]
22582284
from typing import Any, Generic, TypeVar, Callable, Self
22592285
from dataclasses import dataclass, InitVar

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1-
from typing import Any, Callable, Generic, Mapping, Optional, TypeVar, overload, Type
1+
from typing import Any, Callable, Generic, Literal, Mapping, Optional, TypeVar, overload, Type, \
2+
Protocol, ClassVar
3+
from typing_extensions import TypeGuard
4+
5+
# DataclassInstance is in _typeshed.pyi normally, but alas we can't do the same for lib-stub
6+
# due to test-data/unit/lib-stub/builtins.pyi not having 'tuple'.
7+
class DataclassInstance(Protocol):
8+
__dataclass_fields__: ClassVar[dict[str, Field[Any]]]
29

310
_T = TypeVar('_T')
11+
_DataclassT = TypeVar("_DataclassT", bound=DataclassInstance)
412

513
class InitVar(Generic[_T]):
614
...
@@ -33,4 +41,12 @@ def field(*,
3341

3442
class Field(Generic[_T]): pass
3543

36-
def replace(__obj: _T, **changes: Any) -> _T: ...
44+
@overload
45+
def is_dataclass(obj: DataclassInstance) -> Literal[True]: ...
46+
@overload
47+
def is_dataclass(obj: type) -> TypeGuard[type[DataclassInstance]]: ...
48+
@overload
49+
def is_dataclass(obj: object) -> TypeGuard[DataclassInstance | type[DataclassInstance]]: ...
50+
51+
52+
def replace(__obj: _DataclassT, **changes: Any) -> _DataclassT: ...

0 commit comments

Comments
 (0)