properties_value.py 44.7 KB
Newer Older
1
2
#!/usr/bin/env python
#
3
4
5
# properties_value.py - Definitions of the PropertyValue and
#                       PropertyValueList classes.
#
6
7
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
8
9
10
"""Definitions of the :class:`PropertyValue` and :class:`PropertyValueList`
classes.

Paul McCarthy's avatar
Paul McCarthy committed
11
 .. autosummary::
12
13
    :nosignatures:

Paul McCarthy's avatar
Paul McCarthy committed
14
15
16
    PropertyValue
    PropertyValueList

17

18
19
20
21
22
23
``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.


Paul McCarthy's avatar
Paul McCarthy committed
24
25
26
27
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.
28
"""
29

30

31
import logging
32
import inspect
33
34
import types
import weakref
35
36
37

from collections import OrderedDict

38
39
import six

40
41
from . import callqueue
from . import bindable
42

43
import fsl.utils.weakfuncref as weakfuncref
44

Paul McCarthy's avatar
Paul McCarthy committed
45

46
log = logging.getLogger(__name__)
47
48


49
50
51
52
class Listener(object):
    """The ``Listener`` class is used by :class:`PropertyValue` instances to
    manage their listeners - see :meth:`PropertyValue.addListener`.
    """
53
    def __init__(self, propVal, name, function, enabled, immediate):
54
55
        """Create a ``Listener``.

56
        :arg propVal:   The ``PropertyValue`` that owns this ``Listener``.
57
58
59
60
61
62
63
        :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`.
        """

64
        self.propVal   = weakref.ref(propVal)
65
66
67
68
69
70
        self.name      = name
        self.function  = function
        self.enabled   = enabled
        self.immediate = immediate


71
72
73
74
75
    def makeQueueName(self):
        """Returns a more descriptive name for this ``Listener``, which
        is used as its name when passed to the :class:`.CallQueue`.
        """

76
77
        ctxName = self.propVal()._context().__class__.__name__
        pvName  = self.propVal()._name
78
79

        return '{} ({}.{})'.format(self.name, ctxName, pvName)
Paul McCarthy's avatar
Paul McCarthy committed
80

81
82


83
class PropertyValue(object):
84
85
    """An object which encapsulates a value of some sort.

Paul McCarthy's avatar
Paul McCarthy committed
86
87
    The value may be subjected to casting and validation rules, and listeners
    may be registered for notification of value and validity changes.
88
89
90
91

    Notification of value and attribute listeners is performed by the
    :mod:`.bindable` module - see the :func:`.bindable.syncAndNotify` and
    :func:`.bindable.syncAndNotifyAtts` functions.
92
93
    """

94
95

    queue = callqueue.CallQueue(skipDuplicates=True)
96
97
98
99
100
101
    """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.
102
    """
Paul McCarthy's avatar
Paul McCarthy committed
103

104

105
    def __init__(self,
106
                 context,
107
                 name=None,
108
                 value=None,
109
                 castFunc=None,
110
                 validateFunc=None,
111
                 equalityFunc=None,
112
                 preNotifyFunc=None,
113
                 postNotifyFunc=None,
114
                 allowInvalid=True,
115
                 parent=None,
116
                 **attributes):
117
        """Create a ``PropertyValue`` object.
Paul McCarthy's avatar
Paul McCarthy committed
118

119
120
121
        :param context:        An object which is passed as the first argument
                               to the ``validateFunc``, ``preNotifyFunc``,
                               ``postNotifyFunc``, and any registered
122
123
                               listeners. Can technically be anything, but will
                               nearly always be a :class:`.HasProperties`
124
125
                               instance.

126
        :param name:           Value name - if not provided, a default, unique
127
128
129
130
131
132
133
134
                               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
135
                               return that value, cast/converted appropriately.
Paul McCarthy's avatar
Paul McCarthy committed
136

137
138
139
140
141
142
        :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.

143
144
145
146
147
        :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.

148
149
        :param preNotifyFunc:  Function to be called whenever the property
                               value changes, but before any registered
150
151
152
                               listeners are called. See the
                               :meth:`addListener` method for details of the
                               parameters this function must accept.
Paul McCarthy's avatar
Paul McCarthy committed
153

154
155
156
157
        :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``.
Paul McCarthy's avatar
Paul McCarthy committed
158

159
160
161
162
163
164
165
166
167
        :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.
168

Paul McCarthy's avatar
Paul McCarthy committed
169
        :param parent:         If this PV instance is a member of a
170
171
172
173
174
                               :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.
Paul McCarthy's avatar
Paul McCarthy committed
175
176
177
178
179

        :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
180
181
182
183
184
185
186
                               ``PropertyValue`` or ``PropertyValueList``
                               classes, however they are used by the
                               :class:`.PropertyBase` and
                               :class:`.ListPropertyBase` classes to store
                               per-instance property constraints. Listeners
                               may register to be notified when attribute
                               values change.
187
        """
