Depending on how you want to go about this, there are multiple options available. The one by @blhsing certainly works well. Here are some more:
Wrap in dictionary (simple)
def wrap_with_info(**kwargs):
def decorator(obj):
if isinstance(obj, dict):
return obj | kwargs
# `obj` must be a function
assert callable(obj)
return kwargs | {"callback": obj}
return decorator
This allows you to decorate like this:
@wrap_with_info(
name="poke",
documentation="handle poke instruction",
layout={"address": 2, "data": 1},
)
def your_function():
pass
And also like this:
@wrap_with_info(name="poke")
@wrap_with_info(documentation="handle poke instruction")
@wrap_with_info(layout={"address": 2, "data": 1})
def your_function():
pass
Note that this method always essentially turns your_function into a dictionary:
print(your_function)
Gives this output:
{'layout': {'address': 2, 'data': 1}, 'callback': <function your_function at 0x7fd1184ae710>, 'documentation': 'handle poke instruction', 'name': 'poke'}
Decorate to return dictionary
from functools import wraps
def add_info(**kwargs):
def decorator(func):
def wrapper(*func_args, **func_kwargs):
@wraps(func)
def callback():
return func(*func_args, **func_kwargs)
return kwargs | {"callback": callback}
return wrapper
return decorator
Then you also decorate like this:
@add_info(
name="poke",
documentation="handle poke instruction",
layout={"address": 2, "data": 1},
)
def another_function():
pass
But now another_function remains a callable that returns the dictionary containing the original function (wrapped) and the additional data:
print(another_function())
Gives this output:
{'name': 'poke', 'documentation': 'handle poke instruction', 'layout': {'address': 2, 'data': 1}, 'callback': <function another_function at 0x7fb4ae7a67a0>}
The functools.wraps decorator ensures that the original function's signature is retained when we stick it in the callback. This is why you see its name in the output instead of something like ...local.callback....
(Side note: I am using the | notation to combine dictionaries, which is available since Python 3.9. If that doesn't work for you, you can use other means, like dict.update for example.)
Advantages
It seems like all you are doing with your decorators is adding key-value-pairs to a dictionary of information around an actual function. This method makes it unnecessary to use multiple decorators. Instead you can pass any number of key-value-pairs depending on the function.
They can also be constructed dynamically, if that is something you need, and if you know the data you want to add to the functions ahead of time:
info_x = {"spam": 1, "eggs": 2}
info_y = {"info": "abc"}
info_z = {"something": ["else"]}
...
@add_info(**info_x, **info_y)
def foo():
pass
@add_info(**info_y, **info_z)
def bar():
pass
@wrap_with_info(**info_z, **info_x)
def baz():
pass
print(foo())
print(bar())
print(baz)
Gives this output:
{'spam': 1, 'eggs': 2, 'info': 'abc', 'callback': <function foo at 0x7f5d1abb29e0>}
{'info': 'abc', 'something': ['else'], 'callback': <function bar at 0x7f5d1abb29e0>}
{'something': ['else'], 'spam': 1, 'eggs': 2, 'callback': <function baz at 0x7efd492aeb00>}