概要
Pythonでコマンドライン引数とはどんなものなのか、djangoでどこで使われているか、また、Djangoのsettings.pyで、コマンドライン引数を使ってテスト用のDATABASE設定を行う方法も紹介します。
サンプルコード
実際に動きを見てからの方が早いと思うので、まずは以下のサンプルコード。
import sys
if __name__ == "__main__":
try:
print(f"sys.argv: {sys.argv}")
if len(sys.argv) >= 3:
arg1 = sys.argv[1]
arg2 = sys.argv[2]
print(f"引数1: {arg1}")
print(f"引数2: {arg2}")
else:
print("引数が少なくとも2つ必要です。")
except Exception as ex:
print(ex)
実行結果は以下の通り。
sample-test.py abc def
↓
sys.argv: ['sample-test.py', 'abc', 'def']
引数1: abc
引数2: def
上記から分かる通り、コマンドライン引数に期待通りの数指定されていれば、正常に出力されます。
sys.argvとは?
sys.argvとは、標準ライブラリsysモジュールに含まれる変数で、コマンドライン引数を取得するためのリストです。
上述の通り、Pythonスクリプトを実行するときに、python script.py arg1 arg2のように、スクリプト名の後にスペースで区切られた引数を追加された際、この引数のリストをsys.argvに格納することができます。
引数を一つにすると...?
では引数を一つにしたらどうなるでしょうか。
sample-test.py abc
↓
sys.argv: ['sample-test.py', 'abc']
引数が少なくとも2つ必要です。
一つしか連携されなかった場合は、if len(sys.argv) >= 3:がFalseになるので、elseで指定したプリント文が出力されます。
なぜ3以上にしているか、というと、上記デバッグ結果を見て分かる通り、sys.argvリストにはスクリプト名も格納されているためです。スクリプト名、引数1、引数2が入っているかどうか、を確認するために、3以上かどうかを判定しています。
if sys.argv[1] and sys.argv[2]:でも良いのか?
ちなみに、私は最初以下でも可能と思っていました。
一見、以下のIF文でも「引数が2つあるかどうか」をチェックしているので問題なさそうに見えます。
import sys
if __name__ == "__main__":
try:
print(f"sys.argv: {sys.argv}")
if sys.argv[1] and sys.argv[2]: # ここを変えています
arg1 = sys.argv[1]
arg2 = sys.argv[2]
print(f"引数1: {arg1}")
print(f"引数2: {arg2}")
else:
print("引数が少なくとも2つ必要です。")
except Exception as ex:
print(ex)
しかし、これでは、たとえ引数が一つでも"引数が少なくとも2つ必要です。"が表示されず、代わりにExceptionが拾われてlist index out of rangeと表示されます。インデックスエラーですね。
なぜそうなるか、というと、if sys.argv[1] and sys.argv[2]:の場合は、sys.argv[1]とsys.argv[2]が存在するかつ空でない場合にTrueになるからです。しかし、そもそもこの条件式で使う引数が足りていないので、その時点でプログラムの実行が中断され、tryブロックの例外処理が行われます。その結果、list index out of rangeエラーになってしまいます。
端的に言えば「条件式が評価される前にエラーが発生している」ということですね...。
Djangoのmanage.pyでも使われている
例えばどこで使われているか?というと、Djangoで一番身近なところで言えばmanage.pyです。
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()
python manage.py runserverを実行すると、sys.argvは['manage.py', 'runserver']のようなリストになります。そしてこの引数に応じて、異なる処理を実行するのです。runserverコマンドは開発サーバーの起動、makemigrationsコマンドはデータベースのマイグレーションファイルの作成、といった感じです。上述のtestもですね。
関数が具体的にどんなことをしているのか?という詳細は以下にコードがあります。
Code source de django.core.management
Djangoのsettings.pyでテスト用Databaseを指定する
最後に。sys.argvを使うことで、Djangoでテスト実行時のみ、テスト用のDatabaseを指定することができます。
Djangoでは通常、テストファイルを実行するときにpython manage.py test xxx(xxxは実行したいアプリケーション)といった形でtests.pyを実行させます。つまりテスト実行時のコマンドライン引数には必ずtestが入っていることになるので、testを指定した際にはテスト用のデータベース設定を見るように指定すれば良い、というわけです。
import sys
if 'test' in sys.argv:
DATABASES['default'] = DATABASES['test'];
DATABASES = {
# 本番用
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': DATABASE,
'USER': USER,
'PASSWORD': PASSWORD,
'HOST': HOST,
'PORT': '3306',
'OPTIONS': {
'init_command':
'SET character_set_connection=utf8,collation_connection=utf8_unicode_ci'
}
},
# テスト用
'test': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'test_db',
'USER': 'root',
'PASSWORD': 'root_password',
'HOST': 'mysql',
'PORT': '3306',
'OPTIONS': {
'connect_timeout': 500000,
'init_command':
'SET character_set_connection=utf8,collation_connection=utf8_unicode_ci'
}
}
}
上記により、DATABASES['test']に対応する値がDATABASES['default']にコピーされます。
これはあくまでテスト実施時にテスト用のDBを設ける場合、という想定です(本番とローカルのDBを分ける、という意味ではありません)。
ローカル用と本番用、という意味では、基本的にはsettings.pyをsettings_local.pyとしてローカル環境で別々に設定し、環境変数や環境ごとの設定ファイルを読み込む方法などが推奨されると思います。