properties_types.py 56 KB
Newer Older
Paul McCarthy's avatar
Paul McCarthy committed
1
2
#!/usr/bin/env python
#
3
# properties_types.py - Definitions for different property types.
Paul McCarthy's avatar
Paul McCarthy committed
4
5
6
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
7
8
"""Definitions for different property types.

9
This module provides a number of :class:`.PropertyBase` subclasses which
Paul McCarthy's avatar
Paul McCarthy committed
10
11
define properties of different types. These classes are intended to be
added as attributes of a :class:`.HasProperties` class definition.
12
13

 .. autosummary::
14
15
    :nosignatures:

16
17
18
19
20
21
22
23
24
25
26
27
28
29
    Object
    Boolean
    Number
    Int
    Real
    Percentage
    String
    Choice
    FilePath
    List
    Colour
    ColourMap
    Bounds
    Point
30
    Array
31
"""
Paul McCarthy's avatar
Paul McCarthy committed
32

33

Paul McCarthy's avatar
Paul McCarthy committed
34
35
import os.path as op

36
37
import matplotlib.pyplot as plt
import matplotlib.cm     as mplcm
38
39
import matplotlib.colors as mplcolors
import numpy             as np
40

41
42
from . import properties        as props
from . import properties_value  as propvals
Paul McCarthy's avatar
Paul McCarthy committed
43

44

45
46
47
48
class Object(props.PropertyBase):
    """A property which encapsulates any value. """

    def __init__(self, **kwargs):
49
        """Create a ``Object`` property. If an ``equalityFunc`` is not
50
51
52
53
54
55
56
57
58
59
60
        provided, any writes to this property will be treated as if the value
        has changed (and any listeners will be notified).
        """

        def defaultEquals(this, other):
            return False

        kwargs['equalityFunc'] = kwargs.get('equalityFunc', defaultEquals)
        props.PropertyBase.__init__(self, **kwargs)


Paul McCarthy's avatar
Paul McCarthy committed
61
class Boolean(props.PropertyBase):
62
    """A property which encapsulates a ``bool`` value."""
Paul McCarthy's avatar
Paul McCarthy committed
63
64

    def __init__(self, **kwargs):
65
        """Create a ``Boolean`` property.
66
67
68
69

        If the ``default`` ``kwarg`` is not provided, a default value of
        ``False`` is used.
        """
Paul McCarthy's avatar
Paul McCarthy committed
70
71

        kwargs['default'] = kwargs.get('default', False)
72
73
        props.PropertyBase.__init__(self, **kwargs)

Paul McCarthy's avatar
Paul McCarthy committed
74

75
    def cast(self, instance, attributes, value):
76
77
        """Overrides :meth:`.PropertyBase.cast`. Casts the given value to a
        ``bool``.
78
        """
79
        return bool(value)
Paul McCarthy's avatar
Paul McCarthy committed
80
81
82


class Number(props.PropertyBase):
83
84
85
    """Base class for the :class:`Int` and :class:`Real` classes.

    A property which represents a number.  Don't use/subclass this,
86
    use/subclass one of ``Int`` or ``Real``.
Paul McCarthy's avatar
Paul McCarthy committed
87
    """
Paul McCarthy's avatar
Paul McCarthy committed
88

89
90
91
92
93
    def __init__(self,
                 minval=None,
                 maxval=None,
                 clamped=False,
                 **kwargs):
94
        """Define a :class:`Number` property.
Paul McCarthy's avatar
Paul McCarthy committed
95

96
        :param minval:  Minimum valid value
Paul McCarthy's avatar
Paul McCarthy committed
97

98
        :param maxval:  Maximum valid value
Paul McCarthy's avatar
Paul McCarthy committed
99

100
101
        :param clamped: If ``True``, the value will be clamped to its
                        min/max bounds.
Paul McCarthy's avatar
Paul McCarthy committed
102

103
104
105
        :param kwargs:  Passed through to :meth:`.PropertyBase.__init__`.
                        If a ``default`` value is not provided, it is set
                        to something sensible.
106
        """
Paul McCarthy's avatar
Paul McCarthy committed
107
108
109
110

        default = kwargs.get('default', None)

        if default is None:
111
112
113
114
115
116
            if minval is not None and maxval is not None:
                default = (minval + maxval) / 2
            elif minval is not None:
                default = minval
            elif maxval is not None:
                default = maxval
117
118
            else:
                default = 0
119

120
121
122
123
        kwargs['default']    = default
        kwargs['minval']     = minval
        kwargs['maxval']     = maxval
        kwargs['clamped']    = clamped
124
        props.PropertyBase.__init__(self, **kwargs)
Paul McCarthy's avatar
Paul McCarthy committed
125

Paul McCarthy's avatar
Paul McCarthy committed
126

127
    def validate(self, instance, attributes, value):
128
        """Overrides :meth:`.PropertyBase.validate`. Validates the given
Paul McCarthy's avatar
Paul McCarthy committed
129
        number.
130

131
132
        Calls the :meth:`.PropertyBase.validate` method.
        Then, if the ``minval`` and/or ``maxval`` attributes have been set,
133
134
135
        and the given value is not within those values, a :exc:`ValueError` is
        raised.

136
137
        :param instance:   The owning :class:`.HasProperties` instance (or
                           ``None`` for unbound property values).
Paul McCarthy's avatar
Paul McCarthy committed
138

139
        :param attributes: Dictionary containing property attributes.
Paul McCarthy's avatar
Paul McCarthy committed
140

141
        :param value:      The value to validate.
142
        """
Paul McCarthy's avatar
Paul McCarthy committed
143

144
        props.PropertyBase.validate(self, instance, attributes, value)
Paul McCarthy's avatar
Paul McCarthy committed
145

146
147
        minval = attributes['minval']
        maxval = attributes['maxval']
Paul McCarthy's avatar
Paul McCarthy committed
148

149
150
        if minval is not None and value < minval:
            raise ValueError('Must be at least {}'.format(minval))
Paul McCarthy's avatar
Paul McCarthy committed
151

152
153
        if maxval is not None and value > maxval:
            raise ValueError('Must be at most {}'.format(maxval))
Paul McCarthy's avatar
Paul McCarthy committed
154

155

156
    def cast(self, instance, attributes, value):
157
        """Overrides :meth:`.PropertyBase.cast`.
158

159
        If the ``clamped`` attribute is ``True`` and the ``minval`` and/or
160
        ``maxval`` have been set, this function ensures that the given value
161
162
        lies within the ``minval`` and ``maxval`` limits. Otherwise the value
        is returned unchanged.
163
        """
164

165
166
167
        if value is None:
            return value

168
        clamped = attributes['clamped']
Paul McCarthy's avatar
Paul McCarthy committed
169

170
171
        if not clamped: return value

172
173
        minval = attributes['minval']
        maxval = attributes['maxval']
174
175
176
177
178

        if minval is not None and value < minval: return minval
        if maxval is not None and value > maxval: return maxval

        return value
Paul McCarthy's avatar
Paul McCarthy committed
179
180


Paul McCarthy's avatar
Paul McCarthy committed
181
class Int(Number):
182
    """A :class:`Number` which encapsulates an integer."""
Paul McCarthy's avatar
Paul McCarthy committed
183

Paul McCarthy's avatar
Paul McCarthy committed
184
    def __init__(self, **kwargs):
185
        """Create an ``Int`` property. """
186
        Number.__init__(self, **kwargs)
Paul McCarthy's avatar
Paul McCarthy committed
187

Paul McCarthy's avatar
Paul McCarthy committed
188

189
    def cast(self, instance, attributes, value):
190
        """Overrides :meth:`Number.cast`. Casts the given value to an ``int``,
191
192
        and then passes the value to :meth:`Number.cast`.
        """
193
194
        if value is None:
            return value
195
        return Number.cast(self, instance, attributes, int(value))
Paul McCarthy's avatar
Paul McCarthy committed
196

Paul McCarthy's avatar
Paul McCarthy committed
197

198
class Real(Number):
199
    """A :class:`.Number` which encapsulates a floating point number."""
200

201
202

    def __equals(self, a, b):
203
204
205
206
207
        """Custom equality function passed to :class`.PropertyBase.__init__`.

        Tests for equality according to the ``precision`` passed to
        :meth:`__init__`.
        """
208

209
210
211
        if any((a is None, b is None, self.__precision is None)):
            return a == b

212
213
        return abs(a - b) < self.__precision

Paul McCarthy's avatar
Paul McCarthy committed
214

215
    def __init__(self, precision=0.000000001, **kwargs):
216
        """Define a ``Real`` property.
217

218
219
220
        :param precision: Tolerance for equality testing. Set to ``None`` to
                          use exact equality.
        """
221
        self.__precision = precision
Paul McCarthy's avatar
Paul McCarthy committed
222

223
        Number.__init__(self, equalityFunc=self.__equals, **kwargs)
