I'm writing a storage automation module that provisions volumes. Instead of passing the half dozen or more arguments needed to actually create a volume on the storage controller, I created a parameter class using __slots__
that is passed into the create method like this:
from mock import Mock
from six import with_metaclass
class VolumeParameterMeta(type):
def __new__(mcs, name, bases, dct):
# set __slots__ docstrings here?
return super(VolumeParameterMeta, mcs).__new__(mcs, name, bases, dct)
class VolumeParameter(with_metaclass(VolumeParameterMeta, object)):
__slots__ = ('name', 'size', 'junctionPath', 'svmName', 'securityStyle'
'spaceReserve')
def __init__(self, name):
self.name = name
class Volume(object):
def __init__(self, driver, name):
self.driver = driver
self.name = name
@classmethod
def create(cls, driver, param):
# do sanity check on param (volume not too large, etc)
driver.provision(param)
return cls(driver, param.name)
if __name__ == '__main__':
param = VolumeParameter('volume1')
param.svmName = 'vserver1'
param.junctionPath = '/some/path'
param.size = 2 ** 30
param.spaceReserve = param.size * 0.1
param.securityStyle = 'mixed'
volume = Volume.create(driver=Mock(), param=param)
The above example works great with one small exception. It's not obvious how to add docstrings to the descriptors in the parameter class. It seems like it should be possible with a metaclass, but the descriptors aren't defined when the metaclass is instantiated.
I'm keenly aware that some may disagree with my using a side-effect of __slots__
, but I like that it helps eliminate typos. Try to set a parameter that doesn't exist and boom, AttributeError
is raised. All without any boilerplate code. It's arguably more Pythonic to let it fail at volume creation time, but the result would be that a default is used instead of an incorrectly spelled parameter. It would effectively be a silent failure.
I realize it's possible to simply expand the parameter class docstring, but the result of pydoc
and other documentation building scripts is a large free-form docstring for the class and empty docstrings for each of the descriptors.
Surely there must be a way to define docstrings for the descriptors created by __slots__
? Perhaps there's another way apart from slots? A mutable namedtuple
or similar?
No, there is no option to add docstrings to the descriptor objects that are generated for names defined in __slots__
. They are like regular non-descriptor attributes in that regard.
On a regular object without slots, you can, at most, add comments and set default values:
class VolumeParameter(object):
# the name of the volume (str)
name = ''
# volume size in bytes (int)
size = 0
# ...
The same still applies even if you declared all those attributes as __slots__
.
You could 'wrap' each slot in another descriptor in the form of a property
object:
class VolumeParameterMeta(type):
@staticmethod
def _property_for_name(name, docstring):
newname = '_' + name
def getter(self):
return getattr(self, newname)
def setter(self, value):
setattr(self, newname, value)
def deleter(self):
delattr(self, newname)
return newname, property(getter, setter, deleter, docstring)
def __new__(mcs, name, bases, dct):
newslots = []
clsslots = dct.pop('__slots__', ())
slotdocs = dct.pop('__slot_docs__', {})
if isinstance(clsslots, str):
clsslots = clsslots.split()
for name in clsslots:
newname, prop = mcs._property_for_name(name, slotdocs.get(name))
newslots.append(newname)
dct[name] = prop
if newslots:
dct['__slots__'] = tuple(newslots)
return super(VolumeParameterMeta, mcs).__new__(mcs, name, bases, dct)
class VolumeParameter(with_metaclass(VolumeParameterMeta, object)):
__slots__ = ('name', 'size', 'junctionPath', 'svmName', 'securityStyle'
'spaceReserve')
__slot_docs__ = {
'name': 'the name of the volume (str)',
# ...
}
Note that this almost doubles the number of descriptors on the class:
>>> pprint(VolumeParameter.__dict__)
mappingproxy({'__doc__': None,
'__module__': '__main__',
'__slots__': ('_name',
'_size',
'_junctionPath',
'_svmName',
'_securityStylespaceReserve'),
'_junctionPath': <member '_junctionPath' of 'securityStylespaceReserve' objects>,
'_name': <member '_name' of 'securityStylespaceReserve' objects>,
'_securityStylespaceReserve': <member '_securityStylespaceReserve' of 'securityStylespaceReserve' objects>,
'_size': <member '_size' of 'securityStylespaceReserve' objects>,
'_svmName': <member '_svmName' of 'securityStylespaceReserve' objects>,
'junctionPath': <property object at 0x105edbe08>,
'name': <property object at 0x105edbd68>,
'securityStylespaceReserve': <property object at 0x105edb098>,
'size': <property object at 0x105edbdb8>,
'svmName': <property object at 0x105edb048>})
but now your properties at least have docstrings:
>>> VolumeParameter.name.__doc__
'the name of the volume (str)'