はじめに
この記事は、Django Advent Calendar 2016 2日目の記事です。
ここでやること
自分自身の勉強も兼ねて、Djangoのエントリポイントを追っていきます。
Djangoアプリケーションには2通りの実行方法があり、それぞれエントリポイントが異なります。
manage.py
wsgi.py
今回はmanage.py
を取り上げます。
参考にしたもの
tokibito先生の解説がとてもわかりやすく、勉強になります。
この記事に書いたことは全て網羅されています。
バージョン
- Django 1.10.3
プロジェクト作成
$ django-admin startproject mysite && cd mysite
$ tree
.
├── manage.py
└── mysite
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py
1 directory, 5 files
実行コマンド
$ ./manage.py runserver
これでアプリケーションが実行できます。
それでは、内部のソースコードを読んでいきましょう。
コードリーディング
manage.py
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
try:
from django.core.management import execute_from_command_line
except ImportError:
# The above import may fail for some other reason. Ensure that the
# issue is really that Django is missing to avoid masking other
# exceptions on Python 2.
try:
import django
except ImportError:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
)
raise
execute_from_command_line(sys.argv)
- 環境変数のデフォルト設定
-
execute_from_command_line
メソッド実行
※ Django1.10からImportError
の例外処理が加わりました。
django/core/management/__init__.py
def execute_from_command_line(argv=None):
"""
A simple method that runs a ManagementUtility.
"""
utility = ManagementUtility(argv)
utility.execute()
-
ManagementUtility
の初期化 -
execute
メソッド実行
ManagementUtility
クラスはdjango-admin
とmanage.py
で実行できるコマンド群をカプセル化しているものです。
class ManagementUtility(object):
"""
Encapsulates the logic of the django-admin and manage.py utilities.
A ManagementUtility has a number of commands, which can be manipulated
by editing the self.commands dictionary.
"""
def __init__(self, argv=None):
self.argv = argv or sys.argv[:]
self.prog_name = os.path.basename(self.argv[0])
self.settings_exception = None
def execute(self):
"""
Given the command-line arguments, this figures out which subcommand is
being run, creates a parser appropriate to that command, and runs it.
"""
try:
subcommand = self.argv[1]
except IndexError:
subcommand = 'help' # Display help if no arguments were given.
# Preprocess options to extract --settings and --pythonpath.
# These options could affect the commands that are available, so they
# must be processed early.
parser = CommandParser(None, usage="%(prog)s subcommand [options] [args]", add_help=False)
parser.add_argument('--settings')
parser.add_argument('--pythonpath')
parser.add_argument('args', nargs='*') # catch-all
try:
options, args = parser.parse_known_args(self.argv[2:])
handle_default_options(options)
except CommandError:
pass # Ignore any option errors at this point.
no_settings_commands = [
'help', 'version', '--help', '--version', '-h',
'compilemessages', 'makemessages',
'startapp', 'startproject',
]
try:
settings.INSTALLED_APPS
except ImproperlyConfigured as exc:
self.settings_exception = exc
# A handful of built-in management commands work without settings.
# Load the default settings -- where INSTALLED_APPS is empty.
if subcommand in no_settings_commands:
settings.configure()
if settings.configured:
# Start the auto-reloading dev server even if the code is broken.
# The hardcoded condition is a code smell but we can't rely on a
# flag on the command class because we haven't located it yet.
if subcommand == 'runserver' and '--noreload' not in self.argv:
try:
autoreload.check_errors(django.setup)()
except Exception:
# The exception will be raised later in the child process
# started by the autoreloader. Pretend it didn't happen by
# loading an empty list of applications.
apps.all_models = defaultdict(OrderedDict)
apps.app_configs = OrderedDict()
apps.apps_ready = apps.models_ready = apps.ready = True
# In all other cases, django.setup() is required to succeed.
else:
django.setup()
self.autocomplete()
if subcommand == 'help':
if '--commands' in args:
sys.stdout.write(self.main_help_text(commands_only=True) + '\n')
elif len(options.args) < 1:
sys.stdout.write(self.main_help_text() + '\n')
else:
self.fetch_command(options.args[0]).print_help(self.prog_name, options.args[0])
# Special-cases: We want 'django-admin --version' and
# 'django-admin --help' to work, for backwards compatibility.
elif subcommand == 'version' or self.argv[1:] == ['--version']:
sys.stdout.write(django.get_version() + '\n')
elif self.argv[1:] in (['--help'], ['-h']):
sys.stdout.write(self.main_help_text() + '\n')
else:
self.fetch_command(subcommand).run_from_argv(self.argv)
-
コマンドラインで渡された第1引数から実行するコマンドを決定(ここでは
runserver
) -
パーサーを生成し、
--settings
オプション・--pythonpath
オプション・第2引数以降のその他引数
を解析して読み込む -
設定ファイルが読み込みこまれてるかの確認
-
django.setup()
でアプリケーションのロード実行(*あとで詳しく) -
self.fetch_command(subcommand)
によってrun_from_argv
の主体クラスを発見-
django.contrib.staticfiles.management.commands.runserver.Command
(親クラスはdjango.core.management.base.BaseCommand
)
-
-
run_from_argv(self.argv)
メソッド実行-
django.core.management.base.BaseCommand
のexecute(self, *args, **options)
メソッドが最終的に呼ばれる
-
django/__init__.py
from __future__ import unicode_literals
from django.utils.version import get_version
VERSION = (1, 10, 3, 'final', 0)
__version__ = get_version(VERSION)
def setup(set_prefix=True):
"""
Configure the settings (this happens as a side effect of accessing the
first setting), configure logging and populate the app registry.
Set the thread-local urlresolvers script prefix if `set_prefix` is True.
"""
from django.apps import apps
from django.conf import settings
from django.urls import set_script_prefix
from django.utils.encoding import force_text
from django.utils.log import configure_logging
configure_logging(settings.LOGGING_CONFIG, settings.LOGGING)
if set_prefix:
set_script_prefix(
'/' if settings.FORCE_SCRIPT_NAME is None else force_text(settings.FORCE_SCRIPT_NAME)
)
apps.populate(settings.INSTALLED_APPS)
- ロギングの設定
-
FORCE_SCRIPT_NAME
の設定があれば、URL逆引きの際にprefix
をつけるように設定 -
INSTALLED_APPS
に設定されたアプリケーションのロード
django/apps/registry.py
def populate(self, installed_apps=None):
"""
Loads application configurations and models.
This method imports each application module and then each model module.
It is thread safe and idempotent, but not reentrant.
"""
if self.ready:
return
# populate() might be called by two threads in parallel on servers
# that create threads before initializing the WSGI callable.
with self._lock:
if self.ready:
return
# app_config should be pristine, otherwise the code below won't
# guarantee that the order matches the order in INSTALLED_APPS.
if self.app_configs:
raise RuntimeError("populate() isn't reentrant")
# Load app configs and app modules.
for entry in installed_apps:
if isinstance(entry, AppConfig):
app_config = entry
else:
app_config = AppConfig.create(entry)
if app_config.label in self.app_configs:
raise ImproperlyConfigured(
"Application labels aren't unique, "
"duplicates: %s" % app_config.label)
self.app_configs[app_config.label] = app_config
# Check for duplicate app names.
counts = Counter(
app_config.name for app_config in self.app_configs.values())
duplicates = [
name for name, count in counts.most_common() if count > 1]
if duplicates:
raise ImproperlyConfigured(
"Application names aren't unique, "
"duplicates: %s" % ", ".join(duplicates))
self.apps_ready = True
# Load models.
for app_config in self.app_configs.values():
all_models = self.all_models[app_config.label]
app_config.import_models(all_models)
self.clear_cache()
self.models_ready = True
for app_config in self.get_app_configs():
app_config.ready()
self.ready = True
- スレッドセーフのためのチェック
-
INSTALLED_APPS
に設定されているAppConfig
の登録-
AppConfig
のインスタンスではなく単なるアプリケーションモジュールの場合は、AppConfig.create(entry)
でAppConfig
インスタンスを生成
-
-
AppConfig
のlabel
とname
がそれぞれユニークだと確認できたら、self.apps_ready
をTrue
をセット -
app_config.import_models(all_models)
でモデルのインポートとキャッシュ生成 -
self.models_ready
をTrue
をセット -
app_config.ready()
を実行- 各
AppConfig
のready()
メソッドには、アプリケーションロード後のフック処理を記述できる
- 各
-
self.ready
にTrue
をセット
おわりに
こんな感じで簡単に流れを追うことができました。
内部の処理を1つずつ理解していくことで、なにか予期せぬことで困った時に役立ちます。なのでステップ実行しながらのコードリーディングおすすめです。
もう1つのエントリポイントwsgi.py
に関しては、また別の機会で書こうと思いますm(_ _)m