From a657d10655612c0298791a63bba8d7e59a1ffd91 Mon Sep 17 00:00:00 2001 From: Ilya Priven Date: Sat, 26 Aug 2023 01:51:08 -0400 Subject: [PATCH 1/3] dataclasses.replace: fall through to typeshed sig --- mypy/plugins/dataclasses.py | 25 +------------- test-data/unit/check-dataclasses.test | 44 ++++++++++++++++++++----- test-data/unit/lib-stub/_typeshed.pyi | 6 +++- test-data/unit/lib-stub/dataclasses.pyi | 15 +++++++-- 4 files changed, 54 insertions(+), 36 deletions(-) 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: ... From 0b95f3eaad6857d3b001d0278c8f2b40c16e11aa Mon Sep 17 00:00:00 2001 From: Ilya Priven Date: Sat, 26 Aug 2023 23:00:09 -0400 Subject: [PATCH 2/3] move DataclassInstance to dataclasses.pyi --- test-data/unit/check-dataclass-transform.test | 2 +- test-data/unit/lib-stub/_typeshed.pyi | 6 +----- test-data/unit/lib-stub/dataclasses.pyi | 9 +++++++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/test-data/unit/check-dataclass-transform.test b/test-data/unit/check-dataclass-transform.test index 9029582ece82..58cd5e5a90f8 100644 --- a/test-data/unit/check-dataclass-transform.test +++ b/test-data/unit/check-dataclass-transform.test @@ -853,7 +853,7 @@ class Person: name: str p = Person('John') -y = replace(p, name='Bob') # E: Argument 1 to "replace" has incompatible type "Person"; expected a dataclass +y = replace(p, name='Bob') [typing fixtures/typing-full.pyi] [builtins fixtures/dataclasses.pyi] diff --git a/test-data/unit/lib-stub/_typeshed.pyi b/test-data/unit/lib-stub/_typeshed.pyi index 7ae427b3c7a6..054ad0ec0c46 100644 --- a/test-data/unit/lib-stub/_typeshed.pyi +++ b/test-data/unit/lib-stub/_typeshed.pyi @@ -1,5 +1,4 @@ -from dataclasses import Field -from typing import Any, ClassVar, Protocol, TypeVar, Iterable +from typing import Protocol, TypeVar, Iterable _KT = TypeVar("_KT") _VT_co = TypeVar("_VT_co", covariant=True) @@ -7,6 +6,3 @@ _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 2d0910e52339..cf43747757bd 100644 --- a/test-data/unit/lib-stub/dataclasses.pyi +++ b/test-data/unit/lib-stub/dataclasses.pyi @@ -1,7 +1,12 @@ -from _typeshed import DataclassInstance -from typing import Any, Callable, Generic, Literal, Mapping, Optional, TypeVar, overload, Type +from typing import Any, Callable, Generic, Literal, Mapping, Optional, TypeVar, overload, Type, \ + Protocol, ClassVar from typing_extensions import TypeGuard +# DataclassInstance is in _typeshed.pyi normally, but alas we can't do the same for lib-stub +# due to test-data/unit/lib-stub/builtins.pyi not having 'tuple'. +class DataclassInstance(Protocol): + __dataclass_fields__: ClassVar[dict[str, Field[Any]]] + _T = TypeVar('_T') _DataclassT = TypeVar("_DataclassT", bound=DataclassInstance) From 1a538d4306ee6b5d1903720a99c18edcf4326b03 Mon Sep 17 00:00:00 2001 From: Ilya Priven Date: Sun, 27 Aug 2023 22:23:30 -0400 Subject: [PATCH 3/3] clarify testReplaceIsDataclass --- test-data/unit/check-dataclasses.test | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index a80d0ae1936e..4eb109a3dc2e 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -2237,11 +2237,11 @@ replace(None) # E: Value of type variable "_DataclassT" of "replace" cannot be 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) + _ = replace(x) # E: Value of type variable "_DataclassT" of "replace" cannot be "object" + if is_dataclass(x): + _ = replace(x) # E: Value of type variable "_DataclassT" of "replace" cannot be "Union[DataclassInstance, Type[DataclassInstance]]" + if not isinstance(x, type): + _ = replace(x) [builtins fixtures/tuple.pyi]