Completed
Push — develop ( 891ee6...5661c9 )
by Kale
57s
created

auxlib.StringField.box()   A

Complexity

Conditions 2

Size

Total Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 2
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.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 functools import reduce
243
from json import loads as json_loads, dumps as json_dumps
244
from logging import getLogger
245
246
from ._vendor.dateutil.parser import parse as dateparse
247
from ._vendor.five import with_metaclass, items, values
248
from ._vendor.six import string_types, text_type, integer_types
249
from .collection import AttrDict
250
from .exceptions import ValidationError, Raise
251
from .ish import find_or_none
252
from .logz import DumpEncoder
253
from .type_coercion import maybecall
254
255
log = getLogger(__name__)
256
257
__all__ = [
258
    "Entity", "ImmutableEntity", "Field",
259
    "BooleanField", "BoolField", "IntegerField", "IntField",
260
    "NumberField", "StringField", "DateField",
261
    "EnumField", "ListField", "MapField", "ComposableField",
262
]
263
264
KEY_OVERRIDES_MAP = "__key_overrides__"
265
266
267
NOTES = """
268
This module provides facilities for serializable, validatable, and type-enforcing
269
domain objects.
270
271
This module has many of the same motivations as the python Marshmallow package.
272
<http://marshmallow.readthedocs.org/en/latest/why.html>
273
274
Also need to be explicit in explaining what Marshmallow doesn't do, and why this module is needed.
275
  - Provides type safety like an ORM. And like an ORM, all classes subclass Entity.
276
  - Provides BUILT IN serialization and deserialization.  Marhmallow requires a lot of code
277
    duplication.
278
279
This module gives us:
280
  - type safety
281
  - custom field validation
282
  - serialization and deserialization
283
  - rock solid foundational domain objects
284
285
286
Current deficiencies to schematics:
287
  - no get_mock_object method
288
  - no context-dependent serialization or MultilingualStringType
289
  - name = StringType(serialized_name='person_name')
290
  - name = StringType(serialize_when_none=False)
291
  - more flexible validation error messages
292
  - field validation can depend on other fields
293
  - 'roles' containing blacklists for .dump() and .json()
294
295
296
TODO:
297
  - alternate field names
298
  - add dump_if_null field option
299
  - add help/description parameter to Field
300
301
302
Optional Field Properties:
303
  - validation = None
304
  - default = None
305
  - required = True
306
  - in_dump = True
307
  - nullable = False
308
309
Behaviors:
310
  - Nullable is a "hard" setting, in that the value is either always or never allowed to be None.
311
  - What happens then if required=False and nullable=False?
312
      - The object can be init'd without a value (though not with a None value).
313
        getattr throws AttributeError
314
      - Any assignment must be not None.
315
316
317
  - Setting a value to None doesn't "unset" a value.  (That's what del is for.)  And you can't
318
    del a value if required=True, nullable=False, default=None.
319
320
  - If a field is not required, del does *not* "unmask" the default value.  Instead, del
321
    removes the value from the object entirely.  To get back the default value, need to recreate
322
    the object.  Entity.from_objects(old_object)
323
324
325
  - Disabling in_dump is a "hard" setting, in that with it disabled the field will never get
326
    dumped.  With it enabled, the field may or may not be dumped depending on its value and other
327
    settings.
328
329
  - Required is a "hard" setting, in that if True, a valid value or default must be provided. None
330
    is only a valid value or default if nullable is True.
331
332
  - In general, nullable means that None is a valid value.
333
    - getattr returns None instead of raising Attribute error
334
    - If in_dump, field is given with null value.
335
    - If default is not None, assigning None clears a previous assignment. Future getattrs return
336
      the default value.
337
    - What does nullable mean with default=None and required=True? Does instantiation raise
338
      an error if assignment not made on init? Can IntField(nullable=True) be init'd?
339
340
  - If required=False and nullable=False, field will only be in dump if field!=None.
341
    Also, getattr raises AttributeError.
342
  - If required=False and nullable=True, field will be in dump if field==None.
343
344
  - If in_dump is True, does default value get dumped:
345
    - if no assignment, default exists
346
    - if nullable, and assigned None
347
  - How does optional validation work with nullable and assigning None?
348
  - When does gettattr throw AttributeError, and when does it return None?
349
350
351
352
"""
353
354
355
class Field(object):
356
    """
357
    Fields are doing something very similar to boxing and unboxing
358
    of c#/java primitives.  __set__ should take a "primitive" or "raw" value and create a "boxed"
359
    or "programatically useable" value of it.  While __get__ should return the boxed value,
360
    dump in turn should unbox the value into a primitive or raw value.
361
362
    Arguments:
363
        types_ (primitive literal or type or sequence of types):
364
        default (any, callable, optional):  If default is callable, it's guaranteed to return a
365
            valid value at the time of Entity creation.
366
        required (boolean, optional):
367
        validation (callable, optional):
368
        dump (boolean, optional):
369
    """
