So maybe it's not quite a mutable namedtuple. However, I borrowed heavily against the namedtuple implementation for this one.
I wanted to have a collection of classes that described data types without any functionality on them, sort of state holders. However, I wanted to use defaults and I wanted it to be mutable.
I noticed, as I started building my classes, that each was following the same pattern, so I extracted it out into this recipe. Wrapping namedtuple to get the same result would probably be feasible, but I enjoyed doing this too.
The distinction between parameters and properties is mostly one I was maintaining between single objects and collections. Realistically everything could have been parameters.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | from collections import namedtuple
Param = namedtuple("Param", "name default doc")
ReqParam = namedtuple("ReqParam", "name doc")
Getter = namedtuple("Getter", "name initial doc")
PROPERTY_TEMPLATE = """\
@property
def {name}(self):
"{doc}"
if not hasattr(self, "_{name}"):
self._{name} = {initial}
return self._{name}"""
def dataclass(typename, doc, params, properties, verbose=False):
"Return a new class around the params and properties."
namespace = {}
# handle class docstring
if not doc:
doc = ""
docstring = doc.splitlines()
if not docstring:
docstring = ['""']
elif len(docstring) > 1:
docstring.insert(0, '"""')
docstring.append('"""')
docstring.extend(line for line in doc.splitlines)
else:
docstring = ['"{}"'.format(docstring[0])]
# handle __init__ and params
params_constant = []
__init__ = []
if params:
params_constant = ["PARAMS = ("]
doc = []
assignment = []
for param in params:
params_constant.append(" {}{},".format(
param.__class__.__name__,
param.__getnewargs__(),
))
namespace[param.__class__.__name__] = param.__class__
default = ""
if hasattr(param, "default"):
default = param.default
if hasattr(default, "__name__"):
namespace[default.__name__] = default
default = default.__name__
default = "={}".format(default)
__init__.append(" {}{},".format(param.name, default))
if param.doc:
doc.append(" {} - {}".format(param.name, param.doc))
assignment.append("self.{} = {}".format(param.name, param.name))
__init__.append("):")
params_constant.append(" )")
if doc:
__init__.append('"""')
__init__.append("Parameters:")
__init__.extend(doc)
__init__.append("")
__init__.append('"""')
__init__.append("")
__init__.extend(assignment)
__init__ = [" "+line for line in __init__]
__init__.insert(0, "def __init__(self,")
# handle properties
properties_constant = []
props = []
for prop in properties:
properties_constant.append(" {}{},".format(
prop.__class__.__name__,
prop.__getnewargs__(),
))
namespace[prop.__class__.__name__] = prop.__class__
initial = prop.initial
if hasattr(initial, "__name__"):
namespace[initial.__name__] = initial
initial = initial.__name__
prop = PROPERTY_TEMPLATE.format(
name=prop.name,
initial=initial,
doc=prop.doc,
)
props.extend(prop.splitlines())
props.append("")
if properties_constant:
properties_constant.insert(0, "PROPERTIES = (")
properties_constant.append(" )")
# put it all together
template = ["class {}(object):".format(typename)]
template.extend(" "+line for line in docstring)
template.extend(" "+line for line in params_constant)
template.extend(" "+line for line in properties_constant)
template.append("")
template.extend(" "+line for line in __init__)
template.append("")
template.extend(" "+line for line in props)
template.append("")
template = "\n".join(template)
if verbose:
print(template)
try:
exec(template, namespace)
#except SyntaxError, e:
except SyntaxError as e:
raise SyntaxError(e.msg + ':\n' + template)
return namespace[typename]
def data(cls):
"A class decorator for the dataclass function."
return dataclass(cls.__name__, cls.__doc__, cls.PARAMS, cls.PROPERTIES)
|
Here is an example:
@data
class User:
PARAMS = (
ReqParam("username", ""),
Param("password", None, ""),
)
PROPERTIES = (
Getter("email_addresses", [], "All addresses pointing to this user."),
)
And here is another:
class EmailAddress:
"A prototypical email address."
PARAMS = (
ReqParam("address", "the actual address"),
)
PROPERTIES = (
Getter("users", [], "Target users for this address."),
)
EmailAddress = dataclass(
EmailAddress.__name__,
EmailAddress.__doc__,
EmailAddress.PARAMS,
EmailAddress.PROPERTIES,
True,
)
One thing I had considered is expanding on this to have all the names be descriptors, and have get/set/del handlers registered against the classes, tied to the descriptors. But that is something for another day. For now anything like that will just be handled more directly in superclasses.