Completed
Push — develop ( b15263...cccc5b )
by Kale
58s
created

auxlib.ListField.dump()   A

Complexity

Conditions 4

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 4
dl 0
loc 5
rs 9.2
1
# -*- coding: utf-8 -*-
2
"""This module provides facilities for serializable, validatable, and type-enforcing
3
domain objects.
4
5
This module has many of the same motivations as the python Marshmallow package.
6
<http://marshmallow.readthedocs.org/en/latest/why.html>
7
8
Also need to be explicit in explaining what Marshmallow doesn't do, and why this module is needed.
9
  - Provides type safety like an ORM. And like an ORM, all classes subclass Entity.
10
  - Provides BUILT IN serialization and deserialization.  Marhmallow requires a lot of code
11
    duplication.
12
13
This module gives us:
14
  - type safety
15
  - custom field validation
16
  - serialization and deserialization
17
  - rock solid foundational domain objects
18
19
20
21
22
Optional Field Properties:
23
  - validation = None
24
  - default = None
25
  - required = True
26
  - in_dump = True
27
  - nullable = False
28
29
Behaviors:
30
  - Nullable is a "hard" setting, in that the value is either always or never allowed to be None.
31
  - What happens then if required=False and nullable=False?
32
      - The object can be init'd without a value (though not with a None value).
33
        getattr throws AttributeError
34
      - Any assignment must be not None.
35
36
37
  - Setting a value to None doesn't "unset" a value.  (That's what del is for.)  And you can't
38
    del a value if required=True, nullable=False, default=None.  That should raise
39
    OperationNotAllowedError.
40
41
42
  - Disabling in_dump is a "hard" setting, in that with it disabled the field will never get
43
    dumped.  With it enabled, the field may or may not be dumped depending on its value and other
44
    settings.
45
46
  - Required is a "hard" setting, in that if True, a valid value or default must be provided. None
47
    is only a valid value or default if nullable is True.
48
49
  - In general, nullable means that None is a valid value.
50
    - getattr returns None instead of raising Attribute error
51
    - If in_dump, field is given with null value.
52
    - If default is not None, assigning None clears a previous assignment. Future getattrs return
53
      the default value.
54
    - What does nullable mean with default=None and required=True? Does instantiation raise
55
      an error if assignment not made on init? Can IntField(nullable=True) be init'd?
56
57
  - If required=False and nullable=False, field will only be in dump if field!=None.
58
    Also, getattr raises AttributeError.
59
  - If required=False and nullable=True, field will be in dump if field==None.
60
61
  - What does required and default mean?
62
  - What does required and default and nullable mean?
63
  - If in_dump is True, does default value get dumped:
64
    - if no assignment, default exists
65
    - if nullable, and assigned None
66
  - Can assign None?
67
  - How does optional validation work with nullable and assigning None?
68
  - When does gettattr throw AttributeError, and when does it return None?
69
70
71
72
73
74
75
Fields are doing something very similar to boxing and unboxing
76
of java primitives.  __set__ should take a "primitve" or "raw" value and create a "boxed"
77
or "programatically useable" value of it.  While __get__ should return the boxed value,
78
dump in turn should unbox the value into a primitive or raw value.
79
80
81
82
83
Examples:
84
    >>> class Color(Enum):
85
    ...     blue = 0
86
    ...     black = 1
87
    ...     red = 2
88
    >>> class Truck(Entity):
89
    ...     color = EnumField(Color)
90
    ...     weight = NumberField()
91
    ...     wheels = IntField(4, in_dump=False)
92
93
    >>> truck = Truck(weight=44.4, color=Color.blue, wheels=18)
94
    >>> truck.wheels
95
    18
96
    >>> truck.color
97
    <Color.blue: 0>
98
    >>> sorted(truck.dump().items())
99
    [('color', 0), ('weight', 44.4)]
100
101
"""
102
from __future__ import absolute_import, division, print_function
103
from datetime import datetime
104
105
import collections
106
107
from functools import reduce
108
from json import loads as json_loads
109
from logging import getLogger
110
111
from dateutil.parser import parse as dateparser
112
from enum import Enum
113
114
from ._vendor.five import with_metaclass, items, values
115
from ._vendor.six import integer_types, string_types
116
from .collection import AttrDict
117
from .exceptions import ValidationError, Raise
118
from .type_coercion import maybecall
119
120
log = getLogger(__name__)
121
122
KEY_OVERRIDES_MAP = "__key_overrides__"
123
124
125
class Field(object):
126
    """
127
128
    Arguments:
129
        types_ (primitive literal or type or sequence of types):
130
        default (any, callable, optional):  If default is callable, it's guaranteed to return a
131
            valid value at the time of Entity creation.
132
        required (boolean, optional):
133
        validation (callable, optional):
134
        dump (boolean, optional):
135
    """
