Skip to content

Commit e6bf118

Browse files
committed
pythongh-102699: Add dataclasses.DataclassLike
1 parent 8709697 commit e6bf118

File tree

4 files changed

+105
-0
lines changed

4 files changed

+105
-0
lines changed

Doc/library/dataclasses.rst

+24
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,30 @@ Module contents
468468
def is_dataclass_instance(obj):
469469
return is_dataclass(obj) and not isinstance(obj, type)
470470

471+
.. class:: DataclassLike
472+
473+
An abstract base class for all dataclasses. Mainly useful for type-checking.
474+
475+
All classes created using the :func:`@dataclass <dataclass>` decorator are
476+
considered subclasses of this class; all dataclass instances are considered
477+
instances of this class:
478+
479+
>>> from dataclasses import dataclass, DataclassLike
480+
>>> @dataclass
481+
... class Foo:
482+
... x: int
483+
...
484+
>>> issubclass(Foo, DataclassLike)
485+
True
486+
>>> isinstance(Foo(), DataclassLike)
487+
True
488+
489+
``DataclassLike`` is an abstract class that cannot be instantiated. It is
490+
also a "final" class that cannot be subclassed: use the
491+
:func:`@dataclass <dataclass>` decorator to create new dataclasses.
492+
493+
.. versionadded:: 3.12
494+
471495
.. data:: MISSING
472496

473497
A sentinel value signifying a missing default or default_factory.

Lib/dataclasses.py

+27
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
'make_dataclass',
2727
'replace',
2828
'is_dataclass',
29+
'DataclassLike',
2930
]
3031

3132
# Conditions for adding methods. The boxes indicate what action the
@@ -1267,6 +1268,32 @@ def is_dataclass(obj):
12671268
return hasattr(cls, _FIELDS)
12681269

12691270

1271+
class DataclassLike(metaclass=abc.ABCMeta):
1272+
"""Abstract base class for all dataclass types.
1273+
1274+
Mainly useful for type-checking.
1275+
"""
1276+
# __dataclass_fields__ here is really an "abstract class variable",
1277+
# but there's no good way of expressing that at runtime,
1278+
# so just make it a regular class variable with a dummy value
1279+
__dataclass_fields__ = {}
1280+
1281+
def __init_subclass__(cls):
1282+
raise TypeError(
1283+
"Use the @dataclass decorator to create dataclasses, "
1284+
"rather than subclassing dataclasses.DataclassLike"
1285+
)
1286+
1287+
def __new__(cls):
1288+
raise TypeError(
1289+
"dataclasses.DataclassLike is an abstract class that cannot be instantiated"
1290+
)
1291+
1292+
@classmethod
1293+
def __subclasshook__(cls, other):
1294+
return hasattr(other, _FIELDS)
1295+
1296+
12701297
def asdict(obj, *, dict_factory=dict):
12711298
"""Return the fields of a dataclass instance as a new dictionary mapping
12721299
field names to field values.

Lib/test/test_dataclasses.py

+52
Original file line numberDiff line numberDiff line change
@@ -1503,6 +1503,58 @@ class A(types.GenericAlias):
15031503
self.assertTrue(is_dataclass(type(a)))
15041504
self.assertTrue(is_dataclass(a))
15051505

1506+
def test_DataclassLike(self):
1507+
with self.assertRaises(TypeError):
1508+
DataclassLike()
1509+
1510+
with self.assertRaises(TypeError):
1511+
class Foo(DataclassLike): pass
1512+
1513+
@dataclass
1514+
class Dataclass:
1515+
x: int
1516+
1517+
self.assertTrue(issubclass(Dataclass, DataclassLike))
1518+
self.assertIsInstance(Dataclass(42), DataclassLike)
1519+
1520+
with self.assertRaises(TypeError):
1521+
issubclass(Dataclass(42), DataclassLike)
1522+
1523+
class NotADataclass:
1524+
def __init__(self):
1525+
self.x = 42
1526+
1527+
self.assertFalse(issubclass(NotADataclass, DataclassLike))
1528+
self.assertNotIsInstance(NotADataclass(), DataclassLike)
1529+
1530+
class NotADataclassButDataclassLike:
1531+
"""A class from an outside library (attrs?) with dataclass-like behaviour"""
1532+
__dataclass_fields__ = {}
1533+
1534+
self.assertTrue(issubclass(NotADataclassButDataclassLike, DataclassLike))
1535+
self.assertIsInstance(NotADataclassButDataclassLike(), DataclassLike)
1536+
1537+
class HasInstanceDataclassFieldsAttribute:
1538+
def __init__(self):
1539+
self.__dataclass_fields__ = {}
1540+
1541+
self.assertFalse(issubclass(HasInstanceDataclassFieldsAttribute, DataclassLike))
1542+
self.assertNotIsInstance(HasInstanceDataclassFieldsAttribute(), DataclassLike)
1543+
1544+
class HasAllAttributes:
1545+
def __getattr__(self, name):
1546+
return {}
1547+
1548+
self.assertFalse(issubclass(HasAllAttributes, DataclassLike))
1549+
self.assertNotIsInstance(HasAllAttributes(), DataclassLike)
1550+
1551+
@dataclass
1552+
class GenericAliasSubclass(types.GenericAlias):
1553+
origin: type
1554+
args: type
1555+
1556+
self.assertTrue(issubclass(GenericAliasSubclass, DataclassLike))
1557+
self.assertIsInstance(GenericAliasSubclass(int, str), DataclassLike)
15061558

15071559
def test_helper_fields_with_class_instance(self):
15081560
# Check that we can call fields() on either a class or instance,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add :class:`dataclasses.DataclassLike`, an abstract base class for all
2+
dataclasses. Patch by Alex Waygood.

0 commit comments

Comments
 (0)