1

A small add-on to python 3.9 was that

Changed in version 3.9: Class methods can now wrap other descriptors such as property().

Which is useful in several contexts, for example as a solution for Using property() on classmethods or in order to create what is essentially a lazily evaluated class-attribute via the recipe

class A:
   @classmethod
   @property
   @cache
   def lazy_class_attribute(cls):
       """Method docstring."""
       return expensive_computation(cls)

This works well and fine within python, importing the class, instantiating it or subclasses of it will not cause expensive_computation to occur, however it seems that both pydoc and sphinx will not only cause execution expensive_computation when they try to obtain the docstring, but not display any docstring for this @classmethod whatsoever.

Question: Is it possible - from within python (*) - to have lazily evaluated class-attributes/properties that do not get executed when building documentation?

(*) One workaround presented in Stop Sphinx from Executing a cached classmethod property, thanks to /u/jsbueno, consists of modifying the function body based on an environment variable:

def lazy_class_attribute(cls):
   """Method docstring."""
    if os.environ.get("GENERATING_DOCS", False):
        return
   return expensive_computation(cls)

I like this workaround a lot, since in particular, it allows one to present a different output when documenting. (For example, my classes have attributes which are paths based on the class-name. If a different user executes the same script, the path will be different since their home folder is different.)

There are 2 problems, however:

  1. This approach depends on doing things outside of python that, imo, should, in an ideal world, not be necessary to do outside of python itself
  2. To get the docstring in documentation, it seems we end up having to do something unintuitive like
class MetaClass:
    @property
    @cache
    def _expensive_function(cls):
        """some expensive function"""

class BaseClass(metaclass=MetaClass):
    lazy_attribute: type = classmethod(MetaClass._expensive_funcion)
    """lazy_attribute docstring"""

enter image description here

PS: By the way, is there any functional difference between a @classmethod@property and an attribute? They seem very similar. The only difference I can make out at the moment is that if the attribute needs access to other @classmethods, we need to move everything in the metaclass as above.

Hyperplane
  • 1,422
  • 1
  • 14
  • 28

0 Answers0