properties_value.py 48.9 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

44
45
log = logging.getLogger(__name__)

46

47
class WeakFunctionRef(object):
48
49
50
51
52
53
    """Class which encapsulates a :mod:`weakref` to a function or method.

    This class is used by :class:`PropertyValue` instances to reference
    listeners which have been registered to be notified of property value
    or attribute changes.
    """
54

Paul McCarthy's avatar
Paul McCarthy committed
55

56
    def __init__(self, func):
57
58
59
        """Create a new ``WeakFunctionRef`` to encapsulate the given
        function or bound/unbound method.
        """
60
61

        # Bound method
62
63
64
65
        if self.__isMethod(func):

            boundMeth = six.get_method_function(func)
            boundSelf = six.get_method_self(    func)
66

67
68
69
70
71
            # We can't take a weakref of the method
            # object, so we have to weakref the object
            # and the unbound class function. The
            # function method will search for and
            # return the bound method, though.
72
73
            self.obj  = weakref.ref(boundSelf)
            self.func = weakref.ref(boundMeth)
74

75
76
            self.objType  = type(boundSelf).__name__
            self.funcName =      boundMeth .__name__
77
78
79

        # Unbound/class method or function
        else:
Paul McCarthy's avatar
Paul McCarthy committed
80

81
82
83
84
85
86
87
            self.obj      = None
            self.objType  = None
            self.func     = weakref.ref(func)
            self.funcName = func.__name__


    def __str__(self):
88
        """Return a string representation of the function."""
89
90
91
92
93
94
95
96
97
98
99
100

        selftype = type(self).__name__
        func     = self.function()

        if self.obj is None:
            s = '{}: {}'   .format(selftype, self.funcName)
        else:
            s = '{}: {}.{}'.format(selftype, self.objType, self.funcName)

        if func is None: return '{} <dead>'.format(s)
        else:            return s

Paul McCarthy's avatar
Paul McCarthy committed
101

102
103
    def __repr__(self):
        """Return a string representation of the function."""
104
105
        return self.__str__()

Paul McCarthy's avatar
Paul McCarthy committed
106

107
108
109
    def __isMethod(self, func):
        """Returns ``True`` if the given function is a bound method,
        ``False`` otherwise.
Paul McCarthy's avatar
Paul McCarthy committed
110

111
112
113
        This seems to be one of the few areas where python 2 and 3 are
        irreconcilably incompatible (or just where :mod:`six` does not have a
        function to help us).
Paul McCarthy's avatar
Paul McCarthy committed
114

115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
        In Python 2 there is no difference between an unbound method and a
        function. But in Python 3, an unbound method is still a method (and
        inspect.ismethod returns True).
        """

        ismethod = False

        # Therefore, in python2 we need to test
        # whether the function is a method, and
        # also test whether it is bound.
        if six.PY2:
            ismethod = (inspect.ismethod(func) and
                        six.get_method_self(func) is not None)

        # But in python3, if the function is a
        # method it is, by definition, bound.
        elif six.PY3:
            ismethod = inspect.ismethod(func)

        return ismethod
Paul McCarthy's avatar
Paul McCarthy committed
135

136
137

    def __findPrivateMethod(self):
138
139
140
        """Finds and returns the bound method associated with the encapsulated
        function.
        """
141
142
143
144
145

        obj      = self.obj()
        func     = self.func()
        methName = self.funcName

146
147
148
149
        # Find all attributes on the object which end with
        # the method name - there will be more than one of
        # these if the object has base classes which have
        # private methods of the same name.
150
        attNames = dir(obj)
Paul McCarthy's avatar
Paul McCarthy committed
151
        attNames = [a for a in attNames if a.endswith(methName)]
152

153
154
        # Find the attribute with the correct name, which
        # is a method, and has the correct function.
155
156
157
158
159
        for name in attNames:

            att = getattr(obj, name)

            if isinstance(att, types.MethodType) and \
160
               six.get_method_function(att) is func:
161
                return att
162
163
164

        return None

Paul McCarthy's avatar
Paul McCarthy committed
165

166
    def function(self):
167
168
169
        """Return a reference to the encapsulated function or method,
        or ``None`` if the function has been garbage collected.
        """
170
171
172
173
174
175
176
177
178
179
180
181
182

        # Unbound/class method or function
        if self.obj is None:
            return self.func()

        # The instance owning the method has been destroyed
        if self.obj() is None or self.func() is None:
            return None

        obj = self.obj()

        # Return the bound method object
        try:    return getattr(obj, self.funcName)
