From b33aaa7d5fc99eae199ec8fa2d7d645f0a98295d Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 6 Nov 2021 13:24:09 +0300 Subject: [PATCH 1/6] Adds `slots=True` support for `@dataclass`, refs #11482 --- mypy/plugins/dataclasses.py | 33 +++++++++++++++ test-data/unit/check-dataclasses.test | 59 +++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index 62e3279eac1e..ff860d2a916c 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -125,7 +125,9 @@ def transform(self) -> None: 'eq': _get_decorator_bool_argument(self._ctx, 'eq', True), 'order': _get_decorator_bool_argument(self._ctx, 'order', False), 'frozen': _get_decorator_bool_argument(self._ctx, 'frozen', False), + 'slots': _get_decorator_bool_argument(self._ctx, 'slots', False), } + py_version = self._ctx.api.options.python_version # If there are no attributes, it may be that the semantic analyzer has not # processed them yet. In order to work around this, we can simply skip generating @@ -188,6 +190,9 @@ def transform(self) -> None: else: self._propertize_callables(attributes) + if decorator_arguments['slots']: + self.add_slots(info, attributes, py_version >= (3, 10)) + self.reset_init_only_vars(info, attributes) self._add_dataclass_fields_magic_attribute() @@ -197,6 +202,34 @@ def transform(self) -> None: 'frozen': decorator_arguments['frozen'], } + def add_slots(self, + info: TypeInfo, + attributes: List[DataclassAttribute], + correct_version: bool) -> None: + if not correct_version: + # This means that version is lower than `3.10`, + # it is just a non-existent argument for `dataclass` function. + self._ctx.api.fail( + 'Unexpected keyword argument "slots" for "dataclass"', + self._ctx.reason, + ) + return + if info.slots: + # This means we have a slots confict. + # Class explicitly specifies `__slots__` field. + # And `@dataclass(slots=True)` is used. + # In runtime this raises a type error. + self._ctx.api.fail( + '"{}" both defines "__slots__" and is used with "slots=True"'.format( + self._ctx.cls.name, + ), + self._ctx.cls, + ) + return + + slots = [attr.name for attr in attributes] + info.slots = set(slots) + def reset_init_only_vars(self, info: TypeInfo, attributes: List[DataclassAttribute]) -> None: """Remove init-only vars from the class and reset init var declarations.""" for attr in attributes: diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index 2e5e1cfd5383..ff22f63cf900 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -1350,3 +1350,62 @@ class Foo: reveal_type(Foo(bar=1.5)) # N: Revealed type is "__main__.Foo" [builtins fixtures/dataclasses.pyi] + + +[case testDataclassWithSlotsArg] +# flags: --python-version 3.10 +from dataclasses import dataclass + +@dataclass(slots=True) +class Some: + x: int + + def __init__(self, x: int) -> None: + self.x = x + self.y = 0 # E: Trying to assign name "y" that is not in "__slots__" of type "__main__.Some" + + def __post_init__(self) -> None: + self.y = 1 # E: Trying to assign name "y" that is not in "__slots__" of type "__main__.Some" +[builtins fixtures/dataclasses.pyi] + +[case testDataclassWithSlotsDef] +# flags: --python-version 3.10 +from dataclasses import dataclass + +@dataclass(slots=False) +class Some: + __slots__ = ('x',) + x: int + + def __init__(self, x: int) -> None: + self.x = x + self.y = 0 # E: Trying to assign name "y" that is not in "__slots__" of type "__main__.Some" + + def __post_init__(self) -> None: + self.y = 1 # E: Trying to assign name "y" that is not in "__slots__" of type "__main__.Some" +[builtins fixtures/dataclasses.pyi] + +[case testDataclassWithSlotsConflict] +# flags: --python-version 3.10 +from dataclasses import dataclass + +@dataclass(slots=True) +class Some: # E: "Some" both defines "__slots__" and is used with "slots=True" + __slots__ = ('x',) + x: int +[builtins fixtures/dataclasses.pyi] + +[case testDataclassWithSlotsArgBefore310] +# flags: --python-version 3.9 +from dataclasses import dataclass + +@dataclass(slots=True) # E: Unexpected keyword argument "slots" for "dataclass" +class Some: + x: int + +# Possible conflict: +@dataclass(slots=True) # E: Unexpected keyword argument "slots" for "dataclass" +class Other: + __slots__ = ('x',) + x: int +[builtins fixtures/dataclasses.pyi] From 38b6d0344d63f60ea4a1a35b8de0f43effb796ce Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 6 Nov 2021 17:32:55 +0300 Subject: [PATCH 2/6] Improves test with slots conflict --- mypy/plugins/dataclasses.py | 2 +- test-data/unit/check-dataclasses.test | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index ff860d2a916c..3b1d6a780360 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -214,7 +214,7 @@ def add_slots(self, self._ctx.reason, ) return - if info.slots: + if info.slots is not None or info.names.get('__slots__'): # This means we have a slots confict. # Class explicitly specifies `__slots__` field. # And `@dataclass(slots=True)` is used. diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index ff22f63cf900..e80eb5777248 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -1393,6 +1393,18 @@ from dataclasses import dataclass class Some: # E: "Some" both defines "__slots__" and is used with "slots=True" __slots__ = ('x',) x: int + +@dataclass(slots=True) +class EmptyDef: # E: "EmptyDef" both defines "__slots__" and is used with "slots=True" + __slots__ = () + x: int + +slots = ('x',) + +@dataclass(slots=True) +class DynamicDef: # E: "DynamicDef" both defines "__slots__" and is used with "slots=True" + __slots__ = slots + x: int [builtins fixtures/dataclasses.pyi] [case testDataclassWithSlotsArgBefore310] From edb1b967bc9af9c8447fc6bf143473af6d1bb795 Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Sun, 7 Nov 2021 17:37:07 +0300 Subject: [PATCH 3/6] Update mypy/plugins/dataclasses.py Co-authored-by: Jelle Zijlstra --- mypy/plugins/dataclasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index 3b1d6a780360..c6247ea5225f 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -210,7 +210,7 @@ def add_slots(self, # This means that version is lower than `3.10`, # it is just a non-existent argument for `dataclass` function. self._ctx.api.fail( - 'Unexpected keyword argument "slots" for "dataclass"', + 'Keyword argument "slots" for "dataclass" is only valid in Python 3.10 and higher', self._ctx.reason, ) return From d7aab985423dee2e52e4eff39e522c83449c543f Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Sun, 7 Nov 2021 17:37:16 +0300 Subject: [PATCH 4/6] Update mypy/plugins/dataclasses.py Co-authored-by: Jelle Zijlstra --- mypy/plugins/dataclasses.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index c6247ea5225f..f50eedf805b5 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -227,8 +227,7 @@ def add_slots(self, ) return - slots = [attr.name for attr in attributes] - info.slots = set(slots) + info.slots = {attr.name for attr in attributes} def reset_init_only_vars(self, info: TypeInfo, attributes: List[DataclassAttribute]) -> None: """Remove init-only vars from the class and reset init var declarations.""" From cab3eced0a58609bcc34719996c73a4f5abc4e7f Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Sun, 7 Nov 2021 17:37:41 +0300 Subject: [PATCH 5/6] Update mypy/plugins/dataclasses.py Co-authored-by: Jelle Zijlstra --- mypy/plugins/dataclasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index f50eedf805b5..6a5927dccc9c 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -191,7 +191,7 @@ def transform(self) -> None: self._propertize_callables(attributes) if decorator_arguments['slots']: - self.add_slots(info, attributes, py_version >= (3, 10)) + self.add_slots(info, attributes, correct_version=py_version >= (3, 10)) self.reset_init_only_vars(info, attributes) From 9c94bde168a23c6ff4e64a871ce50e3e80eeb7cb Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sun, 7 Nov 2021 18:59:58 +0300 Subject: [PATCH 6/6] Addresses review --- mypy/plugins/dataclasses.py | 4 +++- test-data/unit/check-dataclasses.test | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index 6a5927dccc9c..dab1fc23ef26 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -205,12 +205,14 @@ def transform(self) -> None: def add_slots(self, info: TypeInfo, attributes: List[DataclassAttribute], + *, correct_version: bool) -> None: if not correct_version: # This means that version is lower than `3.10`, # it is just a non-existent argument for `dataclass` function. self._ctx.api.fail( - 'Keyword argument "slots" for "dataclass" is only valid in Python 3.10 and higher', + 'Keyword argument "slots" for "dataclass" ' + 'is only valid in Python 3.10 and higher', self._ctx.reason, ) return diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index e80eb5777248..3d5f0a1da6e5 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -1411,12 +1411,12 @@ class DynamicDef: # E: "DynamicDef" both defines "__slots__" and is used with " # flags: --python-version 3.9 from dataclasses import dataclass -@dataclass(slots=True) # E: Unexpected keyword argument "slots" for "dataclass" +@dataclass(slots=True) # E: Keyword argument "slots" for "dataclass" is only valid in Python 3.10 and higher class Some: x: int # Possible conflict: -@dataclass(slots=True) # E: Unexpected keyword argument "slots" for "dataclass" +@dataclass(slots=True) # E: Keyword argument "slots" for "dataclass" is only valid in Python 3.10 and higher class Other: __slots__ = ('x',) x: int