Completed
Pull Request — develop (#5)
by Kale
56s
created

auxlib.ListField.unbox()   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
"""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):
205
            if self._validation is None or self._validation(val):
206
                return val
207
        elif val is None and self.nullable:
208
            return val
209
        raise ValidationError(getattr(self, 'name', 'undefined name'), val)
210
211
    @property
212
    def required(self):
213
        return self.is_required
214
215
    @property
216
    def is_required(self):
217
        return self._required
218
219
    @property
220
    def is_enum(self):
221
        return isinstance(self._type, type) and issubclass(self._type, Enum)
222
223
    @property
224
    def type(self):
225
        return self._type
226
227
    @property
228
    def default(self):
229
        return self._default
230
231
    @property
232
    def in_dump(self):
233
        return self._in_dump
234
235
    @property
236
    def nullable(self):
237
        return self.is_nullable
238
239
    @property
240
    def is_nullable(self):
241
        return self._nullable
242
243
244
class IntField(Field):
245
    _type = integer_types
246
247
248
class StringField(Field):
249
    _type = string_types
250
251
252
class NumberField(Field):
253
    _type = integer_types + (float, complex)
254
255
256
class BooleanField(Field):
257
    _type = bool
258
259
    def box(self, val):
260
        return None if val is None else bool(val)
261
262
263
class DateField(Field):
264
    _type = datetime
265
266
    def box(self, val):
267
        try:
268
            return dateparser(val) if isinstance(val, string_types) else val
269
        except ValueError as e:
270
            raise ValidationError(val, msg=e)
271
272
    def dump(self, val):
273
        return None if val is None else val.isoformat()
274
275
276
class EnumField(Field):
277
278
    def __init__(self, enum_class, default=None, required=True, validation=None,
279
                 in_dump=True, nullable=False):
280
        if not issubclass(enum_class, Enum):
281
            raise ValidationError(None, msg="enum_class must be an instance of Enum")
282
        self._type = enum_class
283
        super(self.__class__, self).__init__(default, required, validation, in_dump, nullable)
284
285
    def box(self, val):
286
        if val is None:
287
            return None
288
        try:
289
            return val if isinstance(val, self._type) else self._type(val)
290
        except ValueError as e:
291
            raise ValidationError(val, msg=e)
292
293
    def dump(self, val):
294
        return None if val is None else val.value
295
296
297
class ListField(Field):
298
    _type = tuple
299
300
    def __init__(self, element_type, default=None, required=True, validation=None,
301
                 in_dump=True, nullable=False):
302
        self._element_type = element_type
303
        super(self.__class__, self).__init__(default, required, validation, in_dump, nullable)
304
305
    def box(self, val):
306
        if val is None:
307
            return None
308
        elif isinstance(val, string_types):
309
            raise ValidationError("Attempted to assign a string to ListField {0}"
310
                                  "".format(self.name))
311
        elif isinstance(val, collections.Iterable):
312
            et = self._element_type
313
            if isinstance(et, type) and issubclass(et, Entity):
314
                return tuple(v if isinstance(v, et) else et(**v) for v in val)
315
            else:
316
                return tuple(val)
317
        else:
318
            raise ValidationError(val, msg="Cannot assign a non-iterable value to "
319
                                           "{0}".format(self.name))
320
321
    def unbox(self, val):
322
        return tuple() if val is None and not self.nullable else val
323
324
    def dump(self, val):
325
        if isinstance(self._element_type, type) and issubclass(self._element_type, Entity):
326
            return tuple(v.dump() for v in val)
327
        else:
328
            return val
329
330
    def validate(self, val):
331
        if val is None:
332
            if not self.nullable:
333
                raise ValidationError(self.name, val)
334
            return None
335
        else:
336
            val = super(self.__class__, self).validate(val)
337
            et = self._element_type
338
            tuple(Raise(ValidationError(self.name, el, et)) for el in val
339
                  if not isinstance(el, et))
340
            return val
341
342
343
class MapField(Field):
344
    _type = dict
345
    __eq__ = dict.__eq__
346
    __hash__ = dict.__hash__
347
348
349
class ComposableField(Field):
350
351
    def __init__(self, field_class, default=None, required=True, validation=None,
352
                 in_dump=True, nullable=False):
353
        self._type = field_class
354
        super(self.__class__, self).__init__(default, required, validation, in_dump, nullable)
355
356
    def box(self, val):
357
        if val is None:
358
            return None
359
        if isinstance(val, self._type):
360
            return val
361
        else:
362
            # assuming val is a dict now
363
            try:
364
                # if there is a key named 'self', have to rename it
365
                val['slf'] = val.pop('self')
366
            except KeyError:
367
                pass  # no key of 'self', so no worries
368
            return val if isinstance(val, self._type) else self._type(**val)
369
370
    def dump(self, val):
371
        return None if val is None else val.dump()
372
373
374
class EntityType(type):
375
376
    @staticmethod
377
    def __get_entity_subclasses(bases):
378
        try:
379
            return [base for base in bases if issubclass(base, Entity) and base is not Entity]
380
        except NameError:
381
            # NameError: global name 'Entity' is not defined
382
            return []
383
384
    def __new__(mcs, name, bases, dct):
385
        # if we're about to mask a field that's already been created with something that's
386
        #  not a field, then assign it to an alternate variable name
387
        non_field_keys = (key for key, value in items(dct)
388
                          if not isinstance(value, Field) and not key.startswith('__'))
389
        entity_subclasses = EntityType.__get_entity_subclasses(bases)
390
        if entity_subclasses:
391
            keys_to_override = [key for key in non_field_keys
392
                                if any(isinstance(base.__dict__.get(key, None), Field)
393
                                       for base in entity_subclasses)]
394
            dct[KEY_OVERRIDES_MAP] = {key: dct.pop(key) for key in keys_to_override}
395
        else:
396
            dct[KEY_OVERRIDES_MAP] = dict()
397
398
        return super(EntityType, mcs).__new__(mcs, name, bases, dct)
399
400
    def __init__(cls, name, bases, attr):
401
        super(EntityType, cls).__init__(name, bases, attr)
402
        cls.__fields__ = dict(cls.__fields__) if hasattr(cls, '__fields__') else dict()
403
        cls.__fields__.update({name: field.set_name(name)
404
                               for name, field in cls.__dict__.items()
405
                               if isinstance(field, Field)})
406
        if hasattr(cls, '__register__'):
407
            cls.__register__()
408
409
    @property
410
    def fields(cls):
411
        return cls.__fields__.keys()
412
413
414
@with_metaclass(EntityType)
415
class Entity(object):
416
    __fields__ = dict()
417
    # TODO: add arg order to fields like in enum34
418
419
    def __init__(self, **kwargs):
420
        for key, field in items(self.__fields__):
421
            try:
422
                setattr(self, key, kwargs[key])
423
            except KeyError:
424
                # handle the case of fields inherited from subclass but overrode on class object
425
                if key in getattr(self, KEY_OVERRIDES_MAP):
426
                    setattr(self, key, getattr(self, KEY_OVERRIDES_MAP)[key])
427
                elif field.required and field.default is None:
428
                    raise ValidationError(key, msg="{0} requires a {1} field. Instantiated with "
429
                                                   "{2}".format(self.__class__.__name__,
430
                                                                key, kwargs))
431
            except ValidationError:
432
                if kwargs[key] is not None or field.required:
433
                    raise
434
        self.validate()
435
436
    @classmethod
437
    def create_from_objects(cls, *objects, **override_fields):
438
        init_vars = dict()
439
        search_maps = (AttrDict(override_fields), ) + objects
440
        for key, field in items(cls.__fields__):
441
            value = find_or_none(key, search_maps)
442
            if value is not None or field.required:
443
                init_vars[key] = field.type(value) if field.is_enum else value
444
        return cls(**init_vars)
445
446
    @classmethod
447
    def from_json(cls, json_str):
448
        return cls(**json_loads(json_str))
449
450
    @classmethod
451
    def load(cls, data_dict):
452
        return cls(**data_dict)
453
454
    def validate(self):
455
        # TODO: here, validate should only have to determine if the required keys are set
456
        try:
457
            reduce(lambda x, y: y, (getattr(self, name)
458
                                    for name, field in self.__fields__.items()
459
                                    if field.required))
460
        except AttributeError as e:
461
            raise ValidationError(None, msg=e)
462
        except TypeError as e:
463
            if "no initial value" in str(e):
464
                # TypeError: reduce() of empty sequence with no initial value
465
                pass
466
            else:
467
                raise  # pragma: no cover
468
469
    def __repr__(self):
470
        def _repr(val):
471
            return repr(val.value) if isinstance(val, Enum) else repr(val)
472
        return "{0}({1})".format(
473
            self.__class__.__name__,
474
            ", ".join("{0}={1}".format(key, _repr(value)) for key, value in self.__dict__.items()))
475
476
    @classmethod
477
    def __register__(cls):
478
        pass
479
480
    def dump(self):
481
        return {field.name: field.dump(value)
482
                for field, value in ((field, getattr(self, field.name, None))
483
                                     for field in self.__dump_fields())
484
                if value is not None or field.nullable}
485
486
    @classmethod
487
    def __dump_fields(cls):
488
        if '__dump_fields_cache' not in cls.__dict__:
489
            cls.__dump_fields_cache = set([field for field in values(cls.__fields__)
490
                                           if field.in_dump])
491
        return cls.__dump_fields_cache
492
493
    def __eq__(self, other):
494
        if self.__class__ != other.__class__:
495
            return False
496
        rando_default = 19274656290
497
        return all(getattr(self, field, rando_default) == getattr(other, field, rando_default)
498
                   for field in self.__fields__)
499
500
    def __hash__(self):
501
        return sum(hash(getattr(self, field, None)) for field in self.__fields__)
502
503
504
def find_or_none(key, search_maps, map_index=0):
505
    """Return the value of the first key found in the list of search_maps,
506
    otherwise return None.
507
508
    Examples:
509
        >>> d1 = AttrDict({'a': 1, 'b': 2, 'c': 3, 'e': None})
510
        >>> d2 = AttrDict({'b': 5, 'e': 6, 'f': 7})
511
        >>> find_or_none('c', (d1, d2))
512
        3
513
        >>> find_or_none('f', (d1, d2))
514
        7
515
        >>> find_or_none('b', (d1, d2))
516
        2
517
        >>> print(find_or_none('g', (d1, d2)))
518
        None
519
        >>> find_or_none('e', (d1, d2))
520
        6
521
522
    """
523
    try:
524
        attr = getattr(search_maps[map_index], key)
525
        return attr if attr is not None else find_or_none(key, search_maps[1:])
526
    except AttributeError:
527
        # not found in first map object, so go to next
528
        return find_or_none(key, search_maps, map_index+1)
529
    except IndexError:
530
        # ran out of map objects to search
531
        return None
532