Project

General

Profile

Statistics
| Branch: | Revision:

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

History | View | Annotate | Download (17 KB)

1
"""
2
Like the old south.modelsparser, but using introspection where possible
3
rather than direct inspection of models.py.
4
"""
5

    
6
import datetime
7
import re
8
import decimal
9

    
10
from south.utils import get_attribute, auto_through
11

    
12
from django.db import models
13
from django.db.models.base import ModelBase, Model
14
from django.db.models.fields import NOT_PROVIDED
15
from django.conf import settings
16
from django.utils.functional import Promise
17
from django.contrib.contenttypes import generic
18
from django.utils.datastructures import SortedDict
19
from django.utils import datetime_safe
20

    
21
NOISY = False
22

    
23
try:
24
    from django.utils import timezone
25
except ImportError:
26
    timezone = False
27

    
28

    
29
# Define any converter functions first to prevent NameErrors
30

    
31
def convert_on_delete_handler(value):
32
    django_db_models_module = 'models'  # relative to standard import 'django.db'
33
    if hasattr(models, "PROTECT"):
34
        if value in (models.CASCADE, models.PROTECT, models.DO_NOTHING, models.SET_DEFAULT):
35
            # straightforward functions
36
            return '%s.%s' % (django_db_models_module, value.__name__)
37
        else:
38
            # This is totally dependent on the implementation of django.db.models.deletion.SET
39
            func_name = getattr(value, '__name__', None)
40
            if func_name == 'set_on_delete':
41
                # we must inspect the function closure to see what parameters were passed in
42
                closure_contents = value.func_closure[0].cell_contents
43
                if closure_contents is None:
44
                    return "%s.SET_NULL" % (django_db_models_module)
45
                # simple function we can perhaps cope with:
46
                elif hasattr(closure_contents, '__call__'):
47
                    raise ValueError("South does not support on_delete with SET(function) as values.")
48
                else:
49
                    # Attempt to serialise the value
50
                    return "%s.SET(%s)" % (django_db_models_module, value_clean(closure_contents))
51
        raise ValueError("%s was not recognized as a valid model deletion handler. Possible values: %s." % (value, ', '.join(f.__name__ for f in (models.CASCADE, models.PROTECT, models.SET, models.SET_NULL, models.SET_DEFAULT, models.DO_NOTHING))))
52
    else:
53
        raise ValueError("on_delete argument encountered in Django version that does not support it")
54

    
55
# Gives information about how to introspect certain fields.
56
# This is a list of triples; the first item is a list of fields it applies to,
57
# (note that isinstance is used, so superclasses are perfectly valid here)
58
# the second is a list of positional argument descriptors, and the third
59
# is a list of keyword argument descriptors.
60
# Descriptors are of the form:
61
#  [attrname, options]
62
# Where attrname is the attribute on the field to get the value from, and options
63
# is an optional dict.
64
#
65
# The introspector uses the combination of all matching entries, in order.
66
                                     
