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 |