Completed
Push — develop ( e1a83c...06a9d5 )
by Kale
01:02
created

Entity.__hash__()   A

Complexity

Conditions 2

Size

Total Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
c 1
b 0
f 0
dl 0
loc 2
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.46)
21
    >>> car
22
    Car(weight=4242.46, 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 '...OrderedDict'>
40
    >>> car.dump()
41
    OrderedDict([('weight', 4242.46), ('wheels', 4), ('color', 0)])
42
43
    >>> # and json too (note the order!)
44
    >>> car.json()
45
    '{"weight": 4242.46, "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.46, "wheels": 4, "color": 1})
88
    True
89
    >>> car == Car.from_json('{"weight": 4242.46, "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.46,
127
          "wheels": 4
128
          "color": 1,
129
        },
130
        {
131
          "weight": 4242.46,
132
          "wheels": 4
133
          "color": 1,
134
        },
135
        {
136
          "weight": 4242.46,
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
152
Chapter 3: Immutability
153
-----------------------
154
155
    >>> class ImmutableCar(ImmutableEntity):
156
    ...     wheels = IntField(default=4, validation=lambda x: 3 <= x <= 4)
157
    ...     color = EnumField(Color)
158
    >>> icar = ImmutableCar.from_objects({'wheels': 3, 'color': 'blue'})
159
    >>> icar
160
    ImmutableCar(wheels=3, color=0)
161
162
    >>> icar.wheels = 4
163
    Traceback (most recent call last):
164
    AttributeError: Assignment not allowed. ImmutableCar is immutable.
165
166
    >>> class FixedWheelCar(Entity):
167
    ...     wheels = IntField(default=4, immutable=True)
168
    ...     color = EnumField(Color)
169
    >>> fwcar = FixedWheelCar.from_objects(icar)
170
    >>> fwcar.json()
171
    '{"wheels": 3, "color": 0}'
172
173
    >>> # repainting the car is easy
174
    >>> fwcar.color = Color.red
175
    >>> fwcar.color.name
176
    'red'
177
178
    >>> # can't really change the number of wheels though
179
    >>> fwcar.wheels = 18
180
    Traceback (most recent call last):
181
    AttributeError: The wheels field is immutable.
182
183
184
Chapter X: The del and null Weeds
185
---------------------------------
186
187
    >>> old_date = lambda: dateparse('1982-02-17')
188
    >>> class CarBattery(Entity):
189
    ...     # NOTE: default value can be a callable!
190
    ...     first_charge = DateField(required=False)  # default=None, nullable=False
191
    ...     latest_charge = DateField(default=old_date, nullable=True)  # required=True
192
    ...     expiration = DateField(default=old_date, required=False, nullable=False)
193
194
    >>> # starting point
195
    >>> battery = CarBattery()
196
    >>> battery
197
    CarBattery()
198
    >>> battery.json()
199
    '{"latest_charge": "1982-02-17T00:00:00", "expiration": "1982-02-17T00:00:00"}'
200
201
    >>> # first_charge is not assigned a default value. Once one is assigned, it can be deleted,
202
    >>> #   but it can't be made null.
203
    >>> battery.first_charge = dateparse('2016-03-23')
204
    >>> battery
205
    CarBattery(first_charge=datetime.datetime(2016, 3, 23, 0, 0))
206
    >>> battery.first_charge = None
207
    Traceback (most recent call last):
208
    ValidationError: Value for first_charge not given or invalid.
209
    >>> del battery.first_charge
210
    >>> battery
211
    CarBattery()
212
213
    >>> # latest_charge can be null, but it can't be deleted. The default value is a callable.
214
    >>> del battery.latest_charge
215
    Traceback (most recent call last):
216
    AttributeError: The latest_charge field is required and cannot be deleted.
217
    >>> battery.latest_charge = None
218
    >>> battery.json()
219
    '{"latest_charge": null, "expiration": "1982-02-17T00:00:00"}'
220
221
    >>> # expiration is assigned by default, can't be made null, but can be deleted.
222
    >>> battery.expiration
223
    datetime.datetime(1982, 2, 17, 0, 0)
224
    >>> battery.expiration = None
225
    Traceback (most recent call last):
226
    ValidationError: Value for expiration not given or invalid.
227
    >>> del battery.expiration
228
    >>> battery.json()
229
    '{"latest_charge": null}'
230
231
232
"""
233
from __future__ import absolute_import, division, print_function
234
235
from functools import reduce
236
237
from collections import Iterable
238
from datetime import datetime
239
from enum import Enum
240
from json import loads as json_loads, dumps as json_dumps
241
from logging import getLogger
242
243
from ._vendor.dateutil.parser import parse as dateparse
244
from .compat import (with_metaclass, string_types, text_type, integer_types, iteritems,
245
                     itervalues, odict)
246
from .collection import AttrDict
247
from .exceptions import ValidationError, Raise
248
from .ish import find_or_none
249
from .logz import DumpEncoder
250
from .type_coercion import maybecall
251
252
log = getLogger(__name__)
253
254
__all__ = [
255
    "Entity", "ImmutableEntity", "Field",
256
    "BooleanField", "BoolField", "IntegerField", "IntField",
257
    "NumberField", "StringField", "DateField",
258
    "EnumField", "ListField", "MapField", "ComposableField",
259
]
260
261
KEY_OVERRIDES_MAP = "__key_overrides__"
262
263
264
NOTES = """
265
This module provides facilities for serializable, validatable, and type-enforcing
266
domain objects.
267
268
This module has many of the same motivations as the python Marshmallow package.
269
<http://marshmallow.readthedocs.org/en/latest/why.html>
270
271
Also need to be explicit in explaining what Marshmallow doesn't do, and why this module is needed.
272
  - Provides type safety like an ORM. And like an ORM, all classes subclass Entity.
273
  - Provides BUILT IN serialization and deserialization.  Marhmallow requires a lot of code
274
    duplication.
275
276
This module gives us:
277
  - type safety
278
  - custom field validation
279
  - serialization and deserialization
280
  - rock solid foundational domain objects
281
282
283
Current deficiencies to schematics:
284
  - no get_mock_object method
285
  - no context-dependent serialization or MultilingualStringType
286
  - name = StringType(serialized_name='person_name', alternate_names=['human_name'])
287
  - name = StringType(serialize_when_none=False)
288
  - more flexible validation error messages
289
  - field validation can depend on other fields
290
  - 'roles' containing blacklists for .dump() and .json()
291
    __roles__ = {
292
        EntityRole.registered_name: Blacklist('field1', 'field2'),
293
        EntityRole.another_registered_name: Whitelist('field3', 'field4'),
294
    }
295
296
297
TODO:
298
  - alternate field names
299
  - add dump_if_null field option
300
  - add help/description parameter to Field
301
302
303
Optional Field Properties:
304
  - validation = None
305
  - default = None
306
  - required = True
307
  - in_dump = True
308
  - nullable = False
309
310
Behaviors:
311
  - Nullable is a "hard" setting, in that the value is either always or never allowed to be None.
312
  - What happens then if required=False and nullable=False?
313
      - The object can be init'd without a value (though not with a None value).
314
        getattr throws AttributeError
315
      - Any assignment must be not None.
316
317
318
  - Setting a value to None doesn't "unset" a value.  (That's what del is for.)  And you can't
319
    del a value if required=True, nullable=False, default=None.
320
321
  - If a field is not required, del does *not* "unmask" the default value.  Instead, del
322
    removes the value from the object entirely.  To get back the default value, need to recreate
323
    the object.  Entity.from_objects(old_object)
324
325
326
  - Disabling in_dump is a "hard" setting, in that with it disabled the field will never get
327
    dumped.  With it enabled, the field may or may not be dumped depending on its value and other
328
    settings.
329
330
  - Required is a "hard" setting, in that if True, a valid value or default must be provided. None
331
    is only a valid value or default if nullable is True.
332
333
  - In general, nullable means that None is a valid value.
334
    - getattr returns None instead of raising Attribute error
335
    - If in_dump, field is given with null value.
336
    - If default is not None, assigning None clears a previous assignment. Future getattrs return
337
      the default value.
338
    - What does nullable mean with default=None and required=True? Does instantiation raise
339
      an error if assignment not made on init? Can IntField(nullable=True) be init'd?
340
341
  - If required=False and nullable=False, field will only be in dump if field!=None.
342
    Also, getattr raises AttributeError.
343
  - If required=False and nullable=True, field will be in dump if field==None.
344
345
  - If in_dump is True, does default value get dumped:
346
    - if no assignment, default exists
347
    - if nullable, and assigned None
348
  - How does optional validation work with nullable and assigning None?
349
  - When does gettattr throw AttributeError, and when does it return None?
350
351
352
353
"""
354
355
356
class Field(object):
357
    """
358
    Fields are doing something very similar to boxing and unboxing
359
    of c#/java primitives.  __set__ should take a "primitive" or "raw" value and create a "boxed"
360
    or "programatically useable" value of it.  While __get__ should return the boxed value,
361
    dump in turn should unbox the value into a primitive or raw value.
362
363
    Arguments:
364
        types_ (primitive literal or type or sequence of types):
365
        default (any, callable, optional):  If default is callable, it's guaranteed to return a
366
            valid value at the time of Entity creation.
367
        required (boolean, optional):
368
        validation (callable, optional):
369
        dump (boolean, optional):
370
    """
371
372
    # Used to track order of field declarations. Supporting python 2.7, so can't rely
373
    #   on __prepare__.  Strategy lifted from http://stackoverflow.com/a/4460034/2127762
374
    _order_helper = 0
375
376
    def __init__(self, default=None, required=True, validation=None, post_validation=None,
377
                 in_dump=True, nullable=False, immutable=False):
378
        self._default = default if callable(default) else self.box(None, default)
379
        self._required = required
380
        self._validation = validation
381
        self._post_validation = post_validation
382
        self._in_dump = in_dump
383
        self._nullable = nullable
384
        self._immutable = immutable
385
        if default is not None:
386
            self.validate(None, self.box(None, maybecall(default)))
387
388
        self._order_helper = Field._order_helper
389
        Field._order_helper += 1
390
391
    @property
392
    def name(self):
393
        try:
394
            return self._name
395
        except AttributeError:
396
            log.error("The name attribute has not been set for this field. "
397
                      "Call set_name at class creation time.")
398
            raise
399
400
    def set_name(self, name):
401
        self._name = name
402
        return self
403
404
    def __get__(self, instance, instance_type):
405
        try:
406
            if instance is None:  # if calling from the class object
407
                val = getattr(instance_type, KEY_OVERRIDES_MAP)[self.name]
408
            else:
409
                val = instance.__dict__[self.name]
410
        except AttributeError:
411
            log.error("The name attribute has not been set for this field.")
412
            raise AttributeError("The name attribute has not been set for this field.")
413
        except KeyError:
414
            if self.default is not None:
415
                val = maybecall(self.default)  # default *can* be a callable
416
            elif self._nullable:
417
                return None
418
            else:
419
                raise AttributeError("A value for {0} has not been set".format(self.name))
420
        if val is None and not self.nullable:
421
            # means the "tricky edge case" was activated in __delete__
422
            raise AttributeError("The {0} field has been deleted.".format(self.name))
423
        return self.post_validate(instance, instance_type,
424
                                  self.unbox(instance, instance_type, val))
425
426
    def __set__(self, instance, val):
427
        if self.immutable and instance._initd:
428
            raise AttributeError("The {0} field is immutable.".format(self.name))
429
        # validate will raise an exception if invalid
430
        # validate will return False if the value should be removed
431
        instance.__dict__[self.name] = self.validate(instance, self.box(instance, val))
432
433
    def __delete__(self, instance):
434
        if self.immutable and instance._initd:
435
            raise AttributeError("The {0} field is immutable.".format(self.name))
436
        elif self.required:
437
            raise AttributeError("The {0} field is required and cannot be deleted."
438
                                 .format(self.name))
439
        elif not self.nullable:
440
            # tricky edge case
441
            # given a field Field(default='some value', required=False, nullable=False)
442
            # works together with Entity.dump() logic for selecting fields to include in dump
443
            # `if value is not None or field.nullable`
444
            instance.__dict__[self.name] = None
445
        else:
446
            instance.__dict__.pop(self.name, None)
447
448
    def box(self, instance, val):
449
        return val
450
451
    def unbox(self, instance, instance_type, val):
452
        return val
453
454
    def dump(self, val):
455
        return val
456
457
    def validate(self, instance, val):
458
        """
459
460
        Returns:
461
            True: if val is valid
462
463
        Raises:
464
            ValidationError
465
        """
466
        # note here calling, but not assigning; could lead to unexpected behavior
467
        if isinstance(val, self._type) and (self._validation is None or self._validation(val)):
468
                return val
469
        elif val is None and self.nullable:
470
            return val
471
        else:
472
            raise ValidationError(getattr(self, 'name', 'undefined name'), val)
473
474
    def post_validate(self, instance, instance_type, val):
475
        """
476
477
        Returns:
478
            True: if val is valid
479
480
        Raises:
481
            ValidationError
482
        """
483
        if self._post_validation is None or not instance._initd or self._post_validation(val):
484
            return val
485
        else:
486
            raise ValidationError(getattr(self, 'name', 'undefined name'), val)
487
488
    @property
489
    def required(self):
490
        return self._required
491
492
    @property
493
    def type(self):
494
        return self._type
495
496
    @property
497
    def default(self):
498
        return self._default
499
500
    @property
501
    def in_dump(self):
502
        return self._in_dump
503
504
    @property
505
    def nullable(self):
506
        return self.is_nullable
507
508
    @property
509
    def is_nullable(self):
510
        return self._nullable
511
512
    @property
513
    def immutable(self):
514
        return self._immutable
515
516
517
class BooleanField(Field):
518
    _type = bool
519
520
    def box(self, instance, val):
521
        return None if val is None else bool(val)
522
523
BoolField = BooleanField
524
525
526
class IntegerField(Field):
527
    _type = integer_types
528
529
IntField = IntegerField
530
531
532
class NumberField(Field):
533
    _type = integer_types + (float, complex)
534
535
536
class StringField(Field):
537
    _type = string_types
538
539
    def box(self, instance, val):
540
        return text_type(val) if isinstance(val, NumberField._type) else val
541
542
543
class DateField(Field):
544
    _type = datetime
545
546
    def box(self, instance, val):
547
        try:
548
            return dateparse(val) if isinstance(val, string_types) else val
549
        except ValueError as e:
550
            raise ValidationError(val, msg=e)
551
552
    def dump(self, val):
553
        return None if val is None else val.isoformat()
554
555
556
class EnumField(Field):
557
558
    def __init__(self, enum_class, default=None, required=True, validation=None,
559
                 post_validation=None, in_dump=True, nullable=False):
560
        if not issubclass(enum_class, Enum):
561
            raise ValidationError(None, msg="enum_class must be an instance of Enum")
562
        self._type = enum_class
563
        super(EnumField, self).__init__(default, required, validation, post_validation,
564
                                        in_dump, nullable)
565
566
    def box(self, instance, val):
567
        if val is None:
568
            # let the required/nullable logic handle validation for this case
569
            return None
570
        try:
571
            # try to box using val as an Enum name
572
            return val if isinstance(val, self._type) else self._type(val)
573
        except ValueError as e1:
574
            try:
575
                # try to box using val as an Enum value
576
                return self._type[val]
577
            except KeyError:
578
                raise ValidationError(val, msg=e1)
579
580
    def dump(self, val):
581
        return None if val is None else val.value
582
583
584
class ListField(Field):
585
    _type = tuple
586
587
    def __init__(self, element_type, default=None, required=True, validation=None,
588
                 post_validation=None, in_dump=True, nullable=False):
589
        self._element_type = element_type
590
        super(ListField, self).__init__(default, required, validation, post_validation,
591
                                        in_dump, nullable)
592
593
    def box(self, instance, val):
594
        if val is None:
595
            return None
596
        elif isinstance(val, string_types):
597
            raise ValidationError("Attempted to assign a string to ListField {0}"
598
                                  "".format(self.name))
599
        elif isinstance(val, Iterable):
600
            et = self._element_type
601
            if isinstance(et, type) and issubclass(et, Entity):
602
                return self._type(v if isinstance(v, et) else et(**v) for v in val)
603
            else:
604
                return self._type(val)
605
        else:
606
            raise ValidationError(val, msg="Cannot assign a non-iterable value to "
607
                                           "{0}".format(self.name))
608
609
    def unbox(self, instance, instance_type, val):
610
        return self._type() if val is None and not self.nullable else val
611
612
    def dump(self, val):
613
        if isinstance(self._element_type, type) and issubclass(self._element_type, Entity):
614
            return self._type(v.dump() for v in val)
615
        else:
616
            return val
617
618
    def validate(self, instance, val):
619
        if val is None:
620
            if not self.nullable:
621
                raise ValidationError(self.name, val)
622
            return None
623
        else:
624
            val = super(ListField, self).validate(instance, val)
625
            et = self._element_type
626
            self._type(Raise(ValidationError(self.name, el, et)) for el in val
627
                       if not isinstance(el, et))
628
            return val
629
630
631
class MutableListField(ListField):
632
    _type = list
633
634
635
class MapField(Field):
636
    _type = dict
637
    __eq__ = dict.__eq__
638
    __hash__ = dict.__hash__
639
640
641
class ComposableField(Field):
642
643
    def __init__(self, field_class, default=None, required=True, validation=None,
644
                 post_validation=None, in_dump=True, nullable=False):
645
        self._type = field_class
646
        super(ComposableField, self).__init__(default, required, validation, post_validation,
647
                                              in_dump, nullable)
648
649
    def box(self, instance, val):
650
        if val is None:
651
            return None
652
        if isinstance(val, self._type):
653
            return val
654
        else:
655
            # assuming val is a dict now
656
            try:
657
                # if there is a key named 'self', have to rename it
658
                val['slf'] = val.pop('self')
659
            except KeyError:
660
                pass  # no key of 'self', so no worries
661
            return val if isinstance(val, self._type) else self._type(**val)
662
663
    def dump(self, val):
664
        return None if val is None else val.dump()
665
666
667
class EntityType(type):
668
669
    @staticmethod
670
    def __get_entity_subclasses(bases):
671
        try:
672
            return [base for base in bases if issubclass(base, Entity) and base is not Entity]
673
        except NameError:
674
            # NameError: global name 'Entity' is not defined
675
            return ()
676
677
    def __new__(mcs, name, bases, dct):
678
        # if we're about to mask a field that's already been created with something that's
679
        #  not a field, then assign it to an alternate variable name
680
        non_field_keys = (key for key, value in iteritems(dct)
681
                          if not isinstance(value, Field) and not key.startswith('__'))
682
        entity_subclasses = EntityType.__get_entity_subclasses(bases)
683
        if entity_subclasses:
684
            keys_to_override = [key for key in non_field_keys
685
                                if any(isinstance(base.__dict__.get(key), Field)
686
                                       for base in entity_subclasses)]
687
            dct[KEY_OVERRIDES_MAP] = dict((key, dct.pop(key)) for key in keys_to_override)
688
        else:
689
            dct[KEY_OVERRIDES_MAP] = dict()
690
691
        return super(EntityType, mcs).__new__(mcs, name, bases, dct)
692
693
    def __init__(cls, name, bases, attr):
694
        super(EntityType, cls).__init__(name, bases, attr)
695
        cls.__fields__ = odict(cls.__fields__) if hasattr(cls, '__fields__') else odict()
696
        cls.__fields__.update(sorted(((name, field.set_name(name))
697
                                      for name, field in iteritems(cls.__dict__)
698
                                      if isinstance(field, Field)),
699
                                     key=lambda item: item[1]._order_helper))
700
        if hasattr(cls, '__register__'):
701
            cls.__register__()
702
703
    def __call__(cls, *args, **kwargs):
704
        instance = super(EntityType, cls).__call__(*args, **kwargs)
705
        setattr(instance, '_{0}__initd'.format(cls.__name__), True)
706
        return instance
707
708
    @property
709
    def fields(cls):
710
        return cls.__fields__.keys()
711
712
713
@with_metaclass(EntityType)
714
class Entity(object):
715
    __fields__ = odict()
716
717
    def __init__(self, **kwargs):
718
        for key, field in iteritems(self.__fields__):
719
            try:
720
                setattr(self, key, kwargs[key])
721
            except KeyError:
722
                # handle the case of fields inherited from subclass but overrode on class object
723
                if key in getattr(self, KEY_OVERRIDES_MAP):
724
                    setattr(self, key, getattr(self, KEY_OVERRIDES_MAP)[key])
725
                elif field.required and field.default is None:
726
                    raise ValidationError(key, msg="{0} requires a {1} field. Instantiated with "
727
                                                   "{2}".format(self.__class__.__name__,
728
                                                                key, kwargs))
729
            except ValidationError:
730
                if kwargs[key] is not None or field.required:
731
                    raise
732
        self.validate()
733
734
    @classmethod
735
    def from_objects(cls, *objects, **override_fields):
736
        init_vars = dict()
737
        search_maps = tuple(AttrDict(o) if isinstance(o, dict) else o
738
                            for o in ((override_fields,) + objects))
739
        for key in cls.__fields__:
740
            init_vars[key] = find_or_none(key, search_maps)
741
        return cls(**init_vars)
742
743
    @classmethod
744
    def from_json(cls, json_str):
745
        return cls(**json_loads(json_str))
746
747
    @classmethod
748
    def load(cls, data_dict):
749
        return cls(**data_dict)
750
751
    def validate(self):
752
        # TODO: here, validate should only have to determine if the required keys are set
753
        try:
754
            reduce(lambda _, name: getattr(self, name),
755
                   (name for name, field in iteritems(self.__fields__) if field.required)
756
                   )
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 itervalues(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