Skip to content

Commit 5bba0f3

Browse files
authored
Adds slots=True support for @dataclass (#11483)
Closes #11482
1 parent d41e34a commit 5bba0f3

File tree

2 files changed

+105
-0
lines changed

2 files changed

+105
-0
lines changed

mypy/plugins/dataclasses.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,9 @@ def transform(self) -> None:
125125
'eq': _get_decorator_bool_argument(self._ctx, 'eq', True),
126126
'order': _get_decorator_bool_argument(self._ctx, 'order', False),
127127
'frozen': _get_decorator_bool_argument(self._ctx, 'frozen', False),
128+
'slots': _get_decorator_bool_argument(self._ctx, 'slots', False),
128129
}
130+
py_version = self._ctx.api.options.python_version
129131

130132
# If there are no attributes, it may be that the semantic analyzer has not
131133
# processed them yet. In order to work around this, we can simply skip generating
@@ -188,6 +190,9 @@ def transform(self) -> None:
188190
else:
189191
self._propertize_callables(attributes)
190192

193+
if decorator_arguments['slots']:
194+
self.add_slots(info, attributes, correct_version=py_version >= (3, 10))
195+
191196
self.reset_init_only_vars(info, attributes)
192197

193198
self._add_dataclass_fields_magic_attribute()
@@ -197,6 +202,35 @@ def transform(self) -> None:
197202
'frozen': decorator_arguments['frozen'],
198203
}
199204

205+
def add_slots(self,
206+
info: TypeInfo,
207+
attributes: List[DataclassAttribute],
208+
*,
209+
correct_version: bool) -> None:
210+
if not correct_version:
211+
# This means that version is lower than `3.10`,
212+
# it is just a non-existent argument for `dataclass` function.
213+
self._ctx.api.fail(
214+
'Keyword argument "slots" for "dataclass" '
215+
'is only valid in Python 3.10 and higher',
216+
self._ctx.reason,
217+
)
218+
return
219+
if info.slots is not None or info.names.get('__slots__'):
220+
# This means we have a slots confict.
221+
# Class explicitly specifies `__slots__` field.
222+
# And `@dataclass(slots=True)` is used.
223+
# In runtime this raises a type error.
224+
self._ctx.api.fail(
225+
'"{}" both defines "__slots__" and is used with "slots=True"'.format(
226+
self._ctx.cls.name,
227+
),
228+
self._ctx.cls,
229+
)
230+
return
231+
232+
info.slots = {attr.name for attr in attributes}
233+
200234
def reset_init_only_vars(self, info: TypeInfo, attributes: List[DataclassAttribute]) -> None:
201235
"""Remove init-only vars from the class and reset init var declarations."""
202236
for attr in attributes:

test-data/unit/check-dataclasses.test

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1350,3 +1350,74 @@ class Foo:
13501350

13511351
reveal_type(Foo(bar=1.5)) # N: Revealed type is "__main__.Foo"
13521352
[builtins fixtures/dataclasses.pyi]
1353+
1354+
1355+
[case testDataclassWithSlotsArg]
1356+
# flags: --python-version 3.10
1357+
from dataclasses import dataclass
1358+
1359+
@dataclass(slots=True)
1360+
class Some:
1361+
x: int
1362+
1363+
def __init__(self, x: int) -> None:
1364+
self.x = x
1365+
self.y = 0 # E: Trying to assign name "y" that is not in "__slots__" of type "__main__.Some"
1366+
1367+
def __post_init__(self) -> None:
1368+
self.y = 1 # E: Trying to assign name "y" that is not in "__slots__" of type "__main__.Some"
1369+
[builtins fixtures/dataclasses.pyi]
1370+
1371+
[case testDataclassWithSlotsDef]
1372+
# flags: --python-version 3.10
1373+
from dataclasses import dataclass
1374+
1375+
@dataclass(slots=False)
1376+
class Some:
1377+
__slots__ = ('x',)
1378+
x: int
1379+
1380+
def __init__(self, x: int) -> None:
1381+
self.x = x
1382+
self.y = 0 # E: Trying to assign name "y" that is not in "__slots__" of type "__main__.Some"
1383+
1384+
def __post_init__(self) -> None:
1385+
self.y = 1 # E: Trying to assign name "y" that is not in "__slots__" of type "__main__.Some"
1386+
[builtins fixtures/dataclasses.pyi]
1387+
1388+
[case testDataclassWithSlotsConflict]
1389+
# flags: --python-version 3.10
1390+
from dataclasses import dataclass
1391+
1392+
@dataclass(slots=True)
1393+
class Some: # E: "Some" both defines "__slots__" and is used with "slots=True"
1394+
__slots__ = ('x',)
1395+
x: int
1396+
1397+
@dataclass(slots=True)
1398+
class EmptyDef: # E: "EmptyDef" both defines "__slots__" and is used with "slots=True"
1399+
__slots__ = ()
1400+
x: int
1401+
1402+
slots = ('x',)
1403+
1404+
@dataclass(slots=True)
1405+
class DynamicDef: # E: "DynamicDef" both defines "__slots__" and is used with "slots=True"
1406+
__slots__ = slots
1407+
x: int
1408+
[builtins fixtures/dataclasses.pyi]
1409+
1410+
[case testDataclassWithSlotsArgBefore310]
1411+
# flags: --python-version 3.9
1412+
from dataclasses import dataclass
1413+
1414+
@dataclass(slots=True) # E: Keyword argument "slots" for "dataclass" is only valid in Python 3.10 and higher
1415+
class Some:
1416+
x: int
1417+
1418+
# Possible conflict:
1419+
@dataclass(slots=True) # E: Keyword argument "slots" for "dataclass" is only valid in Python 3.10 and higher
1420+
class Other:
1421+
__slots__ = ('x',)
1422+
x: int
1423+
[builtins fixtures/dataclasses.pyi]

0 commit comments

Comments
 (0)