Django源码分析(二)-创建项目流程

阅读量:71, 66

搭建好环境,我们开始看代码了。Django代码已经超过十万行,这么多代码,我们从哪里看起呢? 用Django开发时,首先是用django-admin startproject xx创建项目,我们就从django-admin看起.

创建项目流程

使用which django-admin查看django-admin位置: /home/jason/.venv/django-inside-env/bin/django-admin 你电脑上的位置可能不一样,以你的本地环境为准。

#!/home/jason/.venv/django-inside-env/bin/python3.6

# -*- coding: utf-8 -*-
import re
import sys 

from django.core.management import execute_from_command_line

if __name__ == '__main__':
    sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
    sys.exit(execute_from_command_line())

这段代码关键在execute_from_command_line,我们跟进去:

def execute_from_command_line(argv=None):
    """Run a ManagementUtility."""
    utility = ManagementUtility(argv)
    utility.execute()

execute_from_command_line使用argv创建了ManagementUtility对象,然后调用其execute方法. ManagementUtility是Django的命令行工具集的入口,读取命令行参数,解析出应该执行那个子命令, 并创建合适的解析器来执行命令:

def execute(self):
    """
    Given the command-line arguments, figure out which subcommand is being
    run, create a parser appropriate to that command, and run it.
    """
    try:
        # 获取子命令 subcommand=startproject
        subcommand = self.argv[1]
    except IndexError:
        # 没有子命令,使用help命令
        subcommand = 'help'  # Display help if no arguments were given.

    # Preprocess options to extract --settings and --pythonpath.
    # These options could affect the commands that are available, so they
    # must be processed early.
    # CommandParser继承自ArgumentParser,是一个命令行解析工具
    # 这里主要对处理命令行参数配置进行预处理
    parser = CommandParser(None, usage="%(prog)s subcommand [options] [args]", add_help=False)
    parser.add_argument('--settings')
    parser.add_argument('--pythonpath')
    parser.add_argument('args', nargs='*')  # catch-all
    try:
        options, args = parser.parse_known_args(self.argv[2:])
        handle_default_options(options)
    except CommandError:
        pass  # Ignore any option errors at this point.

    try:
        # 这里settings是django.conf.LazySettings的实例,后面会具体讲settings
        # 如果尝试获取`INSTALLED_APPS`,而没有找到配置文件会抛出异常
        # 创建项目使用的是`startproject`,显然是没有配置INSTALLED_APPS,进入异常
        settings.INSTALLED_APPS
    except ImproperlyConfigured as exc:
        self.settings_exception = exc

    # 条件不成立,跳过
    if settings.configured:
        # Start the auto-reloading dev server even if the code is broken.
        # The hardcoded condition is a code smell but we can't rely on a
        # flag on the command class because we haven't located it yet.
        if subcommand == 'runserver' and '--noreload' not in self.argv:
            try:
                autoreload.check_errors(django.setup)()
            except Exception:
                # The exception will be raised later in the child process
                # started by the autoreloader. Pretend it didn't happen by
                # loading an empty list of applications.
                apps.all_models = defaultdict(OrderedDict)
                apps.app_configs = OrderedDict()
                apps.apps_ready = apps.models_ready = apps.ready = True

                # Remove options not compatible with the built-in runserver
                # (e.g. options for the contrib.staticfiles' runserver).
                # Changes here require manually testing as described in
                # #27522.
                _parser = self.fetch_command('runserver').create_parser('django', 'runserver')
                _options, _args = _parser.parse_known_args(self.argv[2:])
                for _arg in _args:
                    self.argv.remove(_arg)

        # In all other cases, django.setup() is required to succeed.
        else:
            django.setup()

    # 后面会讲如何实现
    self.autocomplete()

    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)

execute方法主要解析命令行参数,加载settings配置,如果setting配置成功, 则执行django.setup函数(此函数主要是加载App),最后一步调用的核心命令为fetch_command命令,‘ 并执行run_from_argv函数。先看一下fetch_command:

