From 53160692be239fb0bc46634d03bd2a032dc8a4d6 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 18 Sep 2021 13:17:39 +0300 Subject: [PATCH 1/3] Fixes mypy crash on `dataclasses.field(**unpack)` --- mypy/plugins/dataclasses.py | 6 ++++- test-data/unit/check-dataclasses.test | 35 +++++++++++++++++++++++++ test-data/unit/fixtures/dataclasses.pyi | 25 ++++++++++++++++-- 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index 96b58b3f43a7..49eb49e43ebe 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -460,7 +460,11 @@ def _collect_field_args(expr: Expression) -> Tuple[bool, Dict[str, Expression]]: # field() only takes keyword arguments. args = {} for name, arg in zip(expr.arg_names, expr.args): - assert name is not None + if name is None: + # This means that `field` is used with `**` unpacking, + # the best we can do for now is not to fail. + # TODO: we can infer what's inside `**` and try to collect it. + return True, {} args[name] = arg return True, args return False, {} diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index 62959488aa27..30ad109c45c5 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -1300,3 +1300,38 @@ a.x = x a.x = x2 # E: Incompatible types in assignment (expression has type "Callable[[str], str]", variable has type "Callable[[int], int]") [builtins fixtures/dataclasses.pyi] + + +[case testDataclassFieldDoesNotFailOnKwargsUnpacking] +# flags: --python-version 3.7 +# https://github.com/python/mypy/issues/10879 +from dataclasses import dataclass, field + +@dataclass +class Foo: + bar: float = field(**{"repr": False}) +[out] +main:7: error: No overload variant of "field" matches argument type "Dict[str, bool]" +main:7: note: Possible overload variants: +main:7: note: def [_T] field(*, default: _T, init: bool = ..., repr: bool = ..., hash: Optional[bool] = ..., compare: bool = ..., metadata: Optional[Mapping[str, Any]] = ..., kw_only: bool = ...) -> _T +main:7: note: def [_T] field(*, default_factory: Callable[[], _T], init: bool = ..., repr: bool = ..., hash: Optional[bool] = ..., compare: bool = ..., metadata: Optional[Mapping[str, Any]] = ..., kw_only: bool = ...) -> _T +main:7: note: def field(*, init: bool = ..., repr: bool = ..., hash: Optional[bool] = ..., compare: bool = ..., metadata: Optional[Mapping[str, Any]] = ..., kw_only: bool = ...) -> Any +[builtins fixtures/dataclasses.pyi] + + +[case testDataclassFieldWithTypedDictUnpacking] +# flags: --python-version 3.7 +from dataclasses import dataclass, field +from typing_extensions import TypedDict + +class FieldKwargs(TypedDict): + repr: bool + +field_kwargs: FieldKwargs = {"repr": False} + +@dataclass +class Foo: + bar: float = field(**field_kwargs) + +reveal_type(Foo(bar=1.5)) # N: Revealed type is "__main__.Foo" +[builtins fixtures/dataclasses.pyi] diff --git a/test-data/unit/fixtures/dataclasses.pyi b/test-data/unit/fixtures/dataclasses.pyi index fb0053c80b25..206843a88b24 100644 --- a/test-data/unit/fixtures/dataclasses.pyi +++ b/test-data/unit/fixtures/dataclasses.pyi @@ -1,7 +1,12 @@ -from typing import Generic, Sequence, TypeVar +from typing import ( + Generic, Iterator, Iterable, Mapping, Optional, Sequence, Tuple, + TypeVar, Union, overload, +) _T = TypeVar('_T') _U = TypeVar('_U') +KT = TypeVar('KT') +VT = TypeVar('VT') class object: def __init__(self) -> None: pass @@ -15,7 +20,23 @@ class int: pass class float: pass class str: pass class bool(int): pass -class dict(Generic[_T, _U]): pass + +class dict(Mapping[KT, VT]): + @overload + def __init__(self, **kwargs: VT) -> None: pass + @overload + def __init__(self, arg: Iterable[Tuple[KT, VT]], **kwargs: VT) -> None: pass + def __getitem__(self, key: KT) -> VT: pass + def __setitem__(self, k: KT, v: VT) -> None: pass + def __iter__(self) -> Iterator[KT]: pass + def __contains__(self, item: object) -> int: pass + def update(self, a: Mapping[KT, VT]) -> None: pass + @overload + def get(self, k: KT) -> Optional[VT]: pass + @overload + def get(self, k: KT, default: Union[KT, _T]) -> Union[VT, _T]: pass + def __len__(self) -> int: ... + class list(Generic[_T], Sequence[_T]): pass class function: pass class classmethod: pass From f070eab6fa39302e63bf9fceebbdbad873567518 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sun, 19 Sep 2021 11:55:34 +0300 Subject: [PATCH 2/3] Adds explicit error for **kwargs unpacking in field() --- mypy/plugins/dataclasses.py | 9 +++++++-- test-data/unit/check-dataclasses.test | 3 ++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index 49eb49e43ebe..d69b701ed279 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -269,7 +269,7 @@ def collect_attributes(self) -> Optional[List[DataclassAttribute]]: if self._is_kw_only_type(node_type): kw_only = True - has_field_call, field_args = _collect_field_args(stmt.rvalue) + has_field_call, field_args = _collect_field_args(stmt.rvalue, ctx) is_in_init_param = field_args.get('init') if is_in_init_param is None: @@ -447,7 +447,8 @@ def dataclass_class_maker_callback(ctx: ClassDefContext) -> None: transformer.transform() -def _collect_field_args(expr: Expression) -> Tuple[bool, Dict[str, Expression]]: +def _collect_field_args(expr: Expression, + ctx: ClassDefContext) -> Tuple[bool, Dict[str, Expression]]: """Returns a tuple where the first value represents whether or not the expression is a call to dataclass.field and the second is a dictionary of the keyword arguments that field() was called with. @@ -464,6 +465,10 @@ def _collect_field_args(expr: Expression) -> Tuple[bool, Dict[str, Expression]]: # This means that `field` is used with `**` unpacking, # the best we can do for now is not to fail. # TODO: we can infer what's inside `**` and try to collect it. + ctx.api.fail( + 'Unpacking dynamic **kwargs in "field()" is not supported', + expr, + ) return True, {} args[name] = arg return True, args diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index 30ad109c45c5..59c513378f96 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -1311,6 +1311,7 @@ from dataclasses import dataclass, field class Foo: bar: float = field(**{"repr": False}) [out] +main:7: error: Unpacking dynamic **kwargs in "field()" is not supported main:7: error: No overload variant of "field" matches argument type "Dict[str, bool]" main:7: note: Possible overload variants: main:7: note: def [_T] field(*, default: _T, init: bool = ..., repr: bool = ..., hash: Optional[bool] = ..., compare: bool = ..., metadata: Optional[Mapping[str, Any]] = ..., kw_only: bool = ...) -> _T @@ -1331,7 +1332,7 @@ field_kwargs: FieldKwargs = {"repr": False} @dataclass class Foo: - bar: float = field(**field_kwargs) + bar: float = field(**field_kwargs) # E: Unpacking dynamic **kwargs in "field()" is not supported reveal_type(Foo(bar=1.5)) # N: Revealed type is "__main__.Foo" [builtins fixtures/dataclasses.pyi] From c44781e0279c607711f7e8d559d48ca930a5aa50 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Mon, 20 Sep 2021 22:40:39 -0700 Subject: [PATCH 3/3] Apply suggestions from code review --- mypy/plugins/dataclasses.py | 2 +- test-data/unit/check-dataclasses.test | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index d69b701ed279..9c615f857731 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -466,7 +466,7 @@ def _collect_field_args(expr: Expression, # the best we can do for now is not to fail. # TODO: we can infer what's inside `**` and try to collect it. ctx.api.fail( - 'Unpacking dynamic **kwargs in "field()" is not supported', + 'Unpacking **kwargs in "field()" is not supported', expr, ) return True, {} diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index 59c513378f96..80ad554d846c 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -1311,7 +1311,7 @@ from dataclasses import dataclass, field class Foo: bar: float = field(**{"repr": False}) [out] -main:7: error: Unpacking dynamic **kwargs in "field()" is not supported +main:7: error: Unpacking **kwargs in "field()" is not supported main:7: error: No overload variant of "field" matches argument type "Dict[str, bool]" main:7: note: Possible overload variants: main:7: note: def [_T] field(*, default: _T, init: bool = ..., repr: bool = ..., hash: Optional[bool] = ..., compare: bool = ..., metadata: Optional[Mapping[str, Any]] = ..., kw_only: bool = ...) -> _T @@ -1332,7 +1332,7 @@ field_kwargs: FieldKwargs = {"repr": False} @dataclass class Foo: - bar: float = field(**field_kwargs) # E: Unpacking dynamic **kwargs in "field()" is not supported + bar: float = field(**field_kwargs) # E: Unpacking **kwargs in "field()" is not supported reveal_type(Foo(bar=1.5)) # N: Revealed type is "__main__.Foo" [builtins fixtures/dataclasses.pyi]