manage.py(django-admin.py)でコマンドを実行するための実装を見る
django のアプリやモデルに依存しているバッヂ処理やちょっとしたユーティリティーを書くとき、カスタムコマンドをつくることがままある。そのとき、作り方はチュートリアルを参考にお約束を守ってディレクトリを掘り、コピペして scaffold をつくればとくに問題なく動く。とはいえ、どういう仕組みのお約束なのかなどがわからないということがあった。
結論としては、ソースを呼んでる中で気づいたのだけど django/base.py at master · django/django · GitHub を読むのが一番確実で早いかと思う。
参考
- django-admin and manage.py | Django documentation | Django
- Writing custom django-admin commands | Django documentation | Django
- django.core.management | Django documentation | Django
- django/base.py at master · django/django · GitHub
おさらい
howtoから引用するとこういう内容。django app に management/commands
をほって
polls/ __init__.py models.py management/ __init__.py commands/ __init__.py _private.py closepoll.py tests.py views.py
BaseCommand
を継承した Command
をつくって、 handle
に処理をかく
from django.core.management.base import BaseCommand, CommandError from polls.models import Poll class Command(BaseCommand): help = 'Closes the specified poll for voting' def add_arguments(self, parser): parser.add_argument('poll_id', nargs='+', type=int) def handle(self, *args, **options): for poll_id in options['poll_id']: try: poll = Poll.objects.get(pk=poll_id) except Poll.DoesNotExist: raise CommandError('Poll "%s" does not exist' % poll_id) poll.opened = False poll.save() self.stdout.write('Successfully closed poll "%s"' % poll_id)
実装を見る
ざっくりと流れを見る
django-admin.py
または manage.py
を見ると execute_from_commnd_line
を実行しているので、そこから読むことになる。
execute_from_commnd_line
ManagementUtility クラスのインスタンス化
- コマンドのパース
- 登録してるコマンドを探して登録
ManagementUtility.execute()
ManagementUtility.fetch_command().run_from_argv()
BaseCommand.execute()
- BaseCommand.handle()
つまりコマンドをパース、コマンドを探してみつかれば実行。みつかったコマンドクラスで実行メソッドを呼び出して最終的に BaseCommand.handle()
を呼び出す。ここからは自分で書いたコードが実行される。
コマンドの登録
読むモジュールは django.core.management.__init__
ManagementUtility クラスでコマンド一覧の取得とチェックをする
- 引数の初期化(
CommandParser
), 設定ファイルが正しいかと設定(settings.configure()
,django.setup()
) autocomplete 用の設定(for bash) - 最終的には
fetch_command(subcommand).run_from_argv(argv)
の実行 get_commands
とその中で呼び出しているfind_commands
が django 標準のコマンドを探しに行っている。fetch_commands
の中で呼び出しているload_command_class
内でimport_module
している。そのときに文字列で指定している。ソースが短いので貼るとこう。
- 引数の初期化(
def load_command_class(app_name, name): """ Given a command name and an application name, returns the Command class instance. All errors raised by the import process (ImportError, AttributeError) are allowed to propagate. """ module = import_module('%s.management.commands.%s' % (app_name, name)) return module.Command()
なので、 management.commands
を指定しているのは、単にこういう仕様としているからという理由になる。あと module を import したあと Command()
でインスタンス化しているのだけど、ここのクラス名も決め打ちということになる。
コマンドの実行
読むモジュールは django.core.management.base
run_from_argv()
BaseCommandクラスのメソッド。大まかな流れはこう
- run_from_argv
- execute
- handle
なので、公式ドキュメント上の BaseCommand を継承して handle メソッドに実装を書くのは、こういう実行順序になっているから。
おわりに
公式ドキュメントを読んで素直に実装すればOKなのだけど、こうして実装をすこし知ると理解しやすい。よく management.commands
を management.command
に typo して動かないということをしたのも、単に文字列指定が間違っていたということがわかれば納得。