If you instrument attach_wrapper you can see that it accounts for its func argument being None.
def attach_wrapper(obj, func=None): # Helper function that attaches function as attribute of an object
if func is None:
print(f'obj is {obj.__name__} - func is',func)
return partial(attach_wrapper, obj)
print(f'a_w:obj is {obj.__name__} - func is {func.__name__}')
setattr(obj, func.__name__, func)
return func
Then
@log(logging.WARN, "example-param")
def somefunc(args):
return args
Results in
>>>
obj is somefunc - func is None
a_w:obj is somefunc - func is set_level
obj is somefunc - func is None
a_w:obj is somefunc - func is set_message
>>>
attach_wrapper is called twice. The first time its func is None and it returns a partial function attach_wrapper with its first argument obj set to some_func. Then the partial is called (all parts of the decorator mechanism) with the function being decorated.
attach_wrapper's obj looks like some_func but it is actually some_func wrapped with wrapper - it looks and feels the same because that is what @wraps was made for.
Here is a little more detail adding inspect.stack to the instrumentation.
import inspect
def attach_wrapper(obj, func=None): # Helper function that attaches function as attribute of an object
print("called from: ", inspect.stack()[1].code_context[0].strip())
print("\tby function ", inspect.stack()[1].function)
if func is None:
print(f'\tobj is {obj.__name__}, func is {func}\n')
return partial(attach_wrapper, obj)
print(f'\tobj is {obj.__name__}, func is {func.__name__}\n')
setattr(obj, func.__name__, func)
return func
Which produces
>>>
called from: @attach_wrapper(wrapper) # Attaches "set_level" to "wrapper" as attribute
by function decorate
obj is somefunc, func is None
called from: def set_level(new_level): # Function that allows us to set log level
by function decorate
obj is somefunc, func is set_level
called from: @attach_wrapper(wrapper) # Attaches "set_message" to "wrapper" as attribute
by function decorate
obj is somefunc, func is None
called from: def set_message(new_message): # Function that allows us to set message
by function decorate
obj is somefunc, func is set_message
>>>
There are some better detailed explanations around. If and when I find one I'll update.