67
introspection_details = [
68
    (
69
        (models.Field, ),
70
        [],
71
        {
72
            "null": ["null", {"default": False}],
73
            "blank": ["blank", {"default": False, "ignore_if":"primary_key"}],
74
            "primary_key": ["primary_key", {"default": False}],
75
            "max_length": ["max_length", {"default": None}],
76
            "unique": ["_unique", {"default": False}],
77
            "db_index": ["db_index", {"default": False}],
78
            "default": ["default", {"default": NOT_PROVIDED, "ignore_dynamics": True}],
79
            "db_column": ["db_column", {"default": None}],
80
            "db_tablespace": ["db_tablespace", {"default": settings.DEFAULT_INDEX_TABLESPACE}],
81
        },
82
    ),
83
    (
84
        (models.ForeignKey, models.OneToOneField),
85
        [],
86
        dict([
87
            ("to", ["rel.to", {}]),
88
            ("to_field", ["rel.field_name", {"default_attr": "rel.to._meta.pk.name"}]),
89
            ("related_name", ["rel.related_name", {"default": None}]),
90
            ("db_index", ["db_index", {"default": True}]),
91
            ("on_delete", ["rel.on_delete", {"default": getattr(models, "CASCADE", None), "is_django_function": True, "converter": convert_on_delete_handler, "ignore_missing": True}])
92
        ])
93
    ),
94
    (
95
        (models.ManyToManyField,),
96
        [],
97
        {
98
            "to": ["rel.to", {}],
99
            "symmetrical": ["rel.symmetrical", {"default": True}],
100
            "related_name": ["rel.related_name", {"default": None}],
101
            "db_table": ["db_table", {"default": None}],
102
            # TODO: Kind of ugly to add this one-time-only option
103
            "through": ["rel.through", {"ignore_if_auto_through": True}],
104
        },
105
    ),
106
    (
107
        (models.DateField, models.TimeField),
108
        [],
109
        {
110
            "auto_now": ["auto_now", {"default": False}],
111
            "auto_now_add": ["auto_now_add", {"default": False}],
112
        },
113
    ),
114
    (
115
        (models.DecimalField, ),
116
        [],
117
        {
118
            "max_digits": ["max_digits", {"default": None}],
119
            "decimal_places": ["decimal_places", {"default": None}],
120
        },
121
    ),
122
    (
123
        (models.SlugField, ),
124
        [],
125
        {
126
            "db_index": ["db_index", {"default": True}],
127
        },
128
    ),
129
    (
130
        (models.BooleanField, ),
131
        [],
132
        {
133
            "default": ["default", {"default": NOT_PROVIDED, "converter": bool}],
134
            "blank": ["blank", {"default": True, "ignore_if":"primary_key"}],
135
        },
136
    ),
137
    (
138
        (models.FilePathField, ),
139
        [],
140
        {
141
            "path": ["path", {"default": ''}],
142
            "match": ["match", {"default": None}],
143
            "recursive": ["recursive", {"default": False}],
144
        },
145
    ),
146
    (
147
        (generic.GenericRelation, ),
148
        [],
149
        {
150
            "to": ["rel.to", {}],
151
            "symmetrical": ["rel.symmetrical", {"default": True}],
152
            "object_id_field": ["object_id_field_name", {"default": "object_id"}],
153
            "content_type_field": ["content_type_field_name", {"default": "content_type"}],
154
            "blank": ["blank", {"default": True}],
155
        },
156
    ),
157
]
158

    
159
# Regexes of allowed field full paths
160
allowed_fields = [
161
    "^django\.db",
162
    "^django\.contrib\.contenttypes\.generic",
163
    "^django\.contrib\.localflavor",
164
]
165

    
166
# Regexes of ignored fields (custom fields which look like fields, but have no column behind them)
167
ignored_fields = [
168
    "^django\.contrib\.contenttypes\.generic\.GenericRelation",
169
    "^django\.contrib\.contenttypes\.generic\.GenericForeignKey",
170
]
171

    
172
# Similar, but for Meta, so just the inner level (kwds).
173
meta_details = {
174
    "db_table": ["db_table", {"default_attr_concat": ["%s_%s", "app_label", "module_name"]}],
175
    "db_tablespace": ["db_tablespace", {"default": settings.DEFAULT_TABLESPACE}],
176
    "unique_together": ["unique_together", {"default": []}],
177
    "ordering": ["ordering", {"default": []}],
178
    "proxy": ["proxy", {"default": False, "ignore_missing": True}],
179
}
180

    
181
# 2.4 compatability
182
any = lambda x: reduce(lambda y, z: y or z, x, False)
183

    
184

    
185
def add_introspection_rules(rules=[], patterns=[]):
186
    "Allows you to add some introspection rules at runtime, e.g. for 3rd party apps."
187
    assert isinstance(rules, (list, tuple))
188
    assert isinstance(patterns, (list, tuple))
189
    allowed_fields.extend(patterns)
190
    introspection_details.extend(rules)
191

    
192

    
193
def add_ignored_fields(patterns):
194
    "Allows you to add some ignore field patterns."
195
    assert isinstance(patterns, (list, tuple))
196
    ignored_fields.extend(patterns)
197
    
198

    
199
def can_ignore(field):
200
    """
201
    Returns True if we know for certain that we can ignore this field, False
202
    otherwise.
203
    """
204
    full_name = "%s.%s" % (field.__class__.__module__, field.__class__.__name__)
205
    for regex in ignored_fields:
206
        if re.match(regex, full_name):
207
            return True
208
    return False
209

    
210

    
211
def can_introspect(field):
212
    """
213
    Returns True if we are allowed to introspect this field, False otherwise.
214
    ('allowed' means 'in core'. Custom fields can declare they are introspectable
215
    by the default South rules by adding the attribute _south_introspects = True.)
216
    """
217
    # Check for special attribute
218
    if hasattr(field, "_south_introspects") and field._south_introspects:
219
        return True
220
    # Check it's an introspectable field
221
    full_name = "%s.%s" % (field.__class__.__module__, field.__class__.__name__)
