Welcome, guest | Sign In | My Account | Store | Cart

If you need your decorator to receive arguments, optional or not, then you should decorate it with the opt_arg_dec decorator to handle all the logic that this implies for you.

Python, 154 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# 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())

When you create a decorator, it will happen that you need to pass parameters to it. Frequently, those parameters will also be optionnal. Usually, this problem is solved by adding logic in the decorator, making itmuch more complicated to read and forcing you to repeat similar code elsewhere if the need arises again. This code defines a decorator opt_arg_dec to apply to your decorator to allow them to receive arguments (required or not) automatically. You don't even need to tell opt_arg_dec about them, the parameters received will be passed to your decorator. Using this decorator is very simple. @opt_arg_dec def call_logger(func, include_args=False, include_return=False): def call_logging_func(params, *kwargs): print("Calling", func.__name__, include_args, include_return) # Do your logging ret = func(params, *kwargs) return ret return call_logging_func

# You can now use the decorator in the following ways:
@call_logger
def hello1(arg1, arg2="!"):
    print("hello", arg1, arg2)

@call_logger()
def hello2(arg1, arg2="!"):
    print("hello", arg1, arg2)

@call_logger(True, False)
def hello3(arg1, arg2="!"):
    print("hello", arg1, arg2)

@call_logger(include_return=True)
def hello4(arg1, arg2="!"):
    print("hello", arg1, arg2)

def hello5(arg1, arg2="!"):
    print("hello", arg1, arg2)
hello5 = call_logger(True, True)(hello5)

def hello6(arg1, arg2="!"):
    print("hello", arg1, arg2)
hello6 = call_logger(hello6, True)


>>> hello1("world")
Calling hello1 with False and False
hello world !

>>> hello2(arg1="world")
Calling hello2 with False and False
hello world !

>>> hello3("world", arg2="!!!!!!")
Calling hello3 with True and False
hello world !!!!!!

>>> hello4("guys")
Calling hello4 with False and True
hello guys !

>>> hello5("everybody", ":)")
Calling hello5 with True and True
hello everybody :)

>>> hello6("sir")
Calling hello6 with True and False
hello sir !

This basically allows you do decorate things the way you want. However, there is a little catch created by allowing you do only call the decorator once. (as used in the example hello1 and hello6). In order to know your decorator should be called on the first argument of the first call(hello1 and hello6) or wait for a second call (hello2, hello3, hello4, hello5), the decorator needs to do checks on the first parameter sent to it. So if you send something that is a valid target as first argument, you will have a problem:

def should_i_include_args():
    pass

# Won't work!
@call_logger(should_i_include_args)
def hello7(arg1, arg2="!"):
    print("hello", arg1, arg2)

This will fail. You passed a function as first argument, so the decorator accepts it as the function to be decorated. It will then call this function with hello7. Same thing happens if you do this:

# Won't work!
def hello8(arg1, arg2="!"):
    print("hello", arg1, arg2)
hello8 = call_logger(should_i_include_args)(hello8)

The solution is to pass in Ellipsis as first argument. The opt_arg_dec will automatically remove it (only when first argument) and understand that this means that there will be another call to receive the real target of the decorator. You can have more control on this behavior, see further down.

def should_i_include_args():
    pass

# This works!
@call_logger(Ellipsis, should_i_include_args)
def hello7(arg1, arg2="!"):
    print("hello", arg1, arg2)

# This works! but really, why are you using this form?
def hello8(arg1, arg2="!"):
    print("hello", arg1, arg2)
hello8 = call_logger(Ellipsis, should_i_include_args)(hello8)

If you are using Python 3, then be happy to know that Ellipsis has a shortcut. You can simply write ... and python will recognize it as Ellipsis. The result is much simpler to read.

@call_logger(..., should_i_include_args)
def hello7(arg1, arg2="!"):
    print("hello", arg1, arg2)

# This works! but really, why are you using this form?
def hello8(arg1, arg2="!"):
    print("hello", arg1, arg2)
hello8 = call_logger(..., should_i_include_args)(hello8)

Another thing to be careful about is that if you want a method or a classmethod to be an opt_arg_dec, then you need to pass an optionnal parameter (is_method=True) to opt_arg_dec so that it handle the self or cls correctly. Do not do this for staticmethod as they do not add an additionnal argument.

