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.
Download
Copy to clipboard