136
137
    def __init__(self, default=None, required=True, validation=None, in_dump=True, nullable=False):
138
        self._default = default if callable(default) else self.box(default)
139
        self._required = required
140
        self._validation = validation
141
        self._in_dump = in_dump
142
        self._nullable = nullable
143
        if default is not None:
144
            self.validate(self.box(maybecall(default)))
145
146
    @property
147
    def name(self):
148
        try:
149
            return self._name
150
        except AttributeError:
151
            log.error("The name attribute has not been set for this field. "
152
                      "Call set_name at class creation time.")
153
            raise
154
155
    def set_name(self, name):
156
        self._name = name
157
        return self
158
159
    def __get__(self, instance, instance_type):
160
        try:
161
            if instance is None:  # if calling from the class object
162
                val = getattr(instance_type, KEY_OVERRIDES_MAP)[self.name]
163
            else:
164
                val = instance.__dict__[self.name]
165
        except AttributeError:
166
            log.error("The name attribute has not been set for this field.")
167
            raise
168
        except KeyError:
169
            if self.default is not None:
170
                val = maybecall(self.default)  # default *can* be a callable
171
            elif self._nullable:
172
                return None
173
            else:
174
                raise AttributeError("A value for {0} has not been set".format(self.name))
175
        return self.unbox(val)
176
177
    def __set__(self, instance, val):
178
        # validate will raise an exception if invalid
179
        # validate will return False if the value should be removed
180
        instance.__dict__[self.name] = self.validate(self.box(val))
181
182
    def __delete__(self, instance):
183
        instance.__dict__.pop(self.name, None)
184
185
    def box(self, val):
186
        return val
187
188
    def unbox(self, val):
189
        return val
190
191
    def dump(self, val):
192
        return val
193
194
    def validate(self, val):
195
        """
196
197
        Returns:
198
            True: if val is valid
199
200
        Raises:
201
            ValidationError
202
        """
203
        # note here calling, but not assigning; could lead to unexpected behavior
204
        if isinstance(val, self._type) and (self._validation is None or self._validation(val)):
205
                return val
206
        elif val is None and self.nullable:
207
            return val
208
        raise ValidationError(getattr(self, 'name', 'undefined name'), val)
209
210
    @property
211
    def required(self):
212
        return self.is_required
213
214
    @property
215
    def is_required(self):
216
        return self._required
217
218
    @property
219
    def is_enum(self):
220
        return isinstance(self._type, type) and issubclass(self._type, Enum)
221
222
    @property
223
    def type(self):
224
        return self._type
225
226
    @property
227
    def default(self):
228
        return self._default
229
230
    @property
231
    def in_dump(self):
232
        return self._in_dump
233
234
    @property
235
    def nullable(self):
236
        return self.is_nullable
237
238
    @property
239
    def is_nullable(self):
240
        return self._nullable
241
242
243
class IntField(Field):
244
    _type = integer_types
245
246
247
class StringField(Field):
248
    _type = string_types
249
250
251
class NumberField(Field):
252
    _type = integer_types + (float, complex)
253
254
255
class BooleanField(Field):
256
    _type = bool
257
258
    def box(self, val):
259
        return None if val is None else bool(val)
260
261
262
class DateField(Field):
263
    _type = datetime
264
265
    def box(self, val):
266
        try:
267
            return dateparser(val) if isinstance(val, string_types) else val
268
        except ValueError as e:
269
            raise ValidationError(val, msg=e)
270
271
    def dump(self, val):
272
        return None if val is None else val.isoformat()
273
274
275
class EnumField(Field):
276
277
    def __init__(self, enum_class, default=None, required=True, validation=None,
278
                 in_dump=True, nullable=False):
279
        if not issubclass(enum_class, Enum):
280
            raise ValidationError(None, msg="enum_class must be an instance of Enum")
281
        self._type = enum_class
282
        super(self.__class__, self).__init__(default, required, validation, in_dump, nullable)
283
284
    def box(self, val):
285
        if val is None:
286
            return None
287
        try:
288
            return val if isinstance(val, self._type) else self._type(val)
289
        except ValueError as e:
290
            raise ValidationError(val, msg=e)
291
292
    def dump(self, val):
293
        return None if val is None else val.value
294
295
296
class ListField(Field):
297
    _type = tuple
298
299
    def __init__(self, element_type, default=None, required=True, validation=None,
300
                 in_dump=True, nullable=False):
301
        self._element_type = element_type
302
        super(self.__class__, self).__init__(default, required, validation, in_dump, nullable)
303
304
    def box(self, val):
305
        if val is None:
306
            return None
307
        elif isinstance(val, string_types):
308
            raise ValidationError("Attempted to assign a string to ListField {0}"
309
                                  "".format(self.name))
310
        elif isinstance(val, collections.Iterable):
311
            et = self._element_type
312
            if isinstance(et, type) and issubclass(et, Entity):
313
                return tuple(v if isinstance(v, et) else et(**v) for v in val)
314
            else:
315
                return tuple(val)
316
        else:
317
            raise ValidationError(val, msg="Cannot assign a non-iterable value to "
318
                                           "{0}".format(self.name))
319
320
    def unbox(self, val):
321
        return tuple() if val is None and not self.nullable else val
322
323
    def dump(self, val):
324
        if isinstance(self._element_type, type) and issubclass(self._element_type, Entity):
325
            return tuple(v.dump() for v in val)
326
        else:
327
            return val
328
329
    def validate(self, val):
330
        if val is None:
331
            if not self.nullable:
332
                raise ValidationError(self.name, val)
333
            return None
334
        else:
335
            val = super(self.__class__, self).validate(val)
336
            et = self._element_type
337
            tuple(Raise(ValidationError(self.name, el, et)) for el in val
338
                  if not isinstance(el, et))
339
            return val
340
341
342
class MapField(Field):
343
    _type = dict
344
    __eq__ = dict.__eq__
345
    __hash__ = dict.__hash__
346
347
348
class ComposableField(Field):
349
350
    def __init__(self, field_class, default=None, required=True, validation=None,
351
                 in_dump=True, nullable=False):
352
        self._type = field_class
353
        super(self.__class__, self).__init__(default, required, validation, in_dump, nullable)
354
355
    def box(self, val):
356
        if val is None:
357
            return None
358
        if isinstance(val, self._type):
359
            return val
360
        else:
361
            # assuming val is a dict now
362
            try:
363
                # if there is a key named 'self', have to rename it
364
                val['slf'] = val.pop('self')
365
            except KeyError:
366
                pass  # no key of 'self', so no worries
367
            return val if isinstance(val, self._type) else self._type(**val)
368
369
    def dump(self, val):
370
        return None if val is None else val.dump()
371
372
373
class EntityType(type):
374
375
    @staticmethod
376
    def __get_entity_subclasses(bases):
377
        try:
378
            return [base for base in bases if issubclass(base, Entity) and base is not Entity]
379
        except NameError:
380
            # NameError: global name 'Entity' is not defined
381
            return []
382
383
    def __new__(mcs, name, bases, dct):
384
        # if we're about to mask a field that's already been created with something that's
385
        #  not a field, then assign it to an alternate variable name
386
        non_field_keys = (key for key, value in items(dct)
387
                          if not isinstance(value, Field) and not key.startswith('__'))
388
        entity_subclasses = EntityType.__get_entity_subclasses(bases)
389
        if entity_subclasses:
390
            keys_to_override = [key for key in non_field_keys
391
                                if any(isinstance(base.__dict__.get(key), Field)
392
                                       for base in entity_subclasses)]
393
            dct[KEY_OVERRIDES_MAP] = {key: dct.pop(key) for key in keys_to_override}
394
        else:
395
            dct[KEY_OVERRIDES_MAP] = dict()
396
397
        return super(EntityType, mcs).__new__(mcs, name, bases, dct)
398
399
    def __init__(cls, name, bases, attr):
400
        super(EntityType, cls).__init__(name, bases, attr)
401
        cls.__fields__ = dict(cls.__fields__) if hasattr(cls, '__fields__') else dict()
402
        cls.__fields__.update({name: field.set_name(name)
403
                               for name, field in cls.__dict__.items()
404
                               if isinstance(field, Field)})
405
        if hasattr(cls, '__register__'):
406
            cls.__register__()
407
408
    @property
409
    def fields(cls):
410
        return cls.__fields__.keys()