370
371
    # Used to track order of field declarations. Supporting python 2.7, so can't rely
372
    #   on __prepare__.  Strategy lifted from http://stackoverflow.com/a/4460034/2127762
373
    _order_helper = 0
374
375
    def __init__(self, default=None, required=True, validation=None, post_validation=None,
376
                 in_dump=True, nullable=False, immutable=False):
377
        self._default = default if callable(default) else self.box(None, default)
378
        self._required = required
379
        self._validation = validation
380
        self._post_validation = post_validation
381
        self._in_dump = in_dump
382
        self._nullable = nullable
383
        self._immutable = immutable
384
        if default is not None:
385
            self.validate(self.box(None, maybecall(default)))
386
387
        self._order_helper = Field._order_helper
388
        Field._order_helper += 1
389
390
    @property
391
    def name(self):
392
        try:
393
            return self._name
394
        except AttributeError:
395
            log.error("The name attribute has not been set for this field. "
396
                      "Call set_name at class creation time.")
397
            raise
398
399
    def set_name(self, name):
400
        self._name = name
401
        return self
402
403
    def __get__(self, instance, instance_type):
404
        try:
405
            if instance is None:  # if calling from the class object
406
                val = getattr(instance_type, KEY_OVERRIDES_MAP)[self.name]
407
            else:
408
                val = instance.__dict__[self.name]
409
        except AttributeError:
410
            log.error("The name attribute has not been set for this field.")
411
            raise AttributeError("The name attribute has not been set for this field.")
412
        except KeyError:
413
            if self.default is not None:
414
                val = maybecall(self.default)  # default *can* be a callable
415
            elif self._nullable:
416
                return None
417
            else:
418
                raise AttributeError("A value for {0} has not been set".format(self.name))
419
        if val is None and not self.nullable:
420
            # means the "tricky edge case" was activated in __delete__
421
            raise AttributeError("The {0} field has been deleted.".format(self.name))
422
        return self.post_validate(self.unbox(instance, instance_type, val))
423
424
    def __set__(self, instance, val):
425
        if self.immutable and instance._initd:
426
            raise AttributeError("The {0} field is immutable.".format(self.name))
427
        # validate will raise an exception if invalid
428
        # validate will return False if the value should be removed
429
        instance.__dict__[self.name] = self.validate(self.box(instance, val))
430
431
    def __delete__(self, instance):
432
        if self.immutable and instance._initd:
433
            raise AttributeError("The {0} field is immutable.".format(self.name))
434
        elif self.required:
435
            raise AttributeError("The {0} field is required and cannot be deleted."
436
                                 .format(self.name))
437
        elif not self.nullable:
438
            # tricky edge case
439
            # given a field Field(default='some value', required=False, nullable=False)
440
            # works together with Entity.dump() logic for selecting fields to include in dump
441
            # `if value is not None or field.nullable`
442
            instance.__dict__[self.name] = None
443
        else:
444
            instance.__dict__.pop(self.name, None)
445
446
    def box(self, instance, val):
447
        return val
448
449
    def unbox(self, instance, instance_type, val):
450
        return val
451
452
    def dump(self, val):
453
        return val
454
455
    def validate(self, val):
456
        """
457
458
        Returns:
459
            True: if val is valid
460
461
        Raises:
462
            ValidationError
463
        """
