Completed
Push — develop ( 3606a5...c5328e )
by Kale
01:06
created

Entity._val()   A

Complexity

Conditions 2

Size

Total Lines 3

Duplication

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