Showing posts with label decorator. Show all posts
Showing posts with label decorator. Show all posts

Thursday, February 16, 2017

Templating Other Entities

Today's post will be long from a scrolling standpoint, but since there's not much meat to the template-code, it should go pretty quickly. The goal for today is to generate final templates/snippets for interface, abstract class, class and final class constructs that can be used as starting-points for defining those types in project-code without having to start from scratch every single time. If you cringed, or started to think out a non-Pythonic-themed comment when you saw final, stop right now, and read this first.

I'm going to tackle them in the order noted above.

The Interface Template

These cannot be instantiated, and must be extended into concrete implementations. In most languages that I'm aware of, an interface definition cannot have properties (not even abstract ones), and cannot have concrete method-implementations. Python doesn't have that constraint (it doesn't have a formal interface-construct, but allows for abstract property- and method-definition through the abc module, using its ABCMeta as a __metaclass__ specification to define a class that has some abstract members, and the @abstractmethod and @abstractproperty decorator-functions to define which members are abstract.

@describe.InitClass()
class InterfaceName( object ):
    """TODO: Document InterfaceName
Provides interface requirements and type-identity for objects that can 
REPRESENT SOMETHING"""

    #-----------------------------------#
    # Abstraction through abc.ABCMeta   #
    #-----------------------------------#
    __metaclass__ = abc.ABCMeta

    #-----------------------------------#
    # Static interface attributes (and  #
    # default values?)                  #
    #-----------------------------------#

    #-----------------------------------#
    # Abstract Properties               #
    #-----------------------------------#

#    PropertyName = abc.abstractproperty()

    #-----------------------------------#
    # Instance Initializer              #
    #-----------------------------------#
    @describe.AttachDocumentation()
    @describe.todo( 'Document __init__' )
    @describe.todo( 'Implement __init__' )
    def __init__( self ):
        """
Instance initializer"""
        # InterfaceName is intended to be an interface,
        # and is NOT intended to be instantiated. Alter at your own risk!
        if self.__class__ == InterfaceName:
            raise NotImplementedError( 'InterfaceName is '
                'intended to be an interface, NOT to be instantiated.' )
        # Call parent initializers, if applicable.

    #-----------------------------------#
    # Instance Garbage Collection       #
    #-----------------------------------#

    #-----------------------------------#
    # Abstract Instance Methods         #
    #-----------------------------------#

#    @abc.abstractmethod
#    def RequiredMethod( arg1, arg2=None, *args, **kwargs ):
#        raise NotImplementedError( '%s.RequiredMethod is not implemented as '
#            'required by InterfaceName' % self.__class__.__name__ )

    #-----------------------------------#
    # Abstract Class Methods            #
    #-----------------------------------#

    #-----------------------------------#
    # Static Class Methods              #
    #-----------------------------------#

#---------------------------------------#
# Append to __all__                     #
#---------------------------------------#
__all__.append( 'InterfaceName' )

Like the module- and package-templates, a lot of this is just structural, organizational comment-blocks, indicating where to put verious types of members. After the initial declaration of the __metaclass__ for the interface, there are seven major blocks:

  • A place to define static(-ish) interface-level attributes/constants;
  • A place to define abstract (required) properties;
  • A concrete __init__, more on that shortly;
  • A block for a __del__ method, if one needs to be required in derived classes;
  • A block for defining abstract instance-methods;
  • A block for defining abstract class-methods; and
  • A block for defining static methods.

The check for the InterfaceName is in place so that an interface could be made more-or-less operational on Python versions that predate the abc module (sometime in the 2.6.x versions). It'd be kinda tedious, but all that would have to be done would be to remove all of the abc.[whatever] calls and references, and the interface would still be non-instantiable. A similar pattern is also in place for the templated abstract-method for the same reason.

The Abstract Class Template

These also cannot be instantiated, but can contain concrete implementations of properties and methods both, as well as abstract requirements for properties and methods. Much of it is very similar to the Interface template, for much the same reasons, but there are a couple additional sections:

  • Blocks for collecting the (usually protected) property-getter, -setter and -deleter methods for any concrete property implementations; and
  • A concrete- and abstract-property location;

@describe.InitClass()
class AbstractClassName( object ):
    """TODO: Document AbstractClassName
Provides baseline functionality, interface requirements and type-identity for 
objects that can REPRESENT SOMETHING"""

    #-----------------------------------#
    # Abstraction through abc.ABCMeta   #
    #-----------------------------------#
    __metaclass__ = abc.ABCMeta

    #-----------------------------------#
    # Class attributes (and instance-   #
    # attribute default values)         #
    #-----------------------------------#

    #-----------------------------------#
    # Instance property-getter methods  #
    #-----------------------------------#

    #-----------------------------------#
    # Instance property-setter methods  #
    #-----------------------------------#

    #-----------------------------------#
    # Instance property-deleter methods #
    #-----------------------------------#

    #-----------------------------------#
    # Instance Properties (abstract OR  #
    # concrete!)                        #
    #-----------------------------------#

#    PropertyName = abc.abstractproperty()
#    PropertyName = describe.makeProperty()

    #-----------------------------------#
    # Instance Initializer              #
    #-----------------------------------#
    @describe.AttachDocumentation()
    @describe.todo( 'Document __init__' )
    @describe.todo( 'Implement __init__' )
    def __init__( self ):
        """
Instance initializer"""
        # AbstractClassName is intended to be an abstract class,
        # and is NOT intended to be instantiated. Alter at your own risk!
        if self.__class__ == AbstractClassName:
            raise NotImplementedError( 'AbstractClassName is '
                'intended to be an abstract class, NOT to be instantiated.' )
        # Call parent initializers, if applicable.
        # Set default instance property-values with _Del... methods as needed.
        # Set instance property values from arguments if applicable.

    #-----------------------------------#
    # Instance Garbage Collection       #
    #-----------------------------------#

    #-----------------------------------#
    # Instance Methods                  #
    #-----------------------------------#

#    @abc.abstractmethod
#    def RequiredMethod( arg1, arg2=None, *args, **kwargs ):
#        raise NotImplementedError( '%s.RequiredMethod is not implemented as '
#            'required by InterfaceName' % self.__class__.__name__ )

    #-----------------------------------#
    # Class Methods                     #
    #-----------------------------------#

    #-----------------------------------#
    # Static Class Methods              #
    #-----------------------------------#

#---------------------------------------#
# Append to __all__                     #
#---------------------------------------#
__all__.append( 'AbstractClassName' )

There are a few more prompt-comments in the __init__ method as well, keeping with my normal programming structure: I explicitly delete all of an instance's properties during initialization so that they will always have a value after instances are first created.

The Class Template

Classes are intended to be instantiated, and can be extended freely. This template doesn't have anything new, that I haven't shown before, so there's not much that can be said about it.

@describe.InitClass()
class ClassName( object ):
    """TODO: Document ClassName
Represents SOMETHING"""
    #-----------------------------------#
    # Class attributes (and instance-   #
    # attribute default values)         #
    #-----------------------------------#

    #-----------------------------------#
    # Instance property-getter methods  #
    #-----------------------------------#

    #-----------------------------------#
    # Instance property-setter methods  #
    #-----------------------------------#

    #-----------------------------------#
    # Instance property-deleter methods #
    #-----------------------------------#

    #-----------------------------------#
    # Instance Properties               #
    #-----------------------------------#

    #-----------------------------------#
    # Instance Initializer              #
    #-----------------------------------#
    @describe.AttachDocumentation()
    @describe.todo( 'Document __init__' )
    @describe.todo( 'Implement __init__' )
    def __init__( self ):
        """
Instance initializer"""
        # Call parent initializers, if applicable.
        # Set default instance property-values with _Del... methods as needed.
        # Set instance property values from arguments if applicable.
        pass # TODO: Remove this line after __init__ is implemented

    #-----------------------------------#
    # Instance Garbage Collection       #
    #-----------------------------------#

    #-----------------------------------#
    # Instance Methods                  #
    #-----------------------------------#

    #-----------------------------------#
    # Class Methods                     #
    #-----------------------------------#

    #-----------------------------------#
    # Static Class Methods              #
    #-----------------------------------#

#---------------------------------------#
# Append to __all__                     #
#---------------------------------------#
__all__.append( 'ClassName' )

The Final Class Template

A final class is intended to be instantiated, but not extended. The only significant difference between this template and the Class template above is how the __init__ handles extension-attempts:

    #-----------------------------------#
    # Instance Initializer              #
    #-----------------------------------#
    @describe.AttachDocumentation()
    @describe.todo( 'Document __init__' )
    @describe.todo( 'Implement __init__' )
    def __init__( self ):
        """
Instance initializer"""
        # FinalClassName is intended to be a nominally-final class
        # and is NOT intended to be extended. Alter at your own risk!
        #---------------------------------------------------------------------#
        # TODO: Explain WHY it's nominally final!                             #
        #---------------------------------------------------------------------#
        if self.__class__ != FinalClassName:
            raise NotImplementedError( 'FinalClassName is '
                'intended to be a nominally-final class, NOT to be extended.' )
        # Call parent initializers, if applicable.
        # Set default instance property-values with _Del... methods as needed.
        # Set instance property values from arguments if applicable.

After taking some time to roll the doc_metadata.py file into the new standard template, I can (finally) make good on my promise to make it available for download, as well as the collection of template-files:

109.6kB

It's been fourteen posts now, and just over a month since I started in on the documentation-process. I'm not sure exactly where I want to go next on the Python side of things, though I have a couple of ideas, so I think for my next post, I'll play around a bit with implementing some design-patterns in JavaScript. If nothing else, that will give me some time to ponder what the next logical step is.

Tuesday, February 7, 2017

Documentation Decorators: Classes and their Members

Long Post

Today I'm going to work out a process for documenting class properties and classes themselves. I'm going to start with documentation for properties. The goal for that would be to end up with documentation that looks something like this, at a minimum:

----------------------------------------------------------------------------
PropertyName ..... (int, float) Gets, sets, or deletes the PropertyName 
                   value of the instance. PropertyName description.
----------------------------------------------------------------------------

A few posts back, I noted that one of my key development principles was to manage/control all public interface entities/members, and that one aspect of that was to create property getter-, setter- and deleter-methods (even if they weren't ever actually attached to a property definition) and create property elements as members of the class.

There are a couple of ways to create properties in Python classes. One is a decorator-based structure that would look like so:

class Example( object ):
    @property
    def PropertyName( self ):
        return self._propertyName
    @PropertyName.setter
    def PropertyName( self, value ):
        self._propertyName = value
    @PropertyName.deleter
    def PropertyName( self ):
        del self._propertyName

x = Example()
x.PropertyName = 'Spam'
print x.PropertyName
del x.PropertyName
try:
    print x.PropertyName
except AttributeError:
    print 'PropertyName deletion performed as expected'

This approach executes just fine, but it doesn't appear to provide any way of attaching any documentation to the resulting property, so that won't meet my goal. That's too bad, because it'd result in a bit less code. That discovery, which I made a while back, was what prompted the original approach shown in that previous post. The example code-structure from that post, with the appropriate documentation-decorators on those methods would look like this:

class Ook( object ):
    #####################################
    # Instance property-getter methods  #
    #####################################
    @describe.AttachDocumentation()
    def _GetPropertyName( self ):
        """
Gets the PropertyName property-value of the instance."""
        return self._propertyName

    #####################################
    # Instance property-setter methods  #
    #####################################
    @describe.AttachDocumentation()
    @describe.argument( 'value', 'The value to set as the instance\'s '
        'PropertyName value', int, float )
    @describe.raises( TypeError, 'if passed an invalid value-type' )
    @describe.raises( ValueError, 'if passed an invalid value' )
    def _SetPropertyName( self, value ):
        """
Sets the PropertyName property-value of the instance."""
        # TODO: Type-check the value argument, and raise an error on a 
        #       failure, or remove this comment.
        # TODO: Value-check the value argument, and raise an error on a 
        #       failure, or remove this comment.
        self._propertyName = value

    #####################################
    # Instance property-deleter methods #
    #####################################
    @describe.AttachDocumentation()
    def _DelPropertyName( self ):
        """
"Deletes"" the PropertyName property-value of the instance by setting it to 
the default value specified in the ClassName attributes."""
        try:
            del self._propertyName
        except AttributeError:
            pass

    #####################################
    # Instance Properties               #
    #####################################

    PropertyName = property( _GetPropertyName, _SetPropertyName, 
        _DelPropertyName,
        'Gets the PropertyName value of the instance.' )

    #####################################
    # Instance Initializer              #
    #####################################
    @describe.AttachDocumentation()
    def __init__( self ):
        """
Instance initializer"""
        # Call parent initializers, if applicable.
        # Set default instance property-values with _Del... methods as needed.
        self._DelPropertyName()
        # Set instance property values from arguments if applicable.

The drawback to this structure is that a simple decoration approach simply won't work. This is easily demonstrated:

def mydecorator():
    def innerDecorator( decoratedItem ):
        return decoratedItem
    return innerDecorator

class Example2( object ):
    @mydecorator
    eggs = property()

Trying to execute this code-snippet raises a SyntaxError. Trying the other decorator-execution structure (eggs = mydecorator()( property() ) raises a TypeError ('property' object is not callable). This is probably just as well, since that sort of decorator-application feels... awkward, I think, at a minimum, and it's certainly not as readable as the decorators for callables that are already established. it doesn't appear that there's a decorator-based approach that would work and meet my needs.

All that said, though, the idea of using a function or method to perform the same kinds of tasks as a decorator isn't beyond reach. Consider the following code:

def makeProperty( getter, setter, deleter, description, *expects ):
    if not description:
        description = 'the property value'
    actions = []
    if getter:
        actions.append( 'gets' )
    if setter:
        actions.append( 'sets' )
    if deleter:
        actions.append( 'deletes' )
    actions[ 0 ] = actions[ 0 ].title()
    if expects:
        expectedTypes = ( '|'.join( 
            [
                item.__name__ if hasattr( item, '__name__' ) 
                else str( item ) 
                for item in expects 
            ] ) ).replace( 'NoneType', 'None' )
    else:
        expectedTypes = 'any'
    if len( actions ) > 1:
        description = ', '.join( actions[0:-1] ) + ' or %s %s (%s)' % ( 
            actions[ -1 ], description, expectedTypes )
    else:
        description = '%s %s (%s)' % ( actions[ 0 ], description, expectedTypes )
    return property( getter, setter, deleter, description )

class Eek( object ):
    def _Getter( self ): pass
    @describe.AttachDocumentation()
    @describe.raises( TypeError, 'if set to an invalid value-type' )
    @describe.raises( ValueError, 'if set to an invalid value' )
    def _Setter( self, value ): pass
    def _Deleter( self ): pass
    x = makeProperty( _Getter, _Setter, _Deleter, 
        'the example property', 
        int, float, None )

After execution, calling print Eek.x.__doc__ yields:

Gets, sets or deletes the example property (int|float|None)

Apart from the name of the property itself, which isn't always needed, this matches the need pretty well. It's just about perfect, I think, for a print [propertyName].__doc__ call, which I've already mentioned that I use on occasion to read documentation from a command-line. The missing property-name is easily supplied in the context of a class for generation of class-level, summary-level documentation, I suspect. It might well be useful to include any of the raises decorations from the provided setter-method, if the setter exists and has been decorated with any describe.raises calls. That's not difficult to achieve, it just requires the addition of a few lines of code:

    if setter:
        try:
            exceptions = setter._documentation.raises
            for exception in sorted( exceptions, key=lambda e: e.__name__ ):
                description += ( )'; Raises %s ' % exception.__name__  + 
                    ', '.join( exceptions[ exception ] ) )
        except:
            pass
    return property( getter, setter, deleter, description )

The same property-wrapping call, with that code in place, yields:

Gets, sets or deletes the example property (int|float|None); 
Raises TypeError if set to an invalid value-type; 
Raises ValueError if set to an invalid value

Ideally, any item that's been documented with the sort of decorator approach that I'm creating should have a reasonably similar underlying data-structure, and properties are no exception to that. However, once a property has been created, there's no way that I've found thus far to actually attach new attributes to them, which would be necessary if I was to create, for example, a property_documentation class to hold on to all of the items actually used to create the documentation. By way of example, I'll create a class with a single property on it, and try to attach another attribute to it:

class TestClass( object ):
    def _getter( self ):
        return self._property
    def _setter( self, value):
        self._property = value
    def _deleter( self ):
        self._property = None
    propertyName = property( _getter, _setter, _deleter, 'This is my property' )
    def __init__( self, value ):
        self._deleter()
        self._setter( value )

try:
    print 'Trying ..._newAttribute = None'
    TestClass.propertyName._newAttribute = None
    print 'YES! This worked!'
except Exception, error:
    print 'NOPE: %s: %s' % ( error.__class__.__name__, error )
print

try:
    print 'Trying ...__dict__[ "_newAttribute" ] = None'
    TestClass.propertyName.__dict__[ '_newAttribute' ] = None
    print 'YES! This worked!'
except Exception, error:
    print 'NOPE: %s: %s' % ( error.__class__.__name__, error )
print

try:
    print 'Trying setattr( propertyName, "_newAttribute", None )'
    setattr( TestClass.propertyName, '_newAttribute', None )
    print 'YES! This worked!'
except Exception, error:
    print 'NOPE: %s: %s' % ( error.__class__.__name__, error )
print

Running this code results in:

Trying ..._newAttribute = None
NOPE: AttributeError: 'property' object has no attribute '_newAttribute'

Trying ...__dict__[ "_newAttribute" ] = None
NOPE: AttributeError: 'property' object has no attribute '__dict__'

Trying setattr( propertyName, "_newAttribute", None )
NOPE: AttributeError: 'property' object has no attribute '_newAttribute'

If there are any other ways to even try to attach new attributes to a property during or after its definition, I can't think of them offhand. Certainly nothing else jumps readily to mind that'd be at least reasonably simple and straightforward. All that said, though, when I examine the uses I'd have or expect for the documentation of a property, they start me wondering if that's really all that much of an impediment. What I'd want or need would include:

  • Printing the property's documentation from a command-line: do-able. Though the doc-string itself might not have any reference back to the actual property-name, it's immaterial because I'd know the name of the property anyway, having called something like print propertyName.__doc__.
  • Including the property's documentation in the entire documentation for the class that the property is a member of: do-able, I think, since the properties themselves would contain all of the information needed to re-create the doc-string if it weren't actually generated on the fly. The probable lack of a property-name association in the doc-strings is irrelevant here as well, because I'd almost certainly be iterating through a series of class-members, and would be able to get their names during that iteration.
  • Creating documentation-output, particularly of entire class' interfaces, in formats other than plain text: probably do-able. The same name-relationship as noted just above would be usable, at least for generating documentation for an entire class and its members. The main consideration here is probably that once the doc-string has been created by the pseudo-decoration/wrapper-method process, the original doc-string, the one passed in the description in that method-call, is irrevocably altered.
Of those, only the last item represents any potential major stumbling-block, I think. But I also think that so long as the structure of the resulting doc-string generated by the pseudo-decorator call is consistent, extracting those individual items during a documentation-generation run for, say, LATEX would not be terribly difficult either. It might be better served (and maybe make more sense in documentation-output anyway) to re-structure those to something more like:

[name, from context] ... ([expected types]) [Actions] [description]. [Exceptions]

That structure, once stored in the doc-string of a documented property, would be pretty easy to extract the relevant items from for whatever re-sequencing or formatting was desired for output in other than plain text, while still remaining useful in plain text as well:

  • [name, from context] would be supplied outside the doc-string, by whatever process was determining which property the documentation is being retrieved for;
  • ([expected types]) would always be the first item in the doc-string, and wrapped in ();
  • [Actions] and [description] make sense to keep together, and would always end with a . and start right after the closing ) of the expected types.
  • [Exceptions], if they exist, would be the balance of the doc-string, and would be separated by . or ; characters
If the separators for major blocks are always periods, a single split call will take care of identifying the exceptions and non-exceptions items in one pass, and a second split, on ) would suffice to separate expected types from the actions/description text. The extraction-process wouldn't even require regular expressions to implement. Even the exception-classes in the list of exceptions would be easily identified, since they would always be the second word in the individual exceptions-lists, and the actual conditions wouldn't be hard to extract either, should it me needed. It's not as clean an implementation as having a consistent underlying data-structure to refer to, but it's pretty easily managed.