Paul McCarthy's avatar
Paul McCarthy committed
224

225

226
    def cast(self, instance, attributes, value):
227
        """Overrides :meth:`Number.cast`. Casts the given value to a ``float``,
228
        and then passes the value to :meth:`Number.cast`.
Paul McCarthy's avatar
Paul McCarthy committed
229
        """
230
231
        if value is None:
            return value
232
        return Number.cast(self, instance, attributes, float(value))
Paul McCarthy's avatar
Paul McCarthy committed
233

Paul McCarthy's avatar
Paul McCarthy committed
234

235
class Percentage(Real):
236
    """A :class:`Real` property which represents a percentage.
237

238
239
    A ``Percentage`` property is just a ``Real`` property with
    a default minimum value of ``0`` and default maximum value of ``100``.
Paul McCarthy's avatar
Paul McCarthy committed
240
241
242
    """

    def __init__(self, **kwargs):
243
        """Create a ``Percentage`` property."""
244
245
246
        kwargs['minval']  = kwargs.get('minval',    0.0)
        kwargs['maxval']  = kwargs.get('maxval',  100.0)
        kwargs['default'] = kwargs.get('default',  50.0)
247
        Real.__init__(self, **kwargs)
Paul McCarthy's avatar
Paul McCarthy committed
248
249
250


class String(props.PropertyBase):
251
    """A property which encapsulates a string."""
Paul McCarthy's avatar
Paul McCarthy committed
252

Paul McCarthy's avatar
Paul McCarthy committed
253
    def __init__(self, minlen=None, maxlen=None, **kwargs):
254
        """Cteate a ``String`` property.
Paul McCarthy's avatar
Paul McCarthy committed
255

256
257
        :param int minlen: Minimum valid string length.
        :param int maxlen: Maximum valid string length.
Paul McCarthy's avatar
Paul McCarthy committed
258
259
        """

260
        kwargs['default'] = kwargs.get('default', None)
261
262
        kwargs['minlen']  = minlen
        kwargs['maxlen']  = maxlen
263
        props.PropertyBase.__init__(self, **kwargs)
Paul McCarthy's avatar
Paul McCarthy committed
264

265

266
    def cast(self, instance, attributes, value):
267
        """Overrides :meth:`.PropertyBase.cast`.
268
269
270
271

        Casts the given value to a string. If the given value is the empty
        string, it is replaced with ``None``.
        """
272

273
274
        if value == '': return None
        else:           return value
275

Paul McCarthy's avatar
Paul McCarthy committed
276

277
    def validate(self, instance, attributes, value):
278
        """Overrides :meth:`.PropertyBase.validate`.
279
280

        Passes the given value to
281
        :meth:`.PropertyBase.validate`. Then, if either the
282
        ``minlen`` or ``maxlen`` attributes have been set, and the given
283
284
285
        value has length less than ``minlen`` or greater than ``maxlen``,
        raises a :exc:`ValueError`.
        """
Paul McCarthy's avatar
Paul McCarthy committed
286

287
        if value == '': value = None
Paul McCarthy's avatar
Paul McCarthy committed
288

289
        props.PropertyBase.validate(self, instance, attributes, value)
Paul McCarthy's avatar
Paul McCarthy committed
290
291

        if value is None: return
292

Paul McCarthy's avatar
Paul McCarthy committed
293
        if not isinstance(value, str):
294
295
            raise ValueError('Must be a string')

296
297
        minlen = attributes['minlen']
        maxlen = attributes['maxlen']
298
299
300

        if minlen is not None and len(value) < minlen:
            raise ValueError('Must have length at least {}'.format(minlen))
Paul McCarthy's avatar
Paul McCarthy committed
301

302
303
        if maxlen is not None and len(value) > maxlen:
            raise ValueError('Must have length at most {}'.format(maxlen))
Paul McCarthy's avatar
Paul McCarthy committed
304

Paul McCarthy's avatar
Paul McCarthy committed
305

306
class Choice(props.PropertyBase):
307
    """A property which may only be set to one of a set of predefined values.
308

309
310
311
312
    Choices can be added/removed via the :meth:`addChoice`,
    :meth:`removeChoice` method, and :meth:`setChoices` methods. Existing
    choices can be modified with the :meth:`updateChoice` method.

313
    Individual choices can be enabled/disabled via the :meth:`enableChoice`
314
315
316
    and :meth:`disableChoice` methods. The ``choiceEnabled`` attribute
    contains a dictionary of ``{choice : boolean}`` mappings
    representing the enabled/disabled state of each choice.
Paul McCarthy's avatar
Paul McCarthy committed
317

318
319
    A set of alternate values can be provided for each choice - these
    alternates will be accepted when assigning to a ``Choice`` property.
320
321
322
323
324

    .. note:: If you create a ``Choice`` property with non-string choice and
              alternate values, you may run into problems when using
              :mod:`.serialise` and/or :mod:`.cli` functionality, unless you
              set ``allowStr`` to ``True``.
325
    """
