properties_types.py 55.9 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 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
        clamped = attributes['clamped']
Paul McCarthy's avatar
Paul McCarthy committed
166

167
168
        if not clamped: return value

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

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


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

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

Paul McCarthy's avatar
Paul McCarthy committed
185

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

Paul McCarthy's avatar
Paul McCarthy committed
192

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

196
197

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

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

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

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

Paul McCarthy's avatar
Paul McCarthy committed
209

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

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

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

220

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

Paul McCarthy's avatar
Paul McCarthy committed
227

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

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

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


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

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

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

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

258

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

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

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

Paul McCarthy's avatar
Paul McCarthy committed
269

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

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

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

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

        if value is None: return
285

Paul McCarthy's avatar
Paul McCarthy committed
286
        if not isinstance(value, str):
287
288
            raise ValueError('Must be a string')

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

        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
294

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

Paul McCarthy's avatar
Paul McCarthy committed
298

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

395
396
397
398
399
400

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

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

Paul McCarthy's avatar
Paul McCarthy committed
403

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

Paul McCarthy's avatar
Paul McCarthy committed
410

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


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

Paul McCarthy's avatar
Paul McCarthy committed
426

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

Paul McCarthy's avatar
Paul McCarthy committed
431

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

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

Paul McCarthy's avatar
Paul McCarthy committed
441

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

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

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

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

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

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

466

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

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

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

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

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

493

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

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

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

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

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

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

Paul McCarthy's avatar
Paul McCarthy committed
513

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

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

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

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


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

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

543
        return alternates
544

545

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

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

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

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

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

        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
591

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

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

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

Paul McCarthy's avatar
Paul McCarthy committed
601

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

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

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

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

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

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

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


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

Paul McCarthy's avatar
Paul McCarthy committed
636
637
638


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

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

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

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

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

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

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

Paul McCarthy's avatar
Paul McCarthy committed
667

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

766

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

771
772
    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.
773
774
    """

Paul McCarthy's avatar
Paul McCarthy committed
775

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

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

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

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

        kwargs['default'] = default

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


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


802
    def cast(self, instance, attributes, value):
803
804
        """Ensures that the given ``value`` contains three or four floating
        point numbers, in the range ``(0.0 - 1.0)``.
805
806
807

        If the alpha channel is not provided, it is set to the current alpha
        value (which defaults to ``1.0``).
808
        """
809
810
        if value is not None: return mplcolors.to_rgba(value)
        else:                 return value
811

812

813
class ColourMap(props.PropertyBase):
814
815
    """A property which encapsulates a :class:`matplotlib.colors.Colormap`.

816
817
    A ``ColourMap`` property can take any ``Colormap`` instance as its
    value. ColourMap values may be specified either as a
818
    ``Colormap`` instance, or as a string containing
819
    the name of a registered colour map instance.
820
821
822
823
824

    ``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
825
826
    by the :meth:`getColourMaps` method. See the :func:`widgets._ColourMap`
    function.
827
828
    """

829
    def __init__(self, cmaps=None, **kwargs):
830
        """Define a ``ColourMap`` property. """
831
832
833

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

834
835
        if cmaps is None:
            cmaps = []
Paul McCarthy's avatar
Paul McCarthy committed
836

837
838
839
840
841
        if default is None and len(cmaps) > 0:
            default = cmaps[0]

        kwargs['default'] = default
        kwargs['cmaps']   = list(cmaps)
Paul McCarthy's avatar
Paul McCarthy committed
842

843
844
        props.PropertyBase.__init__(self, **kwargs)

Paul McCarthy's avatar
Paul McCarthy committed
845