Search code examples
pythonpydoc

Adding docstring to __slots__ descriptor?


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?


Solution

  • 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)'