Django4でデフォルトのホスト/ポートを環境変数で指定する
TL;DR
- HTTPの場合
-
django.core.management.commands.runserver.Command
を継承したクラスをカスタムコマンドとして使用する
-
- HTTPS(SSL)の場合
-
django-sslserver
を用いてsslserver.management.commands.runsslserver.Command
を継承したクラスをカスタムコマンドとして使用する
-
-
add_arguments()
をオーバーライドし、super().add_arguments(parser)
実行後parser._get_positional_actions()
からAction.dest
がaddrport
のAction
に対し、Action.default
に環境変数から取得したホスト:ポート
を設定する
背景
- 前提
-
Django
開発:未経験
-
Django
で開発サーバーを起動する場合、既定値はdjango.core.management.commands.runserver.Command
のdefault_addr
(IPv4
)、default_addr_ipv6
(IPv6
)、default_port
でホスト/ポートが設定される。
default_addr = "127.0.0.1"
default_addr_ipv6 = "::1"
default_port = "8000"
protocol = "http"
server_cls = WSGIServer
一方で起動コマンドをpython manage.py runserver ホスト:ポート
、あるいはSSL化するならdjango-sslserver
でpython manage.py runsslserver ホスト:ポート
とすればホスト/ポートを変更することができる。
しかしながら、VSCodeで起動設定にコマンドを記述する等ソースコード管理対象に起動コマンドが記述されている場合、毎回ローカル環境に合わせて記述を変える必要があり、誤って自環境に合わせた変更をコミットしてしまう恐れがある。
これを防ぐために開発環境要因で実コードに変更を入れることは方針として正しくはないが、Flask
等の別フレームワークであればサーバー起動メソッドの引数で指定するため環境変数から設定することができるので、これと同様な指定方法を取りたい。
だがDjango
で環境変数からホスト/ポートを設定する方法を見つけることができなかったため、実コードに新規のカスタムコマンドを追加してそこからサーバーを起動するというアプローチをとる。
カスタムコマンドの実装
基本的には以下の通り。
mkdir -p customCommand/management/commands
touch customCommand/management/__init__.py
touch customCommand/management/commands/__init__.py
環境変数名DJANGO_DEFAULT_ADDR
でデフォルトのアドレス、DJANGO_DEFAULT_PORT
でデフォルトのポートを指定する。
import os
from django.core.management.base import CommandParser
from django.core.management.commands.runserver import Command as RunServer
class Command(RunServer):
ARG_ADDRPORT_DEST: str = "addrport"
ENV_DEFAULT_ADDR: str = "DJANGO_DEFAULT_ADDR"
ENV_DEFAULT_PORT: str = "DJANGO_DEFAULT_PORT"
def add_arguments(self, parser: CommandParser) -> None:
super().add_arguments(parser)
default_host: str = os.environ.get(self.ENV_DEFAULT_ADDR, "")
default_port: str = os.environ.get(self.ENV_DEFAULT_PORT, self.default_port)
for action in parser._get_positional_actions():
if action.dest == self.ARG_ADDRPORT_DEST:
action.default = (
default_host + f":{default_port}"
if default_host != ""
else default_port
)
django-sslserver
でSSLにしたい場合、from django.core.management.commands.runserver import Command as RunServer
の部分を以下のように変更する。
from sslserver.management.commands.runsslserver import Command as RunServer
INSTALLED_APPS = [
"customCommand",
・・・
開発サーバー起動
export DJANGO_DEFAULT_ADDR=0.0.0.0
export DJANGO_DEFAULT_PORT=8888
python manage.py custom_run
System check identified no issues (0 silenced).
*** **, **** - **:**:**
Django version 4.2, using settings 'config.settings'
Starting development server at http://0.0.0.0:8888/
Quit the server with CTRL-BREAK.
VSCodeで起動する場合
通常のlaunch.json
で環境変数を読み込ませればよい。直書きしても良いが恩恵は得られないので、外部ファイルから読み込ませる。
DJANGO_DEFAULT_ADDR=0.0.0.0
DJANGO_DEFAULT_PORT=8888
"configurations": [
{
"name": "Run django dev server",
"type": "debugpy",
"request": "launch",
"args": ["custom_run"],
"django": true,
"autoStartBrowser": false,
"program": "${workspaceFolder}\\manage.py",
"envFile": "${workspaceFolder}/.django.env"
},
詳細
引数で指定する開発サーバーのホスト/ポートは、実質argparse
モジュールのArgumentParser
の位置引数で処理されている。
具体的にはdjango.core.management.commands.runserver.Command
のadd_arguments()
メソッドで、メンバ名としてはaddrport
で処理されている。
def add_arguments(self, parser):
parser.add_argument(
"addrport", nargs="?", help="Optional port number, or ipaddr:port"
)
parser.add_argument(
"--ipv6",
"-6",
action="store_true",
dest="use_ipv6",
help="Tells Django to use an IPv6 address.",
)
この引数parser
は起動時にはdjango.core.management.base.BaseCommand.create_parser()
の呼び出しによりdjango.core.management.base.CommandParser
クラスが来るが、このクラスはArgumentParser
を継承している。
def create_parser(self, prog_name, subcommand, **kwargs):
"""
Create and return the ``ArgumentParser`` which will be used to
parse the arguments to this command.
"""
kwargs.setdefault("formatter_class", DjangoHelpFormatter)
parser = CommandParser(
prog="%s %s" % (os.path.basename(prog_name), subcommand),
description=self.help or None,
missing_args_message=getattr(self, "missing_args_message", None),
called_from_command_line=getattr(self, "_called_from_command_line", None),
**kwargs,
)
・・・
self.add_arguments(parser)
class CommandParser(ArgumentParser):
"""
Customized ArgumentParser class to improve some error messages and prevent
SystemExit in several occasions, as SystemExit is unacceptable when a
command is called programmatically.
"""
一方で、ArgumentParser
で一度追加した引数は後から削除することを想定しておらず、一応_remove_action()
という内部メソッドが用意されているがこれだけでは完全に削除することができず、同一の引数に対して後から自前でadd_argument()
を用いて追加することは難しかった。
そこで、方針としては先にadd_arguments()
を実行した後、既に追加した引数に対して生成されたargparse.Action
クラスのインスタンスに対して、メンバdefault
を直接書き換えることでデフォルト値を自前で設定するように対応したのがcustom_run.py
での内容である。
なお、環境変数で指定がされなかった場合はdjango.core.management.commands.runserver.Command
のデフォルト値を使用するようにしている。
ただし、ホストについてはIPv4
/IPv6
を共存させることができない(どちらか一方をのアドレスを採用すると--ipv6
の有無によりCommandError: "0.0.0.0" is not a valid IPv6 address.
などと出る)起動できなくなるので、既定値を空文字として環境変数があった場合のみホスト:ポート
の書式にし、ない場合はポートのみの書式にしている。
また、custom_run.py
ではArgumentParser._get_positional_actions()
という内部メソッドを用いているため、このアプローチ自体は今後の標準モジュールの仕様変更次第では使えなくなるのであまり推奨はできない。
参考
- argparse
Discussion