From e7ba013d870012157f695ead7e3645c2828a7fc5 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Sat, 6 Jan 2018 12:41:53 -0500 Subject: [PATCH] bpo-32499: Add dataclasses.is_dataclass(obj), which returns True if obj is a dataclass or an instance of one. (#5113) --- Lib/dataclasses.py | 19 +++++++--- Lib/test/test_dataclasses.py | 37 +++++++++++-------- .../2018-01-06-10-54-16.bpo-32499.koyY-4.rst | 2 + 3 files changed, 36 insertions(+), 22 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2018-01-06-10-54-16.bpo-32499.koyY-4.rst diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index eaaed63ef28..b4786bf502e 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -16,6 +16,7 @@ __all__ = ['dataclass', 'astuple', 'make_dataclass', 'replace', + 'is_dataclass', ] # 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) -def _isdataclass(obj): +def _is_dataclass_instance(obj): """Returns True if obj is an instance of a dataclass.""" 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): """Return the fields of a dataclass instance as a new dictionary mapping 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: tuples, lists, and dicts. """ - if not _isdataclass(obj): + if not _is_dataclass_instance(obj): raise TypeError("asdict() should be called on dataclass instances") return _asdict_inner(obj, dict_factory) def _asdict_inner(obj, dict_factory): - if _isdataclass(obj): + if _is_dataclass_instance(obj): result = [] for f in fields(obj): value = _asdict_inner(getattr(obj, f.name), dict_factory) @@ -678,12 +685,12 @@ def astuple(obj, *, tuple_factory=tuple): tuples, lists, and dicts. """ - if not _isdataclass(obj): + if not _is_dataclass_instance(obj): raise TypeError("astuple() should be called on dataclass instances") return _astuple_inner(obj, tuple_factory) def _astuple_inner(obj, tuple_factory): - if _isdataclass(obj): + if _is_dataclass_instance(obj): result = [] for f in fields(obj): 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 # 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") # It's an error to have init=False fields in 'changes'. diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py index ed695639882..fca384d8c3c 100755 --- a/Lib/test/test_dataclasses.py +++ b/Lib/test/test_dataclasses.py @@ -1,6 +1,6 @@ from dataclasses import ( dataclass, field, FrozenInstanceError, fields, asdict, astuple, - make_dataclass, replace, InitVar, Field, MISSING + make_dataclass, replace, InitVar, Field, MISSING, is_dataclass, ) import pickle @@ -1365,27 +1365,32 @@ class TestCase(unittest.TestCase): self.assertIs(C().x, int) - def test_isdataclass(self): - # There is no isdataclass() helper any more, but the PEP - # describes how to write it, so make sure that works. Note - # that this version returns True for both classes and - # instances. - def isdataclass(obj): - try: - fields(obj) - return True - except TypeError: - return False + def test_is_dataclass(self): + class NotDataClass: + pass - self.assertFalse(isdataclass(0)) - self.assertFalse(isdataclass(int)) + self.assertFalse(is_dataclass(0)) + self.assertFalse(is_dataclass(int)) + self.assertFalse(is_dataclass(NotDataClass)) + self.assertFalse(is_dataclass(NotDataClass())) @dataclass class C: x: int - self.assertTrue(isdataclass(C)) - self.assertTrue(isdataclass(C(0))) + @dataclass + 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): # Check that we can call fields() on either a class or instance, diff --git a/Misc/NEWS.d/next/Library/2018-01-06-10-54-16.bpo-32499.koyY-4.rst b/Misc/NEWS.d/next/Library/2018-01-06-10-54-16.bpo-32499.koyY-4.rst new file mode 100644 index 00000000000..bf3e99c8d86 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-01-06-10-54-16.bpo-32499.koyY-4.rst @@ -0,0 +1,2 @@ +Add dataclasses.is_dataclass(obj), which returns True if obj is a dataclass +or an instance of one.