326

327
328
329
330
331
    def __init__(self,
                 choices=None,
                 alternates=None,
                 allowStr=False,
                 **kwargs):
332
        """Create a ``Choice`` property.
Paul McCarthy's avatar
Paul McCarthy committed
333

334
        :arg choices:    List of values, the possible values that this property
335
336
                         can take. Can alternately be a ``dict`` - see the note
                         above.
Paul McCarthy's avatar
Paul McCarthy committed
337

338
339
340
341
342
        :arg alternates: A list of lists, specificying alternate acceptable
                         values for each choice. Can also be a dict of
                         ``{choice : [alternates]}`` mappings. All alternate
                         values must be unique - different choices cannot have
                         equivalent alternate values.
343
344
345
346
347

        :arg allowStr:   If ``True``, string versions of any non-string choice
                         values will be accepted - ``str`` versions of each
                         choice are added as alternate values for that choice.
                         Defaults to ``False``.
Paul McCarthy's avatar
Paul McCarthy committed
348
349
350
        """

        if choices is None:
351
352
            choices    = []
            alternates = {}
Paul McCarthy's avatar
Paul McCarthy committed
353

354
        # Alternates are stored twice:
Paul McCarthy's avatar
Paul McCarthy committed
355
        #
356
357
        #   - As a dict of { choice    : [alternate] } mappings
        #   - As a dict of { alternate : choice      } mappings
358
359
        #
        # We generate the first dict here
360
361
362
363
364
365
366
367
368
        if alternates is None:
            alternates = {c : [] for c in choices}

        elif isinstance(alternates, dict):
            alternates = dict(alternates)

        elif isinstance(alternates, (list, tuple)):
            alternates = {c : list(a) for (c, a) in zip(choices, alternates)}

369
370
371
372
373
374
375
376
377
        # Add stringified versions of all
        # choices if allowStr is True
        if allowStr:
            for c in choices:
                strc = str(c)
                alts = alternates[c]
                if strc not in alts:
                    alts.append(strc)

378
379
380
381
382
383
384
        # Generate the second alternates dict
        altLists   = alternates
        alternates = self.__generateAlternatesDict(altLists)

        # Enabled flags are stored as a dict
        # of {choice : bool} mappings
        enabled = {choice: True for choice in choices}
Paul McCarthy's avatar
Paul McCarthy committed
385

386
387
        if len(choices) > 0: default = choices[0]
        else:                default = None
Paul McCarthy's avatar
Paul McCarthy committed
388

389
390
        if len(choices) != len(altLists):
            raise ValueError('Alternates are required for every choice')
391

392
        kwargs['choices']       = list(choices)
393
394
395
        kwargs['alternates']    = dict(alternates)
        kwargs['altLists']      = dict(altLists)
        kwargs['choiceEnabled'] = enabled
396
        kwargs['allowStr']      = allowStr
397
398
        kwargs['default']       = kwargs.get('default',      default)
        kwargs['allowInvalid']  = kwargs.get('allowInvalid', False)
Paul McCarthy's avatar
Paul McCarthy committed
399

400
        props.PropertyBase.__init__(self, **kwargs)
Paul McCarthy's avatar
Paul McCarthy committed
401

402
403
404
405
406
407

    def setDefault(self, default, instance=None):
        """Sets the default choice value. """
        if default not in self.getChoices(instance):
            raise ValueError('{} is not a choice'.format(default))

408
        self.setAttribute(instance, 'default', default)
409

Paul McCarthy's avatar
Paul McCarthy committed
410

411
    def enableChoice(self, choice, instance=None):
412
        """Enables the given choice. """
413
        choiceEnabled = dict(self.getAttribute(instance, 'choiceEnabled'))
414
        choiceEnabled[choice] = True
415
        self.setAttribute(instance, 'choiceEnabled', choiceEnabled)
416

Paul McCarthy's avatar
Paul McCarthy committed
417

418
419
    def disableChoice(self, choice, instance=None):
        """Disables the given choice. An attempt to set the property to
420
        a disabled value will result in a :exc:`ValueError`.
421
        """
422
        choiceEnabled = dict(self.getAttribute(instance, 'choiceEnabled'))
423
        choiceEnabled[choice] = False
424
        self.setAttribute(instance, 'choiceEnabled', choiceEnabled)
425
426


427
428
429
430
    def choiceEnabled(self, choice, instance=None):
        """Returns ``True`` if the given choice is enabled, ``False``
        otherwise.
        """
431
        return self.getAttribute(instance, 'choiceEnabled')[choice]
432

Paul McCarthy's avatar
Paul McCarthy committed
433

434
435
    def getChoices(self, instance=None):
        """Returns a list of the current choices. """
436
        return list(self.getAttribute(instance, 'choices'))
437

Paul McCarthy's avatar
Paul McCarthy committed
438

439
440
441
442
    def getAlternates(self, instance=None):
        """Returns a list of the current acceptable alternate values for each
        choice.
        """
443
444
        choices  = self.getAttribute(instance, 'choices')
        altLists = self.getAttribute(instance, 'altLists')
Paul McCarthy's avatar
Paul McCarthy committed
445

446
        return [altLists[c] for c in choices]
447

Paul McCarthy's avatar
Paul McCarthy committed
448

449
450
451
    def updateChoice(self,
                     choice,
                     newChoice=None,
452
                     newAlt=None,
453
                     instance=None):
454
455
        """Updates the choice value and/or alternates for the specified choice.
        """
456

457
458
        choices    = list(self.getAttribute(instance, 'choices'))
        altLists   = dict(self.getAttribute(instance, 'altLists'))
Paul McCarthy's avatar
Paul McCarthy committed
459
        idx        = choices.index(choice)
460

461
462
463
        if newChoice is not None:
            choices[ idx]       = newChoice
            altLists[newChoice] = altLists[choice]
464

465
466
467
            altLists.pop(choice)
        else:
            newChoice = choice
Paul McCarthy's avatar
Paul McCarthy committed
468

469
        if newAlt is not None: altLists[newChoice] = list(newAlt)
470

471
        self.__updateChoices(choices, altLists, instance)
472

473

474
475
476
    def setChoices(self, choices, alternates=None, instance=None):
        """Sets the list of possible choices (and their alternate values, if
        not None).
477
478
        """

479
480
481
482
483
        if alternates is None:
            alternates = {c : [] for c in choices}
        elif isinstance(alternates, (list, tuple)):
            alternates = {c : a for (c, a) in zip(choices, alternates)}
        elif isinstance(alternates, dict):
484
485
486
487
            alternates = dict(alternates)

        # Add stringified versions of all
        # choices if allowStr is True
488
        if self.getAttribute(instance, 'allowStr'):
489
490
491
492
            for c in choices:
                strc = str(c)
                alts = alternates[c]
                if strc not in alts:
Paul McCarthy's avatar
Paul McCarthy committed
493
                    alts.append(strc)
494

495
496
        if len(choices) != len(alternates):
            raise ValueError('Alternates are required for every choice')
497

498
        self.__updateChoices(choices, alternates, instance)
499

500

501
    def addChoice(self, choice, alternate=None, instance=None):
502
503
        """Adds a new choice to the list of possible choices."""

504
        if alternate is None: alternate = []
505
        else:                 alternate = list(alternate)
506

507
508
        choices  = list(self.getAttribute(instance, 'choices'))
        altLists = dict(self.getAttribute(instance, 'altLists'))
509

510
        if self.getAttribute(instance, 'allowStr'):
511
512
            strc = str(choice)
            if strc not in alternate:
Paul McCarthy's avatar
Paul McCarthy committed
513
                alternate.append(strc)
514

515
        choices.append(choice)
516
        altLists[choice] = list(alternate)
517

518
        self.__updateChoices(choices, altLists, instance)
519

Paul McCarthy's avatar
Paul McCarthy committed
520

521
522
    def removeChoice(self, choice, instance=None):
        """Removes the specified choice from the list of possible choices. """
Paul McCarthy's avatar
Paul McCarthy committed
523

524
525
        choices   = list(self.getAttribute(instance, 'choices'))
        altLists  = dict(self.getAttribute(instance, 'altLists'))
526
527
528
529

        choices .remove(choice)
        altLists.pop(   choice)

530
        self.__updateChoices(choices, altLists, instance)
