The last of the argument-related documentation-decorators is for
keyword arguments — the **kwargs
in the
methods of the Ook
class that I've been using as a demonstration
class for the past few articles:
class Ook( object ):
"""
Test-class."""
@describe.argument( 'arg1', 'Ook.Fnord (method) arg1 description', int, long, float )
@describe.argument( 'arg2', 'Ook.Fnord (method) arg2 description' )
@describe.arglist( 'Ook.Fnord (method) arglist description', str, unicode )
def Fnord( self, arg1, arg2, *args, **kwargs ):
"""Ook.Fnord (method) original doc-string"""
return None
@classmethod
@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' )
def Bleep( cls, arg1, arg2=None, *args, **kwargs ):
"""Ook.Bleep (classmethod) original doc-string"""
return None
@staticmethod
@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', str, unicode )
def Flup( arg1, arg2, *args, **kwargs ):
"""Ook.Flup (staticmethod) original doc-string"""
return None
Keyword arguments, in use, are name-value pairs (e.g., keyword=value,
keyword2=value2
), and they allow a function or method to accept any
number of arbitrarily-named arguments, including no arguments. Within
the body of the function/method, they are expressed as a dict
type.
That is, a **kwargs
passed keyword=value, keyword2=value2
would be accessable in the body of the function as kwargs
, and
would contain this dict
:
{
'keyword': value,
'keyword2': value2
}
inside the function that they were provided to. Generally, in my experience,
most functions and methods that use keyword arguments have a specific set of
knownor
acceptedkeyword names, though this is not always the case. It's not hard to imagine a function that performs some operation against every member of a
dict
, and leveraging the fact that keyword
arguments are a dict
inside the function. In cases
where a keyword-argument expects certain names/values, or may not
expect them, but supplies a default value if they aren't provided, those
would be facts worthy of note in the documentation. It's also not uncommon
for the existence of one keyword to mandate the provision of another, though
that's not a pattern that I see a lot of.
As a bare minimum, then, the documentation for keyword arguments would need to be able to generate output looking something like this:
-------------------------------------------------------------------------------- Fnord( self, arg1, arg2, *args, **kwargs ) [instancemethod] Ook.Fnord (method) original doc-string ARGUMENTS: self .............. (instance, required): The object-instance that the method will bind to for execution. [arguments and argument-list removed for brevity] **kwargs .......... Ook.Fnord (method) keyword-arguments description. Known/accepted keyword values are: - keyword1 ...... (float, required): Ook.Fnord keyword1 description - keyword2 ...... (bool, defaults to False): Ook.Fnord keyword2 description - keyword3 ...... (int|long): Ook.Fnord keyword3 description.
The one thing this example output does not contain is any
sort of keyword-dependency information. Assume, for the moment, that
keyword3
is required if keyword2
is True
.
At first glance it doesn't sound too difficult to set up some process,
maybe another decorator-method, perhaps like describe.arglistitem
,
was generated for argument-lists, that allows a developer to specify a
relationship between two known keyword names. The number of possible
relationships between two such keyword names is relatively small, at least
if it's a simple condition like this keyword is required if that keyword
is a given value.
However, the number of conditions possible, and
the number of relationships possible as more known
keywords are
added grows very quickly, and I'm not confident that I'd be able
to identify even a useful fraction of the possible relationships between
any two keyword names. Off the top of my head, and without getting into
relationship-conditions that rely on specific values with more than a few
simple options (True
and False
, for example,
and maybe None
), I can think of several in a few seconds:
- Another keyword exists;
- Another keyword doesn't exist;
- Some specific combination(s) of keywords exist;
- Some specific combination(s) of keywords doesn't exist;
- Another keyword has a specific value out of a very large set of possible values (e.g., an integer value, but only -1 is significant);
- Some specific combination(s) of keywords have one or more specific values, even if those values are a small set;
If the goal of such a keyword-item decorator was to result in documentation that looked like this:
ARGUMENTS: self .............. (instance, required): The object-instance that the method will bind to for execution. [arguments and argument-list removed for brevity] **kwargs .......... Ook.Fnord (method) keyword-arguments description. Known/accepted keyword values are: - keyword1 ...... (float, required): Ook.Fnord keyword1 description - keyword2 ...... (bool, defaults to False): Ook.Fnord keyword2 description - keyword3 ...... (int|long): Ook.Fnord keyword3 description. Required if keyword2 is True.
The decoration involved is really nothing more than a specific
description add-on — something that allows the developer to specify,
as simple text, some sort of additional description that can then be
guaranteed to exist after the normal
description of an item.
I must admit that I'm on the fence about this prospect. On one hand,
it does enforce structural consistency in documentation. On the
other hand, it does so by using yet another decorator-call and
complicating the underlying data-structure (slightly) when simply adhering
to a certain amount of discipline in writing the description would take
care of the need. Admittedly, the idea of simplifying, if not enforcing
some common degree of discipline in generating documentation is what this
entire exercise is all about. This feels like it might be going too far,
though, perhaps for no good reason. Additionally, I cannot think of any real
need for the additional descriptions to be distinct elements in the
data-structure. And, as an afterthought, since it would be feasible for multiple
relationships to exist for a single keyword-name, that implies that there
would be, at a minimum, a list of relationships, which might well
have a meaningful sequence when output. Yes, I have code already
written for the describe.arglistitem
decorator that could be
adapted to handle the potential sequence-requirement, but then the trade-off
is that each and every keyword-relationship would have to have a sequence
set. That feels like way more work just in decorating functions
and methods than I'd be willing to undertake, especially since I could
just as easily just write those relationship-items in the
keyword-item decoration.
I may revisit this idea later, but for now, I think I'll leave the
relationship
decorator idea alone. It just feels too complicated all
around for what feels like no real gain.
So, what would the underlying metadata data-structure for keyword arguments look like? Something like this, I think:
{
# arguments omitted for brevity
# argument-list omitted for brevity
'keywords':{
'description':<str|unicode>,
'keywords':{<dict <str|unicode>:<dict> >,
# <str|unicode>,: {
# 'defaultValue': <object>,
# 'description': <str|unicode>,
# 'expects': <tuple <type*> >,
# 'hasDefault': <bool>,
# 'name': <str|unicode>
# 'required': <bool>,
# },
'name':<str|unicode>,
}
}
}
This structure is very similar to the structures defined for arguments and
argument-list items. In fact, apart from the addition of the required
member, it is identical to the structure of a normal
argument's
metadata. The required
field in the dictionary is a necessary addition
because there is no way to infer from the keyword-argument's specification
itself whether it has any expected items, let alone whether those items are
required. The hasDefault
member, while it is common between the two
structures, faces a similar issue in the context of a keyword-item decorator,
for much the same reason as the required
member: There's no way to
ascertain what a default for a given keyword would be, since there's
no way to determine that the keyword is known
— it has to be
specified.
In the interests of consistency, I'd like, if I can, to keep the keyword-item decorator looking as much like the existing decorators as I can manage. I expect the main keyword-argument decorator to be very straightforward: All it needs is a description, since its name can be gleaned from the function or method by inspection. The keyword-item decorator should look very much like athe existing argument decorator, since it will have many of the same elements and requirements. Even if there's nothing else, it just feels good to have the same sequence of arguments (if possible) for similar decoration efforts.
The fact that keyword-items can have either default values or required flags is, I think, the only thing that will potentially interfere with keeping that consistency. My initial thought was to simply add them in as additional standard arguments in the signature, looking something like this:
@classmethod
def keyworditem( cls, name, description='No description provided', required=False,
default=None, *expects ):
There are at least two issues with taking this approach, though. The first
is that required and default in this method are almost certainly going to be
mutually exclusive. That is, if a keyword-item name has a default value, it
cannot be required, since the function or method should be executable
without supplying the required
name. That means that whichever of the
two arguments is first in the argument sequence must always be passed
a value if the second is used. In the example above, any item that has a
default
must still be explicitly passed a required
of
False
. That's do-able, and might even be manageable without being...
horribly inelegant, maybe... by generating some sort of constant like
describe.NOT_REQUIRED
and passing that when necessary. But it just
feels stupid to me to have to do that, and I don't like it.
The second flaw, and the more important of the two, I think, is that by
setting up default
as an argument with a default value, the method
would be assuming that either all keyword-items have a default value
(of None
in this case), or that no keyword-item can have
a default value of whatever the argument's default is (None
again
in this case). Again, setting up some sort of specific constant value that
indicates that the default doesn't really exist would be viable as a solution,
but it also feels stupid.
I arrived at what I think is a much better solution by asking myself why this
wasn't a problem with the describe.argument
decorator. The answer to
that question is that in describe.argument
, the existence of a default
value could be ascertained by whether or not a default value actually existed.
That is, as part of all of the other decoration methods created so far, a call
is made to inspect.getargspec
, and the resulting argument-specification
is examined looking for the existence of a default value corresponding
with each argument. If it doesn't exist, then there is no default
value, not even None
. There is a way to achieve a similar
does/does-not exist
state for arguments: Using a keyword-argument list:
@classmethod
def keyworditem( cls, name, description='No description provided', *expects,
**requiredOrDefault ):
This approach, I think, is as good as it's going to get:
- Keyword-items that have no default and are not required use the
exact same argument-sequence as the
argument
decorator. It keeps decorator calls consistent (which I really like). - There is never any requirement to specify either a required flag or default value as not existing, since if they aren't provided, they don't exist. It doesn't require stupid/inelegant argument structure (which is a Good Thing®™).
- If there ever arose an actual need to flag something as required, and provide it with a default value, as unlikely as I think that is, it's still a viable option.
- If there ever arose a need for some other documentation-item that I haven't thought of yet, there is at least a possibility that it could fit as just another keyword in the decorator's arguments.
The implementation-steps for keyword-argument decoration are pretty similar to those for argument-list decoration:
- The existing
api_documentation
class needs to be altered to set up default keyword-argument metadata structures; - The
describe.keywordargs
method needs to be built; - The
describe.keyworditem
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.
The implementation of these methods was pretty quick and easy, since a fair
portion followed a pattern similar to something already in place in earlier
argument
or arglist
implementations. Starting with
api_documentation
:
class api_documentation( object ):
# ...
#####################################
# Instance property-getter methods #
#####################################
# ...
def _GetKeywordArgs( self ):
"""
Gets the dictionary of the keyword-arguments that the instance has been used to
document/describe:
'keywordargs': {
'description':<str|unicode>,
'keywords':{
<str|unicode name>:
{
'defaultValue':<None|object>,
'description':<str|unicode>,
'expects':<tuple<types>>,
'hasDefault':<bool>,
'name':<str|unicode>,
'required':<bool>,
}
},
'name':<str|unicode>,
}"""
return self._keywordArgs
# ...
#####################################
# Instance Properties #
#####################################
# ...
keywordargs = property( _GetKeywordArgs, None, None,
_GetKeywordArgs.__doc__.strip() )
# ...
#####################################
# Instance Methods #
#####################################
# ...
def AddKeywordargs( self, callable, description ):
"""
Adds keyword-args-metadata for the specified argument to the instance's
documentation-metadata."""
if not self._keywordArgs:
# If it doesn't exist, create it
self._keywordArgs = self._createKeywordargsMetadata(
callable, description )
else:
# If it DOES exist, update it (partially)
self._keywordArgs[ 'description' ] = description
def AddKeyword( self, callable, name, description, *expects, **options ):
"""
Adds keyword-args-metadata for the specified argument to the instance's
documentation-metadata."""
if not self._keywordArgs:
# If it doesn't exist, create it
self._keywordArgs = self._createKeywordargsMetadata( callable )
if self._keywordArgs[ 'keywords' ].get( name ):
raise DescriptionError( 'The "%s" keyword has already been '
'described for the %s %s' % ( name, callable.__name__,
type( callable ).__name__ ) )
metadata = {
'defaultValue':options.get( 'default' ),
'description':description,
'expects':( object, ),
'hasDefault':( 'default' in options.keys() ),
'name':name,
'required':False,
}
if expects:
metadata[ 'expects' ] = expects
if 'required' in options and 'default' in options:
raise DescriptionError( 'The "%s" keyword cannot be both required '
'and optional with a default value' % (
name, callable.__name__ ) )
required = options.get( 'required' )
if required != None and required in ( True, False ):
metadata[ 'required' ] = required
else:
if required != None:
raise TypeError( 'The "required" keyword expects a boolean '
'(True|False) value, but was passed "%s" (%s)' % (
required, type( required ).__name__ ) )
self._keywordArgs[ 'keywords' ][ name ] = metadata
# ...
def _createKeywordargsMetadata( self, callable,
description='No description provided' ):
"""
Creates and returns a metadata-dictionary for a function or method keyword-
arguments list.
RETURNS
- dict of metadata values
ARGUMENTS:
callable ......... (callable, required) The function or method whose arglist-
metadata is being created
description ...... (str|unicode, required) The description of the arglist.
RAISES:
- TypeError:
+ If the supplied callable is not a function, method, or generator
- ValueError:
+ If a value for an argument is invalid
"""
# Type-check callable
if not inspect.isfunction( callable ) and \
not inspect.isgenerator( callable ) and \
not inspect.ismethod( callable ):
raise TypeError( '%s._createKeywordargsMetadata expects a '
'callable (function, generator or method) in its "callable" '
'argument, but was passed "%s" (%s)' % (
self.__name__, callable,
type( callable ).__name__ ) )
# Get the argspecs of the callable if necessary
if not self._argSpecs:
self._argSpecs = inspect.getargspec( callable )
# Set up the default structure:
results = {
'name':self._argSpecs.keywords,
'description':None,
'keywords':{}
}
# Type- and value-check description. Any non-empty value is kosher,
# since a description should, in theory, be unrestricted.
if type( description ) not in ( str, unicode ):
raise TypeError( '%s._createKeywordargsMetadata expects a non-'
'empty string or unicode text-value for its "description" '
'argument, but was passed "%s" (%s)' % (
self.__name__, description,
type( description ).__name__ ) )
if not description.strip():
raise ValueError( '%s._createArgumentMetadata expects a non-empty '
'string or unicode text-value for its "description" argument, '
'but was passed "%s" (%s)' % ( self.__name__, description,
type( description ).__name__ ) )
# Description checks out, so add it to the structure.
results[ 'description' ] = description
# Return the structure
return results
There's not much here that's significantly different than previous
implementations for other decorators, though the use and initial population of
metadata
values in AddKeyword
is, perhaps, a more
elegant means of generating the data-structure, since it captures almost
everything in one chunk of execution.
The additional implementation items in describe
also look
very similar to previous decorator-implementations:
class describe( object ):
"""
Nominally-static class (not intended to be instantiated) that provides the
actual functionality for generating documentation-metadata structures."""
#####################################
# Class attributes (and instance- #
# attribute default values) #
#####################################
# ...
#####################################
# Class Methods #
#####################################
# ...
@classmethod
def keyword( cls, name, description='No description provided',
*expects, **options ):
"""
Decorates a function or method by attaching documentation-metadata about a
keyword-argument list item in the metadata for the decorted item's keywords
metadata."""
# Type-check name it must be a string or unicode value
if type( name ) not in ( str, unicode ):
raise TypeError( 'describe.argument expects a non-empty string or '
'unicode text-value containing an argument-name for the item '
'being decorated, but was passed "%s" (%s)' % (
name, type( name ).__name__ ) )
# Type- and value-check description
if type( description ) not in ( str, unicode ):
raise TypeError( '%s.argument expects a non-empty '
'string or unicode text-value for its "description" argument, '
'but was passed "%s" (%s)' % ( cls.__name__, description,
type( description ).__name__ ) )
if not description.strip():
raise ValueError( '%s.argument expects a non-empty '
'string or unicode text-value for its "description" argument, '
'but was passed "%s" (%s)' % ( cls.__name__, description,
type( description ).__name__ ) )
def _keywordDecorator( 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.__dict__[ '_documentation' ] = api_documentation(
decoratedItem )
_documentation = decoratedItem._documentation
# If we reach this point, then we should add the item to the
# metadata, but first we have to create the metadata entry...
_documentation.AddKeyword( decoratedItem, name, description,
*expects, **options )
# Return the decorated item!
return decoratedItem
return _keywordDecorator
@classmethod
def keywordargs( cls, description='No description provided' ):
"""
Decorates a function or method by attaching documentation-metadata about its
keyword-argument list - a description of it."""
# Type- and value-check description
if type( description ) not in ( str, unicode ):
raise TypeError( '%s.keywordargs expects a non-empty '
'string or unicode text-value for its "description" argument, '
'but was passed "%s" (%s)' % ( cls.__name__, description,
type( description ).__name__ ) )
if not description.strip():
raise ValueError( '%s.keywordargs expects a non-empty '
'string or unicode text-value for its "description" argument, '
'but was passed "%s" (%s)' % ( cls.__name__, description,
type( description ).__name__ ) )
def _keywordargsDecorator( decoratedItem ):
"""
Performs the actual keyword-args 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 add the item to the
# metadata, but first we have to create the metadata entry...
_documentation.AddKeywordargs( decoratedItem, description )
# Return the decorated item!
return decoratedItem
return _keywordargsDecorator
Throwing a few keyword-decorations onto the Ook
test-class:
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 )
def Fnord( self, arg1, arg2, *args, **kwargs ):
"""Ook.Fnord (method) original doc-string"""
return None
The resulting documentation-output is:
-------------------------------------------------------------------------------- Fnord( self, arg1, arg2, *args, **kwargs ) [function] Ook.Fnord (method) original doc-string 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
And that is pretty much exactly what I wanted.
That takes care of all the decorators for function- and method-arguments that Python supports, but there are a few other items that I think are nearly as important. Several posts back, I noted a fairly detailed list of what I think needs to be present (if applicable) in every callable's documentation. In a somewhat cleaner form, that list is:
- A description of the method.
Done, uses thenormal
doc-string that is returned as the__doc__
of an element; - A description of each argument
Done. - A description of the argument-list (and any specific meaning of items
in that sequence)
Done. - A description of the keyword-arguments (and any keywords that have
specific meaning or use in that context)
Done. - What the callable returns, if anything (with
None
being the default if nothing is returned); and - What exceptions might be raised, and the conditions under which they would be raised.
- ToDo
- Used in the documentation to tag items that are going to be worked on, but that should represent little or no risk of introducing breaking changes when they are addressed.
- FixMe
- Used in the documentation to tag items that are in need of potentially significant re-work, that need attention because of inefficiencies, or that may otherwise be considered less-than-stable, but were part of the current version for some reason nevertheless.
- Deprecated
- Used in the documentation to tag items that are going to go away in the foreseeable future.
So, my plan is to pick up next time with either the three new items or the remaining two from the original list. I don't know which one I'm going to tackle just yet, though, so come back next time and see for yourself...
No comments:
Post a Comment