Project

General

Profile

Statistics
| Branch: | Revision:

root / env / lib / python2.7 / site-packages / south / orm.py @ d1a4905f

History | View | Annotate | Download (14.7 KB)

1
"""
2
South's fake ORM; lets you not have to write SQL inside migrations.
3
Roughly emulates the real Django ORM, to a point.
4
"""
5

    
6
import inspect
7

    
8
from django.db import models
9
from django.db.models.loading import cache
10
from django.core.exceptions import ImproperlyConfigured
11

    
12
from south.db import db
13
from south.utils import ask_for_it_by_name, datetime_utils
14
from south.hacks import hacks
15
from south.exceptions import UnfreezeMeLater, ORMBaseNotIncluded, ImpossibleORMUnfreeze
16

    
17

    
18
class ModelsLocals(object):
19
    
20
    """
21
    Custom dictionary-like class to be locals();
22
    falls back to lowercase search for items that don't exist
23
    (because we store model names as lowercase).
24
    """
25
    
26
    def __init__(self, data):
27
        self.data = data
28
    
29
    def __getitem__(self, key):
30
        try:
31
            return self.data[key]
32
        except KeyError:
33
            return self.data[key.lower()]
34

    
35

    
36
# Stores already-created ORMs.
37
_orm_cache = {}
38

    
39
def FakeORM(*args):
40
    """
41
    Creates a Fake Django ORM.
42
    This is actually a memoised constructor; the real class is _FakeORM.
43
    """
44
    if not args in _orm_cache:
45
        _orm_cache[args] = _FakeORM(*args)  
46
    return _orm_cache[args]
47

    
48

    
49
class LazyFakeORM(object):
50
    """
51
    In addition to memoising the ORM call, this function lazily generates them
52
    for a Migration class. Assign the result of this to (for example)
53
    .orm, and as soon as .orm is accessed the ORM will be created.
54
    """
55
    
56
    def __init__(self, *args):
57
        self._args = args
58
        self.orm = None
59
    
60
    def __get__(self, obj, type=None):
61
        if not self.orm:
62
            self.orm = FakeORM(*self._args)
63
        return self.orm
64

    
65

    
66
class _FakeORM(object):
67
    
68
    """
69
    Simulates the Django ORM at some point in time,
70
    using a frozen definition on the Migration class.
71
    """
72
    
73
    def __init__(self, cls, app):
74
        self.default_app = app
75
        self.cls = cls
76
        # Try loading the models off the migration class; default to no models.
77
        self.models = {}
78
        try:
79
            self.models_source = cls.models
80
        except AttributeError:
81
            return
82
        
83
        # Start a 'new' AppCache
84
        hacks.clear_app_cache()
85
        
86
        # Now, make each model's data into a FakeModel
87
        # We first make entries for each model that are just its name
88
        # This allows us to have circular model dependency loops
89
        model_names = []
90
        for name, data in self.models_source.items():
91
            # Make sure there's some kind of Meta
92
            if "Meta" not in data:
93
                data['Meta'] = {}
94
            try:
95
                app_label, model_name = name.split(".", 1)
96
            except ValueError:
97
                app_label = self.default_app
98
                model_name = name
99
            
100
            # If there's an object_name in the Meta, use it and remove it
101
            if "object_name" in data['Meta']:
102
                model_name = data['Meta']['object_name']
103
                del data['Meta']['object_name']
104
            
105
            name = "%s.%s" % (app_label, model_name)
106
            self.models[name.lower()] = name
107
            model_names.append((name.lower(), app_label, model_name, data))
108
        
109
        # Loop until model_names is entry, or hasn't shrunk in size since
110
        # last iteration.
111
        # The make_model method can ask to postpone a model; it's then pushed
112
        # to the back of the queue. Because this is currently only used for
113
        # inheritance, it should thus theoretically always decrease by one.
114
        last_size = None
115
        while model_names:
116
            # First, make sure we've shrunk.
117
            if len(model_names) == last_size:
118
                raise ImpossibleORMUnfreeze()
119
            last_size = len(model_names)
120
            # Make one run through
121
            postponed_model_names = []
122
            for name, app_label, model_name, data in model_names:
123
                try:
124
                    self.models[name] = self.make_model(app_label, model_name, data)
125
                except UnfreezeMeLater:
126
                    postponed_model_names.append((name, app_label, model_name, data))
127
            # Reset
128
            model_names = postponed_model_names
129
        
130
        # And perform the second run to iron out any circular/backwards depends.
131
        self.retry_failed_fields()
132
        
133
        # Force evaluation of relations on the models now
134
        for model in self.models.values():
135
            model._meta.get_all_field_names()
136
        
137
        # Reset AppCache
138
        hacks.unclear_app_cache()
139
    
140
    
141
    def __iter__(self):
142
        return iter(self.models.values())
143

    
144
    
145
    def __getattr__(self, key):
