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.
to-door
fix-meitems 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-door
fix-meas 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.
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.
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.
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:
- Rewrite list-loops to perform the same operations in fewer passes
- Magic _parameters value needs to be removed
- 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
- NotImplementedError
- if called
- Change output to class with the same interface
- Clean up output to remove empty members
- 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
- Rewrite list-loops to perform the same operations in fewer passes
- Magic _parameters value needs to be removed
- 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
- 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!
No comments:
Post a Comment