22
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

DjangoAdvent Calendar 2016

Day 2

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

Last updated at Posted at 2016-12-02

はじめに

この記事は、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.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)
  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

22
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?