properties_types.py 56.5 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
from collections import abc
37

38
39
import six

40
41
import numpy as np

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

45

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

    def __init__(self, **kwargs):
50
        """Create a ``Object`` property. If an ``equalityFunc`` is not
51
52
53
54
55
56
57
58
59
60
61
        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
62
class Boolean(props.PropertyBase):
63
    """A property which encapsulates a ``bool`` value."""
Paul McCarthy's avatar
Paul McCarthy committed
64
65

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

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

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

Paul McCarthy's avatar
Paul McCarthy committed
75

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


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

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

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

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

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

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

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

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

        if default is None:
112
113
114
115
116
117
            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
118
119
            else:
                default = 0
120

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

Paul McCarthy's avatar
Paul McCarthy committed
127

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

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

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

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

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

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

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

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

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

156

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

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

166
        clamped = attributes['clamped']
Paul McCarthy's avatar
Paul McCarthy committed
167

168
169
        if not clamped: return value

170
171
        minval = attributes['minval']
        maxval = attributes['maxval']
172
173
174
175
176

        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
177
178


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

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

Paul McCarthy's avatar
Paul McCarthy committed
186

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

Paul McCarthy's avatar
Paul McCarthy committed
193

194
class Real(Number):
195
    """A :class:`.Number` which encapsulates a floating point number."""
196

197
198

    def __equals(self, a, b):
199
200
201
202
203
        """Custom equality function passed to :class`.PropertyBase.__init__`.

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

205
206
207
        if any((a is None, b is None, self.__precision is None)):
            return a == b

208
209
        return abs(a - b) < self.__precision

Paul McCarthy's avatar
Paul McCarthy committed
210

211
    def __init__(self, precision=0.000000001, **kwargs):
212
        """Define a ``Real`` property.
213

214
215
216
        :param precision: Tolerance for equality testing. Set to ``None`` to
                          use exact equality.
        """
217
        self.__precision = precision
Paul McCarthy's avatar
Paul McCarthy committed
218

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

221

222
    def cast(self, instance, attributes, value):
223
        """Overrides :meth:`Number.cast`. Casts the given value to a ``float``,
224
        and then passes the value to :meth:`Number.cast`.
Paul McCarthy's avatar
Paul McCarthy committed
225
        """
226
        return Number.cast(self, instance, attributes, float(value))
Paul McCarthy's avatar
Paul McCarthy committed
227

Paul McCarthy's avatar
Paul McCarthy committed
228

229
class Percentage(Real):
230
    """A :class:`Real` property which represents a percentage.
231

232
233
    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
234
235
236
    """

    def __init__(self, **kwargs):
237
        """Create a ``Percentage`` property."""
238
239
240
        kwargs['minval']  = kwargs.get('minval',    0.0)
        kwargs['maxval']  = kwargs.get('maxval',  100.0)
        kwargs['default'] = kwargs.get('default',  50.0)
241
        Real.__init__(self, **kwargs)
Paul McCarthy's avatar
Paul McCarthy committed
242
243
244


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

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

250
251
        :param int minlen: Minimum valid string length.
        :param int maxlen: Maximum valid string length.
Paul McCarthy's avatar
Paul McCarthy committed
252
253
        """

254
        kwargs['default'] = kwargs.get('default', None)
255
256
        kwargs['minlen']  = minlen
        kwargs['maxlen']  = maxlen
257
        props.PropertyBase.__init__(self, **kwargs)
Paul McCarthy's avatar
Paul McCarthy committed
258

259

260
    def cast(self, instance, attributes, value):
261
        """Overrides :meth:`.PropertyBase.cast`.
262
263
264
265

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

267
268
        if value == '': return None
        else:           return value
269

Paul McCarthy's avatar
Paul McCarthy committed
270

271
    def validate(self, instance, attributes, value):
272
        """Overrides :meth:`.PropertyBase.validate`.
273
274

        Passes the given value to
275
        :meth:`.PropertyBase.validate`. Then, if either the
276
        ``minlen`` or ``maxlen`` attributes have been set, and the given
277
278
279
        value has length less than ``minlen`` or greater than ``maxlen``,
        raises a :exc:`ValueError`.
        """