The net result of all of this is that while I'd rather have a data-structure for property-documentation elements, something like the callable_documentation class I created for function- and method-documentation, it's just not possible. It's also not a major road-block, though, since the structure of the resulting documentation is relatively simple, and can be defined with consistency for retrieving whatever might be needed later on. So, with all of that said, the process for creating consistently-documented properties will be provided by describe.makeProperty:

    @classmethod
    def makeProperty( cls, getter, setter, deleter, description, *expects ):
        """
Creates and returns a property object, using the built-in property method, 
building/creating a detailed documentation-string on it in the process."""
        if not description:
            description = 'the property value'
        actions = []
        if getter:
            actions.append( 'gets' )
        if setter:
            actions.append( 'sets' )
        if deleter:
            actions.append( 'deletes' )
        actions[ 0 ] = actions[ 0 ].title()
        if expects:
            expectedTypes = ( '|'.join( 
                [
                    item.__name__ if hasattr( item, '__name__' ) 
                    else str( item ) 
                    for item in expects 
                ] ) ).replace( 'NoneType', 'None' )
        else:
            expectedTypes = 'any'
        if len( actions ) > 1:
            description = '(%s) %s or %s %s. ' % ( expectedTypes, ', '.join( actions[0:-1] ), actions[ -1 ], description.strip() )
        else:
            description = '(%s) %s %s. ' % ( expectedTypes, actions[ 0 ], description )
        if setter:
            try:
                exceptions = setter._documentation.raises
                for exception in sorted( exceptions, key=lambda e: e.__name__ ):
                    description += 'Raises %s ' % exception.__name__  + '; '.join( exceptions[ exception ] ) + '. '
            except:
                pass
        return property( getter, setter, deleter, description.strip() )

While I was working out the documentation-decoration for the makeProperty method, I chanced across an... interesting... discovery: The decoration-effort that I'd been making on the decorators themselves is, for most practical purposes, useless. I'm not sure exactly why this is the case, but it's apparently not possible to overwrite the __doc__ of an instance method once that instance method has been defined, so the decoration metadata, while it exists in the callable_documentation instance attached to each method, is not being attached to those methods by the final AttachDocumentation call. After a brief panic that I'd spent quite some time writing code that would turn out to be useless (and moving some code around in the process) I re-checked the documentation on the Ook test-class, and it still worked as expected. But the documentation on the decorators themselves will have to be manually maintained. That's mildly annoying, but so long as similar issues don't arise later on, it should be fine. While I was thrashing through all of that double-checking, I altered the AttachDocumentation method so that it simply retrieves the formatted doc-string text from callable_documentation through a new method (_createDocString), instead of building it itself. I probably should've done that from the start, if only in keeping with basic encapsulation, and maybe the Single Responsibility Principle, but it simply didn't occur to me until now.

So: On to class-level documentation. The first thing I wanted to do was to make sure that my expectation of the execution-sequence of decorators on classes and decorators on their members was correct. I expected that, given a decorated class, with decorated members (methods, in this case) that the method decorators would execute to completion before the class-level decorators. Additionally, I wanted to check to confirm whether or not a class' __doc__ could be modified/set by a decorator on the class. Here's the code I spun up to test that:

def classdec():
    print 'Calling classdec()'
    def _classdec( decoratedItem ):
        print 'calling _classdec( %s )' % decoratedItem.__name__
        try:
            decoratedItem.__doc__ = 'This has been decorated'
        except Exception, error:
            print ' + - %s: decorating %s: %s' % ( error.__class__.__name__, decoratedItem.__name__, error )
        decoratedItem._documentation = { 'documentation_added':True }
        return decoratedItem
    return _classdec

def methoddec():
    print 'Calling methoddec()'
    def _methoddec( decoratedItem ):
        print ' + calling _methoddec( %s )' % decoratedItem.__name__
        return decoratedItem
    return _methoddec
    

@classdec()
class decorated_class( object ):
    @methoddec()
    def instance_method1( self ):
        pass
    @methoddec()
    def instance_method2( self ):
        pass

print decorated_class._documentation

When this code is run, it generates the following output:

Calling classdec()
Calling methoddec()
 + calling _methoddec( instance_method1 )
Calling methoddec()
 + calling _methoddec( instance_method2 )
calling _classdec( decorated_class )
 + - AttributeError: decorating decorated_class: attribute '__doc__' of 'type' objects is not writable
{'documentation_added': True}

That seems to me to provide confirmation that decorators execute in the order I'd expected. It also confirms, unfortunately, that the __doc__ of a class cannot be modified by a decorator. As with properties, I'm not sure precisely why this is the case, but given that I rarely need to access class-level documentation at the level of detail that I do documentation of functions, methods or properties, I'm OK with that. I do want to be able to access whatever underlying data-structure I can attach to the class in order to generate external documentation — HTML output, or LATEX, for example — but the addition of an arbitrary data-structure (decoratedItem._documentation = {} in the classdec method above) is shown in the results as doing exactly what I need it to do.

