diff --git a/Doc/library/dataclasses.rst b/Doc/library/dataclasses.rst index 85a7d9026d1d30..310c3ea77b1d1c 100644 --- a/Doc/library/dataclasses.rst +++ b/Doc/library/dataclasses.rst @@ -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 ` 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 ` decorator to create new dataclasses. + + .. versionadded:: 3.12 + .. data:: MISSING A sentinel value signifying a missing default or default_factory. diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index a73cdc22a5f4b3..27755f9353c550 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -26,6 +26,7 @@ 'make_dataclass', 'replace', 'is_dataclass', + 'DataclassLike', ] # Conditions for adding methods. The boxes indicate what action the @@ -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. diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py index 7dd81a8855f1be..19c8e2b3434e65 100644 --- a/Lib/test/test_dataclasses.py +++ b/Lib/test/test_dataclasses.py @@ -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, diff --git a/Misc/NEWS.d/next/Library/2023-03-22-21-05-34.gh-issue-102699.48uE4z.rst b/Misc/NEWS.d/next/Library/2023-03-22-21-05-34.gh-issue-102699.48uE4z.rst new file mode 100644 index 00000000000000..a6e4d242d28ef3 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-03-22-21-05-34.gh-issue-102699.48uE4z.rst @@ -0,0 +1,2 @@ +Add :class:`dataclasses.DataclassLike`, an abstract base class for all +dataclasses. Patch by Alex Waygood.