411
412
413
@with_metaclass(EntityType)
414
class Entity(object):
415
    __fields__ = dict()
416
    # TODO: add arg order to fields like in enum34
417
418
    def __init__(self, **kwargs):
419
        for key, field in items(self.__fields__):
420
            try:
421
                setattr(self, key, kwargs[key])
422
            except KeyError:
423
                # handle the case of fields inherited from subclass but overrode on class object
424
                if key in getattr(self, KEY_OVERRIDES_MAP):
425
                    setattr(self, key, getattr(self, KEY_OVERRIDES_MAP)[key])
426
                elif field.required and field.default is None:
427
                    raise ValidationError(key, msg="{0} requires a {1} field. Instantiated with "
428
                                                   "{2}".format(self.__class__.__name__,
429
                                                                key, kwargs))
430
            except ValidationError:
431
                if kwargs[key] is not None or field.required:
432
                    raise
433
        self.validate()
434
435
    @classmethod
436
    def create_from_objects(cls, *objects, **override_fields):
437
        init_vars = dict()
438
        search_maps = (AttrDict(override_fields), ) + objects
439
        for key, field in items(cls.__fields__):
440
            value = find_or_none(key, search_maps)
441
            if value is not None or field.required:
442
                init_vars[key] = field.type(value) if field.is_enum else value
443
        return cls(**init_vars)
444
445
    @classmethod
446
    def from_json(cls, json_str):
447
        return cls(**json_loads(json_str))
448
449
    @classmethod
450
    def load(cls, data_dict):
451
        return cls(**data_dict)
452
453
    def validate(self):
454
        # TODO: here, validate should only have to determine if the required keys are set
455
        try:
456
            reduce(lambda x, y: y, (getattr(self, name)
457
                                    for name, field in self.__fields__.items()
458
                                    if field.required))
459
        except AttributeError as e:
460
            raise ValidationError(None, msg=e)
461
        except TypeError as e:
462
            if "no initial value" in str(e):
463
                # TypeError: reduce() of empty sequence with no initial value
464
                pass
465
            else:
466
                raise  # pragma: no cover
467
468
    def __repr__(self):
469
        def _repr(val):
470
            return repr(val.value) if isinstance(val, Enum) else repr(val)
471
        return "{0}({1})".format(
472
            self.__class__.__name__,
473
            ", ".join("{0}={1}".format(key, _repr(value)) for key, value in self.__dict__.items()))
474
475
    @classmethod
476
    def __register__(cls):
477
        pass
478
479
    def dump(self):
480
        return {field.name: field.dump(value)
481
                for field, value in ((field, getattr(self, field.name, None))
482
                                     for field in self.__dump_fields())
483
                if value is not None or field.nullable}
484
485
    @classmethod
486
    def __dump_fields(cls):
487
        if '__dump_fields_cache' not in cls.__dict__:
488
            cls.__dump_fields_cache = {field for field in values(cls.__fields__) if field.in_dump}
489
        return cls.__dump_fields_cache
490
491
    def __eq__(self, other):
492
        if self.__class__ != other.__class__:
493
            return False
494
        rando_default = 19274656290
495
        return all(getattr(self, field, rando_default) == getattr(other, field, rando_default)
496
                   for field in self.__fields__)
497
498
    def __hash__(self):
499
        return sum(hash(getattr(self, field, None)) for field in self.__fields__)
500
501
502
def find_or_none(key, search_maps, map_index=0):
503
    """Return the value of the first key found in the list of search_maps,
504
    otherwise return None.
505
506
    Examples:
507
        >>> d1 = AttrDict({'a': 1, 'b': 2, 'c': 3, 'e': None})
508
        >>> d2 = AttrDict({'b': 5, 'e': 6, 'f': 7})
509
        >>> find_or_none('c', (d1, d2))
510
        3
511
        >>> find_or_none('f', (d1, d2))
512
        7
513
        >>> find_or_none('b', (d1, d2))
514
        2
515
        >>> print(find_or_none('g', (d1, d2)))
516
        None
517
        >>> find_or_none('e', (d1, d2))
518
        6
519
520
    """
521
    try:
522
        attr = getattr(search_maps[map_index], key)
523
        return attr if attr is not None else find_or_none(key, search_maps[1:])
524
    except AttributeError:
525
        # not found in first map object, so go to next
526
        return find_or_none(key, search_maps, map_index+1)
527
    except IndexError:
528
        # ran out of map objects to search
529
        return None
530