Paul McCarthy's avatar
Paul McCarthy committed
280

281
        if value == '': value = None
Paul McCarthy's avatar
Paul McCarthy committed
282

283
        props.PropertyBase.validate(self, instance, attributes, value)
Paul McCarthy's avatar
Paul McCarthy committed
284
285

        if value is None: return
286

287
        if not isinstance(value, six.string_types):
288
289
            raise ValueError('Must be a string')

290
291
        minlen = attributes['minlen']
        maxlen = attributes['maxlen']
292
293
294

        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
295

296
297
        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
298

Paul McCarthy's avatar
Paul McCarthy committed
299

300
class Choice(props.PropertyBase):
301
    """A property which may only be set to one of a set of predefined values.
302

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

307
    Individual choices can be enabled/disabled via the :meth:`enableChoice`
308
309
310
    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
311

312
313
    A set of alternate values can be provided for each choice - these
    alternates will be accepted when assigning to a ``Choice`` property.
314
315
316
317
318

    .. 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``.
319
    """
320

321
322
323
324
325
    def __init__(self,
                 choices=None,
                 alternates=None,
                 allowStr=False,
                 **kwargs):
326
        """Create a ``Choice`` property.
Paul McCarthy's avatar
Paul McCarthy committed
327

328
        :arg choices:    List of values, the possible values that this property
329
330
                         can take. Can alternately be a ``dict`` - see the note
                         above.
Paul McCarthy's avatar
Paul McCarthy committed
331

332
333
334
335
336
        :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.
337
338
339
340
341

        :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
342
343
344
        """

        if choices is None:
345
346
            choices    = []
            alternates = {}
Paul McCarthy's avatar
Paul McCarthy committed
347

348
        # Alternates are stored twice:
Paul McCarthy's avatar
Paul McCarthy committed
349
        #
350
351
        #   - As a dict of { choice    : [alternate] } mappings
        #   - As a dict of { alternate : choice      } mappings
352
353
        #
        # We generate the first dict here
354
355
356
357
358
359
360
361
362
        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)}

363
364
365
366
367
368
369
370
371
        # 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)

372
373
374
375
376
377
378
        # 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
379

380
381
        if len(choices) > 0: default = choices[0]
        else:                default = None
Paul McCarthy's avatar
Paul McCarthy committed
382

383
384
        if len(choices) != len(altLists):
            raise ValueError('Alternates are required for every choice')
385

386
        kwargs['choices']       = list(choices)
387
388
389
        kwargs['alternates']    = dict(alternates)
        kwargs['altLists']      = dict(altLists)
        kwargs['choiceEnabled'] = enabled
390
        kwargs['allowStr']      = allowStr
391
392
        kwargs['default']       = kwargs.get('default',      default)
        kwargs['allowInvalid']  = kwargs.get('allowInvalid', False)
Paul McCarthy's avatar
Paul McCarthy committed
393

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

396
397
398
399
400
401

    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))

402
        self.setAttribute(instance, 'default', default)
403

Paul McCarthy's avatar
Paul McCarthy committed
404

405
    def enableChoice(self, choice, instance=None):
406
        """Enables the given choice. """
407
        choiceEnabled = dict(self.getAttribute(instance, 'choiceEnabled'))
408
        choiceEnabled[choice] = True
409
        self.setAttribute(instance, 'choiceEnabled', choiceEnabled)
410

Paul McCarthy's avatar
Paul McCarthy committed
411

412
413
    def disableChoice(self, choice, instance=None):
        """Disables the given choice. An attempt to set the property to
414
        a disabled value will result in a :exc:`ValueError`.
415
        """
416
        choiceEnabled = dict(self.getAttribute(instance, 'choiceEnabled'))
417
        choiceEnabled[choice] = False
418
        self.setAttribute(instance, 'choiceEnabled', choiceEnabled)
419
420


421
422
423
424
    def choiceEnabled(self, choice, instance=None):
        """Returns ``True`` if the given choice is enabled, ``False``
        otherwise.
        """
425
        return self.getAttribute(instance, 'choiceEnabled')[choice]
