Skip to content

Instantly share code, notes, and snippets.

@tritium21
Last active June 8, 2020 13:00
Show Gist options
  • Save tritium21/fca09e7ace6cb8edcc28b6372a88e7e9 to your computer and use it in GitHub Desktop.
Save tritium21/fca09e7ace6cb8edcc28b6372a88e7e9 to your computer and use it in GitHub Desktop.
This is a demo of one way to write a decorator.
"""
This is not the right way, or wrong way, to write a decorator;
its just things you can do, with Python 3.8+, to make decorators
more powerful
"""
import functools
import inspect
def demo(func=None, /, *, message='Default Message'):
# we accept func as a positional only argument, and everything else as keyword only.
# If all our arguments are keyword only, then we dont have to use a level of nesting
# to have a decorator that takes options. We assume if no arguments are passed
# positionally, then we have not been used on a function yet, and return
# ourselves partially applied.
if func is None:
return functools.partial(demo, message=message)
# Because decorators can be used on a class definition, we will do so here. in
# this implementation, we are just applying our decorator to all the functions defined
# in our class, unconditionally.
if inspect.isclass(func):
cls = func # so the names are less confusing.
for name, func in inspect.getmembers(cls, inspect.isfunction):
setattr(cls, name, demo(func, message=message))
return cls
# We are adding an attribute to our functions to record if we decorated them or
# not already, so we don't get multiple applications of the decorator. Multiple
# applications of a decorator is not inherently bad, this is just a demo of
# setting and looking up attributes on function objects - something decorators
# are commonly used for.
if getattr(func, '_decorated', False):
return func
func._decorated = True
# We could `return func` here - there is no inherent need to write a wrapper,
# but they are such a common use case that we will continue.
# Finally we create a wrapper function, copying the metadata from the decorated
# function and applying it to the wrapper, so things like help() work on it.
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__qualname__} with {message=}")
return func(*args, **kwargs)
return wrapper
# A demo of our demo.
@demo(message='Class Message')
class Test:
@demo(message="Second Application")
@demo(message="First Application")
def test1(self, bar):
print(f"Test.test1({bar=})")
@demo
def test2(self, bar):
print(f"Test.test2({bar=})")
def test3(self, bar):
print(f"Test.test3({bar=})")
@demo
def test4(bar):
print(f"test4({bar=})")
t = Test()
t.test1('Multiple Applications')
print()
t.test2('No arguments')
print()
t.test3('No decorator')
print()
test4('Not in a class')
# Output:
# Calling Test.test1 with message='First Application'
# Test.test1(bar='Multiple Applications')
#
# Calling Test.test2 with message='Default Message'
# Test.test2(bar='No arguments')
#
# Calling Test.test3 with message='Class Message'
# Test.test3(bar='No decorator')
#
# Calling test4 with message='Default Message'
# test4(bar='Not in a class')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment