django源碼分析之項(xiàng)目創(chuàng)建

注:本文分析涉及到的源碼基于Django stable/2.0.x 分支。

計(jì)算機(jī)大部分思想都是來自于現(xiàn)實(shí)生活,所以完全可以用日常生活積累的常識(shí)去理解計(jì)算機(jī)里面的概念。
比如開發(fā)一套完整的軟件系統(tǒng),就好比去開發(fā)一片商業(yè)區(qū)。開發(fā)商業(yè)區(qū)的第一步就是要有一塊空地;軟件系統(tǒng)也是,第一步需要有一個(gè)空的項(xiàng)目。在Django世界里,這很簡(jiǎn)單,用下面命令就可以創(chuàng)建Django空項(xiàng)目。

django-admin.py startproject demo_project

有了一塊空地后,下一步就是在它上面建一棟棟大樓(以及給大樓起名)。而在Django中,與之對(duì)應(yīng)的就是創(chuàng)建app的過程,同樣非常簡(jiǎn)單的執(zhí)行下面命令就可,其中demo_app1就是給大樓起的名字。

python manage.py startapp demo_app1

然而要弄好一片商業(yè)區(qū)必然沒這么簡(jiǎn)單,還需要對(duì)功能不同的大樓進(jìn)行不同的設(shè)計(jì),建造和裝修,以及道路指引牌,甚至還有和安全相關(guān)的一系列配套設(shè)施等等。
這些在后續(xù)的文章中會(huì)一一道來,這篇文章并不打算繼續(xù)介紹它們,而是先深入分析上面兩個(gè)看似非常簡(jiǎn)單的過程(startproject和startapp),因?yàn)楸疚牟⒉幌雽懗扇绾谓倘耸褂肈jango的說明書類的文章。
目前為止僅僅執(zhí)行兩個(gè)命令,就可以讓Django幫你創(chuàng)建好了項(xiàng)目。下文將撥開云霧,來分析它們背后實(shí)際的代碼邏輯。
好了,先曬源碼,
django-admin.py源碼:

#!/usr/bin/env python
from django.core import management

if __name__ == "__main__":
    management.execute_from_command_line()

manage.py源碼:

#!/usr/bin/env python
import os
import sys

if __name__ == "__main__":
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo_project.settings")
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    execute_from_command_line(sys.argv)

文件django-admin.pymanage.py都用到了django.core.management模塊,并執(zhí)行execute_from_command_line方法。
此外后者多做了兩點(diǎn):1)設(shè)置環(huán)境變量DJANGO_SETTINGS_MODULE為當(dāng)前項(xiàng)目的settings文件;2)判斷Django是否能被正常導(dǎo)入使用。
上面的第一點(diǎn)不是本文的重點(diǎn),以后的文章再來討論(注意這里不討論,并不是說DJANGO_SETTINGS_MODULE不重要,恰恰相反,它很重要,它是Django項(xiàng)目的入口,指向項(xiàng)目的配置文件,該配置文件中指向ROOT_URLCONF,ROOT_URLCONF指向視圖以及其他的部分,Django項(xiàng)目需要定位到它們之后才能正常運(yùn)行)。
繼續(xù)看django.core.management.execute_from_command_line方法:

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

其中execute_from_command_line方法只是實(shí)例化了類ManagementUtility,接下來的重點(diǎn)查看ManagementUtility.execute方法。
該方法有點(diǎn)長(zhǎng),為了方便我們拆開來分析(源碼位置django/core/management/__init__.py):

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 = self.argv[1]
    except IndexError:
        subcommand = 'help'  # Display help if no arguments were given.

上面這段代碼的目的在于獲取命令參數(shù)self.argv[1];如果用戶沒有輸入命令參數(shù),將用help命令為默認(rèn)的參數(shù),對(duì)應(yīng)本文開頭的django-admin.py startproject demo_projectpython manage.py startapp demo_app1兩條指令,系統(tǒng)獲取到的命令參數(shù)為startprojectstartapp。

    # 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.

