Django源码浅析-命令系统

作者: 大师兄在简书 | 来源:发表于2017-05-25 18:48 被阅读233次

    摘要: 基于Django version 1.11.0 . alpha版本

    鉴于笔者水平有限,文中不免出现一些错误,还请多多指教!



    好了,下边是正文....
    首先大概看一下Django 项目的主要目录,初步建立一下Django源码的世界观。

    ├── django          //工程代码存放路径
    ├── docs            //文档
    ├── extras          
    ├── js_tests        //测试
    ├── scripts         //脚本
    └── tests           //单元测试
    

    Django核心代码主要在django目录下边

    django/
    ├── apps(app模块)
    ├── bin(可执行命令)
    ├── conf(配置)
    ├── contrib(其他开发者贡献代码)
    ├── core(核心组件)
    ├── db(ORM模块)
    ├── dispatch
    ├── forms(表单模块)
    ├── http
    ├── middleware(中间件)
    ├── template(模板)
    ├── templatetags
    ├── test(测试代码)
    ├── urls(url路由模块)
    ├── utils(工具代码)
    └── views(视图模块)
    

    在django中我们常用的命令主要有两个,一个是django-admin,一个是xxxx,我们先看一下django-admin
    1、命令位置

    lion@localhost:~/django/django$ whereis django-admin
    django-admin: /usr/local/bin/django-admin /usr/local/bin/django-admin.py /usr/local/bin/django-admin.pyc
    

    2、命令内容

    lion@localhost:~/django/django$ cat /usr/local/bin/django-admin
    #!/usr/bin/python
    
    # -*- 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())
    

    其实对比不难发现,django-admin命令其实对应的是django源码中的.django/bin/django-admin.py这个文件。
    django-admin.py 引用了django.core中的management,并调用了其execute_from_command_line函数。
    注:在最新版中django-admin和manage.py中调用的都是execute_from_command_line函数了,较旧版本的django中可能不同。
    所以要分析django的命令系统,就要从execute_from_command_line函数入手。
    execute_from_command_line函数定义:

    def execute_from_command_line(argv=None):
        """
        A simple method that runs a ManagementUtility.
        """
        utility = ManagementUtility(argv)
        utility.execute()
    

    函数初始化ManagementUtility类,传入argv(也就是命令行参数)参数,并执行execute方法
    execute方法:

    def execute(self):
        """
        Given the command-line arguments, this figures out which subcommand is
        being run, creates a parser appropriate to that command, and runs it.
        """
        try:
            subcommand = self.argv[1]
        except IndexError:
            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.
        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.
    
        no_settings_commands = [
            'help', 'version', '--help', '--version', '-h',
            'startapp', 'startproject', 'compilemessages',
        ]
    
        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()
    
        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
    
            # 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)
    

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

    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 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:
            klass = load_command_class(app_name, subcommand)
        return klass
    

    这个fetch_command函数类似一个工厂函数,由get_commands函数扫描出所有的子命令,包括managemen中的子命令和app下的managemen中commands的子命令(自定义),然后根据传入的subcommand初始化Command类。
    如果子命令不在commands字典内的话,会抛出一个“Unknown command”的提示,如果子命令存在则返回初始化的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业务的工作逻辑,提供了统一的命令调用接口使得命令系统更易于扩展。

    相关文章

      网友评论

        本文标题:Django源码浅析-命令系统

        本文链接:https://www.haomeiwen.com/subject/gvyzxxtx.html