8000 Add error for asdict, astuple, fields, and replace in dataclasses - Fixes #14215 by tdscheper · Pull Request #14263 · python/mypy · GitHub
[go: up one dir, main page]

Skip to content

Add error for asdict, astuple, fields, and replace in dataclasses - Fixes #14215 #14263

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion mypy/plugins/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
TypeVarExpr,
Var,
)
from mypy.plugin import ClassDefContext, SemanticAnalyzerPluginInterface
from mypy.plugin import ClassDefContext, FunctionContext, SemanticAnalyzerPluginInterface
from mypy.plugins.common import (
_get_decorator_bool_argument,
add_attribute_to_class,
Expand Down Expand Up @@ -631,6 +631,39 @@ def dataclass_class_maker_callback(ctx: ClassDefContext) -> bool:
return transformer.transform()


def _dataclass_exclusive_function_callback(ctx: FunctionContext, func 8000 _name: str) -> Type:
"""Called for functions that should only be called on dataclasses.

Functions are (from dataclasses module): asdict, astuple, fields, replace
"""
# Each of asdict, astuple, fields, and replace require the first argument
# to be a dataclass
arg_type = get_proper_type(ctx.arg_types[0][0])
if isinstance(arg_type, Instance) and "dataclass" not in arg_type.type.metadata:
ctx.api.msg.fail(f"{func_name}() should be called on dataclass instances", ctx.context)
return ctx.default_return_type


def asdict_callback(ctx: FunctionContext) -> Type:
"""Called for dataclasses.asdict."""
return _dataclass_exclusive_function_callback(ctx, "asdict")


def astuple_callback(ctx: FunctionContext) -> Type:
"""Called for dataclasses.astuple."""
return _dataclass_exclusive_function_callback(ctx, "astuple")


def fields_callback(ctx: FunctionContext) -> Type:
"""Called for dataclasses.fields."""
return _dataclass_exclusive_function_callback(ctx, "fields")


def replace_callback(ctx: FunctionContext) -> Type:
"""Called for dataclasses.replace."""
return _dataclass_exclusive_function_callback(ctx, "replace")


def _collect_field_args(
expr: Expression, ctx: ClassDefContext
) -> tuple[bool, dict[str, Expression]]:
Expand Down
13 changes: 11 additions & 2 deletions mypy/plugins/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,21 @@ class DefaultPlugin(Plugin):
"""Type checker plugin that is enabled by default."""

def get_function_hook(self, fullname: str) -> Callable[[FunctionContext], Type] | None:
from mypy.plugins import ctypes, singledispatch
from mypy.plugins import ctypes, dataclasses, singledispatch

if fullname == "ctypes.Array":
return ctypes.array_constructor_callback
elif fullname == "functools.singledispatch":
if fullname == "functools.singledispatch":
return singledispatch.create_singledispatch_function_callback
name_pieces = fullname.split(".")
if len(name_pieces) == 2 and name_pieces[0] == "dataclasses":
callbacks: dict[str, Callable[[FunctionContext], Type]] = {
"asdict": dataclasses.asdict_callback,
"astuple": dataclasses.astuple_callback,
"fields": dataclasses.fields_callback,
"replace": dataclasses.replace_callback,
}
return callbacks.get(name_pieces[1], None)
return None

def get_method_signature_hook(
Expand Down
26 changes: 26 additions & 0 deletions test-data/unit/check-dataclasses.test
Original file line number Diff line number Diff line change
Expand Up @@ -2001,3 +2001,29 @@ class Bar(Foo): ...
e: Element[Bar]
reveal_type(e.elements) # N: Revealed type is "typing.Sequence[__main__.Element[__main__.Bar]]"
[builtins fixtures/dataclasses.pyi]

[case testFuncsOnlyTakeDataclassArg]
# flags: --python-version 3.7
# Ensure asdict, astuple, fields, and replace methods from dataclasses module only accept
# a dataclass instance as the first argument.
# See mypy issue #14215
from dataclasses import asdict, astuple, dataclass, fields, replace

class Klass:
pass

@dataclass
class DClass:
pass

klass = Klass()
dclass = DClass()
asdict(dclass)
astuple(dclass)
replace(dclass)
fields(dclass)
asdict(klass) # E: asdict() should be called on dataclass instances
astuple(klass) # E: astuple() should be called on dataclass instances
fields(klass) # E: fields() should be called on dataclass instances
replace(klass) # E: replace() should be called on dataclass instances
[builtins fixtures/dataclasses.pyi]
21 changes: 20 additions & 1 deletion test-data/unit/lib-stub/dataclasses.pyi
872B
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from typing import Any, Callable, Generic, Mapping, Optional, TypeVar, overload, Type
from typing import (
Any, Callable, Generic, Mapping, Optional, TypeVar, overload, Tuple,
Type,
)

_T = TypeVar('_T')

Expand Down Expand Up @@ -32,3 +35,19 @@ def field(*,


class Field(Generic[_T]): pass

@overload
def asdict(obj: Any) -> dict[str, Any]: ...

@overload
def asdict(obj: Any, *, dict_factory: Callable[[list[tuple[str, Any]]], _T]) -> _T: ...

@overload
def astuple(obj: Any) -> Tuple[Any, ...]: ...

@overload
def astuple(obj: Any, *, tuple_factory: Callable[[list[Any]], _T]) -> _T: ...

def replace(obj: _T, **changes: Any) -> _T: ...

def fields(class_or_instance: Any) -> Tuple[Field[Any], ...]: ...
0