Django application import and missed class_prepared signals
Due to opaque application loading semantics within the Django i18n code, several model classes may be imported before a
class_prepared signal listener connects, resulting in missing the signal entirely. Specifically, connecting to this signal from an application which appears first in
INSTALLED_APPS, does not guarantee that it will execute when a class is first imported by any of the subsequent applications.
class_prepared signal is dispatched when the model class is instantiated for the first time; this happens inside class Model's metaclass at django.db.models.base: 257:
Therefore, we would expect that connecting to this signal from the __init__.py of an app which appears first in the
INSTALLED_APPS list, would ensure that our listener receives the signal for all Models loaded in the remainder apps (following is the beginning of a typical such list):
1 INSTALLED_APPS = ( 2 'class_prepared_listener_app', 3 'django.contrib.auth', 4 'django.contrib.contenttypes', 5 'django.contrib.sessions', 6 'django.contrib.sites', 7 'django.contrib.messages', 8 'django.contrib.admin', 9 ... 10 )
However, this is not (always) the case. Tracing, for example, shell execution, we find that django.core.management.commands.shell: 45:
1 from django.db.models.loading import get_models 2 loaded_models = get_models()
requests all models from the app cache which first calls
_populate to initialize itself at django.db.models.loading: 167:
which loads all apps into the cache the first time it's called and does nothing for subsequent calls at django.db.models.loading: 58:
1 for app_name in settings.INSTALLED_APPS: 2 if app_name in self.handled: 3 continue 4 self.load_app(app_name, True) 5 if not self.nesting_level: 6 for app_name in self.postponed: 7 self.load_app(app_name) 8 self.loaded = True
The apps are loaded in
_populate in the order they're listed in
INSTALLED_APPS; some apps can be postponed though if they can't be imported at the time of traversal at django.db.models.loading: 77:
1 try: 2 models = import_module('.models', app_name) 3 except ImportError: 4 self.nesting_level -= 1 5 # If the app doesn't have a models module, we can just ignore the 6 # ImportError and return no models for it. 7 if not module_has_submodule(app_module, 'models'): 8 return None 9 # But if the app does have a models module, we need to figure out 10 # whether to suppress or propagate the error. If can_postpone is 11 # True then it may be that the package is still being imported by 12 # Python and the models module isn't available yet. So we add the 13 # app to the postponed list and we'll try it again after all the 14 # recursion has finished (in populate). If can_postpone is False 15 # then it's time to raise the ImportError. 16 else: 17 if can_postpone: 18 self.postponed.append(app_name) 19 return None 20 else: 21 raise
A print statement in
_populate indicates that no apps are postponed and that all apps are, in fact, loaded in sequence. Furthermore the signal gets connected before any other apps are loaded. The output of manage.py shell with several print statements:
Signal connected. class_prepared_listener_app django.contrib.auth django.contrib.contenttypes django.contrib.sessions django.contrib.sites django.contrib.messages django.contrib.admin ...
Adding a print statement in the Model's metaclass indicates that some classes are instantiated before their owner apps are loaded:
Preparing class 'django.contrib.contenttypes.models.ContentType' Preparing class 'django.contrib.auth.models.Permission' Preparing class 'django.contrib.auth.models.Group_permissions' Preparing class 'django.contrib.auth.models.Group' Preparing class 'django.contrib.auth.models.User_user_permissions' Preparing class 'django.contrib.auth.models.User_groups' Preparing class 'django.contrib.auth.models.User' Preparing class 'django.contrib.auth.models.Message' Preparing class 'django.contrib.sites.models.Site' Signal connected. Preparing class 'django.contrib.sessions.models.Session' Preparing class 'django.contrib.admin.models.LogEntry' ...
Further snooping shows that these classes are instantiated by translation-related preparation code, before the shell command is properly handled, at django.core.management.base: 202:
1 # Switch to English, because django-admin.py creates database content 2 # like permissions, and those shouldn't contain any translations. 3 # But only do this if we can assume we have a working settings file, 4 # because django.utils.translation requires settings. 5 if self.can_import_settings: 6 try: 7 from django.utils import translation 8 translation.activate('en-us')
Models are instantiated inside the last call to
activate() which eventually calls
translation() at django.utils.translation.trans_real: 161:
1 for appname in reversed(settings.INSTALLED_APPS): 2 app = import_module(appname) 3 apppath = os.path.join(os.path.dirname(app.__file__), 'locale') 4 5 if os.path.isdir(apppath): 6 res = _merge(apppath)
From the above, it is straightforward to see that by removing the admin and auth apps, or by placing our app after the admin and auth apps, our signal connects before any Models are loaded. Neither of these two "solutions" however appear robust or welcome.
It should be noted that, because the call to
translation.activate() is being made inside the execute method of the base class from which all management commands derive, this behaviour is to be expected when Django starts up through commands other than shell.
Admittedly, the official Django documentation for this signal states "Django uses this signal internally; it's not generally used in third-party applications." however, I strongly feel that Django would benefit from a formal and deterministic load order for