183
184
185

        # If the function is a bound private method,
        # its name on the instance will have been
Paul McCarthy's avatar
Paul McCarthy committed
186
        # mangled, so we need to search for it
187
188
189
        except: return self.__findPrivateMethod()


190
191
192
193
class Listener(object):
    """The ``Listener`` class is used by :class:`PropertyValue` instances to
    manage their listeners - see :meth:`PropertyValue.addListener`.
    """
194
    def __init__(self, propVal, name, function, enabled, immediate):
195
196
        """Create a ``Listener``.

197
        :arg propVal:   The ``PropertyValue`` that owns this ``Listener``.
198
199
200
201
202
203
204
        :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`.
        """

205
        self.propVal   = weakref.ref(propVal)
206
207
208
209
210
211
        self.name      = name
        self.function  = function
        self.enabled   = enabled
        self.immediate = immediate


212
213
214
215
216
    def makeQueueName(self):
        """Returns a more descriptive name for this ``Listener``, which
        is used as its name when passed to the :class:`.CallQueue`.
        """

217
218
        ctxName = self.propVal()._context().__class__.__name__
        pvName  = self.propVal()._name
219
220

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

222
223


224
class PropertyValue(object):
225
226
    """An object which encapsulates a value of some sort.

Paul McCarthy's avatar
Paul McCarthy committed
227
228
    The value may be subjected to casting and validation rules, and listeners
    may be registered for notification of value and validity changes.
229
230
231
232

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

235
236

    queue = callqueue.CallQueue(skipDuplicates=True)
237
238
239
240
241
242
    """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.
243
    """
Paul McCarthy's avatar
Paul McCarthy committed
244

245

246
    def __init__(self,
247
                 context,
248
                 name=None,
249
                 value=None,
250
                 castFunc=None,
251
                 validateFunc=None,
252
                 equalityFunc=None,
253
                 preNotifyFunc=None,
254
                 postNotifyFunc=None,
255
                 allowInvalid=True,
256
                 parent=None,
257
                 **attributes):
258
        """Create a ``PropertyValue`` object.
Paul McCarthy's avatar
Paul McCarthy committed
259

260
261
262
        :param context:        An object which is passed as the first argument
                               to the ``validateFunc``, ``preNotifyFunc``,
                               ``postNotifyFunc``, and any registered
263
264
                               listeners. Can technically be anything, but will
                               nearly always be a :class:`.HasProperties`
265
266
                               instance.

267
        :param name:           Value name - if not provided, a default, unique
268
269
270
271
272
273
274
275
                               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
276
                               return that value, cast/converted appropriately.
Paul McCarthy's avatar
Paul McCarthy committed
277

278
279
280
281
282
283
        :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.

284
285
286
287
288
        :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.

289
290
        :param preNotifyFunc:  Function to be called whenever the property
                               value changes, but before any registered
291
292
293
                               listeners are called. See the
                               :meth:`addListener` method for details of the
                               parameters this function must accept.
Paul McCarthy's avatar
Paul McCarthy committed
294

295
296
297
298
        :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
299

300
301
302
303
304
305
306
307
308
        :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.
309

Paul McCarthy's avatar
Paul McCarthy committed
310
        :param parent:         If this PV instance is a member of a
311
312
313
314
315
                               :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
316
317
318
319
320

        :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
321
322
323
324
325
326
327
                               ``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.
328
        """
Paul McCarthy's avatar
Paul McCarthy committed
329

330
        if name     is     None: name  = 'PropertyValue_{}'.format(id(self))
331
        if castFunc is not None: value = castFunc(context, attributes, value)
332
        if equalityFunc is None: equalityFunc = lambda a, b: a == b
Paul McCarthy's avatar
Paul McCarthy committed
333

334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
        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

350
351
        self._preNotifyListener  = Listener(self,
                                            'prenotify',
352
353
354
                                            preNotifyFunc,
                                            True,
                                            True)
355
356
        self._postNotifyListener = Listener(self,
                                            'postnotify',
357
358
359
                                            postNotifyFunc,
                                            True,
                                            False)
360

361
362
        if parent is not None: self.__parent = weakref.ref(parent)
        else:                  self.__parent = None
363

Paul McCarthy's avatar
Paul McCarthy committed
364
        if not allowInvalid and validateFunc is not None:
365
366
            validateFunc(context, self._attributes, value)

367

368
369
370
371
372
373
374
375
376
377
    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__()


378
379
380
381
    def __hash__(self):
        return id(self)


382
383
384
385
386
387
388
389
    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
390

391
392
393
    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
394
        """
