forked from awesto/django-shop
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdeferred.py
258 lines (210 loc) · 9.95 KB
/
deferred.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# -*- coding: utf-8 -*-
import copy
from django.core.exceptions import ImproperlyConfigured
from django.db.models.base import ModelBase
from django.db import models
from django.utils import six
from django.utils.functional import LazyObject, empty
from polymorphic.models import PolymorphicModelBase
from shop.conf import app_settings
class DeferredRelatedField(object):
def __init__(self, to, **kwargs):
try:
self.abstract_model = to._meta.object_name
except AttributeError:
assert isinstance(to, six.string_types), "%s(%r) is invalid. First parameter must be either a model or a model name" % (self.__class__.__name__, to)
self.abstract_model = to
self.options = kwargs
class OneToOneField(DeferredRelatedField):
"""
Use this class to specify a one-to-one key in abstract classes. It will be converted into a real
``OneToOneField`` whenever a real model class is derived from a given abstract class.
"""
MaterializedField = models.OneToOneField
class ForeignKey(DeferredRelatedField):
"""
Use this class to specify foreign keys in abstract classes. It will be converted into a real
``ForeignKey`` whenever a real model class is derived from a given abstract class.
"""
MaterializedField = models.ForeignKey
class ManyToManyField(DeferredRelatedField):
"""
Use this class to specify many-to-many keys in abstract classes. They will be converted into a
real ``ManyToManyField`` whenever a real model class is derived from a given abstract class.
"""
MaterializedField = models.ManyToManyField
def __init__(self, to, **kwargs):
super(ManyToManyField, self).__init__(to, **kwargs)
through = kwargs.get('through')
if through is None:
self.abstract_through_model = None
else:
try:
self.abstract_through_model = through._meta.object_name
except AttributeError:
assert isinstance(through, six.string_types), ('%s(%r) is invalid. '
'Through parameter must be either a model or a model name'
% (self.__class__.__name__, through))
self.abstract_through_model = through
class ForeignKeyBuilder(ModelBase):
"""
In Django we can not point a ``OneToOneField``, ``ForeignKey`` or ``ManyToManyField`` onto
an abstract Model class. In Django-SHOP this limitation is circumvented by creating deferred
foreign keys, which are mapped to their correct model's counterpart during the model materialization
step.
If the main application stores its models in its own directory, add to settings.py:
SHOP_APP_LABEL = 'myshop'
so that the models are created inside your own shop instantiation.
"""
_model_allocation = {}
_pending_mappings = []
_materialized_models = {}
def __new__(cls, name, bases, attrs):
class Meta:
app_label = app_settings.APP_LABEL
attrs.setdefault('Meta', Meta)
attrs.setdefault('__module__', getattr(bases[-1], '__module__'))
if not hasattr(attrs['Meta'], 'app_label') and not getattr(attrs['Meta'], 'abstract', False):
attrs['Meta'].app_label = Meta.app_label
Model = super(ForeignKeyBuilder, cls).__new__(cls, name, bases, attrs)
if Model._meta.abstract:
return Model
if any(isinstance(base, cls) for base in bases):
for baseclass in bases:
if not isinstance(baseclass, cls):
continue
assert issubclass(baseclass, models.Model)
basename = baseclass.__name__
if baseclass._meta.abstract:
if basename in cls._model_allocation:
raise ImproperlyConfigured(
"Both Model classes '%s' and '%s' inherited from abstract "
"base class %s, which is disallowed in this configuration."
% (Model.__name__, cls._model_allocation[basename], basename)
)
cls._model_allocation[basename] = Model.__name__
# remember the materialized model mapping in the base class for further usage
baseclass._materialized_model = Model
cls.process_pending_mappings(Model, basename)
else:
# Non abstract model that uses this Metaclass
basename = Model.__name__
cls._model_allocation[basename] = basename
Model._materialized_model = Model
cls.process_pending_mappings(Model, basename)
cls.handle_deferred_foreign_fields(Model)
cls.perform_meta_model_check(Model)
cls._materialized_models[name] = Model
return Model
@classmethod
def handle_deferred_foreign_fields(cls, Model):
"""
Search for deferred foreign fields in our Model and contribute them to the class or
append them to our list of pending mappings
"""
for attrname in dir(Model):
try:
member = getattr(Model, attrname)
except AttributeError:
continue
if not isinstance(member, DeferredRelatedField):
continue
if member.abstract_model == 'self':
mapmodel = Model
else:
mapmodel = cls._model_allocation.get(member.abstract_model)
abstract_through_model = getattr(member, 'abstract_through_model', None)
mapmodel_through = cls._model_allocation.get(abstract_through_model)
if mapmodel and (not abstract_through_model or mapmodel_through):
if mapmodel_through:
member.options['through'] = mapmodel_through
field = member.MaterializedField(mapmodel, **member.options)
field.contribute_to_class(Model, attrname)
else:
ForeignKeyBuilder._pending_mappings.append((Model, attrname, member,))
@staticmethod
def process_pending_mappings(Model, basename):
assert basename in ForeignKeyBuilder._model_allocation
assert Model._materialized_model
"""
Check for pending mappings and in case, process, and remove them from the list
"""
for mapping in ForeignKeyBuilder._pending_mappings[:]:
member = mapping[2]
mapmodel = ForeignKeyBuilder._model_allocation.get(member.abstract_model)
abstract_through_model = getattr(member, 'abstract_through_model', None)
mapmodel_through = ForeignKeyBuilder._model_allocation.get(abstract_through_model)
if member.abstract_model == basename or abstract_through_model == basename:
if member.abstract_model == basename and abstract_through_model and not mapmodel_through:
continue
elif abstract_through_model == basename and not mapmodel:
continue
if mapmodel_through:
member.options['through'] = mapmodel_through
field = member.MaterializedField(mapmodel, **member.options)
field.contribute_to_class(mapping[0], mapping[1])
ForeignKeyBuilder._pending_mappings.remove(mapping)
def __getattr__(self, key):
if key == '_materialized_model':
msg = "No class implements abstract base model: `{}`."
raise ImproperlyConfigured(msg.format(self.__name__))
return object.__getattribute__(self, key)
@classmethod
def perform_meta_model_check(cls, Model):
"""
Hook for each meta class inheriting from ForeignKeyBuilder, to perform checks on the
implementation of the just created type.
"""
@classmethod
def check_for_pending_mappings(cls):
if cls._pending_mappings:
msg = "Deferred foreign key '{0}.{1}' has not been mapped"
pm = cls._pending_mappings
raise ImproperlyConfigured(msg.format(pm[0][0].__name__, pm[0][1]))
class PolymorphicForeignKeyBuilder(ForeignKeyBuilder, PolymorphicModelBase):
"""
Base class for PolymorphicProductMetaclass
"""
class MaterializedModel(LazyObject):
"""
Wrap the base model into a lazy object, so that we can refer to members of its
materialized model using lazy evaluation.
"""
def __init__(self, base_model):
self.__dict__['_base_model'] = base_model
super(MaterializedModel, self).__init__()
def _setup(self):
self._wrapped = getattr(self._base_model, '_materialized_model')
def __call__(self, *args, **kwargs):
# calls the constructor of the materialized model
if self._wrapped is empty:
self._setup()
return self._wrapped(*args, **kwargs)
def __copy__(self):
if self._wrapped is empty:
# If uninitialized, copy the wrapper. Use type(self),
# not self.__class__, because the latter is proxied.
return type(self)(self._base_model)
else:
# In Python 2.7 we can't return `copy.copy(self._wrapped)`,
# it fails with `TypeError: can't pickle int objects`.
# In Python 3 it works, because it checks if the copied value
# is a subclass of `type`.
# In this case it just returns the value.
# As we know that self._wrapped is a subclass of `type`,
# we can just return it here.
return self._wrapped
def __deepcopy__(self, memo):
if self._wrapped is empty:
# We have to use type(self), not self.__class__,
# because the latter is proxied.
result = type(self)(self._base_model)
memo[id(self)] = result
return result
return copy.deepcopy(self._wrapped, memo)
def __repr__(self):
if self._wrapped is empty:
repr_attr = self._base_model
else:
repr_attr = self._wrapped
return '<MaterializedModel: {}>'.format(repr_attr)