Djangoのエントリポイント読解 - manage.py 編

  • 12
    いいね
  • 0
    コメント

はじめに

この記事は、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)
  1. 環境変数のデフォルト設定
  2. execute_from_command_lineメソッド実行

※ Django1.10からImportErrorの例外処理が加わりました。

django/core/management/__init__.py

__init__.py
def execute_from_command_line(argv=None):
    """
    A simple method that runs a ManagementUtility.
    """
    utility = ManagementUtility(argv)
    utility.execute()

  1. ManagementUtilityの初期化
  2. executeメソッド実行

ManagementUtilityクラスはdjango-adminmanage.pyで実行できるコマンド群をカプセル化しているものです。

__init__.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
__init.py
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. コマンドラインで渡された第1引数から実行するコマンドを決定(ここではrunserver)
  2. パーサーを生成し、--settingsオプション・--pythonpathオプション・第2引数以降のその他引数を解析して読み込む
  3. 設定ファイルが読み込みこまれてるかの確認
  4. django.setup()でアプリケーションのロード実行(*あとで詳しく)
  5. self.fetch_command(subcommand)によってrun_from_argvの主体クラスを発見

    1. django.contrib.staticfiles.management.commands.runserver.Command(親クラスはdjango.core.management.base.BaseCommand)
  6. run_from_argv(self.argv)メソッド実行

    1. django.core.management.base.BaseCommandexecute(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)
  1. ロギングの設定
  2. FORCE_SCRIPT_NAMEの設定があれば、URL逆引きの際にprefixをつけるように設定
  3. 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

  1. スレッドセーフのためのチェック
  2. INSTALLED_APPSに設定されているAppConfigの登録
    1. AppConfigのインスタンスではなく単なるアプリケーションモジュールの場合は、AppConfig.create(entry)AppConfigインスタンスを生成
  3. AppConfiglabelnameがそれぞれユニークだと確認できたら、self.apps_readyTrueをセット
  4. app_config.import_models(all_models)でモデルのインポートとキャッシュ生成
  5. self.models_readyTrueをセット
  6. app_config.ready()を実行
    1. AppConfigready()メソッドには、アプリケーションロード後のフック処理を記述できる
  7. self.readyTrueをセット

おわりに

こんな感じで簡単に流れを追うことができました。
内部の処理を1つずつ理解していくことで、なにか予期せぬことで困った時に役立ちます。なのでステップ実行しながらのコードリーディングおすすめです。

もう1つのエントリポイントwsgi.pyに関しては、また別の機会で書こうと思いますm(_ _)m