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 して動かないということをしたのも、単に文字列指定が間違っていたということがわかれば納得。
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
のインスタンスを作成、レスポンスを返すときにもろもろの処理をはさんでいる。
実装をみる
実装がどこにあるか
実装を調べようにも SessionMiddleware
は MIDDLEWARE_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_middleware
で MIDDLEWARE_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
をつかう。
- Pythonでスクリプトを動的にimportする - Qiita
- 31.5. importlib – The implementation of import — Python 3.4.3 ドキュメント
あまり詳しく調べてないけど、特に理由がなければ 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__.py
と hello.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 できる。