1

What's the best way NOT to have to extracts the SAME KWARGS twice: once in the decorator (wrapper function) and once in the function itself.

Here is the function:

@close_logger
def close(**kwargs):
    """ returns close object"""

    # Get params
    session_attributes = kwargs.get('sessionAttrbiutes', {})
    message = kwargs.get('message', '')
    LID = kwargs.get('LIData', {})
    SANS = kwargs.get('SANS', [])
    FS = kwargs.get('fulfillmentState', 'Fulfilled')

    response = {
        'sessionAttributes': session_attributes,
        'dialogAction': {
            'type': SANS,
            'fulfillmentState': FS,
            'message': {
                'contentType': LID,
                'content': message
            }
        }
    }

    return response

and here is the decorator (used for logging the close event):

def close_logger(func):

    @functools.wraps(func)
    def wrapper(**kwargs):

        # Get params
        session_attributes = kwargs.get('sessionAttrbiutes', {})
        message = kwargs.get('message', '')
        LID = kwargs.get('LIData', {})
        SANS = kwargs.get('SANS', [])
        FS = kwargs.get('fulfillmentState', 'Fulfilled')

        logger.debug('Logging:\n Function:{} Session Attributes: {}\n \
        Message{}\n: LID: {}\n SANS: {}\n FS: {}'.format(
            func.__name__,
            session_attributes,
            message,
            LID,
            SANS,
            FS
        ))

        return func(**kwargs)

    return wrapper
Cœur
  • 37,241
  • 25
  • 195
  • 267
Bas
  • 211
  • 1
  • 12
  • Note: You have a typo, `'sessionAttrbiutes'` should probably be `'sessionAttributes'`. – Graipher Mar 13 '18 at 16:20
  • 1
    Why does your function need to take `**kwargs` as input, rather than those specific arguments you need? – palivek Mar 13 '18 at 16:22
  • because I'm trying to have an all purpose logging decorator for several functions each with a different set of args. Does that make sense? – Bas Mar 13 '18 at 16:24
  • 2
    @Bas That just means that `close_logger` needs to take `**kwargs`. `close` can have named keyword arguments with defaults. – Graipher Mar 13 '18 at 16:25
  • Hi @Graipher I guess that's what I'm trying to figure out.... if 'close' has (*args) only, how can they be passed as **kwargs to a decorator? A bit more details would be very helpful.... Tks. – Bas Mar 13 '18 at 16:31
  • @Bas "if 'close' has (*args) only," -- just to be clear, you meant `**kwargs`, right? Neither of you functions takes `*args` as input. – palivek Mar 13 '18 at 16:34
  • @palivek currently, both take **kwargs (correct!) but as Graipher is suggesting: only the decorator needs **kwargs (to work with several functions). So my question is: how can you have a function that takes (*args) pass them as (**kwargs) to the decorator. – Bas Mar 13 '18 at 16:39
  • @Bas So, what you are trying to do is have the decorator-function take any number of positional arguments and have them passed on to the function call as keyword arguments? – palivek Mar 13 '18 at 16:44
  • @palivek it's the other way around: I need to create a logging decorator that takes keyword arguments to decorate several functions. Each function has a number of positional arguments (the only reason I converted them to keyword arguments is to be able to pass them in the wrapper function). – Bas Mar 13 '18 at 16:55
  • @Bas Ah, in that case I will have to rethink my answer... – Graipher Mar 13 '18 at 16:57

2 Answers2

0

Start by making your keyword arguments explicit for the close function.

def close(sessionAttrbiutes=None, message='', LIData=None, SANS=None, fulfillmentState='Fulfilled'):
    if sessionAttrbiutes is None:
        sessionAttrbiutes = {}
    ...

Note that I used None as default values for the mutable default values to avoid a common pitfall.

Then in your decorator use inspect.getfullargspec, similar to this answer:

import inspect
import functools

def get_default_args(func):
    """
    returns a dictionary of arg_name:default_values for the input function
    """
    argspec = inspect.getfullargspec(func)
    return dict(zip(reversed(argspec.args), reversed(argspec.defaults)))


def close_logger(func):
    @functools.wraps(func)
    def wrapper(**kwargs):
        kwargs_local = get_default_args(func)
        kwargs_local.update(kwargs)

        logger.debug("""Logging:
Function: {}
Session Attributes: {sessionAttrbiutes}
Message: {message}
LID: {LIData}
SANS: {SANS}
FS: {fulfillmentState}""".format(func.__name__, **kwargs_local))

        return func(**kwargs)

    return wrapper

This will raise a KeyError if one of those fields is not defined (either in the signature or the passed keywords). Of course it will show None for those where that is the default in the signature.

The full argspec will also contain positional arguments, so you might be able to figure those out as well.

Graipher
  • 6,891
  • 27
  • 47
  • Thanks....I feel that a good solution is to have all functions WRITTEN with positional arguments (*args) but CALLED using keyword arguments (**kwargs). This will not confuse the wrapper function in the logging decorator but also makes sure we 'extract' the arguments only once (in the wrapper). Is this a good approach? – Bas Mar 14 '18 at 13:09
0

**-notation can be used within function argument lists to both pack and unpack arguments.

**-notation within a function definition collects all the values into a dictionary.

>>> def kwargs_in_definition(**kwargs): return kwargs
>>> kwargs_in_definition(arg1 = 1, arg2 = 2, arg3 = 3)
{'arg1': 1, 'arg2': 2, 'arg3': 3}

**-notation within a function call unpacks all the values as keyword argumnts.

def kwargs_in_call(arg1 =0, arg2 = 0, arg3 = 0): return arg1, arg2, arg3

So when you pass kwargs (kwargs := {'arg1': 1, 'arg2': 2, 'arg3': 3}) to kwargs_in_call you get this:

>>> kwargs_in_call(kwargs)
({'arg1': 1, 'arg2': 2, 'arg3': 3}, 0, 0) # kwargs is used as arg1.

...but if you unpack it first:

>>> kwargs_in_call(**kwargs)
(1, 2, 3) # kwargs unapcks to "arg1 = 1, arg2 = 2, arg3 = 3"

All of this applies for * as well

Your specific case:

**kwargs in a function defintion gives you a regular old dict. You can turn that dictionary into a list -- however you wish to do that --, unpack that list and pass it on to another function. You'll have to account for ordering though, so it's difficult to make this work for all functions.

I'm not sure why you insist on your decorator function taking keyword arguments while your decorated function doesn't. You can, of-course, do whatever you want, but I would argue against such an approach, because you're decorator function is changing how your actual function behaves.

If you manage to sort those keyword arguments in to a valid argument list you'll be good.

Hope this helps.

EDIT: Oh, I forgot to point out that you're unpacking **kwargs in your decorator function (return func(**kwargs)) and then you're repacking them in your actual function (def close(**kwargs):. I'll be honest, that's a silly thing to do.

EDIT: You can use inspect.getargspec or inspect.signature to get a functions argument list. If you just have your decorator take in *args, you can match each value to a corresponding name from the argument list.

palivek
  • 115
  • 7
  • Thanks Palivek..... and yes I agree that ideally you shouldn't have a decorator with keyword arguments (**kwargs) and a decorated function with positional arguments (*args). But if that decorator needs to be a multi-purpose logger that logs (arg_name and arg_value) for every arg in the decorated function, is there a better way to do it? off course, each of the decorated functions has a different set of arguments (otherwise this wouldn't be a problem). Thanks. – Bas Mar 13 '18 at 17:48
  • @Bas Hmmmm, I guess you could add another level to your logger, which would accept an ordered list of argument names. You're decorator would end up looking like this though: `close_logger(...list of agument names...)`. You could maybe also create another wrapper that adds an argument names list attribute to your function. Alas, I don't think there's anyway to automatically create such a list. – palivek Mar 13 '18 at 17:57
  • @Bas I think I might have figured it out. I added an eddit to my solution. – palivek Mar 13 '18 at 18:37
  • thanks..... as my comment on the other posted solution, here is what I'm suggesting: Having all functions WRITTEN with positional arguments (*args) but CALLED using keyword arguments (**kwargs). This will not confuse the wrapper function in the logging decorator but also makes sure we 'extract' the arguments only once (in the wrapper). Is this a good approach? – Bas Mar 14 '18 at 13:10
  • @Bas With what I suggested in my last edit, you can have both functions called with positional arguments, because the wrapper function's argument list is the same as your actual function's. Then you can use either `inspect.getargspec` or `inspect.singature` to make name and value pairs to print. Your actual function then, shouldn't need to take any number of keyword arguments as input, but rather only the ones it needs. – palivek Mar 14 '18 at 15:42
  • @Bas Another approach would be taking *one* object as input (a dictionary or a list), as opposed to many. This is not an uncommon thing to do; for example `socket.connect`, from the socket package, takes in `(host, port)` tuple, rather than `host` and `port`. – palivek Mar 14 '18 at 15:46