464
        # note here calling, but not assigning; could lead to unexpected behavior
465
        if isinstance(val, self._type) and (self._validation is None or self._validation(val)):
466
                return val
467
        elif val is None and self.nullable:
468
            return val
469
        else:
470
            raise ValidationError(getattr(self, 'name', 'undefined name'), val)
471
472
    def post_validate(self, val):
473
        """
474
475
        Returns:
476
            True: if val is valid
477
478
        Raises:
479
            ValidationError
480
        """
481
        if self._post_validation is None or self._post_validation(val):
482
            return val
483
        else:
484
            raise ValidationError(getattr(self, 'name', 'undefined name'), val)
485
486
    @property
487
    def required(self):
488
        return self._required
489
490
    @property
491
    def type(self):
492
        return self._type
493
494
    @property
495
    def default(self):
496
        return self._default
497
498
    @property
499
    def in_dump(self):
500
        return self._in_dump
501
502
    @property
503
    def nullable(self):
504
        return self.is_nullable
505
506
    @property
507
    def is_nullable(self):
508
        return self._nullable
509
510
    @property
511
    def immutable(self):
512
        return self._immutable
513
514
515
class BooleanField(Field):
516
    _type = bool
517
518
    def box(self, instance, val):
519
        return None if val is None else bool(val)
520
521
BoolField = BooleanField
522
523
524
class IntegerField(Field):
525
    _type = integer_types
526
527
IntField = IntegerField
528
529
530
class NumberField(Field):
531
    _type = integer_types + (float, complex)
532
533
534
class StringField(Field):
535
    _type = string_types
536
537
    def box(self, instance, val):
538
        return text_type(val) if isinstance(val, NumberField._type) else val
539
540
541
class DateField(Field):
542
    _type = datetime
543
544
    def box(self, instance, val):
545
        try:
546
            return dateparse(val) if isinstance(val, string_types) else val
547
        except ValueError as e:
548
            raise ValidationError(val, msg=e)
549
550
    def dump(self, val):
551
        return None if val is None else val.isoformat()
552
553
554
class EnumField(Field):
555
556
    def __init__(self, enum_class, default=None, required=True, validation=None,
557
                 post_validation=None, in_dump=True, nullable=False):
558
        if not issubclass(enum_class, Enum):
559
            raise ValidationError(None, msg="enum_class must be an instance of Enum")
560
        self._type = enum_class
561
        super(self.__class__, self).__init__(default, required, validation, post_validation,
562
                                             in_dump, nullable)
563
564
    def box(self, instance, val):
565
        if val is None:
566
            # let the required/nullable logic handle validation for this case
567
            return None
568
        try:
569
            # try to box using val as an Enum name
570
            return val if isinstance(val, self._type) else self._type(val)
571
        except ValueError as e1:
572
            try:
573
                # try to box using val as an Enum value
574
                return self._type[val]
575
            except KeyError:
576
                raise ValidationError(val, msg=e1)
577
578
    def dump(self, val):
579
        return None if val is None else val.value
580
581
582
class ListField(Field):
583
    _type = tuple
584
585
    def __init__(self, element_type, default=None, required=True, validation=None,
586
                 post_validation=None, in_dump=True, nullable=False):
587
        self._element_type = element_type
588
        super(self.__class__, self).__init__(default, required, validation, post_validation,
589
                                             in_dump, nullable)
590
591
    def box(self, instance, val):
592
        if val is None:
593
            return None
594
        elif isinstance(val, string_types):
595
            raise ValidationError("Attempted to assign a string to ListField {0}"
596
                                  "".format(self.name))
597
        elif isinstance(val, Iterable):
598
            et = self._element_type
599
            if isinstance(et, type) and issubclass(et, Entity):
600
                return self._type(v if isinstance(v, et) else et(**v) for v in val)
601
            else:
602
                return self._type(val)
603
        else:
604
            raise ValidationError(val, msg="Cannot assign a non-iterable value to "
605
                                           "{0}".format(self.name))
606
607
    def unbox(self, instance, instance_type, val):
608
        return self._type() if val is None and not self.nullable else val
