해당 글에서는 장고의 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()를 실행한다.
레퍼런스
'서버(Server) > 장고 (Django)' 카테고리의 다른 글
[Django Basic] 3. URL, urls.py, URLConf, path (0) | 2023.07.15 |
---|---|
[Django Basic] 2. Model, models.py, Database (0) | 2023.07.15 |
[Django Basic] 1.3. manage.py, django-admin (0) | 2023.07.15 |
[Django Basic] 1.2. 장고 앱 배포하기 (0) | 2023.07.15 |
[Django Basic] 1.1. 파이썬에서의 가상 환경 (0) | 2023.07.15 |