fictive.patterns.decorators module

Common patterns for working with decorators

Functions

optional_arguments

decroate a decorator to be used with or without arguments

fictive.patterns.decorators.optional_arguments(argumented_decorator)[source]

decroate a decorator to be used with or without arguments

Some decorators are used without additional arguments, e.g.:

import functools

def shout(function):
    suffix = '!'
    @functools.wraps(function)
    def decorated(*args, **kwargs):
        return function(*args, **kwargs).upper() + suffix
    return decorated

@shout
def say_hi(name):
    return f"Hi, {name}"
>>> say_hi('adam')
'HI, ADAM!'

Other decorators are used with additional arguments that can control characteristics of the decorator. E.g.:

import functools

def shout(volume=1):
    def controlled_shout(function):
        suffix = '!' * volume
        @functools.wraps(function)
        def decorated(*args, **kwargs):
            return function(*args, **kwargs).upper() + suffix
        return decorated
    return controlled_shout

@shout(1)
def say_ok(name):
    return f"Ok, {name}"

@shout(3)
def say_watch_out(name):
    return f"Watch out, {name}"
>>> say_ok('adam')
'OK, ADAM!'
>>> say_watch_out('adam')
'WATCH OUT, ADAM!!!'

As above, the decorator may take optional keyword arguments or provide suitable default options. In that the case, the syntax would still require empty parentheses:

@shout()
def say_goodbye(name):
    return f"Goodbye, {name}"

While functional, this can look a bit non-idiomatic in practice. Furthermore, if one inadvertently neglects to add the (), this might not be caught as a syntax error, but will likely lead to an Exception when calling the decorated object:

@shout
def say_welcome(name):
    return f"Welcome, {name}"
>>> say_welcome('adam')
Traceback (most recent call last):
    ...
TypeError: can't multiply sequence by non-int of type 'function'

This optional_arguments decorator can decorate other decorators so that they can be used with or without the argument parentheses syntax. E.g.:

import functools
from fictive.patterns.decorators import optional_arguments

@optional_arguments
def shout(volume=1):
    def controlled_shout(function):
        suffix = '!' * volume
        @functools.wraps(function)
        def decorated(*args, **kwargs):
            return function(*args, **kwargs).upper() + suffix
        return decorated
    return controlled_shout

@shout
def say_ok(name):
    return f"Ok, {name}"

@shout(3)
def say_watch_out(name):
    return f"Watch out, {name}"
>>> say_ok('adam')
'OK, ADAM!'
>>> say_watch_out('adam')
'WATCH OUT, ADAM!!!'
Parameters

argumented_decorator (callable) – this should be a decorator that can take one or more optional arguments. I.e, argumented_decorrator should be useable as argumented_decorator()(function)

Returns

a new decorator that can be used with or without the parentheses syntax.

NOTE: if the ONLY argument to the argumented_decorator is a callable, it MUST be provided as a keyword argument when using the new decorator. E.g:

import functools
from fictive.patterns.decorators import optional_arguments

@optional_arguments
def modulate(transform=str.upper):
    def controlled_say(function):
        @functools.wraps(function)
        def decorated(*args, **kwargs):
            return transform(function(*args, **kwargs))
        return decorated
    return controlled_say

@modulate
def say_hi(name):
    return f"Hi, {name}"

@modulate(transform=str.title)
def say_good_day(name):
    return f"Good day, {name}"
>>> say_hi('adam')
'HI, ADAM'
>>> say_good_day('adam')
'Good Day, Adam'

If the only argument is a callable and it is used as a positional argument, the new decorator will attempt to decorate the argument, and then that decorated callable will get used as a decorator. This is most likely not what was intended:

>>> @modulate(str.lower)
... def say_excuse_me(name):
...     return f"Excuse me, {name}"
Traceback (most recent call last):
    ...
TypeError: descriptor 'lower' for 'str' objects doesn't apply            to a 'function' object