"""
================================
INFRASTRUCTURE FOR FUNCTIONS AND
CLASSES WITH MANAGED KEYWORDS
================================
* ``kfunc`` decorator,
* ``iskfunc`` test.
"""
import inspect
import warnings
from .integration import SDE
# (replace with SDE = type(None)
# to make this module stand alone)
########################################
# Private functions for recurring tasks
########################################
def _wraps(wrapped):
"""
Decorator to preserve some basic attributes
when wrapping a function or class.
"""
def decorator(wrapper):
for attr in ('__module__', '__name__', '__qualname__', '__doc__'):
setattr(wrapper, attr, getattr(wrapped, attr))
return wrapper
return decorator
##########################################
# kfunc decorator: function wrapper with
# managed keyword arguments
##########################################
def _kfunc_split_args(x, args):
call_args = {k: z for k, z in args.items()
if k in x._kfunc_call_args}
init_args = {k: z for k, z in args.items()
if k not in x._kfunc_call_args}
return call_args, init_args
class _meta(type):
"""
Metaclass that calls only __new__, not __init__,
upon instantiation.
"""
def __call__(cls, *var, **args):
return cls.__new__(cls, *var, **args)
def _kfunc_decorate_class(f):
"""
Decorator to wrap the given class as a kfunc.
"""
# try to prevent unexpected collateral damage
# with some preemptive checks
# -------------------------------------------
def checkattr(attr):
return any(attr in vars(base) for base in f.__mro__[:-1])
if not hasattr(f, '_is_kfunc'):
# if f is not already a kfunc subclass, make sure
# that __new__ has not been messed with,
# that __init__ and __call__ have been provided,
# and warn against params attribute overwriting
if checkattr('__new__'):
raise TypeError('class {} has a a customized __new__ method, '
'should not be wrapped as a kfunc'
.format(f))
if not (checkattr('__init__') and checkattr('__call__')):
raise TypeError('wrapping {} as a kfunc, no user defined '
'__init__ and/or __call__ methods were found'
.format(f))
if hasattr(f, 'params'):
warnings.warn('wrapping {} as a kfunc, the params attribute '
'will be overwritten'.format(f),
RuntimeWarning)
# get init and call signatures and enforce discipline:
# - init arguments always passed as keywords
# - no overlapping between call and init keywords
init_signature = [(k, z) for k, z in
inspect.signature(f.__init__).parameters.items()
if not z.kind == z.VAR_KEYWORD][1:] # discard self
call_signature = [(k, z) for k, z in
inspect.signature(f.__call__).parameters.items()
if not z.kind == z.VAR_KEYWORD][1:] # discard self
if any(z.kind != z.KEYWORD_ONLY for k, z in init_signature):
raise TypeError(
'cannot wrap {} as a kfunc, all of its parameters '
'(initialization arguments) should be keyword-only'
.format(f))
init_signature = dict([(k, z.default) for k, z in init_signature])
call_signature = dict(call_signature)
if not set(call_signature).isdisjoint(init_signature):
raise TypeError(
'cannot wrap {} as a kfunc, none of its parameters '
'(initialization arguments) should be named as any '
'of its variables (calling arguments)'
.format(f))
# create and return a wrapper class as a subclass of f
# ----------------------------------------------------
@_wraps(f)
class kfunc_class_wrapper(f, metaclass=_meta):
"""
Decorator to add kfunc functionality to a class.
A wrapping subclass of f is returned.
"""
_kfunc_init_args = init_signature
_kfunc_call_args = call_signature
def __new__(cls, *call_vars, **args):
# avoid unsafe subclassing of kfunc classes
if hasattr(cls, '_is_kfunc') and '_is_kfunc' not in vars(cls):
warnings.warn(
'to prevent unexpected init and call behavior, '
'a subclass of a kfunc class should be decorated '
'with kfunc, but {} was not'.format(cls),
RuntimeWarning)
self = f.__new__(cls)
self.__init__(*call_vars, **args)
return self
# separate function arguments from instantiation parameters
call_args, init_args = _kfunc_split_args(cls, args)
# create a new class instance
self = object.__new__(cls)
self._kfunc_params = init_args
self._kfunc_parent = None
self.__init__(**init_args)
# either return the instance, or call it and
# return the result
if call_vars or call_args:
return super(cls, self).__call__(*call_vars, **call_args)
else:
return self
__new__.__wrapped__ = f.__init__
@_wraps(f.__call__)
def __call__(self, *call_vars, **args):
# separate function arguments from instantiation parameters
call_args, init_args = _kfunc_split_args(self, args)
if not init_args:
# if no parameters, make a plain call to self
return super().__call__(*call_vars, **call_args)
else:
# merge stored parameters with current ones
# (current parameters override stored ones)
new_init_args = {**self._kfunc_params, **init_args}
# instantiate a derived object with merged parameters
cls = type(self)
new = object.__new__(cls)
new.__init__(**new_init_args)
new._kfunc_params = new_init_args
new._kfunc_parent = self
# handle evaluation/instantiation
# with merged parameters
if call_vars or call_args:
return super(cls, new).__call__(*call_vars, **call_args)
else:
return new
__call__.__wrapped__ = f.__call__
if issubclass(f, SDE):
# For integrators, the kfunc.params property is specialized
# to include all parameters used by the SDE
# (access is read-only)
@property
def params(self):
return {**self._kfunc_init_args,
**self._kfunc_params,
**self.args}
else:
@property
def params(self):
return {**self._kfunc_init_args,
**self._kfunc_params}
# mark the class as wrapped as a kfunc
_is_kfunc = True
return kfunc_class_wrapper
def _kfunc_decorate_function(nvar):
"""
Decorator to wrap the given function as a kfunc.
"""
def decorator(f):
if isinstance(f, type):
raise SyntaxError('improper use of kfunc decorator - see '
'kfunc docstring')
f_signature = [(k, z) for k, z in
inspect.signature(f).parameters.items()
if not z.kind == z.VAR_KEYWORD]
if not 0 < nvar <= len(f_signature):
# avoid unexpected behaviour with nvar <= 0
raise ValueError('expecting 0 < nvar <= {}, not {}'
.format(len(f_signature), nvar))
if any(z.kind != z.KEYWORD_ONLY for k, z in f_signature[nvar:]):
raise TypeError(
'error wrapping {} as a kfunc - expecting nvar={} '
'initial keyword or non-keyword arguments, '
'as kfunc variables, followed by keyword arguments only, '
'as kfunc parameters'
.format(f, nvar))
f_init_args = dict([(k, z.default) for k, z in f_signature[nvar:]])
f_call_args = set(dict(f_signature[:nvar]))
@_kfunc_decorate_class
@_wraps(f)
class kfunc_function_wrapper:
def __init__(self, **args):
self._kfunc_params = args
def __call__(self, *var, **args):
return f(*var, **args, **self._kfunc_params)
__call__.__wrapped__ = f
kfunc_function_wrapper._kfunc_init_args = f_init_args
kfunc_function_wrapper._kfunc_call_args = f_call_args
kfunc_function_wrapper.__wrapped__ = f
return kfunc_function_wrapper
return decorator
[docs]def kfunc(f=None, *, nvar=None):
"""
Decorator to wrap classes or functions as objects
with managed keyword arguments.
This decorator, intended as an aid to interactive and notebook sessions,
wraps a callable, class or function, as a "kfunc" object
that handles separately its parameters (keyword-only),
whose values are stored in the object, and its variables
(positional or keyword), always provided upon evaluation.
Syntax::
@kfunc
class my_class:
def __init___(self, **kwparams):
...
def __call__(self, *var, **kwvar):
...
@kfunc(nvar=k)
def my_function(*var, **kwargs):
...
After decoration, ``my_class`` is a kfunc with ``kwparams`` as
parameters, and with ``var`` and ``kwvar`` as variables, and
``my_function`` is a kfunc with the first ``k`` of ``var, kwargs``
as variables, and the remaining ``kwargs`` as parameters.
For usage, see examples below.
Attributes
----------
params : dictionary
Parameter values stored in the instance (read-only). For
wrapped ``SDE`` subclasses, also includes default values
of all SDE-specific parameters, as stored in the ``args`` attribute.
Examples
--------
Wrap ``wiener_source`` into a kfunc, named ``dw``:
>>> from sdepy import wiener_source, kfunc
>>> dw = kfunc(wiener_source)
Instantiate ``dw`` and evaluate it (this is business as usual):
>>> my_instance = dw(paths=100, dtype=np.float32)
>>> x = my_instance(t=0, dt=1)
>>> x.shape, x.dtype
((100,), dtype('float32'))
Inspect kfunc parameters stored in ``my_instance``:
>>> my_instance.params # doctest: +SKIP
{'paths': 100, 'vshape': (), 'dtype': <class 'numpy.float32'>, \
'corr': None, 'rho': None}
Evaluate ``my_instance`` changing some parameters (call
the instance with one or more):
>>> x = my_instance(t=0, dt=1, paths=999)
>>> x.shape, x.dtype
((999,), dtype('float32'))
Parameters stored in ``my_instance`` are not affected:
>>> my_instance.paths == my_instance.params['paths'] == 100
True
Create a new instance, changing some parameters and keeping those
already set (call the instance without passing any
variables):
>>> new_instance = my_instance(vshape=2, rho=0.5)
>>> new_instance.params # doctest: +SKIP
{'paths': 100, 'vshape': 2, 'dtype': <class 'numpy.float32'>, \
'corr': None, 'rho': 0.5}
Instantiate and evaluate at once (pass one or more variables
to the class constructor):
>>> x = dw(0, 1, paths=100, dtype=np.float32)
>>> x.shape, x.dtype
((100,), dtype('float32'))
As long as variables are passed by name, order doesn't
matter (omitted variables take default values, if any):
>>> x = dw(paths=100, dtype=np.float32, dt=1, t=0)
>>> x.shape, x.dtype
((100,), dtype('float32'))
"""
# either f is a class and nvar is None,
# or f is None and kfunc has been called with nvar
# specified
case1 = inspect.isclass(f) and nvar is None
case2 = f is None and nvar is not None
if not (case1 or case2):
raise SyntaxError('improper use of kfunc decorator - see '
'kfunc docstring')
if nvar is None:
return _kfunc_decorate_class(f)
else:
return _kfunc_decorate_function(nvar)
[docs]def iskfunc(cls_or_object):
"""
Tests if the given class or instance has been
wrapped as a kfunc.
"""
return hasattr(cls_or_object, '_is_kfunc')