395
        return not self.__eq__(other)
396

Paul McCarthy's avatar
Paul McCarthy committed
397

398
399
400
401
402
403
404
405
    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
406

407
408
409
410
411
412
413
    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
414

415
416
        return name[len(salt):]

417
418
419
420
421
422
423
424
425

    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
426

427
428
429
430
431
432
433
434
435
436
    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
437
438


439
    def enableNotification(self, bound=False):
440
        """Enables notification of property value and attribute listeners for
441
        this ``PropertyValue`` object.
442
443
444
445
446
447

        :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``.
448
449
450
        """
        self.__notification = True

451
452
453
454
455
456
457
        if not bound:
            return

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

Paul McCarthy's avatar
Paul McCarthy committed
458

459
    def disableNotification(self, bound=False):
460
        """Disables notification of property value and attribute listeners for
461
462
        this ``PropertyValue`` object. Notification can be re-enabled via
        the :meth:`enableNotification` or :meth:`setNotificationState` methods.
463
464
465
466
467
468


        :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
469
                    ``PropertyValue``.
470
471
        """
        self.__notification = False
472

473
        if not bound:
Paul McCarthy's avatar
Paul McCarthy committed
474
            return
475
476
477

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

480

481
482
483
484
    def getNotificationState(self):
        """Returns ``True`` if notification is currently enabled, ``False``
        otherwise.
        """
485
        return self.__notification
486

Paul McCarthy's avatar
Paul McCarthy committed
487

488
489
    def setNotificationState(self, value):
        """Sets the current notification state."""
490
491
        if value: self.enableNotification()
        else:     self.disableNotification()
492

Paul McCarthy's avatar
Paul McCarthy committed
493

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

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

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

502
503
          - ``value``:     The new attribute value.

504
          - ``name``:      The name of this ``PropertyValue`` instance.
505

506
507
508
        :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
509

510
        :param listener:  The callback function.
511

