[[Djangoを読む]]
 
 #contents
 
 *django/bin/django-admin.py [#b2eebbce]
 
 Djangoを使うときに初めに打つコマンド、
 
  $ django-admin startproject mysite
 
 流そうと思ったのですがなかなか面白いことをしているのでちゃんと読むことにします。
 というわけで、django-admin。
 
 #code(Python){{
 #!/usr/bin/env python
 from django.core import management
 
 if __name__ == "__main__":
     management.execute_from_command_line()
 }}
 
 というわけで、core/managementに移動。
 
 *django/core/management/__init__.py [#ofe0765e]
 
 念のため、import fooとした場合、
 
 -fooディレクトリに__init__.pyがあればそれがインポートされる
 -fooディレクトリに__init__.pyがなく、fooディレトクリと同じ階層にfoo.pyがあればそれがインポートされる
 
 という動作をします。ちなみに、__init__.pyがなく、foo.pyもないと場合、エラーにはならないようです。あれ?昔は__init__.py入れとかないでディレクトリ内のpyファイルインポートしようとしたらエラーになった気がするけど。
 
 で、本題。execute_from_command_lineは一番下に書かれています。
 
 #code(Python){{
 def execute_from_command_line(argv=None):
     """
     A simple method that runs a ManagementUtility.
     """
     utility = ManagementUtility(argv)
     utility.execute()
 }}
 
 ManagementUtilityはすぐ上にあります。とりあえず__init__。
 
 #code(Python){{
 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の前半は無視して次のところから、
 
 #code(Python){{
         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 [#g2914fdb]
 
 settingsはファイルの最後に書かれています。
 
 #code(Python){{
 settings = LazySettings()
 }}
 
 Lazy、よくある遅延処理ですね。LazySettingsはファイルの上の方に書かれています。
 
 #code(Python){{
 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とかにすればいいのに。
 
 #code(Python){{
     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続き [#cef8ec55]
 
 settingsの初期化が終わるとdjango.setupが呼ばれていますが、今回はまだ設定がないので何もしないだろうということで無視します。
 
 残りはコマンドラインで指定されたコマンドに応じた分岐です。
 
 #code(Python){{
         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 [#y38c95bb]
 
 #code(Python){{
     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クラスのメソッドではなくただの関数です、念のため。
 
 #code(Python){{
 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に行きます。
 
 #code(Python){{
 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のやってることを組み合わせると、
 
 #code(Python){{
 {'startproject': 'django.core'}
 }}
 
 みたいな辞書ができるわけですね。
 
 fetch_commandに戻る。ここまでで見てきてapp_nameに入っているのがBaseCommandのインスタンスではなくただの文字列であることが分かったので、
 
 #code(Python){{
 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 [#s4b9854b]
 
 さて、というわけでようやく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をオーバーライドしてコマンド処理を実行するようです。
 
 #code(Python){{
     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 [#l030f611]
 
 上で示したように、create_parserはadd_argumentsを呼び出します。BaseCommandのadd_argumentsは空ですがTemplateCommandでオーバーライドされています。後から出てくるので、ちゃんと見ておきましょう。
 
 #code(Python){{
     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 [#ad9bc505]
 
 executeはシステムチェックとかが行われていますが今回はコマンドの設定により行われないし今回の注目点でもないので省略。それらを省くとexecuteはhandleを呼び出しているだけです。
 
 というわけで、startprojectモジュールのhandleにやってきました。
 
 #code(Python){{
     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 [#yf0ed21c]
 
 TemplateCommandの方のhandle、まずは前半(関係ないところは省略)、
 
 #code(Python){{
     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のクラスです。
 
 中盤、
 
 #code(Python){{
         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、
 
 #code(Python){{
     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」の部分はプロジェクトの名前で置き換え、ということをしています。
 
 後半、
 
 #code(Python){{
             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:
                     # 省略
 }}
 
 テンプレートの中身に踏み込むのはまた今度。ともかくこれでテンプレートディレクトリにあるファイルがプロジェクトディレクトリにコピーされました。
 
 *おわりに [#c55d33d0]
 
 ということでDjangoのとっかかり、startprojectの処理を見てきました。Pythonに慣れてないと「なんでこんな風に書いてあるの?」という部分がいくつかありましたが、本質的な部分はrailsと同じに思いました。・・・いや、黒魔法が少ない分Railsより簡単かな?(笑)
 
 なお、startappについてはmanage.pyを使う、プロジェクトのsettings.pyがあるという違いはありますが、やってることは9割以上同じなので飛ばします。
 

トップ   編集 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS