Completed
Push — develop ( 4ef487...23a79f )
by Kale
59s
created

auxlib.Field.is_enum()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 1
dl 0
loc 3
rs 10
1
# -*- coding: utf-8 -*-
2
"""
3
4
Tutorial:
5
    # ## Chapter 1: Entity and Field Basics ##
6
    >>> class Color(Enum):
7
    ...     blue = 0
8
    ...     black = 1
9
    ...     red = 2
10
    >>> class Car(Entity):
11
    ...     weight = NumberField(required=False)
12
    ...     wheels = IntField(default=4, validation=lambda x: 3 <= x <= 4)
13
    ...     color = EnumField(Color)
14
15
    >>> # create a new car object
16
    >>> car = Car(color=Color.blue, weight=4242.42)
17
    >>> car
18
    Car(weight=4242.42, color=0)
19
20
    >>> # it has 4 wheels, all by default
21
    >>> car.wheels
22
    4
23
24
    >>> # but a car can't have 5 wheels!
25
    >>> #  the `validation=` field is a simple callable that returns a boolean based on validity
26
    >>> car.wheels = 5
27
    Traceback (most recent call last):
28
    ValidationError: Invalid value 5 for wheels
29
30
    >>> # we can call .dump() on car, and just get back a standard python dict
31
    >>> #   actually, it's an ordereddict to match attribute declaration order
32
    >>> type(car.dump())
33
    <class 'collections.OrderedDict'>
34
    >>> car.dump()
35
    OrderedDict([('weight', 4242.42), ('wheels', 4), ('color', 0)])
36
37
    >>> # and json too (note the order!)
38
    >>> car.json()
39
    '{"weight": 4242.42, "wheels": 4, "color": 0}'
40
41
    >>> # green cars aren't allowed
42
    >>> car.color = "green"
43
    Traceback (most recent call last):
44
    ValidationError: 'green' is not a valid Color
45
46
    >>> # but black cars are!
47
    >>> car.color = "black"
48
    >>> car.color
49
    <Color.black: 1>
50
51
    >>> # car.color really is an enum, promise
52
    >>> type(car.color)
53
    <enum 'Color'>
54
55
    >>> # enum assignment can be with any of (and preferentially) (1) an enum literal,
56
    >>> #   (2) a valid enum value, or (3) a valid enum name
57
    >>> car.color = Color.blue; car.color.value
58
    0
59
    >>> car.color = 1; car.color.name
60
    'black'
61
62
    >>> # let's do a round-trip marshalling of this thing
63
    >>> same_car = Car.from_json(car.json())  # or equally Car.from_json(json.dumps(car.dump()))
64
    >>> same_car == car
65
    True
66
67
    >>> # actually, they're two different instances
68
    >>> same_car is not car
69
    True
70
71
    >>> # this works too
72
    >>> cloned_car = Car(**car.dump())
73
    >>> cloned_car == car
74
    True
75
76
    >>> # while we're at it, these are all equivalent
77
    >>> Car(**car.dump()) == \
78
            Car.from_objects(car) == \
79
            Car.from_objects({"weight": 4242.42, "wheels": 4, "color": 1}) == \
80
            Car.from_json('{"weight": 4242.42, "color": 1}')
81
    True
82
83
    >>> # .from_objects() even lets you stack and combine objects
84
    >>> class DumbClass:
85
    ...     color = 0
86
    ...     wheels = 3
87
    >>> Car.from_objects(DumbClass(), dict(weight=2222, color=1))
88
    Car(weight=2222, wheels=3, color=0)
89
    >>> # and also pass kwargs that override properties pulled off any objects
90
    >>> Car.from_objects(DumbClass(), {'weight': 2222, 'color': 1}, color=2, weight=33)
91
    Car(weight=33, wheels=3, color=2)
92
93
94
    # ## Chapter 2: Entity and Field Composition ##
95
    >>> # now let's get fancy
96
    >>> # a ComposableField "nests" another valid Entity
97
    >>> # a ListField's first argument is a "generic" type, which can be a valid Entity,
98
    >>> #   any python primitive type, or a list of Entities/types
99
    >>> class Fleet(Entity):
100
    ...     boss_car = ComposableField(Car)
101
    ...     cars = ListField(Car)
102
103
    >>> # here's our fleet of company cars
104
    >>> company_fleet = Fleet(boss_car=Car(color='red'), cars=[car, same_car, cloned_car])
105
    >>> company_fleet.pretty_json()  #doctest: +SKIP
106
    {
107
      "boss_car": {
108
        "wheels": 4
109
        "color": 2,
110
      },
111
      "cars": [
112
        {
113
          "weight": 4242.42,
114
          "wheels": 4
115
          "color": 1,
116
        },
117
        {
118
          "weight": 4242.42,
119
          "wheels": 4
120
          "color": 1,
121
        },
122
        {
123
          "weight": 4242.42,
124
          "wheels": 4
125
          "color": 1,
126
        }
127
      ]
128
    }
129
130
    >>> # the boss' car is red of course (and it's still an Enum)
131
    >>> company_fleet.boss_car.color.name
132
    'red'
133
134
    >>> # and there are three cars left for the employees
135
    >>> len(company_fleet.cars)
136
    3
137
138
    >>> # because we can
139
    >>> sum(c.weight for c in company_fleet.cars)
140
    12727.26
141
142
143
    # ## Chapter 3: Immutability ##
144
    >>> class ImmutableCar(ImmutableEntity):
145
    ...     wheels = IntField(default=4, validation=lambda x: 3 <= x <= 4)
146
    ...     color = EnumField(Color)
147
    >>> icar = ImmutableCar.from_objects({'wheels': 3, 'color': 'blue'})
148
    >>> icar
149
    ImmutableCar(wheels=3, color=0)
150
151
    >>> icar.wheels = 4
152
    Traceback (most recent call last):
153
    AttributeError: Assignment not allowed. ImmutableCar is immutable.
154
155
    >>> del icar.wheels
156
    Traceback (most recent call last):
157
    AttributeError: Deletion not allowed. ImmutableCar is immutable.
158
159
    >>> class FixedWheelCar(Entity):
160
    ...     wheels = IntField(default=4, immutable=True)
161
    ...     color = EnumField(Color)
162
    >>> fwcar = FixedWheelCar.from_objects(icar)
163
    >>> fwcar.json()
164
    '{"wheels": 3, "color": 0}'
165
166
    >>> # repainting the car is easy
167
    >>> fwcar.color = Color.red
168
    >>> fwcar.color.name
169
    'red'
170
171
    >>> # can't really change the number of wheels though
172
    >>> fwcar.wheels = 18
173
    Traceback (most recent call last):
174
    AttributeError: The wheels field is immutable.
175
176
177
    # ## Chapter X: The del and null Weeds ##
178
    >>> old_date = lambda: dateparse('1982-02-17')
179
    >>> class CarBattery(Entity):
180
    ...     # NOTE: default value can be a callable!
181
    ...     first_charge = DateField(required=False)  # default=None, nullable=False
182
    ...     latest_charge = DateField(default=old_date, nullable=True)  # required=True
183
    ...     expiration = DateField(default=old_date, required=False, nullable=False)
184
185
    # starting point
186
    >>> battery = CarBattery()
187
    >>> battery
188
    CarBattery()
189
    >>> battery.json()
190
    '{"latest_charge": "1982-02-17T00:00:00", "expiration": "1982-02-17T00:00:00"}'
191
192
    # first_charge is not assigned a default value. Once one is assigned, it can be deleted,
193
    #   but it can't be made null.
194
    >>> battery.first_charge = dateparse('2016-03-23')
195
    >>> battery
196
    CarBattery(first_charge=datetime.datetime(2016, 3, 23, 0, 0))
197
    >>> battery.first_charge = None
198
    Traceback (most recent call last):
199
    ValidationError: Value for first_charge not given or invalid.
200
    >>> del battery.first_charge
201
    >>> battery
202
    CarBattery()
203
204
    # latest_charge can be null, but it can't be deleted. The default value is a callable.
205
    >>> del battery.latest_charge
206
    Traceback (most recent call last):
207
    AttributeError: The latest_charge field is required and cannot be deleted.
208
    >>> battery.latest_charge = None
209
    >>> battery.json()
210
    '{"latest_charge": null, "expiration": "1982-02-17T00:00:00"}'
211
212
    # expiration is assigned by default, can't be made null, but can be deleted.
213
    >>> battery.expiration
214
    datetime.datetime(1982, 2, 17, 0, 0)
215
    >>> battery.expiration = None
216
    Traceback (most recent call last):
217
    ValidationError: Value for expiration not given or invalid.
218
    >>> del battery.expiration
219
    >>> battery.json()
220
    '{"latest_charge": null}'
221
222
223
"""
224
from __future__ import absolute_import, division, print_function
225
226
from collections import Iterable, OrderedDict as odict
227
from datetime import datetime
228
from enum import Enum
229
from functools import reduce
230
from json import loads as json_loads, dumps as json_dumps
231
from logging import getLogger
232
233
from ._vendor.dateutil.parser import parse as dateparse
234
from ._vendor.five import with_metaclass, items, values
235
from ._vendor.six import integer_types, string_types
236
from .collection import AttrDict
237
from .exceptions import ValidationError, Raise
238
from .ish import find_or_none
239
from .type_coercion import maybecall
240
241
log = getLogger(__name__)
242
243
__all__ = [
244
    "BooleanField", "BoolField", "IntegerField", "IntField",
245
    "NumberField", "StringField", "DateField",
246
    "EnumField", "ListField", "MapField", "ComposableField",
247
    "Entity", "ImmutableEntity",
248
]
249
250
KEY_OVERRIDES_MAP = "__key_overrides__"
251
252
253
NOTES = """
254
This module provides facilities for serializable, validatable, and type-enforcing
255
domain objects.
256
257
This module has many of the same motivations as the python Marshmallow package.
258
<http://marshmallow.readthedocs.org/en/latest/why.html>
259
260
Also need to be explicit in explaining what Marshmallow doesn't do, and why this module is needed.
261
  - Provides type safety like an ORM. And like an ORM, all classes subclass Entity.
262
  - Provides BUILT IN serialization and deserialization.  Marhmallow requires a lot of code
263
    duplication.
264
265
This module gives us:
266
  - type safety
267
  - custom field validation
268
  - serialization and deserialization
269
  - rock solid foundational domain objects
270
271
272
Deficiencies to schematics:
273
  - no get_mock_object method (yet)
274
  - no context-dependent serialization or MultilingualStringType (yet)
275
  - name = StringType(serialized_name='person_name')
276
  - name = StringType(serialize_when_none=False)
277
  - more flexible validation error messages
278
279
280
TODO:
281
  - alternate field names
282
  - add dump_if_null field option
283
284
285
Optional Field Properties:
286
  - validation = None
287
  - default = None
288
  - required = True
289
  - in_dump = True
290
  - nullable = False
291
292
Behaviors:
293
  - Nullable is a "hard" setting, in that the value is either always or never allowed to be None.
294
  - What happens then if required=False and nullable=False?
295
      - The object can be init'd without a value (though not with a None value).
296
        getattr throws AttributeError
297
      - Any assignment must be not None.
298
299
300
  - Setting a value to None doesn't "unset" a value.  (That's what del is for.)  And you can't
301
    del a value if required=True, nullable=False, default=None.
302
303
  - If a field is not required, del does *not* "unmask" the default value.  Instead, del
304
    removes the value from the object entirely.  To get back the default value, need to recreate
305
    the object.  Entity.from_objects(old_object)
306
307
308
  - Disabling in_dump is a "hard" setting, in that with it disabled the field will never get
309
    dumped.  With it enabled, the field may or may not be dumped depending on its value and other
310
    settings.
311
312
  - Required is a "hard" setting, in that if True, a valid value or default must be provided. None
313
    is only a valid value or default if nullable is True.
314
315
  - In general, nullable means that None is a valid value.
316
    - getattr returns None instead of raising Attribute error
317
    - If in_dump, field is given with null value.
318
    - If default is not None, assigning None clears a previous assignment. Future getattrs return
319
      the default value.
320
    - What does nullable mean with default=None and required=True? Does instantiation raise
321
      an error if assignment not made on init? Can IntField(nullable=True) be init'd?
322
323
  - If required=False and nullable=False, field will only be in dump if field!=None.
324
    Also, getattr raises AttributeError.
325
  - If required=False and nullable=True, field will be in dump if field==None.
326
327
  - If in_dump is True, does default value get dumped:
328
    - if no assignment, default exists
329
    - if nullable, and assigned None
330
  - How does optional validation work with nullable and assigning None?
331
  - When does gettattr throw AttributeError, and when does it return None?
332
333
334
335
"""
336
337
338
class Field(object):
339
    """
340
    Fields are doing something very similar to boxing and unboxing
341
    of c#/java primitives.  __set__ should take a "primitive" or "raw" value and create a "boxed"
342
    or "programatically useable" value of it.  While __get__ should return the boxed value,
343
    dump in turn should unbox the value into a primitive or raw value.
344
345
    Arguments:
346
        types_ (primitive literal or type or sequence of types):
347
        default (any, callable, optional):  If default is callable, it's guaranteed to return a
348
            valid value at the time of Entity creation.
349
        required (boolean, optional):
350
        validation (callable, optional):
351
        dump (boolean, optional):
352
    """
