Django--INSTALLED_APPSに関してのソース解析

![templarbit-illustration-django-1a748506.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320164/11e7ad06-cbbd-e206-11c4-9ca632d3e814.jpeg)
# 前書き
今回の記事を書くに至る経緯を説明します、teratailでこんな質問がありました、
[Djangoのモデルを有効にするときのプロジェクトへの教え方が分からない](https://teratail.com/questions/236336#reply-343809)

質問の内容に関しては以下の通りでした。
>DjangoのINSTALLED_APPSのモジュールは何故`app名前.apps.app名前config`ように入れているだろう、
ディレクトリ構成で言えば `../app名前/apps.app名前config`のはずではないか。

よって、`INSTALLED_APPS`に関するDjangoのソースコードを説明したいと思います。
Djangoのコアに興味ある方はぜひ最後までお付き合いください。:relaxed:

# 下準備
解説するため、`django_test`という簡単なプロジェクトを作ります、ディレクトリ構成は以下のようです。

```
djnago_test
|-- django_test
|-- |-- __init__.py
|-- |-- asgi.py
|-- |-- settings.py
|-- |-- urls.py
|-- |-- wsgi.py
|-- manage.py
```
# runserver
Djangoのプロジェクトをコマンドラインから起動します。

```
python manage.py runserver
```

では、その後何が起こったのでしょうか、`manage.py`の中身を見てみましょう。

```python:manage.py
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys


def main():
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_test.settings')
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        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?"
        ) from exc
    execute_from_command_line(sys.argv)


if __name__ == '__main__':
    main()
```
- `setdefault`は`environ`にkey、valueの辞書形データを追加します、もしkeyが存在する場合、valueを取得します。
- `management`モジュールをインポート。
- `execute_from_command_line(sys.argv)`を実行します。

まず、`sys.argv`の中身から見ていきます、manage.pyのpathと実行コマンドです。

```
['manage.py', 'runserver']
```
次は`execute_from_command_line(sys.argv)`の中身を見ます。

```python:management/__init__.py
def execute_from_command_line(argv=None):
    """Run a ManagementUtility."""
    utility = ManagementUtility(argv)
    utility.execute()
```
内容は非常にシンプルです、`sys.argv`の値を受け取って、
`ManagementUtility`をインスタンス化して、`execute `関数を実行しました。
`ManagementUtility `の `__init__`関数を見ていきます。

```python:management.py
    def __init__(self, argv=None):
        self.argv = argv or sys.argv[:]
        self.prog_name = os.path.basename(self.argv[0])
        if self.prog_name == '__main__.py':
            self.prog_name = 'python -m django'
        self.settings_exception = None
```
この場合:

 -  `self.arg` == `sys.argv` == `['manage.py', 'runserver']`
 - `self.prog_name` == `manage.py`のpath

次を見ていきます。
`execute`関数は何をやったのでしょう、関数の中身は非常に長いので重要な箇所だけ説明します。

```python:management.py
try:
    subcommand = self.argv[1]
except IndexError:
    subcommand = 'help'
```
`python manage.py runserver `実行する際に、subcommandの値は`runserver`になります。
そして、下記のブロックに入ります。


```python:management.py
...
if subcommand == 'runserver' and '--noreload' not in self.argv:
   try:
      autoreload.check_errors(django.setup)()
...
```
`django.setup`の中身を見ていきます。

```python:django/__init__.py
...
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.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 settings.FORCE_SCRIPT_NAME
        )
    apps.populate(settings.INSTALLED_APPS)
```
ここまできたら、 `INSTALLED_APPS`に関する処理が明らかになってきました、
`populate`のソースを見ていきます、そこそこ長いので、重要な箇所だけ説明します。

```python:registry.py
...
    def populate(self, installed_apps=None):
        ...
            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
                app_config.apps = self
...
```

こちらの`installed_apps`の中身は下記のものになります、ループして、一個づつ取り出して次の処理に入ります。
一つ注意すべき点は`'django.contrib.admin'`のような内容は、タイプは**文字列**です。
 
```
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]
```
ループの中身は、`INSTALLED_APPS`に入ってる要素に対する継承関係の条件判断になります。

```python
if isinstance(entry, AppConfig):
    app_config = entry
else:
    app_config = AppConfig.create(entry)
```
もちろん、現在はただの文字列なので、elseのブロックに入ります。

```python
app_config = AppConfig.create(entry)
```
`create() `の中身を見てみます。

```python:config.py
    def create(cls, entry):
        ...
        try:
            module = import_module(entry)
        except ImportError:
            module = None
            mod_path, _, cls_name = entry.rpartition('.')

            if not mod_path:
                raise
        ...
```
ここまできたら、 `import_module`関数を実際使ってみたいと思います、
`users`appを作り、それを `INSTALLED_APPS `に追加します。

```
python manage.py startapp users
```

```python 
INSTALLED_APPS= [
...
users or users.apps.UsersConfig
]
```
`users`として追加される場合。

```python:test.py
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_test.settings')

from importlib import import_module
print(import_module("users"))
# <module 'users' from '/Users/user/django/django_test/users/__init__.py'>
```
ちゃんとappを見つけてくれました。

`users.apps.UsersConfig`として追加される場合。

```python:test.py
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_test.settings')

from importlib import import_module
print(import_module("users.apps.UsersConfig"))
# ModuleNotFoundError: No module named 'users.apps.UsersConfig'; 'users.apps' is not a package
```
app見つからないようです、よって、以下のブロックに入ります。

```python:config.py
except ImportError:
    module = None
    mod_path, _, cls_name = entry.rpartition('.')

    if not mod_path:
       raise
```
`mod_path, _, cls_name = entry.rpartition('.')`が実行され、
`mod_path`の値が`users.apps`になり、`cls_name`が `UsersConfig`になります。
再度テストしてみます。

```python:main.py
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_test.settings')

from importlib import import_module
mod_path, _, cls_name = "users.apps.UsersConfig".rpartition('.')
print(import_module(mod_path))
# <module 'users.apps' from '/Users/user/django/django_test/users/apps.py'>
```
appを見つけてくれました。