接下來用CommandParser類來解析剩下的命令行參數(shù)(該類只是對(duì)類ArgumentParser的簡(jiǎn)單封裝),這段代碼主要是預(yù)處理settingspythonpath兩個(gè)可選參數(shù)。解析這兩個(gè)參數(shù)到options中(其他參數(shù)放在args),然后由方法handle_default_options去處理options

def handle_default_options(options):
    """
    Include any default options that all commands should accept here
    so that ManagementUtility can handle them before searching for
    user commands.
    """
    if options.settings:
        os.environ['DJANGO_SETTINGS_MODULE'] = options.settings
    if options.pythonpath:
        sys.path.insert(0, options.pythonpath)

從上面代碼中可以看到handle_default_options()根據(jù)對(duì)象optionssettingspythonpath來設(shè)置環(huán)境變量和設(shè)置python模塊的搜索路徑。
繼續(xù)分析ManagementUtility.execute函數(shù):

    try:
        settings.INSTALLED_APPS
    except ImproperlyConfigured as exc:
        self.settings_exception = exc
    except ImportError as exc:
        self.settings_exception = exc

這段代碼可能會(huì)讓讀者詫異,莫名出現(xiàn)了settings對(duì)象,其實(shí)在這個(gè)源代碼文件開頭有行代碼from django.conf import settings引入了settings,在執(zhí)行這行代碼時(shí),會(huì)讀取os.environ中的DJANGO_SETTINGS_MODULE配置,加載項(xiàng)目配置文件后生成了settings對(duì)象(這里先簡(jiǎn)單的說明下,以后會(huì)有專門的文章講配置文件加載過程)。
代碼settings.INSTALLED_APPS是用來導(dǎo)入配置文件中所有app(通過查看文件django/conf/__init__.py,發(fā)現(xiàn)settings = LazySettings(),它具有__getattr__方法,所以這行代碼相當(dāng)于調(diào)用了LazySettings().__getattr__(INSTALLED_APPS),該方法獲取屬性時(shí)如果沒有導(dǎo)入配置則導(dǎo)入)。

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()

第一行代碼表示如果settings對(duì)象已經(jīng)配置好,就會(huì)執(zhí)行django.setup()方法(注:第一個(gè)分支專門針對(duì)runserver啟動(dòng)的服務(wù),并且使用autoreload機(jī)制的時(shí)候做的特殊處理,autoreload機(jī)制也是挺有意思的地方,后續(xù)也會(huì)寫專門的文章來說明,這里先簡(jiǎn)單略過,只需要知道其實(shí)第一個(gè)分支的autoreload.check_errors(django.setup)()也是執(zhí)行了django.setup()即可)
接著看django.setup(),在文件django/__init__.py中:

from django.utils.version import get_version

VERSION = (2, 0, 5, 'alpha', 0)

__version__ = get_version(VERSION)


def setup(set_prefix=True):
    """
    Configure the settings (this happens as a side effect of accessing the
    first setting), configure logging and populate the app registry.
    Set the thread-local urlresolvers script prefix if `set_prefix` is True.
    """
    from django.apps import apps
    from django.conf import settings
    from django.urls import set_script_prefix
    from django.utils.log import configure_logging

    configure_logging(settings.LOGGING_CONFIG, settings.LOGGING)
    if set_prefix:
        set_script_prefix(
            '/' if settings.FORCE_SCRIPT_NAME is None else settings.FORCE_SCRIPT_NAME
        )
    apps.populate(settings.INSTALLED_APPS)

django.setup()主要做了3件事:1)對(duì)logging模塊進(jìn)行了配置處理;2)設(shè)置當(dāng)前線程的script_prefix,可以理解成當(dāng)前目錄的前綴;3)循環(huán)載入之前從配置文件中導(dǎo)入的apps和models:(通過from django.apps import apps去查看文件django/apps/registry.py看到apps = Apps(installed_apps=None),這里Apps初始化的時(shí)候定義了全局的多個(gè)字典、變量、線程鎖等,然后通過apps.populate(settings.INSTALLED_APPS)方法循環(huán)載入之前從配置文件中導(dǎo)入的app和model,該方法是線程安全和冪等的,但不可重入,也等下篇文章再來詳細(xì)講解)。
繼續(xù)ManagementUtility.execute函數(shù):

    self.autocomplete()

