Djangoを読む

django/bin/django-admin.py

Djangoを使うときに初めに打つコマンド、

$ django-admin startproject mysite

流そうと思ったのですがなかなか面白いことをしているのでちゃんと読むことにします。 というわけで、django-admin。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
-
!
 
 
 
#!/usr/bin/env python
from django.core import management
 
if __name__ == "__main__":
    management.execute_from_command_line()

というわけで、core/managementに移動。

django/core/management/__init__.py

念のため、import fooとした場合、

  • fooディレクトリに__init__.pyがあればそれがインポートされる
  • fooディレクトリに__init__.pyがなく、fooディレトクリと同じ階層にfoo.pyがあればそれがインポートされる

という動作をします。ちなみに、__init__.pyがなく、foo.pyもないと場合、エラーにはならないようです。あれ?昔は__init__.py入れとかないでディレクトリ内のpyファイルインポートしようとしたらエラーになった気がするけど。

で、本題。execute_from_command_lineは一番下に書かれています。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
 
-
|
!
 
 
def execute_from_command_line(argv=None):
    """
    A simple method that runs a ManagementUtility.
    """
    utility = ManagementUtility(argv)
    utility.execute()

ManagementUtilityはすぐ上にあります。とりあえず__init__。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 
-
|
|
|
|
!
 
 
 
 
class ManagementUtility(object):
    """
    Encapsulates the logic of the django-admin and manage.py utilities.
 
    A ManagementUtility has a number of commands, which can be manipulated
    by editing the self.commands dictionary.
    """
    def __init__(self, argv=None):
        self.argv = argv or sys.argv[:]
        self.prog_name = os.path.basename(self.argv[0])
        self.settings_exception = None

sys.argv[:]って何やってるのかと思いましたが、[:]と書くと先頭要素から最終要素までの部分リストが返される、つまり、リストのコピーが作成されるようです。

exeuteの前半は無視して次のところから、

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 
 
 
 
 
 
 
 
 
 
-
|
!
 
        no_settings_commands = [
            'help', 'version', '--help', '--version', '-h',
            'compilemessages', 'makemessages',
            'startapp', 'startproject',
        ]
 
        try:
            settings.INSTALLED_APPS
        except ImproperlyConfigured as exc:
            self.settings_exception = exc
            # A handful of built-in management commands work without settings.
            # Load the default settings -- where INSTALLED_APPS is empty.
            if subcommand in no_settings_commands:
                settings.configure()

まだファイルも何も作ってないのだから例外に行きそうなのはわかりますがsettingsの中身を追っかけてみましょう。

django/conf/__init__.py

settingsはファイルの最後に書かれています。

Everything is expanded.Everything is shortened.
  1
 
settings = LazySettings()

Lazy、よくある遅延処理ですね。LazySettingsはファイルの上の方に書かれています。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 
-
|
|
|
!
 
 
 
 
 
 
-
|
|
|
!
 
 
 
 
-
|
!
 
 
 
class LazySettings(LazyObject):
    """
    A lazy proxy for either global Django settings or a custom settings object.
    The user can manually configure settings prior to using them. Otherwise,
    Django uses the settings module pointed to by DJANGO_SETTINGS_MODULE.
    """
    def __getattr__(self, name):
        if self._wrapped is empty:
            self._setup(name)
        return getattr(self._wrapped, name)
 
    def _setup(self, name=None):
        """
        Load the settings module pointed to by the environment variable. This
        is used the first time we need any settings at all, if the user has not
        previously configured the settings manually.
        """
        settings_module = os.environ.get(ENVIRONMENT_VARIABLE)
        if not settings_module:
            desc = ("setting %s" % name) if name else "settings"
            raise ImproperlyConfigured(
                "Requested %s, but settings are not configured. "
                "You must either define the environment variable %s "
                "or call settings.configure() before accessing settings."
                % (desc, ENVIRONMENT_VARIABLE))
 
        self._wrapped = Settings(settings_module)

説明の都合上メソッドの順番を入れ替えましたが、LazyObjectは汎用の遅延初期化用クラスで_wrappedに遅延対象のオブジェクトを格納しています(django/utils/functional.pyに書かれています)。 INSTALLED_APPSという属性を取ろうとすると__getattr__が呼ばれ、まだ初期化されていないので_setupが呼ばれ、settings.pyはないので例外送信、ということになります。 普通の条件分岐っぽい処理を例外処理でやってるのに違和感を感じるのですがエラーの場合は例外を投げるのがPython流らしいですね。

というわけで例外が送信されて、managementの方に戻ってきて、定義されている「settingsの要らないコマンド」なので今度はsettingsのconfigureが呼ばれます。なんか設定が不適切という例外が飛んできたのにconfigure呼ぶって違和感を感じます。コメントにあるようにload_defaultとかにすればいいのに。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 
-
|
|
|
!
 
 
 
 
 
 
 
 
 
-
|
!
 
    def configure(self, default_settings=global_settings, **options):
        """
        Called to manually configure the settings. The 'default_settings'
        parameter sets where to retrieve any unspecified values from (its
        argument must support attribute access (__getattr__)).
        """
        if self._wrapped is not empty:
            raise RuntimeError('Settings already configured.')
        holder = UserSettingsHolder(default_settings)
        for name, value in options.items():
            setattr(holder, name, value)
        self._wrapped = holder
 
    @property
    def configured(self):
        """
        Returns True if the settings have already been configured.
        """
        return self._wrapped is not empty

settings要らないって言ってるのだからデフォルト値も要らない気がしますが、いろんなところで参照されているから初期化しているのでしょうね。

ManagementUtility.execute続き

settingsの初期化が終わるとdjango.setupが呼ばれていますが、今回はまだ設定がないので何もしないだろうということで無視します。

残りはコマンドラインで指定されたコマンドに応じた分岐です。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 
 
 
 
 
 
 
-
|
!
 
 
 
 
 
        if subcommand == 'help':
            if '--commands' in args:
                sys.stdout.write(self.main_help_text(commands_only=True) + '\n')
            elif len(options.args) < 1:
                sys.stdout.write(self.main_help_text() + '\n')
            else:
                self.fetch_command(options.args[0]).print_help(self.prog_name, options.args[0])
        # Special-cases: We want 'django-admin --version' and
        # 'django-admin --help' to work, for backwards compatibility.
        elif subcommand == 'version' or self.argv[1:] == ['--version']:
            sys.stdout.write(django.get_version() + '\n')
        elif self.argv[1:] in (['--help'], ['-h']):
            sys.stdout.write(self.main_help_text() + '\n')
        else:
            self.fetch_command(subcommand).run_from_argv(self.argv)

というわけでfetch_commandに続く。

fetch_command

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 
-
|
|
!
 
-
!
 
 
 
-
!
-
!
 
 
 
    def fetch_command(self, subcommand):
        """
        Tries to fetch the given subcommand, printing a message with the
        appropriate command called from the command line (usually
        "django-admin" or "manage.py") if it can't be found.
        """
        # Get commands outside of try block to prevent swallowing exceptions
        commands = get_commands()
        try:
            app_name = commands[subcommand]
        except KeyError:
            # 省略
        if isinstance(app_name, BaseCommand):
            # If the command is already loaded, use it directly.
            klass = app_name
        else:
            klass = load_command_class(app_name, subcommand)
        return klass

まずはget_commandsへ。なお、get_commandsはManagementUtilityクラスのメソッドではなくただの関数です、念のため。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
!
 
 
 
 
 
 
 
 
 
 
def get_commands():
    """
    Returns a dictionary mapping command names to their callback applications.
 
    This works by looking for a management.commands package in django.core, and
    in each installed application -- if a commands package exists, all commands
    in that package are registered.
 
    Core commands are always included. If a settings module has been
    specified, user-defined commands will also be included.
 
    The dictionary is in the format {command_name: app_name}. Key-value
    pairs from this dictionary can then be used in calls to
    load_command_class(app_name, command_name)
 
    If a specific version of a command must be loaded (e.g., with the
    startapp command), the instantiated module can be placed in the
    dictionary in place of the application name.
 
    The dictionary is cached on the first call and reused on subsequent
    calls.
    """
    commands = {name: 'django.core' for name in find_commands(upath(__path__[0]))}
 
    if not settings.configured:
        return commands
 
    for app_config in reversed(list(apps.get_app_configs())):
        path = os.path.join(app_config.path, 'management')
        commands.update({name: app_config.name for name in find_commands(path)})
 
    return commands

__path__はパッケージの__init__.pyがあるディレクトリが入っているそうです。upathはUnicodeなパスになるようにしているだけなのようなので詳細省略、find_commandsに行きます。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 
-
|
|
|
|
!
 
 
 
def find_commands(management_dir):
    """
    Given a path to a management directory, returns a list of all the command
    names that are available.
 
    Returns an empty list if no commands are defined.
    """
    command_dir = os.path.join(management_dir, 'commands')
    return [name for _, name, is_pkg in pkgutil.iter_modules([npath(command_dir)])
            if not is_pkg and not name.startswith('_')]

