Source code for autosig.autosig

"""Implementation of autosig."""
from attr import attrib, NOTHING, fields_dict, make_class
from collections import OrderedDict
from functools import wraps
from inspect import getsource, signature
from itertools import chain
import re
from toolz.functoolz import curry
from types import BuiltinFunctionType

__all__ = ["Signature", "autosig", "param", "Retval"]

AUTOSIG_DOCSTRING = "__autosig_docstring__"
AUTOSIG_POSITION = "__autosig_position__"


def always_valid(x):
    return True


def identity(x):
    return x


[docs]class Retval: """Define return values in a Signature class. Parameters ---------- validator : callable or type If a callable, it takes the return value as an argument, raising an exception or returning False if invalid; returning True otherwise. If a type, the return value must be an instance of that type. converter : callable The callable is executed with the return value as an argument and its return value is returned instead. Useful to enforce properties of return values, e.g. type, but not only. docstring : string The content for the docstring Returns section. """ def __init__(self, validator=always_valid, converter=identity, docstring=""): """See class docs.""" self._validator = check(validator, is_retval=True) self._converter = converter self._docstring = docstring def __call__(self, x): """Execute converter and validator with x as argument. Returns converter(x) if validator and converter succeed, raises an exception otherwise. Parameters ---------- x : Any The return value of the autosig-decorated function. Returns ------- Any The return value of converter(x). """ x = self._converter(x) self._validator(x) return x
[docs]def param( default=NOTHING, validator=always_valid, converter=identity, docstring="", position=-1, kw_only=False, ): """Define parameters in a signature class. Parameters ---------- default : Any The default value for the parameter (defaults to no default, that is, mandatory). validator : callable or type If a callable, it takes the actual parameter as an argument, raising an exception or returning False if invalid; returning True otherwise. If a type, the actual parameter must be instance of that type. converter : callable The callable is executed with the parameter as an argument and its value assigned to the parameter itself. Useful for type conversions, but not only (e.g. truncate range of parameter). docstring : string The docstring fragment for this parameter. position : int Desired position of the param in the signature. Negative values start from the end. kw_only : bool Whether to make this parameter keyword-only. Returns ------- attr.Attribute Object describing all the properties of the parameter. Can be reused in multiple signature definitions to enforce consistency. """ validator = check(validator, is_retval=False) metadata = {AUTOSIG_DOCSTRING: docstring, AUTOSIG_POSITION: position} kwargs = locals() for key in ("docstring", "position"): del kwargs[key] return attrib(**kwargs)
@curry def keyfun(x, l): pos = x[1].metadata[AUTOSIG_POSITION] return pos if pos >= 0 else l + pos
[docs]class Signature: r"""Class to represent signatures. Parameters ---------- \*params : (str, attr.Attribute) Optional first non-pair argument describes the return value. Each following argument is a pair with the name of an argument in the signature and a description of it generated with a call to param. \*\*kwparams : attr.Attribute Each keyword argument becomes an argument named after the key in the signature of a function and must be initialized with a param call. Requires python >=3.6. If both *param and **params are provided the first will be concatenated with items of the second, in this order. Returns ------- Signature The object created. """ def __init__(self, *params, **kwparams): """See class docs.""" # grab retval id any if len(params) > 0 and isinstance(params[0], Retval): self._retval = params[0] params = params[1:] else: self._retval = None assert all(map(lambda x: len(x) == 2, params)), "Non keyword args must be pairs" all_params = list(chain(iter(params), kwparams.items())) self._params = OrderedDict(sorted(all_params, key=keyfun(l=len(all_params)))) self._late_init = identity def __add__(self, other): """Combine signatures. The resulting signature has the union of the arguments of the left and right operands. The order is determined by the position property of the parameters and when there's a tie, positions are stably sorted with the left operand coming before the right one. Once a name clash occurs, the right operand, quite arbitrarily, wins. Please do not rely on this behavior, it may change. """ assert ( self._retval is None or other._retval is None or self._retval == other._retval ) # must return compatible retvals to combine, or at most one of the two returns anything retval = self._retval if self._retval is not None else other._retval retval = [retval] if retval is not None else [] return Signature( *(chain(retval, self._params.items(), other._params.items())) ).set_late_init( lambda param_dict: ( self._late_init(param_dict), other._late_init(param_dict), ) )
[docs] def set_late_init(self, init): """Set a function to be called immediately after all arguments have been initialized. Use this function to perform initialization logic that involves multiple arguments in the signature. Parameters ---------- init : FunctionType The init function is called after the initialization of all arguments in the signature but before the execution of the body of a function with that signature and is passed as an argument a dictionary with all arguments of the function. Returns None and acts exclusively by side effects. Returns ------- Signature Returns self. """ self._late_init = init return self
def __call__(self, f): """Decorate function f with signature. Makes class directly usable as decorator Parameters ---------- f : Function or method Function or method to be decorated. Returns ------- Function A function decorated with this signature executing and returning values returned by f. """ return autosig(self)(f)
def make_sig_class(sig): return make_class( "Sig_" + str(abs(hash(sig))), attrs=sig._params, # bases=(SigBase, ), eq=False, order=False, )
[docs]def autosig(sig_or_f): """Decorate functions or methods to attach signatures. Use with (W) or without (WO) an argument:: @autosig(Signature(a = param(), b=param())) def fun(a, b) or, equivlently (WO):: @autosig def fun(a=param(), b=param()) Do not include the self argument in the signature when decorating methods Parameters ---------- sig_or_f : Signature or function An instance of class Signature (W) or a function or method (WO) whose arguments are intialized with a call to param. Returns ------- function A decorator (W) or an already decorated function (WO) The decorated function, will intialize, convert, and validate its arguments and will include argument docstrings in its docstring. """ argument_deco = isinstance(sig_or_f, Signature) Sig = make_sig_class( ( sig_or_f if argument_deco else Signature( *[(k, v.default) for k, v in signature(sig_or_f).parameters.items()] ) ) ) retval_sig = ( sig_or_f._retval if argument_deco and sig_or_f._retval is not None else lambda x: x ) retval_docstring = ( sig_or_f._retval._docstring if argument_deco and sig_or_f._retval is not None else "" ) def decorator(f): # if decorator used on instance method, f is still a func here # hence I special case self arg in the following # will break if regular func has self arg TODO: fix if argument_deco: f_params = dict(signature(f).parameters) f_params.pop("self", None) Sig_params = signature(Sig).parameters assert f_params == Sig_params, "\n".join( [ "Mismatched signatures:", str(f), str(f_params), str(Sig), str(Sig_params), ] ) # compared as OrderedDicts, retval ignored TODO: support retval? @wraps(f) def wrapped(*args, **kwargs): try: bound_args = signature(f).bind(*args, **kwargs).arguments args_wo_self = bound_args.copy() args_wo_self.pop("self", None) params = Sig(**args_wo_self) except TypeError as te: raise TypeError(re.sub("__init__", f.__qualname__, te.args[0])) param_dict = params.__dict__ if argument_deco: sig_or_f._late_init(param_dict) if "self" in bound_args: param_dict["self"] = bound_args["self"] retval = f(**param_dict) return retval_sig(retval) wrapped.__doc__ = ( wrapped.__doc__ or """Short summary. """ ) wrapped.__doc__ += "\n\nParameters\n---------\n" + "\n".join( [ k + ": " + v.metadata[AUTOSIG_DOCSTRING] for k, v in fields_dict(Sig).items() ] ) wrapped.__doc__ += "\n\nReturns\n-------\n" + ( retval_docstring if retval_docstring else """ type Description of returned object. """ ) return wrapped return decorator if argument_deco else decorator(sig_or_f)
def check(type_or_predicate, is_retval): """Transform a type or predicate into a autosig-friendly validator. Parameters ---------- type_or_predicate : type or callable A type or a single argument function returning a bool, indicating whether the check was passed. The function will be passed an argument value when check(function) is used as validator argument to param. Returns ------- Callable A Callable to be used as validator argument to param. """ is_type = isinstance(type_or_predicate, type) predicate = ( lambda x: isinstance(x, type_or_predicate) if is_type else type_or_predicate ) def msg(name, value): predicate_desc = ( type_or_predicate if is_type else ( type_or_predicate.__qualname__ if type_or_predicate.__qualname__ != "<lambda>" else getsource(type_or_predicate) ) ) m = ( "type of {name} = {value} should be {predicate_desc}, {type} found instead" if is_type else "{name} = {value} should satisfy {predicate_desc}" ) return m.format( name=name, value=value, type=type(value), predicate_desc=predicate_desc ) def f_param(_, attribute=None, x=None): assert predicate(x), msg(name=attribute.name, value=x) def f_retval(x): assert predicate(x), msg(name="return value", value=x) return f_retval if is_retval else f_param