Skip to content

Commit 472ec74

Browse files
committed
Issue #25: Implementation of contextual view with tests coverage
1 parent 0d3ed6d commit 472ec74

File tree

2 files changed

+238
-72
lines changed

2 files changed

+238
-72
lines changed

domain_models/views.py

Lines changed: 137 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,170 @@
11
"""Contextual view module."""
2-
from domain_models import models
2+
from . import models
33
import six
44

55

66
class ContextViewMetaClass(type):
77
"""Context view meta class."""
88

9+
attributes = dict()
10+
911
def __new__(mcs, class_name, bases, attributes):
1012
"""Context view class factory."""
11-
if bases[0] is not object:
12-
__model_cls__ = attributes.get('__model_cls__')
13-
if __model_cls__ is None:
14-
raise AttributeError("Attribute __model_cls__ is required.")
15-
if not issubclass(__model_cls__, models.DomainModel):
16-
raise TypeError("Attribute __model_cls__ must be subclass of "
17-
"DomainModel.")
13+
mcs.attributes = attributes
14+
mcs.validate(bases)
15+
cls = type.__new__(mcs, class_name, bases, attributes)
16+
cls.__fields__ = mcs.get_properties()
17+
return cls
18+
19+
@classmethod
20+
def validate(mcs, bases):
21+
"""Check attributes."""
22+
if bases[0] is object:
23+
return None
24+
mcs.chk_model_cls()
25+
mcs.chk_incl_excl()
26+
27+
@classmethod
28+
def chk_model_cls(mcs):
29+
"""Check __model_cls__ attribute."""
30+
model_cls = mcs.attributes.get('__model_cls__')
31+
if model_cls is None:
32+
raise AttributeError("Attribute __model_cls__ is required.")
33+
34+
if not issubclass(model_cls, models.DomainModel):
35+
raise TypeError("Attribute __model_cls__ must be subclass of "
36+
"DomainModel.")
37+
38+
@classmethod
39+
def chk_incl_excl(mcs):
40+
"""Check __include__ and __exclude__ attributes."""
41+
include = mcs.attributes.get('__include__', tuple())
42+
exclude = mcs.attributes.get('__exclude__', tuple())
43+
44+
if not isinstance(include, tuple):
45+
raise TypeError("Attribute __include__ must be a tuple.")
46+
47+
if not isinstance(exclude, tuple):
48+
raise TypeError("Attribute __exclude__ must be a tuple.")
1849

19-
return type.__new__(mcs, class_name, bases, attributes)
50+
if all((not include, not exclude)):
51+
return None
52+
53+
if all((include, exclude)):
54+
raise AttributeError("Usage of __include__ and __exclude__ "
55+
"at the same time is prohibited.")
56+
57+
include_names = [item.name for item in include]
58+
exclude_names = [item.name for item in exclude]
59+
60+
mcs.chk_intersections(include_names, exclude_names)
61+
mcs.match_unknown_attrs(include_names, exclude_names)
62+
63+
@classmethod
64+
def chk_intersections(mcs, include, exclude):
65+
"""Check whether intersections exist."""
66+
intersections = mcs.get_intersections(include)
67+
attr, intersections = ('__include__', intersections) \
68+
if intersections \
69+
else ('__exclude__', mcs.get_intersections(exclude))
70+
71+
if intersections:
72+
raise AttributeError(
73+
"It is not allowed to mention already defined properties: "
74+
"{0} in {1} attributes.".format(", ".join(intersections),
75+
attr))
76+
77+
@classmethod
78+
def get_intersections(mcs, attr):
79+
"""Return intersection with defined properties if exists.
80+
81+
:type attr: list
82+
:rtype: list
83+
"""
84+
if not attr:
85+
return []
86+
return list(set(mcs.get_properties()).intersection(attr))
87+
88+
@classmethod
89+
def match_unknown_attrs(mcs, include, exclude):
90+
"""Check about using nonexistent attributes."""
91+
model_cls = mcs.attributes.get('__model_cls__')
92+
unknown_attr = []
93+
for item in include:
94+
if not hasattr(model_cls, item):
95+
unknown_attr.append(item)
96+
97+
for item in exclude:
98+
if not hasattr(model_cls, item):
99+
unknown_attr.append(item)
100+
101+
if unknown_attr:
102+
raise AttributeError(
103+
"Nonexistent attributes: {0}.".format(
104+
", ".join(unknown_attr)))
105+
106+
@classmethod
107+
def get_properties(mcs):
108+
"""Return list of names of defined properties.
109+
110+
:rtype: list
111+
"""
112+
return [key for key, value in six.iteritems(mcs.attributes) if
113+
isinstance(value, property)]
20114

21115

22116
@six.add_metaclass(ContextViewMetaClass)
23117
class ContextView(object):
24118
"""Contextual view class."""
25119

26120
__model_cls__ = None
121+
__model__ = None
122+
__include__ = tuple()
123+
__exclude__ = tuple()
124+
__fields__ = list()
27125

28126
def __init__(self, model):
29127
"""Model validation.
30128
31-
:param model: DomainModel
129+
:type model: DomainModel
32130
"""
33131
if not isinstance(model, self.__model_cls__):
34132
raise TypeError("\"{0}\" is not an instance of {1}".format(
35133
model, self.__model_cls__))
36134

37135
self.__model__ = model
38136

137+
if self.__include__:
138+
self._include()
139+
elif self.__exclude__:
140+
self._exclude()
141+
else:
142+
self._fields()
143+
144+
def _include(self):
145+
for field in self.__include__:
146+
value = getattr(self.__model__, field.name)
147+
setattr(self, field.name, value)
148+
self.__fields__.append(field.name)
149+
150+
def _exclude(self):
151+
exclude = [field.name for field in self.__exclude__]
152+
for (field, value) in six.iteritems(self.__model__.get_data()):
153+
if field in exclude:
154+
continue
155+
setattr(self, field, value)
156+
self.__fields__.append(field)
157+
158+
def _fields(self):
159+
for (field, value) in six.iteritems(self.__model__.get_data()):
160+
if field in self.__fields__:
161+
continue
162+
setattr(self, field, value)
163+
self.__fields__.append(field)
164+
39165
def get_data(self):
40166
"""Read only dictionary fields/values of model within current context.
41167
42168
:rtype: dict
43169
"""
44-
raise NotImplementedError("Method get_data is required.")
170+
return dict((field, getattr(self, field)) for field in self.__fields__)

0 commit comments

Comments
 (0)