353
354
    # Used to track order of field declarations. Supporting python 2.7, so can't rely
355
    #   on __prepare__.  Strategy lifted from http://stackoverflow.com/a/4460034/2127762
356
    _order_helper = 0
357
358
    def __init__(self, default=None, required=True, validation=None, in_dump=True,
359
                 nullable=False, immutable=False):
360
        self._default = default if callable(default) else self.box(default)
361
        self._required = required
362
        self._validation = validation
363
        self._in_dump = in_dump
364
        self._nullable = nullable
365
        self._immutable = immutable
366
        if default is not None:
367
            self.validate(self.box(maybecall(default)))
368
369
        self._order_helper = Field._order_helper
370
        Field._order_helper += 1
371
372
    @property
373
    def name(self):
374
        try:
375
            return self._name
376
        except AttributeError:
377
            log.error("The name attribute has not been set for this field. "
378
                      "Call set_name at class creation time.")
379
            raise
380
381
    def set_name(self, name):
382
        self._name = name
383
        return self
384
385
    def __get__(self, instance, instance_type):
386
        try:
387
            if instance is None:  # if calling from the class object
388
                val = getattr(instance_type, KEY_OVERRIDES_MAP)[self.name]
389
            else:
390
                val = instance.__dict__[self.name]
391
        except AttributeError:
