Completed
Push — develop ( c1d212...733b04 )
by Kale
57s
created

auxlib.Field   B

Complexity

Total Complexity 40

Size/Duplication

Total Lines 151
Duplicated Lines 0 %
Metric Value
dl 0
loc 151
rs 8.2608
wmc 40

19 Methods

Rating   Name   Duplication   Size   Complexity  
A set_name() 0 3 1
A is_nullable() 0 3 1
B validate() 0 16 6
A unbox() 0 2 1
A nullable() 0 3 1
A dump() 0 2 1
A __init__() 0 13 3
A is_required() 0 3 1
A name() 0 8 2
A in_dump() 0 3 1
A __set__() 0 6 3
A required() 0 3 1
B __delete__() 0 14 5
A default() 0 3 1
A immutable() 0 3 1
A is_enum() 0 3 1
C __get__() 0 20 8
A box() 0 2 1
A type() 0 3 1

How to fix   Complexity   

Complex Class

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