Paul McCarthy's avatar
Paul McCarthy committed
188

189
        if name     is     None: name  = 'PropertyValue_{}'.format(id(self))
190
        if castFunc is not None: value = castFunc(context, attributes, value)
191
        if equalityFunc is None: equalityFunc = lambda a, b: a == b
Paul McCarthy's avatar
Paul McCarthy committed
192

193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
        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

209
210
        self._preNotifyListener  = Listener(self,
                                            'prenotify',
211
212
213
                                            preNotifyFunc,
                                            True,
                                            True)
214
215
        self._postNotifyListener = Listener(self,
                                            'postnotify',
216
217
218
                                            postNotifyFunc,
                                            True,
                                            False)
219

220
221
        if parent is not None: self.__parent = weakref.ref(parent)
        else:                  self.__parent = None
222

Paul McCarthy's avatar
Paul McCarthy committed
223
        if not allowInvalid and validateFunc is not None:
224
225
            validateFunc(context, self._attributes, value)

226

227
228
229
230
231
232
233
234
235
236
    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__()


237
238
239
240
    def __hash__(self):
        return id(self)


241
242
243
244
245
246
247
248
    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)

Paul McCarthy's avatar
Paul McCarthy committed
249

250
251
252
    def __ne__(self, other):
        """Returns ``True`` if the given object has a different value to
        this instance, ``False`` otherwise.
Paul McCarthy's avatar
Paul McCarthy committed
253
        """
254
        return not self.__eq__(other)
255

Paul McCarthy's avatar
Paul McCarthy committed
256

257
258
259
260
261
262
263
264
    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)

Paul McCarthy's avatar
Paul McCarthy committed
265

266
267
268
269
270
271
272
    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)
Paul McCarthy's avatar
Paul McCarthy committed
273

274
275
        return name[len(salt):]

276
277
278
279
280
281
282
283
284

    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

Paul McCarthy's avatar
Paul McCarthy committed
285

286
287
288
289
290
291
292
293
294
295
    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)
Paul McCarthy's avatar
Paul McCarthy committed
296
297


298
    def enableNotification(self, bound=False):
299
        """Enables notification of property value and attribute listeners for
300
        this ``PropertyValue`` object.
301
302
303
304
305
306

        :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``.
307
308
309
        """
        self.__notification = True

310
311
312
313
314
315
316
        if not bound:
            return

        bpvs = bindable.buildBPVList(self, 'boundPropVals')[0]
        for bpv in bpvs:
            bpv.enableNotification()

Paul McCarthy's avatar
Paul McCarthy committed
317

318
    def disableNotification(self, bound=False):
319
        """Disables notification of property value and attribute listeners for
320
321
        this ``PropertyValue`` object. Notification can be re-enabled via
        the :meth:`enableNotification` or :meth:`setNotificationState` methods.
322
323
324
325
326
327


        :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
Paul McCarthy's avatar
Paul McCarthy committed
328
                    ``PropertyValue``.
329
330
        """
        self.__notification = False
331

332
        if not bound:
Paul McCarthy's avatar
Paul McCarthy committed
333
            return
334
335
336

        bpvs = bindable.buildBPVList(self, 'boundPropVals')[0]
        for bpv in bpvs:
Paul McCarthy's avatar
Paul McCarthy committed
337
338
            bpv.disableNotification()

339

340
341
342
343
    def getNotificationState(self):
        """Returns ``True`` if notification is currently enabled, ``False``
        otherwise.
        """
344
        return self.__notification
345

Paul McCarthy's avatar
Paul McCarthy committed
346

347
348
    def setNotificationState(self, value):
        """Sets the current notification state."""
349
350
        if value: self.enableNotification()
        else:     self.disableNotification()
351

Paul McCarthy's avatar
Paul McCarthy committed
352

353
    def addAttributeListener(self, name, listener, weak=True, immediate=False):
