Creating dynamic docstrings in Python descriptor
Asked Answered
B

1

9

I am trying to generate some class definitions dynamically (for wrapping a C++ extension). The following descriptor works fine except when I try to access the docstring for a field using help(), it gives default documentation for the descriptor rather than the field it self. However when I do help(classname), it retrieves the docstring passed to the descriptor:

class FieldDescriptor(object):
    def __init__(self, name, doc='No documentation available.'):
        self.name = name
        self.__doc__ = doc

    def __get__(self, obj, dtype=None):
        if obj is None and dtype is not None:
            print 'Doc is:', self.__doc__
            return self
        return obj.get_field(self.name)

    def __set__(self, obj, value):
        obj.set_field(self.name, value)

class TestClass(object):
    def __init__(self):
        self.fdict = {'a': None, 'b': None}

    def get_field(self, name):
        return self.fdict[name]

    def set_field(self, name, value):
        self.fdict[name] = value

fields = ['a', 'b']
def define_class(class_name, baseclass):
    class_obj = type(class_name, (baseclass,), {})
    for field in fields:
        setattr(class_obj, field, FieldDescriptor(field, doc='field %s in class %s' % (field, class_name)))
    globals()[class_name] = class_obj


if __name__ == '__main__':
    define_class('DerivedClass', TestClass)
    help(DerivedClass.a)
    help(DerivedClass)
    v = DerivedClass()
    help(v.a)

"python test.py" prints:

Doc is: field a in class DerivedClass
Help on FieldDescriptor in module __main__ object:

class FieldDescriptor(__builtin__.object)
 |  Methods defined here:
 |  
 |  __get__(self, obj, dtype=None)
 |  
 |  __init__(self, name, doc='No documentation available.')
 |  
 |  __set__(self, obj, value)
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)

Doc is: field a in class DerivedClass
Doc is: field b in class DerivedClass
Help on class DerivedClass in module __main__:

class DerivedClass(TestClass)
 |  Method resolution order:
 |      DerivedClass
 |      TestClass
 |      __builtin__.object
 |  
 |  Data descriptors defined here:
 |  
 |  a
 |      field a in class DerivedClass
 |  
 |  b
 |      field b in class DerivedClass
 |  
 |  ----------------------------------------------------------------------
 |  Methods inherited from TestClass:
 |  
 |  __init__(self)
 |  
 |  get_field(self, name)
 |  
 |  set_field(self, name, value)
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors inherited from TestClass:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)

Help on NoneType object:

class NoneType(object)
 |  Methods defined here:
 |  
 |  __hash__(...)
 |      x.__hash__()  hash(x)
 |  
 |  __repr__(...)
 |      x.__repr__()  repr(x)

Any idea how one can get the descriptor.__doc__ for help(class.field) ? And is there a way to bypass this and have something like a getter function for doc in stead of having to store the doc string in the descriptor?

like:

class FieldDescriptor(object):
    def __init__(self, name, doc='No documentation available.'):
        self.name = name
        self.__doc__ = doc

    def __get__(self, obj, dtype=None):
        if obj is None and dtype is not None:
            print 'Doc is:', self.__doc__
            return self
        return obj.get_field(self.name)

    def __set__(self, obj, value):
        obj.set_field(self.name, value)

    # This is what I'd like to have
    def __doc__(self, obj, dtype):
       return dtype.generate_docstring(self.name)

UPDATE: Actually I started with this definition of __get__:

def __get__(self, obj, dtype=None):
    return obj.get_field(self.name)

The problem with this was that when I said:

help(DerivedClass.a)

Python threw an Exception indicating that I was trying to call None.get_field. Thus help() is calling the __get__ method with obj=None and dtype=DerivedClass. That is why I decided to return the FieldDescriptor instance when obj=None and dtype!=None. My impression was help(xyz) tries to display xyz.__doc__. By that logic, if __get__ returns descriptor_instance, then descriptor_instance.__doc__ should be printed by help(), which is the case for the whole class [help(DerivedClass)], but not for the single field [help(DerivedClass.a)].

Bugloss answered 6/4, 2012 at 14:53 Comment(4)
I'm sure it's all there, but could you clarify which calls are giving you the wrong help output? It's too much effort to guess what you expected by reading the code.Refugee
As jsbueno pointed out, it is help(DerivedClass.a) that displays the documentation for the descriptor instead of documentation for the field (saved in descriptor.__doc__).Bugloss
@Bugloss Did you ever find a satisfactory answer?Stinko
@Jérémie As far as I remember, and mentioned in my comment to the jsbueno's answer, it seemed to be due to the way python's builtin help implementation works. I ended up writing a custom help function for this. It was part of a rather complex project, but the code is here: github.com/BhallaLab/moose-core/blob/master/python/moose/…. The Python/C++ interface code is in pymoose directory of the same repo.Bugloss
S
2

What goes on is that when you request help(DerivedClass.a) - python calculates the expression inside the parentheses - which is the object returned by the descriptor's __get__ method - and them searchs for the help (including docstring) on that object.

A way to have this working, including the dynamic docstring generation, is to have your __get__ method to retudn a dynamically generated object that features the desired doc string. But this object would itself need to be a proper proxy object to the original one, and would create some overhead on your code - and a lot of special cases.

Anyway, the only way to get it working ike you want is to modify the objects returned by __get__ itself, so that they behave like you'd like them to.

Id suggest that if all you want in the help is a bit of information like you are doing, maybe you want the objects returned from your __get__ to be of a class that define a __repr__ method (rather than just a __doc__ string) .

Stallfeed answered 6/4, 2012 at 16:42 Comment(1)
The __repr__ approach does not work. In stead, it prints the documentation of the class that implements the __repr__. Using properties instead of ordinary descriptors works, but that suffers from storing the docstring statically. Tracing the help function with pdb revealed that the tests in pydoc.help() are agnostic of descriptors defined in Python, though they take care of property and various descriptors defined using the C API. Thanks for your input anyways, it encouraged me to try other ways.Bugloss

© 2022 - 2024 — McMap. All rights reserved.