上面這行代碼主要為了提供命令自動(dòng)補(bǔ)全功能。不詳細(xì)講了,但是需要知道Bash幾個(gè)內(nèi)置的變量,COMP_WORDS: 類型為數(shù)組,存放當(dāng)前命令行中輸入的所有單詞;COMP_CWORD: 類型為整數(shù),當(dāng)前光標(biāo)下輸入的單詞位于COMP_WORDS數(shù)組中的索引;COMPREPLY: 類型為數(shù)組,候選的補(bǔ)全結(jié)果。

    if subcommand == 'help':
        if '--commands' in args:
            sys.stdout.write(self.main_help_text(commands_only=True) + '\n')
        elif not options.args:
            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)

這段代碼前幾個(gè)分支,主要是處理help命令和version命令,最后一個(gè)分支self.fetch_command(subcommand).run_from_argv(self.argv)才是我們一開始討論的那兩個(gè)命令的真實(shí)入口。
其中fetch_command調(diào)用get_commands從下面幾個(gè)目錄找命令對(duì)象(命令類都繼承自BaseCommand,并實(shí)現(xiàn)handle()方法)

  1. django/core/management/commands目錄
  2. <INSTALLED_APPS>/management/commands/目錄

根據(jù)返回的subcommand實(shí)例,執(zhí)行run_from_argv()方法。
對(duì)應(yīng)我們文章開始討論的地方,也就是相當(dāng)于調(diào)用了startproject.run_from_argv(self.argv)startapp.run_from_argv(self.argv),它們對(duì)應(yīng)的源碼如下
core/management/commands/startproject.py

from django.core.management.templates import TemplateCommand

from ..utils import get_random_secret_key


class Command(TemplateCommand):
    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)

core/management/commands/startapp.py

from django.core.management.templates import TemplateCommand


class Command(TemplateCommand):
    help = (
        "Creates a Django app directory structure for the given app name in "
        "the current directory or optionally in the given directory."
    )
    missing_args_message = "You must provide an application name."

    def handle(self, **options):
        app_name = options.pop('name')
        target = options.pop('directory')
        super().handle('app', app_name, target, **options)

這兩個(gè)命令都繼承了TemplateCommand類,而且它們的代碼邏輯也幾乎一樣,只是傳入的參數(shù)不同,所以接下來通過分析TemplateCommand.handle()即可得知真相。
細(xì)心的讀者可能會(huì)發(fā)現(xiàn),上面明明調(diào)用的是方法run_from_argv(),但是startprojectstartapp兩命令都沒有這個(gè)方法,其實(shí)方法run_from_argv()是它們繼承的父類BaseCommand里的方法(具體可以查看源碼django/core/management/base.py),run_from_argv()最后會(huì)調(diào)用它們的handle()方法。
現(xiàn)在已經(jīng)距離真相越來越近了,繼續(xù)回到TemplateCommand.handle()。
handle方法代碼也有點(diǎn)多,只挑關(guān)鍵的來分析

def handle(self, app_or_project, name, target=None, **options):
    ...
    for root, dirs, files in os.walk(template_dir):
        ...
        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
            if new_path.endswith(extensions) or filename in extra_files:
                with open(old_path, 'r', encoding='utf-8') as template_file:
                    content = template_file.read()
                template = Engine().from_string(content)
                content = template.render(context)
                with open(new_path, 'w', encoding='utf-8') as new_file:
                    new_file.write(content)
            else:
                shutil.copyfile(old_path, new_path)

            if self.verbosity >= 2:
                self.stdout.write("Creating %s\n" % new_path)
            try:
                shutil.copymode(old_path, new_path)
                self.make_writeable(new_path)
            except OSError:
                self.stderr.write(
                      "Notice: Couldn't set permission bits on %s. You're "
                      "probably using an uncommon filesystem setup. No "
                      "problem." % new_path, self.style.NOTICE)
        ...