class call_logger(object):

    @opt_arg_dec(is_method=True)
    def log_call(self, func, include_args=False, include_return=False):
        def call_logging_func(*params, **kwargs):
            print("Calling", func.__name__, include_args, include_return)
            return func(*params, **kwargs)
        return call_logging_func

    @classmethod
    @opt_arg_dec(is_method=True)
    def log_call_everywhere(self, func, include_args=False, include_return=False):
        def call_logging_func(*params, **kwargs):
            print("Calling", func.__name__, include_args, include_return)
            return func(*params, **kwargs)
        return call_logging_func


    @staticmethod
    @opt_arg_dec
    def log_call_default(self, func, include_args=False, include_return=False):
        def call_logging_func(*params, **kwargs):
            print("Calling", func.__name__, include_args, include_return)
            return func(*params, **kwargs)
        return call_logging_func

These work the exact same way as when used on a function. There is simply some added logic to deal with the additionnal paremeter. Also keep in mind when using staticmethod or classmethod on a decorator, they must be the last one called, so they must be the first decorator written.

If you want a class to be useable as an opt_arg_dec, then simply decorate it with it.

# Couldn't think of a context where this was necessary...
@opt_arg_dec
class call_log(object):
    def __new__(cls, func, include_args=False, include_return=False):
        def call_logging_func(*params, **kwargs):
            print("Calling", func.__name__, include_args, include_return)
            return func(*params, **kwargs)
        return call_logging_func

If you want a class instance to be useable as an opt_arg_dec, then decorate the __call__ function with it. Don't forget the is_method=True.

class call_logger(object):
    @opt_arg_dec(is_method=True)
    def __call__(self, func, include_args=False, include_return=False):
        def call_logging_func(*params, **kwargs):
            print("Calling", func.__name__, include_args, include_return)
            return func(*params, **kwargs)
        return call_logging_func

In order to support being called only once and detecting that the first argument should be used as target of the decorator instead of waiting for a second call, opt_arg_dec does some check on the first argument when it is not Ellipsis. You can control those checks depending on what is likely to be used with your decorator. Keep in mind; those checks are only used in the first call to the decorator. The second call will accept anything. Meaning:

@opt_arg_dec
def my_decorator(func, p1=None, p2=None):
    return func # Doesn't do anything.

def test1():
    pass

# "hi" will be checked to see if it's a valid target. It's not, so it will wait
# for the next call.
my_decorator("hi", "you")
def test1(): pass

# "hi" is checked again, wait for second call again. "world" is not checked and
# just goes through. Meaning your decorator would receive ("world", "hi", "you")
# as argument, no function or callable.
my_decorator("hi", "you")("world")

The control you have over what you say is a valid target for your decorator is by using the parameters valid_target and allow_non_callable that you can pass to opt_arg_dec when creating your decorator. 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.

Basically, the special values should cover most common cases. If you want your decorator to be applicable to classes only, so a class decorator, then use valid_target="class". Note that opt_arg_dec itself is an opt_arg_dec with valid_target="any", that’s why you can use it on a function or a class.

# a class decorator
@opt_arg_dec(valid_target="class")
def my_class_decorator(cls, arg1=None):
    pass

In order to help with problems related to this target validation, a debugging function is provided: get_misused_opt_arg_dec()

Used to help developpers find places where an optionnal argument decorator was misused. It returns a list of (decorator, params, kwargs).

Basically, once you have created all of your opt_arg_dec, or simply at the end of your application. get_misused_opt_arg_dec() doesn't return an empty list, it means that somewhere, something is not defined properly. The first value of params should normally be the wrong target, and should help you find where you did the mistake. Things appear in this list when a opt_arg_dec is called without a valid target and never called again. Example:

# my_function_decorator is expecting something that is not a class (the default)
# but receives a class
@my_function_decorator
class Foo(object):
    pass

# Right now, Foo is not the class, it's a function waiting to receive anything,
# which will return Foo. Probably not what you want.

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

Thats pretty much it. Hope this becomes the standard answer to "how to make a decorator with (optional) arguments".

Created by Maxime H Lapointe on Mon, 28 Jan 2013 (MIT)
Python recipes (4591)
Maxime H Lapointe's recipes (1)

Required Modules

  • (none specified)

Other Information and Tasks