354
        """Adds an attribute listener for this ``PropertyValue``. The
355
        listener callback function must accept the following arguments:
Paul McCarthy's avatar
Paul McCarthy committed
356

357
          - ``context``:   The context associated with this ``PropertyValue``.
Paul McCarthy's avatar
Paul McCarthy committed
358

359
          - ``attribute``: The name of the attribute that changed.
Paul McCarthy's avatar
Paul McCarthy committed
360

361
362
          - ``value``:     The new attribute value.

363
          - ``name``:      The name of this ``PropertyValue`` instance.
364

365
366
367
        :param name:      A unique name for the listener. If a listener with
                          the specified name already exists, it will be
                          overwritten.
Paul McCarthy's avatar
Paul McCarthy committed
368

369
        :param listener:  The callback function.
370

371
372
373
374
375
376
        :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`.
377
        """
378
        log.debug('Adding attribute listener on {}.{} ({}): {}'.format(
379
            self._context().__class__.__name__, self._name, id(self), name))
380
381

        if weak:
382
            listener = weakfuncref.WeakFunctionRef(listener)
Paul McCarthy's avatar
Paul McCarthy committed
383

384
        name = self.__saltListenerName(name)
385
386
        self._attributeListeners[name] = Listener(self,
                                                  name,
387
388
389
                                                  listener,
                                                  True,
                                                  immediate)
390

Paul McCarthy's avatar
Paul McCarthy committed
391

392
393
394
395
396
397
398
    def disableAttributeListener(self, name):
        """Disables the attribute listener with the specified ``name``. """
        name = self.__saltListenerName(name)
        log.debug('Disabling attribute listener on {}: {}'.format(self._name,
                                                                  name))
        self._attributeListeners[name].enabled = False

Paul McCarthy's avatar
Paul McCarthy committed
399

400
401
402
403
    def enableAttributeListener(self, name):
        """Enables the attribute listener with the specified ``name``. """
        name = self.__saltListenerName(name)
        log.debug('Enabling attribute listener on {}: {}'.format(self._name,
Paul McCarthy's avatar
Paul McCarthy committed
404
                                                                 name))
405
406
        self._attributeListeners[name].enabled = True

Paul McCarthy's avatar
Paul McCarthy committed
407

408
    def removeAttributeListener(self, name):
409
        """Removes the attribute listener of the given name."""
410
        log.debug('Removing attribute listener on {}.{}: {}'.format(
411
            self._context().__class__.__name__, self._name, name))
Paul McCarthy's avatar
Paul McCarthy committed
412

413
414
415
416
        name     = self.__saltListenerName(name)
        listener = self._attributeListeners.pop(name, None)

        if listener is not None:
Paul McCarthy's avatar
Paul McCarthy committed
417

418
419
            cb = listener.function

420
            if isinstance(cb, weakfuncref.WeakFunctionRef):
421
422
423
424
                cb = cb.function()

            if cb is not None:
                PropertyValue.queue.dequeue(listener.makeQueueName())
425

426
427

    def getAttributes(self):
428
        """Returns a dictionary containing all the attributes of this
429
        ``PropertyValue`` object.
430
431
432
        """
        return self._attributes.copy()

Paul McCarthy's avatar
Paul McCarthy committed
433

434
    def setAttributes(self, atts):
435
        """Sets all the attributes of this ``PropertyValue`` object.
436
437
438
439
440
441
        from the given dictionary.
        """

        for name, value in atts.items():
            self.setAttribute(name, value)

Paul McCarthy's avatar
Paul McCarthy committed
442

443
    def getAttribute(self, name):
444
        """Returns the value of the named attribute."""
445
446
        return self._attributes[name]

Paul McCarthy's avatar
Paul McCarthy committed
447

448
    def setAttribute(self, name, value):
449
        """Sets the named attribute to the given value, and notifies any
450
451
452
453
454
        registered attribute listeners of the change.
        """
        oldVal = self._attributes.get(name, None)

        self._attributes[name] = value
455

456
457
        if oldVal == value: return

458
        log.debug('Attribute on {}.{} ({}) changed: {} = {}'.format(
459
            self._context().__class__.__name__,
460
461
462
463
            self._name,
            id(self),
            name,
            value))
464

465
        self.notifyAttributeListeners(name, value)
466

467
468
        self.revalidate()

469

470
471
472
    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.
473

474
475
476
477
        :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.