512
513
514
515
516
517
        :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`.
518
        """
519
        log.debug('Adding attribute listener on {}.{} ({}): {}'.format(
520
            self._context().__class__.__name__, self._name, id(self), name))
521
522
523

        if weak:
            listener = WeakFunctionRef(listener)
Paul McCarthy's avatar
Paul McCarthy committed
524

525
        name = self.__saltListenerName(name)
526
527
        self._attributeListeners[name] = Listener(self,
                                                  name,
528
529
530
                                                  listener,
                                                  True,
                                                  immediate)
531

Paul McCarthy's avatar
Paul McCarthy committed
532

533
534
535
536
537
538
539
    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
540

541
542
543
544
    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
545
                                                                 name))
546
547
        self._attributeListeners[name].enabled = True

Paul McCarthy's avatar
Paul McCarthy committed
548

549
    def removeAttributeListener(self, name):
550
        """Removes the attribute listener of the given name."""
551
        log.debug('Removing attribute listener on {}.{}: {}'.format(
552
            self._context().__class__.__name__, self._name, name))
Paul McCarthy's avatar
Paul McCarthy committed
553

554
555
556
557
        name     = self.__saltListenerName(name)
        listener = self._attributeListeners.pop(name, None)

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

559
560
561
562
563
564
565
            cb = listener.function

            if isinstance(cb, WeakFunctionRef):
                cb = cb.function()

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

567
568

    def getAttributes(self):
569
        """Returns a dictionary containing all the attributes of this
570
        ``PropertyValue`` object.
571
572
573
        """
        return self._attributes.copy()

Paul McCarthy's avatar
Paul McCarthy committed
574

575
    def setAttributes(self, atts):
576
        """Sets all the attributes of this ``PropertyValue`` object.
577
578
579
580
581
582
        from the given dictionary.
        """

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

Paul McCarthy's avatar
Paul McCarthy committed
583

584
    def getAttribute(self, name):
585
        """Returns the value of the named attribute."""
586
587
        return self._attributes[name]

Paul McCarthy's avatar
Paul McCarthy committed
588

589
    def setAttribute(self, name, value):
590
        """Sets the named attribute to the given value, and notifies any
591
592
593
594
595
        registered attribute listeners of the change.
        """
        oldVal = self._attributes.get(name, None)

        self._attributes[name] = value
596

597
598
        if oldVal == value: return

599
        log.debug('Attribute on {}.{} ({}) changed: {} = {}'.format(
600
            self._context().__class__.__name__,
601
602
603
604
            self._name,
            id(self),
            name,
            value))
605

606
        self.notifyAttributeListeners(name, value)
607

608
609
        self.revalidate()

610

611
612
613
    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.
614

615
616
617
618
        :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.
619
        """
620

621
622
623
        if not self.__notification:
            return [], []

624
625
        if att: lDict = self._attributeListeners
        else:   lDict = self._changeListeners
626

627
        allListeners = []
Paul McCarthy's avatar
Paul McCarthy committed
628

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

631
632
            if not listener.enabled:
                continue
633
634

            cb = listener.function
635

636
637
            if isinstance(cb, WeakFunctionRef):
                cb = cb.function()
638

Paul McCarthy's avatar
Paul McCarthy committed
639
            # The owner of the referred function/method
640
            # has been GC'd - remove it
641
            if cb is None:
Paul McCarthy's avatar
Paul McCarthy committed
642

643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
                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:

658
659
            if self._preNotifyListener.function is not None and \
               self._preNotifyListener.enabled:
660
                allListeners = [self._preNotifyListener] + allListeners
Paul McCarthy's avatar
Paul McCarthy committed
661

662
663
            if self._postNotifyListener.function is not None and \
               self._postNotifyListener.enabled:
664
665
                allListeners = allListeners + [self._postNotifyListener]

666
667
        if att: args = self._context(), name, value, self._name
        else:   args = (self.get(), self.__valid, self._context(), self._name)
668

669
        return allListeners, args
670
671
672


    def notifyAttributeListeners(self, name, value):
673
674
        """Notifies all registered attribute listeners of an attribute
        changed - see the :func:`.bindable.syncAndNotifyAtts` function.
675
676
        """

677
        bindable.syncAndNotifyAtts(self, name, value)
Paul McCarthy's avatar
Paul McCarthy committed
678
679


680
681
682
683
684
685
    def addListener(self,
                    name,
                    callback,
                    overwrite=False,
                    weak=True,
                    immediate=False):
686
687
688
        """Adds a listener for this value.

        When the value changes, the listener callback function is called. The
689
        callback function must accept the following arguments:
690
691
692
693

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

696
697
        Listener names ``prenotify`` and ``postnotify`` are reserved - if
        either of these are passed in for the listener name, a :exc`ValueError`
698
699
        is raised.

700
        :param str name:  A unique name for this listener. If a listener with
701
                          the name already exists, a :exc`RuntimeError` will be
702
703
                          raised, or it will be overwritten, depending upon
                          the value of the ``overwrite`` argument.
Paul McCarthy's avatar
Paul McCarthy committed
704

705
        :param callback:  The callback function.
Paul McCarthy's avatar
Paul McCarthy committed
706

707
        :param overwrite: If ``True`` any previous listener with the same name
708
709
710
711
                          will be overwritten.

        :param weak:      If ``True`` (the default), a weak reference to the
                          callback function is retained, meaning that it
712
713
714
715
                          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.
716
717
718
719
720
721
722
723

        :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.
724
        """
725
726
727
728

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

730
        log.debug('Adding listener on {}.{}: {}'.format(
731
            self._context().__class__.__name__,
732
733
            self._name,
            name))
Paul McCarthy's avatar
Paul McCarthy committed
734

735
        fullName = self.__saltListenerName(name)
736
        prior    = self._changeListeners.get(fullName, None)
737

738
739
        if weak:
            callback = WeakFunctionRef(callback)
740

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

744
745
746
        elif prior is not None:
            prior.function  = callback
            prior.immediate = immediate
Paul McCarthy's avatar
Paul McCarthy committed
747

748
        else:
749
750
            self._changeListeners[fullName] = Listener(self,
                                                       fullName,
751
752
753
                                                       callback,
                                                       True,
                                                       immediate)
754

755
756

    def removeListener(self, name):
757
        """Removes the listener with the given name from this
758
        ``PropertyValue``.
759
        """
760
761
762
763
764
765
766
767

        # 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
768
        if log.getEffectiveLevel() == logging.DEBUG:
769
            import inspect
770
            stack = inspect.stack()
771

772
773
774
775
776
777
778
779
780
781
782
783
            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))
784

785
786
        name     = self.__saltListenerName(name)
        listener = self._changeListeners.pop(name, None)
787

788
        if listener is not None:
789
790
791
792
793
794
795

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

797
            cb = listener.function
798

799
800
801
            if isinstance(cb, WeakFunctionRef):
                cb = cb.function()

802
803
            if cb is not None:
                PropertyValue.queue.dequeue(listener.makeQueueName())
804
805
806


    def enableListener(self, name):
807
        """(Re-)Enables the listener with the specified ``name``."""
808
        name = self.__saltListenerName(name)
809
        log.debug('Enabling listener on {}: {}'.format(self._name, name))
810
        self._changeListeners[name].enabled = True
811

Paul McCarthy's avatar
Paul McCarthy committed
812

813
    def disableListener(self, name):
814
815
816
        """Disables the listener with the specified ``name``, but does not
        remove it from the list of listeners.
        """
817
        name = self.__saltListenerName(name)
818
        log.debug('Disabling listener on {}: {}'.format(self._name, name))
819
        self._changeListeners[name].enabled = False
820
821


822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
    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)


843
    def hasListener(self, name):
844
845
846
847
        """Returns ``True`` if a listener with the given name is registered,
        ``False`` otherwise.
        """

848
        name = self.__saltListenerName(name)
849
        return name in self._changeListeners.keys()
850

851
852

    def setPreNotifyFunction(self, preNotifyFunc):
853
854
        """Sets the function to be called on value changes, before any
        registered listeners.
855
        """
856
        self._preNotifyListener.function = preNotifyFunc
857

Paul McCarthy's avatar
Paul McCarthy committed
858

859
860
861
862
    def setPostNotifyFunction(self, postNotifyFunc):
        """Sets the function to be called on value changes, after any
        registered listeners.
        """
863
        self._postNotifyListener.function = postNotifyFunc
864
865
866
867
868


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

Paul McCarthy's avatar
Paul McCarthy committed
870

871
    def get(self):
872
        """Returns the current property value."""
873
874
        return self.__value

Paul McCarthy's avatar
Paul McCarthy committed
875

876
    def set(self, newValue):
877
878
879
        """Sets the property value.

        The property is validated and, if the property value or its validity
880
        has changed, any registered listeners are called through the
881
882
883
        :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.
884
        """
885

886
887
        # cast the value if necessary.
        # Allow any errors to be thrown
888
        if self._castFunc is not None:
889
            newValue = self._castFunc(self._context(),
890
891
                                      self._attributes,
                                      newValue)
Paul McCarthy's avatar
Paul McCarthy committed
892

893
        # Check to see if the new value is valid
894
895
        valid    = False
        validStr = None
896
        try:
897
            if self._validate is not None:
898
                self._validate(self._context(), self._attributes, newValue)
899
            valid = True
900

901
902
        except ValueError as e:

903
904
            # Oops, we don't allow invalid values.
            validStr = str(e)
905
            if not self._allowInvalid:
906
                import traceback
907
                log.debug('Attempt to set {}.{} to an invalid value ({}), '
908
                          'but allowInvalid is False ({})'.format(
909
                              self._context().__class__.__name__,
910
911
                              self._name,
                              newValue,
912
                              e), exc_info=True)
913
                traceback.print_stack()
914
915
                raise e

916
917
918
919
        self.__lastValue = self.__value
        self.__lastValid = self.__valid
        self.__value     = newValue
        self.__valid     = valid
920
921
922

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

926
927
        if not changed: return

928
        log.debug('Value {}.{} changed: {} -> {} ({})'.format(
929
            self._context().__class__.__name__,
930
            self._name,
931
            self.__lastValue,
932
            self.__value,
933
            'valid' if valid else 'invalid - {}'.format(validStr)))
Paul McCarthy's avatar
Paul McCarthy committed
934
935

        # Notify any registered listeners.
936
        self.propNotify()
937

938

939
    def propNotify(self):
940
941
        """Notifies registered listeners - see the
        :func:`.bindable.syncAndNotify` function.
942
        """
943

944
945
        bindable.syncAndNotify(self)

946
947
        if not self.__notification:
            return
948

Paul McCarthy's avatar
Paul McCarthy committed
949
        # If this PV is a member of a PV list,
950
951
952
        # tell the list that this PV has
        # changed, so that it can notify its own
        # list-level listeners of the change
953
954
        if self.__parent is not None and self.__parent() is not None:
            self.__parent()._listPVChanged(self)
955

956
957

    def revalidate(self):
958
959
        """Revalidates the current property value, and re-notifies any
        registered listeners if the value validity has changed.
960
961
962
963
        """
        self.set(self.get())


964
    def isValid(self):
965
        """Returns ``True`` if the current property value is valid, ``False``
966
967
        otherwise.
        """
968
        try: self._validate(self._context(), self._attributes, self.get())
969
970
        except: return False
        return True