609
610
    def dump(self, val):
611
        if isinstance(self._element_type, type) and issubclass(self._element_type, Entity):
612
            return self._type(v.dump() for v in val)
613
        else:
614
            return val
615
616
    def validate(self, val):
617
        if val is None:
618
            if not self.nullable:
619
                raise ValidationError(self.name, val)
620
            return None
621
        else:
622
            val = super(self.__class__, self).validate(val)
623
            et = self._element_type
624
            self._type(Raise(ValidationError(self.name, el, et)) for el in val
625
                       if not isinstance(el, et))
626
            return val
627
628
629
class MutableListField(ListField):
630
    _type = list
631
632
633
class MapField(Field):
634
    _type = dict
635
    __eq__ = dict.__eq__
636
    __hash__ = dict.__hash__
637
638
639
class ComposableField(Field):
640
641
    def __init__(self, field_class, default=None, required=True, validation=None,
642
                 post_validation=None, in_dump=True, nullable=False):
643
        self._type = field_class
644
        super(self.__class__, self).__init__(default, required, validation, post_validation,
645
                                             in_dump, nullable)
646
647
    def box(self, instance, val):
648
        if val is None:
649
            return None
650
        if isinstance(val, self._type):
651
            return val
652
        else:
653
            # assuming val is a dict now
654
            try:
655
                # if there is a key named 'self', have to rename it
656
                val['slf'] = val.pop('self')
657
            except KeyError:
658
                pass  # no key of 'self', so no worries
659
            return val if isinstance(val, self._type) else self._type(**val)
660
661
    def dump(self, val):
662
        return None if val is None else val.dump()
663
664
665
class EntityType(type):
666
667
    @staticmethod
668
    def __get_entity_subclasses(bases):
669
        try:
670
            return [base for base in bases if issubclass(base, Entity) and base is not Entity]
671
        except NameError:
672
            # NameError: global name 'Entity' is not defined
673
            return ()
674
675
    def __new__(mcs, name, bases, dct):
676
        # if we're about to mask a field that's already been created with something that's
677
        #  not a field, then assign it to an alternate variable name
678
        non_field_keys = (key for key, value in items(dct)
679
                          if not isinstance(value, Field) and not key.startswith('__'))
680
        entity_subclasses = EntityType.__get_entity_subclasses(bases)
681
        if entity_subclasses:
682
            keys_to_override = [key for key in non_field_keys
683
                                if any(isinstance(base.__dict__.get(key), Field)
684
                                       for base in entity_subclasses)]
685
            dct[KEY_OVERRIDES_MAP] = {key: dct.pop(key) for key in keys_to_override}
686
        else:
687
            dct[KEY_OVERRIDES_MAP] = dict()
688
689
        return super(EntityType, mcs).__new__(mcs, name, bases, dct)
690
691
    def __init__(cls, name, bases, attr):
692
        super(EntityType, cls).__init__(name, bases, attr)
693
        cls.__fields__ = odict(cls.__fields__) if hasattr(cls, '__fields__') else odict()
694
        cls.__fields__.update(sorted(((name, field.set_name(name))
695
                                      for name, field in cls.__dict__.items()
696
                                      if isinstance(field, Field)),
697
                                     key=lambda item: item[1]._order_helper))
698
        if hasattr(cls, '__register__'):
699
            cls.__register__()
700
701
    def __call__(cls, *args, **kwargs):
702
        instance = super(EntityType, cls).__call__(*args, **kwargs)
703
        setattr(instance, '_{0}__initd'.format(cls.__name__), True)
704
        return instance
705
706
    @property
707
    def fields(cls):
708
        return cls.__fields__.keys()
709
710
711
@with_metaclass(EntityType)
712
class Entity(object):
713
    __fields__ = odict()
714
715
    def __init__(self, **kwargs):
716
        for key, field in items(self.__fields__):
717
            try:
718
                setattr(self, key, kwargs[key])
719
            except KeyError:
720
                # handle the case of fields inherited from subclass but overrode on class object
721
                if key in getattr(self, KEY_OVERRIDES_MAP):
