Auto documenting and validating named arguments

In real life there are companies were keywords (**kw) are prohibited, and others where positional arguments can stack up as high as seven. Let's state the obvious concerning arguments:

Human beings are flawed


The positional arguments are fixed arguments. Some are mandatory others are optional (and are being set at function definition). 

Named arguments is a way to give arguments which are known by their name not their positions. 

If we humans were not limited by our short term memory then positional arguments would be enough. Alas we are limited to 7 items in memory +- 2. I normally strongly advise to remember that even if you can go up to 9 because you are genius, when you are woken up at 3 am after a party for a critical bug your short term memory might drop to 5+-2 items. So be prepared for the worse and follow my advice and try to stick to 3 mandatory positional arguments the more you can. 

Then, you have the case against the named arguments.

Named arguments are great for writing readable calls to function especially when there are a lot of optional arguments or when calls can be augmented on the fly thanks to duck typing and all funky stuffs that makes programming fun.

However, the documentation of the function might seems complex because you have to do it by hand as you can see here :
https://github.com/kennethreitz/requests/blob/master/requests/api.py

Plus the signature of the function is quite ugly.

    
get(url, **kwargs)
        Sends a GET request. Returns :class:`Response` object.
        
        :param url: URL for the new :class:`Request` object.
        :param **kwargs: Optional arguments that ``request`` takes.

So, even if named arguments are great they are painful to document (thus a little less maintainable), and gives function a cryptic signature when used in the form **kwargs.


Having explicit named arguments with default values are therefore «more pythonic» since :

Explicit is better than implicit. 

Decorators for easing validation documentation 



Since I am an advocate of optional named arguments and I find them cool, I thought why not write code ... 

>>> @set_default_kw_value(port=1026,nawak=123)
... @must_have_key("name")
... @min_positional(2)
... @validate(name = naming_convention(), port = in_range(1024,1030 ))
... def toto(*a,**kw):
...     """useless fonction"""
...     return 1

... that would magically return a documentation looking like this:


toto(*a, **kw) useless fonction

keywords must validate the following rules:
  • key: <port> must belong to [ 1024, 1030 [,
  • key: <name> must begin with underscore
at_least_n_positional :2

keyword_must_contain_key :name

default_keyword_value :
  • params: port is 1026,
  • params: nawak is 123

The idea was just to make a class making decorator with reasonable defaults that would enhance the decorated function documentation based on functools.wrap code.


class Sentinel(object):
    pass
SENTINEL=Sentinel()

def default_doc_maker(a_func, *pos, **opt):
    doc = "\n\n%s:%s" % (a_func, a_func.__doc__)
    posd= "%s\n" % ",".join(map(str, pos))  if len(pos)  else ""
    named = "\n%s" % ",\n".join([ "* params: %s is %r"%(k,v) for k,v in opt.items() ]
        ) if len(opt) else ""
    return """
**%s** :%s
%s""" % ( 
        a_func.__name__,
        posd,
        named
    )


def valid_and_doc(
            pre_validate = SENTINEL,
            post_validate = SENTINEL,
            doc_maker = default_doc_maker
        ):
    def wraps(*pos, **named):
        additionnal_doc=""
        if pre_validate is not SENTINEL:
            additionnal_doc += doc_maker(pre_validate, *pos, **named)
        if post_validate is not SENTINEL:
            additionnal_doc += doc_maker(post_validate, *pos, **named)
        def wrap(func):
            def rewrapped(*a,**kw):
                if pre_validate is not SENTINEL:
                    pre_validate(*pos,**named)(*a,**kw)
                res = func(*a,**kw)
                if post_validate is not SENTINEL:
                    post_validate(*pos,**named)(*a,**kw)
                return res

            rewrapped.__module__ = func.__module__
            rewrapped.__doc__=func.__doc__  + additionnal_doc
            rewrapped.__name__ = func.__name__
            return rewrapped
        return wrap
    return wraps



That can be used this way :

def keyword_must_contain_key(*key):
    def keyword_must_contain_key(*a,**kw):
        if set(key) & set(kw) != set(key):
            raise Exception("missing key %s in %s" % (
                  set(key)^( set(kw)& set(key)),kw)
            )
    return keyword_must_contain_key


def at_least_n_positional(ceil):
    def at_least_n_positional(*a, **kw):
        if a is not None and len(a) < ceil:
            raise Exception("Expected at least %s argument got %s" % (ceil,len(a)))
    return at_least_n_positional

min_positional= valid_and_doc(at_least_n_positional)
must_have_key = valid_and_doc(keyword_must_contain_key) 

Okay, my code might not get an award for its beauty, but you can test it here https://github.com/jul/check_arg


And at least sphinx.automodule accepts the modified docs, and interactive help is working too. 

Of course, it relies on people correctly naming their functions and having sensibles parameters names :P

However, though it sound ridicule, I do think that most of our experience comes from knowing the importance of naming variables, modules, classes and functions correctly.


Conclusion



Since I am not satisfied by the complexity/beauty of the code I strictly have no idea if I will package it, or even work on it. But at least, I hope you got the point that what makes optional optional named arguments difficult to document is only some lack of imagination. :)



No comments: