"""
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