478
        """
479

480
481
482
        if not self.__notification:
            return [], []

483
484
        if att: lDict = self._attributeListeners
        else:   lDict = self._changeListeners
485

486
        allListeners = []
Paul McCarthy's avatar
Paul McCarthy committed
487

488
        for lName, listener in list(lDict.items()):
Paul McCarthy's avatar
Paul McCarthy committed
489

490
491
            if not listener.enabled:
                continue
492
493

            cb = listener.function
494

495
            if isinstance(cb, weakfuncref.WeakFunctionRef):
496
                cb = cb.function()
497

Paul McCarthy's avatar
Paul McCarthy committed
498
            # The owner of the referred function/method
499
            # has been GC'd - remove it
500
            if cb is None:
Paul McCarthy's avatar
Paul McCarthy committed
501

502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
                log.debug('Removing dead listener {}'.format(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:

517
518
            if self._preNotifyListener.function is not None and \
               self._preNotifyListener.enabled:
519
                allListeners = [self._preNotifyListener] + allListeners
Paul McCarthy's avatar
Paul McCarthy committed
520

521
522
            if self._postNotifyListener.function is not None and \
               self._postNotifyListener.enabled:
523
524
                allListeners = allListeners + [self._postNotifyListener]

525
526
        if att: args = self._context(), name, value, self._name
        else:   args = (self.get(), self.__valid, self._context(), self._name)
527

528
        return allListeners, args
529
530
531


    def notifyAttributeListeners(self, name, value):
532
533
        """Notifies all registered attribute listeners of an attribute
        changed - see the :func:`.bindable.syncAndNotifyAtts` function.
534
535
        """

536
        bindable.syncAndNotifyAtts(self, name, value)
Paul McCarthy's avatar
Paul McCarthy committed
537
538


539
540
541
542
543
544
    def addListener(self,
                    name,
                    callback,
                    overwrite=False,
                    weak=True,
                    immediate=False):
545
546
547
        """Adds a listener for this value.

        When the value changes, the listener callback function is called. The
548
        callback function must accept the following arguments:
549
550
551
552

          - ``value``:   The property value
          - ``valid``:   Whether the value is valid or invalid
          - ``context``: The context object passed to :meth:`__init__`.
553
          - ``name``:    The name of this ``PropertyValue`` instance.
554

555
556
        Listener names ``prenotify`` and ``postnotify`` are reserved - if
        either of these are passed in for the listener name, a :exc`ValueError`
557
558
        is raised.

559
        :param str name:  A unique name for this listener. If a listener with
560
                          the name already exists, a :exc`RuntimeError` will be
561
562
                          raised, or it will be overwritten, depending upon
                          the value of the ``overwrite`` argument.
Paul McCarthy's avatar
Paul McCarthy committed
563

564
        :param callback:  The callback function.
Paul McCarthy's avatar
Paul McCarthy committed
565

566
        :param overwrite: If ``True`` any previous listener with the same name
567
568
569
570
                          will be overwritten.

        :param weak:      If ``True`` (the default), a weak reference to the
                          callback function is retained, meaning that it
571
572
573
574
                          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.
575
576
577
578
579
580
581
582

        :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.
583
        """
584
585
586
587

        if name in ('prenotify', 'postnotify'):
            raise ValueError('Reserved listener name used: {}. '
                             'Use a different name.'.format(name))
Paul McCarthy's avatar
Paul McCarthy committed
588

589
        log.debug('Adding listener on {}.{}: {}'.format(
590
            self._context().__class__.__name__,
591
592
            self._name,
            name))
Paul McCarthy's avatar
Paul McCarthy committed
593

594
        fullName = self.__saltListenerName(name)
595
        prior    = self._changeListeners.get(fullName, None)
596

597
        if weak:
598
            callback = weakfuncref.WeakFunctionRef(callback)
599

600
601
        if (prior is not None) and (not overwrite):
            raise RuntimeError('Listener {} already exists'.format(name))
Paul McCarthy's avatar
Paul McCarthy committed
602

603
604
605
        elif prior is not None:
            prior.function  = callback
            prior.immediate = immediate
Paul McCarthy's avatar
Paul McCarthy committed
606

607
        else:
608
609
            self._changeListeners[fullName] = Listener(self,
                                                       fullName,
610
611
612
                                                       callback,
                                                       True,
                                                       immediate)
613

614
615

    def removeListener(self, name):
616
        """Removes the listener with the given name from this
617
        ``PropertyValue``.
618
        """
619
620
621
622
623
624
625
626

        # 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
627
        if log.getEffectiveLevel() == logging.DEBUG:
628
            import inspect
629
            stack = inspect.stack()
630

631
632
633
634
635
636
637
638
639
640
641
642
            if len(stack) >= 4: frame = stack[ 3]
            else:               frame = stack[-1]

            srcMod  = '...{}'.format(frame[1][-20:])
            srcLine = frame[2]

            log.debug('Removing listener on {}.{}: {} ({}:{})'.format(
                self._context().__class__.__name__,
                self._name,
                name,
                srcMod,
                srcLine))
643

644
645
        name     = self.__saltListenerName(name)
        listener = self._changeListeners.pop(name, None)
646

647
        if listener is not None:
648
649
650
651
652
653
654

            # 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
Paul McCarthy's avatar
Paul McCarthy committed
655

656
            cb = listener.function
657

658
            if isinstance(cb, weakfuncref.WeakFunctionRef):
659
660
                cb = cb.function()

661
662
            if cb is not None:
                PropertyValue.queue.dequeue(listener.makeQueueName())
663
664
665


    def enableListener(self, name):
666
        """(Re-)Enables the listener with the specified ``name``."""
667
        name = self.__saltListenerName(name)
668
        log.debug('Enabling listener on {}: {}'.format(self._name, name))
669
        self._changeListeners[name].enabled = True
670

Paul McCarthy's avatar
Paul McCarthy committed
671

672
    def disableListener(self, name):
673
674
675
        """Disables the listener with the specified ``name``, but does not
        remove it from the list of listeners.
        """
676
        name = self.__saltListenerName(name)
677
        log.debug('Disabling listener on {}: {}'.format(self._name, name))
678
        self._changeListeners[name].enabled = False
679
680


681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
    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


    def setListenerState(self, name, state):
        """Enables/disables the specified listener. """

        if state: self.enableListener(name)
        else:     self.disableListener(name)


702
    def hasListener(self, name):
703
704
705
706
        """Returns ``True`` if a listener with the given name is registered,
        ``False`` otherwise.
        """

707
        name = self.__saltListenerName(name)
708
        return name in self._changeListeners.keys()
709

710
711

    def setPreNotifyFunction(self, preNotifyFunc):
712
713
        """Sets the function to be called on value changes, before any
        registered listeners.
714
        """
715
        self._preNotifyListener.function = preNotifyFunc
716

Paul McCarthy's avatar
Paul McCarthy committed
717

718
719
720
721
    def setPostNotifyFunction(self, postNotifyFunc):
        """Sets the function to be called on value changes, after any
        registered listeners.
        """
722
        self._postNotifyListener.function = postNotifyFunc
723
724
725
726
727


    def getLast(self):
        """Returns the most recent property value before the current one."""
        return self.__lastValue
728

Paul McCarthy's avatar
Paul McCarthy committed
729

730
    def get(self):
731
        """Returns the current property value."""
732
733
        return self.__value

Paul McCarthy's avatar
Paul McCarthy committed
734

735
    def set(self, newValue):
736
737
738
        """Sets the property value.

        The property is validated and, if the property value or its validity
739
        has changed, any registered listeners are called through the
740
741
742
        :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.
743
        """
744

745
746
        # cast the value if necessary.
        # Allow any errors to be thrown
747
        if self._castFunc is not None:
748
            newValue = self._castFunc(self._context(),
749
750
                                      self._attributes,
                                      newValue)
Paul McCarthy's avatar
Paul McCarthy committed
751

752
        # Check to see if the new value is valid
753
754
        valid    = False
        validStr = None
755
        try:
756
            if self._validate is not None:
757
                self._validate(self._context(), self._attributes, newValue)
758
            valid = True
759

760
761
        except ValueError as e:

762
763
            # Oops, we don't allow invalid values.
            validStr = str(e)
764
            if not self._allowInvalid:
765
                import traceback
766
                log.debug('Attempt to set {}.{} to an invalid value ({}), '
767
                          'but allowInvalid is False ({})'.format(
768
                              self._context().__class__.__name__,
769
770
                              self._name,
                              newValue,
771
                              e), exc_info=True)
772
                traceback.print_stack()
773
774
                raise e

775
776
777
778
        self.__lastValue = self.__value
        self.__lastValid = self.__valid
        self.__value     = newValue
        self.__valid     = valid
779
780
781

        # If the value or its validity has not
        # changed, listeners are not notified
782
783
        changed = (self.__valid != self.__lastValid) or \
                  not self._equalityFunc(self.__value, self.__lastValue)
784

785
786
        if not changed: return

787
        log.debug('Value {}.{} changed: {} -> {} ({})'.format(
788
            self._context().__class__.__name__,
789
            self._name,
790
            self.__lastValue,
791
            self.__value,
792
            'valid' if valid else 'invalid - {}'.format(validStr)))
Paul McCarthy's avatar
Paul McCarthy committed
793
794

        # Notify any registered listeners.
795
        self.propNotify()
796

797

798
    def propNotify(self):
799
800
        """Notifies registered listeners - see the
        :func:`.bindable.syncAndNotify` function.
801
        """
802

803
804
        bindable.syncAndNotify(self)

805
806
        if not self.__notification:
            return
807

Paul McCarthy's avatar
Paul McCarthy committed
808
        # If this PV is a member of a PV list,
809
810
811
        # tell the list that this PV has
        # changed, so that it can notify its own
        # list-level listeners of the change
812
813
        if self.__parent is not None and self.__parent() is not None:
            self.__parent()._listPVChanged(self)
814

815
816

    def revalidate(self):
817
818
        """Revalidates the current property value, and re-notifies any
        registered listeners if the value validity has changed.
819
820
821
822
        """
        self.set(self.get())


823
    def isValid(self):
824
        """Returns ``True`` if the current property value is valid, ``False``
825
826
        otherwise.
        """
827
        try: self._validate(self._context(), self._attributes, self.get())
828
829
        except: return False
        return True
830

831
832

class PropertyValueList(PropertyValue):
833
834
835
    """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.
836
837
838

    When created, separate validation functions may be passed in for
    individual items, and for the list as a whole. Listeners may be registered
839
840
    on individual ``PropertyValue`` instances (accessible via the
    :meth:`getPropertyValueList` method), or on the entire list.
841

842
    The values contained in this ``PropertyValueList`` may be accessed
843
844
    through standard Python list operations, including slice-based access and
    assignment, :meth:`append`, :meth:`insert`, :meth:`extend`, :meth:`pop`,
845
846
    :meth:`index`, :meth:`count`, :meth:`move`, :meth:`insertAll`,
    :meth:`removeAll`, and :meth:`reorder` (these last few are non-standard).
847

848
    Because the values contained in this list are ``PropertyValue``
Paul McCarthy's avatar
Paul McCarthy committed
849
    instances themselves, some limitations are present on list modifying
850
    operations::
851

Paul McCarthy's avatar
Paul McCarthy committed
852
853
      class MyObj(props.HasProperties):
        mylist = props.List(default[1, 2, 3])
854

Paul McCarthy's avatar
Paul McCarthy committed
855
      myobj = MyObj()
856

857
    Simple list-slicing modifications work as expected::
858

859
860
      # the value after this will be [5, 2, 3]
      myobj.mylist[0]  = 5
Paul McCarthy's avatar
Paul McCarthy committed
861

862
863
      # the value after this will be [5, 6, 7]
      myobj.mylist[1:] = [6, 7]
864

865
866
    However, modifications which would change the length of the list are not
    supported::
867

868
869
      # This will result in an IndexError
      myobj.mylist[0:2] = [6, 7, 8]
870

Paul McCarthy's avatar
Paul McCarthy committed
871
    The exception to this rule concerns modifications which would replace
872
    every value in the list::
873

874
875
876
877
      # These assignments are equivalent
      myobj.mylist[:] = [1, 2, 3, 4, 5]
      myobj.mylist    = [1, 2, 3, 4, 5]

878
    While the simple list modifications described above will change the
879
880
881
882
883
    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.
884

885
    A listener registered on a ``PropertyValueList`` will be notified
Paul McCarthy's avatar
Paul McCarthy committed
886
887
    whenever the list is modified (e.g. additions, removals, reorderings), and
    whenever any individual value in the list changes. Alternately, listeners
888
    may be registered on the individual ``PropertyValue`` instances (which
Paul McCarthy's avatar
Paul McCarthy committed
889
890
    are accessible through the :meth:`getPropertyValueList` method) to be
    nofitied of changes to those values only.
891
892

    There are some interesting type-specific subclasses of the
893
    ``PropertyValueList``, which provide additional functionality:
894

895
      - The :class:`.PointValueList`, for :class:`.Point` properties.
896

897
      - The :class:`.BoundsValueList`, for :class:`.Bounds` properties.
898
899
900
    """

    def __init__(self,
901
902
903
                 context,
                 name=None,
                 values=None,
904
                 itemCastFunc=None,
905
                 itemEqualityFunc=None,
906
907
                 itemValidateFunc=None,
                 listValidateFunc=None,
908
                 itemAllowInvalid=True,
909
                 preNotifyFunc=None,