# -*- 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.
from __future__ import absolute_import
import types
import sys
from soma.translation import translate as _
from soma.sorted_dictionary import SortedDictionary
from soma.notification import Notifier, VariableParametersNotifier, \
ObservableAttributes
from soma.singleton import Singleton
from soma.undefined import Undefined
import copy
import six
#-------------------------------------------------------------------------
class DataType(object):
"""
C{DataType} derived classes manage the behaviour of all attributes defined in
a signature of an object (most often a L{HasSignature} instance). In order to
define a new type of parameter, one must derive from C{DataType} and define
at least the L{checkValue} method. See C{DataType} methods documentation for
more information on how to customize a type.
"""
def __init__(self):
super(DataType, self).__init__()
# Some inherited class forbid attribute modification, therefore __dict__
# is accessed directly
#: Indicate wether the value of this type are mutable (I{i.e.} can be
#: modified) or not. By default, C{mutable} is set to C{True}.For more
#: information about mutable or immutable objects, see
#: U{Python documentation<https://docs.python.org/>}.
self.mutable = True
#: Name of this C{DataType}, by default, it is the class name without the
#: module(s) prefix (I{i.e.} as defined in the C{class} statement).
self.name = self.__class__.__name__.split('.')[-1]
def dataTypeInstance(value):
"""
Static method to create a L{DataType} instance from a user defined value.
The C{value} argument must be one of the following:
- B{A DataType instance}: C{value} is returned untouched.
- B{A class deriving from L{DataType}}: C{value()} is returned.
- B{A class not deriving from DataType}: returns
C{L{ClassDataType}( value )}.
Any other value lead to a C{TypeError} exception.
"""
if isinstance( value, six.class_types ):
if issubclass(value, DataType):
value = value()
else:
value = ClassDataType(value)
elif not isinstance(value, DataType):
raise TypeError(
_('Expected a DataType instance or a class but got %s') % (repr(value),))
return value
dataTypeInstance = staticmethod(dataTypeInstance)
def checkValue(self, value):
"""
Verify that C{value} is valid for this type of attribute. Raises a
C{ValueError} if the value is not valid. Must be overloaded in derived
classes.
@return: the real value that will be used for the attribute. It is
recomended to return C{value} here unless you know what you are doing.
"""
raise RuntimeError(_('checkValue method has not be defined for %s') %
(self.__class__.__name__, ))
def createValue(self):
'''
Create a default value for this type of attribute.
'''
raise RuntimeError(_('createValue method has not be defined for %s') %
(self.__class__.__name__, ))
def _getAttribute(self, hasSignatureObject, name):
"""
This method allow to customize attribute read acces of L{HasSignature}
instances for all attributes declared as a specific C{DataType} in the
signature.
If not overloaded, it returns the value of attribute C{name} of
C{hasSignatureObject}, or raises an C{AttributeError} if this attribute is
not defined.
@param hasSignatureObject: object containing an attribute to read.
@type hasSignatureObject: L{HasSignature} instance
@param name: name of the attribute to read
@type name: C{string}
@return: value of C{getattr( hasSignatureObject, name )}
"""
return super(HasSignature, hasSignatureObject).__getattribute__(name)
def _setAttribute(self, hasSignatureObject, name, value):
"""
This method allow to customize attribute write acces of L{HasSignature}
instances for all attributes declared as a specific C{DataType} in the
signature.
If not overloaded, if calls C{self.L{checkValue}( value )} and set the
attribute with the value returned.
@param hasSignatureObject: object where an attribute must be set.
@type hasSignatureObject: L{HasSignature} instance
@param name: name of the attribute to set.
@type name: C{string}
@param value: value to set to the attribute.
@return: the new value of the attribute
"""
checkValue = getattr(hasSignatureObject, '_check_' + name + '_value',
self.checkValue)
newValue = checkValue(value)
super(HasSignature, hasSignatureObject).__setattr__(name, newValue)
return newValue
def _deleteAttribute(self, hasSignatureObject, name):
"""
This method allow to customize attribute deletion of L{HasSignature}
instances for all attributes declared as a specific C{DataType} in the
signature.
If not overloaded, it raises C{TypeError} because attributes declared in
the signature cannot be deleted.
@param hasSignatureObject: object containing an attribute to delete
@type hasSignatureObject: L{HasSignature} instance
@param name: name of the attribute to delete
@type name: C{string}
"""
raise TypeError(_('Cannot delete signature attribute %s') % (name, ))
def _checkValueError(self, value):
"""
Raises a C{ValueError} with a message saying that C{value} is not a valid
value for this data type. This method is used in several classes
derived from C{DataType}.
"""
raise ValueError(_('%(value)s is not a valid value for data type '
'%(type)s')
% {'type': str(self), 'value': repr(value)})
def _convertError(self, value):
"""
Raises a C{ValueError} with a message saying that C{value} cannot be
converted into this data type. This method is used in several classes
derived from C{DataType}.
"""
raise ValueError(_('cannot convert %(value)s into %(type)s') %
{'type': str(self), 'value': repr(value)})
def convert(self, value, checkValue=None):
"""
Convert a given value in a valid value for this data type. If not
overloaded, it raises a C{TypeError} by calling L{_convertError}.
C{checkValue} if the function used to check valid values, if it
is C{None}, C{self.checkValue} is used.
"""
self._convertError(value)
def convertAttribute(self, hasSignatureObject, name, value=Undefined):
"""
@param hasSignatureObject: object where an attribute must be converted.
@type hasSignatureObject: L{HasSignature} instance
@param name: name of the attribute to convert.
@type name: C{string}
@param value: value to convert (optional).
@return: the converted value
"""
checkValue = getattr(hasSignatureObject, '_check_' + name + '_value',
self.checkValue)
if value is Undefined:
value = getattr(hasSignatureObject, name)
try:
newValue = checkValue(value)
except Exception:
newValue = self.convert(value, checkValue=checkValue)
return newValue
def copy(self):
"""
Return a copy of C{self}. If not overloaded, it creates a new instance by
passing the result of
C{self.L{__getinitkwargs__<DataType.__getinitkwargs__>}()} to the constructor.
"""
args, kwargs = self.__getinitkwargs__()
return self.__class__(*args, **kwargs)
def __getinitkwargs__(self):
"""
Returns all the parameters that have been passed to the constructor to
create this instance. This method must be overloaded by derived classes
that overload C{__init__}. See L{Number} class for an example.
@return: a pair C{( args, kwargs )} where C{args} is a tuple containing the
unnamed parameters and C{kwargs} is a dictionary containing named
parameters.
"""
return (), {}
def __str__(self):
return self.name
def __repr__(self):
args, kwargs = self.__getinitkwargs__()
if args:
strArgs = ', '.join([repr(i) for i in args])
else:
strArgs = ''
if kwargs:
if strArgs:
strArgs += ', '
strArgs += ', '.join([n + '=' + repr(v)
for n, v in six.iteritems(kwargs)])
if strArgs:
return self.name + '( ' + strArgs + ' )'
else:
return self.name + '()'
#-------------------------------------------------------------------------
class ClassDataType(DataType):
"""
This class is used internally to allow the use of classes that do not derive
from L{DataType} in L{Signature} constructor. When such a class (let's call
it C{C}) is used for the type of an attribute, it is replaced by
C{ClassDataType( C )}. This data type only check that only C{C} instances are
used as attribute value.
"""
def __init__(self, cls):
DataType.__init__(self)
#: Store the class used as attribute type in a L{Signature}.
self.cls = cls
def __getinitkwargs__(self):
args, kwargs = DataType.__getinitkwargs__(self)
return (self.cls,), kwargs
def checkValue(self, value):
"""
Check that C{value} is an instance of L{self.cls}.
"""
if not isinstance(value, self.cls):
raise TypeError(_('%(instance)s is not an instance of %(class)s') %
{'instance': str(value),
'class': str(self.cls)
})
return value
def createValue(self):
return self.cls()
#-------------------------------------------------------------------------
class Signature(DataType):
"""
A C{Signature} instance contains type definition for a series of attributes,
it is mainly used to define attributes for L{HasSignature} derived class. It
behave like a L{SortedDictionary} where keys are attribute names and values
are attribute types (I{i.e.} L{DataType} instances). Since the order of the
attribute is user defined, it is not possible to use any syntax or data
structure that use a Python dictionary (because dictionaries keys are sorted
according to their values). Therefore, a signature instance is created by
using a list containing attribute names followed by attribute types.
Example::
from soma.signature.api import HasSignature, Signature
from soma.signature.api import Unicode, Number, Integer
class ZooAnimal( HasSignature ):
signature = Signature(
'identifier', Integer( minimum=0 ),
'race', Unicode,
'name', Unicode,
'gender', Choice( 'male', 'female' ),
)
The first element of a signature is always an attribute named C{signature}
and whose type is the C{Signature} instance (this attribute B{must not} be
defined in the constructor). Hence, a L{HasSignature} instance has always a
C{signature} attribute that can be managed like any other user-defined
attributes (especially for modification notification).
@see: L{HasSignature}, L{soma.signature.api}, L{SortedDictionary}
"""
class Item(ObservableAttributes):
"""
@todo: documentation
"""
def setType(self, dataType):
newValue = DataType.dataTypeInstance(dataType)
self.__dict__['type'] = newValue
def getType(self):
try:
return self.__dict__['type']
except KeyError:
raise AttributeError('type')
type = property(getType, setType, None)
def __init__(self, name, type,
defaultValue=Undefined,
doc=None,
readOnly=False,
visible=True,
collapsed=False,
**kwargs):
"""
@todo: documentation
"""
ObservableAttributes.__init__(self)
#: Name of the attribute
self.name = str(name)
#: Type the attribute
self.type = type
#: Default attribute value
self.defaultValue = defaultValue
#: Documentation of the attribute
if doc:
self.doc = six.text_type(doc)
else:
self.doc = None
#: If set to C{True}, the attribute value cannot be modified
self.readOnly = bool(readOnly)
#: If set to C{False}, the attribute is not visible on graphical interface
self.visible = bool(visible)
#: If set to C{True}, the attribute is reduced on graphical interface (when possible)
self.collapsed = bool(collapsed)
# store other kwargs as attributes
if kwargs:
self._options = list(kwargs.keys())
for k, v in six.iteritems(kwargs):
setattr(self, k, v)
def copy(self):
args, kwargs = self.__getinitkwargs__()
return self.__class__(*args, **kwargs)
def __getinitkwargs__(self):
d = {}
if self.defaultValue is not Undefined:
d['defaultValue'] = self.defaultValue
if self.doc:
d['doc'] = self.doc
if self.readOnly:
d['readOnly'] = self.readOnly
if not self.visible:
d['visible'] = self.visible
if self.collapsed:
d['collapsed'] = self.collapsed
return (self.name, self.type), d
if hasattr(self, '_options'):
d['_options'] = self._options
for k in self._options:
d[k] = getattr(self, k)
_msgCannotModify = 'Cannot modify read-only signature'
def __init__(self, *args):
super(Signature, self).__init__()
# Notifier is created on read-only Signature because listeners registration
# can be done on Signature and copied when Signature is changed to
# VariableSignature
self.on_change = Notifier(1)
self.__dict__[ '_signature_data' ] = \
SortedDictionary(('signature', self.Item('signature', self)))
i = 0
while i < len(args):
name = args[i]
i += 1
if isinstance(name, Signature):
for n, item in six.iteritems(name):
if n == 'signature':
continue
self._insertElement(len(self), n, item.copy())
else:
dataType = args[i]
if i + 1 < len(args) and isinstance(args[i + 1], dict):
i += 1
options = args[i]
else:
options = {}
self._insertElement(len(self), name, dataType, options)
i += 1
def _copyItem(self, item):
args, kwargs = item.__getinitkwargs__()
return self.Item(*args, **kwargs)
def _insertElement(self, index, name, dataType, options={}):
if name in self:
raise KeyError(_('Element "%s" is defined twice' % (name, )))
if isinstance(dataType, Signature.Item):
item = self._copyItem(dataType)
else:
dataType = DataType.dataTypeInstance(dataType)
item = self.Item(name, dataType, **options)
self._signature_data.insert(index, name, item)
def has_key(self, key):
"""
see L{SortedDictionary.has_key}
"""
return key in self.__dict__['_signature_data']
def __contains__(self, key):
return key in self.__dict__['_signature_data']
def __getitem__(self, key):
"""
see L{SortedDictionary.__getitem__}
"""
return self.__dict__['_signature_data'][key]
def __setitem__(self, key, value):
"""
see L{SortedDictionary.__setitem__}
"""
raise RuntimeError(_(self._msgCannotModify))
def __delitem__(self, key):
"""
see L{SortedDictionary.__delitem__}
"""
raise RuntimeError(_(self._msgCannotModify))
def __len__(self):
"""
see L{SortedDictionary.__len__}
"""
return len(self.__dict__['_signature_data'])
def get(self, key, default=None):
"""
see L{SortedDictionary.get}
"""
return self.__dict__['_signature_data'].get(key, default)
def keys(self):
"""
see L{SortedDictionary.keys}
"""
return list(self.__dict__['_signature_data'].keys())
def values(self):
"""
see L{SortedDictionary.values}
"""
return list(self.__dict__['_signature_data'].values())
def items(self):
"""
see L{SortedDictionary.items}
"""
return list(self.__dict__['_signature_data'].items())
def __iter__(self):
"""
see L{SortedDictionary.__iter__}
"""
return iter(self.__dict__['_signature_data'])
def iterkeys(self):
"""
see L{SortedDictionary.iterkeys}
"""
return six.iterkeys(self.__dict__['_signature_data'])
def itervalues(self):
"""
see L{SortedDictionary.itervalues}
"""
return six.itervalues(self.__dict__['_signature_data'])
def iteritems(self):
"""
see L{SortedDictionary.iteritems}
"""
return six.iteritems(self.__dict__['_signature_data'])
def insert(self, index, key, value, **kwargs):
"""
see L{SortedDictionary.insert}
"""
raise RuntimeError(_(self._msgCannotModify))
def _setAttribute(self, hasSignatureObject, name, value):
"""
This method is called when the C{signature} attribute of an L{HasSignature}
instance is modified, it calls
C{hasSignatureObject.L{_changeSignature<HasSignature._changeSignature>}} to
change the attribute value. The new signature becomes an instance signature
(overriding the class signature). Once an instance signature is created,
the class signature can be restored by deleting the C{signature} attribute.
@see: L{_deleteAttribute}
"""
checkValue = getattr(hasSignatureObject, '_check_' + name + '_value',
self.checkValue)
newValue = checkValue(value)
hasSignatureObject._changeSignature(newValue)
return newValue
def _deleteAttribute(self, hasSignatureObject, name):
"""
This method is called when the C{signature} attribute of an L{HasSignature}
instance is deleted. It tries to restore the class signature if an instance
signature has been defined. If no instance signature has been defined, it
raises a C{RuntimeError}.
@see: L{_setAttribute}
"""
if not hasSignatureObject._changeSignature(None):
raise RuntimeError(_('Signature cannot be deleted'))
def checkValue(self, value):
"""
Raises a C{TypeError} if C{value} is not a C{Signature} instance.
"""
if not isinstance(value, Signature):
raise TypeError(_('invalid value type for signature attribute: %s') %
(type(value), ))
return value
def __getinitkwargs__(self):
args, kwargs = DataType.__getinitkwargs__(self)
args = []
it = six.iteritems(self)
next(it) # skip signature
for name, type in it:
args.append(name)
args.append(type)
return args, kwargs
def copy(self):
args, kwargs = DataType.__getinitkwargs__(self)
args = []
it = six.iteritems(self)
next(it) # skip signature
for name, item in it:
args.append(name)
args.append(item.copy())
return self.__class__(*args, **kwargs)
def __repr__(self):
result = self.name + '( '
it = six.itervalues(self)
next(it)
for item in it:
result += repr(item.name) + ', ' + repr(item.type) + ', '
args, kwargs = item.__getinitkwargs__()
if kwargs:
result += 'dict( ' + ', '.join(
[str(i) + '=' + repr(j) for i, j in six.iteritems(kwargs)]) + '), '
return result + ' )'
#-------------------------------------------------------------------------
class VariableSignature(Signature):
"""
This class is a non constant version of C{Signature}. You can add or remove
elements at run-time on a C{VariableSignature} instance. These changes are
taken into account by the notification system.
"""
def __setitem__(self, key, value):
if key == 'signature':
raise KeyError(
_('Attribute named "signature" cannot be redefined.'))
self._insertElement(len(self), key, value)
self.on_change.notify(key)
def __delitem__(self, key):
if key == 'signature':
raise KeyError(
_('Attribute named "signature" cannot be deleted.'))
del self.__dict__['_signature_data'][key]
self.on_change.notify(key)
def insert(self, index, key, value, **kwargs):
self._insertElement(index, key, value, kwargs)
self.on_change.notify(key)
def append(self, key, value, **kwargs):
self._insertElement(len(self), key, value, kwargs)
self.on_change.notify(key)
#-------------------------------------------------------------------------
class MetaHasSignature(type):
def __init__(cls, name, bases, dict):
signature = dict.get('signature')
if signature is not None:
for c in bases:
referenceSignature = getattr(c, 'signature', None)
if referenceSignature is not None:
break
if referenceSignature is not None and not isinstance(signature, referenceSignature.__class__):
raise TypeError(_('signature must be an instance of %s') %
(six.text_type(referenceSignature.__class__), ))
super(MetaHasSignature, cls).__init__(name, bases, dict)
#-------------------------------------------------------------------------
class HasSignature(six.with_metaclass(MetaHasSignature, ObservableAttributes)):
"""
HasSignature is the base class for all object using the signature system.
"""
#: The default empty signature
signature = Signature()
def __init__(self, *args, **kwargs):
super(HasSignature, self).__init__(*args, **kwargs)
for attribute in self.signature:
self._onAttributeChange[
attribute] = self._createAttributeNotifier()
def initializeSignatureAttributes(self, **attributesValues):
"""
Initialize signature attributes according to the values given in
parameters and the default values given in the signature. Values
given in parameters takes precedence over signature default values.
If a parameter name is not in the signature, a TypeError is raised.
Example
=======
L{initializeSignatureAttributes} is suitable for using in constructors::
class Time( HasSignature ):
signature = Signature(
'hour', Integer( minimum=0, maximum=23 ), dict( defaultValue=0 ),
'minute', Integer( minimum=0, maximum=59 ), dict( defaultValue=0 ),
'second', Integer( minimum=0, maximum=59 à, dict( defaultValue=0 ),
)
def __init__( self, **kwargs ):
HasSignature.__init__( self )
self.initializeSignatureAttributes( **kwargs )
"""
# Copy the dictionary to allow modification
it = six.iteritems(self.signature)
next(it)
for attributeName, signatureItem in it:
value = attributesValues.pop(attributeName, Undefined)
if value is Undefined:
value = copy.copy(signatureItem.defaultValue)
if value is not Undefined:
setattr(self, attributeName, value)
if attributesValues:
raise TypeError(_("Attribute '%s' is not in the signature") %
(next(six.iterkeys(attributesValues)), ))
def __getattribute__(self, name):
"""
For all attributes that are declared in the signature, calls the
L{_getAttribute<DataType._getAttribute>} method of the attribute type.
Directly return the value of all other attributes.
"""
signatureItem = \
super(HasSignature, self).__getattribute__('signature').get(name)
if signatureItem is None:
return super(HasSignature, self).__getattribute__(name)
else:
try:
return signatureItem.type._getAttribute(self, name)
except AttributeError:
defaultValue = signatureItem.defaultValue
if defaultValue is Undefined:
raise
defaultValue = copy.copy(defaultValue)
super(ObservableAttributes, self).__setattr__(
name, defaultValue)
return defaultValue
def __setattr__(self, name, value):
"""
For all attributes that are declared in the signature, calls the
L{_setAttribute<DataType._setAttribute>} method of the attribute type
and then calls L{self.notifyAttributeChange}. Directly change the value of
all other attributes.
"""
signatureItem = self.signature.get(name)
if signatureItem is None:
super(HasSignature, self).__setattr__(name, value)
else:
signatureItem.type._setAttribute(self, name, value)
def __delattr__(self, name):
"""
For all attributes that are declared in the signature, calls the
L{_deleteAttribute<DataType._deleteAttribute>} method of the attribute type
and then calls L{self.notifyAttributeChange}. Directly change the value of
all other attributes.
"""
signatureItem = self.signature.get(name)
if signatureItem is None:
super(HasSignature, self).__delattr__(name)
else:
signatureItem.type._deleteAttribute(self, name)
def _changeSignature(self, newSignature):
"""
Change or delete the instance signature. C{newSignature} becomes the
instance signature. If C{newSignature} is C{None}, the instance signature
is deleted and the class signature is restored. Notification for attributes
that are in both old and new signature is preserved.
"""
signatureChanged = True
oldSignature = self.signature
if newSignature is None:
try:
super(HasSignature, self).__delattr__('signature')
newSignature = self.signature
except AttributeError:
signatureChanged = False
else:
super(HasSignature, self).__setattr__('signature', newSignature)
if signatureChanged:
oldSignature.on_change.remove(self._signatureChanged)
newSignature.on_change.add(self._signatureChanged)
for attribute in newSignature:
notifier = self._onAttributeChange.get(attribute)
if notifier is None:
self._onAttributeChange[
attribute] = self._createAttributeNotifier()
for attribute in oldSignature:
if not attribute in newSignature:
del self._onAttributeChange[attribute]
return signatureChanged
def _signatureChanged(self, attributeName):
"""
This method is called whenever the signature content is modified.
"""
if attributeName in self._onAttributeChange:
if attributeName not in self.signature:
del self._onAttributeChange[attributeName]
elif attributeName in self.signature:
self._onAttributeChange[
attributeName] = self._createAttributeNotifier()
self._onAttributeChange['signature'].notify(self, 'signature',
self.signature,
self.signature)
def delayAttributeNotification(self, ignoreDoubles=False):
super(HasSignature, self).delayAttributeNotification(ignoreDoubles)
for item in six.itervalues(self.signature):
item.delayAttributeNotification(ignoreDoubles)
def restartAttributeNotification(self):
super(HasSignature, self).restartAttributeNotification()
for item in six.itervalues(self.signature):
item.restartAttributeNotification()