Skip to content

Commit

Permalink
Add a class constructor for namedtuples with default values
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
vpodzime committed Nov 5, 2015
1 parent 918d0bd commit 9d82239
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 0 deletions.
43 changes: 43 additions & 0 deletions blivet/util.py
Expand Up @@ -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")
Expand Down Expand Up @@ -861,3 +862,45 @@ 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)
:returns: a new default namedtuple class
:rtype: type
"""
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
23 changes: 23 additions & 0 deletions tests/util_test.py
Expand Up @@ -22,3 +22,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))

0 comments on commit 9d82239

Please sign in to comment.