Tuesday, January 31, 2017

Interlude: Do Decorators Impact Performance?

It may occur to you that calling several decorator functions on several elements (especially functions and methods, since I expect there will be more of those than any other single element-type, and they could have any number of arguments, argument-list and keyword-item documentation-items) might impact performance. Will it? The short answer is I'm not sure, but I don't think so. The thinking behind that answer depends heavily on how I expect most of my development to take shape:

  • Most of the code I plan on writing I expect to be of a class-library sort of structure: A number of packages and modules that will be imported into the real code.
  • As a result, even in cases where there are a lot of decorator-calls taking place, they will happen during the import-process far more often than during real execution. Since an import results in a compiled (*.pyc) file, but only if the corresponding *.py file is newer, decorators will usually run only once each.
These interactions can be demonstrated easily enough...

Standing in for a final program, here is execution.py, which imports and executes MyFunction from decorated.py.

#!/usr/bin/env python
"""execution.py - Decorator proof-of-concept code: A program that calls a 
decorated function"""
import time
startTime = time.time()
print '# +- Importing from decorated'
from decorated import MyFunction
print '# +- Import from decorated complete in %0.6f seconds' % ( 
    time.time() - startTime )

print '# +- Executing in execution'
startTime = time.time()
print '#'*80
MyFunction( 'arg-value 1' )
print '#' + '-'*78 + '#'
MyFunction( 'arg-value 2' )
print '#' + '-'*78 + '#'
MyFunction( 'arg-value 3' )
print '#' + '-'*78 + '#'
MyFunction( 'arg-value 4' )
print '#' + '-'*78 + '#'
print '# +- Executing in execution completed in %0.6f seconds' % ( 
    time.time() - startTime )

Standing in for a fully-decorated class-library, here is decorated.py, which defines the MyFunction function called above, decorating it with the argument function from decorators.py.

#!/usr/bin/env python
"""decorated.py - Decorator proof-of-concept code: The decorators to import"""
import time
startTime = time.time()
print '#    +- Importing from decorators'
from decorators import argument
print '#    +- Import from decorators complete in %0.6f seconds' % ( 
    time.time() - startTime )

@argument( 'argname', 'description' )
def MyFunction( argname ):
    print 'MyFunction( %s <%s> ) called.' % ( argname, 
        type( argname ).__name__ )

Finally, standing in for the documentation-metadata decorators to be built, here is decorators.py.

#!/usr/bin/env python
"""decorators.py - Decorator proof-of-concept code: The decorators to import"""

def argument( name, description, *expects ):
    """
Decorates a function by adding an entry to its __documentationMetadata element 
for the supplied argument."""
    if not expects:
        expects = None
    print '# +- Calling argument( %s <%s>, %s <%s>, %s )' % (
        name, type( name ).__name__, description, type( description ).__name__, 
        str( expects ) )
    def _decorateAddArgument( decoratedItem ):
        """
Performs the actual decoration."""
        print '#    +- Calling _decorateAddArgument( %s <%s> )' % ( 
            decoratedItem.__name__, type( decoratedItem ).__name__ )
        if not hasattr( decoratedItem, '__documentationMetadata' ):
            decoratedItem.__documentationMetadata = {
                'arguments':{},
                }
        decoratedItem.__documentationMetadata[ 'arguments' ][ name ] = {
            'name':name,
            'description':description,
            'expects':expects,
        }
        print '#    +- _decorateAddArgument() complete' 
        return decoratedItem
    print '# +- argument() call complete'
    return _decorateAddArgument

Output from first run of code, no pyc files present.

# +- Importing from decorated
#    +- Importing from decorators
#    +- Import from decorators complete in 0.000471 seconds
# +- Calling argument( argname <str>, description <str>, None )
# +- argument() call complete
#    +- Calling _decorateAddArgument( MyFunction <function> )
#    +- _decorateAddArgument() complete
# +- Import from decorated complete in 0.000993 seconds
# +- Executing in execution
################################################################################
MyFunction( arg-value 1 <str> ) called.
#------------------------------------------------------------------------------#
MyFunction( arg-value 2 <str> ) called.
#------------------------------------------------------------------------------#
MyFunction( arg-value 3 <str> ) called.
#------------------------------------------------------------------------------#
MyFunction( arg-value 4 <str> ) called.
#------------------------------------------------------------------------------#
# +- Executing in execution completed in 0.000265 seconds

################################################################################
mtime for decorated.pyc .... 14:50:44
mtime for decorators.pyc ... 14:50:44

Output from second run of code, pyc files present.

# +- Importing from decorated
#    +- Importing from decorators
#    +- Import from decorators complete in 0.000157 seconds
# +- Calling argument( argname <str>, description <str>, None )
# +- argument() call complete
#    +- Calling _decorateAddArgument( MyFunction <function> )
#    +- _decorateAddArgument() complete
# +- Import from decorated complete in 0.000546 seconds
# +- Executing in execution
################################################################################
MyFunction( arg-value 1 <str> ) called.
#------------------------------------------------------------------------------#
MyFunction( arg-value 2 <str> ) called.
#------------------------------------------------------------------------------#
MyFunction( arg-value 3 <str> ) called.
#------------------------------------------------------------------------------#
MyFunction( arg-value 4 <str> ) called.
#------------------------------------------------------------------------------#
# +- Executing in execution completed in 0.000142 seconds

