293 lines
12 KiB
Python
293 lines
12 KiB
Python
import inspect
|
|
import os
|
|
import warnings
|
|
from importlib import import_module
|
|
|
|
from django.core.exceptions import ImproperlyConfigured
|
|
from django.utils.deprecation import RemovedInDjango41Warning
|
|
from django.utils.module_loading import import_string, module_has_submodule
|
|
|
|
APPS_MODULE_NAME = 'apps'
|
|
MODELS_MODULE_NAME = 'models'
|
|
|
|
|
|
class AppConfig:
|
|
"""Class representing a Django application and its configuration."""
|
|
|
|
def __init__(self, app_name, app_module):
|
|
# Full Python path to the application e.g. 'django.contrib.admin'.
|
|
self.name = app_name
|
|
|
|
# Root module for the application e.g. <module 'django.contrib.admin'
|
|
# from 'django/contrib/admin/__init__.py'>.
|
|
self.module = app_module
|
|
|
|
# Reference to the Apps registry that holds this AppConfig. Set by the
|
|
# registry when it registers the AppConfig instance.
|
|
self.apps = None
|
|
|
|
# The following attributes could be defined at the class level in a
|
|
# subclass, hence the test-and-set pattern.
|
|
|
|
# Last component of the Python path to the application e.g. 'admin'.
|
|
# This value must be unique across a Django project.
|
|
if not hasattr(self, 'label'):
|
|
self.label = app_name.rpartition(".")[2]
|
|
|
|
# Human-readable name for the application e.g. "Admin".
|
|
if not hasattr(self, 'verbose_name'):
|
|
self.verbose_name = self.label.title()
|
|
|
|
# Filesystem path to the application directory e.g.
|
|
# '/path/to/django/contrib/admin'.
|
|
if not hasattr(self, 'path'):
|
|
self.path = self._path_from_module(app_module)
|
|
|
|
# Module containing models e.g. <module 'django.contrib.admin.models'
|
|
# from 'django/contrib/admin/models.py'>. Set by import_models().
|
|
# None if the application doesn't have a models module.
|
|
self.models_module = None
|
|
|
|
# Mapping of lowercase model names to model classes. Initially set to
|
|
# None to prevent accidental access before import_models() runs.
|
|
self.models = None
|
|
|
|
def __repr__(self):
|
|
return '<%s: %s>' % (self.__class__.__name__, self.label)
|
|
|
|
def _path_from_module(self, module):
|
|
"""Attempt to determine app's filesystem path from its module."""
|
|
# See #21874 for extended discussion of the behavior of this method in
|
|
# various cases.
|
|
# Convert paths to list because Python's _NamespacePath doesn't support
|
|
# indexing.
|
|
paths = list(getattr(module, '__path__', []))
|
|
if len(paths) != 1:
|
|
filename = getattr(module, '__file__', None)
|
|
if filename is not None:
|
|
paths = [os.path.dirname(filename)]
|
|
else:
|
|
# For unknown reasons, sometimes the list returned by __path__
|
|
# contains duplicates that must be removed (#25246).
|
|
paths = list(set(paths))
|
|
if len(paths) > 1:
|
|
raise ImproperlyConfigured(
|
|
"The app module %r has multiple filesystem locations (%r); "
|
|
"you must configure this app with an AppConfig subclass "
|
|
"with a 'path' class attribute." % (module, paths))
|
|
elif not paths:
|
|
raise ImproperlyConfigured(
|
|
"The app module %r has no filesystem location, "
|
|
"you must configure this app with an AppConfig subclass "
|
|
"with a 'path' class attribute." % module)
|
|
return paths[0]
|
|
|
|
@classmethod
|
|
def create(cls, entry):
|
|
"""
|
|
Factory that creates an app config from an entry in INSTALLED_APPS.
|
|
"""
|
|
# create() eventually returns app_config_class(app_name, app_module).
|
|
app_config_class = None
|
|
app_config_name = None
|
|
app_name = None
|
|
app_module = None
|
|
|
|
# If import_module succeeds, entry points to the app module.
|
|
try:
|
|
app_module = import_module(entry)
|
|
except Exception:
|
|
pass
|
|
else:
|
|
# If app_module has an apps submodule that defines a single
|
|
# AppConfig subclass, use it automatically.
|
|
# To prevent this, an AppConfig subclass can declare a class
|
|
# variable default = False.
|
|
# If the apps module defines more than one AppConfig subclass,
|
|
# the default one can declare default = True.
|
|
if module_has_submodule(app_module, APPS_MODULE_NAME):
|
|
mod_path = '%s.%s' % (entry, APPS_MODULE_NAME)
|
|
mod = import_module(mod_path)
|
|
# Check if there's exactly one AppConfig candidate,
|
|
# excluding those that explicitly define default = False.
|
|
app_configs = [
|
|
(name, candidate)
|
|
for name, candidate in inspect.getmembers(mod, inspect.isclass)
|
|
if (
|
|
issubclass(candidate, cls) and
|
|
candidate is not cls and
|
|
getattr(candidate, 'default', True)
|
|
)
|
|
]
|
|
if len(app_configs) == 1:
|
|
app_config_class = app_configs[0][1]
|
|
app_config_name = '%s.%s' % (mod_path, app_configs[0][0])
|
|
else:
|
|
# Check if there's exactly one AppConfig subclass,
|
|
# among those that explicitly define default = True.
|
|
app_configs = [
|
|
(name, candidate)
|
|
for name, candidate in app_configs
|
|
if getattr(candidate, 'default', False)
|
|
]
|
|
if len(app_configs) > 1:
|
|
candidates = [repr(name) for name, _ in app_configs]
|
|
raise RuntimeError(
|
|
'%r declares more than one default AppConfig: '
|
|
'%s.' % (mod_path, ', '.join(candidates))
|
|
)
|
|
elif len(app_configs) == 1:
|
|
app_config_class = app_configs[0][1]
|
|
app_config_name = '%s.%s' % (mod_path, app_configs[0][0])
|
|
|
|
# If app_module specifies a default_app_config, follow the link.
|
|
# default_app_config is deprecated, but still takes over the
|
|
# automatic detection for backwards compatibility during the
|
|
# deprecation period.
|
|
try:
|
|
new_entry = app_module.default_app_config
|
|
except AttributeError:
|
|
# Use the default app config class if we didn't find anything.
|
|
if app_config_class is None:
|
|
app_config_class = cls
|
|
app_name = entry
|
|
else:
|
|
message = (
|
|
'%r defines default_app_config = %r. ' % (entry, new_entry)
|
|
)
|
|
if new_entry == app_config_name:
|
|
message += (
|
|
'Django now detects this configuration automatically. '
|
|
'You can remove default_app_config.'
|
|
)
|
|
else:
|
|
message += (
|
|
"However, Django's automatic detection %s. You should "
|
|
"move the default config class to the apps submodule "
|
|
"of your application and, if this module defines "
|
|
"several config classes, mark the default one with "
|
|
"default = True." % (
|
|
"picked another configuration, %r" % app_config_name
|
|
if app_config_name
|
|
else "did not find this configuration"
|
|
)
|
|
)
|
|
warnings.warn(message, RemovedInDjango41Warning, stacklevel=2)
|
|
entry = new_entry
|
|
app_config_class = None
|
|
|
|
# If import_string succeeds, entry is an app config class.
|
|
if app_config_class is None:
|
|
try:
|
|
app_config_class = import_string(entry)
|
|
except Exception:
|
|
pass
|
|
# If both import_module and import_string failed, it means that entry
|
|
# doesn't have a valid value.
|
|
if app_module is None and app_config_class is None:
|
|
# If the last component of entry starts with an uppercase letter,
|
|
# then it was likely intended to be an app config class; if not,
|
|
# an app module. Provide a nice error message in both cases.
|
|
mod_path, _, cls_name = entry.rpartition('.')
|
|
if mod_path and cls_name[0].isupper():
|
|
# We could simply re-trigger the string import exception, but
|
|
# we're going the extra mile and providing a better error
|
|
# message for typos in INSTALLED_APPS.
|
|
# This may raise ImportError, which is the best exception
|
|
# possible if the module at mod_path cannot be imported.
|
|
mod = import_module(mod_path)
|
|
candidates = [
|
|
repr(name)
|
|
for name, candidate in inspect.getmembers(mod, inspect.isclass)
|
|
if issubclass(candidate, cls) and candidate is not cls
|
|
]
|
|
msg = "Module '%s' does not contain a '%s' class." % (mod_path, cls_name)
|
|
if candidates:
|
|
msg += ' Choices are: %s.' % ', '.join(candidates)
|
|
raise ImportError(msg)
|
|
else:
|
|
# Re-trigger the module import exception.
|
|
import_module(entry)
|
|
|
|
# Check for obvious errors. (This check prevents duck typing, but
|
|
# it could be removed if it became a problem in practice.)
|
|
if not issubclass(app_config_class, AppConfig):
|
|
raise ImproperlyConfigured(
|
|
"'%s' isn't a subclass of AppConfig." % entry)
|
|
|
|
# Obtain app name here rather than in AppClass.__init__ to keep
|
|
# all error checking for entries in INSTALLED_APPS in one place.
|
|
if app_name is None:
|
|
try:
|
|
app_name = app_config_class.name
|
|
except AttributeError:
|
|
raise ImproperlyConfigured(
|
|
"'%s' must supply a name attribute." % entry
|
|
)
|
|
|
|
# Ensure app_name points to a valid module.
|
|
try:
|
|
app_module = import_module(app_name)
|
|
except ImportError:
|
|
raise ImproperlyConfigured(
|
|
"Cannot import '%s'. Check that '%s.%s.name' is correct." % (
|
|
app_name,
|
|
app_config_class.__module__,
|
|
app_config_class.__qualname__,
|
|
)
|
|
)
|
|
|
|
# Entry is a path to an app config class.
|
|
return app_config_class(app_name, app_module)
|
|
|
|
def get_model(self, model_name, require_ready=True):
|
|
"""
|
|
Return the model with the given case-insensitive model_name.
|
|
|
|
Raise LookupError if no model exists with this name.
|
|
"""
|
|
if require_ready:
|
|
self.apps.check_models_ready()
|
|
else:
|
|
self.apps.check_apps_ready()
|
|
try:
|
|
return self.models[model_name.lower()]
|
|
except KeyError:
|
|
raise LookupError(
|
|
"App '%s' doesn't have a '%s' model." % (self.label, model_name))
|
|
|
|
def get_models(self, include_auto_created=False, include_swapped=False):
|
|
"""
|
|
Return an iterable of models.
|
|
|
|
By default, the following models aren't included:
|
|
|
|
- auto-created models for many-to-many relations without
|
|
an explicit intermediate table,
|
|
- models that have been swapped out.
|
|
|
|
Set the corresponding keyword argument to True to include such models.
|
|
Keyword arguments aren't documented; they're a private API.
|
|
"""
|
|
self.apps.check_models_ready()
|
|
for model in self.models.values():
|
|
if model._meta.auto_created and not include_auto_created:
|
|
continue
|
|
if model._meta.swapped and not include_swapped:
|
|
continue
|
|
yield model
|
|
|
|
def import_models(self):
|
|
# Dictionary of models for this app, primarily maintained in the
|
|
# 'all_models' attribute of the Apps this AppConfig is attached to.
|
|
self.models = self.apps.all_models[self.label]
|
|
|
|
if module_has_submodule(self.module, MODELS_MODULE_NAME):
|
|
models_module_name = '%s.%s' % (self.name, MODELS_MODULE_NAME)
|
|
self.models_module = import_module(models_module_name)
|
|
|
|
def ready(self):
|
|
"""
|
|
Override this method in subclasses to run code when Django starts.
|
|
"""
|