|
| 1 | +"""Contextual view module.""" |
| 2 | + |
| 3 | +from . import models |
| 4 | +import six |
| 5 | + |
| 6 | + |
| 7 | +class ContextViewMetaClass(type): |
| 8 | + """Context view meta class.""" |
| 9 | + |
| 10 | + def __new__(mcs, class_name, bases, attributes): |
| 11 | + """Context view class factory.""" |
| 12 | + mcs.validate(bases, attributes) |
| 13 | + cls = type.__new__(mcs, class_name, bases, attributes) |
| 14 | + cls.__fields__ = mcs.get_properties(attributes) |
| 15 | + return cls |
| 16 | + |
| 17 | + @classmethod |
| 18 | + def validate(mcs, bases, attributes): |
| 19 | + """Check attributes.""" |
| 20 | + if bases[0] is object: |
| 21 | + return None |
| 22 | + mcs.check_model_cls(attributes) |
| 23 | + mcs.check_include_exclude(attributes) |
| 24 | + mcs.check_properties(attributes) |
| 25 | + |
| 26 | + @staticmethod |
| 27 | + def check_model_cls(attributes): |
| 28 | + """Check __model_cls__ attribute. |
| 29 | +
|
| 30 | + :type attributes: dict |
| 31 | + """ |
| 32 | + model_cls = attributes.get('__model_cls__') |
| 33 | + if model_cls is None: |
| 34 | + raise AttributeError("Attribute __model_cls__ is required.") |
| 35 | + |
| 36 | + if not issubclass(model_cls, models.DomainModel): |
| 37 | + raise TypeError("Attribute __model_cls__ must be subclass of " |
| 38 | + "DomainModel.") |
| 39 | + |
| 40 | + @staticmethod |
| 41 | + def get_prepared_include_exclude(attributes): |
| 42 | + """Return tuple with prepared __include__ and __exclude__ attributes. |
| 43 | +
|
| 44 | + :type attributes: dict |
| 45 | + :rtype: tuple |
| 46 | + """ |
| 47 | + attrs = dict() |
| 48 | + for attr in ('__include__', '__exclude__'): |
| 49 | + attrs[attr] = tuple([item.name for item in |
| 50 | + attributes.get(attr, tuple())]) |
| 51 | + return attrs['__include__'], attrs['__exclude__'] |
| 52 | + |
| 53 | + @staticmethod |
| 54 | + def check_include_exclude(attributes): |
| 55 | + """Check __include__ and __exclude__ attributes. |
| 56 | +
|
| 57 | + :type attributes: dict |
| 58 | + """ |
| 59 | + include = attributes.get('__include__', tuple()) |
| 60 | + exclude = attributes.get('__exclude__', tuple()) |
| 61 | + |
| 62 | + if not isinstance(include, tuple): |
| 63 | + raise TypeError("Attribute __include__ must be a tuple.") |
| 64 | + |
| 65 | + if not isinstance(exclude, tuple): |
| 66 | + raise TypeError("Attribute __exclude__ must be a tuple.") |
| 67 | + |
| 68 | + if all((not include, not exclude)): |
| 69 | + return None |
| 70 | + |
| 71 | + if all((include, exclude)): |
| 72 | + raise AttributeError("Usage of __include__ and __exclude__ " |
| 73 | + "at the same time is prohibited.") |
| 74 | + |
| 75 | + @staticmethod |
| 76 | + def get_properties(attributes): |
| 77 | + """Return tuple of names of defined properties. |
| 78 | +
|
| 79 | + :type attributes: dict |
| 80 | + :rtype: list |
| 81 | + """ |
| 82 | + return [key for key, value in six.iteritems(attributes) |
| 83 | + if isinstance(value, property)] |
| 84 | + |
| 85 | + @classmethod |
| 86 | + def check_properties(mcs, attributes): |
| 87 | + """Check whether intersections exist. |
| 88 | +
|
| 89 | + :type attributes: dict |
| 90 | + """ |
| 91 | + include, exclude = mcs.get_prepared_include_exclude(attributes) |
| 92 | + properties = mcs.get_properties(attributes) |
| 93 | + intersections = list( |
| 94 | + set(properties).intersection(include if include else exclude)) |
| 95 | + if not intersections: |
| 96 | + return None |
| 97 | + |
| 98 | + attr_name = '__include__' if include else '__exclude__' |
| 99 | + |
| 100 | + raise AttributeError( |
| 101 | + "It is not allowed to mention already defined properties: " |
| 102 | + "{0} in {1} attributes.".format(", ".join(intersections), |
| 103 | + attr_name)) |
| 104 | + |
| 105 | + |
| 106 | +@six.add_metaclass(ContextViewMetaClass) |
| 107 | +class ContextView(object): |
| 108 | + """Contextual view class.""" |
| 109 | + |
| 110 | + __model_cls__ = None |
| 111 | + __include__ = tuple() |
| 112 | + __exclude__ = tuple() |
| 113 | + __fields__ = list() |
| 114 | + |
| 115 | + def __init__(self, model): |
| 116 | + """Model validation. |
| 117 | +
|
| 118 | + :type model: DomainModel |
| 119 | + """ |
| 120 | + if not isinstance(model, self.__model_cls__): |
| 121 | + raise TypeError("\"{0}\" is not an instance of {1}".format( |
| 122 | + model, self.__model_cls__)) |
| 123 | + |
| 124 | + self._model = model |
| 125 | + |
| 126 | + if self.__include__: |
| 127 | + self._include_fields() |
| 128 | + elif self.__exclude__: |
| 129 | + self._exclude_fields() |
| 130 | + else: |
| 131 | + self._all_fields() |
| 132 | + |
| 133 | + def _include_fields(self): |
| 134 | + """Fill __fields__ out based on __include__.""" |
| 135 | + for field in self.__include__: |
| 136 | + value = getattr(self._model, field.name) |
| 137 | + setattr(self, field.name, value) |
| 138 | + self.__fields__.append(field.name) |
| 139 | + |
| 140 | + def _exclude_fields(self): |
| 141 | + """Fill __fields__ out based on __exclude__.""" |
| 142 | + exclude = [field.name for field in self.__exclude__] |
| 143 | + for (field, value) in six.iteritems(self._model.get_data()): |
| 144 | + if field in exclude: |
| 145 | + continue |
| 146 | + setattr(self, field, value) |
| 147 | + self.__fields__.append(field) |
| 148 | + |
| 149 | + def _all_fields(self): |
| 150 | + """Fill __fields__ out based on full model data.""" |
| 151 | + for (field, value) in six.iteritems(self._model.get_data()): |
| 152 | + if field in self.__fields__: |
| 153 | + continue |
| 154 | + setattr(self, field, value) |
| 155 | + self.__fields__.append(field) |
| 156 | + |
| 157 | + def get_data(self): |
| 158 | + """Read only dictionary fields/values of model within current context. |
| 159 | +
|
| 160 | + :rtype: dict |
| 161 | + """ |
| 162 | + return dict((field, getattr(self, field)) for field in self.__fields__) |
0 commit comments