With that discovery out of the way, it's time to figure out what needs to be documented in a class. The complete list (that I can think of, at any rate) is:

  • Class-level attributes (i.e., non-property value-attributes, whether they are expected to be constant or not);
  • Instance properties (already accounted for);
  • Instance methods (already accounted for);
  • Whether the class is abstract or not;
That doesn't really leave a lot to be implemented: Just documentation of class attributes and abstraction, really. it seems likely to me that a call to AttachDocumentation will also be desirable, if only to aggregate all of the relevant property- and method-documentation information into the class-level documentation-structure.

Before I start digging into the implementation details for class-level documentation, I'm going to add a few items to the Ook class to be documented. As part of that, I'm going to make it an abstract class with the abc.__ABCMeta__ meta-class. I'm also going to add in my standard template comments, if only because Ook is getting pretty long now, even with no real implementation in any of its methods. In the process, I'll add calls for the various decorators as I hope they will shake out:

@AttachDocumentation()
@describe.attribute( '__PrivateAttribute', 'Private attribute description' )
@describe.attribute( '_ProtectedAttribute', 'Protected attribute description' )
@describe.attribute( 'PublicAttribute', 'Public attribute description' )
class Ook( object ):
    """
Test-class."""

    #####################################
    # Abstraction through abc.ABCMeta   #
    #####################################
    __metaclass__ = abc.ABCMeta

    #####################################
    # Class attributes (and instance-   #
    # attribute default values)         #
    #####################################
    __PrivateAttribute = None
    _ProtectedAttribe = None
    PublicAttribute = None

    #####################################
    # Instance property-getter methods  #
    #####################################

    def _GetPropertyName( self ):
        return self._propertyName

    #####################################
    # Instance property-setter methods  #
    #####################################
    @describe.raises( TypeError, 'if set to an invalid value-type' )
    @describe.raises( ValueError, 'if set to an invalid value' )
    def _SetPropertyName( self, value ):
        self._propertyName = value

    #####################################
    # Instance property-deleter methods #
    #####################################
    def _DelPropertyName( self ):
        self._propertyName = None

    #####################################
    # Instance Properties               #
    #####################################
    propertyName = describe.makeProperty( _GetPropertyName, 
        _SetPropertyName, _DelPropertyName, 
        'the PropertyName property of the instance', 
        int, float )


    #####################################
    # Instance Initializer              #
    #####################################
    def __init__( self ):
        """
Object initializer"""
        self._DelPropertyName()

    @describe.AttachDocumentation()
    @abc.abstractmethod()
    @describe.argument( 'arg1', 'Ook.Fnord (method) arg1 description', bool, None )
    @describe.argument( 'arg2', 'Ook.Fnord (method) arg2 description' )
    @describe.arglist( 'Ook.Fnord (method) arglist description', int, long, float )
    @describe.arglistitem( 0, 'argitem1', 'Ook.Fnord.args[0] description', float )
    @describe.arglistitem( 1, 'argitem2', 'Ook.Fnord.args[1] description', int, long )
    @describe.arglistitem( 2, 'argitem3', 'Ook.Fnord.args[2] description', bool )
    @describe.arglistitem( -1, 'values', 'Ook.Fnord.args[3] (values) description', str, unicode )
    @describe.keywordargs( 'Ook.Fnord keyword-arguments list description' )
    @describe.keyword( 'keyword1', 'Ook.Fnord (method) "keyword1" description',int, long, float, required=True )
    @describe.keyword( 'keyword2', 'Ook.Fnord (method) "keyword2" description',None, str, unicode, default=None )
    @describe.keyword( 'keyword3', 'Ook.Fnord (method) "keyword3" description',None, str, unicode )
    @describe.deprecated( 'Use new_Fnord instead.' )
    @describe.returns( 'None (at least until the method is implemented)' )
    @describe.raises( NotImplementedError, 'if called' )
    @describe.todo( 'Clean up output to remove empty members' )
    @describe.todo( 'Change output to class with the same interface' )
    @describe.fixme( 'Magic _parameters value needs to be removed' )
    @describe.fixme( 'Rewrite list-loops to perform the same operations in fewer passes' )
    def Fnord( self, arg1, arg2, *args, **kwargs ):
        """Ook.Fnord (method) original doc-string"""
        raise NotImplementedError( '%s.Fnord is not yet implemented' % 
            self.__class__.__name__ )

    @classmethod
    @describe.AttachDocumentation()
    @describe.argument( 'arg1', 'Ook.Bleep (classmethod) arg1 description', int, long, float )
    @describe.argument( 'arg2', 'Ook.Bleep (classmethod) arg2 description' )
    @describe.arglist( 'Ook.Bleep (classmethod) arglist description' )
    @describe.deprecated( 'Will be removed by version X.YY.ZZ' )
    def Bleep( cls, arg1, arg2=None, *args, **kwargs ):
        """Ook.Bleep (classmethod) original doc-string"""
        return None

    @staticmethod
    @describe.AttachDocumentation()
    @describe.argument( 'arg1', 'Ook.Flup (staticmethod) arg1 description', int, long, float )
    @describe.argument( 'arg2', 'Ook.Flup (staticmethod) arg2 description' )
    @describe.arglist( 'Ook.Flup (staticmethod) arglist description' )
    @describe.todo( 'Clean up output to remove empty members' )
    @describe.todo( 'Change output to class with the same interface' )
    @describe.fixme( 'Magic _parameters value needs to be removed' )
    @describe.fixme( 'Rewrite list-loops to perform the same operations in fewer passes' )
    def Flup( arg1, arg2, *args, **kwargs ):
        """Ook.Flup (staticmethod) original doc-string"""
        return None

The new items added to Ook's definition are:

  • It has an @AttachDocumentation() call, which should gather up all of the documentation items from other decorators as well as from documented properties and methods.
  • It has describe.attribute decorator-calls for three class-level attributes:
    • __PrivateAttribute, a private attribute;
    • _ProtectedAttribe, a protected attribute; and
    • PublicAttribute, a public sttribute
    • all of which have default/set values of None.
  • It has been made capable of supporting abstract properties and methods through the __metaclass__ = abc.ABCMeta addition.
  • It has one abstract method, Fnord.

Documentation metadata for class attributes should probably be stored in much the same fashion as the metadata for callables, and for much the same reasons: There will be internal data-structures that need to be read-only to prevent accidental overwriting or corruption of their values, probably some checking for duplicate names, and all of the other factors that led to the decision to create the callable_documentation class. Expressed as a dict, I expect that structure to look something like this, for an entire class:

{
    'attributes':{
        'name':{ # <str|unicode>
            'description':<str|unicode>,
            'name':<str|unicode>,
            'value':<object>,
        },
        # ...
    },
    'isAbstract':<bool>,
    'methods':{
        'name':<callable_documentation>,
        # ...
        }
    },
    'originalDocstring':<str|unicode>,
    'properties':{
        'name':{ # <str|unicode>
            'description':<str|unicode>,
            'expects':<type*>
            'name':<str|unicode>,
            'raises':{
                <Exception>:<str|unicode>,
                # ...
            }
        },
    }
}

So, the first thing I'll tackle is documenting class attributes. I'm going to assemble the class_documentation class that actually stores and manages class documentation as I go. The attribute documentation is pretty simple, I think. The decorators in question on the Ook class (and the attributes that relate to them) are:

@AttachDocumentation()
@describe.attribute( '__PrivateAttribute', 'Private attribute description' )
@describe.attribute( '_ProtectedAttribute', 'Protected attribute description' )
@describe.attribute( 'PublicAttribute', 'Public attribute description' )
class Ook( object ):
    """
Test-class."""

    #####################################
    # Abstraction through abc.ABCMeta   #
    #####################################
    __metaclass__ = abc.ABCMeta

    #####################################
    # Class attributes (and instance-   #
    # attribute default values)         #
    #####################################
    __PrivateAttribute = None
    _ProtectedAttribe = None
    PublicAttribute = None

Which requires an attribute decorator in the describe class:

@classmethod
def attribute( cls, name, description=None ):
        """
Decorates a class by attaching documentation-metadata about a single 
class-attribute to it."""
    # Type- and value-check incoming arguments
    if type( name ) not in ( str, unicode ):
        raise TypeError( '%s.attribute expects a non-empty str or '
            'unicode value whose value is the name of an attribute of '
            'the decorated class for its "name" argument, but was '
            'passed "%s" (%s)' % ( cls.__name__, name, 
                type( name ).__name__ ) )
    if description != None:
        if type( description ) not in ( str, unicode ):
            raise TypeError( '%s.attribute expects a non-empty str or '
                'unicode value, or None, but was passed "%s" (%s)' % ( 
                    cls.__name__, description, 
                    type( description ).__name__ ) )
    def _attributeDecorator( decoratedItem ):
        # Make sure the decorated item has a _documentation attribute, and 
        # if it doesn't, create and attach one:
        try:
            _documentation = decoratedItem._documentation
        except AttributeError:
            decoratedItem._documentation = class_documentation( decoratedItem )
            _documentation = decoratedItem._documentation
        # If we reach this point, then we should set the deprecated value 
        # to the information provided.
        value = getattr( decoratedItem, name )
        _documentation.AddAttribute( name, value, description )
        # Return the decorated item!
        return decoratedItem
    return _attributeDecorator

And an AddAttribute method in class_documentation:

def AddAttribute( self, name, value, description=None ):
    """
Adds attribute for the specified attribute to the instance's 
documentation-metadata."""
    # Check to assure that self.DecoratedItem has an attribute with the 
    # supplied name
    if not name in self.DecoratedItem.__dict__.keys():
        raise AttributeError( '%s does not have an attribute with the '
            'name "%s"' % ( self.DecoratedItem.__name__, name ) )
    # Make sure the decoration-attempt isn't going to override an existing 
    # method- or property-documentation
    if self.methods.get( name ):
        raise NameError( '%s.%s is already documented as a method' % ( 
            self.DecoratedItem.__name__, name ) )
    if self.properties.get( name ):
        raise NameError( '%s.%s is already documented as a property' % ( 
            self.DecoratedItem.__name__, name ) )
    if not description:
        description = 'Not documented'
    # Create the dictionary entry
    self.attributes[ name ] = {
        'description':description,
        'name':name,
        'value':value,
        }

With the exception of the decorator for the __PrivateAttribute, this behaves exactly as expected, populating Ook._documentation.attributes with:

{
'PublicAttribute': {
    'description': 'Public attribute description',
    'name': 'PublicAttribute',
    'value': None
    },
'_ProtectedAttribute': {
    'description': 'Protected attribute description',
    'name': '_ProtectedAttribute',
    'value': None}
    }

The __PrivateAttribute bears some discussion, perhaps. Since it is nominally private, it shouldn't really be accessed outside the class it's a member of — that's what private means in an OO context. Since private class-members in Python are only private by convention, it would be feasible to modify the decoration process (in AddAttribute) to check for the mangled name of a private class-attribute, but I see no real need for that, so I'll leave it as-is, and remove the decorator for __PrivateAttribute. The process automatically acquires the values of the decorated attributes, so all of that feels pretty tidy, overall.

The initialization of class_documentation also bears some examination, I think. At this point, it's pretty simple, though there are some aspects to it that I'd like to find better ways of handling:

def __init__( self, decoratedItem ):
    """
Instance initializer"""
    # Call parent initializers, if applicable.
    # Set default instance property-values with _Del... methods as needed.
    self._DelDecoratedItem()
    self._attributes = {}
    self._decoratedItem = None
    self._isAbstract = inspect.isabstract( decoratedItem )
    self._methods = {}
    self._originalDocstring = None
    self._properties = {}
    # Set instance property values from arguments if applicable.
    self._SetDecoratedItem( decoratedItem )
    if decoratedItem.__doc__:
        self._originalDocstring = decoratedItem.__doc__
    else:
        self._originalDocstring = """
No original doc-string provided for %s""" % ( decoratedItem.__name__ )

The potential issue I see (and would like to work out) resides in the _SetDecoratedItem method:

def _SetDecoratedItem( self, value ):
    if not type( value ).__name__ in ( 'type', 'classobj', 'ABCMeta' ):
        raise TypeError( '%s.DecoratedItem expects a class (new- or old-'
            'style), but was passed "%s" (%s).' % ( 
                self.__class__.__name__, value, type( value ).__name__ ) )
    self._decoratedItem = value

