properties_value.py 45.5 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 uuid
32
import logging
33
import weakref
34
35
36

from collections import OrderedDict

37
38
from . import callqueue
from . import bindable
39

40
import fsl.utils.weakfuncref as weakfuncref
41

Paul McCarthy's avatar
Paul McCarthy committed
42

43
log = logging.getLogger(__name__)
44
45


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

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

61
        self.propVal   = weakref.ref(propVal)
62
63
64
65
66
67
        self.name      = name
        self.function  = function
        self.enabled   = enabled
        self.immediate = immediate


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

73
74
        ctxName = self.propVal()._context().__class__.__name__
        pvName  = self.propVal()._name
75
76

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

78

79
class PropertyValue(object):
80
81
    """An object which encapsulates a value of some sort.

Paul McCarthy's avatar
Paul McCarthy committed
82
83
    The value may be subjected to casting and validation rules, and listeners
    may be registered for notification of value and validity changes.
84
85
86
87

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

90
91

    queue = callqueue.CallQueue(skipDuplicates=True)
92
93
94
95
96
97
    """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.
98
    """
Paul McCarthy's avatar
Paul McCarthy committed
99

100

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

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

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

133
134
135
136
137
138
        :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.

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

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

150
151
152
153
        :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
154

155
156
157
158
159
160
161
162
163
        :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.
164

Paul McCarthy's avatar
Paul McCarthy committed
165
        :param parent:         If this PV instance is a member of a
166
167
168
169
170
                               :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
171
172
173
174
175

        :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
176
177
178
179
180
181
182
                               ``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.
183
        """
Paul McCarthy's avatar
Paul McCarthy committed
184

185
        if name     is     None: name  = 'PropertyValue_{}'.format(id(self))
186
        if castFunc is not None: value = castFunc(context, attributes, value)
187
        if equalityFunc is None: equalityFunc = lambda a, b: a == b
Paul McCarthy's avatar
Paul McCarthy committed
188

189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
        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

205
206
        self._preNotifyListener  = Listener(self,
                                            'prenotify',
207
208
209
                                            preNotifyFunc,
                                            True,
                                            True)
210
211
        self._postNotifyListener = Listener(self,
                                            'postnotify',
212
213
214
                                            postNotifyFunc,
                                            True,
                                            False)
215

216
217
        if parent is not None: self.__parent = weakref.ref(parent)
        else:                  self.__parent = None
218

Paul McCarthy's avatar
Paul McCarthy committed
219
        if not allowInvalid and validateFunc is not None:
220
221
            validateFunc(context, self._attributes, value)

222

223
224
225
226
227
228
229
230
231
232
    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__()


233
234
235
236
    def __hash__(self):
        return id(self)


237
238
239
240
241
242
243
244
    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
245

246
247
248
    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
249
        """
250
        return not self.__eq__(other)
251

Paul McCarthy's avatar
Paul McCarthy committed
252

253
254
255
256
257
258
259
260
    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
261

262
263
264
265
266
267
268
    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
269

270
271
        return name[len(salt):]

272
273
274
275
276
277
278
279
280

    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
281

282
283
284
285
286
287
288
289
290
291
    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
292
293


294
    def enableNotification(self, bound=False, att=False):
295
        """Enables notification of property value and attribute listeners for
296
        this ``PropertyValue`` object.
297
298
299
300
301
302

        :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``.
303
304
305

        :arg att:   If ``True``, notification is enabled on all attribute
                    listeners as well as property value listeners.
306
307
308
        """
        self.__notification = True

309
310
311
        if not bound:
            return

312
313
314
315
316
        bpvs = list(bindable.buildBPVList(self, 'boundPropVals')[0])

        if att:
            bpvs += list(bindable.buildBPVList(self, 'boundAttPropVals')[0])

317
318
319
        for bpv in bpvs:
            bpv.enableNotification()

Paul McCarthy's avatar
Paul McCarthy committed
320

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


        :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
331
                    ``PropertyValue``.
332
333
334

        :arg att:   If ``True``, notification is disabled on all attribute
                    listeners as well as property value listeners.
335
336
        """
        self.__notification = False
337

338
        if not bound:
Paul McCarthy's avatar
Paul McCarthy committed
339
            return
340

341
342
343
344
345
        bpvs = list(bindable.buildBPVList(self, 'boundPropVals')[0])

        if att:
            bpvs += list(bindable.buildBPVList(self, 'boundAttPropVals')[0])

346
        for bpv in bpvs:
Paul McCarthy's avatar
Paul McCarthy committed
347
348
            bpv.disableNotification()

349

350
351
352
353
    def getNotificationState(self):
        """Returns ``True`` if notification is currently enabled, ``False``
        otherwise.
        """
354
        return self.__notification
355

Paul McCarthy's avatar
Paul McCarthy committed
356

357
358
    def setNotificationState(self, value):
        """Sets the current notification state."""
359
360
        if value: self.enableNotification()
        else:     self.disableNotification()
361

Paul McCarthy's avatar
Paul McCarthy committed
362

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

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

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

371
372
          - ``value``:     The new attribute value.

373
          - ``name``:      The name of this ``PropertyValue`` instance.
374

375
376
377
        :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
378

379
        :param listener:  The callback function.
380

381
382
383
384
385
386
        :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`.
387
        """
388
        log.debug('Adding attribute listener on {}.{} ({}): {}'.format(
389
            self._context().__class__.__name__, self._name, id(self), name))
390
391

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

394
        name = self.__saltListenerName(name)
395
396
        self._attributeListeners[name] = Listener(self,
                                                  name,
397
398
399
                                                  listener,
                                                  True,
                                                  immediate)
400

Paul McCarthy's avatar
Paul McCarthy committed
401

402
403
404
405
406
407
408
    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
409

410
411
412
413
    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
414
                                                                 name))
415
416
        self._attributeListeners[name].enabled = True

Paul McCarthy's avatar
Paul McCarthy committed
417

418
    def removeAttributeListener(self, name):
419
        """Removes the attribute listener of the given name."""
420
        log.debug('Removing attribute listener on {}.{}: {}'.format(
421
            self._context().__class__.__name__, self._name, name))
Paul McCarthy's avatar
Paul McCarthy committed
422

423
424
425
426
        name     = self.__saltListenerName(name)
        listener = self._attributeListeners.pop(name, None)

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

428
429
            cb = listener.function

430
            if isinstance(cb, weakfuncref.WeakFunctionRef):
431
432
433
434
                cb = cb.function()

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

436
437

    def getAttributes(self):
438
        """Returns a dictionary containing all the attributes of this
439
        ``PropertyValue`` object.
440
441
442
        """
        return self._attributes.copy()

Paul McCarthy's avatar
Paul McCarthy committed
443

444
    def setAttributes(self, atts):
445
        """Sets all the attributes of this ``PropertyValue`` object.
446
447
448
449
450
451
        from the given dictionary.
        """

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

Paul McCarthy's avatar
Paul McCarthy committed
452

453
    def getAttribute(self, name):
454
        """Returns the value of the named attribute."""
455
456
        return self._attributes[name]

Paul McCarthy's avatar
Paul McCarthy committed
457

458
    def setAttribute(self, name, value):
459
        """Sets the named attribute to the given value, and notifies any
460
461
462
463
464
        registered attribute listeners of the change.
        """
        oldVal = self._attributes.get(name, None)

        self._attributes[name] = value
465

466
467
        if oldVal == value: return

468
        log.debug('Attribute on {}.{} ({}) changed: {} = {}'.format(
469
            self._context().__class__.__name__,
470
471
472
473
            self._name,
            id(self),
            name,
            value))
474

475
        self.notifyAttributeListeners(name, value)
476

477
478
        self.revalidate()

479

480
481
482
    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.
483

484
485
486
487
        :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.
488
        """
489

490
491
492
        if not self.__notification:
            return [], []

493
494
        if att: lDict = self._attributeListeners
        else:   lDict = self._changeListeners
495

496
        allListeners = []
Paul McCarthy's avatar
Paul McCarthy committed
497

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

500
501
            if not listener.enabled:
                continue
502
503

            cb = listener.function
504

505
            if isinstance(cb, weakfuncref.WeakFunctionRef):
506
                cb = cb.function()
507

Paul McCarthy's avatar
Paul McCarthy committed
508
            # The owner of the referred function/method
509
            # has been GC'd - remove it
510
            if cb is None:
Paul McCarthy's avatar
Paul McCarthy committed
511

512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
                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:

527
528
            if self._preNotifyListener.function is not None and \
               self._preNotifyListener.enabled:
529
                allListeners = [self._preNotifyListener] + allListeners
Paul McCarthy's avatar
Paul McCarthy committed
530

531
532
            if self._postNotifyListener.function is not None and \
               self._postNotifyListener.enabled:
533
534
                allListeners = allListeners + [self._postNotifyListener]

535
536
        if att: args = self._context(), name, value, self._name
        else:   args = (self.get(), self.__valid, self._context(), self._name)
537

538
        return allListeners, args
539
540
541


    def notifyAttributeListeners(self, name, value):
542
543
        """Notifies all registered attribute listeners of an attribute
        changed - see the :func:`.bindable.syncAndNotifyAtts` function.
544
545
        """

546
        bindable.syncAndNotifyAtts(self, name, value)
Paul McCarthy's avatar
Paul McCarthy committed
547
548


549
550
551
552
553
554
    def addListener(self,
                    name,
                    callback,
                    overwrite=False,
                    weak=True,
                    immediate=False):
555
556
557
        """Adds a listener for this value.

        When the value changes, the listener callback function is called. The
558
        callback function must accept the following arguments:
559
560
561
562

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

565
566
        Listener names ``prenotify`` and ``postnotify`` are reserved - if
        either of these are passed in for the listener name, a :exc`ValueError`
567
568
        is raised.

569
        :param str name:  A unique name for this listener. If a listener with
570
                          the name already exists, a :exc`RuntimeError` will be
571
572
                          raised, or it will be overwritten, depending upon
                          the value of the ``overwrite`` argument.
Paul McCarthy's avatar
Paul McCarthy committed
573

574
        :param callback:  The callback function.
Paul McCarthy's avatar
Paul McCarthy committed
575

576
        :param overwrite: If ``True`` any previous listener with the same name
577
578
579
580
                          will be overwritten.

        :param weak:      If ``True`` (the default), a weak reference to the
                          callback function is retained, meaning that it
581
582
583
584
                          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.
585
586
587
588
589
590
591
592

        :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.
593
        """
594
595
596
597

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

599
        log.debug('Adding listener on {}.{}: {}'.format(
600
            self._context().__class__.__name__,
601
602
            self._name,
            name))
Paul McCarthy's avatar
Paul McCarthy committed
603

604
        fullName = self.__saltListenerName(name)
605
        prior    = self._changeListeners.get(fullName, None)
606

607
        if weak:
608
            callback = weakfuncref.WeakFunctionRef(callback)
609

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

613
614
615
        elif prior is not None:
            prior.function  = callback
            prior.immediate = immediate
Paul McCarthy's avatar
Paul McCarthy committed
616

617
        else:
618
619
            self._changeListeners[fullName] = Listener(self,
                                                       fullName,
620
621
622
                                                       callback,
                                                       True,
                                                       immediate)
623

624
625

    def removeListener(self, name):
626
        """Removes the listener with the given name from this
627
        ``PropertyValue``.
628
        """
629
630
631
632
633
634
635
636

        # 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
637
        if log.getEffectiveLevel() == logging.DEBUG:
638
            import inspect
639
            stack = inspect.stack()
640

641
642
643
644
645
646
647
648
649
650
651
652
            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))
653

654
655
        name     = self.__saltListenerName(name)
        listener = self._changeListeners.pop(name, None)
656

657
        if listener is not None:
658
659
660
661
662
663
664

            # 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
665

666
            cb = listener.function
667

668
            if isinstance(cb, weakfuncref.WeakFunctionRef):
669
670
                cb = cb.function()

671
672
            if cb is not None:
                PropertyValue.queue.dequeue(listener.makeQueueName())
673
674
675


    def enableListener(self, name):
676
        """(Re-)Enables the listener with the specified ``name``."""
677
        name = self.__saltListenerName(name)
678
        log.debug('Enabling listener on {}: {}'.format(self._name, name))
679
        self._changeListeners[name].enabled = True
680

Paul McCarthy's avatar
Paul McCarthy committed
681

682
    def disableListener(self, name):
683
684
685
        """Disables the listener with the specified ``name``, but does not
        remove it from the list of listeners.
        """
686
        name = self.__saltListenerName(name)
687
        log.debug('Disabling listener on {}: {}'.format(self._name, name))
688
        self._changeListeners[name].enabled = False
689
690


691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
    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)


712
    def hasListener(self, name):
713
714
715
716
        """Returns ``True`` if a listener with the given name is registered,
        ``False`` otherwise.
        """

717
        name = self.__saltListenerName(name)
718
        return name in self._changeListeners.keys()
719

720
721

    def setPreNotifyFunction(self, preNotifyFunc):
722
723
        """Sets the function to be called on value changes, before any
        registered listeners.
724
        """
725
        self._preNotifyListener.function = preNotifyFunc
726

Paul McCarthy's avatar
Paul McCarthy committed
727

728
729
730
731
    def setPostNotifyFunction(self, postNotifyFunc):
        """Sets the function to be called on value changes, after any
        registered listeners.
        """
732
        self._postNotifyListener.function = postNotifyFunc
733
734
735
736
737


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

Paul McCarthy's avatar
Paul McCarthy committed
739

740
    def get(self):
741
        """Returns the current property value."""
742
743
        return self.__value

Paul McCarthy's avatar
Paul McCarthy committed
744

745
    def set(self, newValue):
746
747
748
        """Sets the property value.

        The property is validated and, if the property value or its validity
749
        has changed, any registered listeners are called through the
750
751
752
        :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.
753
        """
754

755
756
        # cast the value if necessary.
        # Allow any errors to be thrown
757
        if self._castFunc is not None:
758
            newValue = self._castFunc(self._context(),
759
760
                                      self._attributes,
                                      newValue)
Paul McCarthy's avatar
Paul McCarthy committed
761

762
        # Check to see if the new value is valid
763
764
        valid    = False
        validStr = None
765
        try:
766
            if self._validate is not None:
767
                self._validate(self._context(), self._attributes, newValue)
768
            valid = True
769

770
771
        except ValueError as e:

772
773
            # Oops, we don't allow invalid values.
            validStr = str(e)
774
            if not self._allowInvalid:
775
                import traceback
776
                log.debug('Attempt to set {}.{} to an invalid value ({}), '
777
                          'but allowInvalid is False ({})'.format(
778
                              self._context().__class__.__name__,
779
780
                              self._name,
                              newValue,
781
                              e), exc_info=True)
782
                traceback.print_stack()
783
784
                raise e

785
786
787
788
        self.__lastValue = self.__value
        self.__lastValid = self.__valid
        self.__value     = newValue
        self.__valid     = valid
789
790
791

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

795
796
        if not changed: return

797
        log.debug('Value {}.{} changed: {} -> {} ({})'.format(
798
            self._context().__class__.__name__,
799
            self._name,
800
            self.__lastValue,
801
            self.__value,
802
            'valid' if valid else 'invalid - {}'.format(validStr)))
Paul McCarthy's avatar
Paul McCarthy committed
803
804

        # Notify any registered listeners.
805
        self.propNotify()
806

807

808
    def propNotify(self):
809
810
        """Notifies registered listeners - see the
        :func:`.bindable.syncAndNotify` function.
811
        """
812

813
814
        bindable.syncAndNotify(self)

Paul McCarthy's avatar
Paul McCarthy committed
815
        # If this PV is a member of a PV list,
816
817
818
        # tell the list that this PV has
        # changed, so that it can notify its own
        # list-level listeners of the change
819
820
        if self.__parent is not None and self.__parent() is not None:
            self.__parent()._listPVChanged(self)
821

822
823

    def revalidate(self):
824
825
        """Revalidates the current property value, and re-notifies any
        registered listeners if the value validity has changed.
826
827
828
829
        """
        self.set(self.get())


830
    def isValid(self):
831
        """Returns ``True`` if the current property value is valid, ``False``
832
833
        otherwise.
        """
834
        try: self._validate(self._context(), self._attributes, self.get())
835
836
        except: return False
        return True
837

838
839

class PropertyValueList(PropertyValue):
840
841
842
    """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.
843
844
845

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

849
    The values contained in this ``PropertyValueList`` may be accessed
850
851
    through standard Python list operations, including slice-based access and
    assignment, :meth:`append`, :meth:`insert`, :meth:`extend`, :meth:`pop`,
852
853
    :meth:`index`, :meth:`count`, :meth:`move`, :meth:`insertAll`,
    :meth:`removeAll`, and :meth:`reorder` (these last few are non-standard).
854

855
    Because the values contained in this list are ``PropertyValue``
Paul McCarthy's avatar
Paul McCarthy committed
856
    instances themselves, some limitations are present on list modifying
857
    operations::
858

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

Paul McCarthy's avatar
Paul McCarthy committed
862
      myobj = MyObj()
863

864
    Simple list-slicing modifications work as expected::
865

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

869
870
      # the value after this will be [5, 6, 7]
      myobj.mylist[1:] = [6, 7]
871

872
873
    However, modifications which would change the length of the list are not
    supported::
874

875
876
      # This will result in an IndexError
      myobj.mylist[0:2] = [6, 7, 8]
877

Paul McCarthy's avatar
Paul McCarthy committed
878
    The exception to this rule concerns modifications which would replace
879
    every value in the list::
880

881
882
883
884
      # These assignments are equivalent
      myobj.mylist[:] = [1, 2, 3, 4, 5]
      myobj.mylist    = [1, 2, 3, 4, 5]

885
    While the simple list modifications described above will change the
886
887
888
889
890
    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.
891

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

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

902
      - The :class:`.PointValueList`, for :class:`.Point` properties.
903

904
      - The :class:`.BoundsValueList`, for :class:`.Bounds` properties.
905
906
907
    """

    def __init__(self,
908
909
910
                 context,
                 name=None,
                 values=None,
911
                 itemCastFunc=None,