Skip to content

Commit 7de1816

Browse files
authored
Merge pull request #33 from ets-labs/25-context_view
Issue #25: Implementation of contextual view
2 parents ff7a5a1 + 1712d9b commit 7de1816

File tree

4 files changed

+455
-5
lines changed

4 files changed

+455
-5
lines changed

domain_models/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Domain models."""
22

3-
VERSION = '0.0.8'
3+
VERSION = '0.0.9'

domain_models/models.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,8 @@ def get(self, field_name, default=None):
192192
converted to right type value.
193193
If the field does not exist, `AttributeError` is raised as well.
194194
195-
:param string field_name:
196-
:param object default:
195+
:type field_name: string
196+
:type default: object
197197
"""
198198
try:
199199
field = self.__class__.__fields__[field_name]
@@ -206,7 +206,7 @@ def get(self, field_name, default=None):
206206
def get_data(self):
207207
"""Read only dictionary of model fields/values.
208208
209-
:rtype dict:
209+
:rtype: dict
210210
"""
211211
return dict((name, field.get_builtin_type(self))
212212
for name, field in
@@ -215,7 +215,7 @@ def get_data(self):
215215
def set_data(self, data):
216216
"""Set dictionary data to model.
217217
218-
:param dict data:
218+
:type data: dict
219219
"""
220220
for name, field in six.iteritems(self.__class__.__fields__):
221221
field.init_model(self, data.get(name))

domain_models/views.py

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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

Comments
 (0)