It arises in the type-check performed first thing in the method, where I discovered that classes defined with a __metaclass__ (like the abc.ABCMeta used to define abstract classes) may not be standard type or classobj types. In the case of an abstract class, the type of the class returns as ABCMeta. The __metaclass__ declaration, as I understand it, allows a developer to override what base class a new class is derived from, and removes the original (implied?) type (or classobj?) from the ancestry of the new class. Completely. That's based on some examples I've read out and around on the web (here and here, as starting-points). Maybe I'm just not Pythonic enough in my development efforts to date, but apart from using ABCMeta, I've never used __metaclass__. Maybe I just don't really understand __metaclass__. I don't know. At any rate, the practical implication of using __metaclass__ in classes that are being documented with class_documentation is that any __metaclass__ used in any class must be accounted for in class_documentation._SetDecoratedItem. An additional (if hopefully minor) item of note is that the check is using magic strings to identify the accepted types. I could convert those to real types, type and ABCMeta, but doing so would be at the cost of not supporting classic Python classes. The underlying cause of that is that classic classes, report back as being <type 'classobj'>, but there is no classobj type available without importing the types module and comparing against types.ClassType. Whether classic classes are formally deprecated or not, the new class is the officially-recommended way to create classes in Python in Python 3.x (as noted here), and while I use the new class structure in Python 2.x code I write, I cannot rule out the possibility that I'll want to use classic-style code even before I convert over to Python 3.x.

For now, I think I'll leave things be. I may come back and add types.ClassType and remove the magic strings at some point later, or I may even come back and significantly alter the process if other changes warrant it. As things stand right now, though, it's sufficient until proven otherwise, even if I cringe a little bit at the thought.

Next up is abstraction. When I performed a test-run of the documentation-generation on the final version of the Ook class was that even though I'd made the Fnord method abstract, there was no indication of that in the output:

[function]
Fnord(self, arg1, arg2, *args, **kwargs)
Ook.Fnord (method) original doc-string
Deprecated: Use new_Fnord instead.
Returns: None (at least until the method is implemented)
...
This is complicated by the fact that while Python's inspect module does provide an isabstract method, that method only operates against abstract classes, not abstract methods within those classes. At first blush, that made it seem like there was no good way to determine if a given method was abstract at all. However, after creating two new Ook methods, one abstract and one not, and examining their respective __dict__s, I noticed a difference: The abstract method had an __isabstractmethod__ attribute while the non-abstract one did not. My initial plan was to add an isabstract flag to the callable_documentation class, defaulting to False, and set it to True if the results if inspect.isabstract indicated that the method was abstract. Changing the approach to a check for the __isabstractmethod__ instead still allows that approach to be viable.

That, however, raised another interesting facet: Since a method's abstraction is controlled by a decorator rather than by a direct declaration on the method as in other languages, a method's abstraction isn't necessarily detectable at any given point during the interpretation of the class. Looking at Ook.Fnord as an example, and remembering that decorators fire from last to first:

  • The first fixmedecorator fires, creating the callable_documentation instance. At this point, the method has not been defined as abstract.
  • The remaining describe decorators up to the argument decoration for arg1 execute. At no point through these decorators' execution is the method abstract.
  • The abc.abstractmethod decorator executes. Now the method is abstract.
  • describe.AttachDocumentation executes, and the method is still abstract.
Basically, the method will not be recognizable as abstract until the abc.abstractmethod call is executed, and since that's another decorator, that could happen anywhere. So, what I did was to create a helper method on callable_documentation: _checkForAbstraction that looks for the __isabstractmethod__ attribute in the decorated item's __dict__, (but only if it hasn't already been flagged as abstract, since that's not removable after the fact):

def _checkForAbstraction( self ):
    if self._decoratedItem and not self.isabstract:
        if self.DecoratedItem.__dict__.get( '__isabstractmethod__' ):
            self._isAbstract = True

That method is then called in every instance in the various method- and function-decorators where a callable_documentation is either retrieved or created:

# Make sure the decorated item has a _documentation attribute, and 
# if it doesn't, create and attach one:
try:
    _documentation = decoratedItem._documentation
except AttributeError:
    decoratedItem.__dict__[ '_documentation' ] = callable_documentation( decoratedItem )
    _documentation = decoratedItem._documentation
_documentation._checkForAbstraction()

That's a little wasteful of cycles, maybe, but it guarantees that detection of abstraction is handled correctly so long as the abc.abstractmethod decoration executes before at least one of the relevant describe decorators (including describe.AttachDocumentation). Essentially, the only rule that has to be followed for it to work is that the abc.abstractmethod decorator has to live after the describe.AttachDocumentation decorator in the code, which shouldn't be too difficult to do. With a couple of minor tweaks here and there, the documentation accommodates the abstract method just fine:

[abstract function]
Fnord(self, arg1, arg2, *args, **kwargs)
Ook.Fnord (method) original doc-string
Deprecated: Use new_Fnord instead.
Returns: None (at least until the method is implemented)
...

Interestingly, inspect.isabstract, when used to check whether a class is abstract or not, suffers from the same limitation: Until a member is declared as an abstract method (or, I presume, an abstract property), it does not recognize that the class is abstract. Fortunately, by the time the first class-level decorator fires, any such abstract decorations will already have completed, so it will not be an issue.

On to properties and methods!

Acquisition of a class' property-documentation can happen during the creation of the related class_documentation object:

    def __init__( self, decoratedItem ):
        """
Instance initializer"""
        # Call parent initializers, if applicable.
        # ...
        self._properties = {}
        # Populate the property documentation here and now...
        for name in dir( decoratedItem ):
            item = getattr( decoratedItem, name )
            if inspect.isdatadescriptor( item ):
                try:
                    expects, main = item.__doc__.split( ')', 1 )
                    expects += ')'
                    description, allRaises = main.split( '. ', 1 )
                    description += '.'
                    description = description.strip()
                    raises = [ item.strip() + '.' for item in allRaises.split( '.' ) if item ]
                    self._properties[ name ] = {
                        'description':description,
                        'expects':expects,
                        'name':name,
                        'raises':raises,
                        }
                except:
                    # __doc__ doesn't follow our standard structure, so it 
                    # presumably isn't one of ours. Ignore it.
                    pass
        # Set instance property values from arguments if applicable.

While that process doesn't actually yield the exception-keyed dict that I'd originally wanted, it does keep the data that I actually want in a usable form. Dropping pprint( self._properties[ name ] ) after setting self._properties[ name ] shows this:

{'description': 'Gets, sets or deletes the PropertyName property of the instance.',
 'expects': '(int|float)',
 'name': 'PropertyName',
 'raises': ['Raises TypeError if set to an invalid value-type.',
            'Raises ValueError if set to an invalid value.']}

While I'm reasonably certain I could figure out a way to get the exception names out of the raises items, and get the actual exception class from those names to use as keys, the more I thought about it, the less it seemed needful to go that far — particularly since any process that serialized the documentation metadata would have to undo that effort anyway.

With that caveat in mind, the class-level methods structure should arguably use a plain-vanilla dict as well, rather than the original callable_documentation object. That would require either a substantial rework of the class, or the addition of a method that can generate the sort of dict representation needed — call it toDict() — and method-documentation can be acquired in the same pass as property-documentation:

    def toDict( self ):
        results = {
            'arglist':self.arglist,
            'arguments':self.arguments,
            'deprecated':self.deprecated,
            'fixmes':self.fixmes,
            'isabstract':self.isabstract,
            'keywordargs':self.keywordargs,
            'raises':self.raises,
            'returns':self.returns,
            'todos':self.todos,
            }
        # TODO: Deal with the type-objects in various "expects" in argument-
        #       related items?
        return results

I'm not sure, but I suspect that somewhere down the line, I'll want or need to convert the real types (int, float, etc.) that are used in the "expects" values of all of the argument-documentation items into string values. I'll leave that alone for now, though. With toDict in place, acquiring method-documentation in the class_documentation object is simple:

        # Populate the property documentation here and now...
        for name in dir( decoratedItem ):
            item = getattr( decoratedItem, name )
            if inspect.isdatadescriptor( item ):
                # ...
            elif inspect.ismethod( item ):
                try:
                    self._methods[ name ] = item._documentation.toDict()
                except AttributeError:
                    # No _documentation item, so not a method with documentation
                    pass
        # Set instance property values from arguments if applicable.

With access to all of the callable_documentation items in a documented class and/or it's dictionary representation, generating complete HTML documentation-output for an entire class is pretty straightforward. It's mostly iterating over collections of metadata-items and wrapping some markup-structure around those values. With some tweaks to the structures of methods, utilization of some HTML 5 tags, and some changes to the style-sheet that controls what they look like, the documentation for our Ook class looks like so:

[ABCMeta]
Ook
Test-class.
Class Attributes
PublicAttribute
Public attribute description
_ProtectedAttribute
Protected attribute description
Properties
PropertyName
(int|float) Gets, sets or deletes the PropertyName property of the instance.
Methods
[function]
Bleep(cls, arg1, arg2, *args, **kwargs)
Ook.Bleep (classmethod) original doc-string
Deprecated: Will be removed by version X.YY.ZZ
Arguments
cls
(class, required): The class that the method will bind to for execution.
arg1
(int|long|float, required): Ook.Bleep (classmethod) arg1 description
arg2
(any, optional, defaults to None): Ook.Bleep (classmethod) arg2 description
*args
(any): Ook.Bleep (classmethod) arglist description
[abstract function]
Fnord(self, arg1, arg2, *args, **kwargs)
Ook.Fnord (method) original doc-string
Deprecated: Use new_Fnord instead.
Returns: None (at least until the method is implemented)
Fix Me:
  • Rewrite list-loops to perform the same operations in fewer passes
  • Magic _parameters value needs to be removed
Arguments
self
(instance, required): The object-instance that the method will bind to for execution.
arg1
(bool|None, required): Ook.Fnord (method) arg1 description
arg2
(any, required): Ook.Fnord (method) arg2 description
*args
(int|long|float): Ook.Fnord (method) arglist description
The following values are specified by position:
argitem1
(float): Ook.Fnord.args[0] description
argitem2
(int|long): Ook.Fnord.args[1] description
argitem3
(bool): Ook.Fnord.args[2] description
values
(str|unicode): Ook.Fnord.args[3] (values) description
**kwargs
Ook.Fnord keyword-arguments list description
keyword1
(int|long|float, required): Ook.Fnord (method) "keyword1" description
keyword2
(None|str|unicode, defaults to None): Ook.Fnord (method) "keyword2" description
keyword3
(None|str|unicode): Ook.Fnord (method) "keyword3" description
Exceptions
NotImplementedError
if called
To-Do:
  • Change output to class with the same interface
  • Clean up output to remove empty members

The code that generates this output is:

    def toHTML( self ):
        """
Creates and returns an HTML representation of the documentation of the item."""
        moduleName = self.DecoratedItem.__module__
        for name, cls in inspect.getmembers( inspect.getmodule( self.DecoratedItem ), inspect.isclass ):
            classMembers = dict( inspect.getmembers( cls ) )
            for classMember in classMembers.values():
                try:
                    if classMember.__name__ == self.DecoratedItem.__name__:
                        property_documentation = name
                except AttributeError:
                    pass
        itemId = '%s.%s' % ( moduleName, self.DecoratedItem.__name__ )
        documentation = self.DecoratedItem._documentation.toDict()
        results = '<section id="%s" class="class documentation">\n' % itemId
        results += """    <header class="heading">
        <div class="api_type">[%s]</div>
        <div class="class_name"><span class="api_name">%s</span></div>
    </header>\n""" % ( type( self.DecoratedItem ).__name__, self.DecoratedItem.__name__ )
        results += """    <details>
        <summary>%s</summary>\n""" % ( documentation[ 'originalDocstring' ].strip() )
        if documentation[ 'attributes' ]:
            attributes = documentation[ 'attributes' ]
            results += """        <section>
            <header class="heading">Class Attributes</header>
            <dl>\n"""
            for attribute in sorted( attributes.values(), key=lambda item: item[ 'name' ] ):
                results += """                <dt>
                    <span class="attribute name">%s</span>
                </dt>\n""" % ( attribute[ 'name' ] )
                results += """                <dd class="attribute description">%s</dd>\n""" % ( attribute[ 'description' ] )
            results += """            </dl>
        </section>\n"""
        if documentation[ 'properties' ]:
            properties = documentation[ 'properties' ]
            results += """        <section>
            <header class="heading">Properties</header>
            <dl>\n"""
            for property in sorted( properties.values(), key=lambda item: item[ 'name' ] ):
                results += """                <dt>
                    <span class="property name">%s</span>
                </dt>\n""" % ( property[ 'name' ] )
                results += """                <dd class="property description">%s %s</dd>\n""" % ( 
                    str( property[ 'expects' ] ), property[ 'description' ] )
            results += """            </dl>
        </section>\n"""
        if self.DecoratedItem._documentation.methods:
            methods = self.DecoratedItem._documentation.methods
            results += """    <section>
        <header class="heading">Methods</header>\n"""
            pprint( methods )
            for name in sorted( methods ):
                if name[ 0 ] == '_':
                    continue
                method = getattr( self.DecoratedItem, name )
                results += method._documentation.toHTML()
            results += """    </section>\n"""
        results += """    </details>\n"""
        results += '</section>\n'
        return results

