int_tested was supposed to do
>>> if x!=int(x) raise error, else x = int(x)
which happens in a couple of handful cases in the SymPy code.
(In fact the whole int_tested idea sprouted from issue #1740, where this idiom was not applied but should have been.)

Chris proposed using a decorator, Aaron proposed to extend it to float, my proposal here is to use a decorator that can handle arbitrary types and even arbitrary "normalization functions". I also reworked the interface so that the decorator can use an arbitrary mixture of normalizers; e.g.

>>> @normalize_args(int, float, abs)
>>> def something_with_a_square_root(a, b, needs_to_be_a_square)

Attached is a heavily documented and commented implementation of that idea.
There are a handful things I'd like to change before this goes productive, but I think it is now in a shape that people can experiment with it and comment on it.
The things I'm already aware of are:
1) I'd like to use @wraps or update_wrapper to make it play nicer with the metadata system of Python.
2) Somehow, both camelCase and underlined_words slipped in.
3) There's a TODO left about reporting a stumbling block to the Python community that they might want to fix in their documentation.

Regards,
Jo

P.S:: In hindsight, issue #1740 wasn't EasyToFix; decorators rock, but diving right into them fully general was a real challenge.

--
You received this message because you are subscribed to the Google Groups 
"sympy" group.
To post to this group, send email to [email protected].
To unsubscribe from this group, send email to 
[email protected].
For more options, visit this group at 
http://groups.google.com/group/sympy?hl=en.

#!/usr/bin/env python

from inspect import getargspec, getcallargs

def __unchanged(x):
    return x

def normalize_args(*normalizers):
    """A decorator that pairs up normalizer functions to arguments.
    
    E.g.
    
    >>>@normalizer_args(abs, int)
    >>>def some_func(a, b):
    >>>    print a, b

    pairs the abs function with a, and the int function with b.
    
    When the wrapped function (some_func in the above example) is called,
    the arguments are passed through the corresponding normalizer.

    If each result compares equal with the original argument, the result
    is passed to the wrapped function; if any result does not compare
    equal, the wrapped function is not called and a ValueError is raised
    instead.
    
    E.g. in the example above, you will have

    >>>some_func(1, 3)
    1 3
    >>>some_func(-1, 1.4)
    ValueError ### FIXME: insert actual stack trace here

    Note that comparing equal does not necessarily mean the same value;
    1.0 compares equal to 1, for example.
    
    Unpaired parameters and those that are paired with None are passed
    through unchanged.

    USAGE TIPS
    
    This function not only gives a simple way to check the validity of
    function parameters and any potentially helpful conversion, it
    also provides a standardized and thorough exception message.
    
    Passing in lambdas as normalizers will work, but the name of a
    lambda is "<lambda>", which is not going to be helpful.
    So it is recommended to put any normalization code in a normal
    named Python function.
    Consider making that function public even: If a parameter is
    normalized, there is a nonzero chance that callers will want to
    use that normalization for their own purposes.

    The vast majority of normalization function uses is expected to be
    int and float, plus maybe complex and long.
    In SymPy, int and float have been redefined for symbolic
    expression types to force evaluation and return an actual
    number, a relatively common need.
    """
    # TODO Enhancement request
    # Ask them to document that int is both the builtin function and
    # the builtin type (it's one and the same object that uses duck typing)
    # This applies to int, long, float, complex
    # Note that this surprises only programmers who are not used to duck
    # typing. I.e. intermediate learners who come from a (statically-typed)
    # language where each object has one and exactly one type.
    def wrap(f):
        # Build the paramNormalizers dictionary
        # It maps parameter names to pertinent normalizer functions
        # If no normalizer is given or the normalizer is None,
        # the parameter name is mapped to the __unchanged function
        paramNames = getargspec(f)[0]
        # We start with mapping all parameters to __unchanged
        paramNormalizers = dict.fromkeys(paramNames, __unchanged);
        # Then fill in the normalizers
        for paramName, normalizer in zip(paramNames, normalizers):
            if normalizer != None:
                paramNormalizers[paramName] = normalizer
        def wrapped_f(*args, **kwargs):
            # unify args and kwargs into a single dictionary
            # we don't use the fact, but it could be used as kwargs
            callargs = getcallargs(f, *args, **kwargs)
            # construct a dict of normalized kwargs
            # this one will indeed be used when calling f
            normalizedCallargs = {
                paramName: paramNormalizers[paramName](value)
                for paramName, value in callargs.iteritems()
            }
            # check that normalization returned values that compare equal
            if callargs != normalizedCallargs:
                # at least one parameter was turned out different after
                # normalization
                # raise a ValueError that describes the defective parameter
                # we'll report just the first we find
                # to keep the message short
                for name, value in callargs.iteritems():
                    normalizer = paramNormalizers[name]
                    normalizedValue = normalizer(value)
                    if value != normalizedValue:
                        raise ValueError(
                            "Invalid value for parameter %(paramname)s: "
                            "%(funcname)s(%(value)s) yielded %(result)s, "
                            "which is not equal to %(value)s"
                            % {
                                "funcname": normalizer.__name__,
                                "paramname": name,
                                "value": value,
                                "result": normalizedValue
                            }
                        )
                raise ValueError, "Oops"
            # call f with normalized args
            return f(**normalizedCallargs)
        return wrapped_f
    return wrap

@normalize_args(int, float, None)
def test(a, b, c, d=15, e=17):
    print "a", a, "b", b, "c", c, "d", d, "e", e

test(1.5, 2, 4, e=30)

Reply via email to