531
532
533
534
535
536
537
538
539
540
541


    def __generateAlternatesDict(self, altLists):
        """Given a dictionary containing ``{choice : [alternates]}``
        mappings, creates and returns a dictionary containing
        ``{alternate : choice}`` mappings.

        Raises a ``ValueError`` if there are any duplicate alternate values.
        """

        alternates = {}
542

543
544
545
546
547
548
        for choice, altList in altLists.items():
            for alt in altList:
                if alt in alternates:
                    raise ValueError('Duplicate alternate value '
                                     '(choice: {}): {}'.format(choice, alt))
                alternates[alt] = choice
549

550
        return alternates
551

552

553
    def __updateChoices(self, choices, alternates, instance=None):
554
555
556
557
558
559
560
        """Used by all of the public choice modifying methods. Updates
        all choices, labels, and altenrates.

        :param choices:    A list of choice values

        :param alternates: A dict of ``{choice :  [alternates]}`` mappings.
        """
Paul McCarthy's avatar
Paul McCarthy committed
561

562
563
564
        propVal    = self.getPropVal(  instance)
        default    = self.getAttribute(instance, 'default')
        oldEnabled = self.getAttribute(instance, 'choiceEnabled')
565
566
        newEnabled = {}

567
568
        # Prevent notification while
        # we're updating constraints
569
570
571
572
573
574
575
576
577
578
579
580
581
582
        if propVal is not None:
            oldChoice  = propVal.get()
            notifState = propVal.getNotificationState()
            validState = propVal.allowInvalid()
            propVal.disableNotification()
            propVal.allowInvalid(True)

        for choice in choices:
            if choice in oldEnabled: newEnabled[choice] = oldEnabled[choice]
            else:                    newEnabled[choice] = True

        if default not in choices:
            default = choices[0]

583
584
585
        altLists   = alternates
        alternates = self.__generateAlternatesDict(altLists)

586
587
588
589
590
        self.setAttribute(instance, 'choiceEnabled', newEnabled)
        self.setAttribute(instance, 'altLists',      altLists)
        self.setAttribute(instance, 'alternates',    alternates)
        self.setAttribute(instance, 'choices',       choices)
        self.setAttribute(instance, 'default',       default)
591
592
593
594
595
596
597

        if propVal is not None:

            if oldChoice not in choices:
                if   default in choices: propVal.set(default)
                elif len(choices) > 0:   propVal.set(choices[0])
                else:                    propVal.set(None)
Paul McCarthy's avatar
Paul McCarthy committed
598

599
600
601
602
603
            propVal.setNotificationState(notifState)
            propVal.allowInvalid(        validState)

            if notifState:
                propVal.notifyAttributeListeners('choices', choices)
604

605
            if propVal.get() != oldChoice:
606
                propVal.propNotify()
607

Paul McCarthy's avatar
Paul McCarthy committed
608

609
    def validate(self, instance, attributes, value):
610
611
612
        """Overrides :meth:`.PropertyBase.validate`.

        Raises a :exc:`ValueError` if the given value is not one of the
613
        possible values for this :class:`Choice` property.
Paul McCarthy's avatar
Paul McCarthy committed
614
        """
615
        props.PropertyBase.validate(self, instance, attributes, value)
Paul McCarthy's avatar
Paul McCarthy committed
616

617
618
619
        choices    = self.getAttribute(instance, 'choices')
        enabled    = self.getAttribute(instance, 'choiceEnabled')
        alternates = self.getAttribute(instance, 'alternates')
Paul McCarthy's avatar
Paul McCarthy committed
620

621
622
        if len(choices) == 0: return

Paul McCarthy's avatar
Paul McCarthy committed
623
        # Check to see if this is an
624
625
626
627
        # acceptable alternate value
        altValue = alternates.get(value, None)

        if value not in choices and altValue not in choices:
628
            raise ValueError('Invalid choice ({})'    .format(value))
Paul McCarthy's avatar
Paul McCarthy committed
629

630
        if not enabled.get(value, False):
631
            raise ValueError('Choice is disabled ({})'.format(value))
632
633
634


    def cast(self, instance, attributes, value):
635
636
637
638
        """Overrides :meth:`.PropertyBase.cast`.

        Checks to see if the given value is a valid alternate value for a
        choice. If so, the alternate value is replaced with the choice value.
639
        """
640
        alternates = self.getAttribute(instance, 'alternates')
641
        return alternates.get(value, value)
Paul McCarthy's avatar
Paul McCarthy committed
642

Paul McCarthy's avatar
Paul McCarthy committed
643
644
645