As I was working on material for the next post, I realized that while I'd created a method in the describe class to initialize class-level documentation, I never actually showed it here. On top of that, there was a minor bug that was causing the documentation of classes derived from other documented classes to acquire the wrong class_documentation object. The complete (and fixed) code for the describe.InitClass method is:

    @classmethod
    def InitClass( cls ):
        """
Decorates a class by attaching documentation-metadata to it."""
        # Type- and value-check incoming arguments
        def _InitClassDecorator( decoratedItem ):
            # Make sure the decorated item has a _documentation attribute, and 
            # if it doesn't, create and attach one:
            try:
                _documentation = decoratedItem._documentation
                # Make sure we're looking at the proper decorated item. If 
                # _documentation is resolved, but doesn't have the 
                # decoratedItem provided to the decorator, we need to create a 
                # *new* documentation-instance!
                if _documentation.DecoratedItem != decoratedItem:
                    decoratedItem._documentation = class_documentation( decoratedItem )
            except AttributeError:
                decoratedItem._documentation = class_documentation( decoratedItem )
                _documentation = decoratedItem._documentation
            # Return the decorated item!
            return decoratedItem
        return _InitClassDecorator

The issue noted, which was remedied by the if _documentation.DecoratedItem != decoratedItem check, boiled down to the _documentation being returned acquiring the class_documentation instance of the first super-class encountered that had one. The check forces the creation of a new class_documentation instance if the applicable decorated item (the class being decorated) doesn't match the decorated item of the found class_documentation instance. Pretty straightforward, I think.

I'm reasonably happy with this code for the time being, but once I start working out how to generate markup in a more DOM-object-oriented fashion, I suspect that I'll want to come back and re-address this. Right now, the code that generates this output is so laden with magic strings (if only mostly for tag-names) that alteration of it would be painful. On top of that, keeping track of where things actually live in the markup structure is somewhat tedious.

That, I think, pretty much wraps up the documentation, though (finally)! It took six posts (not quite 300k), over 2,000 lines of code (at just about 100k), and I suspect that it was pretty dry stuff, so I'll give some thought to what I'm going to hit next, if only to see if I can come up with something a bit more exciting.

Thursday, February 2, 2017

Documentation Decorators: Deprecated, FIXME and TODO

Another long post...

