Welcome, guest | Sign In | My Account | Store | Cart
# callable is not in Python 3.0 and 3.1. We create our own if needed.
try:
    callable
except:
    import collections
    def callable(o):
        return isinstance(o, collections.Callable)

def isbuiltin(o):
    return isinstance(o, type) and o.__module__ in ["__builtin__", "builtins"]

_valid_target_defaults = {
    None: lambda o: not isinstance(o, type),
    "class": lambda o: isinstance(o, type) and not isbuiltin(o),
    "type": lambda o: isinstance(o, type),
    "any": lambda o: True,
    "never": lambda o: False
}

# opt_arg_dec_with_args (basically unique to the call):
#       (decorated_function, params, kwargs)
incompletely_used_decorators = {}

# In Python 3, i like to make the arguments keywork-only like this:
# def opt_arg_dec(decorated, *, is_method=False, valid_target=None):
def opt_arg_dec(decorated, is_method=False, valid_target=None, 
    allow_non_callable=False):
    """ 
    is_method: True if the function to become an opt_arg_dec is a 
        method or a classmethod (not a staticmethod).
    valid_target: A callable receiving a single object and returning True
        if it is a valid target for the decorator to decorate. 

        This is not used as error validation. This is to detect when the 
        decorator will only be called once (when no initial call to set 
        arguments is made or when it is not used in the form of a decorator.)
        If arguments were previously passed, the target will be sent through
        without any form of checking. You must still to error validation in
        your decorator if you need it.

        The target must be callable unless allow_non_callable is True.
        Special values:
        - None (default): the target must not be a type/class (but it can be
                an instance of a type/class).
                Use this for function decorator, unless you want to pass in
                a function to be more specific and only allow certain specific
                types.
        - "class": the target must be a class (not an instance of a class).
                However, builtin classes that are global to python will not
                be accepted. Meaning (int, float, str, set, dict, list, tuple
                and maybe a few more) since it is unlikely to want to decorate
                those.
                Use this for class decorators.
        - "type": same as "class", but builtin classes are also accepted.
        - "any": anything is allowed. (Must still be callable unless
                allow_non_callable is True)
        - "never": nothing will ever be accepted as target. This means the
                decorator must always be called before being applied to the
                real target, even if it means with no arguments.
    
    allow_non_callable: If False (the default), then the target to be
        decorated must be callable! See valid_target for more details,
        this check is done in the same conditions. Setting this to True means
        that valid_target must do more advanced check, otherwise almost
        anything will be considered a valid target and the first call to the
        function will be treated as a full call.
    """
    try:
        if valid_target in _valid_target_defaults:
            valid_target = _valid_target_defaults[valid_target]
    except:
        # valid_target is not hashable. So it's not one of the special values.
        pass

    is_method = bool(is_method)

    options = {"is_method": is_method,
               "valid_target": valid_target,
               "allow_non_callable": allow_non_callable}

    def opt_arg_dec_wrapped(*params, **kwargs):
        return _opt_arg_dec_used(decorated, options, params, kwargs)
    
    opt_arg_dec_wrapped.__name__ = decorated.__name__
    opt_arg_dec_wrapped.__doc__ = decorated.__doc__

    return opt_arg_dec_wrapped


# I don't want to nest all of that inside opt_arg_dec_wrapped! You are free to
# do so, but there is really little to be gained.

def _opt_arg_dec_used(decorated, options, params, kwargs):
    is_method = options["is_method"]

    # is_method is a bool. True == 1, False == 0
    if len(params) - is_method > 0:
        potential_target = params[is_method]

        if potential_target == Ellipsis:
            # We remove the Ellipsis from the parameters.
            params = params[0:is_method] + params[is_method+1:]

        elif ( (options["allow_non_callable"] or callable(potential_target)) 
            and options["valid_target"](potential_target) ):
            return decorated(*params, **kwargs)

        elif isinstance(potential_target, (classmethod, staticmethod)):
            raise TypeError(("Object of type %s passed in as first argument. " + 
                            "Might be expected or an error. If expected, " +
                            "pass Ellipsis as first argument, then the rest. " +
                            "Otherwise, fix the error. staticmethod and " +
                            "classmethod must be the higher decorator so that "+ 
                            "it is the last one called.")
                            % type(potential_target).__name__)

    def opt_arg_dec_with_args(target):
        incompletely_used_decorators.pop(opt_arg_dec_with_args, None)
        if is_method:
            return decorated(params[0], target, *params[1:], **kwargs)
        else:
            return decorated(target, *params, **kwargs)
            
    infos = (decorated, params, kwargs)
    incompletely_used_decorators[opt_arg_dec_with_args] = infos

    return opt_arg_dec_with_args

# The optional arguments decorator is itself an optional arguments decorator!
opt_arg_dec = opt_arg_dec(opt_arg_dec, valid_target="any")

def get_misused_opt_arg_dec():
    """ 
    Used to help developpers find places where an optionnal argument decorator
    was misused. 

    returns a list of (decorator, params, kwargs).

    Things appear in this list when a opt_arg_dec is called without a valid
    target and never called again. Example:

    @my_function_decorator
    class Foo(object):
        pass

    The decorator was expecting to receive a function, but received a class in
    the first call. If this was really what was desired, then you must call the
    decorator first to skip this validation:

    @my_function_decorator()
    class Foo(object):
        pass
    """
    return list(incompletely_used_decorators.values())

Diff to Previous Revision

--- revision 1 2013-01-28 01:35:13
+++ revision 2 2013-01-28 05:03:43
@@ -69,7 +69,7 @@
         if valid_target in _valid_target_defaults:
             valid_target = _valid_target_defaults[valid_target]
     except:
-        # valid_target is not hashable. So its not one of the special values.
+        # valid_target is not hashable. So it's not one of the special values.
         pass
 
     is_method = bool(is_method)

History