# -*- coding: utf-8 -*-
# This software and supporting documentation are distributed by
# Institut Federatif de Recherche 49
# CEA/NeuroSpin, Batiment 145,
# 91191 Gif-sur-Yvette cedex
# France
#
# This software is governed by the CeCILL-B license under
# French law and abiding by the rules of distribution of free software.
# You can use, modify and/or redistribute the software under the
# terms of the CeCILL-B license as circulated by CEA, CNRS
# and INRIA at the following URL "http://www.cecill.info".
#
# As a counterpart to the access to the source code and rights to copy,
# modify and redistribute granted by the license, users are provided only
# with a limited warranty and the software's author, the holder of the
# economic rights, and the successive licensors have only limited
# liability.
#
# In this respect, the user's attention is drawn to the risks associated
# with loading, using, modifying and/or developing or reproducing the
# software by the user in light of its specific status of free software,
# that may mean that it is complicated to manipulate, and that also
# therefore means that it is reserved for developers and experienced
# professionals having in-depth computer knowledge. Users are therefore
# encouraged to load and test the software's suitability as regards their
# requirements in conditions enabling the security of their systems and/or
# data to be ensured and, more generally, to use and operate it in the
# same conditions as regards security.
#
# The fact that you are presently reading this means that you have had
# knowledge of the CeCILL-B license and that you accept its terms.
'''
Utility classes and functions for Python callable.
* author: Yann Cointepas
* organization: `NeuroSpin <http://www.neurospin.org>`_
* license: `CeCILL B <http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.html>`_
'''
from __future__ import absolute_import
from six.moves import zip
import six
__docformat__ = "restructuredtext en"
import inspect
# handle deprecation of getargspec in python3
getfullargspec = getattr(inspect, 'getfullargspec', inspect.getargspec)
#-------------------------------------------------------------------------
from soma.translation import translate as _
#-------------------------------------------------------------------------
class Empty(object):
pass
#-------------------------------------------------------------------------
[docs]class SomaPartial(object):
'''
This is a reimplementation of :py:func:`functools.partial`, which adds
compatibility with the :py:mod:`traits` module.
See the documentation of :py:func:`functools.partial` for details:
https://docs.python.org/library/functools.html#functools.partial
Example::
from soma.functiontools import partial
def f(a, b, c):
return (a, b, c)
g = partial(f, 'a', c='c')
g('b') # calls f('a', 'b', c='c')
'''
def __init__(self, function, *args, **kwargs):
self.func = function
self.args = args
self.keywords = kwargs
def __call__(self, *args, **kwargs):
merged_kwargs = self.keywords.copy()
merged_kwargs.update(kwargs)
return self.func(*(self.args + args), **merged_kwargs)
@property
def func_code(self):
'''
This property make SomaPartial useable with traits module. The method
on_trait_change is requiring that the function has a func_code.co_argcount
attribute.
'''
result = Empty()
result.co_argcount = numberOfParameterRange(self)[0]
return result
@property
def __code__(self):
return self.func_code
try:
from functools import partial
except ImportError:
partial = SomaPartial
#-------------------------------------------------------------------------
[docs]def getArgumentsSpecification(callable):
'''
This is an extension of Python module :py:mod:`inspect.getargspec` that
accepts classes and returns only information about the parameters that can
be used in a call to *callable* (*e.g.* the first *self* parameter of bound
methods is ignored). If *callable* has not an appropriate type, a
:class:`TypeError <exceptions.TypeError>` exception is raised.
Parameters
----------
callable: callable
*function*, *method*, *class* or *instance* to inspect
Returns
-------
tuple:
As :func:`inspect.getargspec`, returns
*(args, varargs, varkw, defaults)* where *args* is a list of the
argument names (it may contain nested lists). *varargs* and *varkw* are
the names of the ``*`` and ``**`` arguments or *None*. *defaults* is a
n-tuple of the default values of the last *n* arguments.
'''
if inspect.isfunction(callable):
return getfullargspec(callable)[:4]
elif inspect.ismethod(callable):
args, varargs, varkw, defaults = getfullargspec(callable)[:4]
args = args[1:] # ignore the first "self" parameter
return args, varargs, varkw, defaults
elif inspect.isclass(callable):
try:
init = callable.__init__
except AttributeError:
return [], None, None, None
return getArgumentsSpecification(init)
elif isinstance(callable, (partial, SomaPartial)):
args, varargs, varkw, defaults = getArgumentsSpecification(
callable.func)
if defaults:
d = dict(zip(reversed(args), reversed(defaults)))
else:
d = {}
d.update(zip(reversed(args), reversed(callable.args)))
if callable.keywords:
d.update(callable.keywords)
if len(d):
defaults = tuple((d[i] for i in args[-len(d):]))
else:
defaults = d
return (args, varargs, varkw, defaults)
else:
try:
call = callable.__call__
except AttributeError:
raise TypeError(_('%s is not callable') %
repr(callable))
return getArgumentsSpecification(call)
#-------------------------------------------------------------------------
[docs]def getCallableString(callable):
'''
Returns a translated human readable string representing a callable.
Parameters
----------
callable: callable
*function*, *method*, *class* or *instance* to inspect
Returns
--------
string:
type and name of the callable
'''
if inspect.isfunction(callable):
name = _('function %s') % (callable.__name__, )
elif inspect.ismethod(callable):
name = _('method %s') % (six.get_method_self(callable).__class__.__name__ + '.' +
callable.__name__, )
elif inspect.isclass(callable):
name = _('class %s') % (callable.__name__, )
else:
name = str(callable)
return name
#-------------------------------------------------------------------------
[docs]def hasParameter(callable, parameterName):
'''
Returns *True* if *callable* can be called with a parameter named
*parameterName*. Otherwise, returns *False*.
.. seealso:: :py:func:`getArgumentsSpecification`
Parameters
----------
callable: callable
*function*, *method*, *class* or *instance* to inspect
parameterName: string
name of the parameter
Returns
-------
bool:
'''
args, varargs, varkw, defaults = getArgumentsSpecification(callable)
return varkw is not None or parameterName in args
#-------------------------------------------------------------------------
[docs]def numberOfParameterRange(callable):
'''
Returns the minimum and maximum number of parameter that can be used to
call a function. If the maximum number of argument is not defined, it is
set to *None*.
.. seealso:: :func:`getArgumentsSpecification`
Parameters
----------
callable: callable
*function*, *method*, *class* or *instance* to inspect
Returns
-------
tuple:
two elements (minimum, maximum)
'''
args, varargs, varkw, defaults = getArgumentsSpecification(callable)
if defaults is None or len(defaults) > len(args):
lenDefault = 0
else:
lenDefault = len(defaults)
minimum = len(args) - lenDefault
if varargs is None:
maximum = len(args)
else:
maximum = None
return minimum, maximum
#-------------------------------------------------------------------------
[docs]def checkParameterCount(callable, paramCount):
'''
Checks that a callable can be called with *paramCount* arguments. If not, a
RuntimeError is raised.
.. seealso:: :func:`getArgumentsSpecification`
Parameters
----------
callable: callable
*function*, *method*, *class* or *instance* to inspect
paramCount: int
number of parameters
'''
minimum, maximum = numberOfParameterRange(callable)
if ( maximum is not None and paramCount > maximum ) or \
paramCount < minimum:
raise RuntimeError(
_('%(callable)s cannot be called with %(paramCount)d arguments') %
{'callable': getCallableString(callable),
'paramCount': paramCount})
#-------------------------------------------------------------------------
[docs]def drange(start, stop, step=1):
'''
Creates lists containing arithmetic progressions of any number type (int,
float, ...)
'''
r = start
while r < stop:
yield r
r += step