392
            log.error("The name attribute has not been set for this field.")
393
            raise AttributeError("The name attribute has not been set for this field.")
394
        except KeyError:
395
            if self.default is not None:
396
                val = maybecall(self.default)  # default *can* be a callable
397
            elif self._nullable:
398
                return None
399
            else:
400
                raise AttributeError("A value for {0} has not been set".format(self.name))
401
        if val is None and not self.nullable:
402
            # means the "tricky edge case" was activated in __delete__
403
            raise AttributeError("The {0} field has been deleted.".format(self.name))
404
        return self.unbox(val)
405
406
    def __set__(self, instance, val):
407
        if self.immutable and instance._initd:
408
            raise AttributeError("The {0} field is immutable.".format(self.name))
409
        # validate will raise an exception if invalid
410
        # validate will return False if the value should be removed
411
        instance.__dict__[self.name] = self.validate(self.box(val))
412
413
    def __delete__(self, instance):
414
        if self.immutable and instance._initd:
415
            raise AttributeError("The {0} field is immutable.".format(self.name))
416
        elif self.required:
417
            raise AttributeError("The {0} field is required and cannot be deleted."
418
                                 .format(self.name))
419
        elif not self.nullable:
420
            # tricky edge case
421
            # given a field Field(default='some value', required=False, nullable=False)