核心邏輯是遍歷處理template_dir目錄下的文件,這里的template_dirstartproject命令和startapp命令中分別對(duì)應(yīng)目錄django/conf/project_template/和目錄django/conf/app_template/。目錄結(jié)構(gòu)如下

模版目錄

為了更好的理解,先把文章一開始執(zhí)行startprojectstartapp命令生成的目錄結(jié)構(gòu)也拿出來,對(duì)比后發(fā)現(xiàn)幾乎完全一樣,除了文件后綴名不一樣:
project目錄

app目錄

其實(shí)到這里為止,已經(jīng)能猜到大致過程了,在執(zhí)行startproject的時(shí)候,會(huì)遍歷django/conf/project_template/下面的文件,把源文件的后綴.py-tpl改成.py,然后根據(jù)設(shè)置好的模版引擎生成相應(yīng)的文件。這樣項(xiàng)目就創(chuàng)建完成了。同樣,startapp也是一樣的道理。
下面幾行代碼是專門改文件后綴的

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

其中self.rewrite_template_suffixes就是個(gè)python 元組對(duì)象,里面包含了原后綴名(py-tpl),以及新后綴名(py)

# Rewrite the following suffixes when determining the target filename.
rewrite_template_suffixes = (
    # Allow shipping invalid .py files without byte-compilation.
    ('.py-tpl', '.py'),
)

現(xiàn)在還剩下模版引擎這塊,照樣先曬代碼

# Only render the Python files, as we don't want to
# accidentally render Django templates files
for filename in files:
    if new_path.endswith(extensions) or filename in extra_files:
        with open(old_path, 'r', encoding='utf-8') as template_file:
            content = template_file.read()
        template = Engine().from_string(content)
        content = template.render(context)
        with open(new_path, 'w', encoding='utf-8') as new_file:
            new_file.write(content)
    else:
        shutil.copyfile(old_path, new_path)

這段代碼主要是讀取模版文件里面的內(nèi)容,通過調(diào)用方法Engine().from_string()生成Template模版對(duì)象。
比如下面settings.py-tpl模版文件內(nèi)容(只截取了部分)被用來生成Template模版對(duì)象

"""
Django settings for {{ project_name }} project.

Generated by 'django-admin startproject' using Django {{ django_version }}.

For more information on this file, see
https://docs.djangoproject.com/en/{{ docs_version }}/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/
"""

import os

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/{{ docs_version }}/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '{{ secret_key }}'
...

接著template.render()會(huì)根據(jù)context內(nèi)容渲染模版,context內(nèi)容如下

context = Context({
    **options,
    base_name: name,
    base_directory: top_dir,
    camel_case_name: camel_case_value,
    'docs_version': get_docs_version(),
    'django_version': django.__version__,
}, autoescape=False)

可見,context包含的docs_version、django_versionoptions里面的內(nèi)容會(huì)用來替換模版文件里被{{}}包含的內(nèi)容,比如{{ docs_version }},然后替換后的新內(nèi)容會(huì)被寫入到對(duì)應(yīng)的新文件里,這樣Django就幫忙生成了項(xiàng)目需要的所有默認(rèn)文件。
到現(xiàn)在為止,我們已經(jīng)知道了文章開頭那兩個(gè)簡(jiǎn)單命令具體做了什么,可以讓我們輕松的完成項(xiàng)目的創(chuàng)建。
最后總結(jié):
startprojectstartapp屬于初期創(chuàng)建項(xiàng)目階段的命令,所以更多的是完成項(xiàng)目配置相關(guān)的工作,之后再根據(jù)用戶輸入的參數(shù)subcommand到命令工具集中去找到對(duì)應(yīng)的命令對(duì)象。繼承自TemplateCommand類的命令對(duì)象,使用模版引擎把相應(yīng)template_dir下面的模版文件渲染生成新項(xiàng)目需要的文件和目錄,最終Django就幫我們創(chuàng)建好了新項(xiàng)目。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容