- ルーティン作業を自動化したい
- カスタムコマンドを実装したい
カスタムコマンドを使うと、あらかじめ書いたスクリプトを manage.py 経由で実行できます。
例えば、以下のような作業はカスタムコマンドと相性が良いです。
- 定期的に発生するバッチ処理
- データベースへマスタデータ登録
これらをカスタムコマンド以外で実現しようとすると、毎回新しいスクリプトを書くハメになりとても不便。
実装方法はかなり定型的なので、本記事で詳しく解説していきます!
【基本編】カスタムコマンドの作り方
実装の流れは次の通りです。
- 必要なディレクトリ・ファイルを作成
- カスタムコマンド作成
まずはこのセクションでカスタムコマンドの基本をおさえましょう。
1. 必要なディレクトリ・ファイルを作成
アプリディレクトリに、management 以下を追加してください。
./
├── my_project/
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── asgi.py or wsgi.py
├── my_app/
│ ├── management/ # この配下を追加
│ │ ├── __init__.py
│ │ ├── commands/
│ │ │ ├── __init__.py
│ │ │ └── my_command.py
│ ├── models.py
│ ├── views.py
│ └── ...
├── manage.py
└── ...
カスタムコマンドを書く Python モジュール名がそのまま「コマンド名」になります。
すなわち、今回の場合には my_command がコマンド名になります。
python manage.py my_command
カスタムコマンドを実行したい場合には、上記をコマンドライン上で実行してください。
ファイル・ディレクトリを手作業で追加するのは面倒なので、自動で作成してくれるシェルスクリプトを作成しました。
以下、よろしければご利用ください。
#!/bin/bash
# Declare the app name as a variable
APP_NAME="my_app"
echo "Creating the ${APP_NAME} directory and its subdirectories..."
# Create the app directory
mkdir -p "${APP_NAME}/management/commands"
echo "Directories have been created."
echo "Creating __init__.py files..."
# Create __init__.py files
touch "${APP_NAME}/management/__init__.py"
touch "${APP_NAME}/management/commands/__init__.py"
echo "__init__.py files have been created."
echo "Creating my_command.py file..."
# Create my_command.py file
touch "${APP_NAME}/management/commands/my_command.py"
echo "my_command.py file has been created."
echo "Directory and file creation is complete."
2. カスタムコマンド作成
続いて実現したい内容を my_command.py に書き込みます。
内容は定型的なので、まずは以下をコピペいただくとラクだと思います。
from django.core.management.base import BaseCommand
class Command(BaseCommand):
# python manage.py help <カスタムコマンド名> を実行すると表示されるメッセージ
help = "Help message"
def add_arguments(self, parser):
# オプションをつけたい場合のみ必要
pass
def handle(self, *args, **options):
# 具体的な処理内容
print("Hello, World!")
色々書いてありますが、実際に実行されるのは handle() メソッドです。
つまり、上記を実行するとコンソール上に “Hello, World!” と表示されます。
python manage.py my_command
カスタムコマンドを実行するには、上記をコマンドラインで実行ください。
【中級編】オプションをつけて動的に呼び出す
ここからは add_arguments() メソッドについての解説です。
おさらいになりますが、カスタムコマンドのコード例を再掲します。
from django.core.management.base import BaseCommand
class Command(BaseCommand):
# python manage.py help <カスタムコマンド名> を実行すると表示されるメッセージ
help = "Help message"
def add_arguments(self, parser):
# オプションをつけたい場合のみ必要
pass
def handle(self, *args, **options):
# 具体的な処理内容
print("Hello, World!")
add_arguments() メソッドを作り込むことにより、オプションをつけて動的にコマンドを呼び出すことができるようになります。
オプションの設定と、値の受け取り
まず、オプションの設定には add_arguments() メソッドの中に parser.add_argument() メソッドを加えます。
class Command(BaseCommand):
help = "Help message"
def add_arguments(self, parser):
# name というオプションを追加
parser.add_argument("-n", "--name")
def handle(self, *args, **options):
name = options.get("name", None)
print(name)
ここでは、引数として “-n”, “–name” を指定しました。
すると、コマンドラインで以下を実行するとオプション値を受け取れるようになります。
# 短いオプション名を使う場合
python manage.py my_command -n "John Lennon"
# 長いオプション名を使う場合(上と意味は同じ)
python manage.py my_command --name "John Lennon"
これでコマンドライン上から渡した “John Lennon” という文字列が handle() メソッドに渡されます。
そして、handle() メソッド内で値を受け取るために options という辞書を使います。
handle() メソッドだけを抜粋して示しますね。
def handle(self, *args, **options):
name = options.get("name", None)
つまり、options という辞書の中に {“name”: “John Lennon”} が入っている状態ですね。
今回は get() メソッドで辞書から値を取り出しましたが、他の方法を使っても構いません。
以上で、オプション値を handle() メソッド内で取り出すことができました。
add_arguments() メソッドを極める
add_arguments()メソッドにはいくつかのオプションがあります。
def add_arguments(self, parser):
parser.add_argument("-name", nargs="?", default=None, type=str)
よく使うオプションは、以下の通りです。
- name_or_flags: コマンドラインから引数に設定する名前。
- nargs: 受け取ることができる引数の数。
- default: 引数が指定されなかった場合のデフォルト値。
- type: 引数の型を指定。指定した型に変換できない場合にはエラーが発生。
このうち、name_or_flags と nargs は少し分かりづらいので以下で補足説明します。
name_or_flags
name_or_flags ではフラグの名前を定義します。実は、引数には2種類が設定可能です。
- 位置引数: 順番に応じて呼び出される引数
- 名前付き引数: 名前に応じて呼び出される引数
位置引数について
以下のように引数名にハイフンをつけない( “-filename” ではない)場合には、位置引数として扱われます。
parser.add_argument("filename")
位置引数では引数の順番だけが考慮されて値が渡されるので、名前付き引数のように複数のパターンを設定することができません。(今回の例では、コマンド実行時に filename という文字列は使われない)
位置引数に値を渡す場合には、コマンドラインで以下のように実行します。
python manage.py new_command "filename.csv"
名前付き引数について
一方で、以下のように引数名の前にハイフンをつけた場合には、名前付き引数として扱われます。
parser.add_argument("-f", "--filename")
名前付き引数に値を渡す場合には、コマンドライン上で以下のように実行します。
# 短いコマンドライン引数を使うなら・・・
python manage.py new_command -f "filename.csv"
# または、長いコマンドライン引数を使うなら・・・
python manage.py new_command --filename "filename.csv"
補足)ハイフンの数は1つ?それとも2つ?
考え方としては “-d” などの短い引数はハイフン1つを、”–directory” のように長い引数はハイフン2つで表します。
複数のオプションを設定したい場合には、parser.add_argument() を増やします。
def add_arguments(self, parser):
parser.add_argument("-n", "--name")
parser.add_argument("-f", "--filename")
nargs
nargs は引数に取ることができる要素数を決められます。
記号 | 意味 |
---|---|
* | 0個以上 |
+ | 1個以上 |
? | 0個または1個 |
具体的な数字 | 指定した数の引数を強制 |
正規表現に少し似ているので、考え方にはなじみやすいかもしれません。
オプションが指定されたら True にする方法
オプションが指定されたら True にしたい場合には、action を使いましょう。
class Command(BaseCommand):
help = "Help message"
def add_arguments(self, parser):
parser.add_argument("--skip", action="store_true")
def handle(self, *args, **options):
name = options.get("skip", None)
if skip:
print("Skipping the operation.")
逆にオプションが指定されたら False にしたい場合には、action=”store_false” を指定すれば OK です。
【応用編】カスタムコマンドをさらに使いこなす
ここまで解説した内容を実践していただければ、カスタムコマンドの基本的な操作としては十分です。
ここからはさらに一歩進んだ使い方をご紹介します。
コマンドラインにメッセージを表示させる
print() 関数でも実現できますが、Djangoより細かい制御ができるstrdout, stderr オブジェクトが用意されています。
class Command(BaseCommand):
help = "Help message"
def add_arguments(self, parser):
parser.add_argument("-n", "--name")
def handle(self, *args, **options):
self.stdout.write("メッセージ")
self.stderr.write("エラーメッセージ")
例えば style オブジェクトを使って文字色を変更できます。
class Command(BaseCommand):
help = "Help message"
def add_arguments(self, parser):
parser.add_argument("-n", "--name")
def handle(self, *args, **options):
self.stdout.write(self.style.SUCCESS("Success!")) # 緑色
self.stdout.write(self.style.WARNING("Warning!")) # 黄色
self.stdout.write(self.style.NOTICE("Notice!")) # 茶色
self.stderr.write(self.style.ERROR("Error!")) # 赤色
エラーハンドリングの方法
エラーハンドリングには CommandError を活用します。
from django.core.management.base import BaseCommand, CommandError
class Command(BaseCommand):
model_id = 100 # 存在しないID
def handle(self, *args, **options):
try:
instance = Model.objects.get(pk=model_id)
except Model.DoesNotExist:
raise CommandError(f"Model instance does not exists. pk={model_id}")
コメント