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 して動かないということをしたのも、単に文字列指定が間違っていたということがわかれば納得。

middleware がどう動くか実装をみる

前回セッションのバックエンド実装について書いた。

実際に使うときは、ミドルウェアで指定する必要がある。

Djangoミドルウェアは公式ドキュメントによると object を継承したクラスをかけばいいという。自分もちょっとだけ書いたことがある。ただこのとき、固定されたメソッド名をどう拾って処理できるようにしているか、あと object を継承していてどこから定義したものがつかえるのかよくわからなかった。

参考

おさらい

設定する

MIDDLEWARE_CLASSES でタブル内に文字列でクラスのパスを指定する。

Middleware | Django documentation | Django

実装する

先ほどの SessionMiddleware の場合こうなっている

django/middleware.py at master · django/django · GitHub

Python の object を継承したクラスをつくる。あと必要なメソッドを実装すればOK。 Middleware | Django documentation | Django によると以下の5つ

  • process_request
  • process_view
  • process_exception
  • process_template_response
  • process_response

SessionMiddleware の場合、 Django 読み込み時に backend のエンジンを読み込み、 SessionStore クラスのインスタンスを保持しておく。 リクエストが来たら都度 SessionStoreインスタンスを作成、レスポンスを返すときにもろもろの処理をはさんでいる。

実装をみる

実装がどこにあるか

実装を調べようにも SessionMiddlewareMIDDLEWARE_CLASSES で読み込むよう定義しているだけ。なので、 MIDDLEWARE_CLASSES そのものを検索する。すると、 django.core.handlers.base でなにか処理していることがわかる。

django/base.py at master · django/django · GitHub

BaseHandler

BaseHandler クラスでやってることの大枠はたぶんこう

  • init
  • load_middleware
  • get_response

初期化時に process_xxxメソッドに対応する変数を初期化しておく。 継承先の __call__ で呼ばれる load_middlewareMIDDLEWARE_CLASSES で定義したミドルウェアを取り出し、ミドルウェアインスタンスの属性に process_xxx があれば追加しておく。 レスポンスを返すときにフックポイントにきたら先ほど登録したミドルウェアを取り出して実行する。

WSGIHandler

先ほどの BaseHandler クラスを使っているのが WSGIHandler__call__ されたらレスポンスを返す。どこかで while True: のようなループがあって、そこでインスタンスになっているものが待ち受けている?

django/wsgi.py at master · django/django · GitHub

使われているところを探すと、 django.core.wsgi#get_wsgi_application で使われていることがわかった。これは django.setup() して return WSGIServer するだけの関数のようだった。これは django-admin.py startproject proj したときに生成される wsgi.py で使われている。gunicorn などで動かすときに使う。

おわりに

  • 文字列で指定したクラスを import する
  • middleware から固定のメソッド名を探して格納、フック時に適用する

というような話だった。

次は余力があれば runserver か render あたりをかく。

文字列で指定しているモジュールを import する

前回のセッションバックエンドについて書いた。 Djangoのセッションバックエンドを調べる(file, db) - そのあれ

そのとき、 django の設定ファイルでは文字列でパスを指定している。ただ、文字列をパスで指定しているだけでどう読み込んでいるかまでよく知らなかった。

セッションバックエンドの指定では django/middleware.py at master · django/django · GitHub で使われている。

結論

__import__ をつかうか importlib.import_module をつかう。

あまり詳しく調べてないけど、特に理由がなければ importlib.import_module のほうが標準ライブラリなのでいいんじゃないかなという感じがする。実際、 import_module 関数の中ではキャッシュやロックの管理のようななにかが書かれている。

検証

リポジトリはこちら altnight/djsession at importlib · GitHub

こういう構成になっている。

$ cat app_a/__init__.py
print('init %s' % __file__)
$ cat hello.py
print('hello %s' % __file__)

__init__.pyhello.py という import すると自身のファイルパスを表示するだけのモジュール。

$ tree app_a
app_a
├── __init__.py
├── hello.py
└── level1
    ├── __init__.py
    ├── hello.py
    └── level2
        ├── __init__.py
        └── hello.py

2 directories, 6 files

ためしにつかう main.py はこういう内容。

  • import_module でモジュール指定
  • __import__ でモジュール指定
  • import_module でパッケージ指定
  • __import__ でパッケージ指定
from importlib import import_module

import_module('hello')
import_module('app_a.hello')
import_module('app_a.level1.hello')
import_module('app_a.level1.level2.hello')

#__import__('hello')
#__import__('app_a.hello')
#__import__('app_a.level1.hello')
#__import__('app_a.level1.level2.hello')

#import_module('app_a')
#import_module('app_a.level1')
#import_module('app_a.level1.level2')

#__import__('app_a')
#__import__('app_a.level1')
#__import__('app_a.level1.level2')

結果

$ python main.py
hello /Users/altnight/u/src/djsession/djsession/hello.py
init /Users/altnight/u/src/djsession/djsession/app_a/__init__.py
hello /Users/altnight/u/src/djsession/djsession/app_a/hello.py
init /Users/altnight/u/src/djsession/djsession/app_a/level1/__init__.py
hello /Users/altnight/u/src/djsession/djsession/app_a/level1/hello.py
init /Users/altnight/u/src/djsession/djsession/app_a/level1/level2/__init__.py
hello /Users/altnight/u/src/djsession/djsession/app_a/level1/level2/hello.py

ちなみにパッケージじゃなくても(=ディレクトリに init.py がなくても) import できる。