【Django源碼淺析】-Django命令系統

鑑於筆者水平有限,文中難免出現一些錯誤,還請多多指教!python

好了,下邊是正文....django

首先大概看一下Django 項目的主要目錄,初步創建一下Django源碼的世界觀。bash

├── django          //工程代碼存放路徑
├── docs            //文檔
├── extras          
├── js_tests        //測試
├── scripts         //腳本
└── tests           //單元測試

Django核心代碼主要在django目錄下邊app

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-adminide

一、命令位置函數

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

二、命令內容工具

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業務的工做邏輯,提供了統一的命令調用接口使得命令系統更易於擴展。

相關文章
相關標籤/搜索