Skip to content

gh-102699: Add dataclasses.DataclassLike #102933

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
wants to merge 11 commits into from
24 changes: 24 additions & 0 deletions Doc/library/dataclasses.rst
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,30 @@ Module contents
def is_dataclass_instance(obj):
return is_dataclass(obj) and not isinstance(obj, type)

.. class:: DataclassLike

An abstract base class for all dataclasses. Mainly useful for type-checking.

All classes created using the :func:`@dataclass <dataclass>` decorator are
considered subclasses of this class; all dataclass instances are considered
instances of this class:

>>> from dataclasses import dataclass, DataclassLike
>>> @dataclass
... class Foo:
... x: int
...
>>> issubclass(Foo, DataclassLike)
True
>>> isinstance(Foo(42), DataclassLike)
True

``DataclassLike`` is an abstract class that cannot be instantiated. It is
also a "final" class that cannot be subclassed: use the
:func:`@dataclass <dataclass>` decorator to create new dataclasses.

.. versionadded:: 3.12

.. data:: MISSING

A sentinel value signifying a missing default or default_factory.
Expand Down
21 changes: 21 additions & 0 deletions Lib/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
'make_dataclass',
'replace',
'is_dataclass',
'DataclassLike',
]

# Conditions for adding methods. The boxes indicate what action the
Expand Down Expand Up @@ -1294,6 +1295,26 @@ def is_dataclass(obj):
return hasattr(cls, _FIELDS)


@dataclass(init=False, repr=False, eq=False, match_args=False)
class DataclassLike(metaclass=abc.ABCMeta):
"""Abstract base class for all dataclass types.

Mainly useful for type-checking.
"""
def __init_subclass__(cls):
raise TypeError(
"Use the @dataclass decorator to create dataclasses, "
"rather than subclassing dataclasses.DataclassLike"
)

def __new__(cls):
raise TypeError(
"dataclasses.DataclassLike is an abstract class that cannot be instantiated"
)

__subclasshook__ = is_dataclass


def asdict(obj, *, dict_factory=dict):
"""Return the fields of a dataclass instance as a new dictionary mapping
field names to field values.
Expand Down
57 changes: 57 additions & 0 deletions Lib/test/test_dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -1522,6 +1522,63 @@ class A(types.GenericAlias):
self.assertTrue(is_dataclass(type(a)))
self.assertTrue(is_dataclass(a))

def test_DataclassLike(self):
# As an abstract base class for all dataclasses,
# it makes sense for DataclassLike to also be considered a dataclass
self.assertTrue(is_dataclass(DataclassLike))
self.assertTrue(issubclass(DataclassLike, DataclassLike))

with self.assertRaises(TypeError):
DataclassLike()

with self.assertRaises(TypeError):
class Foo(DataclassLike): pass

@dataclass
class Dataclass:
x: int

self.assertTrue(issubclass(Dataclass, DataclassLike))
self.assertIsInstance(Dataclass(42), DataclassLike)

with self.assertRaises(TypeError):
issubclass(Dataclass(42), DataclassLike)

class NotADataclass:
def __init__(self):
self.x = 42

self.assertFalse(issubclass(NotADataclass, DataclassLike))
self.assertNotIsInstance(NotADataclass(), DataclassLike)

class NotADataclassButDataclassLike:
"""A class from an outside library (attrs?) with dataclass-like behaviour"""
__dataclass_fields__ = {}

self.assertTrue(issubclass(NotADataclassButDataclassLike, DataclassLike))
self.assertIsInstance(NotADataclassButDataclassLike(), DataclassLike)

class HasInstanceDataclassFieldsAttribute:
def __init__(self):
self.__dataclass_fields__ = {}

self.assertFalse(issubclass(HasInstanceDataclassFieldsAttribute, DataclassLike))
self.assertNotIsInstance(HasInstanceDataclassFieldsAttribute(), DataclassLike)

class HasAllAttributes:
def __getattr__(self, name):
return {}

self.assertFalse(issubclass(HasAllAttributes, DataclassLike))
self.assertNotIsInstance(HasAllAttributes(), DataclassLike)

@dataclass
class GenericAliasSubclass(types.GenericAlias):
origin: type
args: type

self.assertTrue(issubclass(GenericAliasSubclass, DataclassLike))
self.assertIsInstance(GenericAliasSubclass(int, str), DataclassLike)

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