222
    for regex in allowed_fields:
223
        if re.match(regex, full_name):
224
            return True
225
    return False
226

    
227

    
228
def matching_details(field):
229
    """
230
    Returns the union of all matching entries in introspection_details for the field.
231
    """
232
    our_args = []
233
    our_kwargs = {}
234
    for classes, args, kwargs in introspection_details:
235
        if any([isinstance(field, x) for x in classes]):
236
            our_args.extend(args)
237
            our_kwargs.update(kwargs)
238
    return our_args, our_kwargs
239

    
240

    
241
class IsDefault(Exception):
242
    """
243
    Exception for when a field contains its default value.
244
    """
245

    
246

    
247
def get_value(field, descriptor):
248
    """
249
    Gets an attribute value from a Field instance and formats it.
250
    """
251
    attrname, options = descriptor
252
    # If the options say it's not a attribute name but a real value, use that.
253
    if options.get('is_value', False):
254
        value = attrname
255
    else:
256
        try:
257
            value = get_attribute(field, attrname)
258
        except AttributeError:
259
            if options.get("ignore_missing", False):
260
                raise IsDefault
261
            else:
262
                raise
263
            
264
    # Lazy-eval functions get eval'd.
265
    if isinstance(value, Promise):
266
        value = unicode(value)
267
    # If the value is the same as the default, omit it for clarity
268
    if "default" in options and value == options['default']:
269
        raise IsDefault
270
    # If there's an ignore_if, use it
271
    if "ignore_if" in options:
272
        if get_attribute(field, options['ignore_if']):
273
            raise IsDefault
274
    # If there's an ignore_if_auto_through which is True, use it
275
    if options.get("ignore_if_auto_through", False):
276
        if auto_through(field):
277
            raise IsDefault
278
    # Some default values need to be gotten from an attribute too.
279
    if "default_attr" in options:
280
        default_value = get_attribute(field, options['default_attr'])
281
        if value == default_value:
282
            raise IsDefault
283
    # Some are made from a formatting string and several attrs (e.g. db_table)
284
    if "default_attr_concat" in options:
285
        format, attrs = options['default_attr_concat'][0], options['default_attr_concat'][1:]
286
        default_value = format % tuple(map(lambda x: get_attribute(field, x), attrs))
287
        if value == default_value:
288
            raise IsDefault
289
    # Clean and return the value
290
    return value_clean(value, options)
291

    
292

    
293
def value_clean(value, options={}):
294
    "Takes a value and cleans it up (so e.g. it has timezone working right)"
295
    # Lazy-eval functions get eval'd.
296
    if isinstance(value, Promise):
297
        value = unicode(value)
298
    # Callables get called.
299
    if not options.get('is_django_function', False) and callable(value) and not isinstance(value, ModelBase):
300
        # Datetime.datetime.now is special, as we can access it from the eval
301
        # context (and because it changes all the time; people will file bugs otherwise).
302
        if value == datetime.datetime.now:
303
            return "datetime.datetime.now"
304
        elif value == datetime.datetime.utcnow:
305
            return "datetime.datetime.utcnow"
306
        elif value == datetime.date.today:
307
            return "datetime.date.today"
308
        # In case we use Django's own now function, revert to datetime's
309
        # original one since we'll deal with timezones on our own.
310
        elif timezone and value == timezone.now:
311
            return "datetime.datetime.now"
312
        # All other callables get called.
313
        value = value()
314
    # Models get their own special repr()
315
    if isinstance(value, ModelBase):
316
        # If it's a proxy model, follow it back to its non-proxy parent
317
        if getattr(value._meta, "proxy", False):
318
            value = value._meta.proxy_for_model
319
        return "orm['%s.%s']" % (value._meta.app_label, value._meta.object_name)
320
    # As do model instances
321
    if isinstance(value, Model):
322
        if options.get("ignore_dynamics", False):
323
            raise IsDefault
324
        return "orm['%s.%s'].objects.get(pk=%r)" % (value.__class__._meta.app_label, value.__class__._meta.object_name, value.pk)
325
    # Make sure Decimal is converted down into a string
326
    if isinstance(value, decimal.Decimal):
327
        value = str(value)
328
    # in case the value is timezone aware
329
    datetime_types = (
330
        datetime.datetime,
331
        datetime.time,
332
        datetime_safe.datetime,
333
    )
334
    if (timezone and isinstance(value, datetime_types) and
335
            getattr(settings, 'USE_TZ', False) and
336
            value is not None and timezone.is_aware(value)):
337
        default_timezone = timezone.get_default_timezone()