class FilePath(String):
646
647
648
    """A property which represents a file or directory path.

    There is currently no support for validating a path which may be either a
Paul McCarthy's avatar
Paul McCarthy committed
649
    file or a directory - only one or the other.
Paul McCarthy's avatar
Paul McCarthy committed
650
651
    """

652
    def __init__(self, exists=False, isFile=True, suffixes=None, **kwargs):
653
        """Create a ``FilePath`` property.
654
655

        :param bool exists:   If ``True``, the path must exist.
Paul McCarthy's avatar
Paul McCarthy committed
656

657
658
659
660
        :param bool isFile:   If ``True``, the path must be a file. If
                              ``False``, the path must be a directory. This
                              check is only performed if ``exists`` is
                              ``True``.
Paul McCarthy's avatar
Paul McCarthy committed
661

662
663
        :param list suffixes: List of acceptable file suffixes (only relevant
                              if ``isFile`` is ``True``).
Paul McCarthy's avatar
Paul McCarthy committed
664
        """
665
666
        if suffixes is None:
            suffixes = []
Paul McCarthy's avatar
Paul McCarthy committed
667

668
669
        kwargs['exists']   = exists
        kwargs['isFile']   = isFile
670
        kwargs['suffixes'] = list(suffixes)
Paul McCarthy's avatar
Paul McCarthy committed
671

Paul McCarthy's avatar
Paul McCarthy committed
672
673
        String.__init__(self, **kwargs)

Paul McCarthy's avatar
Paul McCarthy committed
674

675
    def validate(self, instance, attributes, value):
676
        """Overrides :meth:`.PropertyBase.validate`.
677

678
        If the ``exists`` attribute is not ``True``, does nothing. Otherwise,
679
680
681
682
683
684
685
        if ``isFile`` is ``False`` and the given value is not a path to an
        existing directory, a :exc:`ValueError` is raised.

        If ``isFile`` is ``True``, and the given value is not a path to an
        existing file (which, if ``suffixes`` is not None, must end in one of
        the specified suffixes), a :exc:`ValueError` is raised.
        """
Paul McCarthy's avatar
Paul McCarthy committed
686

687
        String.validate(self, instance, attributes, value)
Paul McCarthy's avatar
Paul McCarthy committed
688

689
690
691
        exists   = attributes['exists']
        isFile   = attributes['isFile']
        suffixes = attributes['suffixes']
692
693
694
695

        if value is None: return
        if value == '':   return
        if not exists:    return
Paul McCarthy's avatar
Paul McCarthy committed
696

697
        if isFile:
Paul McCarthy's avatar
Paul McCarthy committed
698

699
            matchesSuffix = any([value.endswith(s) for s in suffixes])
Paul McCarthy's avatar
Paul McCarthy committed
700

701
            # If the file doesn't exist, it's bad
702
            if not op.isfile(value):
703
704
705
706
                raise ValueError('Must be a file ({})'.format(value))

            # if the file exists, and matches one of
            # the specified suffixes, then it's good
707
            if len(suffixes) == 0 or matchesSuffix: return
708
709
710
711
712

            # Otherwise it's bad
            else:
                raise ValueError(
                    'Must be a file ending in [{}] ({})'.format(
713
                        ','.join(suffixes), value))
Paul McCarthy's avatar
Paul McCarthy committed
714
715
716
717
718

        elif not op.isdir(value):
            raise ValueError('Must be a directory ({})'.format(value))


719
class List(props.ListPropertyBase):
720
721
    """A property which represents a list of items, of another property type.

722
723
724
    If you use ``List`` properties, you really should read the documentation
    for the :class:`.PropertyValueList`, as it contains important usage
    information.
Paul McCarthy's avatar
Paul McCarthy committed
725
    """
Paul McCarthy's avatar
Paul McCarthy committed
726

727
728
    def __init__(self, listType=None, minlen=None, maxlen=None, **kwargs):
        """Create a ``List`` property.
Paul McCarthy's avatar
Paul McCarthy committed
729

730
731
732
733
        :param listType:   A :class:`.PropertyBase` type, specifying the
                           values allowed in the list. If ``None``, anything
                           can be stored in the list, but no casting or
                           validation will occur.
Paul McCarthy's avatar
Paul McCarthy committed
734

735
        :param int minlen: Minimum list length.
Paul McCarthy's avatar
Paul McCarthy committed
736

737
        :param int maxlen: Maximum list length.
Paul McCarthy's avatar
Paul McCarthy committed
738
739
        """

740
741
        if (listType is not None) and \
           (not isinstance(listType, props.PropertyBase)):
