bpo-32499: Add dataclasses.is_dataclass(obj), which returns True if obj is a dataclass or an instance of one. (#5113)

This commit is contained in:
Eric V. Smith 2018-01-06 12:41:53 -05:00 committed by GitHub
parent 3cd7c6e6eb
commit e7ba013d87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 36 additions and 22 deletions

View File

@ -16,6 +16,7 @@ __all__ = ['dataclass',
'astuple', 'astuple',
'make_dataclass', 'make_dataclass',
'replace', 'replace',
'is_dataclass',
] ]
# Raised when an attempt is made to modify a frozen class. # Raised when an attempt is made to modify a frozen class.
@ -615,11 +616,17 @@ def fields(class_or_instance):
return tuple(f for f in fields.values() if f._field_type is _FIELD) return tuple(f for f in fields.values() if f._field_type is _FIELD)
def _isdataclass(obj): def _is_dataclass_instance(obj):
"""Returns True if obj is an instance of a dataclass.""" """Returns True if obj is an instance of a dataclass."""
return not isinstance(obj, type) and hasattr(obj, _MARKER) return not isinstance(obj, type) and hasattr(obj, _MARKER)
def is_dataclass(obj):
"""Returns True if obj is a dataclass or an instance of a
dataclass."""
return hasattr(obj, _MARKER)
def asdict(obj, *, dict_factory=dict): def asdict(obj, *, dict_factory=dict):
"""Return the fields of a dataclass instance as a new dictionary mapping """Return the fields of a dataclass instance as a new dictionary mapping
field names to field values. field names to field values.
@ -639,12 +646,12 @@ def asdict(obj, *, dict_factory=dict):
dataclass instances. This will also look into built-in containers: dataclass instances. This will also look into built-in containers:
tuples, lists, and dicts. tuples, lists, and dicts.
""" """
if not _isdataclass(obj): if not _is_dataclass_instance(obj):
raise TypeError("asdict() should be called on dataclass instances") raise TypeError("asdict() should be called on dataclass instances")
return _asdict_inner(obj, dict_factory) return _asdict_inner(obj, dict_factory)
def _asdict_inner(obj, dict_factory): def _asdict_inner(obj, dict_factory):
if _isdataclass(obj): if _is_dataclass_instance(obj):
result = [] result = []
for f in fields(obj): for f in fields(obj):
value = _asdict_inner(getattr(obj, f.name), dict_factory) value = _asdict_inner(getattr(obj, f.name), dict_factory)
@ -678,12 +685,12 @@ def astuple(obj, *, tuple_factory=tuple):
tuples, lists, and dicts. tuples, lists, and dicts.
""" """
if not _isdataclass(obj): if not _is_dataclass_instance(obj):
raise TypeError("astuple() should be called on dataclass instances") raise TypeError("astuple() should be called on dataclass instances")
return _astuple_inner(obj, tuple_factory) return _astuple_inner(obj, tuple_factory)
def _astuple_inner(obj, tuple_factory): def _astuple_inner(obj, tuple_factory):
if _isdataclass(obj): if _is_dataclass_instance(obj):
result = [] result = []
for f in fields(obj): for f in fields(obj):
value = _astuple_inner(getattr(obj, f.name), tuple_factory) value = _astuple_inner(getattr(obj, f.name), tuple_factory)
@ -751,7 +758,7 @@ def replace(obj, **changes):
# We're going to mutate 'changes', but that's okay because it's a new # We're going to mutate 'changes', but that's okay because it's a new
# dict, even if called with 'replace(obj, **my_changes)'. # dict, even if called with 'replace(obj, **my_changes)'.
if not _isdataclass(obj): if not _is_dataclass_instance(obj):
raise TypeError("replace() should be called on dataclass instances") raise TypeError("replace() should be called on dataclass instances")
# It's an error to have init=False fields in 'changes'. # It's an error to have init=False fields in 'changes'.

View File

@ -1,6 +1,6 @@
from dataclasses import ( from dataclasses import (
dataclass, field, FrozenInstanceError, fields, asdict, astuple, dataclass, field, FrozenInstanceError, fields, asdict, astuple,
make_dataclass, replace, InitVar, Field, MISSING make_dataclass, replace, InitVar, Field, MISSING, is_dataclass,
) )
import pickle import pickle
@ -1365,27 +1365,32 @@ class TestCase(unittest.TestCase):
self.assertIs(C().x, int) self.assertIs(C().x, int)
def test_isdataclass(self): def test_is_dataclass(self):
# There is no isdataclass() helper any more, but the PEP class NotDataClass:
# describes how to write it, so make sure that works. Note pass
# that this version returns True for both classes and
# instances.
def isdataclass(obj):
try:
fields(obj)
return True
except TypeError:
return False
self.assertFalse(isdataclass(0)) self.assertFalse(is_dataclass(0))
self.assertFalse(isdataclass(int)) self.assertFalse(is_dataclass(int))
self.assertFalse(is_dataclass(NotDataClass))
self.assertFalse(is_dataclass(NotDataClass()))
@dataclass @dataclass
class C: class C:
x: int x: int
self.assertTrue(isdataclass(C)) @dataclass
self.assertTrue(isdataclass(C(0))) class D:
d: C
e: int
c = C(10)
d = D(c, 4)
self.assertTrue(is_dataclass(C))
self.assertTrue(is_dataclass(c))
self.assertFalse(is_dataclass(c.x))
self.assertTrue(is_dataclass(d.d))
self.assertFalse(is_dataclass(d.e))
def test_helper_fields_with_class_instance(self): def test_helper_fields_with_class_instance(self):
# Check that we can call fields() on either a class or instance, # Check that we can call fields() on either a class or instance,

View File

@ -0,0 +1,2 @@
Add dataclasses.is_dataclass(obj), which returns True if obj is a dataclass
or an instance of one.