################################################################################
mtime for decorated.pyc .... 14:50:44
mtime for decorators.pyc ... 14:50:44

The initial run, where the .pyc files are being compiled, does take a little bit longer, and I have to admit that I was surprised to see that the argument() decorator-call executed during the import of decorated, but even with that, the total run-time for that import was still roughly half of what it was during the first run. I'd conclude that there is some (small) performance hit that surfaces because of the decoration, but after adding some time-tracking code to the actual decorator, its run-time seems to stay pretty consistently between 0.0001 and 0.0002 seconds, so the hit is very minimal. While I was surprised that the decorators were executed on each and every import, in retrospect, it makes perfect sense — the import, even of a compiled .pyc file, still has to be interpreted by the runtime engine, and the decorators would logically have to be executed as part of that interpretation process. I briefly added a class with a similar (and decorated) method and calls to that method of an instance to see if the pattern holds true for class-level method also, and it does:

class MyClass( object ):
    @argument( 'argname', 'description' )
    def MyMethod( self, argname ):
        print 'MyFunction( %s <%s> ) called.' % ( 
            argname, type( argname ).__name__ )
# First run:
# ...
# +- Calling argument( argname <str>, description <str>, None )
# +- argument() call complete
#    +- Calling _decorateAddArgument( MyMethod <function> )
#    +- _decorateAddArgument() complete in 0.000023 seconds
# +- Import from decorated complete in 0.017891 seconds
# +- Executing in execution
# ...
# +- Executing in execution completed in 0.000042 seconds

# Second run:
# ...
# +- Calling argument( argname <str>, description <str>, None )
# +- argument() call complete
#    +- Calling _decorateAddArgument( MyMethod <function> )
#    +- _decorateAddArgument() complete in 0.000023 seconds
# +- Import from decorated complete in 0.000500 seconds
# +- Executing in execution
# ...
# +- Executing in execution completed in 0.000037 seconds

So, while there is some small performance hit, it is small — and well within what I'd consider an acceptable range.

Thursday, January 26, 2017

Documentation Decorators: Keyword Arguments

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 known or accepted keyword 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;
I suspect that the number of useful possibilities is huge. As a result, the number of conditions, variations, or whatever of decorators to handle those permutations would also be huge.

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 the normal 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.
I'd like to have those last two in place before I call the callable documentation done. I'd also like to add a few new items to that list, I think:
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.
Ideally, a FixMe should never make its way into live code, but we don't live in an ideal world, so having some sort of indicator of functionality that is sketchy, likely to change, or otherwise not yet ready for prime time is probably a good idea. I'd rather have it and not need it than need it and not have it, at any rate. I've not yet worked on a project with a long enough lifespan to need a Deprecated in play either, but again, I'd rather have it and not need it than need it and not have it.

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...

Tuesday, January 24, 2017

Documentation Decorators: Argument Lists

Apologies...

This post turned out to be a lot longer than I'd anticipated it would be when I started writing it. I struggled for a while with trying to find one or more good break-points, in order to split it into two or more smaller posts (easier to digest), but there wasn't anything that felt like a good spot, so this is long...

So, picking up where I left of with my last post, today I'm planning to tackle the documentation-metadata structure for argument lists. Like arguments, arglist documentation should have a description, and should allow but not require the specification of expected value-types. Since only one arglist is allowed on any callable in Python, there's no need to specify the name the way the describe.argument decorator does for arguments, but the name should still be stored as part of the metadata structure, if only for consistency. The data-structure should, then, be somewhat familiar, looking much like the structure for arguments, but would be stored as a separate metadata-item in the data-structure. By way of example, consider the Ook test-class defined last time, with describe.arglist decorators set up for its methods:

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

In its most basic form, the metadata for the args arglist would be very simple. In context, with the metadata for the arguments, the data-structure would look something like this:

{
    'arg1': {
        'description': 'Ook.Fnord (method) arg1 description',
        'expects': ( <type 'int'>, <type 'long'>, <type 'float'>),
        'hasDefault': False,
        'name': 'arg1'
        },
    'arg2': {
        'description': 'Ook.Fnord (method) arg2 description',
        'expects': (<type 'object'>,),
        'hasDefault': False,
        'name': 'arg2'
        },
    'arglist': {
        'description': 'Ook.Fnord (method) arglist description',
        'expects': (<type 'str'>, <type 'unicode'>),
        'name': 'args'
        }
}