I'm hoping this isn't a pattern for all my posts, but at the same time, I'm trying to make sure that I keep all the stuff I'm writing at least somewhat logically grouped (even if I can't seem to keep it short, sweet, and to the point).

So, having given it some thought since the post before last, I think I'm going to attack what I believe to be the simpler set of choices that I left off with in my last post: Decorators for to-do, fix-me, and deprecated items. I believe these to be the simpler choice because:

  • Each of them feels to me like they should be little more than a list of string or unicode values at most; and
  • The sequence of those values doesn't strike me as being significant, at least not within the context of all to-do items, or all of any of the other types.
Consider, if you will, a function or method that has two or more of either to-do or fix-me items that should be documented. Chances are good that if there are any critical details for either, they will (should?) be documented either in the code as comments, or in some completely external document or system. All that is really relevant from an API documentation perspective is some indication that they exist, and some indication of their scope or effects, either now or as expected in the future. Any single given item, of either type, may have a lot of information associated with it, and it may even be scattered around and about several places in the code that needs the attention, but each individual item is, for all practical purposes, a single to-do or fix-me as far as the documentation itself is concerned.

Is the sequence of those items relevant from the perspective of someone reading the documentation? It might be. But I suspect that most of the time (nearly always in my experience), it won't be. If there is some urgent need for a to-do or fix-me to be resolved, it will be communicated to the developer(s) as needed, but there is no reason to commit within the documentation to any specific resolution sequence.

So, structurally, to-do and fix-me items need not be any more complicated than a couple of lists of strings in the overall metadata-structure. Something like this:

{
    'fixmes':<list<str|unicode>>,
    'todos':<list<str|unicode>>,
}

Deprecated items are even more simple, I think. Functionality that is being deprecated is going to fall into one of two categories that I can recall personally:

  • It's going to be removed because something else does the job better
  • It's going to be removed because it's no longer useful.
The latter of these is not common, in my experience, but I have seen it on occasion, usually when the no-longer-useful functionality serves no good purpose any longer and there's some impetus to keep the codebase it resides in clear of code that isn't in use. In that case, it's as much a development-policy decision as a functional one. In either case, use of the deprecated functionality should be avoided, because there's no guarantee that it will even exist in later versions. From a documentation perspective, it would be useful to provide information on the replacement functionality, if applicable. It might also be useful to provide an anticipated time-frame, whether by actual date, or by some future version indicator, when the deprecated functionality will no longer be available. None of these, though, require anything more than a simple text-value.

Here's what I expect these three decorations to look like in use, added to the Ook class that I've been using for examples for the last few posts:

class Ook( object ):
    """
Test-class."""
    # argument decorators removed for brevity
    @describe.deprecated( 'Use new_Fnord instead.' )
    def Fnord( self, arg1, arg2, *args, **kwargs ):
        """Ook.Fnord (method) original doc-string"""
        return None

    # argument decorators removed for brevity
    def new_Fnord( self, arg1, arg2, *args, **kwargs ):
        """Ook.Fnord (method) original doc-string"""
        return None

    @classmethod
    # argument decorators removed for brevity
    @describe.deprecated( 'Will be removed by version X.YY.ZZ' )
    def Bleep( cls, arg1, arg2=None, *args, **kwargs ):
        """Ook.Bleep (classmethod) original doc-string"""
        return None


    @staticmethod
    # argument decorators removed for brevity
    @describe.todo( 'Clean up output to remove empty members' )
    @describe.todo( 'Change output to class with the same interface' )
    @describe.fixme( 'Magic _parameters value needs to be removed' )
    @describe.fixme( 'Rewrite list-loops to perform the same operations in fewer passes' )
    def Flup( arg1, arg2, *args, **kwargs ):
        """Ook.Flup (staticmethod) original doc-string"""
        return None

In the data-structure, they would look like this:

{
    # argument metadata removed for brevity
    'deprecated':<str|unicode>,
    'fixmes':<list<str|unicode>>,
    'todos':<list<str|unicode>>,
}

...and that, I believe, will make their implementation easy.

Before I dive into those implementations, though it occurred to me that the returns metadata probably also falls neatly into one of these models. This realization stemmed from thinking out the answer to the question what can functions retrurn, really, when it comes right down to it? The answer is pretty much anything, really. None is the default if there is no explicit return defined. Strings and other text-values, numbers, booleans, dictionaries, sequences, and objects can all be returned. The number of permutations allowed is mathematically infinite, I think, even if the reality is much more restricted. Given that, I asked myself if it made any sense to even try to generate a decoration process that could capture all of those possibilities, when writing True if [some condition], False otherwise, or something equally difficult is so simple?

I think not. That, fortunately or not, falls squarely in the realm of expecting or requiring a certain amount of discipline in writing documentation. That same certain amount of discipline is already a requirement for providing any documentation for what a function returns in Python anyway, since there is no indication in the code itself what (if anything) will be returned. It leaves that particular part of the documentation in the same semi-nebulous state that pure doc-string documentation is in, but at least it provides a ready means of identifying what part of the documentation states what the return-value is. That, while it may not be much, is worth something to my thinking.

I'm only going to show the detailed code for one of the two variations of these constructs (one each for a list-of-strings and single-string metadata model). Apart from some name-changes, and, of course, where they are stored in the metadata data-structure, they function identically. The same set of changes need to be made in all four cases, though:

  • The existing api_documentation class needs to be altered to set up default storage;
  • A describe.[whatever] method needs to be built;
  • The __str__ method of the api_documentation class needs to be altered to output the argument-list metadata; and (since I'm documenting these decorators with themselves)
  • The documentation-decorators need to be created for anything new that's being added to the mix.
So, here we go. First, the changes to the api_documentation class:

class api_documentation( object ):
    """
Provides a common collection-point for all API documentation metadata for a 
programmatic element (class, function, method, whatever)."""

    # ...

    #####################################
    # Instance property-getter methods  #
    #####################################

    # ...

    def _GetDeprecated( self ):
        """
Gets the deprecation-information, if any, of the item that the instance has 
been used to document/describe."""
        return self._deprecated

    # ...

    def _GetReturns( self ):
        """
Gets the returns-information, if any, of the item that the instance has 
been used to document/describe."""
        return self._returns

    # ...

    #####################################
    # Instance Properties               #
    #####################################

    # ...

    deprecated = property( _GetDeprecated, None, None, 
        _GetDeprecated.__doc__)
    # ...
    returns = property( _GetReturns, None, None, 
        _GetReturns.__doc__)

    # ...

Then the additions to describe:

class describe( object ):
    """
Nominally-static class (not intended to be instantiated) that provides the 
actual functionality for generating documentation-metadata structures."""

    # ...

    @classmethod
    def deprecated( cls, information ):
        """
Decorates a function or method by attaching documentation-metadata about its 
deprecation-state to it."""
        # Type- and value-check information
        if type( information ) not in ( str, unicode ):
            raise TypeError( '%s.keywordargs expects a non-empty '
                'string or unicode text-value for its "information" argument, '
                'but was passed "%s" (%s)' % ( cls.__name__, information, 
                    type( information ).__name__ ) )
        if not information.strip():
            raise ValueError( '%s.keywordargs expects a non-empty '
                'string or unicode text-value for its "information" argument, '
                'but was passed "%s" (%s)' % ( cls.__name__, information, 
                    type( information ).__name__ ) )
        def _deprecatedDecorator( decoratedItem ):
            """
Performs the actual deprecated-state decoration"""
            # Make sure the decorated item has a _documentation attribute, and 
            # if it doesn't, create and attach one:
            try:
                _documentation = decoratedItem._documentation
            except AttributeError:
                decoratedItem.__dict__[ '_documentation' ] = api_documentation( decoratedItem )
                _documentation = decoratedItem._documentation
            # If we reach this point, then we should set the deprecated value 
            # to the information provided.
            _documentation._deprecated = information
            # Return the decorated item!
            return decoratedItem
        return _deprecatedDecorator

At this point, the documentation for the newly-decorated Ook class members looks like this (bearing in mind that describe.fixme and describe.todo are not implemented yet):

--------------------------------------------------------------------------------
Fnord( self, arg1, arg2, *args, **kwargs ) [function]
Ook.Fnord (method) original doc-string

DEPRECATED: Use new_Fnord instead.

RETURNS: None (at least until the method is implemented)

ARGUMENTS:

self .............. (instance, required): The object-instance that the method will bind to for execution.
arg1 .............. (bool|None, required): Ook.Fnord (method) arg1 description
arg2 .............. (any, required): Ook.Fnord (method) arg2 description
*args ............. (int|long|float): Ook.Fnord (method) arglist description
  - arg1 .......... (float): Ook.Fnord.args[0] description
  - arg2 .......... (int|long): Ook.Fnord.args[1] description
  - arg3 .......... (bool): Ook.Fnord.args[2] description
  - values ........ (str|unicode): Ook.Fnord.args[3] (values) description
**kwargs .......... Ook.Fnord keyword-arguments list description
  - keyword1 ...... (int|long|float, required): Ook.Fnord (method) "keyword1" description
  - keyword2 ...... (None|str|unicode, defaults to None): Ook.Fnord (method) "keyword2" description
  - keyword3 ...... (None|str|unicode): Ook.Fnord (method) "keyword3" description

--------------------------------------------------------------------------------
Bleep( cls, arg1, arg2, *args, **kwargs ) [function]
Ook.Bleep (classmethod) original doc-string

DEPRECATED: Will be removed by version X.YY.ZZ

ARGUMENTS:

cls ............... (class, required): The class that the method will bind to for execution.
arg1 .............. (int|long|float, required): Ook.Bleep (classmethod) arg1 description
arg2 .............. (any, optional, defaults to None): Ook.Bleep (classmethod) arg2 description
*args ............. (any): Ook.Bleep (classmethod) arglist description

--------------------------------------------------------------------------------

The documentation output for Ook.Bleep shows exactly the effects of not exerting that certain amount of discipline mentioned repeatedly earlier: It shows nothing for a return, not even the actual None that would be returned. I'm goiong to let that sit and ferment for a while, though — I have some ideas about how to deal with that situation, but I want to think out the ramifications of them before I commit to any of them.

On, then, to the list-of-strings items. Again, first the changes and additions to api_documentation:

class api_documentation( object ):
    """
Provides a common collection-point for all API documentation metadata for a 
programmatic element (class, function, method, whatever)."""

    # ...

    #####################################
    # Instance property-getter methods  #
    #####################################

    # ...

    def _GetFixMes( self ):
        """
Gets the "FixMe" items, if any, of the item that the instance has 
been used to document/describe."""
        return self._fixmes

    # ...

    #####################################
    # Instance Properties               #
    #####################################

    # ...

    fixmes = property( _GetFixMes, None, None, 
        _GetFixMes.__doc__ )

And the changes and additions to describe:

class describe( object ):
    """
Nominally-static class (not intended to be instantiated) that provides the 
actual functionality for generating documentation-metadata structures."""

    # ...

    #####################################
    # Class Methods                     #
    #####################################

    # ...

    @classmethod
    def fixme( cls, information ):
        """
Decorates a function or method by attaching a "fixme" item to it."""
        # Type- and value-check information
        if type( information ) not in ( str, unicode ):
            raise TypeError( '%s.keywordargs expects a non-empty '
                'string or unicode text-value for its "information" argument, '
                'but was passed "%s" (%s)' % ( cls.__name__, information, 
                    type( information ).__name__ ) )
        if not information.strip():
            raise ValueError( '%s.keywordargs expects a non-empty '
                'string or unicode text-value for its "information" argument, '
                'but was passed "%s" (%s)' % ( cls.__name__, information, 
                    type( information ).__name__ ) )
        def _fixmeDecorator( decoratedItem ):
            """
Performs the actual fixme-item decoration"""
            # Make sure the decorated item has a _documentation attribute, and 
            # if it doesn't, create and attach one:
            try:
                _documentation = decoratedItem._documentation
            except AttributeError:
                decoratedItem.__dict__[ '_documentation' ] = api_documentation( decoratedItem )
                _documentation = decoratedItem._documentation
            # If we reach this point, then we should set the returns value 
            # to the information provided.
            _documentation._fixmes.append( information )
            # Return the decorated item!
            return decoratedItem
        return _fixmeDecorator

    # ...

And the resulting output:

--------------------------------------------------------------------------------
Flup( arg1, arg2, *args, **kwargs ) [function]
Ook.Flup (staticmethod) original doc-string

FIX ME:
  - Rewrite list-loops to perform the same operations in fewer passes
  - Magic _parameters value needs to be removed

ARGUMENTS:
arg1 .............. (int|long|float, required): Ook.Flup (staticmethod) arg1 description
arg2 .............. (any, required): Ook.Flup (staticmethod) arg2 description
*args ............. (any): Ook.Flup (staticmethod) arglist description

TO DO:
  - Change output to class with the same interface
  - Clean up output to remove empty members
--------------------------------------------------------------------------------

All of these implementations were relatively painless, and none required any helper-methods like the ones created for the arguments — ultimately, they were all either concerned with either just setting a single value, or with appending something to an existing one, so there was no need to complicate things with the decorator-helper structure that's been the pattern so far.

As far as function- and method-API documentation is concerned, then, the only remaining item is documenting what errors/exceptions a callable explicitly raises.

When actually documenting exceptions that a callable raises, there are two important pieces of information:

  • What the error is; and
  • Why the error happens.
Documenting what the error is allows other developers to error-trap if/as necessary when using the callable. Knowing that if anything expected goes wrong, it will raise one of some limited number of error-types makes it relatively easy to write code to handle those errors, if it's even deemed necessary. Documenting why each error-type can happen gives insight into what not to do with the functionality. For example, if a method is documented as raising a TypeError if a certain argument is passed a non-text value, then the developer knows not to pass non-text values in that argument. Since it's possible (perhaps even likely) that multiple conditions can raise the same error-type, the internal metadata-structure should probably be built out as a dictionary of lists:

{
    'raises':<dict <Exception>:<list <str|unicode>>>,
}

The keys of that dict-structure are Exception-derived classes, which would allow the decorator-call to use the actual error-classes in their calls:

class Ook( object ):
    """
Test-class."""
    # argument decorators removed for brevity
    @describe.deprecated( 'Use new_Fnord instead.' )
    @describe.raises( NotImplementedError, 'if executed.' )
    def Fnord( self, arg1, arg2, *args, **kwargs ):
        """Ook.Fnord (method) original doc-string"""
        raise NotImplementedError( '%s.Fnord is not yet implemented' % 
            self.__class__.__name )

The implementation of related api_docuumentation is pretty typical of the other items in this post, so I won't reproduce yet another variant of it and waste your time. The decorator implementation is also pretty typical in many ways, but this is the first time that I've used this sort of dictionary structure, so it probably bears showing:

    @classmethod
    def raises( cls, errorType, errorCondition ):
        """
Decorates a function or method by attaching documentation-metadata about its 
deprecation-state to it."""
        # Type- and value-check errorType
        if not issubclass( errorType, Exception ):
            raise TypeError( '%s.raises expects a non-empty '
                'string or unicode text-value for its "errorCondition" argument, '
                'but was passed "%s" (%s)' % ( cls.__name__, errorCondition, 
                    type( errorCondition ).__name__ ) )
        # Type- and value-check errorCondition
        if type( errorCondition ) not in ( str, unicode ):
            raise TypeError( '%s.raises expects a non-empty '
                'string or unicode text-value for its "errorCondition" argument, '
                'but was passed "%s" (%s)' % ( cls.__name__, errorCondition, 
                    type( errorCondition ).__name__ ) )
        if not errorCondition.strip():
            raise ValueError( '%s.raises expects a non-empty '
                'string or unicode text-value for its "information" argument, '
                'but was passed "%s" (%s)' % ( cls.__name__, errorCondition, 
                    type( errorCondition ).__name__ ) )
        def _raisesDecorator( decoratedItem ):
            """
Performs the actual raises-state decoration"""
            # Make sure the decorated item has a _documentation attribute, and 
            # if it doesn't, create and attach one:
            try:
                _documentation = decoratedItem._documentation
            except AttributeError:
                decoratedItem.__dict__[ '_documentation' ] = api_documentation( decoratedItem )
                _documentation = decoratedItem._documentation
            # If we reach this point, then we should set the raises value 
            # to the information provided.
            try:
                _documentation._raises[ errorType ].append( errorCondition )
            except KeyError:
                _documentation._raises[ errorType ] = [ errorCondition ]
            # Return the decorated item!
            return decoratedItem
        return _raisesDecorator

With all of these decorators in place, here is the decorated code and resulting documentation for the Oook.Fnord method:

class Ook( object ):
    """
Test-class."""
    @describe.argument( 'arg1', 'Ook.Fnord (method) arg1 description', bool, None )
    @describe.argument( 'arg2', 'Ook.Fnord (method) arg2 description' )
    @describe.arglist( 'Ook.Fnord (method) arglist description', int, long, float )
    @describe.arglistitem( 0, 'arg1', 'Ook.Fnord.args[0] description', float )
    @describe.arglistitem( 1, 'arg2', 'Ook.Fnord.args[1] description', int, long )
    @describe.arglistitem( 2, 'arg3', 'Ook.Fnord.args[2] description', bool )
    @describe.arglistitem( -1, 'values', 'Ook.Fnord.args[3] (values) description', str, unicode )
    @describe.keywordargs( 'Ook.Fnord keyword-arguments list description' )
    @describe.keyword( 'keyword1', 'Ook.Fnord (method) "keyword1" description', int, long, float, required=True )
    @describe.keyword( 'keyword2', 'Ook.Fnord (method) "keyword2" description',None, str, unicode, default=None )
    @describe.keyword( 'keyword3', 'Ook.Fnord (method) "keyword3" description',None, str, unicode )
    @describe.deprecated( 'Use new_Fnord instead.' )
    @describe.returns( 'None (at least until the method is implemented)' )
    @describe.raises( NotImplementedError, 'if called' )
    @describe.todo( 'Clean up output to remove empty members' )
    @describe.todo( 'Change output to class with the same interface' )
    @describe.fixme( 'Magic _parameters value needs to be removed' )
    @describe.fixme( 'Rewrite list-loops to perform the same operations in fewer passes' )
    def Fnord( self, arg1, arg2, *args, **kwargs ):
        """Ook.Fnord (method) original doc-string"""
        raise NotImplementedError( '%s.Fnord is not yet implemented' % 
            self.__class__.__name__ )
--------------------------------------------------------------------------------
Fnord( self, arg1, arg2, *args, **kwargs ) [function]
Ook.Fnord (method) original doc-string

DEPRECATED: Use new_Fnord instead.

RETURNS: None (at least until the method is implemented)

FIX ME:
  - Rewrite list-loops to perform the same operations in fewer passes
  - Magic _parameters value needs to be removed

ARGUMENTS:

self .............. (instance, required): The object-instance that the method will bind to for execution.
arg1 .............. (bool|None, required): Ook.Fnord (method) arg1 description
arg2 .............. (any, required): Ook.Fnord (method) arg2 description
*args ............. (int|long|float): Ook.Fnord (method) arglist description
  - arg1 .......... (float): Ook.Fnord.args[0] description
  - arg2 .......... (int|long): Ook.Fnord.args[1] description
  - arg3 .......... (bool): Ook.Fnord.args[2] description
  - values ........ (str|unicode): Ook.Fnord.args[3] (values) description
**kwargs .......... Ook.Fnord keyword-arguments list description
  - keyword1 ...... (int|long|float, required): Ook.Fnord (method) "keyword1" description
  - keyword2 ...... (None|str|unicode, defaults to None): Ook.Fnord (method) "keyword2" description
  - keyword3 ...... (None|str|unicode): Ook.Fnord (method) "keyword3" description

RAISES:
 - NotImplementedError
   + if called

TO DO:
  - Change output to class with the same interface
  - Clean up output to remove empty members

There are a few other things that I want to do before I call this complete. First and foremost, is a name-change for api_documentation. That class is not a full API documentation, but merely the collection of documentation-metadata for a single function or method (a callable), so I'm going to refactor-rename it to callable_documentation.

The next thing I'm going to do is work up an HTML-savvy documentation-output mechanism for it. The text-only documentation is important, and I'll come back to it shortly, but for presentation-purposes here, I'd really like to be able to generate something that looks better.

Like, say, this:

[function]
Fnord(self, arg1, arg2, *args, **kwargs)
Ook.Fnord (method) original doc-string
Deprecated: Use new_Fnord instead.
Returns: None (at least until the method is implemented)
Fix Me:
  • Rewrite list-loops to perform the same operations in fewer passes
  • Magic _parameters value needs to be removed
Arguments
self
(instance, required): The object-instance that the method will bind to for execution.
arg1
(bool|None, required): Ook.Fnord (method) arg1 description
arg2
(any, required): Ook.Fnord (method) arg2 description
*args
(int|long|float): Ook.Fnord (method) arglist description
The following values are specified by position:
arg1
(float): Ook.Fnord.args[0] description
arg2
(int|long): Ook.Fnord.args[1] description
arg3
(bool): Ook.Fnord.args[2] description
values
(str|unicode): Ook.Fnord.args[3] (values) description
**kwargs
Ook.Fnord keyword-arguments list description
keyword1
(int|long|float, required): Ook.Fnord (method) "keyword1" description
keyword2
(None|str|unicode, defaults to None): Ook.Fnord (method) "keyword2" description
keyword3
(None|str|unicode): Ook.Fnord (method) "keyword3" description
Exceptions
NotImplementedError
if called
To-Do:
  • Change output to class with the same interface
  • Clean up output to remove empty members
[function]
Bleep(cls, arg1, arg2, *args, **kwargs)
Ook.Bleep (classmethod) original doc-string
Deprecated: Will be removed by version X.YY.ZZ
Arguments
cls
(class, required): The class that the method will bind to for execution.
arg1
(int|long|float, required): Ook.Bleep (classmethod) arg1 description
arg2
(any, optional, defaults to None): Ook.Bleep (classmethod) arg2 description
*args
(any): Ook.Bleep (classmethod) arglist description
[function]
Flup(arg1, arg2, *args, **kwargs)
Ook.Flup (staticmethod) original doc-string
Fix Me:
  • Rewrite list-loops to perform the same operations in fewer passes
  • Magic _parameters value needs to be removed
Arguments
arg1
(int|long|float, required): Ook.Flup (staticmethod) arg1 description
arg2
(any, required): Ook.Flup (staticmethod) arg2 description
*args
(any): Ook.Flup (staticmethod) arglist description
To-Do:
  • Change output to class with the same interface
  • Clean up output to remove empty members

This is a combination of a toHTML method created in callable_documentation to generate the basic documentation markup...

    def toHTML( self ):
    """
Creates and returns an HTML representation of the documentation of the item."""
    moduleName = self.DecoratedItem.__module__
    className = None
    for name, cls in inspect.getmembers( inspect.getmodule( self.DecoratedItem ), 
        inspect.isclass ):
        classMembers = dict( inspect.getmembers( cls ) )
        for classMember in classMembers.values():
            try:
                if classMember.__name__ == self.DecoratedItem.__name__:
                    className = name
            except AttributeError:
                pass
    methodName = self.DecoratedItem.__name__
    itemId = '.'.join( [ item for item in 
        [ moduleName, className, methodName ] if item ] )
    results = '<div id="%s" class="callable documentation">\n' % itemId
    results += """    <div class="heading">
    <div class="api_type">[%s]</div>
    <div class="signature"><span class="api_name">%s</span>(""" % ( 
        type( self.DecoratedItem ).__name__, self.DecoratedItem.__name__ )
    if self.arguments or self.arglist or self.kwargs:
        argItems = []
        if self._argSpecs.args:
            argItems += self._argSpecs.args
        if self._argSpecs.varargs:
            argItems.append( '*%s' % self._argSpecs.varargs )
        if self._argSpecs.keywords:
            argItems.append( '**%s' % self._argSpecs.keywords )
        results += ', '.join( argItems )
    results += """)</div>\n    </div>\n"""
    if self._originalDocstring:
        results += """    <div>%s</div>\n""" % ( 
            self._originalDocstring.strip().replace( '\n', ' ' ).replace( 
            '  ', ' ' ).replace( '  ', ' ' ) )
    if self.deprecated:
        results += """    <div class="deprecated"><strong>Deprecated:<"""
            """/strong> %s</div>\n""" % self.deprecated
    if self.returns:
        results += """    <div class="returns"><strong>Returns:</strong> """
            """%s</div>\n""" % self.returns
    if self.fixmes:
        results += """    <div class="fixme"><strong>Fix Me:</strong>\n        <ul>\n"""
        for fixme in self.fixmes:
            results += '            <li>%s</li>\n' % fixme
        results += """        </ul>\n    </div>\n"""
        results += '\n'
    if self.arguments or self.arglist or self.kwargs:
        results += """    <div><div class="subhead">Arguments</div>\n"""
        results += """        <dl class="arguments">\n"""
        for argName in self._argSpecs.args:
            results += """            <dt>%s</dt>\n""" % ( argName )
            results += """            <dd>"""
            if argName not in ( 'self', 'cls' ):
                results += '('
                if len( self.arguments[ argName ][ 'expects' ] ) > 1:
                    results += ( '|'.join( 
                        [ item.__name__ if hasattr( item, '__name__' ) 
                        else str( item ) 
                        for item in self.arguments[ argName ][ 'expects' ] 
                        ] ) ).replace( 'NoneType', 'None' )
                else:
                    if self.arguments[ argName ][ 'expects' ] != ( object, ):
                        results += self.arguments[ argName ][ 'expects' ][ 0 ].__name__
                    else:
                        results += 'any'
                if not self.arguments[ argName ][ 'hasDefault' ]:
                    results += ', required'
                else:
                    if self.arguments[ argName ][ 'defaultValue' ]:
                        results += ', optional, defaults to "%s" [%s]' % ( 
                            self.arguments[ argName ][ 'defaultValue' ], 
                            type( self.arguments[ argName ][ 'defaultValue' ] 
                            ).__name__ )
                    else:
                        results += ', optional, defaults to %s' % ( 
                            self.arguments[ argName ][ 'defaultValue' ] )
                results += '): '
                results += self.arguments[ argName ][ 'description' ]
            elif argName == 'self':
                results += '(instance, required): The object-instance that the '
                    'method will bind to for execution.'
            elif argName == 'cls':
                results += '(class, required): The class that the method will '
                    'bind to for execution.'
            results += """</dd>\n"""

        if self.arglist:
            results += """            <dt>*%s</dt>\n""" % ( 
                self.arglist[ 'name' ] )
            results += """            <dd>("""
            if len( self.arglist[ 'expects' ] ) > 1:
                results += ( '|'.join( 
                    [ item.__name__ if hasattr( item, '__name__' ) 
                        else str( item ) for item 
                        in self.arglist[ 'expects' ] 
                    ] ) ).replace( 'NoneType', 'None' )
            else:
                if self.arglist[ 'expects' ] != ( object, ):
                    results += self.arglist[ 'expects' ][ 0 ].__name__
                else:
                    results += 'any'
            results += '): ' + self.arglist[ 'description' ] + '</dd>\n'
            if self.arglist[ 'sequence' ]:
                results += """            <dd>The following values are """
                    """specified by position:</dd>\n"""
                results += """            <dd><dl>\n"""
                for argListItem in self.arglist[ 'sequence' ]:
                    results += """                <dt>%s</dt>\n""" % ( 
                        argListItem[ 'name' ] )
                    results += """                <dd>("""
                    if len( argListItem[ 'expects' ] ) > 1:
                        results += ( '|'.join( 
                            [ item.__name__ if hasattr( item, '__name__' ) 
                                else str( item ) for item 
                                in argListItem[ 'expects' ] 
                            ] ) ).replace( 'NoneType', 'None' )
                    else:
                        if argListItem[ 'expects' ] != ( object, ):
                            results += argListItem[ 'expects' ][ 0 ].__name__
                        else:
                            results += 'any'
                    results += '): ' + argListItem[ 'description' ]
                    results += """</dd>\n"""
                if self.arglist.get( 'final' ):
                    argListItem = self.arglist[ 'final' ]
                    results += """                <dt>%s</dt>\n""" % ( 
                        argListItem[ 'name' ] )
                    results += """                <dd>("""
                    if len( argListItem[ 'expects' ] ) > 1:
                        results += ( '|'.join( 
                            [ item.__name__ if hasattr( item, '__name__' ) 
                                else str( item ) for item 
                                in argListItem[ 'expects' ] 
                            ] ) ).replace( 'NoneType', 'None' )
                    else:
                        if argListItem[ 'expects' ] != ( object, ):
                            results += argListItem[ 'expects' ][ 0 ].__name__
                        else:
                            results += 'any'
                    results += '): ' + argListItem[ 'description' ]
                    results += """</dd>\n"""
                results += """            </dl></dd>\n"""
            results += """            </dd>\n"""

        if self.keywordargs:
            results += """            <dt>**%s</dt>\n""" % ( 
                self.keywordargs[ 'name' ] )
            results += """            <dd>%s</dd>\n""" % ( 
                self.keywordargs[ 'description' ] )
            if self.keywordargs[ 'keywords' ]:
                results += """            <dd><dl>\n"""
                for keyword in sorted( self.keywordargs[ 'keywords' ] ):
                    keywordItem = self.keywordargs[ 'keywords' ][ keyword ]
                    results += """                <dt>%s</dt>\n""" % ( 
                        keywordItem[ 'name' ] )
                    results += """                <dd>("""
                    if len( keywordItem[ 'expects' ] ) > 1:
                        results += ( '|'.join( 
                            [ item.__name__ if hasattr( item, '__name__' ) 
                                else str( item ) for item 
                                in keywordItem[ 'expects' ] 
                            ] ) ).replace( 'NoneType', 'None' )
                    else:
                        if keywordItem[ 'expects' ] != ( object, ):
                            results += keywordItem[ 'expects' ][ 0 ].__name__
                        else:
                            results += 'any'
                    if keywordItem[ 'required' ]:
                        results += ', required'
                    if keywordItem[ 'hasDefault' ]:
                        results += ', defaults to %s' % keywordItem[ 'defaultValue' ]
                    results += '): ' + keywordItem[ 'description' ]
                    results += """</dd>\n"""
                results += """            </dl></dd>\n"""

        results += """        </dl>\n"""
        results += """    </div>\n"""
    if self.raises:
        results += """    <div><div class="subhead">Exceptions</div>\n"""
        results += """        <dl class="exceptions">\n"""
        for errorClass in sorted( self.raises, key=lambda c: c.__name__ ):
            results += """            <dt>%s</dt>\n""" % ( errorClass.__name__ )
            for line in self.raises[ errorClass ]:
                results += """            <dd>%s</dd>\n""" % ( line )
        results += """        </dl>\n"""
        results += """    </div>\n"""
    if self.todos:
        results += """    <div class="todo"><strong>To-Do:</strong>\n        <ul>\n"""
        for todo in self.todos:
            results += '            <li>%s</li>\n' % todo
        results += """        </ul>\n    </div>\n"""
    results += '</div>\n'
    return results

...and a relatively basic style-sheet in CSS:

.callable
{}
.documentation
{ font-size: 10pt; margin-top:12pt; font-family: sans-serif; }
.api_type
{ float:right; font-size: 10pt; padding-top:2pt; }
.signature
{ font-family: monospace; margin-bottom:6pt; }
.api_name
{ font-weight:bold; }
.documentation .heading
{ clear:left; font-size: 12pt; margin:12pt 0 6pt 0; padding-top:6px; border-top:1px solid black; border-bottom: 1px solid black; }
.documentation .subhead
{ clear:left; font-weight: bold; font-size: 10pt; margin:6pt 0 3pt 0; }
.documentation dl, .documentation ul
{ margin: 0px; }
.documentation dl dt
{ clear:left; float:left; width:10em; text-align:right; margin-right:0.5em; font-weight:bold; }
.documentation dl dt:after
{ content: ':';}
.documentation dl.arguments dt
{ font-family: monospace; font-size: 9pt; }
.documentation dl dd
{ margin-left: 9.5em; }
.documentation dl dd dl
{ margin-left:-7em; }

Finally, and coming back to the plain-text documentation as promised, I'd like to be able to take the documentation-string that's generated by the callable_documentation class-instances, and stuff that into the __doc__ of the decorated functions/methods. There's some basic formatting that will need to be done as well, to keep the resulting __doc__ within an 80-character width, to provide intelligent dot-leaders and hanging indentation on the text, and maybe a few other light-weight formatting items. The main, or at least visible part, though is one more decorator-method on describe:

    @classmethod
    def AttachDocumentation( cls ):
        """
Decorates a function or method by attaching a "TODO" item to it."""
        def _AttachDocumentationDecorator( decoratedItem ):
            """
Performs the actual __doc__ attachment decoration"""
            # Get the documentation metadata
            _documentation = decoratedItem._documentation
            # Create the formatted docstring
            newDocLines = []
            line = decoratedItem.__name__
            if _documentation.arguments or _documentation.arglist or \
                _documentation.keywords:
                # It's a function or method, so generate a series of arguments, etc.
                line += '( '
                argItems = []
                argSpecs = inspect.getargspec( decoratedItem )
                if argSpecs.args:
                    argItems = argSpecs.args
                if argSpecs.varargs:
                    argItems.append( '*%s' % argSpecs.varargs )
                if argSpecs.keywords:
                    argItems.append( '**%s' % argSpecs.keywords )
                line += ', '.join( argItems )
                line += ' )'
                line = line.replace( '(  )', '()' )
            newDocLines.append( FormatLine( line, 4 ) )
            if _documentation.deprecated:
                newDocLines.append( '' )
                newDocLines.append( FormatLine( 'DEPRECATED: %s' % 
                    _documentation.deprecated, 4 ) )
            else:
                newDocLines.append( '' )
            newDocLines.append( FormatLine( _documentation._originalDocstring, 4 ) )
            if _documentation.returns:
                newDocLines.append( '' )
                newDocLines.append( FormatLine( 'RETURNS: %s' % 
                    _documentation.returns, 4 ) )
            if _documentation.fixmes:
                newDocLines.append( '' )
                newDocLines.append( FormatLine( 'FIX ME:' ) )
                for fixme in _documentation.fixmes:
                    newDocLines.append( FormatLine( '  - %s\n' % fixme, 4 ) )
            if _documentation.arguments or _documentation.arglist or \
                _documentation.keywordargs:
                newDocLines.append( '' )
                newDocLines.append( FormatLine( 'ARGUMENTS:' ) )
                # Determine the dot-leader length for all argument items in the 
                #docstring
                argNames = _documentation.arguments.keys()
                if _documentation.arglist[ 'sequence' ]: 
                    argNames += [ ' - %s' % item[ 'name' ] for item in 
                        _documentation.arglist[ 'sequence' ] ]
                    if _documentation.arglist[ 'final' ]:
                        argNames.append( ' - %s' % _documentation.arglist[ 'final' 
                            ][ 'name' ] )
                if _documentation.keywordargs.get( 'keywords' ): 
                    argNames += [ ' - %s' % _documentation.keywordargs[ 'keywords' 
                        ][ item ][ 'name' ] 
                        for item in _documentation.keywordargs[ 'keywords' ] ]
                dotLeadLen = max( [ len( item ) for item in argNames ] ) + 3
                hang = dotLeadLen + 6
                if _documentation.arguments:
#                     print argSpecs
                    for argName in [ name for name in argSpecs.args 
                        if name[ 0 ] != '*' ]:
                        if argName not in ( 'self', 'cls' ):
                            arg = _documentation.arguments[ argName ]
                            line = ( '%s ' % ( arg[ 'name' ] ) ).ljust( 
                                hang - 1, '.' ) + ' '
                            line += '('
                            if len( arg[ 'expects' ] ) > 1:
                                line += ( '|'.join( [ item.__name__ 
                                    if hasattr( item, '__name__' ) 
                                    else str( item ) for item in arg[ 
                                        'expects' ] ] ) ).replace( 
                                            'NoneType', 'None' )
                            else:
                                if arg[ 'expects' ] != ( object, ):
                                    line += arg[ 'expects' ][ 0 ].__name__
                                else:
                                    line += 'any'
                            if not arg[ 'hasDefault' ]:
                                line += ', required'
                            else:
                                if arg[ 'defaultValue' ]:
                                    line += ', optional, defaults to '
                                    '"%s" [%s]' % ( arg[ 'defaultValue' ], 
                                        type( arg[ 'defaultValue' ] ).__name__ )
                                else:
                                    line += ', optional, defaults to %s' % ( 
                                        arg[ 'defaultValue' ] )
                            line += '): '
                            line += arg[ 'description' ]
                        elif argName == 'self':
                            line = ( 'self %s (instance, required): The object-'
                                'instance that the method will bind to at '
                                'execution.' % ( '.'*dotLeadLen ) )
                        elif argName == 'cls':
                            line = ( 'self %s (class, required): The class '
                                'that the method will bind to at '
                                'execution.' % ( '.'*dotLeadLen ) )
                        else:
                            raise RuntimeError( 'oops, hahaha!')
                        newDocLines.append( FormatLine( line, hang ) )
                if _documentation.arglist:
                    line = ( '*%s ' % ( _documentation.arglist[ 'name' ] ) 
                        ).ljust( hang - 1, '.' ) + ' %s' % ( 
                            _documentation.arglist[ 'description' ] )
                    newDocLines.append( FormatLine( line, hang ) )
                    arglist = _documentation.arglist
                    if arglist[ 'sequence' ]:
                        for arg in arglist[ 'sequence' ]:
                            line = ( ' - %s ' % ( arg[ 'name' ] ) ).ljust( 
                                hang - 1, '.' ) + ' '
                            line += '('
                            if len( arg[ 'expects' ] ) > 1:
                                line += ( '|'.join( [ item.__name__ 
                                    if hasattr( item, '__name__' ) 
                                    else str( item ) for item in arg[ 
                                        'expects' ] ] ) ).replace( 
                                            'NoneType', 'None' )
                            else:
                                if arg[ 'expects' ] != ( object, ):
                                    line += arg[ 'expects' ][ 0 ].__name__
                                else:
                                    line += 'any'
                            line += '): '
                            line += arg[ 'description' ]
                            newDocLines.append( FormatLine( line, hang ) )
                    if arglist.get( 'final' ):
                        arg = arglist[ 'final' ]
                        line = ( ' - %s ' % ( arg[ 'name' ] ) ).ljust( 
                            hang - 1, '.' ) + ' '
                        line += '('
                        if len( arg[ 'expects' ] ) > 1:
                            line += ( '|'.join( [ item.__name__ 
                                if hasattr( item, '__name__' ) 
                                else str( item ) for item in arg[ 'expects' ] 
                                ] ) ).replace( 'NoneType', 'None' )
                        else:
                            if arg[ 'expects' ] != ( object, ):
                                line += arg[ 'expects' ][ 0 ].__name__
                            else:
                                line += 'any'
                        line += '): '
                        line += arg[ 'description' ]
                        newDocLines.append( FormatLine( line, hang ) )
                if _documentation.keywordargs:
                    line = ( '*%s ' % ( _documentation.keywordargs[ 'name' ] 
                        ) ).ljust( hang - 1, '.' ) + ' %s' % ( 
                            _documentation.keywordargs[ 'description' ] )
                    newDocLines.append( FormatLine( line, hang ) )
                    if _documentation.keywordargs[ 'keywords' ]:
                        for keywordItem in sorted( _documentation.keywordargs[ 
                            'keywords' ] ):
                            arg = _documentation.keywordargs[ 'keywords' ][ 
                                keywordItem ]
                            line = ( ' - %s ' % ( arg[ 'name' ] ) ).ljust( 
                                hang - 1, '.' ) + ' '
                            line += '('
                            if len( arg[ 'expects' ] ) > 1:
                                line += ( '|'.join( [ item.__name__ 
                                    if hasattr( item, '__name__' ) 
                                    else str( item ) for item in arg[ 
                                        'expects' ] ] ) ).replace( 
                                            'NoneType', 'None' )
                            else:
                                if arg[ 'expects' ] != ( object, ):
                                    line += arg[ 'expects' ][ 0 ].__name__
                                else:
                                    line += 'any'
                            if not arg[ 'hasDefault' ]:
                                line += ', required'
                            else:
                                if arg[ 'defaultValue' ]:
                                    line += ( ', optional, defaults to "%s" '
                                        '[%s]' % ( arg[ 'defaultValue' ], 
                                            type( arg[ 'defaultValue' ] 
                                                ).__name__ ) )
                                else:
                                    line += ( ', optional, defaults to %s' % ( 
                                        arg[ 'defaultValue' ] ) )
                            line += '): '
                            line += arg[ 'description' ]
                            newDocLines.append( FormatLine( line, hang ) )
            if _documentation.raises:
                newDocLines.append( FormatLine( '' ) )
                newDocLines.append( FormatLine( 'RAISES:' ) )
                for errorClass in sorted( _documentation.raises, 
                    key=lambda err: err.__name__ ):
                    newDocLines.append( FormatLine( ' - %s' % 
                        errorClass.__name__ ) )
                    for line in _documentation.raises[ errorClass ]:
                        newDocLines.append( FormatLine( '   + %s' % line, 
                            5 ) )
            if _documentation.todos:
                newDocLines.append( FormatLine( '' ) )
                newDocLines.append( FormatLine( 'TO-DO:' ) )
                for line in _documentation.todos:
                    newDocLines.append( FormatLine( ' - %s' % line ) )
            # Try to replace the current __doc__ with the new doc-string
            try:
                decoratedItem.__doc__ = ( '\n'.join( newDocLines ) ).strip()
            except:
                pass
            # Return the decorated item!
            return decoratedItem
        return _AttachDocumentationDecorator

As more documentation-decoration efforts are undertaken, I'm expecting that I'll have to come back to this method to add type-based detection to it. The reason behind that is that different documented items will have different metadata structures associated with them. Classes and properties, for example, will not have arguments of any kind. Ideally, though, I'd like to be able to apply this same decorator to any documentation-decorated item and at least not have it raise errors. Whether that will be realized is to be determined (though I already know that it won't matter for classes unless something's changed since the last time I checked).

Ultimately, all the AttachDocumentation method is doing is gathering the documentation-metadata, formatting it, and trying to attach it to the original decorated item in the existing __doc__ property. In generating the final format, it's making an attempt to stick to official (if, maybe outdated) Python conventions of an 80-character line-width, and providing some basic hanging-indentation structure. That's what the global FormatLine function's purpose is:

#####################################
# Defined functions.                #
#####################################

def FormatLine( line, hang=0, width=80 ):
    """
Formats the provided line into one-to-many lines constrained to the width (in 
spaces), with a hanging indent (also in spaces), returning those lines."""
    # First, make sure that the incoming line is just that: ONE line
    if '\n' in line or '\r' in line:
        line = line.replace( '\n', ' ' ).replace( '\r', ' ' )
        # Reduce extraneous spaces
        while '  ' in line:
            line = line.replace( '  ', ' ' )
    # set up second- and subsequent-line indent
    if hang:
        newLineStart =' ' * hang
    else:
        newLineStart = ''
    results = ''
    currentLine = ''
    tokens = line.split( ' ' )
    for token in tokens:
        if len( currentLine ) + len( token ) + 1 <= width:
            currentLine += token + ' '
        else:
            results += currentLine
            currentLine = '\n%s' % newLineStart + token + ' '
    results += currentLine
    return results.rstrip()

__all__.append( 'FormatLine' )

Printing the __doc__ of Ook.Fnord, Ook.Bleep and Ook.Flup with AttachDocumentation called on each yields:

Fnord( self, arg1, arg2, *args, **kwargs )

DEPRECATED: Use new_Fnord instead.
Ook.Fnord (method) original doc-string

RETURNS: None (at least until the method is implemented)

FIX ME:
 - Rewrite list-loops to perform the same operations in fewer passes
 - Magic _parameters value needs to be removed

ARGUMENTS:
self .............. (instance, required): The object-instance that the method 
                    will bind to at execution.
arg1 .............. (bool|None, required): Ook.Fnord (method) arg1 description
arg2 .............. (any, required): Ook.Fnord (method) arg2 description
*args ............. Ook.Fnord (method) arglist description
 - argitem1 ....... (float): Ook.Fnord.args[0] description
 - argitem2 ....... (int|long): Ook.Fnord.args[1] description
 - argitem3 ....... (bool): Ook.Fnord.args[2] description
 - values ......... (str|unicode): Ook.Fnord.args[3] (values) description
*kwargs ........... Ook.Fnord keyword-arguments list description
 - keyword1 ....... (int|long|float, required): Ook.Fnord (method) "keyword1" 
                    description
 - keyword2 ....... (None|str|unicode, optional, defaults to None): Ook.Fnord 
                    (method) "keyword2" description
 - keyword3 ....... (None|str|unicode, required): Ook.Fnord (method) "keyword3" 
                    description

RAISES:
 - NotImplementedError
   + if called

TO-DO:
 - Change output to class with the same interface
 - Clean up output to remove empty members
--------------------------------------------------------------------------------
Bleep( cls, arg1, arg2, *args, **kwargs )

DEPRECATED: Will be removed by version X.YY.ZZ
Ook.Bleep (classmethod) original doc-string

ARGUMENTS:
self ....... (class, required): The class that the method will bind to at 
             execution.
arg1 ....... (int|long|float, required): Ook.Bleep (classmethod) arg1 
             description
arg2 ....... (any, optional, defaults to None): Ook.Bleep (classmethod) arg2 
             description
*args ...... Ook.Bleep (classmethod) arglist description
--------------------------------------------------------------------------------
Flup( arg1, arg2, *args, **kwargs )

Ook.Flup (staticmethod) original doc-string

FIX ME:
 - Rewrite list-loops to perform the same operations in fewer passes
 - Magic _parameters value needs to be removed

ARGUMENTS:
arg1 ....... (int|long|float, required): Ook.Flup (staticmethod) arg1 
             description
arg2 ....... (any, required): Ook.Flup (staticmethod) arg2 description
*args ...... Ook.Flup (staticmethod) arglist description

TO-DO:
 - Change output to class with the same interface
 - Clean up output to remove empty members

With that in place, the API-documentation decorators, at least for functions and methods, is complete for now. As I start working on actual project code, I'm expecting that I'll want to come back and revisit it to deal with things like configuration-file tie-ins, and maybe other items that I'm not anticipating just yet. For now, though, it's complete.

With so many decorators in play, even just on the throw-away Ook class, it feels like it might be time to re-visit the earlier concern about performance impact. To test that, I captured the time it took to define/compile Ook, decorated as above, and the time it took to define/compile another class, Eek, that was identical except for the decoration. The bad news it that the decorated class took upwards of ten times longer to complete its definition/compilation. The good news is that even with all the decoration in place, that ten times longer is still topping out at about 0.0007 seconds, and has gotten as short as 0.0004 seconds with some frequency when run directly.

Next up will be documenting class property-members, and classes themselves, using the same sort of decoration process. See you then!