146
        fullname = (self.default_app+"."+key).lower()
147
        try:
148
            return self.models[fullname]
149
        except KeyError:
150
            raise AttributeError("The model '%s' from the app '%s' is not available in this migration. (Did you use orm.ModelName, not orm['app.ModelName']?)" % (key, self.default_app))
151
    
152
    
153
    def __getitem__(self, key):
154
        # Detect if they asked for a field on a model or not.
155
        if ":" in key:
156
            key, fname = key.split(":")
157
        else:
158
            fname = None
159
        # Now, try getting the model
160
        key = key.lower()
161
        try:
162
            model = self.models[key]
163
        except KeyError:
164
            try:
165
                app, model = key.split(".", 1)
166
            except ValueError:
167
                raise KeyError("The model '%s' is not in appname.modelname format." % key)
168
            else:
169
                raise KeyError("The model '%s' from the app '%s' is not available in this migration." % (model, app))
170
        # If they asked for a field, get it.
171
        if fname:
172
            return model._meta.get_field_by_name(fname)[0]
173
        else:
174
            return model
175
    
176
    
177
    def eval_in_context(self, code, app, extra_imports={}):
178
        "Evaluates the given code in the context of the migration file."
179
        
180
        # Drag in the migration module's locals (hopefully including models.py)
181
        fake_locals = dict(inspect.getmodule(self.cls).__dict__)
182
        
183
        # Remove all models from that (i.e. from modern models.py), to stop pollution
184
        for key, value in fake_locals.items():
185
            if isinstance(value, type) and issubclass(value, models.Model) and hasattr(value, "_meta"):
186
                del fake_locals[key]
187
        
188
        # We add our models into the locals for the eval
189
        fake_locals.update(dict([
190
            (name.split(".")[-1], model)
191
            for name, model in self.models.items()
192
        ]))
193
        
194
        # Make sure the ones for this app override.
195
        fake_locals.update(dict([
196
            (name.split(".")[-1], model)
197
            for name, model in self.models.items()
198
            if name.split(".")[0] == app
199
        ]))
200
        
201
        # Ourselves as orm, to allow non-fail cross-app referencing
202
        fake_locals['orm'] = self
203
        
204
        # And a fake _ function
205
        fake_locals['_'] = lambda x: x
206
        
207
        # Datetime; there should be no datetime direct accesses
208
        fake_locals['datetime'] = datetime_utils
209
        
210
        # Now, go through the requested imports and import them.
211
        for name, value in extra_imports.items():
212
            # First, try getting it out of locals.
213
            parts = value.split(".")
214
            try:
215
                obj = fake_locals[parts[0]]
216
                for part in parts[1:]:
217
                    obj = getattr(obj, part)
218
            except (KeyError, AttributeError):
219
                pass
220
            else:
221
                fake_locals[name] = obj
222
                continue
223
            # OK, try to import it directly
224
            try:
225
                fake_locals[name] = ask_for_it_by_name(value)
226
            except ImportError:
227
                if name == "SouthFieldClass":
228
                    raise ValueError("Cannot import the required field '%s'" % value)
229
                else:
230
                    print "WARNING: Cannot import '%s'" % value
231
        
232
        # Use ModelsLocals to make lookups work right for CapitalisedModels
233
        fake_locals = ModelsLocals(fake_locals)
234
        
235
        return eval(code, globals(), fake_locals)
236
    
237
    
238
    def make_meta(self, app, model, data, stub=False):
239
        "Makes a Meta class out of a dict of eval-able arguments."
240
        results = {'app_label': app}
241
        for key, code in data.items():
242
            # Some things we never want to use.
243
            if key in ["_bases", "_ormbases"]:
244
                continue
245
            # Some things we don't want with stubs.
246
            if stub and key in ["order_with_respect_to"]:
247
                continue
248
            # OK, add it.
249
            try:
250
                results[key] = self.eval_in_context(code, app)
251
            except (NameError, AttributeError), e:
252
                raise ValueError("Cannot successfully create meta field '%s' for model '%s.%s': %s." % (
253
                    key, app, model, e
254
                ))
255
        return type("Meta", tuple(), results) 
256
    
257
    
258
    def make_model(self, app, name, data):
259
        "Makes a Model class out of the given app name, model name and pickled data."
260
        
261
        # Extract any bases out of Meta
262
        if "_ormbases" in data['Meta']:
263
            # Make sure everything we depend on is done already; otherwise, wait.
264
            for key in data['Meta']['_ormbases']:
265
                key = key.lower()
266
                if key not in self.models:
267
                    raise ORMBaseNotIncluded("Cannot find ORM base %s" % key)
268
                elif isinstance(self.models[key], basestring):
269
                    # Then the other model hasn't been unfrozen yet.
270
                    # We postpone ourselves; the situation will eventually resolve.
271
                    raise UnfreezeMeLater()
272
            bases = [self.models[key.lower()] for key in data['Meta']['_ormbases']]
273
        # Perhaps the old style?
274
        elif "_bases" in data['Meta']:
275
            bases = map(ask_for_it_by_name, data['Meta']['_bases'])
276
        # Ah, bog standard, then.
277
        else:
278
            bases = [models.Model]
279
        
280
        # Turn the Meta dict into a basic class
281
        meta = self.make_meta(app, name, data['Meta'], data.get("_stub", False))
282
        
283
        failed_fields = {}
284
        fields = {}
285
        stub = False
286
        
287
        # Now, make some fields!
288
        for fname, params in data.items():
289
            # If it's the stub marker, ignore it.
290
            if fname == "_stub":
291
                stub = bool(params)
292
                continue
293
            elif fname == "Meta":
294
                continue
295
            elif not params:
296
                raise ValueError("Field '%s' on model '%s.%s' has no definition." % (fname, app, name))
297
            elif isinstance(params, (str, unicode)):
298
                # It's a premade definition string! Let's hope it works...
299
                code = params
300
                extra_imports = {}
301
            else:
302
                # If there's only one parameter (backwards compat), make it 3.
303
                if len(params) == 1:
304
                    params = (params[0], [], {})
305
                # There should be 3 parameters. Code is a tuple of (code, what-to-import)
306
                if len(params) == 3:
307
                    code = "SouthFieldClass(%s)" % ", ".join(
308
                        params[1] +
309
                        ["%s=%s" % (n, v) for n, v in params[2].items()]
310
                    )
311
                    extra_imports = {"SouthFieldClass": params[0]}
312
                else:
313
                    raise ValueError("Field '%s' on model '%s.%s' has a weird definition length (should be 1 or 3 items)." % (fname, app, name))
314
            
315
            try:
316
                # Execute it in a probably-correct context.
317
                field = self.eval_in_context(code, app, extra_imports)
318
            except (NameError, AttributeError, AssertionError, KeyError):
319
                # It might rely on other models being around. Add it to the
320
                # model for the second pass.
321
                failed_fields[fname] = (code, extra_imports)
322
            else:
323
                fields[fname] = field
324
        
325
        # Find the app in the Django core, and get its module
326
        more_kwds = {}
327
        try:
328
            app_module = models.get_app(app)
329
            more_kwds['__module__'] = app_module.__name__
330
        except ImproperlyConfigured:
331
            # The app this belonged to has vanished, but thankfully we can still
332
            # make a mock model, so ignore the error.
333
            more_kwds['__module__'] = '_south_mock'
334
        
335
        more_kwds['Meta'] = meta
336
        
337
        # Make our model
338
        fields.update(more_kwds)
339
        
340
        model = type(
341
            str(name),
342
            tuple(bases),
343
            fields,
344
        )
345
        
346
        # If this is a stub model, change Objects to a whiny class
347
        if stub:
348
            model.objects = WhinyManager()
349
            # Also, make sure they can't instantiate it
350
            model.__init__ = whiny_method
351
        else:
352
            model.objects = NoDryRunManager(model.objects)
353
        
354
        if failed_fields:
355
            model._failed_fields = failed_fields
356
        
357
        return model
358
    
359
    def retry_failed_fields(self):
360
        "Tries to re-evaluate the _failed_fields for each model."
361
        for modelkey, model in self.models.items():
362
            app, modelname = modelkey.split(".", 1)
363
            if hasattr(model, "_failed_fields"):
364
                for fname, (code, extra_imports) in model._failed_fields.items():
365
                    try:
366
                        field = self.eval_in_context(code, app, extra_imports)
367
                    except (NameError, AttributeError, AssertionError, KeyError), e:
368
                        # It's failed again. Complain.
369
                        raise ValueError("Cannot successfully create field '%s' for model '%s': %s." % (
370
                            fname, modelname, e
371
                        ))
372
                    else:
373
                        # Startup that field.
374
                        model.add_to_class(fname, field)
375

    
376

    
377
class WhinyManager(object):
378
    "A fake manager that whines whenever you try to touch it. For stub models."
379
    
380
    def __getattr__(self, key):
381
        raise AttributeError("You cannot use items from a stub model.")
382

    
383

    
384
class NoDryRunManager(object):
385
    """
386
    A manager that always proxies through to the real manager,
387
    unless a dry run is in progress.
388
    """
389
    
390
    def __init__(self, real):
391
        self.real = real
392
    
393
    def __getattr__(self, name):
394
        if db.dry_run:
395
            raise AttributeError("You are in a dry run, and cannot access the ORM.\nWrap ORM sections in 'if not db.dry_run:', or if the whole migration is only a data migration, set no_dry_run = True on the Migration class.")
396
        return getattr(self.real, name)
397

    
398

    
399
def whiny_method(*a, **kw):
400
    raise ValueError("You cannot instantiate a stub model.")