422
            # works together with Entity.dump() logic for selecting fields to include in dump
423
            # `if value is not None or field.nullable`
424
            instance.__dict__[self.name] = None
425
        else:
426
            instance.__dict__.pop(self.name, None)
427
428
    def box(self, val):
429
        return val
430
431
    def unbox(self, val):
432
        return val
433
434
    def dump(self, val):
435
        return val
436
437
    def validate(self, val):
438
        """
439
440
        Returns:
441
            True: if val is valid
442
443
        Raises:
444
            ValidationError
445
        """
446
        # note here calling, but not assigning; could lead to unexpected behavior
447
        if isinstance(val, self._type) and (self._validation is None or self._validation(val)):
448
                return val
449
        elif val is None and self.nullable:
450
            return val
451
        else:
452
            raise ValidationError(getattr(self, 'name', 'undefined name'), val)
453
454
    @property
455
    def required(self):
456
        return self.is_required
457
458
    @property
459
    def is_required(self):
460
        return self._required
461
462
    @property
463
    def type(self):
464
        return self._type
465
466
    @property
467
    def default(self):
468
        return self._default
469
470
    @property
471
    def in_dump(self):
472
        return self._in_dump
473
474
    @property
475
    def nullable(self):
476
        return self.is_nullable
477
478
    @property
