Completed
Push — develop ( d85f01...f1224d )
by Kale
14s
created

Entity   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 117
Duplicated Lines 0 %

Importance

Changes 10
Bugs 3 Features 3
Metric Value
c 10
b 3
f 3
dl 0
loc 117
rs 6.8539
wmc 54

16 Methods

Rating   Name   Duplication   Size   Complexity  
A from_objects() 0 8 4
A __register__() 0 3 1
A json() 0 2 1
F __repr__() 0 25 10
A __dump_fields() 0 6 4
A __hash__() 0 2 2
A load() 0 3 1
A _sort_helper() 0 3 2
A _val() 0 3 2
A _valid() 0 11 3
A _initd() 0 3 1
A from_json() 0 3 1
A pretty_json() 0 2 1
B validate() 0 11 7
A __eq__() 0 6 3
B dump() 0 5 5

How to fix   Complexity   

Complex Class

Complex classes like Entity often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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