722
                    setattr(self, key, getattr(self, KEY_OVERRIDES_MAP)[key])
723
                elif field.required and field.default is None:
724
                    raise ValidationError(key, msg="{0} requires a {1} field. Instantiated with "
725
                                                   "{2}".format(self.__class__.__name__,
726
                                                                key, kwargs))
727
            except ValidationError:
728
                if kwargs[key] is not None or field.required:
729
                    raise
730
        self.validate()
731
732
    @classmethod
733
    def from_objects(cls, *objects, **override_fields):
734
        init_vars = dict()
735
        search_maps = tuple(AttrDict(o) if isinstance(o, dict) else o
736
                            for o in ((override_fields,) + objects))
737
        for key in cls.__fields__:
738
            init_vars[key] = find_or_none(key, search_maps)
739
        return cls(**init_vars)
740
741
    create_from_objects = from_objects  # for backward compatibility; deprecated
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 x, y: y, (getattr(self, name)
755
                                    for name, field in self.__fields__.items()
756
                                    if field.required))
757
        except AttributeError as e:
758
            raise ValidationError(None, msg=e)
759
        except TypeError as e:
760
            if "no initial value" in str(e):
761
                # TypeError: reduce() of empty sequence with no initial value
762
                pass
763
            else:
764
                raise  # pragma: no cover
765
766
    def __repr__(self):
767
        def _valid(key):
768
            if key.startswith('_'):
769
                return False
770
            try:
771
                getattr(self, key)
772
                return True
773
            except AttributeError:
774
                return False
775
776
        def _val(key):
777
            val = getattr(self, key)
778
            return repr(val.value) if isinstance(val, Enum) else repr(val)
779
780
        def _sort_helper(key):
781
            field = self.__fields__.get(key)
782
            return field._order_helper if field is not None else -1
783
784
        kwarg_str = ", ".join("{0}={1}".format(key, _val(key))
785
                              for key in sorted(self.__dict__, key=_sort_helper)
786
                              if _valid(key))
787
        return "{0}({1})".format(self.__class__.__name__, kwarg_str)
788
789
    @classmethod
790
    def __register__(cls):
791
        pass
792
793
    def json(self, indent=None, separators=None, **kwargs):
794
        return json_dumps(self, indent=indent, separators=separators, cls=DumpEncoder, **kwargs)
795
796
    def pretty_json(self, indent=2, separators=(',', ': '), **kwargs):
797
        return self.json(indent=indent, separators=separators, **kwargs)
798
799
    def dump(self):
800
        return odict((field.name, field.dump(value))
801
                     for field, value in ((field, getattr(self, field.name, None))
802
                                          for field in self.__dump_fields())
803
                     if value is not None or field.nullable)
804
805
    @classmethod
806
    def __dump_fields(cls):
807
        if '__dump_fields_cache' not in cls.__dict__:
808
            cls.__dump_fields_cache = tuple(field for field in values(cls.__fields__)
809
                                            if field.in_dump)
810
        return cls.__dump_fields_cache
811
812
    def __eq__(self, other):
813
        if self.__class__ != other.__class__:
814
            return False
815
        rando_default = 19274656290  # need an arbitrary but definite value if field does not exist
816
        return all(getattr(self, field, rando_default) == getattr(other, field, rando_default)
817
                   for field in self.__fields__)
818
819
    def __hash__(self):
820
        return sum(hash(getattr(self, field, None)) for field in self.__fields__)
821
822
    @property
823
    def _initd(self):
824
        return getattr(self, '_{0}__initd'.format(self.__class__.__name__), None)
825
826
827
class ImmutableEntity(Entity):
828
829
    def __setattr__(self, attribute, value):
830
        if self._initd:
831
            raise AttributeError("Assignment not allowed. {0} is immutable."
832
                                 .format(self.__class__.__name__))
833
        super(ImmutableEntity, self).__setattr__(attribute, value)
834
835
    def __delattr__(self, item):
836
        if self._initd:
837
            raise AttributeError("Deletion not allowed. {0} is immutable."
838
                                 .format(self.__class__.__name__))
839
        super(ImmutableEntity, self).__delattr__(item)
840