479
    def is_nullable(self):
480
        return self._nullable
481
482
    @property
483
    def immutable(self):
484
        return self._immutable
485
486
487
class BooleanField(Field):
488
    _type = bool
489
490
    def box(self, val):
491
        return None if val is None else bool(val)
492
493
BoolField = BooleanField
494
495
496
class IntegerField(Field):
497
    _type = integer_types
498
499
IntField = IntegerField
500
501
502
class NumberField(Field):
503
    _type = integer_types + (float, complex)
504
505
506
class StringField(Field):
507
    _type = string_types
508
509
510
class DateField(Field):
511
    _type = datetime
512
513
    def box(self, val):
514
        try:
515
            return dateparse(val) if isinstance(val, string_types) else val
516
        except ValueError as e:
517
            raise ValidationError(val, msg=e)
518
519
    def dump(self, val):
520
        return None if val is None else val.isoformat()
521
522
523
class EnumField(Field):
524
525
    def __init__(self, enum_class, default=None, required=True, validation=None,
526
                 in_dump=True, nullable=False):
527
        if not issubclass(enum_class, Enum):
528
            raise ValidationError(None, msg="enum_class must be an instance of Enum")
529
        self._type = enum_class
530
        super(self.__class__, self).__init__(default, required, validation, in_dump, nullable)
531
532
    def box(self, val):
533
        if val is None:
534
            # let the required/nullable logic handle validation for this case
535
            return None
536
        try:
537
            # try to box using val as an Enum name
538
            return val if isinstance(val, self._type) else self._type(val)
539
        except ValueError as e1:
540
            try:
541
                # try to box using val as an Enum value
542
                return self._type[val]
543
            except KeyError:
544
                raise ValidationError(val, msg=e1)
545
546
    def dump(self, val):
547
        return None if val is None else val.value
548
549
550
class ListField(Field):
551
    _type = tuple
552
553
    def __init__(self, element_type, default=None, required=True, validation=None,
554
                 in_dump=True, nullable=False):
555
        self._element_type = element_type
556
        super(self.__class__, self).__init__(default, required, validation, in_dump, nullable)
557
558
    def box(self, val):
559
        if val is None:
560
            return None
561
        elif isinstance(val, string_types):
562
            raise ValidationError("Attempted to assign a string to ListField {0}"
563
                                  "".format(self.name))
564
        elif isinstance(val, Iterable):
565
            et = self._element_type
566
            if isinstance(et, type) and issubclass(et, Entity):
567
                return tuple(v if isinstance(v, et) else et(**v) for v in val)
568
            else:
569
                return tuple(val)
570
        else:
571
            raise ValidationError(val, msg="Cannot assign a non-iterable value to "
572
                                           "{0}".format(self.name))
573
574
    def unbox(self, val):
575
        return tuple() if val is None and not self.nullable else val
576
577
    def dump(self, val):
578
        if isinstance(self._element_type, type) and issubclass(self._element_type, Entity):
579
            return tuple(v.dump() for v in val)
580
        else:
581
            return val
582
583
    def validate(self, val):
584
        if val is None:
585
            if not self.nullable:
586
                raise ValidationError(self.name, val)
587
            return None
588
        else:
589
            val = super(self.__class__, self).validate(val)
590
            et = self._element_type
591
            tuple(Raise(ValidationError(self.name, el, et)) for el in val
592
                  if not isinstance(el, et))
593
            return val
594
595
596
class MapField(Field):
597
    _type = dict
598
    __eq__ = dict.__eq__
599
    __hash__ = dict.__hash__
600
601
602
class ComposableField(Field):
603
604
    def __init__(self, field_class, default=None, required=True, validation=None,
605
                 in_dump=True, nullable=False):
606
        self._type = field_class
607
        super(self.__class__, self).__init__(default, required, validation, in_dump, nullable)
608
609
    def box(self, val):
610
        if val is None:
611
            return None
612
        if isinstance(val, self._type):
613
            return val
614
        else:
615
            # assuming val is a dict now
616
            try:
617
                # if there is a key named 'self', have to rename it
618
                val['slf'] = val.pop('self')
619
            except KeyError:
620
                pass  # no key of 'self', so no worries
621
            return val if isinstance(val, self._type) else self._type(**val)
622
623
    def dump(self, val):
624
        return None if val is None else val.dump()
625
626
627
class EntityType(type):
628
629
    @staticmethod
630
    def __get_entity_subclasses(bases):
631
        try:
632
            return [base for base in bases if issubclass(base, Entity) and base is not Entity]
633
        except NameError:
634
            # NameError: global name 'Entity' is not defined
635
            return []
636
637
    def __new__(mcs, name, bases, dct):
638
        # if we're about to mask a field that's already been created with something that's
639
        #  not a field, then assign it to an alternate variable name
640
        non_field_keys = (key for key, value in items(dct)
641
                          if not isinstance(value, Field) and not key.startswith('__'))
642
        entity_subclasses = EntityType.__get_entity_subclasses(bases)
643
        if entity_subclasses:
644
            keys_to_override = [key for key in non_field_keys
645
                                if any(isinstance(base.__dict__.get(key), Field)
646
                                       for base in entity_subclasses)]
647
            dct[KEY_OVERRIDES_MAP] = {key: dct.pop(key) for key in keys_to_override}
648
        else:
649
            dct[KEY_OVERRIDES_MAP] = dict()
650
651
        return super(EntityType, mcs).__new__(mcs, name, bases, dct)
652
653
    def __init__(cls, name, bases, attr):
654
        super(EntityType, cls).__init__(name, bases, attr)
655
        cls.__fields__ = odict(cls.__fields__) if hasattr(cls, '__fields__') else odict()
656
        cls.__fields__.update(sorted(((name, field.set_name(name))
657
                                      for name, field in cls.__dict__.items()
658
                                      if isinstance(field, Field)),
659
                                     key=lambda item: item[1]._order_helper))
660
        if hasattr(cls, '__register__'):
661
            cls.__register__()
662
663
    def __call__(cls, *args, **kwargs):
664
        instance = super(EntityType, cls).__call__(*args, **kwargs)
665
        setattr(instance, '_{0}__initd'.format(cls.__name__), True)
666
        return instance
667
668
    @property
669
    def fields(cls):
670
        return cls.__fields__.keys()
671
672
673
@with_metaclass(EntityType)
674
class Entity(object):
675
    __fields__ = odict()
676
677
    def __init__(self, **kwargs):
678
        for key, field in items(self.__fields__):
679
            try:
680
                setattr(self, key, kwargs[key])
681
            except KeyError:
682
                # handle the case of fields inherited from subclass but overrode on class object
683
                if key in getattr(self, KEY_OVERRIDES_MAP):
684
                    setattr(self, key, getattr(self, KEY_OVERRIDES_MAP)[key])
685
                elif field.required and field.default is None:
686
                    raise ValidationError(key, msg="{0} requires a {1} field. Instantiated with "
687
                                                   "{2}".format(self.__class__.__name__,
688
                                                                key, kwargs))
689
            except ValidationError:
690
                if kwargs[key] is not None or field.required:
691
                    raise
692
        self.validate()
693
694
    @classmethod
695
    def from_objects(cls, *objects, **override_fields):
696
        init_vars = dict()
697
        search_maps = tuple(AttrDict(o) if isinstance(o, dict) else o
698
                            for o in ((override_fields,) + objects))
699
        for key in cls.__fields__:
700
            init_vars[key] = find_or_none(key, search_maps)
701
        return cls(**init_vars)
702
703
    create_from_objects = from_objects  # for backward compatibility; deprecated
704
705
    @classmethod
706
    def from_json(cls, json_str):
707
        return cls(**json_loads(json_str))
708
709
    @classmethod
710
    def load(cls, data_dict):
711
        return cls(**data_dict)
712
713
    def validate(self):
714
        # TODO: here, validate should only have to determine if the required keys are set
715
        try:
716
            reduce(lambda x, y: y, (getattr(self, name)
717
                                    for name, field in self.__fields__.items()
718
                                    if field.required))
719
        except AttributeError as e:
720
            raise ValidationError(None, msg=e)
721
        except TypeError as e:
722
            if "no initial value" in str(e):
723
                # TypeError: reduce() of empty sequence with no initial value
724
                pass
725
            else:
726
                raise  # pragma: no cover
727
728
    def __repr__(self):
729
        def _valid(key):
730
            if key.startswith('_'):
731
                return False
732
            try:
733
                getattr(self, key)
734
                return True
735
            except AttributeError:
736
                return False
737
738
        def _val(key):
739
            val = getattr(self, key)
740
            return repr(val.value) if isinstance(val, Enum) else repr(val)
741
742
        def _sort_helper(key):
743
            field = self.__fields__.get(key)
744
            return field._order_helper if field is not None else -1
745
746
        kwarg_str = ", ".join("{0}={1}".format(key, _val(key))
747
                              for key in sorted(self.__dict__, key=_sort_helper)
748
                              if _valid(key))
749
        return "{0}({1})".format(self.__class__.__name__, kwarg_str)
750
751
    @classmethod
752
    def __register__(cls):
753
        pass
754
755
    def json(self, indent=None, separators=None, **kwargs):
756
        return json_dumps(self.dump(), indent=indent, separators=separators, **kwargs)
757
758
    def pretty_json(self, indent=2, separators=(',', ': '), **kwargs):
759
        return self.json(indent=indent, separators=separators, **kwargs)
760
761
    def dump(self):
762
        return odict((field.name, field.dump(value))
763
                     for field, value in ((field, getattr(self, field.name, None))
764
                                          for field in self.__dump_fields())
765
                     if value is not None or field.nullable)
766
767
    @classmethod
768
    def __dump_fields(cls):
769
        if '__dump_fields_cache' not in cls.__dict__:
770
            cls.__dump_fields_cache = tuple(field for field in values(cls.__fields__)
771
                                            if field.in_dump)
772
        return cls.__dump_fields_cache
773
774
    def __eq__(self, other):
775
        if self.__class__ != other.__class__:
776
            return False
777
        rando_default = 19274656290  # need an arbitrary but definite value if field does not exist
778
        return all(getattr(self, field, rando_default) == getattr(other, field, rando_default)
779
                   for field in self.__fields__)
780
781
    def __hash__(self):
782
        return sum(hash(getattr(self, field, None)) for field in self.__fields__)
783
784
    @property
785
    def _initd(self):
786
        return getattr(self, '_{0}__initd'.format(self.__class__.__name__), None)
787
788
789
class ImmutableEntity(Entity):
790
791
    def __setattr__(self, attribute, value):
792
        if self._initd:
793
            raise AttributeError("Assignment not allowed. {0} is immutable."
794
                                 .format(self.__class__.__name__))
795
        super(ImmutableEntity, self).__setattr__(attribute, value)
796
797
    def __delattr__(self, item):
798
        if self._initd:
799
            raise AttributeError("Deletion not allowed. {0} is immutable."
800
                                 .format(self.__class__.__name__))
801
        super(ImmutableEntity, self).__delattr__(item)
802