パッケージのディレクトリ、つまり、django/core/managementの中にあるcommandsディレクトリの各モジュールをリストアップしています。というわけで、このfind_commandsとget_commandsのやってることを組み合わせると、

Everything is expanded.Everything is shortened.
  1
 
{'startproject': 'django.core'}

みたいな辞書ができるわけですね。

fetch_commandに戻る。ここまでで見てきてapp_nameに入っているのがBaseCommandのインスタンスではなくただの文字列であることが分かったので、

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
 
-
|
|
|
!
 
 
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()

まあ説明は不要ですね(笑)

django/core/management/commands/startproject.py

さて、というわけでようやくstartprojectコマンドを実行するモジュールにたどり着きました。ただし、run_from_argvは直接このモジュールには書かれていません。startproject.Commandは、django.core.management.templates.TemplateCommand(templates.pyに記載)のサブクラスで、TemplateCommandはdjango.core.base.BaseCommand(base.pyに記載)のサブクラスです。BaseCommandを見るとコマンドがどのように実行されるかが書かれています。大雑把に言うと、

run_from_argv
  create_parser
    add_arguments
  execute
    handle

という順番に実行され、サブクラスはadd_argumentsをオーバーライドして必要な引数を指定、handleをオーバーライドしてコマンド処理を実行するようです。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 
-
|
|
|
|
|
!
 
 
 
 
 
-
!
 
 
 
 
-
!
 
    def run_from_argv(self, argv):
        """
        Set up any environment changes requested (e.g., Python path
        and Django settings), then run this command. If the
        command raises a ``CommandError``, intercept it and print it sensibly
        to stderr. If the ``--traceback`` option is present or the raised
        ``Exception`` is not ``CommandError``, raise it.
        """
        self._called_from_command_line = True
        parser = self.create_parser(argv[0], argv[1])
 
        options = parser.parse_args(argv[2:])
        cmd_options = vars(options)
        # Move positional args out of options to mimic legacy optparse
        args = cmd_options.pop('args', ())
        handle_default_options(options)
        try:
            self.execute(*args, **cmd_options)
        except Exception as e:
            # 省略
        finally:
            connections.close_all()

parserはargparse.ArgumentParser(を少し拡張したクラス)で、parse_argsによりNamespaceオブジェクトが返されます。varsはオブジェクトのメンバー辞書(__dict__)を取得する関数です。

TemplateCommand.add_arguments

上で示したように、create_parserはadd_argumentsを呼び出します。BaseCommandのadd_argumentsは空ですがTemplateCommandでオーバーライドされています。後から出てくるので、ちゃんと見ておきましょう。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 
 
 
 
 
 
 
 
-
!
 
 
 
 
 
 
 
    def add_arguments(self, parser):
        parser.add_argument('name', help='Name of the application or project.')
        parser.add_argument('directory', nargs='?', help='Optional destination directory')
        parser.add_argument('--template', help='The path or URL to load the template from.')
        parser.add_argument(
            '--extension', '-e', dest='extensions',
            action='append', default=['py'],
            help='The file extension(s) to render (default: "py"). '
                 'Separate multiple extensions with commas, or use '
                 '-e multiple times.'
        )
        parser.add_argument(
            '--name', '-n', dest='files',
            action='append', default=[],
            help='The file name(s) to render. Separate multiple extensions '
                 'with commas, or use -n multiple times.'
        )

というわけで、

$ django-admin startproject mysite

とした場合、nameに'mysite'、directoryとtemplateにNoneが入ります。

startproject.Command.handle

executeはシステムチェックとかが行われていますが今回はコマンドの設定により行われないし今回の注目点でもないので省略。それらを省くとexecuteはhandleを呼び出しているだけです。

というわけで、startprojectモジュールのhandleにやってきました。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 
 
 
 
-
!
 
 
 
 
 
-
!
 
 
-
!
 
 
    def handle(self, **options):
        project_name, target = options.pop('name'), options.pop('directory')
        self.validate_name(project_name, "project")
 
        # Check that the project_name cannot be imported.
        try:
            import_module(project_name)
        except ImportError:
            pass
        else:
            raise CommandError(
                "%r conflicts with the name of an existing Python module and "
                "cannot be used as a project name. Please try another name." % project_name
            )
 
        # Create a random SECRET_KEY to put it in the main settings.
        options['secret_key'] = get_random_secret_key()
 
        super(Command, self).handle('project', project_name, target, **options)

