[Django Basic] 1.4. Applications, apps.py, AppConfig
해당 글에서는 장고의 Applications, apps.py 파일에 관해 다룬다.
장고에서 앱이란 재사용 가능한 단위의 모듈을 의미한다. 앱은 모델, 뷰, 템플릿, 정적파일, URLs, 미들웨어등 다양한 것으로 이루어져 있다. 이를 프로젝트 설정의 INSTALLED_APPS에 넣어 앱을 프로젝트에 포함시킨다.
사실 apps.py는 기본적으로 크게 다루는 일이 적다. 그래서 이후에 소개할일이 없을 듯 하여, Django Basic 1장에서 간단히 소개해보려 한다.
INSTALLED_APPS
settings.py의 INSTALLED_APPS를 보면 기본적으로 다음과 같은 앱들이 포함되어 있다.
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
INSTALLED_APPS에 추가되는 문자열의 규칙은 다음과 같다
A list of strings designating all applications that are enabled in this Django installation. Each string should be a dotted Python path to:
an application configuration class (preferred), or
a package containing an application.
즉, 앱의 설정을 정의한 Class 또는 앱을 포함하는 패키지명을 작성하는 것이다.
보통 제작된 앱을 추가할 때, 아래와 같이 앱의 패키지명을 작성하여 추가한다.
# django_basic/settings.py
INSTALLED_APPS = [
...
'books'
]
그러나 실제로 장고에서 추천하는 방법은 Config명을 넣는 것이다.
# django_basic/settings.py
INSTALLED_APPS = [
...
'books.apps.BooksConfig'
]
이는 books 앱 생성 시 books/apps.py 에 자동 생성된 컨피그를 말하는 것이다.
# books/apps.py
from django.apps import AppConfig
class BooksConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "books"
이때 AppConfig를 상속받은 컨피그 들을 subclass라고 부른다.
apps.py
위에서 INSTALLED_APPS에 어떤 것을 포함해줘야 하는지는 알아보았으니, 이제 apps.py에 포함되는 AppConfig를 다루는 법을 알아보자.
여러 개의 Config 정의하는 방법
# django_basic/settings.py
INSTALLED_APPS = [
...
"books",
]
# books/apps.py
class BooksConfig(AppConfig):
...
default = False
class AnotherBooksConfig(AppConfig):
...
default = True # 이러면 해당 books 패키지명으로 로드 했을 때, 해당 컨피그가 로드 된다.
BooksConfig는 INSTALLED_APPS에서 해당 앱을 가져올 때 로드 된다. 이때 INSTALLED_APPS에 'books'라고 패키지 명을 적으면 자동으로 BooksConfig를 로드할 것이다. 이를 방지하려면 default = False를 정의해 주면 된다.
여러 개의 AppConfig를 정의할 수 있는데, 이중 로드될 것에 default = True로 정의해 줌으로써 여러 컨피그 중 기본 패키지명으로 로드될 컨피그를 지정해 줄 수 있다. 다른 컨피그를 사용하고 싶다면 위와 같이 INSTALLED_APPS에 패키지명이 아닌 컨피그 명을 기술하여 'books.apps.AnotherBooksConfig'와 같이 정의해 주면 된다.
AppConfig는 반드시 apps.py에 정의되지 않아도 된다. INSTALLED_APPS에 경로만 잘 적어둔다면 말이다.
App import시 앱 이름 변경방법
다른 사람이 만든 앱을 import 해서 사용할 때, 나타나는 이름을 변경하고 싶을 때는 어떻게 해야 할까?
# django_basic2/apps.py
from books.apps import BooksConfig
class BooooksConfig(BooksConfig):
verbose_name = "Boo oo ks"
# django_basic2/settings.py
INSTALLED_APPS = [
...
'django_basic2.apps.BooooksConfig',
]
위는 django_basic2라는 새로운 프로젝트에서 books앱의 이름을 Boo oo ks로 바꿔서 사용하는 예시이다. 이 경우에는 당연하게도 INSTALLED_APPS에서 정확한 경로를 적어주어야 한다.
AppConfig
AppConfig 내의 설정할 수 있는 것들을 살펴보겠다.
import inspect
import os
from importlib import import_module
from django.core.exceptions import ImproperlyConfigured
from django.utils.functional import cached_property
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]
if not self.label.isidentifier():
raise ImproperlyConfigured(
"The app label '%s' is not a valid Python identifier." % self.label
)
# 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)
@cached_property
def default_auto_field(self):
from django.conf import settings
return settings.DEFAULT_AUTO_FIELD
@property
def _is_default_auto_field_overridden(self):
return self.__class__.default_auto_field is not AppConfig.default_auto_field
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 to list because __path__ may not 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_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]
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]
# 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
# 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.
"""
AppConfig.name
class BooksConfig(AppConfig):
name = "books"
해당 컨피그가 적용되는 앱을 알려준다. 앱까지의 경로를 작성해 주면 된다. 반드시 포함되어야 하며, 모든 장고 프로젝트를 통틀어 unique 해야 한다.
AppConfig.label
AppConfig.name이 중복될 때 구별 가능한 다른 이름을 설정할 수 있게 한다. 장고 프로젝트를 통틀어 unique 해야 한다.
AppConfig.verbose_name
사람이 읽는 앱의 이름을 설정한다. 기본값은 label.title()로 설정된다.
AppConfig.path
앱의 경로. 기본적으로 장고가 알아서 설정한다. 만약 app package가 namespace package일 경우 직접 설정해 줄 필요가 있다.
AppConfig.default
위에서 설명한 기능.
AppConfig.default_auto_field
primary key 필드의 타입을 정한다. 기본값은 settings.py의 DEFAULT_AUTO_FIELD로 적용된다.
AppConfig.get_models(include_auto_created=False, include_swapped=False)
앱의 iterable Model Class를 반환한다.
AppConfig.get_model(model_name, require_ready=True)
model_name에 일치하는 모델을 반환한다. 이때 모델명은 Case insensitive 하다. 모델이 없을 시 LookupError를 Raise 한다.
AppConfig.ready()
subclass에서 해당 메서드를 오버라이딩하여, app registry가 fully polulated 된 후 실행될 작업을 설정할 수 있다. 이하는 Django docs 공식 예시
from django.apps import AppConfig
from django.db.models.signals import pre_save
class RockNRollConfig(AppConfig):
# ...
def ready(self):
# importing model classes
from .models import MyModel # or...
MyModel = self.get_model('MyModel')
# registering signals with the model's string label
pre_save.connect(receiver, sender='app_label.MyModel')
apps
apps는 다음과 같이 import 한다.
from django.apps import apps
apps.ready: 앱 registry들이 모두 준비되었는지에 대한 Boolean 값을 반환한다.
또한 아래와 같은 메서드를 사용할 수 있다.
apps.get_app_configs()
apps.get_app_config(app_label)
apps.is_installed(app_name)
apps.get_model(app_label, model_name, require_ready=True)
Initialization process
장고 시작 시 django.setup()이 앱 registry를 설정한다.
1. INSTALLED_APPS의 각 앱을 로드한다
1-1. 앱 컨피그일 경우 name에 정의된 패키지를 import 한다
1-2. 파이썬 패키지일 경우 apps.py 에서 submodule을 찾아보고, 없으면 기본값의 앱 컨피그를 생성한다
1단계가 완료되면 apps.get_app_config()가 사용 가능해진다.
2. 각 submodule의 model을 import 한다.
2단계가 완료되면 apps.get_model()가 사용 가능해진다.
3. 각 앱 콘피그별 AppConfig.ready()를 실행한다.