426

Paul McCarthy's avatar
Paul McCarthy committed
427

428
429
    def getChoices(self, instance=None):
        """Returns a list of the current choices. """
430
        return list(self.getAttribute(instance, 'choices'))
431

Paul McCarthy's avatar
Paul McCarthy committed
432

433
434
435
436
    def getAlternates(self, instance=None):
        """Returns a list of the current acceptable alternate values for each
        choice.
        """
437
438
        choices  = self.getAttribute(instance, 'choices')
        altLists = self.getAttribute(instance, 'altLists')
Paul McCarthy's avatar
Paul McCarthy committed
439

440
        return [altLists[c] for c in choices]
441

Paul McCarthy's avatar
Paul McCarthy committed
442

443
444
445
    def updateChoice(self,
                     choice,
                     newChoice=None,
446
                     newAlt=None,
447
                     instance=None):
448
449
        """Updates the choice value and/or alternates for the specified choice.
        """
450

451
452
        choices    = list(self.getAttribute(instance, 'choices'))
        altLists   = dict(self.getAttribute(instance, 'altLists'))
Paul McCarthy's avatar
Paul McCarthy committed
453
        idx        = choices.index(choice)
454

455
456
457
        if newChoice is not None:
            choices[ idx]       = newChoice
            altLists[newChoice] = altLists[choice]
458

459
460
461
            altLists.pop(choice)
        else:
            newChoice = choice
Paul McCarthy's avatar
Paul McCarthy committed
462

463
        if newAlt is not None: altLists[newChoice] = list(newAlt)
464

465
        self.__updateChoices(choices, altLists, instance)
466

467

468
469
470
    def setChoices(self, choices, alternates=None, instance=None):
        """Sets the list of possible choices (and their alternate values, if
        not None).
471
472
        """

473
474
475
476
477
        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):
478
479
480
481
            alternates = dict(alternates)

        # Add stringified versions of all
        # choices if allowStr is True
482
        if self.getAttribute(instance, 'allowStr'):
483
484
485
486
            for c in choices:
                strc = str(c)
                alts = alternates[c]
                if strc not in alts:
Paul McCarthy's avatar
Paul McCarthy committed
487
                    alts.append(strc)
488

489
490
        if len(choices) != len(alternates):
            raise ValueError('Alternates are required for every choice')
491

492
        self.__updateChoices(choices, alternates, instance)
493

494

495
    def addChoice(self, choice, alternate=None, instance=None):
496
497
        """Adds a new choice to the list of possible choices."""

498
        if alternate is None: alternate = []
499
        else:                 alternate = list(alternate)
500

501
502
        choices  = list(self.getAttribute(instance, 'choices'))
        altLists = dict(self.getAttribute(instance, 'altLists'))
503

504
        if self.getAttribute(instance, 'allowStr'):
505
506
            strc = str(choice)
            if strc not in alternate:
Paul McCarthy's avatar
Paul McCarthy committed
507
                alternate.append(strc)
508

509
        choices.append(choice)
510
        altLists[choice] = list(alternate)
511

512
        self.__updateChoices(choices, altLists, instance)
513

Paul McCarthy's avatar
Paul McCarthy committed
514

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

518
519
        choices   = list(self.getAttribute(instance, 'choices'))
        altLists  = dict(self.getAttribute(instance, 'altLists'))
520
521
522
523

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

524
        self.__updateChoices(choices, altLists, instance)
525
526
527
528
529
530
531
532
533
534
535


    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 = {}
536

537
538
539
540
541
542
        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
543

544
        return alternates
545

546

547
    def __updateChoices(self, choices, alternates, instance=None):
548
549
550
551
552
553
554
        """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
555

556
557
558
        propVal    = self.getPropVal(  instance)
        default    = self.getAttribute(instance, 'default')
        oldEnabled = self.getAttribute(instance, 'choiceEnabled')
559
560
        newEnabled = {}

561
562
        # Prevent notification while
        # we're updating constraints
563
564
565
566
567
568
569
570
571
572
573
574
575
576
        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]

577
578
579
        altLists   = alternates
        alternates = self.__generateAlternatesDict(altLists)

580
581
582
583
584
        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)
585
586
587
588
589
590
591

        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
592

593
594
595
596
597
            propVal.setNotificationState(notifState)
            propVal.allowInvalid(        validState)

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

599
            if propVal.get() != oldChoice:
600
                propVal.propNotify()
601

Paul McCarthy's avatar
Paul McCarthy committed
602

603
    def validate(self, instance, attributes, value):
604
605
606
        """Overrides :meth:`.PropertyBase.validate`.

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

611
612
613
        choices    = self.getAttribute(instance, 'choices')
        enabled    = self.getAttribute(instance, 'choiceEnabled')
        alternates = self.getAttribute(instance, 'alternates')
Paul McCarthy's avatar
Paul McCarthy committed
614

615
616
        if len(choices) == 0: return

Paul McCarthy's avatar
Paul McCarthy committed
617
        # Check to see if this is an
618
619
620
621
        # acceptable alternate value
        altValue = alternates.get(value, None)

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

624
        if not enabled.get(value, False):
625
            raise ValueError('Choice is disabled ({})'.format(value))
626
627
628


    def cast(self, instance, attributes, value):
629
630
631
632
        """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.
633
        """
634
        alternates = self.getAttribute(instance, 'alternates')
635
        return alternates.get(value, value)
Paul McCarthy's avatar
Paul McCarthy committed
636

Paul McCarthy's avatar
Paul McCarthy committed
637
638
639


class FilePath(String):
640
641
642
    """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
643
    file or a directory - only one or the other.
Paul McCarthy's avatar
Paul McCarthy committed
644
645
    """

646
    def __init__(self, exists=False, isFile=True, suffixes=None, **kwargs):
647
        """Create a ``FilePath`` property.
648
649

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

651
652
653
654
        :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
655

656
657
        :param list suffixes: List of acceptable file suffixes (only relevant
                              if ``isFile`` is ``True``).
Paul McCarthy's avatar
Paul McCarthy committed
658
        """
659
660
        if suffixes is None:
            suffixes = []
Paul McCarthy's avatar
Paul McCarthy committed
661

662
663
        kwargs['exists']   = exists
        kwargs['isFile']   = isFile
664
        kwargs['suffixes'] = list(suffixes)
Paul McCarthy's avatar
Paul McCarthy committed
665

Paul McCarthy's avatar
Paul McCarthy committed
666
667
        String.__init__(self, **kwargs)

Paul McCarthy's avatar
Paul McCarthy committed
668

669
    def validate(self, instance, attributes, value):
670
        """Overrides :meth:`.PropertyBase.validate`.
671

672
        If the ``exists`` attribute is not ``True``, does nothing. Otherwise,
673
674
675
676
677
678
679
        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
680

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

683
684
685
        exists   = attributes['exists']
        isFile   = attributes['isFile']
        suffixes = attributes['suffixes']
686
687
688
689

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

691
        if isFile:
Paul McCarthy's avatar
Paul McCarthy committed
692

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

695
            # If the file doesn't exist, it's bad
696
            if not op.isfile(value):
697
698
699
700
                raise ValueError('Must be a file ({})'.format(value))

            # if the file exists, and matches one of
            # the specified suffixes, then it's good
701
            if len(suffixes) == 0 or matchesSuffix: return
702
703
704
705
706

            # Otherwise it's bad
            else:
                raise ValueError(
                    'Must be a file ending in [{}] ({})'.format(
707
                        ','.join(suffixes), value))
Paul McCarthy's avatar
Paul McCarthy committed
708
709
710
711
712

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


713
class List(props.ListPropertyBase):
714
715
    """A property which represents a list of items, of another property type.

716
717
718
    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
719
    """
Paul McCarthy's avatar
Paul McCarthy committed
720

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

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

729
        :param int minlen: Minimum list length.
Paul McCarthy's avatar
Paul McCarthy committed
730

731
        :param int maxlen: Maximum list length.
Paul McCarthy's avatar
Paul McCarthy committed
732
733
        """

734
735
        if (listType is not None) and \
           (not isinstance(listType, props.PropertyBase)):