validate_nameはプロジェクト名として指定された名前がPythonの識別子として使えるかをチェックしています。識別子として使えない場合は例外が投げられて呼び出し元で適切にハンドリングがされます。

処理のメインはスーパークラス(TemplateCommand)のhandleに、スーパークラスに投げる場合も委譲っていうんですかね?

TemplateCommand.handle

TemplateCommandの方のhandle、まずは前半(関係ないところは省略)、

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 
 
 
 
 
 
 
-
!
 
 
 
 
-
!
-
!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
    def handle(self, app_or_project, name, target=None, **options):
        self.app_or_project = app_or_project
        self.paths_to_remove = []
        self.verbosity = options['verbosity']
 
        self.validate_name(name, app_or_project)
 
        # if some directory is given, make sure it's nicely expanded
        if target is None:
            top_dir = path.join(os.getcwd(), name)
            try:
                os.makedirs(top_dir)
            except OSError as e:
                # 省略
        else:
            # 省略
 
        extensions = tuple(handle_extensions(options['extensions']))
 
        base_name = '%s_name' % app_or_project
        base_subdir = '%s_template' % app_or_project
        base_directory = '%s_directory' % app_or_project
        camel_case_name = 'camel_case_%s_name' % app_or_project
        camel_case_value = ''.join(x for x in name.title() if x != '_')
 
        context = Context(dict(options, **{
            base_name: name,
            base_directory: top_dir,
            camel_case_name: camel_case_value,
            'docs_version': get_docs_version(),
            'django_version': django.__version__,
            'unicode_literals': '' if six.PY3 else 'from __future__ import unicode_literals\n\n',
        }), autoescape=False)

Contextはdjango.templateのクラスです。

中盤、

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 
 
 
 
 
 
 
 
 
 
 
 
        template_dir = self.handle_template(options['template'],
                                            base_subdir)
        prefix_length = len(template_dir) + 1
 
        for root, dirs, files in os.walk(template_dir):
 
            path_rest = root[prefix_length:]
            relative_dir = path_rest.replace(base_name, name)
            if relative_dir:
                target_dir = path.join(top_dir, relative_dir)
                if not path.exists(target_dir):
                    os.mkdir(target_dir)

handle_template、

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 
-
|
|
|
!
 
 
 
-
    def handle_template(self, template, subdir):
        """
        Determines where the app or project templates are.
        Use django.__path__[0] as the default because we don't
        know into which directory Django has been installed.
        """
        if template is None:
            return path.join(django.__path__[0], 'conf', subdir)
        else:
            # 省略

というわけで、django/conf/project_templateにあるファイルがテンプレートとして使用されます。

で、handleの中盤に話を戻すと、テンプレートディレクトリと同じディレクトリ構造、ただし、「project_name」の部分はプロジェクトの名前で置き換え、ということをしています。

後半、

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 
 
 
 
 
 
 
 
 
-
|
!
 
 
 
 
 
 
 
 
 
 
 
 
 
-
            for filename in files:
                old_path = path.join(root, filename)
                new_path = path.join(top_dir, relative_dir,
                                     filename.replace(base_name, name))
                for old_suffix, new_suffix in self.rewrite_template_suffixes:
                    if new_path.endswith(old_suffix):
                        new_path = new_path[:-len(old_suffix)] + new_suffix
                        break  # Only rewrite once
 
                # Only render the Python files, as we don't want to
                # accidentally render Django templates files
                with open(old_path, 'rb') as template_file:
                    content = template_file.read()
                if new_path.endswith(extensions) or filename in extra_files:
                    content = content.decode('utf-8')
                    template = Engine().from_string(content)
                    content = template.render(context)
                    content = content.encode('utf-8')
                with open(new_path, 'wb') as new_file:
                    new_file.write(content)
 
                try:
                    shutil.copymode(old_path, new_path)
                    self.make_writeable(new_path)
                except OSError:
                    # 省略

テンプレートの中身に踏み込むのはまた今度。ともかくこれでテンプレートディレクトリにあるファイルがプロジェクトディレクトリにコピーされました。

おわりに

ということでDjangoのとっかかり、startprojectの処理を見てきました。Pythonに慣れてないと「なんでこんな風に書いてあるの?」という部分がいくつかありましたが、本質的な部分はrailsと同じに思いました。・・・いや、黒魔法が少ない分Railsより簡単かな?(笑)

なお、startappについてはmanage.pyを使う、プロジェクトのsettings.pyがあるという違いはありますが、やってることは9割以上同じなので飛ばします。


トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2017-01-16 (月) 22:06:54 (2654d)