def fetch_command(self, subcommand):
    """
    Try 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
    # get_commands会获取所有子命令,包括:
    #   1. django/core/management/commands下所有命令
    #   2. 用户自定义的命令
    commands = get_commands()
    try:
        # 获取自命令所属的app,django默认的子命令都属于`django.core`app 
        app_name = commands[subcommand]
    except KeyError:
        if os.environ.get('DJANGO_SETTINGS_MODULE'):
            # If `subcommand` is missing due to misconfigured settings, the
            # following line will retrigger an ImproperlyConfigured exception
            # (get_commands() swallows the original one) so the user is
            # informed about it.
            settings.INSTALLED_APPS
        else:
            sys.stderr.write("No Django settings specified.\n")
        sys.stderr.write(
            "Unknown command: %r\nType '%s help' for usage.\n"
            % (subcommand, self.prog_name)
        )
        sys.exit(1)
    if isinstance(app_name, BaseCommand):
        # If the command is already loaded, use it directly.
        klass = app_name
    else:
        # 获取到django.core.management.commands下的startproject模块的Command
        klass = load_command_class(app_name, subcommand)
    return klass

这个fetch_command函数类似一个工厂函数,由get_commands函数扫描出所有的子命令。 包括managemen中的子命令和app下的managemen中commands的子命令(自定义) 如果子命令不在commands字典内的话,会抛出一个异常,如果子命令存在则返回初始化的Command类。

接着视角在返回到execute函数中,接着:

self.fetch_command(subcommand).run_from_argv(self.argv)

将会调用fetch_command(subcommand)初始化Command类的run_from_argv方法. run_from_argv由各个Command的基类BaseCommand定义, 最终将会调用各个子类实现的handle方法。从而执行子命令的业务逻辑。 至此,命令调用的逻辑基本完成。

通过阅读这一部分的代码,其中最值得学习的地方在于fetch_commands函数,这是一个运用工厂方法的最佳实践,这样不但最大程度的解耦了代码实现,同时使得命令系统更易于扩展(App 自定义子命令就是一个很好的说明) 再有一点就是Command基类的定义,对于各种子命令的定义,基类完整的抽象出了command业务的工作逻辑,提供了统一的命令调用接口使得命令系统更易于扩展。

接着具体看startproject是怎么处理的. fetch_command获取到django.core.commands.startproject下的Command命令,然后调用run_from_argv

class Command(TemplateCommand):
    # BaseCommand -> TemplateCommand -> Command
    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'] = get_random_secret_key()

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

我们看到Command命令下没有run_from_argv,可能是在父类里实现的. 果然在BaseCommand下找到它了.

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:
        if options.traceback or not isinstance(e, CommandError):
            raise

        # SystemCheckError takes care of its own formatting.
        if isinstance(e, SystemCheckError):
            self.stderr.write(str(e), lambda x: x)
        else:
            self.stderr.write('%s: %s' % (e.__class__.__name__, e))
        sys.exit(1)
    finally:
        try:
            connections.close_all()
        except ImproperlyConfigured:
            # Ignore if connections aren't setup at this point (e.g. no
            # configured settings).
            pass

这段代码的关键部分在于self.execute(*args, **cmd_options). 然后看execute的逻辑:

def execute(self, *args, **options):
    """
    Try to execute this command, performing system checks if needed (as
    controlled by the ``requires_system_checks`` attribute, except if
    force-skipped).
    """
    if options['no_color']:
        self.style = no_style()
        self.stderr.style_func = None
    if options.get('stdout'):
        self.stdout = OutputWrapper(options['stdout'])
    if options.get('stderr'):
        self.stderr = OutputWrapper(options['stderr'], self.stderr.style_func)

    saved_locale = None
    if not self.leave_locale_alone:
        # Deactivate translations, because django-admin creates database
        # content like permissions, and those shouldn't contain any
        # translations.
        from django.utils import translation
        saved_locale = translation.get_language()
        translation.deactivate_all()

    try:
        if self.requires_system_checks and not options.get('skip_checks'):
            self.check()
        if self.requires_migrations_checks:
            self.check_migrations()
        # 关键部分
        output = self.handle(*args, **options)
        if output:
            if self.output_transaction:
                connection = connections[options.get('database', DEFAULT_DB_ALIAS)]
                output = '%s\n%s\n%s' % (
                    self.style.SQL_KEYWORD(connection.ops.start_transaction_sql()),
                    output,
                    self.style.SQL_KEYWORD(connection.ops.end_transaction_sql()),
                )
            self.stdout.write(output)
    finally:
        if saved_locale is not None:
            translation.activate(saved_locale)
    return output

execute的关键部分在output = self.handle(*args, **options), 而hander方法就是之前看到的startproject下的handle方法。 再往下是startproject具体的操作流程,我们就不再介绍了。

django源码

Django源码分析(二)-创建项目流程 - 有2条评论

  1. jason

    23333333333333333

  2. fffffffffffff

    444444444444444444

发表评论

电子邮件地址不会被公开。 必填项已用*标注

公告

这里是我的博客,欢迎骚扰

近期评论

  • jason发表了 23333333333333333
  • fffffffffffff发表了 444444444444444444

近期评论

  • jason发表了 23333333333333333
  • fffffffffffff发表了 444444444444444444