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

## はじめに

この記事は、[Django Advent Calendar 2016](http://qiita.com/advent-calendar/2016/django) 2日目の記事です。

## ここでやること

自分自身の勉強も兼ねて、Djangoのエントリポイントを追っていきます。
Djangoアプリケーションには2通りの実行方法があり、それぞれエントリポイントが異なります。

+ `manage.py`
+ `wsgi.py`

今回は`manage.py`を取り上げます。

## 参考にしたもの
tokibito先生の解説がとてもわかりやすく、勉強になります。
この記事に書いたことは全て網羅されています。

+ [Djangoのエントリポイントとアプリケーションの仕組み](http://www.slideshare.net/tokibito/django-52696489)
+ [Django 1.7で追加されるAppConfigの紹介](http://tokibito.hatenablog.com/entry/20140301/1393660554)

## バージョン
+ Django 1.10.3

## プロジェクト作成
```bash
$ 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`

```python
#!/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. 環境変数のデフォルト設定
1. `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`の初期化
1. `execute`メソッド実行

`ManagementUtility`クラスは`django-admin`と`manage.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`)
1. パーサーを生成し、`--settings`オプション・`--pythonpath`オプション・`第2引数以降のその他引数`を解析して読み込む
1. 設定ファイルが読み込みこまれてるかの確認
1. `django.setup()`でアプリケーションのロード実行(*あとで詳しく)
1. `self.fetch_command(subcommand)`によって`run_from_argv`の主体クラスを発見
	1. `django.contrib.staticfiles.management.commands.runserver.Command`(親クラスは`django.core.management.base.BaseCommand`)

1. `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. ロギングの設定 
1. `FORCE_SCRIPT_NAME`の設定があれば、URL逆引きの際に`prefix`をつけるように設定
1. `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. スレッドセーフのためのチェック
1. `INSTALLED_APPS`に設定されている`AppConfig`の登録
	1. `AppConfig`のインスタンスではなく単なるアプリケーションモジュールの場合は、`AppConfig.create(entry)`で`AppConfig`インスタンスを生成
1. `AppConfig`の`label`と`name`がそれぞれユニークだと確認できたら、`self.apps_ready`を`True`をセット
1. `app_config.import_models(all_models)`でモデルのインポートとキャッシュ生成
1. `self.models_ready`を`True`をセット
1. `app_config.ready()`を実行
	1. 各`AppConfig`の`ready()`メソッドには、アプリケーションロード後のフック処理を記述できる
1. `self.ready`に`True`をセット



## おわりに

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

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