Paul McCarthy's avatar
Paul McCarthy committed
736
737
738
            raise ValueError(
                'A list type (a PropertyBase instance) must be specified')

739
        kwargs['default'] = kwargs.get('default', [])
740
741
        kwargs['minlen']  = minlen
        kwargs['maxlen']  = maxlen
Paul McCarthy's avatar
Paul McCarthy committed
742

743
744
        # This needs to be removed when you update widgets_list.py
        self.embed = False
745

746
        props.ListPropertyBase.__init__(self, listType,  **kwargs)
Paul McCarthy's avatar
Paul McCarthy committed
747
748


749
    def validate(self, instance, attributes, value):
750
        """Overrides :meth:`.PropertyBase.validate`.
Paul McCarthy's avatar
Paul McCarthy committed
751

752
        Checks that the given value (which should be a list) meets the
753
        ``minlen``/``maxlen`` attribute. Raises a :exc:`ValueError` if it
754
        does not.
755
        """
Paul McCarthy's avatar
Paul McCarthy committed
756

757
758
759
760
        props.ListPropertyBase.validate(self, instance, attributes, value)

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

762
        if minlen is not None and len(value) < minlen:
763
            raise ValueError('Must have length at least {}'.format(minlen))
764
        if maxlen is not None and len(value) > maxlen:
765
            raise ValueError('Must have length at most {}'.format(maxlen))
Paul McCarthy's avatar
Paul McCarthy committed
766

767

768
class Colour(props.PropertyBase):
769
    """A property which represents a RGBA colour, stored as four floating
770
    point values in the range ``0.0 - 1.0``.
771
772
773

    RGB colours are also accepted - if an RGB colour is provided, the
    alpha channel is set to 1.0.
774
775
    """

Paul McCarthy's avatar
Paul McCarthy committed
776

777
    def __init__(self, **kwargs):
778
        """Create a ``Colour`` property.
779
780
781
782
783

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

784
785
786
        default = kwargs.get('default', (1.0, 1.0, 1.0, 1.0))

        if len(default) == 3:
787
            default = list(default) + [1.0]
788
789
790

        kwargs['default'] = default

791
792
793
794
        props.PropertyBase.__init__(self, **kwargs)


    def validate(self, instance, attributes, value):
795
796
        """Checks the given ``value``, and raises a :exc:`ValueError` if
        it does not consist of three or four floating point numbers in the
797
798
799
800
        range ``(0.0 - 1.0)``.
        """
        props.PropertyBase.validate(self, instance, attributes, value)

801
        if (not isinstance(value, abc.Sequence)) or \
802
803
           (len(value) not in (3, 4)):
            raise ValueError('Colour must be a sequence of three/four values')
804
805
806
807

        for v in value:
            if (v < 0.0) or (v > 1.0):
                raise ValueError('Colour values must be between 0.0 and 1.0')
Paul McCarthy's avatar
Paul McCarthy committed
808
809


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

        If the alpha channel is not provided, it is set to the current alpha
        value (which defaults to ``1.0``).
816
817
        """

818
        pv = self.getPropVal(instance)
Paul McCarthy's avatar
Paul McCarthy committed
819

820
        if pv is not None: currentVal = pv.get()
821
        else:              currentVal = self.getAttribute(None, 'default')
822

Paul McCarthy's avatar
Paul McCarthy committed
823
        value = [float(v) for v in value]
824
825
826
827
828

        if len(value) == 3:
            value = value + [currentVal[3]]

        value = value[:4]
829
830
831
832

        for i, v in enumerate(value):
            if v < 0.0: value[i] = 0.0
            if v > 1.0: value[i] = 1.0
Paul McCarthy's avatar
Paul McCarthy committed
833

834
835
        return value

836

837
class ColourMap(props.PropertyBase):
838
839
    """A property which encapsulates a :class:`matplotlib.colors.Colormap`.

840
841
    A ``ColourMap`` property can take any ``Colormap`` instance as its
    value. ColourMap values may be specified either as a
842
    ``Colormap`` instance, or as a string containing
843
    the name of a registered colour map instance.