# sigtools - Collection of Python modules for manipulating function signatures
# Copyright (C) 2013-2022 Yann Kaiser
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
"""
`sigtools.wrappers`: Combine multiple functions
-----------------------------------------------
The functions here help you combine multiple functions into a new callable
which will automatically advertise the correct signature.
"""
from functools import partial, update_wrapper, wraps
from sigtools import _util, signatures, specifiers
[docs]class Combination(object):
"""Creates a callable that passes the first argument through each
callable, using the result of each pass as the argument to the next
"""
def __init__(self, *functions):
funcs = self.functions = []
for function in functions:
if isinstance(function, Combination):
funcs.extend(function.functions)
else:
funcs.append(function)
specifiers.set_signature_forger(self, self.get_signature,
emulate=False)
def __call__(self, arg, *args, **kwargs):
for function in self.functions:
arg = function(arg, *args, **kwargs)
return arg
[docs] def get_signature(self, obj):
return signatures.merge(
signatures.signature(self),
*(specifiers.signature(func) for func in self.functions)
)
def __repr__(self):
return '{0.__module__}.{0.__name__}({1})'.format(
type(self), ', '.join(repr(f) for f in self.functions)
)
[docs]def decorator(func):
"""Turns a function into a decorator.
The function received the decorated function as first argument.
::
from sigtools import wrappers
@wrappers.decorator
def my_decorator(func, *args, deco_param=False, **kwargs):
... # Do stuff with deco_param
return func(*args, **kwargs)
@my_decorator
def my_function(func_param):
...
my_function('value for func_param', deco_param=True)
from sigtools import specifiers
print(specifiers.signature(my_function))
# (func_param, *, deco_param=False)
Unlike `wrapper_decorator`, ``decorator`` does not require you to specify
how your function uses ``*args, **kwargs`` and lets automatic signature
discovery figure it out.
.. note:: Signature reporting will not work in interactive sessions, as per
:ref:`autofwd limits`.
"""
@wraps(func)
def _decorate(decorated):
return _SimpleWrapped(func, decorated)
return _decorate
class _SimpleWrapped(object):
def __init__(self, wrapper, wrapped):
update_wrapper(self, wrapped)
self.func = partial(wrapper, wrapped)
self.wrapper = wrapper
self._sigtools__wrappers = wrapper,
self.__wrapped__ = wrapped
try:
del self._sigtools__forger
except AttributeError:
pass
try:
del self.__signature__
except AttributeError:
pass
__signature__ = specifiers.as_forged
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
def __get__(self, instance, owner):
return type(self)(
self.wrapper,
_util.safe_get(self.__wrapped__, instance, owner))
def __repr__(self):
return '<{0!r} wrapped with {1!r}>'.format(
self.__wrapped__, self.wrapper)
[docs]@specifiers.forwards_to_function(specifiers.forwards, 2)
def wrapper_decorator(*args, **kwargs):
"""Turns a function into a decorator that wraps callables with
that function.
Consult `.signatures.forwards`'s documentation for :ref:`help picking the correct values for the parameters <forwards-pick>`.
The wrapped function is passed as first argument to the wrapper.
As an example, here we create a ``@print_call`` decorator which wraps the
decorated function and prints a line everytime the function is called::
>>> from sigtools import modifiers, wrappers
>>> @wrappers.wrapper_decorator
... @modifiers.autokwoargs
... def print_call(func, _show_return=True, *args, **kwargs):
... print('Calling {0.__name__}(*{1}, **{2})'.format(func, args, kwargs))
... ret = func(*args, **kwargs)
... if _show_return:
... print('Return: {0!r}'.format(ret))
... return ret
...
>>> print_call
<decorate with <<function print_call at 0x7f28d721a950> with signature print_cal
l(func, *args, _show_return=True, **kwargs)>>
>>> @print_call
... def as_list(obj):
... return [obj]
...
>>> as_list
<<function as_list at 0x7f28d721ad40> decorated with <<function print_call at 0x
7f28d721a950> with signature print_call(func, *args, _show_return=True, **kwargs
)>>
>>> from inspect import signature
>>> print(signature(as_list))
(obj, *, _show_return=True)
>>> as_list('ham')
Calling as_list(*('ham',), **{})
Return: ['ham']
['ham']
>>> as_list('spam', _show_return=False)
Calling as_list(*('spam',), **{})
['spam']
"""
if not kwargs and len(args) == 1 and callable(args[0]):
return _WrapperDecorator((), {}, args[0])
return partial(_WrapperDecorator, args, kwargs)
class _WrapperDecorator(object):
def __init__(self, f_args, f_kwargs, wrapper):
self.f_args = f_args
self.f_kwargs = f_kwargs
self.wrapper = wrapper
def wrap(self, wrapped):
return _Wrapped(self, self.wrapper, wrapped)
__call__ = wrap
def __repr__(self):
return '<wrap with {0!r}>'.format(self.wrapper)
class _Wrapped(object):
def __init__(self, deco, wrapper, wrapped):
func = partial(wrapper, wrapped)
update_wrapper(self, wrapped)
self.func = func
self.wrapper = wrapper
self._sigtools__wrappers = wrapper,
self.decorator = deco
self.__wrapped__ = wrapped
try:
del self._sigtools__forger
except AttributeError:
pass
try:
del self.__signature__
except AttributeError:
pass
__signature__ = specifiers.as_forged
def _sigtools__forger(self, obj):
return specifiers.forwards(
self.func, self.__wrapped__,
*self.decorator.f_args, **self.decorator.f_kwargs)
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
def __get__(self, instance, owner):
return type(self)(
self.decorator, self.wrapper,
_util.safe_get(self.__wrapped__, instance, owner))
def __repr__(self):
return '<{0!r} wrapped with {1!r}>'.format(
self.__wrapped__, self.wrapper)
[docs]def wrappers(obj):
"""For introspection purposes, returns an iterable that yields each
wrapping function of obj(as done through `wrapper_decorator`, outermost
wrapper first.
Continuing from the `wrapper_decorator` example::
>>> list(wrappers.wrappers(as_list))
[<<function print_call at 0x7f28d721a950> with signature print_call(func, *args,
_show_return=True, **kwargs)>]
"""
while True:
try:
wrappers = obj._sigtools__wrappers
except AttributeError:
return
for wrapper in wrappers:
yield wrapper
obj = obj.__wrapped__