Contrary to popular believe, @decorator
and decorator(…)
are not exactly equivalent. The first runs before name binding, the latter after name binding. For the common use-case of top-level functions, this allows to cheaply test which case applies.
import sys
def decoraware(subject):
"""
Decorator that is aware whether it was applied using `@deco` syntax
"""
try:
module_name, qualname = subject.__module__, subject.__qualname__
except AttributeError:
raise TypeError(f"subject must define '__module__' and '__qualname__' to find it")
if '.' in qualname:
raise ValueError(f"subject must be a top-level function/class")
# see whether ``subject`` has been bound to its module
module = sys.modules[module_name]
if getattr(module, qualname, None) is not subject:
print('@decorating', qualname) # @decoraware
else:
print('wrapping()', qualname) # decoraware()
return subject
This example will merely print how it was applied.
>>> @decoraware
... def foo(): ...
...
@decorating foo
>>> decoraware(foo)
wrapping() foo
The same means can be used to run arbitrary code in each path, though.
In case that multiple decorators are applied, you must decide whether you want the top or bottom subject. For the top-function, the code works unmodified. For the bottom subject, unwrap it using subject = inspect.unwrap(subject)
before detection.
The same approach can be used in a more general way on CPython. Using sys._getframe(n).f_locals
gives access to the local namespace in which the decorator was applied.
def decoraware(subject):
"""Decorator that is aware whether it was applied using `@deco` syntax"""
modname, topname = subject.__module__, subject.__name__
if getattr(sys.modules[modname], topname, None) is subject:
print('wrapping()', topname, '[top-level]')
else:
at_frame = sys._getframe(1)
if at_frame.f_locals.get(topname) is subject:
print('wrapping()', topname, '[locals]')
elif at_frame.f_globals.get(topname) is subject:
print('wrapping()', topname, '[globals]')
else:
print('@decorating', topname)
return subject
Note that similar to pickle
, this approach will fail if the subject's __qualname__
/__name__
is tampered with or it is del
'ed from its defining namespace.