#!/usr/bin/env python
#
# properties_value.py - Definitions of the PropertyValue and
# PropertyValueList classes.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""Definitions of the :class:`PropertyValue` and :class:`PropertyValueList`
classes.
.. autosummary::
:nosignatures:
PropertyValue
PropertyValueList
``PropertyValue`` and ``PropertyValueList`` instances are intended to be
created and managed by :class:`.PropertyBase` and :class:`.ListPropertyBase`
instances respectively, and are used to encapsulate attribute values of
:class:`.HasProperties` instances.
These class definitions are really a part of the :mod:`.properties` module -
they are separated to keep file sizes down. However, the
:class:`.PropertyValue` class definitions have no dependence upon the
:class:`.PropertyBase` or :class:`.HasProperties` definitions.
"""
import uuid
import inspect
import logging
import weakref
from collections import OrderedDict
from . import callqueue
from . import bindable
import fsl.utils.weakfuncref as weakfuncref
log = logging.getLogger(__name__)
[docs]
class Listener:
"""The ``Listener`` class is used by :class:`PropertyValue` instances to
manage their listeners - see :meth:`PropertyValue.addListener`.
"""
def __init__(self, propVal, name, function, enabled, immediate):
"""Create a ``Listener``.
:arg propVal: The ``PropertyValue`` that owns this ``Listener``.
:arg name: The listener name.
:arg function: The callback function.
:arg enabled: Whether the listener is enabled/disabled.
:arg immediate: Whether the listener is to be called immediately, or
via the :attr:`PropertyValue.queue`.
"""
self.propVal = weakref.ref(propVal)
self.name = name
self.function = function
self.enabled = enabled
self.immediate = immediate
[docs]
def makeQueueName(self):
"""Returns a more descriptive name for this ``Listener``, which
is used as its name when passed to the :class:`.CallQueue`.
"""
ctxName = self.propVal()._context().__class__.__name__
pvName = self.propVal()._name
return '{} ({}.{})'.format(self.name, ctxName, pvName)
@property
def expectsArguments(self):
"""Returns ``True`` if the listener function needs to be passed
arguments, ``False`` otherwise. Property listener functions can
be defined to accept either zero arguments, or a set of
positional arguments - see :meth:`PropertyValue.addListener` and
:meth:`PropertyValue.addAttributeListener` for more details.
"""
func = self.function
if isinstance(func, weakfuncref.WeakFunctionRef):
func = func()
spec = inspect.signature(func)
posargs = 0
varargs = False
for param in spec.parameters.values():
if param.kind in (inspect.Parameter.POSITIONAL_ONLY,
inspect.Parameter.POSITIONAL_OR_KEYWORD):
posargs += 1
elif param.kind == inspect.Parameter.VAR_POSITIONAL:
varargs = True
return varargs or ((not varargs) and (posargs == 4))
[docs]
class PropertyValue:
"""An object which encapsulates a value of some sort.
The value may be subjected to casting and validation rules, and listeners
may be registered for notification of value and validity changes.
Notification of value and attribute listeners is performed by the
:mod:`.bindable` module - see the :func:`.bindable.syncAndNotify` and
:func:`.bindable.syncAndNotifyAtts` functions.
"""
queue = callqueue.CallQueue(skipDuplicates=True)
"""A :class:`.CallQueue` instance which is shared by all
:class:`PropertyValue` instances, and used for notifying listeners
of value and attribute changes.
A queue is used for notification so that listeners are notified in
the order that values were changed.
"""
def __init__(self,
context,
name=None,
value=None,
castFunc=None,
validateFunc=None,
equalityFunc=None,
preNotifyFunc=None,
postNotifyFunc=None,
allowInvalid=True,
parent=None,
**attributes):
"""Create a ``PropertyValue`` object.
:param context: An object which is passed as the first argument
to the ``validateFunc``, ``preNotifyFunc``,
``postNotifyFunc``, and any registered
listeners. Can technically be anything, but will
nearly always be a :class:`.HasProperties`
instance.
:param name: Value name - if not provided, a default, unique
name is created.
:param value: Initial value.
:param castFunc: Function which performs type casting or data
conversion. Must accept three parameters - the
context, a dictionary containing the attributes
of this object, and the value to cast. Must
return that value, cast/converted appropriately.
:param validateFunc: Function which accepts three parameters - the
context, a dictionary containing the attributes
of this object, and a value. This function
should test the provided value, and raise a
:exc:`ValueError` if it is invalid.
:param equalityFunc: Function which accepts two values, and should
return ``True`` if they are equal, ``False``
otherwise. If not provided, the python equailty
operator (i.e. ``==``) is used.
:param preNotifyFunc: Function to be called whenever the property
value changes, but before any registered
listeners are called. See the
:meth:`addListener` method for details of the
parameters this function must accept.
:param postNotifyFunc: Function to be called whenever the property
value changes, but after any registered
listeners are called. Must accept the same
parameters as the ``preNotifyFunc``.
:param allowInvalid: If ``False``, any attempt to set the value to
something invalid will result in a
:exc:`ValueError`. Note that this does not
guarantee that the property will never have an
invalid value, as the definition of 'valid'
depends on external factors (i.e. the
``validateFunc``). Therefore, the validity of
a value may change, even if the value itself
has not changed.
:param parent: If this PV instance is a member of a
:class:`PropertyValueList` instance, the latter
sets itself as the parent of this PV. Whenever
the value of this PV changes, the
:meth:`PropertyValueList._listPVChanged` method
is called.
:param attributes: Any key-value pairs which are to be associated
with this :class:`PropertyValue` object, and
passed to the ``castFunc`` and ``validateFunc``
functions. Attributes are not used by the
``PropertyValue`` or ``PropertyValueList``
classes, however they are used by the
:class:`.PropertyBase` and
:class:`.ListPropertyBase` classes to store
per-instance property attributes. Listeners
may register to be notified when attribute
values change.
"""
if name is None: name = 'PropertyValue_{}'.format(id(self))
if castFunc is not None: value = castFunc(context, attributes, value)
if equalityFunc is None: equalityFunc = lambda a, b: a == b
self._context = weakref.ref(context)
self._validate = validateFunc
self._name = name
self._equalityFunc = equalityFunc
self._castFunc = castFunc
self._allowInvalid = allowInvalid
self._attributes = attributes.copy()
self._changeListeners = OrderedDict()
self._attributeListeners = OrderedDict()
self.__value = value
self.__valid = False
self.__lastValue = None
self.__lastValid = False
self.__notification = True
self._preNotifyListener = Listener(self,
'prenotify',
preNotifyFunc,
True,
True)
self._postNotifyListener = Listener(self,
'postnotify',
postNotifyFunc,
True,
False)
if parent is not None: self.__parent = weakref.ref(parent)
else: self.__parent = None
if not allowInvalid and validateFunc is not None:
validateFunc(context, self._attributes, value)
def __repr__(self):
"""Returns a string representation of this PropertyValue object."""
return 'PV({})'.format(self.__value)
def __str__(self):
"""Returns a string representation of this PropertyValue object."""
return self.__repr__()
def __hash__(self):
return id(self)
def __eq__(self, other):
"""Returns ``True`` if the given object has the same value as this
instance. Returns ``False`` otherwise.
"""
if isinstance(other, PropertyValue):
other = other.get()
return self._equalityFunc(self.get(), other)
def __ne__(self, other):
"""Returns ``True`` if the given object has a different value to
this instance, ``False`` otherwise.
"""
return not self.__eq__(other)
def __saltListenerName(self, name):
"""Adds a constant string to the given listener name.
This is done for debug output, so we can better differentiate between
listeners with the same name registered on different PV objects.
"""
return 'PropertyValue_{}_{}'.format(self._name, name)
def __unsaltListenerName(self, name):
"""Removes a constant string from the given listener name,
which is assumed to have been generated by the
:meth:`__saltListenerName` method.
"""
salt = 'PropertyValue_{}_'.format(self._name)
return name[len(salt):]
[docs]
def getParent(self):
"""If this ``PropertyValue`` is an item in a :class:`PropertyValueList`,
this method returns a reference to the owning ``PropertyValueList``.
Otherwise, this method returns ``None``.
"""
if self.__parent is not None: return self.__parent()
else: return None
[docs]
def allowInvalid(self, allow=None):
"""Query/set the allow invalid state of this value.
If no arguments are passed, returns the current allow invalid state.
Otherwise, sets the current allow invalid state. to the given argument.
"""
if allow is None:
return self._allowInvalid
self._allowInvalid = bool(allow)
[docs]
def enableNotification(self, bound=False, att=False):
"""Enables notification of property value and attribute listeners for
this ``PropertyValue`` object.
:arg bound: If ``True``, notification is enabled on all other
``PropertyValue`` instances that are bound to this one
(see the :mod:`.bindable` module). If ``False`` (the
default), notification is only enabled on this
``PropertyValue``.
:arg att: If ``True``, notification is enabled on all attribute
listeners as well as property value listeners.
"""
self.__notification = True
if not bound:
return
bpvs = list(bindable.buildBPVList(self, 'boundPropVals')[0])
if att:
bpvs += list(bindable.buildBPVList(self, 'boundAttPropVals')[0])
for bpv in bpvs:
bpv.enableNotification()
[docs]
def disableNotification(self, bound=False, att=False):
"""Disables notification of property value and attribute listeners for
this ``PropertyValue`` object. Notification can be re-enabled via
the :meth:`enableNotification` or :meth:`setNotificationState` methods.
:arg bound: If ``True``, notification is disabled on all other
``PropertyValue`` instances that are bound to this one
(see the :mod:`.bindable` module). If ``False`` (the
default), notification is only disabled on this
``PropertyValue``.
:arg att: If ``True``, notification is disabled on all attribute
listeners as well as property value listeners.
"""
self.__notification = False
if not bound:
return
bpvs = list(bindable.buildBPVList(self, 'boundPropVals')[0])
if att:
bpvs += list(bindable.buildBPVList(self, 'boundAttPropVals')[0])
for bpv in bpvs:
bpv.disableNotification()
[docs]
def getNotificationState(self):
"""Returns ``True`` if notification is currently enabled, ``False``
otherwise.
"""
return self.__notification
[docs]
def setNotificationState(self, value):
"""Sets the current notification state."""
if value: self.enableNotification()
else: self.disableNotification()
[docs]
def addAttributeListener(self, name, listener, weak=True, immediate=False):
"""Adds an attribute listener for this ``PropertyValue``. The
listener callback function must accept either no arguments, or the
following arguments:
- ``context``: The context associated with this ``PropertyValue``.
- ``attribute``: The name of the attribute that changed.
- ``value``: The new attribute value.
- ``name``: The name of this ``PropertyValue`` instance.
:param name: A unique name for the listener. If a listener with
the specified name already exists, it will be
overwritten.
:param listener: The callback function.
:param weak: If ``True`` (the default), a weak reference to the
callback function is used.
:param immediate: If ``False`` (the default), the listener is called
immediately; otherwise, it is called via the
:attr:`queue`.
"""
log.debug('Adding attribute listener on %s.%s (%s): %s',
self._context().__class__.__name__,
self._name, id(self), name)
if weak:
listener = weakfuncref.WeakFunctionRef(listener)
name = self.__saltListenerName(name)
self._attributeListeners[name] = Listener(self,
name,
listener,
True,
immediate)
[docs]
def disableAttributeListener(self, name):
"""Disables the attribute listener with the specified ``name``. """
name = self.__saltListenerName(name)
log.debug('Disabling attribute listener on %s: %s', self._name, name)
self._attributeListeners[name].enabled = False
[docs]
def enableAttributeListener(self, name):
"""Enables the attribute listener with the specified ``name``. """
name = self.__saltListenerName(name)
log.debug('Enabling attribute listener on %s: %s', self._name, name)
self._attributeListeners[name].enabled = True
[docs]
def removeAttributeListener(self, name):
"""Removes the attribute listener of the given name."""
log.debug('Removing attribute listener on %s.%s: %s',
self._context().__class__.__name__, self._name, name)
name = self.__saltListenerName(name)
listener = self._attributeListeners.pop(name, None)
if listener is not None:
cb = listener.function
if isinstance(cb, weakfuncref.WeakFunctionRef):
cb = cb.function()
if cb is not None:
PropertyValue.queue.dequeue(listener.makeQueueName())
[docs]
def getAttributes(self):
"""Returns a dictionary containing all the attributes of this
``PropertyValue`` object.
"""
return self._attributes.copy()
[docs]
def setAttributes(self, atts):
"""Sets all the attributes of this ``PropertyValue`` object.
from the given dictionary.
"""
for name, value in atts.items():
self.setAttribute(name, value)
[docs]
def getAttribute(self, name, *arg):
"""Returns the value of the named attribute.
:arg default: If provided, the default value to use if ``name`` is not
an attribute. If not provided, and ``name`` is not an
attribute, a ``KeyError`` is raised.
"""
nodefault = len(arg) == 0
if nodefault: return self._attributes[name]
else: return self._attributes.get(name, arg[0])
[docs]
def setAttribute(self, name, value):
"""Sets the named attribute to the given value, and notifies any
registered attribute listeners of the change.
"""
oldVal = self._attributes.get(name, None)
self._attributes[name] = value
# Use the type-specific equalty function
# for the "default" attribute (see
# PropertyBase.__init__).
if name == 'default':
if self._equalityFunc(oldVal, value):
return
else:
if oldVal == value:
return
log.debug('Attribute on %s.%s (%s) changed: %s = %s',
self._context().__class__.__name__,
self._name,
id(self),
name,
value)
self.notifyAttributeListeners(name, value)
self.revalidate()
[docs]
def prepareListeners(self, att, name=None, value=None):
"""Prepares a list of :class:`Listener` instances ready to be called,
and a list of arguments to pass to them.
:arg att: If ``True``, attribute listeners are returned, otherwise
value listeners are returned.
:arg name: If ``att == True``, the attribute name.
:arg value: If ``att == True``, the attribute value.
"""
if not self.__notification:
return [], []
if att: lDict = self._attributeListeners
else: lDict = self._changeListeners
allListeners = []
allArgs = []
for lName, listener in list(lDict.items()):
if not listener.enabled:
continue
cb = listener.function
if isinstance(cb, weakfuncref.WeakFunctionRef):
cb = cb.function()
# The owner of the referred function/method
# has been GC'd - remove it
if cb is None:
log.debug('Removing dead listener %s', lName)
if att:
self.removeAttributeListener(
self.__unsaltListenerName(lName))
else:
self.removeListener(
self.__unsaltListenerName(lName))
continue
allListeners.append(listener)
# if we're preparing value listenres, add
# the pre-notify and post-notify functions
if not att:
if self._preNotifyListener.function is not None and \
self._preNotifyListener.enabled:
allListeners = [self._preNotifyListener] + allListeners
if self._postNotifyListener.function is not None and \
self._postNotifyListener.enabled:
allListeners = allListeners + [self._postNotifyListener]
# prepare arguments for each listener function
for listener in allListeners:
# listener functions can be defined to accept
# no arguments, or to accept (value, valid,
# context, name) - see the addListener and
# addAttributeListener methods for details
noargs = not listener.expectsArguments
ctx = self._context()
if noargs: args = ()
elif att: args = ctx, name, value, self._name
else: args = (self.get(), self.__valid, ctx, self._name)
allArgs.append(args)
return allListeners, allArgs
[docs]
def notifyAttributeListeners(self, name, value):
"""Notifies all registered attribute listeners of an attribute
changed - see the :func:`.bindable.syncAndNotifyAtts` function.
"""
bindable.syncAndNotifyAtts(self, name, value)
[docs]
def addListener(self,
name,
callback,
overwrite=False,
weak=True,
immediate=False):
"""Adds a listener for this value.
When the value changes, the listener callback function is called. The
callback function may either be defined to accept no arguments, or
to accept the following arguments:
- ``value``: The property value
- ``valid``: Whether the value is valid or invalid
- ``context``: The context object passed to :meth:`__init__`.
- ``name``: The name of this ``PropertyValue`` instance.
Listener names ``prenotify`` and ``postnotify`` are reserved - if
either of these are passed in for the listener name, a :exc`ValueError`
is raised.
:param str name: A unique name for this listener. If a listener with
the name already exists, a :exc`RuntimeError` will be
raised, or it will be overwritten, depending upon
the value of the ``overwrite`` argument.
:param callback: The callback function.
:param overwrite: If ``True`` any previous listener with the same name
will be overwritten.
:param weak: If ``True`` (the default), a weak reference to the
callback function is retained, meaning that it
can be garbage-collected. If passing in a lambda or
inner function, you will probably want to set
``weak`` to ``False``, in which case a strong
reference will be used.
:param immediate: If ``False`` (the default), this listener will be
notified through the :class:`.CallQueue` - listeners
for all ``PropertyValue`` instances are queued, and
notified in turn. If ``True``, If ``True``, the
``CallQueue`` will not be used, and this listener
will be notified as soon as this ``PropertyValue``
changes.
"""
if name in ('prenotify', 'postnotify'):
raise ValueError('Reserved listener name used: {}. '
'Use a different name.'.format(name))
log.debug('Adding listener on %s.%s %s',
self._context().__class__.__name__,
self._name, name)
fullName = self.__saltListenerName(name)
prior = self._changeListeners.get(fullName, None)
if weak:
callback = weakfuncref.WeakFunctionRef(callback)
if (prior is not None) and (not overwrite):
raise RuntimeError('Listener {} already exists'.format(name))
elif prior is not None:
prior.function = callback
prior.immediate = immediate
else:
self._changeListeners[fullName] = Listener(self,
fullName,
callback,
True,
immediate)
[docs]
def removeListener(self, name):
"""Removes the listener with the given name from this
``PropertyValue``.
"""
# The typical stack trace of a call to this method is:
# someHasPropertiesObject.removeListener(...) (the original call)
# HasProperties.removeListener(...)
# PropertyBase.removeListener(...)
# this method
# So to be a bit more informative, we'll examine the stack
# and extract the (assumed) location of the original call
if log.getEffectiveLevel() == logging.DEBUG:
stack = inspect.stack()
if len(stack) >= 4: frame = stack[ 3]
else: frame = stack[-1]
srcMod = '...{}'.format(frame[1][-20:])
srcLine = frame[2]
log.debug('Removing listener on %s.%s: %s (%s:%s)',
self._context().__class__.__name__,
self._name, name, srcMod, srcLine)
name = self.__saltListenerName(name)
listener = self._changeListeners.pop(name, None)
if listener is not None:
# The bindable._allAllListeners does
# funky things to the call queue,
# so we mark this listener as disabled
# just in case bindable tries to call
# a removed listener.
listener.enabled = False
cb = listener.function
if isinstance(cb, weakfuncref.WeakFunctionRef):
cb = cb.function()
if cb is not None:
PropertyValue.queue.dequeue(listener.makeQueueName())
[docs]
def enableListener(self, name):
"""(Re-)Enables the listener with the specified ``name``."""
name = self.__saltListenerName(name)
log.debug('Enabling listener on %s: %s', self._name, name)
self._changeListeners[name].enabled = True
[docs]
def disableListener(self, name):
"""Disables the listener with the specified ``name``, but does not
remove it from the list of listeners.
"""
name = self.__saltListenerName(name)
log.debug('Disabling listener on %s: %s', self._name, name)
self._changeListeners[name].enabled = False
[docs]
def getListenerState(self, name):
"""Returns ``True`` if the specified listener is currently enabled,
``False`` otherwise.
An :exc:`AttributeError` is raised if a listener with the specified
``name`` does not exist.
"""
fullName = self.__saltListenerName(name)
listener = self._changeListeners.get(fullName, None)
return listener.enabled
[docs]
def setListenerState(self, name, state):
"""Enables/disables the specified listener. """
if state: self.enableListener(name)
else: self.disableListener(name)
[docs]
def hasListener(self, name):
"""Returns ``True`` if a listener with the given name is registered,
``False`` otherwise.
"""
name = self.__saltListenerName(name)
return name in self._changeListeners.keys()
[docs]
def setPreNotifyFunction(self, preNotifyFunc):
"""Sets the function to be called on value changes, before any
registered listeners.
"""
self._preNotifyListener.function = preNotifyFunc
[docs]
def setPostNotifyFunction(self, postNotifyFunc):
"""Sets the function to be called on value changes, after any
registered listeners.
"""
self._postNotifyListener.function = postNotifyFunc
[docs]
def getLast(self):
"""Returns the most recent property value before the current one."""
return self.__lastValue
[docs]
def get(self):
"""Returns the current property value."""
return self.__value
[docs]
def set(self, newValue):
"""Sets the property value.
The property is validated and, if the property value or its validity
has changed, any registered listeners are called through the
:meth:`propNotify` method. If ``allowInvalid`` was set to
``False``, and the new value is not valid, a :exc:`ValueError` is
raised, and listeners are not notified.
"""
# cast the value if necessary.
# Allow any errors to be thrown
if self._castFunc is not None:
newValue = self._castFunc(self._context(),
self._attributes,
newValue)
# Check to see if the new value is valid
valid = False
validStr = None
try:
if self._validate is not None:
self._validate(self._context(), self._attributes, newValue)
valid = True
except ValueError as e:
# Oops, we don't allow invalid values.
validStr = str(e)
if not self._allowInvalid:
import traceback
log.debug('Attempt to set %s.%s to an invalid value (%s), '
'but allowInvalid is False (%s)',
self._context().__class__.__name__,
self._name, newValue, e, exc_info=True)
traceback.print_stack()
raise e
self.__lastValue = self.__value
self.__lastValid = self.__valid
self.__value = newValue
self.__valid = valid
# If the value or its validity has not
# changed, listeners are not notified
changed = (self.__valid != self.__lastValid) or \
not self._equalityFunc(self.__value, self.__lastValue)
if not changed: return
log.debug('Value %s.%s changed: %s -> %s (%s)',
self._context().__class__.__name__,
self._name, self.__lastValue, self.__value,
'valid' if valid else 'invalid - {}'.format(validStr))
# Notify any registered listeners.
self.propNotify()
[docs]
def propNotify(self):
"""Notifies registered listeners - see the
:func:`.bindable.syncAndNotify` function.
"""
bindable.syncAndNotify(self)
# If this PV is a member of a PV list,
# tell the list that this PV has
# changed, so that it can notify its own
# list-level listeners of the change
if self.__parent is not None and self.__parent() is not None:
self.__parent()._listPVChanged(self)
[docs]
def revalidate(self):
"""Revalidates the current property value, and re-notifies any
registered listeners if the value validity has changed.
"""
self.set(self.get())
[docs]
def isValid(self):
"""Returns ``True`` if the current property value is valid, ``False``
otherwise.
"""
try: self._validate(self._context(), self._attributes, self.get())
except: return False
return True
[docs]
class PropertyValueList(PropertyValue):
"""A ``PropertyValueList`` is a :class:`PropertyValue` instance which
stores other :class:`PropertyValue` instance in a list. Instances of
this class are generally managed by a :class:`.ListPropertyBase` instance.
When created, separate validation functions may be passed in for
individual items, and for the list as a whole. Listeners may be registered
on individual ``PropertyValue`` instances (accessible via the
:meth:`getPropertyValueList` method), or on the entire list.
The values contained in this ``PropertyValueList`` may be accessed
through standard Python list operations, including slice-based access and
assignment, :meth:`append`, :meth:`insert`, :meth:`extend`, :meth:`pop`,
:meth:`index`, :meth:`count`, :meth:`move`, :meth:`insertAll`,
:meth:`removeAll`, and :meth:`reorder` (these last few are non-standard).
Because the values contained in this list are ``PropertyValue``
instances themselves, some limitations are present on list modifying
operations::
class MyObj(props.HasProperties):
mylist = props.List(default[1, 2, 3])
myobj = MyObj()
Simple list-slicing modifications work as expected::
# the value after this will be [5, 2, 3]
myobj.mylist[0] = 5
# the value after this will be [5, 6, 7]
myobj.mylist[1:] = [6, 7]
However, modifications which would change the length of the list are not
supported::
# This will result in an IndexError
myobj.mylist[0:2] = [6, 7, 8]
The exception to this rule concerns modifications which would replace
every value in the list::
# These assignments are equivalent
myobj.mylist[:] = [1, 2, 3, 4, 5]
myobj.mylist = [1, 2, 3, 4, 5]
While the simple list modifications described above will change the
value(s) of the existing ``PropertyValue`` instances in the list,
modifications which replace the entire list contents will result in
existing ``PropertyValue`` instances being destroyed, and new ones
being created. This is a very important point to remember if you have
registered listeners on individual ``PropertyValue`` items.
A listener registered on a ``PropertyValueList`` will be notified
whenever the list is modified (e.g. additions, removals, reorderings), and
whenever any individual value in the list changes. Alternately, listeners
may be registered on the individual ``PropertyValue`` instances (which
are accessible through the :meth:`getPropertyValueList` method) to be
nofitied of changes to those values only.
There are some interesting type-specific subclasses of the
``PropertyValueList``, which provide additional functionality:
- The :class:`.PointValueList`, for :class:`.Point` properties.
- The :class:`.BoundsValueList`, for :class:`.Bounds` properties.
"""
def __init__(self,
context,
name=None,
values=None,
itemCastFunc=None,
itemEqualityFunc=None,
itemValidateFunc=None,
listValidateFunc=None,
itemAllowInvalid=True,
preNotifyFunc=None,
postNotifyFunc=None,
listAttributes=None,
itemAttributes=None):
"""Create a ``PropertyValueList``.
:param context: See :meth:`PropertyValue.__init__`.
:param name: See :meth:`PropertyValue.__init__`.
:param values: Initial list values.
:param itemCastFunc: Function which casts a single list item.
:param itemEqualityFunc: Function which tests equality of two values.
:param itemValidateFunc: Function which validates a single list item.
:param listValidateFunc: Function which validates the list as a whole.
:param itemAllowInvalid: Whether items are allowed to containg
invalid values.
:param preNotifyFunc: See :meth:`PropertyValue.__init__`.
:param postNotifyFunc: See :meth:`PropertyValue.__init__`.
:param listAttributes: Attributes to be associated with this
``PropertyValueList``.
:param itemAttributes: Attributes to be associated with new
``PropertyValue`` items added to
the list.
"""
if name is None: name = 'PropertyValueList_{}'.format(id(self))
if listAttributes is None: listAttributes = {}
def itemEquals(a, b):
if isinstance(a, PropertyValue): a = a.get()
if isinstance(b, PropertyValue): b = b.get()
if itemEqualityFunc is not None: return itemEqualityFunc(a, b)
else: return a == b
if listValidateFunc is not None:
def listValid(ctx, atts, value):
value = list(value)
for i, v in enumerate(value):
if isinstance(v, PropertyValue):
value[i] = v.get()
return listValidateFunc(ctx, atts, value)
else:
listValid = None
# The list as a whole must be allowed to contain
# invalid values because, if an individual
# PropertyValue item value changes, there is no
# nice way to propagate those changes on to other
# (dependent) items without the list as a whole
# being validated first, and errors being raised.
PropertyValue.__init__(
self,
context,
name=name,
allowInvalid=True,
validateFunc=listValid,
preNotifyFunc=preNotifyFunc,
postNotifyFunc=postNotifyFunc,
**listAttributes)
# These attributes are passed to the PropertyValue
# constructor whenever a new item is added to the list
self._itemCastFunc = itemCastFunc
self._itemValidateFunc = itemValidateFunc
self._itemEqualityFunc = itemEquals
self._itemAllowInvalid = itemAllowInvalid
self._itemAttributes = itemAttributes
# Internal flag used in the __setitem__
# and _listPVChanged methods indicating
# that notifications from list items
# should be temporarily ignored. This
# is intended for internal use only, and
# may change in the future.
self._ignoreListItems = False
# The list of PropertyValue objects.
if values is not None: values = [self.__newItem(v) for v in values]
else: values = []
PropertyValue.set(self, values)
def __eq__(self, other):
"""Retuns ``True`` if the given object contains the same values as
this instance, ``False`` otherwise.
"""
if other is None:
return False
if len(self) != len(other):
return False
return all([self._itemEqualityFunc(ai, bi)
for ai, bi
in zip(self[:], other[:])])
[docs]
def getPropertyValueList(self):
"""Return (a copy of) the underlying property value list, allowing
access to the ``PropertyValue`` instances which manage each list
item.
"""
return list(PropertyValue.get(self))
[docs]
def get(self):
"""Overrides :meth:`PropertyValue.get`. Returns this
``PropertyValueList`` object.
"""
return self
[docs]
def set(self, newValues):
"""Overrides :meth:`PropertyValue.set`.
Sets the values stored in this ``PropertyValueList``. If the
length of the ``newValues`` argument does not match the current list
length, an :exc:`IndexError` is raised.
"""
if self._itemCastFunc is not None:
newValues = [self._itemCastFunc(
self._context(),
self._itemAttributes,
v) for v in newValues]
self[:] = newValues
def __newItem(self, item):
"""Called whenever a new item is added to the list. Encapsulate the
given item in a ``PropertyValue`` instance.
"""
if self._itemAttributes is None: itemAtts = {}
else: itemAtts = self._itemAttributes
propVal = PropertyValue(
self._context(),
name='{}_Item'.format(self._name),
value=item,
castFunc=self._itemCastFunc,
allowInvalid=self._itemAllowInvalid,
equalityFunc=self._itemEqualityFunc,
validateFunc=self._itemValidateFunc,
parent=self,
**itemAtts)
return propVal
[docs]
def getLast(self):
"""Overrides :meth:`PropertyValue.getLast`. Returns the most
recent list values.
"""
lastVal = PropertyValue.getLast(self)
if lastVal is None: return None
else: return [pv.get() for pv in lastVal]
def _listPVChanged(self, pv):
"""This function is called by list items when their value changes.
List-level listeners are notified of the change. See the
:meth:`PropertyValue.propNotify` method.
"""
if self._ignoreListItems:
return
log.debug('List item %s.%s changed (%s) - notifying '
'list-level listeners (%s)',
self._context().__class__.__name__,
self._name,
id(self._context()),
pv)
self.propNotify()
def __getitem__(self, key):
vals = [pv.get() for pv in PropertyValue.get(self)]
return vals.__getitem__(key)
def __len__( self):
return self[:].__len__()
def __repr__( self):
return self[:].__repr__()
def __str__( self):
return self[:].__str__()
def __iter__( self):
return self[:].__iter__()
def __contains__(self, item):
return self[:].__contains__(item)
[docs]
def index( self, item):
return self[:].index(item)
[docs]
def count( self, item):
return self[:].count(item)
[docs]
def insert(self, index, item):
"""Inserts the given item before the given index. """
propVals = self.getPropertyValueList()
propVals.insert(index, self.__newItem(item))
PropertyValue.set(self, propVals)
[docs]
def insertAll(self, index, items):
"""Inserts all of the given items before the given index."""
propVals = self.getPropertyValueList()
propVals[index:index] = [self.__newItem(i) for i in items]
PropertyValue.set(self, propVals)
[docs]
def append(self, item):
"""Appends the given item to the end of the list."""
propVals = self.getPropertyValueList()
propVals.append(self.__newItem(item))
PropertyValue.set(self, propVals)
[docs]
def extend(self, iterable):
"""Appends all items in the given iterable to the end of the list."""
propVals = self.getPropertyValueList()
propVals.extend([self.__newItem(i) for i in iterable])
PropertyValue.set(self, propVals)
[docs]
def pop(self, index=-1):
"""Remove and return the specified value in the list (default:
last).
"""
propVals = self.getPropertyValueList()
poppedPropVal = propVals.pop(index)
PropertyValue.set(self, propVals)
return poppedPropVal.get()
[docs]
def move(self, from_, to):
"""Move the item from ``from_`` to ``to``."""
propVals = self.getPropertyValueList()
propVals.insert(to, propVals.pop(from_))
PropertyValue.set(self, propVals)
[docs]
def remove(self, value):
"""Remove the first item in the list with the specified value. """
# delegates to __delitem__, defined below
del self[self.index(value)]
[docs]
def removeAll(self, values):
"""Removes the first occurrence in the list of all of the specified
values.
"""
propVals = self.getPropertyValueList()
listVals = [pv.get() for pv in propVals]
for v in values:
propVals.pop(listVals.index(v))
PropertyValue.set(self, propVals)
[docs]
def reorder(self, idxs):
"""Reorders the list according to the given sequence of indices."""
idxs = list(idxs)
if list(sorted(idxs)) != list(range(len(self))):
raise ValueError('Indices ({}) must '
'cover the list range '
'([0..{}])'.format(idxs, len(self) - 1))
if idxs == list(range(len(self))):
return
propVals = self.getPropertyValueList()
propVals = [propVals[i] for i in idxs]
PropertyValue.set(self, propVals)
def __setitem__(self, key, values):
"""Sets the value(s) of the list at the specified index/slice."""
if isinstance(key, slice):
indices = list(range(*key.indices(len(self))))
if len(indices) != len(self) and \
len(indices) != len(values):
raise IndexError(
'PropertyValueList does not support complex slices')
elif isinstance(key, int):
indices = [key]
values = [values]
else:
raise IndexError('Invalid key type')
# Replacement of all items in list
if len(indices) == len(self) and \
len(indices) != len(values):
notifState = self.getNotificationState()
self.disableNotification()
del self[:]
self.setNotificationState(notifState)
self.extend(values)
return
# prepare the new values
propVals = self.getPropertyValueList()
oldVals = [pv.get() for pv in propVals]
changedVals = [False] * len(self)
# Update the PV instances that
# correspond to the new values,
# but suppress notification on them
self._ignoreListItems = True
try:
for idx, val in zip(indices, values):
propVal = propVals[idx]
notifState = propVal.getNotificationState()
propVal.disableNotification()
propVal.set(val)
propVal.setNotificationState(notifState)
changedVals[idx] = not self._itemEqualityFunc(
propVal.get(), oldVals[idx])
# Notify list-level and item-level listeners
# if any values in the list were changed
if any(changedVals):
log.debug('Notifying list-level listeners (%s.%s %s)',
self._context().__class__.__name__,
self._name, id(self._context()))
self.propNotify()
log.debug('Notifying item-level listeners (%s.%s %s)',
self._context().__class__.__name__,
self._name, id(self._context()))
for idx in indices:
if changedVals[idx]:
propVals[idx].propNotify()
finally:
self._ignoreListItems = False
def __delitem__(self, key):
"""Remove items at the specified index/slice from the list."""
propVals = self.getPropertyValueList()
propVals.__delitem__(key)
PropertyValue.set(self, propVals)
[docs]
def safeCall(func, *args, **kwargs):
"""This function is may be used to "safely" run a function which may
trigger ``PropertyValue`` notifications. Any notifications are queued
and executed in the correct order.
"""
name = uuid.uuid4()
PropertyValue.queue.call(func, name, *args, **kwargs)