11

I have a module that should have a @property, I solved this by setting a class as the module. I got the idea from this answer: Lazy module variables--can it be done?

I wanted this to be repeatable and easy to use so I made a metaclass for it. This works like a charm.

The problem is that when using Sphinx to generate documentation properties don't get documented. Everything else is documented as expected. I have no idea how to fix this, maybe this is a problem with Sphinx?

The module:

import sys
import types

class ClassAsModule(type):
    def __new__(cls, name, bases, attrs):
        # Make sure the name of the class is the module name.
        name = attrs.pop('__module__')
        # Create a class.
        cls = type.__new__(cls, name, bases, attrs)
        # Instantiate the class and register it.
        sys.modules[name] = cls = cls(name)
        # Update the dict so dir works properly
        cls.__dict__.update(attrs)

class TestClass(types.ModuleType):
    """TestClass docstring."""
    __metaclass__ = ClassAsModule
    @property
    def some_property(self):
        """Property docstring."""
        pass
    def meth():
        """meth doc"""
        pass

And a copy-paste to generate/view Sphinx documentation:

sphinx-apidoc . -o doc --full
sphinx-build doc html
xdg-open html/module.html

The most essential part is to document the class' properties. Bonus points to also document original module members.

EDIT: The class should be documented as the module it is in. The class is used this way and should thus appear this way in Sphinx.

Example of desired output:

Module Foo
    TestClass docstring.

    some_property
        Property docstring.

    meth()
        meth doc

EDIT 2: I found something that may aid in finding a solution. When having a regular module foo with the following content:

#: Property of foo
prop = 'test'

Sphinx documents this like:

foo.prop = 'test'
    Property of foo

The same works if prop is an attribute of a class. I haven't figured out why it doesn't work in my special case.

Community
  • 1
  • 1
siebz0r
  • 14,765
  • 11
  • 55
  • 100
  • Your code does not work. `ModMeta` isn't defined. Could you please post working code? – jterrace May 22 '13 at 02:02
  • @jterrace Copy-paste fail. Is fixed now ;-) – siebz0r May 22 '13 at 04:19
  • Deleted my answer because your original code had `__metaclass_` instead of `__metaclass__`, causing it not to work. – jterrace May 22 '13 at 16:05
  • Are you sure this actually works the way you think it does? The methods/properties seem to have no access to the module namespace. For example, if I replace the `pass` statement in `meth()` with `return sys.path` I get this: `AttributeError: 'NoneType' object has no attribute 'path'` Works fine if I just return "Hello" or something like that but not for accessing what should be globals. – Jacinda May 23 '13 at 20:52
  • Do you see the same behavior as me? – Jacinda May 29 '13 at 06:35
  • @Jacinda Yeah, oddly I'm seeing the same behavior. Never noticed it before. (I've only used builtins for the class.) The `globals()` looks like this (without `__builtins__`): `{'__file__': None, 'ClassAsModule': None, '__package__': None, 'sys': None, 'TestClass': None, '__name__': None, '__doc__': None, 'types': None}` – siebz0r May 29 '13 at 06:54
  • I think it's related to the source of the problem, but I haven't quite connected all the dots yet. – Jacinda May 29 '13 at 17:51

2 Answers2

2

Here's my understanding.

The theory is: making a mutant your class act like a module this (a bit hacky) way makes sphinx think that he doesn't need (to parse) properties from modules (because it's a class-level paradigm). So, for sphinx, TestClass is a module.

First of all, to make sure that the culprit is the code for making a class act like a module - let's remove it:

class ClassAsModule(type):
    pass

we'll see in docs:

package Package
    script Module

    class package.script.ClassAsModule
        Bases: type

    class package.script.TestClass
        Bases: module

        TestClass docstring.

        meth()
            meth doc

        some_property
            Property docstring.

As you see, sphinx read the property without any problems. Nothing special here.


Possible solution for your problem is to avoid using @property decorator and replace it with calling property class constructor. E.g.:

import sys
import types

class ClassAsModule(type):
    def __new__(cls, name, bases, attrs):
        # Make sure the name of the class is the module name.
        name = attrs.pop('__module__')
        # Create a class.
        cls = type.__new__(cls, name, bases, attrs)
        # Instantiate the class and register it.
        sys.modules[name] = cls = cls(name)
        # Update the dict so dir works properly
        cls.__dict__.update(attrs)


class TestClass(types.ModuleType):
    """TestClass docstring."""
    __metaclass__ = ClassAsModule

    def get_some_property(self):
        """Property docstring."""
        pass

    some_property = property(get_some_property)

    def meth(self):
        """meth doc"""
        pass

For this code sphinx generates:

package Package
    script Module
        TestClass docstring.

            package.script.get_some_property(self)
                Property docstring.

            package.script.meth(self)
                meth doc

May be the answer is a piece of nonsense, but I hope it'll point you to the right direction.

alecxe
  • 414,977
  • 106
  • 935
  • 1,083
  • Sadly this is missing the point of having the usage of the properties and the documentation in sync. – siebz0r May 27 '13 at 11:04
  • Yeah, but, it's important to understand that `@property` decorator is just a syntax sugar. Defining properties via instantiating `property` class as I've suggested works actually the same way as using the decorator. But, sure, in this case, documentation shows somewhat a bit different picture. – alecxe May 27 '13 at 11:08
  • I'm awarding you the bounty because else nobody will receive those points. I won't get a refund and I don't expect an accepted answer soon so it seemed like the 'best' thing to do. – siebz0r May 29 '13 at 06:51
  • Thank you! Do you think it would be fair if I start a bounty on this question? Or we should wait a bit? – alecxe May 29 '13 at 08:29
  • I'm fine with either choice. ;-) The last bounty didn't have the desired result, so I'm planning on waiting. – siebz0r May 29 '13 at 08:48
  • Ok, let's just wait for a while. Just let me know when and I'll start a bounty - this seems fair. Thanks. – alecxe May 29 '13 at 09:03
0

The way I've found that works best is to keep the file contents the same as if you were writing a regular module, then at the end replace the embryonic module in sys.modules:

"""Module docstring.  """

import sys
import types

def _some_property(self):
    pass
some_property = property(_some_property)
"""Property docstring."""

def meth():
    """meth doc"""
    pass

def _make_class_module(name):
    mod = sys.modules[name]
    cls = type('ClassModule', (types.ModuleType,), mod.__dict__)
    clsmod = cls(name)
    clsmod.__dict__.update(mod.__dict__)
    clsmod.__wrapped__ = mod
    sys.modules[name] = clsmod
_make_class_module(__name__)

Text documentation:

mymod Module
************

Module docstring.

mymod.meth()

   meth doc

mymod.some_property = None

   Property docstring.

For the version of Sphinx I'm using (v1.1.3), it looks like you have to apply the property constructor explicitly (you can't use it as a decorator), and the docstring has to go in the file at the top level, on the line after the constructor call that creates the property (it doesn't work as a docstring inside the property getter). The source is still fairly readable, though.

ecatmur
  • 137,771
  • 23
  • 263
  • 343