Paul McCarthy's avatar
Paul McCarthy committed
742
743
744
            raise ValueError(
                'A list type (a PropertyBase instance) must be specified')

745
        kwargs['default'] = kwargs.get('default', [])
746
747
        kwargs['minlen']  = minlen
        kwargs['maxlen']  = maxlen
Paul McCarthy's avatar
Paul McCarthy committed
748

749
750
        # This needs to be removed when you update widgets_list.py
        self.embed = False
751

752
        props.ListPropertyBase.__init__(self, listType,  **kwargs)
Paul McCarthy's avatar
Paul McCarthy committed
753
754


755
    def validate(self, instance, attributes, value):
756
        """Overrides :meth:`.PropertyBase.validate`.
Paul McCarthy's avatar
Paul McCarthy committed
757

758
        Checks that the given value (which should be a list) meets the
759
        ``minlen``/``maxlen`` attribute. Raises a :exc:`ValueError` if it
760
        does not.
761
        """
Paul McCarthy's avatar
Paul McCarthy committed
762

763
764
765
766
        props.ListPropertyBase.validate(self, instance, attributes, value)

        minlen = attributes['minlen']
        maxlen = attributes['maxlen']
Paul McCarthy's avatar
Paul McCarthy committed
767

768
        if minlen is not None and len(value) < minlen:
769
            raise ValueError('Must have length at least {}'.format(minlen))
770
        if maxlen is not None and len(value) > maxlen:
771
            raise ValueError('Must have length at most {}'.format(maxlen))
Paul McCarthy's avatar
Paul McCarthy committed
772

773

774
class Colour(props.PropertyBase):
775
    """A property which represents a RGBA colour, stored as four floating
776
    point values in the range ``0.0 - 1.0``.
777

778
779
    Any value which can be interpreted by matplotlib as a RGB(A) colour is
    accepted. If an RGB colour is provided, the alpha channel is set to 1.0.
780
781
    """

Paul McCarthy's avatar
Paul McCarthy committed
782

783
    def __init__(self, **kwargs):
784
        """Create a ``Colour`` property.
785
786
787
788
789

        If the ``default`` ``kwarg`` is not provided, the default is set
        to white.
        """

790
791
792
        default = kwargs.get('default', (1.0, 1.0, 1.0, 1.0))

        if len(default) == 3:
793
            default = list(default) + [1.0]
794
795
796

        kwargs['default'] = default

797
798
799
800
        props.PropertyBase.__init__(self, **kwargs)


    def validate(self, instance, attributes, value):
801
802
        """Checks the given ``value``, and raises a :exc:`ValueError` if
        it does not consist of three or four floating point numbers in the
803
804
805
        range ``(0.0 - 1.0)``.
        """
        props.PropertyBase.validate(self, instance, attributes, value)
806
        mplcolors.to_rgba(value)
Paul McCarthy's avatar
Paul McCarthy committed
807
808


809
    def cast(self, instance, attributes, value):
810
811
        """Ensures that the given ``value`` contains three or four floating
        point numbers, in the range ``(0.0 - 1.0)``.
812
813
814

        If the alpha channel is not provided, it is set to the current alpha
        value (which defaults to ``1.0``).
815
        """
816
817
        if value is not None: return mplcolors.to_rgba(value)
        else:                 return value
818

819

820
class ColourMap(props.PropertyBase):
821
822
    """A property which encapsulates a :class:`matplotlib.colors.Colormap`.

823
824
    A ``ColourMap`` property can take any ``Colormap`` instance as its
    value. ColourMap values may be specified either as a
825
    ``Colormap`` instance, or as a string containing
826
    the name of a registered colour map instance.
827
828
829
830
831

    ``ColourMap`` properties also maintain an internal list of colour
    map names; while these names do not restrict the value that a ``ColourMap``
    property can take, they are used for display purposes - a widget which is
    created for a ``ColourMap`` instance will only display the options returned
832
833
    by the :meth:`getColourMaps` method. See the :func:`widgets._ColourMap`
    function.
834
835
    """

836
    def __init__(self, cmaps=None, **kwargs):
837
        """Define a ``ColourMap`` property. """
838
839
840

        default = kwargs.get('default', None)

841
842
        if cmaps is None:
            cmaps = []
Paul McCarthy's avatar
Paul McCarthy committed
843

844
845
846
847
848
        if default is None and len(cmaps) > 0:
            default = cmaps[0]

        kwargs['default'] = default
        kwargs['cmaps']   = list(cmaps)