338
        value = timezone.make_naive(value, default_timezone)
339
    # datetime_safe has an improper repr value
340
    if isinstance(value, datetime_safe.datetime):
341
        value = datetime.datetime(*value.utctimetuple()[:7])
342
    # converting a date value to a datetime to be able to handle
343
    # timezones later gracefully
344
    elif isinstance(value, (datetime.date, datetime_safe.date)):
345
        value = datetime.datetime(*value.timetuple()[:3])
346
    # Now, apply the converter func if there is one
347
    if "converter" in options:
348
        value = options['converter'](value)
349
    # Return the final value
350
    if options.get('is_django_function', False):
351
        return value
352
    else:
353
        return repr(value)
354

    
355

    
356
def introspector(field):
357
    """
358
    Given a field, introspects its definition triple.
359
    """
360
    arg_defs, kwarg_defs = matching_details(field)
361
    args = []
362
    kwargs = {}
363
    # For each argument, use the descriptor to get the real value.
364
    for defn in arg_defs:
365
        try:
366
            args.append(get_value(field, defn))
367
        except IsDefault:
368
            pass
369
    for kwd, defn in kwarg_defs.items():
370
        try:
371
            kwargs[kwd] = get_value(field, defn)
372
        except IsDefault:
373
            pass
374
    return args, kwargs
375

    
376

    
377
def get_model_fields(model, m2m=False):
378
    """
379
    Given a model class, returns a dict of {field_name: field_triple} defs.
380
    """
381
    
382
    field_defs = SortedDict()
383
    inherited_fields = {}
384
    
385
    # Go through all bases (that are themselves models, but not Model)
386
    for base in model.__bases__:
387
        if hasattr(base, '_meta') and issubclass(base, models.Model):
388
            if not base._meta.abstract:
389
                # Looks like we need their fields, Ma.
390
                inherited_fields.update(get_model_fields(base))
391
    
392
    # Now, go through all the fields and try to get their definition
393
    source = model._meta.local_fields[:]
394
    if m2m:
395
        source += model._meta.local_many_to_many
396
    
397
    for field in source:
398
        # Can we ignore it completely?
399
        if can_ignore(field):
400
            continue
401
        # Does it define a south_field_triple method?
402
        if hasattr(field, "south_field_triple"):
403
            if NOISY:
404
                print " ( Nativing field: %s" % field.name
405
            field_defs[field.name] = field.south_field_triple()
406
        # Can we introspect it?
407
        elif can_introspect(field):
408
            # Get the full field class path.
409
            field_class = field.__class__.__module__ + "." + field.__class__.__name__
410
            # Run this field through the introspector
411
            args, kwargs = introspector(field)
412
            # Workaround for Django bug #13987
413
            if model._meta.pk.column == field.column and 'primary_key' not in kwargs:
414
                kwargs['primary_key'] = True
415
            # That's our definition!
416
            field_defs[field.name] = (field_class, args, kwargs)
417
        # Shucks, no definition!
418
        else:
419
            if NOISY:
420
                print " ( Nodefing field: %s" % field.name
421
            field_defs[field.name] = None
422
    
423
    # If they've used the horrific hack that is order_with_respect_to, deal with
424
    # it.
425
    if model._meta.order_with_respect_to:
426
        field_defs['_order'] = ("django.db.models.fields.IntegerField", [], {"default": "0"})
427
    
428
    return field_defs
429

    
430

    
431
def get_model_meta(model):
432
    """
433
    Given a model class, will return the dict representing the Meta class.
434
    """
435
    
436
    # Get the introspected attributes
437
    meta_def = {}
438
    for kwd, defn in meta_details.items():
439
        try:
440
            meta_def[kwd] = get_value(model._meta, defn)
441
        except IsDefault:
442
            pass
443
    
444
    # Also, add on any non-abstract model base classes.
445
    # This is called _ormbases as the _bases variable was previously used
446
    # for a list of full class paths to bases, so we can't conflict.
447
    for base in model.__bases__:
448
        if hasattr(base, '_meta') and issubclass(base, models.Model):
449
            if not base._meta.abstract:
450
                # OK, that matches our terms.
451
                if "_ormbases" not in meta_def:
452
                    meta_def['_ormbases'] = []
453
                meta_def['_ormbases'].append("%s.%s" % (
454
                    base._meta.app_label,
455
                    base._meta.object_name,
456
                ))
457
    
458
    return meta_def
459

    
460

    
461
# Now, load the built-in South introspection plugins
462
import south.introspection_plugins