That would cover the most basic expected arglist usage pretty nicely. However, there are some additional wrinkles to consider. One is that the generally-accepted way to define functions that have variable but specifically-sequenced arguments is to use arglists and assign values based on the existence and position of the value in the argument-list sequence. That explanation got away from me, so let me provide an example. Say I want a function that might be documented as MyFunction( [spam, [eggs, [beans]] ). That is, MyFunction accepts any of the following:

  • MyFunction()
  • MyFunction( 1 )
  • MyFunction( 1, 2 )
  • MyFunction( 1, 2, 3 )
where the first value is spam, the second is eggs and the third beans. If any given value is provided, all of its predecessors in the function's signature must be provided as well. So beans require spam and eggs. That function might look something like this:

def MyFunction( *arguments ):
    """
An example of a function that can accept zero-to-many arguments with 
specific meanings based on their position in the argument-list."""
    print '#' + '-'*78 + '#'
    print 'MyFunction%s called' % ( str( arguments ) )
    # Get the first three items from the arguments list *plus* three 
    # None values so that zero-to-three values are populated (with 
    # None as a default if not provided)
    spam, eggs, beans = tuple( ( list( arguments[ 0:3 ] ) + [ None, None, None ] )[0:3] )
    if arguments:
        if spam:
            print 'spam .... %s' % spam
        if eggs:
            print 'eggs .... %s' % eggs
        if beans:
            print 'beans ... %s' % beans
    else:
        print 'No spam, eggs or beans specified'

If MyFunction is executed in any of several ways:

MyFunction()
MyFunction( 1 )
MyFunction( 1, 2 )
MyFunction( 1, 2, 3 )
MyFunction( 'spam', 'spam' )

then the results, I hope, make the idea more clear:

#------------------------------------------------------------------------------#
MyFunction() called
No spam, eggs or beans specified
#------------------------------------------------------------------------------#
MyFunction(1,) called
spam .... 1
#------------------------------------------------------------------------------#
MyFunction(1, 2) called
spam .... 1
eggs .... 2
#------------------------------------------------------------------------------#
MyFunction(1, 2, 3) called
spam .... 1
eggs .... 2
beans ... 3
#------------------------------------------------------------------------------#
MyFunction('spam', 'spam') called
spam .... spam
eggs .... spam

Given that it's possible (and even probable) that a function or method will be defined using a variable number of specific-purpose arguments, the question that should, hopefully, come to mind is What if those arguments have specific descriptions or type-expectations? The metadata structure shown so far would most emphatically not handle that case. It would require something with more detail, along the lines of:

{
    # arguments removed for brevity...
    'arglist': {
        'description': 'Ook.Fnord (method) arglist description',
        'expects': (<type 'str'>, <type 'unicode'>),
        'name': 'args',
        'sequence':[
                {
                    'name':'spam', 
                    'defaultValue':None,
                    'description':'The "spam" value to use', 
                    'expects':(<type 'str'>, )
                },
                {
                    'name':'eggs', 
                    'defaultValue':None,
                    'description':'The "eggs" value to use', 
                    'expects':(<type 'int'>, <type 'long'>)
                },
                {
                    'name':'beans', 
                    'defaultValue':None,
                    'description':'The "beans" value to use', 
                    'expects':(<type 'bool'>, None)
                },
            ]
        }
}

Ideally, the decoration that generates that structure would look something like this, then:

@describe.argument( 'arg1', 'Ook.Fnord (method) arg1 description', int, long )
@describe.argument( 'arg2', 'Ook.Fnord (method) arg2 description' )
@describe.arglist( 'Ook.Fnord (method) arglist description', str )
@describe.arglistitem( 0, 'spam', None, 'The "spam" value to use', str, unicode )
@describe.arglistitem( 1, 'eggs', None, 'The "eggs" value to use', int, long )
@describe.arglistitem( 2, 'beans', None, 'The "beans" value to use', bool, None )
def Fnord( self, arg1, arg2, *args, **kwargs ):
    """Ook.Fnord (method) original doc-string"""
    return None

The arguments for the describe.arglistitem decorator are:

  1. A sequence value: The position of the sequence-item in the argument list's sequence. I'd rather not have to specify a sequence, frankly, but since there really is no way to enforce decorator-ordering (and they fire from last to first anyway, on top of that), I don't see any good way around that.
  2. A name. The name is not as formal as an argument's name, since it's really just there for programmer reference;
  3. A default value. There are some considerations about providing a default value that I want to discuss, so more on that in a bit;
  4. A description; and
  5. Zero-to-many expected types for the sequence-item. I expect this to behave pretty much exactly like the expects values in describe.argument.
The end goal for the documentation generated should, then, look something like this (as plain text):

--------------------------------------------------------------------------------
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.
arg1 .............. (int|long|float, required): Ook.Fnord (method) arg1 description
arg2 .............. (any, required): Ook.Fnord (method) arg2 description
args .............. (str, optional): Ook.Fnord (method) arglist description. Specific values accepted, in order, are:
  spam ............ (str|unicode, optional, defaults to None): The "spam" value to use
  eggs ............ (int|long, optional, defaults to None): The "eggs" value to use
  beans ........... (bool|None, optional, defaults to None): The "beans" value to use

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

There are a few caveats and items of note that I think arise from this set-up. First and foremost is that if sequence-items are provided, they must, eventually, resolve to a contiguous list. That is, if the spam and beans items in the example above are specified, then the sequence-item between them (eggs) must also be specified somewhere along the line for the documentation to be accurate and meaningful. If eggs was not specified as an argument sequence item, and there was no generated display as a result, then the documentation would indicate (inaccurately) that spam and beans were the only meaningful items in the argument-sequence, and anyone relying on the documentation would not expect their beans values to appear as eggs. That would be a problem. To prevent that from becoming an issue, I'm planning to put a default metadata-structure in place in any blank spots in the sequence, probably as part of the process of creating or updating the sequence member of the metadata-structure.

Another consideration stems from the fact that a Python argument-list is, for all practical purposes, allowed to be arbitrarily long. Even if, as in this case, some number of arguments are documented as having specific meanings in the sequence, *args can have additional items after those. So, for example, a call like

Fnord( 'spam', 1, True, 'these', 'are', 'additional', 'values' )
is perfectly valid from the standpoint of the syntax of the language, even if it makes no sense in the context of the function. How should that kind of scenario be handled? Honestly, I don't know. In cases where the function is only expecting those first three (or however-many) sequenced items, I'd implement sequence-length checking and raise an error if too many arguments were supplied. But that wouldn't help in cases where there's a legitimate need for a potentially-infinite remaining set of sequence-items, and there are legitimate uses for that sort of structure. I have in mind a variant of a min or max function that supplies one or more cut-off values – something that is equivalent to give me the minimum/maximum value of all supplied values that is also greater than x and optionally less than y, though that case might be better handled with an optional standard argument for y. In any case, the potential for legitimate, continuous sequences of values after the ones that have specific meaning exists, and should be represented in the documentation somehow. My gut feeling is to allow a special sequence-number, -1, for example, that indicates that the documentation-structure applies to all remaining values. Something like this, maybe:

@describe.arglistitem( -1, 'values', None, 'The values to process', int, long )
def Fnord( self, arg1, arg2, *args, **kwargs ):
    # ...

Another wrinkle is that if there are specific meanings associated with each argument in the sequence and the sequence is limited, it's possible (probable, even, maybe?) that the normal type-specification of the argument-list is meaningless. That is, using the same Fnord function above, but constrained in the function so that it only accepts the spam, eggs, and beans items, the general (str, optional) description of the argument-list as a whole is unnecessary as well as being inaccurate. I'm almost certain that it would be possible to write code that would handle all of the various logically-legitimate variants of an argument-list as far as documentation-decoration were concerned, but it seems to me that doing so would complicate things more than I'd like — the premise of these decorators is Documentation Made Easy, after all, not Documentation Made Complicated. I think, in order to keep things easy, a reasonable balance between simplicity and hard-and-fast structure probably requires a rule-set about like this one:

  • Argument-lists with no associated sequence simply use the baseline decorator, with results that look like a decorated argument:
    args .............. (str, optional): Ook.Fnord (method) arglist description.
  • Argument-lists that specify one-to-many sequence items, but that do not have an all-remaining-items sequence-item specified do not show type-information in documentation except those associated with the sequence-items:
    args .............. Ook.Fnord (method) arglist description. Specific values accepted, in order, are:
      spam ............ (str|unicode, optional, defaults to None): The "spam" value to use
      eggs ............ (int|long, optional, defaults to None): The "eggs" value to use
  • Argument-lists that specify one-to-many sequence-items and an all-remaining-items at the end of the sequence show the type-specification of the argument-list as a whole for those, remaining items, and use the all-remaining-items description. Using the example -1 describe.arglistitem decorator-call above, that would yield:
    args .............. Ook.Fnord (method) arglist description. Specific values accepted, in order, are:
      spam ............ (str|unicode, optional, defaults to None): The "spam" value to use
      eggs ............ (int|long, optional, defaults to None): The "eggs" value to use
      values* ......... (str, optional, defaults to None): The values to process.
The complete decoration for that last case would look like this, then:

@describe.argument( 'arg1', 'Ook.Fnord (method) arg1 description', int, long )
@describe.argument( 'arg2', 'Ook.Fnord (method) arg2 description' )
@describe.arglist( 'Ook.Fnord (method) arglist description' )
@describe.arglistitem( 0, 'spam', None, 'The "spam" value to use', str, unicode )
@describe.arglistitem( 1, 'eggs', None, 'The "eggs" value to use', int, long )
@describe.arglistitem( -1, 'values', None, 'The values to process', str )
def Fnord( self, arg1, arg2, *args, **kwargs ):
    """Ook.Fnord (method) original doc-string"""
    return None

So: Implementation time! There are a few things that need to be done:

  • The existing api_documentation class needs to be altered to set up default argument-list metadata structures;
  • The describe.arglist method needs to be built;
  • The describe.arglistitem method needs to be built;
  • The __str__ method of the api_documentation class needs to be altered to output the argument-list metadata;
The changes to api_documentation are pretty straightforward, given the pattern established with the various argument-specific functionality already in place:

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 _GetArglist( self ):
        """
Gets the dictionary of the argument-list that the instance has been used to 
document/describe:
'arglist': {
    'description':<str|unicode>,
    'expects':<tuple<types>>,
    'name':<str|unicode>,
    'sequence':[
        {
            'name':<str|unicode>,
            'defaultValue':<any>,
            'description':<str|unicode>,
            'expects':<tuple<types>>,
        },
    ]
}"""
        return self._arglist

    # ...

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

    # ...
    arglist = property( _GetArglist, None, None, 
        _GetArglist.__doc__.strip() )

    # ...

    #####################################
    # Instance Methods                  #
    #####################################

    # ...

    def _createArglistMetadata( self, callable, description, *expects ):
        """
Creates and returns a metadata-dictionary for a function or method arglist.

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.
expects .......... (tuple of types, optional, if not specified, stores 
                   ( object, ) as the value) The expected type(s) of the 
                   arglist.

RAISES:
 - TypeError:
   + If the supplied callable is not a function, method, or generator
   + If a type for an argument is invalid
 - 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._createArglistMetadata 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.varargs,
            'description':None,
            'expects':( object, ),
            'sequence':[]
            }
        # 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._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__ ) )
        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
        if expects:
            results[ 'expects' ] = expects
        # Return the structure
        return results

    # ...

    def AddArglist( self, callable, description, *expects ):
        """
Adds arglist-metadata for the specified argument to the instance's 
documentation-metadata."""
        self._arglist = self._createArglistMetadata( 
            callable, description, *expects )

As I was working through this chunk of changes, it occurred to me that I've never actually shown the __str__ method of the api_documentation class, which is what is generating the output that I have shown for documentation thus far. I'm going to hold off on going into that in any great detail for a while, though, because while it's functional at present, and sufficient for demonstrating that the metadata-generation is working, it's pretty clunky code and it doesn't generate what I really want it to, ultimately. It's missing hanging indentation and variable-length dot-leaders that I'd like to have in pplace to keep printed documentation well-formatted and with a maximum line-length of 80 characters. I'll plan to hit that after all of the documentation-generation for arguments is complete.

In the meantime, looking at this last chunk of code: The AddArglist method will have to be revisited once the describe.arglistitem decorator is implemented. While it works fine with our Ook test-class (above) so far after adding metadata-decoration for the methods' argument-lists, once argument-list items are added into the mix, it'll overwrite an existing argument-list metadata structure that may well have valid documentation-metadata if the sequence of decorators isn't very specific, and I don't want that.

The decoration on the Ook class' methods, for proving that the decoration works, looks like this now:

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 )
    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' )
    def Flup( arg1, arg2, *args, **kwargs ):
        """Ook.Flup (staticmethod) original doc-string"""
        return None

And the related decorator method, describe.arglist, is:

    @classmethod
    def arglist( cls, description='No description provided', *expects ):
        """
Decorates a function or method by attaching documentation-metadata about its 
argument-list, including a description of the argument-list, and an optional 
type-specification set."""
        # 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__ ) )
        # Type- and value-check expects, if it exists, or set it to ( object, )
        def _arglistDecorator( decoratedItem ):
            """
Performs the actual arglist 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.AddArglist( decoratedItem, description, *expects )
            # Return the decorated item!
            return decoratedItem
        return _arglistDecorator

This yields the following documentation on Ook, when printed:

--------------------------------------------------------------------------------
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.
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

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

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

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

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

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

I mentioned earlier that the logic in the AddArglist method of api_documentation would need to be altered, and I hinted at why that would be necessary. Here's a more detailed breakdown of that issue. Consider the following alterations to Ook.Fnord in the 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', str, unicode )
    @describe.arglistitem( -1, 'values', 'Ook.Fnord.args[3] (values) description', str, unicode )
    def Fnord( self, arg1, arg2, *args, **kwargs ):
        """Ook.Fnord (method) original doc-string"""
        return None

