【Django】テスト用に単独Pythonスクリプトを(manage.pyを使わずに)実行させる方法

# 概要
DjangoフレームワークでPythonプログラムを実装している時、試験用に単独のスクリプトファイルを実行させたくなるケースってありませんか？

通常、Djangoアプリケーションは`manage.py`スクリプトを介して実行されますが、プロジェクト構造とは独立した状況でスクリプトを実行させたい時があります。

私は、DjangoのORMの出力結果（DBとのやり取り）を見たい時やプロジェクトやアプリケーションディレクトリとは無関係にどんな挙動をするのか確認したい時に使いたくなったりします。
本記事では、どのように単独スクリプトを実行させるかを紹介します。

コードのベースは、Djangoチュートリアルから取ってきた`polls`アプリで紹介していますが、あまりそこは関係ありません。
[はじめての Django アプリ作成、その 1](https://docs.djangoproject.com/ja/4.2/intro/tutorial01/)

### 前提

ディレクトリ構造は以下。

```
.
├── mysite
│   ├── mysite
│   │   ├── (略)
│   ├── polls
│   │   ├── (略)
│   ├── manage.py
│   └── sample.py　 <- このスクリプトを実行させる
```

Pythonのバージョンは`3.8.16`
Djangoのバージョンは`4.1`
を使用しています。

# サンプルコード

早速サンプルコードは以下になります。

```py
import os
import django

# Djangoの設定を読み込む
os.environ.setdefault("DJANGO_SETTINGS_MODULE", 'mysite.settings')

# Djangoアプリケーションの初期化
django.setup()

# （モデルにアクセスするコードが必要であればこれより下に書く）
from sample.models import Sample

# 以下に実行したいコードを書く
```

上記方法であれば、モデルをインポートして、DBテーブルとの連携も可能になります。
例えば、DBのクエリを実行したり、モデルを操作してデータを取得・変更したりするコードを追加することができます。

Djangoのプロジェクト構造とは独立した単独のスクリプトを実行し、必要な機能をテストしたり、データベースとのやり取りを行ったりすることができます。

## コード解説
一つずつ、何をしているか解説。

まず、必要モジュールをインポートした後、
`os.environ.setdefault()`でDjangoの設定を現在のスクリプトに読み込みます。

`django.setup()`では、Djangoアプリケーションを初期化しています。これにより、Djangoの環境がセットアップされ、モデルやデータベースとのやり取りが可能になります。

たったこれだけなのですが、色々エラーと遭遇したので、上記コードの詳細も含めて紹介します。

## エラー3種類の紹介
### ①インポート文の順番間違えによるエラー

まずは以下のエラー。

```py
    raise AppRegistryNotReady("Apps aren't loaded yet.")
django.core.exceptions.AppRegistryNotReady: Apps aren't loaded yet.
```

これは、Djangoアプリケーションを正しく初期化できていないために発生したエラー。

モデルへのアクセスは後にする必要がありました。上記例で言えば、`from sample.models import Sample`を初期化のコードより前に書いているとエラーになりました。


### ②`os.environ.setdefault`未設定によるエラー

続いては、以下のエラー。

```py
    raise ImproperlyConfigured(
django.core.exceptions.ImproperlyConfigured: Requested setting LOGGING_CONFIG, but settings are not configured. You must either define the environment variable DJANGO_SETTINGS_MODULE or call settings.configure() before accessing settings.
```

これは、Djangoの設定が適切に構成されていない場合に発生するもので、`settings are not configured`と言われている通り、`settings`が適切に設定されていないことを示しています。

上記は`os.environ.setdefault("DJANGO_SETTINGS_MODULE", 'mysite.settings')`を記載していなかったときに発生しました。

エラーメッセージにある`DJANGO_SETTINGS_MODULE`とは、Djangoが実行時にどの設定ファイルを使用するかを指定する環境変数。Djangoはその設定ファイルを見つけることができず、`settings`が構成されていないよ、というエラーを出しています。

`DJANGO_SETTINGS_MODULE`環境変数は、Pythonの`os.environ`を使用して設定され、実行中のDjangoアプリケーションにDjangoの設定ファイルがどこにあるかを伝えてあげています。通常、この環境変数は`manage.py`スクリプトで使用されるものなので、普段は気にしないですが、単独実行させる場合はDjangoアプリケーションが適切な設定を読み込めるようにする必要があります。

確かに`manage.py`には以下の通り記載があり、`settings.py`を読み込んでいることがわかりますね。

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


def main():
    """Run administrative tasks."""
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.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()
```

ちなみに、

```sh
export DJANGO_SETTINGS_MODULE=mysite.settings
```

を実行することで、`DJANGO_SETTINGS_MODULE`環境変数に`mysite.settings`という値をシェル環境でセットして設定ファイルを指定することも可能です。ただしこれは現在のシェルセッション内のみ有効なもので、シェルを閉じたりdockerを利用したりしている場合は毎回やらないといけませんので、基本的には上述の通りコードに書いちゃった方が良いと思います。


### ③`django.setup()`未設定によるエラー

3つ目は以下のエラー。

```py
    raise AppRegistryNotReady("Apps aren't loaded yet.")
django.core.exceptions.AppRegistryNotReady: Apps aren't loaded yet.
```

上記エラーは、`django.setup()`を記載していなかった場合に発生しました。
エラーメッセージは①と同じですね。
`django.setup()`を呼び出すことで正常にDjangoをセットアップでき、エラーを解決できます。

そもそもこの`django.setup()`って何だ？って話ですが、Djangoアプリケーションを使用する前に必要な環境をセットアップするための関数です。具体的には、`settings.py`からの読み込み、アプリケーション全体の初期化（URLルーティング、モデルの準備、テンプレートエンジンの設定など）、データベースの準備などです。

こちらも普段は見ないぞ？と思いますが、通常は`manage.py`を介して`django.setup()`を内部的に実行しているので気にすることがありません。`manage.py`を使わない場合はこのように明示してあげることで、Djangoアプリケーションがコマンドラインやスクリプトなどからも独立して実行可能になるわけですね。

## そのほか

最初は以下のように実装していました。

```py
import os
from django.conf import settings

# Djangoの設定を読み込む
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
settings.configure()

import django

# Djangoアプリケーションの初期化
django.setup()

# 以下に実行したいコードを書く
```

ただし、`settings.configure()`でDjangoの設定を手動で行おうとすると以下のエラーになりました。モデルがどのアプリケーションに属するかをDjangoが特定できないために発生しています。

```
RuntimeError: Model class sample.models.Sample doesn't declare an explicit app_label and isn't in an application in INSTALLED_APPS.
```

こちらの解決方法は以下記事に記載した通りです。

[【Django】「...doesn't declare an explicit app_label and isn't in an application in INSTALLED_APPS」エラーについて](https://qiita.com/Ryo-0131/items/891fd487477c0a951541)

どうやら`settings.configure()`を使用すると、Djangoが自動的に設定を読み込むことはできますが、必要な構成要素を手動で追加する必要があるよう。上記方法で解決はできるのですが、いちいち`INSTALLED_APPS`とかモデル定義で設定してあげるのが面倒なので、自分はこちらの方法にしませんでした。上述のサンプルコードの方がすっきりしていてわかりやすいですしね。