manage.py(django-admin.py)でコマンドを実行するための実装を見る

django のアプリやモデルに依存しているバッヂ処理やちょっとしたユーティリティーを書くとき、カスタムコマンドをつくることがままある。そのとき、作り方はチュートリアルを参考にお約束を守ってディレクトリを掘り、コピペして scaffold をつくればとくに問題なく動く。とはいえ、どういう仕組みのお約束なのかなどがわからないということがあった。

結論としては、ソースを呼んでる中で気づいたのだけど 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_commandsdjango 標準のコマンドを探しに行っている。
    • 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.commandsmanagement.commandtypo して動かないということをしたのも、単に文字列指定が間違っていたということがわかれば納得。