The decorator-sequence, I hope, makes good, solid sense to a developer. Document the argument list, then document the expected items in that list, in the order they are going to appear in a call. The documentation decorators also include an all remaining values item, as discussed before. The problem is that while the describe decorator-methods are called in the order specified, the underlying real decorator methods are executed in the opposite order. After dropping some print statements into a bare-bones/stub arglistitem method and the current version of arglist, this becomes pretty apparent:

 - Calling describe.arglist( Ook.Fnord (method) arglist description, (<type 'int'>, <type 'long'>, <type 'float'>) )
 - Calling describe.arglistitem( 0, arg1, Ook.Fnord.args[0] description, (<type 'float'>,) )
 - Calling describe.arglistitem( 1, arg2, Ook.Fnord.args[1] description, (<type 'int'>, <type 'long'>) )
 - Calling describe.arglistitem( 2, arg3, Ook.Fnord.args[2] description, (<type 'str'>, <type 'unicode'>) )
 - Calling describe.arglistitem( -1, values, Ook.Fnord.args[3] (values) description, (<type 'str'>, <type 'unicode'>) )
   + Calling _arglistitemDecorator( <function Fnord at 0x7f3a58c0ec08> )
     * sequence ...... -1
     * name .......... values
     * description ... Ook.Fnord.args[3] (values) description
     * expects ....... (<type 'str'>, <type 'unicode'>)
   + Calling _arglistitemDecorator( <function Fnord at 0x7f3a58c0ec08> )
     * sequence ...... 2
     * name .......... arg3
     * description ... Ook.Fnord.args[2] description
     * expects ....... (<type 'str'>, <type 'unicode'>)
   + Calling _arglistitemDecorator( <function Fnord at 0x7f3a58c0ec08> )
     * sequence ...... 1
     * name .......... arg2
     * description ... Ook.Fnord.args[1] description
     * expects ....... (<type 'int'>, <type 'long'>)
   + Calling _arglistitemDecorator( <function Fnord at 0x7f3a58c0ec08> )
     * sequence ...... 0
     * name .......... arg1
     * description ... Ook.Fnord.args[0] description
     * expects ....... (<type 'float'>,)
   + Calling _arglistDecorator( <function Fnord at 0x7f3a58c0ec08> )
     * description ... Ook.Fnord (method) arglist description
     * expects ....... (<type 'int'>, <type 'long'>, <type 'float'>)

