Source code for fictive.patterns.decorators

"""
Common patterns for working with :term:`decorator`\\s

"""

import functools


[docs]def optional_arguments(argumented_decorator): """ decroate a :term:`decorator` to be used with or without :term:`argument`\\s Some :term:`decorator`\\s are used without additional :term:`argument`\\s, e.g.: .. testcode:: 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}" .. doctest:: >>> say_hi('adam') 'HI, ADAM!' Other :term:`decorator`\\s are used with additional :term:`argument`\\s that can control characteristics of the :term:`decorator`. E.g.: .. testcode:: 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}" .. doctest:: >>> say_ok('adam') 'OK, ADAM!' >>> say_watch_out('adam') 'WATCH OUT, ADAM!!!' As above, the :term:`decorator` may take optional :term:`keyword argument`\\s or provide suitable default options. In that the case, the syntax would still require empty parentheses: .. testcode:: @shout() def say_goodbye(name): return f"Goodbye, {name}" .. doctest >>> say_goodbye('adam') 'GOODBYE, ADAM!' 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: .. testcode:: @shout def say_welcome(name): return f"Welcome, {name}" .. doctest:: >>> say_welcome('adam') Traceback (most recent call last): ... TypeError: can't multiply sequence by non-int of type 'function' This `optional_arguments` :term:`decorator` can decorate other :term:`decorator`\\s so that they can be used with or without the :term:`argument` parentheses syntax. E.g.: .. testcode:: 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}" .. doctest:: >>> say_ok('adam') 'OK, ADAM!' >>> say_watch_out('adam') 'WATCH OUT, ADAM!!!' :param argumented_decorator: this should be a :term:`decorator` that can take one or more **optional** :term:`argument`\\s. I.e, `argumented_decorrator` should be useable as :code:`argumented_decorator()(function)` :type argumented_decorator: `callable` :returns: a new :term:`decorator` that can be used with or without the parentheses syntax. **NOTE**: if the **ONLY** :term:`argument` to the `argumented_decorator` is a `callable`, it **MUST** be provided as a :term:`keyword argument` when using the new :term:`decorator`. E.g: .. testcode:: 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}" .. doctest:: >>> say_hi('adam') 'HI, ADAM' >>> say_good_day('adam') 'Good Day, Adam' If the only :term:`argument` is a `callable` and it is used as a :term:`positional argument`, the new :term:`decorator` will attempt to decorate the :term:`argument`, and then that decorated `callable` will get used as a :term:`decorator`. This is most likely not what was intended: .. doctest:: :options: +NORMALIZE_WHITESPACE >>> @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 """ @functools.wraps(argumented_decorator) def optionally_argumented_decorator(*args, **kwargs): if len(args) == 1 and not kwargs and callable(args[0]): return argumented_decorator()(args[0]) return argumented_decorator(*args, **kwargs) return optionally_argumented_decorator