views:

145

answers:

3

This works now for those new to this question:

class ensureparams(object):
    """

    Used as a decorator with an iterable passed in, this will look for each item
    in the iterable given as a key in the params argument of the function being
    decorated. It was built for a series of PayPal methods that require
    different params, and AOP was the best way to handle it while staying DRY.


    >>> @ensureparams(['name', 'pass', 'code'])
    ... def complex_function(params):
    ...     print(params['name'])
    ...     print(params['pass'])
    ...     print(params['code'])
    >>> 
    >>> params = {
    ...     'name': 'John Doe',
    ...     'pass': 'OpenSesame',
    ...     #'code': '1134',
    ... }
    >>> 
    >>> complex_function(params=params)
    Traceback (most recent call last):
        ...
    ValueError: Missing from "params" dictionary in "complex_function": code
    """
    def __init__(self, required):
        self.required = set(required)

    def __call__(self, func):
        def wrapper(*args, **kwargs):
            if not kwargs.get('params', None):
                raise KeyError('"params" kwarg required for {0}'.format(func.__name__))
            missing = self.required.difference(kwargs['params'])
            if missing:
                raise ValueError('Missing from "params" dictionary in "{0}": {1}'.format(func.__name__, ', '.join(sorted(missing))))
            return func(*args, **kwargs)
        return wrapper

if __name__ == "__main__":
    import doctest
    doctest.testmod()
+2  A: 

def wrapper(params): means you're only going to accept one argument -- and so of course calls with (self, params) just won't work. You need to be able to accept either one or two arguments, e.g., at the very least (if you don't need to support calls w/named args):

def wrapper(one, two=None):
  if two is None: params = one
  else: params = two
  # and the rest as above

You can get much more complex / sophisticated in order to also accept named arguments, but this is much simpler and still "mostly works";-).

Alex Martelli
Oh, no no. 'params' is the name of a dictionary argument. It takes just one argument. See the doctest included.
orokusaki
@orokusaki, when you write `def foo(self, params):`, those are **TWO** arguments, not one -- count them. Your failure to acknowledge this is what is causing the bug in your code: you **have** to accept being called with either one or two args, and your `def wrapper`, as coded, just doesn't (it forces a single argument, just like you think it should... and you're wrong, Python is telling you that, and so am I;-).
Alex Martelli
Did you see my question Alex? How do I get the decorator to work for a method of a class and a normal function without having two versions of the decorator. I'm not a complete noob. I just don't understand how to make a version for instance methods and functions.
orokusaki
@orokusaki It's like Alex puts it. I think the confusion is only in Alex' choice of parameter names for the two-arguments form of the decorator. In your example 'one' and 'two' are passed within a dictionary, and this dictionary is the only argument that really matters. Alex used one and two to signify _completely_ different things. Imagine he had written then as, say, `def wrapper(FirstArg, PossibleSecondArg=None)`.
mjv
@orokusaki, the code I give for `wrapper` **does** work for both methods and standalone functions without having two versions -- have you **tried** it, instead of endlessly carping about it?-)
Alex Martelli
My answer is more general.
Omnifarious
If somebody asks: "How do I fix my muffler?", the answer shouldn't be "Don't use a muffler. Instead use straight pipes.".
orokusaki
@Omnifarious: exactly -- your answer's very general, mine is perfectly specific to @orokusaki's **exact** problem -- though he refuses to understand this makes my answer _better_ for his specific problem!-)@orokusaki, I'm **not** telling you "what not to do": I'm fixing your exact, specific problem -- and you're apparently totally blind to the fact that I did: ah well, **your** personality problem, **not** mine!-)
Alex Martelli
Dude, Alex. I appreciate your help for real. I figured out a solution and I think it's much more Pythonic is all. You don't have to get angry with people who don't understand your approach. You're clearly a computer scientist and not all the rest of us are (yet).
orokusaki
@orokusaki, I've never been a computer scientist (though wikipedia keeps claiming I am and I can't get them to fix it) -- I'm an engineer. It's just ridiculous to see on your Q you claiming "I know how to use `*args`, `**kwargs`, et al. I don't need that" and then see you posting a self-answer starting with "What I did was add `*args`, `**kwargs`,"... plus, your solution is just wrong, and does **not** do what you claim it does (if `params` is passed in as a positional argument, as in your docstring's example, your `wrapper` fails -- you only support named-argument `params`!). Blech.
Alex Martelli
I actually fixed my docstring right after I posted that on there (only after doc testing it). Remember my original approach was to use a positional argument. I never updated my doc string but I updated my code. The only thing that is blech is the attitude. I have the utmost respect for you as a programmer. However, you have a cocky attitude (not to mention that the example above is very implicit in style (allowing for a possible run time error if an incorrect argument was passed into the method) and Python's own guide says "Explicit of implicit"). I posted my own version since it was correct.
orokusaki
+2  A: 

Decorators normally look like this:

def wrapper(*args, **kargs):
    # Pull what you need out of the argument lists and do stuff with it
    func(*args, **kargs)

Then they work with any function passed to them, not just functions with a specific number of arguments or with specific keyword arguments. In this specific case, you may want to do some introspection on the func passed to __call__ to find out if it's a one or two argument function and to make sure the last argument is called 'params'. Then just write wrapper like this:

def wrapper(*args):
    params = args[-1]
    missing = self.required.difference(params)
    if missing:
        raise ValueError('Missing from "params" argument: %s' % ', '.join(sorted(missing)))
    func(params)
Omnifarious
Thanks Omnifarious. I ended up using *args, **kwargs instead but your answer got me thinking in the right direction.
orokusaki
A: 

What I did was add *args, **kwargs, and just check for the keys that are required within the 'params' argument via kwargs['params'] after checking that kwargs params exists.

Here's the new version (which works perfectly):

class requiresparams(object):
    """

    Used as a decorator with an iterable passed in, this will look for each item
    in the iterable given as a key in the params argument of the function being
    decorated. It was built for a series of PayPal methods that require
    different params, and AOP was the best way to handle it while staying DRY.


    >>> @requiresparams(['name', 'pass', 'code'])
    ... def complex_function(params):
    ...     print(params['name'])
    ...     print(params['pass'])
    ...     print(params['code'])
    >>> 
    >>> params = {
    ...     'name': 'John Doe',
    ...     'pass': 'OpenSesame',
    ...     #'code': '1134',
    ... }
    >>> 
    >>> complex_function(params=params)
    Traceback (most recent call last):
        ...
    ValueError: Missing from "params" dictionary: code
    """
    def __init__(self, required):
        self.required = set(required)

    def __call__(self, func):
        def wrapper(*args, **kwargs):
            if not kwargs.get('params', None):
                raise KeyError('"params" kwarg required for {0}'.format(func.__name__))
            missing = self.required.difference(kwargs['params'])
            if missing:
                raise ValueError('Missing from "params" dictionary: %s' % ', '.join(sorted(missing)))
            return func(*args, **kwargs)
        return wrapper

if __name__ == "__main__":
    import doctest
    doctest.testmod()
orokusaki