Since the arglistitem decorators execute before the arglist decorator that they relate to, the method needs to establish the underlying bare-minimum argument-list metadata structure in order to be able to do what it needs to do. Following the pattern that's been established thus far, that means that AddArglistItem needs to create it:

    def AddArglistItem( self, callable, description, *expects ):
        """
Adds arglist-metadata for the specified argument to the instance's 
documentation-metadata."""
        if not self._arglist:
            self._arglist = self._createArglistMetadata( 
                callable )

That also means that AddArglist needs to check for an existing metadata-structure, and modify it if it already exists, rather than just creating a new one every single time:

    def AddArglist( self, callable, description, *expects ):
        """
Adds arglist-metadata for the specified argument to the instance's 
documentation-metadata."""
        if not self._arglist:
            # If it doesn't exist, create it
            self._arglist = self._createArglistMetadata( 
                callable, description, *expects )
        else:
            # If it DOES exist, update it (partially)
            self._arglist[ 'description' ] = description
            self._arglist[ 'expects' ] = expects

That should take care of the potential collisions, and yields documentation results for Ook.Fnord, Ook.Bleep and Ook.Flup methods that are identical to those before the modification, even with the arglistitem decorations in place.

It occurs to me that arglistitem documentation would probably be at least slightly easier to implement if the sequence were only a list of non-final argument-list items, rather than allowing (or requiring) a final list-item to be tracked in the same list-structure. The process of building the list of argument-list items would not have to contend with a length that fluctuates based on more than one condition in that case. The decoration-call wouldn't need to change at all, still using -1 as an indicator-value for the final items in the argument-list, just the handling of that indicator during decoration. That would require a minor change to the data-structure, though, looking something like so:

{
    # arguments removed for brevity...
    'arglist': {
        'description':<str|unicode>,
        'expects':<tuple<types>>,
        'name':<str|unicode>,
        'sequence':[
            {
                'name':<str|unicode>,
                'defaultValue':<any>,
                'description':<str|unicode>,
                'expects':<tuple<types>>,
            },
            # ...
        ],
        'final': {
            'name':<str|unicode>,
            'defaultValue':<any>,
            'description':<str|unicode>,
            'expects':<tuple<types>>,
        }
    }
}

Structurally, a final item is identical to any one of the sequence items, so the process for generating the metadata should be easily adaptable, if not actually identical across the board.

Another possibility that occurs to me is that maybe the sequence items (possibly including the final item) could be stored in the metadata-structure not as a list, but as a dictionary. The advantage to that approach is that there would be less need, perhaps no need at all, for rebuilding the sequence items list every time a new item is added. Looking at the Ook.Fnord method with the sequence and final items above, and the processing-sequence that occurred, the steps that would need to happen during execution are (in the order they were reported above):

  • _arglistitemDecorator( -1, values, ... is called:
    arglist: does not exist, so create it with minimal data;
    final: does not exist, so create it and populate it;
    sequence: does not exist, but isn't being populated, so leave it be;
  • _arglistitemDecorator( 2, arg3, ... is called:
    arglist: exists, use as-is;
    final: exists, but isn't being populated, so leave it be;
    sequence: does not exist, so create it and add the item to it;
  • _arglistitemDecorator( 1, arg2, ... is called:
    arglist: exists, use as-is;
    final: exists, but isn't being populated, so leave it be;
    sequence: exists, so add the item to it;
  • _arglistitemDecorator( 0, arg1, ... is called:
    arglist: exists, use as-is;
    final: exists, but isn't being populated, so leave it be;
    sequence: exists, so add the item to it;
  • _arglistDecorator( ... is called:
    arglist: exists, populate with description and expects as needed;
    final: exists, but isn't being populated, so leave it be;
    sequence: exists, but isn't being populated, so leave it be;
If sequence is a list, then each time that it is being touched, it would, at least potentially, need to be analyzed and rebuilt from its current state with the addition of the new item. The same sequence of events, then, from the perspective of the sequence list, would look like so:
  • _arglistitemDecorator( -1, values, ... is called:
    sequence: does not exist, but isn't being populated, so leave it be;
    Nothing to do here
  • _arglistitemDecorator( 2, arg3, ... is called:
    sequence: does not exist, so create it and add the item to it;
    At this point, we know that there will eventually be three items [0..2], so we should create a list 3 elements in length, and insert the new element at index 2;
  • _arglistitemDecorator( 1, arg2, ... is called:
    sequence: exists, so add the item to it;
    At this point, we know that the list exists, and that the index of the item is within its range of indexed values, so we can just insert it.
  • _arglistitemDecorator( 0, arg1, ... is called:
    sequence: exists, so add the item to it;
    At this point, we know that the list exists, and that the index of the item is within its range of indexed values, so we can just insert it.
  • _arglistDecorator( ... is called:
    sequence: exists, but isn't being populated, so leave it be;
    Nothing to do here
If, however, the decorator sequence is out of kilter, so that the sequence of sequences is not in order (say, 1, 0, 2), each call has to check and reconcile the current sequence against the incoming index:
  • _arglistitemDecorator( -1, values, ... is called:
    Nothing to do here
  • _arglistitemDecorator( 1, arg2, ... is called:
    sequence: exists, so add the item to it;
    At this point, we know that there will eventually be two items [0..1], so we should create a list 2 elements in length, and insert the new element at index 1;
  • _arglistitemDecorator( 0, arg1, ... is called:
    sequence: exists, so add the item to it;
    At this point, we know that the list exists, and that the index of the item is within its range of indexed values, so we can just insert it.
  • _arglistitemDecorator( 2, arg3, ... is called:
    sequence: does not exist, so create it and add the item to it;
    At this point, we know that there will eventually be three items [0..2], so we should create a list 3 elements in length, copy the existing sequence into the new sequence, and insert the new element appropriately.
  • _arglistDecorator( ... is called:
    sequence: exists, but isn't being populated, so leave it be;
    Nothing to do here
It would get even more complicated to implement to allow for gaps in the provided decorator-sequence values. Imagine the process-steps for (2, 0, 1, 3, 5, 4) for example: The first three items in the sequence would be fine, then sequence-items 3, 5 and 4 would each require the creation of a new list to replace the old one, population with old values and insertion/replacement of the new missing value at the appropriate index-position.

That feels messy and brittle to me, but it's worth confirming or disproving that feeling, so I'm going to write some code that uses that basic process, and feed it some simple values using the sequence I had concerns about (2, 0, 1, 3, 5, 4):

def addToList( theList, theIndex, theItem ):
    # First, check the index to see if the current list is long enough
    if len( theList ) -1 < theIndex:
        # The current list is too short to accommodate the supplied 
        # index-value, so create a new list that's long enough and 
        # populate it with the elements of the original list.
        theList = [ 
            theList[ index ]
            if len( theList ) > index and theList[ index ]
            else None
            for index in range( 0, theIndex + 1 )
        ]
        # THEN set the appropriate index-location to the value
        theList[ theIndex ] = theItem
    else:
        # Otherwise, just set the appropriate index-location to the 
        # value
        theList[ theIndex ] = theItem
    # Return the list, since we cannot pass it by reference and modify 
    # the original list in place.
    return theList

myList = []

print '#' + '-'*78 + '#'
print myList
myList = addToList( myList, 2, 'two' )

print '#' + '-'*78 + '#'
print myList
myList = addToList( myList, 0, 'zero' )

print '#' + '-'*78 + '#'
print myList
myList = addToList( myList, 1, 'one' )

print '#' + '-'*78 + '#'
print myList
myList = addToList( myList, 3, 'three' )

print '#' + '-'*78 + '#'
print myList
myList = addToList( myList, 5, 'five' )

print '#' + '-'*78 + '#'
print myList
myList = addToList( myList, 4, 'four' )

print '#' + '-'*78 + '#'
print myList

I'll admit that I'm surprised that it's not as bad as I'd expected. This code may not be as optimal as it could be (though I was able to use a conditional list-comprehension to generate the new list, and that feels pretty good). I haven't run across anything in the standard Python libraries (yet) that does exactly what's needed: simultaneously growing and populating a list where the sequence is important. And it actually works, pretty much first try out of the gate, which I'm very happy about:

#------------------------------------------------------------------------------#
[]
#------------------------------------------------------------------------------#
[None, None, 'two']
#------------------------------------------------------------------------------#
['zero', None, 'two']
#------------------------------------------------------------------------------#
['zero', 'one', 'two']
#------------------------------------------------------------------------------#
['zero', 'one', 'two', 'three']
#------------------------------------------------------------------------------#
['zero', 'one', 'two', 'three', None, 'five']
#------------------------------------------------------------------------------#
['zero', 'one', 'two', 'three', 'four', 'five']

Using this proof-of-concept function as a baseline, and dropping default undefined argument-list items into undefined positions in the sequence (instead of None as used in the example) actually feels pretty good, the more I think on it.

With all of these thought out, the relevant implementations are:

class api_documentation( object ):
    """
Provides a common collection-point for all API documentation metadata for a 
programmatic element (class, function, method, whatever)."""
    #####################################
    # Class attributes (and instance-   #
    # attribute default values)         #
    #####################################

    # ...

    #####################################
    # Instance Methods                  #
    #####################################

    # ...

    def AddArglistItem( self, callable, sequence, name, description, *expects ):
        """
Adds arglist-metadata for the specified argument to the instance's 
documentation-metadata."""
        if not self._arglist:
            self._arglist = self._createArglistMetadata( 
                callable )
        # First, check the index to see if the current list is long enough
        if sequence >= 0:
            if len( self._arglist[ 'sequence' ] ) -1 < sequence:
                # The current list is too short to accommodate the supplied 
                # index-value, so create a new list that's long enough and 
                # populate it with the elements of the original list.
                self._arglist[ 'sequence' ] = [ 
                    self._arglist[ 'sequence' ][ index ]
                    if len( self._arglist[ 'sequence' ] ) > index and self._arglist[ 'sequence' ][ index ]
                    else {
                        'name':'unnamed',
                        'description':'No description provided',
                        'expects':( object, ),
                    }
                    for index in range( 0, sequence + 1 )
                ]
                # THEN set the appropriate index-location to the value
                self._arglist[ 'sequence' ][ sequence ] = {
                    'name':name,
                    'description':description,
                }
                if expects:
                    self._arglist[ 'sequence' ][ sequence ][ 'expects' ] = expects
                else:
                    self._arglist[ 'sequence' ][ sequence ][ 'expects' ] = ( object, )
            else:
                # Otherwise, just set the appropriate index-location to the 
                # value
                self._arglist[ 'sequence' ][ sequence ] = {
                    'name':name,
                    'description':description,
                }
                if expects:
                    self._arglist[ 'sequence' ][ sequence ][ 'expects' ] = expects
                else:
                    self._arglist[ 'sequence' ][ sequence ][ 'expects' ] = ( object, )
        elif sequence == -1:
                self._arglist[ 'final' ] = {
                    'name':name,
                    'description':description,
                }
                if expects:
                    self._arglist[ 'final' ][ 'expects' ] = expects
                else:
                    self._arglist[ 'final' ][ 'expects' ] = ( object, )
        else:
            raise RuntimeError( 'AddArglistItem expects a sequence value of -1 or greater.' )
class describe( object ):
    """
Nominally-static class (not intended to be instantiated) that provides the 
actual functionality for generating documentation-metadata structures."""

    # ...

    @classmethod
    def arglistitem( cls, sequence, name, description='No description provided', *expects ):
        """
Decorates a function or method by attaching documentation-metadata about an 
argument-list item at a specific position/index in the sequence of values 
expected/accepted by the argument-list."""
        # Type-check sequence it must be a long or int value
        if type( sequence ) not in ( int, long ):
            raise TypeError( 'describe.argument expects an integer or long '
                'numeric value containing an index-location for the item '
                'being decorated, greater than or equal to -1, but was passed '
                '"%s" (%s)' % ( sequence, type( sequence ).__name__ ) )
        # Value-check sequence it must be >= -1
        if sequence < -1:
            raise ValueError( 'describe.argument expects an integer or long '
                'numeric value containing an index-location for the item '
                'being decorated, greater than or equal to -1, but was passed '
                '"%s" (%s)' % ( sequence, type( sequence ).__name__ ) )
        # 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 _arglistitemDecorator( 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.AddArglistItem( decoratedItem, sequence, name, description, *expects )
            # Return the decorated item!
            return decoratedItem
        return _arglistitemDecorator

So, with the updated Ook 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 )
    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' )
    def Flup( arg1, arg2, *args, **kwargs ):
        """Ook.Flup (staticmethod) original doc-string"""
        return None

the resulting documentation output is:

--------------------------------------------------------------------------------
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.
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

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

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

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

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

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

Argument-lists and their related (child?) items are probably the most complicated item to document. Certainly they involved a substantial volume of code, as well as the discussion of the implementation needs and the strategies and tactics that arose from them. The next (and final) argument-documentation item is keyword arguments, which I expect to be less complex, and to follow a pattern similar to the argument-list items covered in this post.

So come back next time and I'll start digging into that... Oh, wait. I said earlier that I'd discuss the odd decoration-structure that I used in decorating the decorators themselves, didn't I? OK, then: As things stand right now, the decoration of all the decorators written so far looks like this:

######################################################
# Because the actual decorators themselves aren't    # 
# available to decorate themselves until this point, #
# decorate the decorators. O.o :-)                   #
######################################################
# describe.arglist
describe.argument( 'description', 
    'The description of the argument-list', 
    str, unicode )( describe.arglist )
describe.arglist( 'The types expected by the argument-list.', 
    type )( describe.arglist )

# describe.arglistitem
describe.argument( 'sequence', 
    'The sequence of the named item in the argument-list (zero-index)', 
    int, long )( describe.arglistitem )
describe.argument( 'name', 
    'The name of the argument-list item', 
    str, unicode )( describe.arglistitem )
describe.argument( 'description', 
    'The description of the argument-list item', 
    str, unicode )( describe.arglistitem )
describe.arglist( 'The types expected by the argument-list sequence-item.', 
    type )( describe.arglistitem )

# describe.argument
describe.argument( 'name', 
    'The name of the argument', 
    str, unicode )( describe.argument )
describe.argument( 'description', 
    'The description of the argument', 
    str, unicode )( describe.argument )
describe.arglist( 'The types expected in the argument.', 
    type )( describe.argument )

Remember that the decorator methods themselves return a function. In normal" usage, where a @decoratorName( args )-style is set up before a decorated item, Python knows to apply the resulting function to the next item. The automatic chaining of those decorator calls ultimately applies every decorator to the final decorated item — a function or method in all the cases so far. But those functions in need of decoration don't actually exist at the point where a normal decorator-call would be placed to decorate them. That is, looking at arglist, for example, the argument decorator hasn't even been defined by the point where it would be applied as a normal decorator for arglist. That, then, means that we have to wait until all of the decorators' definitions are complete, and their functionality ready to use, before we can decorate the decorators themselves.

Ultimately, that's all the free-standing decoration is doing. Stepping through what happens during the execution of describe.argument( ... ) ( describe.arglist ):

  1. describe.argument is called, returning a function;
  2. that function is then called with ( describe.arglist ), which passes describe.arglist as the decoratedItem;
  3. The decoration happens, returning the now-decorated describe.arglist
It's really that simple. Admittedly, it looks kind of strange, but that's all that's happening under the hood.

OK, now I'll sign off...