From: Vratislav Podzimek <vpodzime(a)redhat.com>
namedtuple classes are great as pure data objects one can store things in and
then access them really easily. The problem is that if a new field is added to a
namedtuple, all places creating instances of it has to be modified to provide a
value for that new field. The default_namedtuple class constructor implemented
in this commit constructs namedtuples with default values for (some) fields and
fields without any value passed defaulting to None.
Such default namedtuple classes are still great for storing data and
e.g. passing data to functions/methods grouped into a few objects (like
DeviceInfo, FormatInfo,...) passed as arguments, but allow us to add extra
fields in the future (e.g. a new device attribute to DeviceInfo) without
breaking the API.
---
blivet/util.py | 41 +++++++++++++++++++++++++++++++++++++++++
tests/util_test.py | 23 +++++++++++++++++++++++
2 files changed, 64 insertions(+)
diff --git a/blivet/util.py b/blivet/util.py
index f23ea83..1583fc1 100644
--- a/blivet/util.py
+++ b/blivet/util.py
@@ -15,6 +15,7 @@
from decimal import Decimal
from contextlib import contextmanager
from functools import wraps
+from collections import namedtuple
import gi
gi.require_version("BlockDev", "1.0")
@@ -805,3 +806,43 @@ def the_func(*args, **kwargs):
return the_func
return deprecate_func
+
+def default_namedtuple(name, fields, doc=""):
+ """Create a namedtuple class
+
+ The difference between a namedtuple class and this class is that default
+ values may be specified for fields and fields with missing values on
+ initialization being initialized to None.
+
+ :param str name: name of the new class
+ :param fields: field descriptions - an iterable of either "name" or
("name", default_value)
+ :type fields: list of str or (str, object) objects
+ :param str doc: the docstring for the new class (should at least describe the
meanings and
+ types of fields)
+
+ """
+ field_names = list()
+ for field in fields:
+ if isinstance(field, tuple):
+ field_names.append(field[0])
+ else:
+ field_names.append(field)
+ nt = namedtuple(name, field_names)
+
+ class TheDefaultNamedTuple(nt):
+ if doc:
+ __doc__ = doc
+ def __new__(cls, *args, **kwargs):
+ args_list = list(args)
+ sorted_kwargs = sorted(kwargs.keys(), key=lambda x: field_names.index(x))
+ for i in range(len(args), len(field_names)):
+ if field_names[i] in sorted_kwargs:
+ args_list.append(kwargs[field_names[i]])
+ elif isinstance(fields[i], tuple):
+ args_list.append(fields[i][1])
+ else:
+ args_list.append(None)
+
+ return nt.__new__(cls, *args_list)
+
+ return TheDefaultNamedTuple
diff --git a/tests/util_test.py b/tests/util_test.py
index e47e252..f2eb6d0 100644
--- a/tests/util_test.py
+++ b/tests/util_test.py
@@ -21,3 +21,26 @@ def test_power_of_two(self):
self.assertTrue(util.power_of_two(2 ** i), msg=i)
self.assertFalse(util.power_of_two(2 ** i + 1), msg=i)
self.assertFalse(util.power_of_two(2 ** i - 1), msg=i)
+
+class TestDefaultNamedtuple(unittest.TestCase):
+ def test_default_namedtuple(self):
+ TestTuple = util.default_namedtuple("TestTuple", ["x",
"y", ("z", 5), "w"])
+ dnt = TestTuple(1, 2, 3, 6)
+ self.assertEqual(dnt.x, 1)
+ self.assertEqual(dnt.y, 2)
+ self.assertEqual(dnt.z, 3)
+ self.assertEqual(dnt.w, 6)
+ self.assertEqual(dnt, (1, 2, 3, 6))
+
+ dnt = TestTuple(1, 2, 3, w=6)
+ self.assertEqual(dnt.x, 1)
+ self.assertEqual(dnt.y, 2)
+ self.assertEqual(dnt.z, 3)
+ self.assertEqual(dnt.w, 6)
+ self.assertEqual(dnt, (1, 2, 3, 6))
+
+ dnt = TestTuple(z=3, x=2, w=1, y=5)
+ self.assertEqual(dnt, (2, 5, 3, 1))
+
+ dnt = TestTuple()
+ self.assertEqual(dnt, (None, None, 5, None))
--
To view this commit on github, visit
https://github.com/rhinstaller/blivet/commit/af5816bb8ab507ea1e94c1df0a96...