Skip to content

Commit

Permalink
pythongh-102699: Add dataclasses.DataclassLike
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexWaygood committed Mar 22, 2023
1 parent 8709697 commit e6bf118
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 0 deletions.
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(), 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
27 changes: 27 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 @@ -1267,6 +1268,32 @@ def is_dataclass(obj):
return hasattr(cls, _FIELDS)


class DataclassLike(metaclass=abc.ABCMeta):
"""Abstract base class for all dataclass types.
Mainly useful for type-checking.
"""
# __dataclass_fields__ here is really an "abstract class variable",
# but there's no good way of expressing that at runtime,
# so just make it a regular class variable with a dummy value
__dataclass_fields__ = {}

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"
)

@classmethod
def __subclasshook__(cls, other):
return hasattr(other, _FIELDS)


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
52 changes: 52 additions & 0 deletions Lib/test/test_dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -1503,6 +1503,58 @@ class A(types.GenericAlias):
self.assertTrue(is_dataclass(type(a)))
self.assertTrue(is_dataclass(a))

def test_DataclassLike(self):
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.

0 comments on commit e6bf118

Please sign in to comment.