django-admin startproject ではディレクトリを指定できる

きっかけはDocker, docker-composeを利用してコードを書いてみようと思ったときに、公式サンプルのコードを見ての疑問でした。

sudo docker compose run web django-admin startproject composeexample .

https://github.com/docker/awesome-compose/tree/master/official-documentation-samples/django/ より

Djangoチュートリアルでは以下のように記述されているのでそのまま使っていたので、 . ってなんだったかな? という感じでした。

$ django-admin startproject mysite

https://docs.djangoproject.com/en/5.0/intro/tutorial01/#creating-a-project

結論、. についてはLinuxコマンド上での、カレントディレクトリのことで、冒頭のコードとしては現在のディレクトリにprojectを作成する、ということでした。

コードを見てみる

startproject で実行されるのは django/core/management/commands/startproject.py です。

startprojectのコード自体は結構シンプルです。

from django.core.checks.security.base import SECRET_KEY_INSECURE_PREFIX
from django.core.management.templates import TemplateCommand

from ..utils import get_random_secret_key


class Command(TemplateCommand):
    help = (
        "Creates a Django project directory structure for the given project "
        "name in the current directory or optionally in the given directory."
    )
    missing_args_message = "You must provide a project name."

    def handle(self, **options):
        project_name = options.pop("name")
        target = options.pop("directory")

        # Create a random SECRET_KEY to put it in the main settings.
        options["secret_key"] = SECRET_KEY_INSECURE_PREFIX + get_random_secret_key()

        super().handle("project", project_name, target, **options)

https://github.com/django/django/blob/main/django/core/management/commands/startproject.py

ここでは

  • from django.core.management.templates から TemplateCommand を継承して Command を作成していること
  • name, directoryoptions から取得して基底クラスのhandle を呼び出し・渡している

ことが分かります。

基底クラスのhandle を呼び出し・渡した後

TemplateCommandhandleの冒頭部分は以下のようになっています。

def handle(self, app_or_project, name, target=None, **options):
        self.app_or_project = app_or_project
        self.a_or_an = "an" if app_or_project == "app" else "a"
        self.paths_to_remove = []
        self.verbosity = options["verbosity"]

        self.validate_name(name)

        # if some directory is given, make sure it's nicely expanded
        if target is None:
            top_dir = os.path.join(os.getcwd(), name)
            try:
                os.makedirs(top_dir)
            except FileExistsError:
                raise CommandError("'%s' already exists" % top_dir)
            except OSError as e:
                raise CommandError(e)
        else:
            top_dir = os.path.abspath(os.path.expanduser(target))
            if app_or_project == "app":
                self.validate_name(os.path.basename(top_dir), "directory")
            if not os.path.exists(top_dir):
                raise CommandError(
                    "Destination directory '%s' does not "
                    "exist, please create it first." % top_dir
                )

この else 以降の処理がディレクトリ名を引数として渡したときに実行される箇所ですが、os.path.abspath(os.path.expanduser(target)) という内容によって実際にディレクトリ名を指定・取得しているようでした。

https://github.com/django/django/blob/main/django/core/management/templates.py#L103-L111

とはいえパッと見て理解できなかったので、それぞれ分割して動かしてみると、以下のような挙動でした。

.のケース

>>> os.path.expanduser('.')
'.'
>>> os.path.abspath('.')
'/Users/tatsuya/django'

プロジェクト名のケース

>>> os.path.expanduser('djangoproject')
'djangoproject'
>>> os.path.abspath('djangoproject')
'/Users/tatsuya/django/djangoproject'

動かすだけではなくリファレンスについても見ておきましょう。

expanduserについてはこちら。

Unix および Windows では、与えられた引数の先頭のパス要素 ~ 、または ~user を、 user のホームディレクトリのパスに置き換えて返します。

https://docs.python.org/ja/3.12/library/os.path.html#os.path.expanduser

abspathについてはこちら。

パス名 path の正規化された絶対パスを返します。

https://docs.python.org/ja/3/library/os.path.html#os.path.abspath

自分の中では完全理解出来たかな、と言う状態です。

expanduserはどちらかというと ~~userを渡した時の挙動に関係するため、使わなくても良いのでは?と個人的に思いましたが、そこまで調べるのは時間がかかりそうなので一旦「こういう感じなんだな〜」と留めることにします。

あとは個人的に、目的別にCommandが存在していることが分かって面白かったので、 Command のコード周りを読むのも面白そうだなと感じました。