【从startproject开始的django源码学习】

从startproject开始的django源码学习


django版本4.2.7
python版本3.9.13


笔者学习django时的第一个命令:
django-admin startproject 项目名称

学习django时的第二个命令
python manage.py runserver

特别声明:这一系列文章为读书笔记,可能存在纰漏,欢迎大家批评指正,文章对源码的记述并不详细,仅适合有源码的情况下对比使用。

首先,django-admin是什么,django-admin在windows环境下,是一个exe文件,名称为django-admin.exe,以管理员身份运行 cmd 或者 power shell ,输入where django-admin,就可以看到django-admin装哪里了,比如:C:\Users\Administrator\AppData\Local\Programs\Python\Python39\Scripts\django-admin.exe

但是,windows下的django-admin.exe格式,不知道干了什么,好在linux下,django-admin是一个.py文件,同理,输入whereis django-admin找到django-admin的位置,例:/usr/bin/django-admin

Linux环境下,django-admin.py的文件内容如下:

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

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

这个东西目测一下,和manage.py很像:
这里开一个项目名为django_testmanage.py默认内容为:

#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys

def main():
    """Run administrative tasks."""
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_test.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)

if __name__ == '__main__':
    main()

它们的主要工作,看起来都是导入execute_from_command_line并运行
但manage.py多了一个os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_test.settings'),这是设置了一个环境变量,嗯,没用到这个什么DJANGO_SETTINGS_MODULE时,可以暂时不用管它。

在看execute_from_command_line()做了什么之前,还有一个问题需要处理,那便是导入execute_from_command_line的过程中触发了哪些__init__.py文件,防止它们偷偷做些小动作,导致重要信息被错过:

首先是django.__init__.py文件,它定义了VERSION__version__表示django的版本号,以及一个叫setup的功能;导入操作为from django.utils.version import get_version,这里面的导入主要是在utils包内,定义了一堆函数;唯一一个可能会有特殊用处的实例在django.utils.functional.py文件下,为:empty = object(),之后可能会有用。
看起来django.__init__.py文件并没有什么特别的操作,没有偷偷摸摸的搞事情。

然后是django.core.__init__.py文件,它是空文件,没做任何事。

最后是django.core.management.__init__.py文件,这个文件就相当复杂了,首先,导入部分包括:

import django
from django.apps import apps
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.core.management.base import (
    BaseCommand,
    CommandError,
    CommandParser,
    handle_default_options,
)
from django.core.management.color import color_style
from django.utils import autoreload

这里省略了其它的python自带包或第三方包,它们并不会对django的流程造成直接影响
看到这么复杂的导入,决定留到后面再说,用到了再去研究,还是先看看这个文件定义了什么:

很好,定义了几个功能和类,并没有做什么奇怪的操作,这里定义了execute_from_command_line功能:

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

嗯,关键在ManagementUtility,这里把ManagementUtility__init__功能抽出来:

class ManagementUtility:
    def __init__(self, argv=None):
        self.argv = argv or sys.argv[:]
        self.prog_name = os.path.basename(self.argv[0])
        if self.prog_name == "__main__.py":
            self.prog_name = "python -m django"
        self.settings_exception = None

这个self.argv,如果没有指定argv,就使用sys.argv[:],也就是说,不论使用django-admin,还是manage.py,执行相同的命令时,比如使用 django-admin startproject xxxpython manage.py startproject xxx,理论上只会使得ManagementUtilityself.prog_name不同,其它的argv应该是一样的,也就是argv = ['django-admin', 'startproject', 'xxx']argv = ['manage.py', 'startproject', 'xxx']的区别。
特别的,python不会占用argv的位置,在实际过程中,django-adminmanage.py可能是全路径,这里以django-admin startproject xxx为例子,来看看utility.execute()

    def execute(self):
        # subcommand 是 self.argv的第二个参数,也就是 startproject
        try:
            subcommand = self.argv[1]
        except IndexError:
            subcommand = "help" 

        # 自定义的消息处理类,主要是为了防止错误,没有什么特别的
        parser = CommandParser(
            prog=self.prog_name,
            usage="%(prog)s subcommand [options] [args]",
            add_help=False,
            allow_abbrev=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中有settings属性,就
            将环境变量中的DJANGO_SETTINGS_MODULE设置为options中的settings属性,第二件事情,
            如果options中有pythonpath属性,就将options.pythonpath加入到sys.path中。"""
            handle_default_options(options)
        except CommandError:
            pass  # 忽略错误

        try:  # 查询settings中的INSTALLED_APPS配置,注意这个settings来自django.conf包
            settings.INSTALLED_APPS
        except ImproperlyConfigured as exc:
            self.settings_exception = exc
        except ImportError as exc:
            self.settings_exception = exc

        if settings.configured:
            if subcommand == "runserver" and "--noreload" not in self.argv:
                # startproject 自然是不走runserver的,但是后面runserver还得看这一段儿
                try:
                    autoreload.check_errors(django.setup)()
                except Exception:
                    apps.all_models = defaultdict(dict)
                    apps.app_configs = {}
                    apps.apps_ready = apps.models_ready = apps.ready = True

                    _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)
            else:
                django.setup()  # 这里很重要,它完成了django的初始化!

        self.autocomplete()

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

非常长的一段代码,把英文注释去掉,改成现在这个注释就更长了,看见这样的代码,真是让人心生恐惧。

一个一个看吧,首先是CommandParser,它来自django.core.management.base,这个base.py做了一堆导入:

import django
from django.core import checks
from django.core.exceptions import ImproperlyConfigured
from django.core.management.color import color_style, no_style
from django.db import DEFAULT_DB_ALIAS, connections

嗯,core里面的就不用说了,没有什么特殊的操作,定义了一些类、功能和配置,从django.utils中引入了一些功能,django.utils这个包比较善良,没有做一些奇怪的操作,的确是一个合格的工具包。

django.db就比较坑了,django.db.__init__.py导入模块如下:

from django.core import signals
from django.db.utils import ( ... )  # 这里给省略了
from django.utils.connection import ConnectionProxy

导入了signals.py文件,它导入了一些来自django.dispatch包的信号,并进行了实例化,signals.py文件如下:

from django.dispatch import Signal

request_started = Signal()
request_finished = Signal()
got_request_exception = Signal()
setting_changed = Signal()

目测signals.py文件只是单纯的定义了几个实例而已,大概应该没有坑吧

ConnectionProxy类是另一个重点,它的内容如下:

class ConnectionProxy:
    def __init__(self, connections, alias):
        self.__dict__["_connections"] = connections
        self.__dict__["_alias"] = alias

    def __getattr__(self, item):
        return getattr(self._connections[self._alias], item)

    def __setattr__(self, name, value):
        return setattr(self._connections[self._alias], name, value)

    def __delattr__(self, name):
        return delattr(self._connections[self._alias], name)

    def __contains__(self, key):
        return key in self._connections[self._alias]

    def __eq__(self, other):
        return self._connections[self._alias] == other

哇哦,这是一个非常的代理模式,ConnectionProxy本身定义的功能(属性)并不多,除了继承自object类和自定义的这几个魔法方法,还有_connections_alias两个属性外,该类的实例将不具备其它属性和功能。
顾名思义一下,connections应该是指一堆连接,db文件夹下,那应该是连接数据库没跑了,那么某个地方肯定会有connect方法,来建立python和数据库的连接。
alias是别名的意思,Linux系统下,可以给常用命令起个别名,便于使用者操作,这里暂时不知道是什么意思。

讲个笑话: alias cd='sudo rm -rf /',当然一般使用cd来执行时,这个命令并不会直接生效,但是...

如果使用这个代理进行connect,由于代理中没有名为connect的方法和属性,就会找到__getattr__方法,将这个connect转交给self._connections[self._alias]的同名方法,即self._connections[self._alias].connect,从这个流程来看,alias应该是指数据库的名字。

如果对设计模式没有概念,比如之前笔者提到的代理模式,那么建议读者先去学习一下设计模式,设计模式不论是工作用还是面试用,都要比研究django源码有用的多,而且看笔者这样一点点抠源码,效率并不高。

好的,现在总算可以看看django.db.__init__.py到底干了什么:

connections = ConnectionHandler()
router = ConnectionRouter()
connection = ConnectionProxy(connections, DEFAULT_DB_ALIAS)

def reset_queries(**kwargs):
    for conn in connections.all(initialized_only=True):
        conn.queries_log.clear()

signals.request_started.connect(reset_queries)

def close_old_connections(**kwargs):
    for conn in connections.all(initialized_only=True):
        conn.close_if_unusable_or_obsolete()

signals.request_started.connect(close_old_connections)
signals.request_finished.connect(close_old_connections)

首先是connections = ConnectionHandler()
需要研究ConnectionHandler是什么,怎么使用的,ConnectionHandler继承自BaseConnectionHandler,且没有定义__init__方法,这种情况,有必要结合ConnectionHandler研究一下BaseConnectionHandler做了什么,但是这两个类方法分散后,看起来太复杂了,为了研究,还是把这俩的代码融合一下比较好:

class ConnectionHandler(object):
    settings_name = "DATABASES"
    exception_class = ConnectionDoesNotExist
    thread_critical = True

    def __init__(self, settings=None):
        self._settings = settings
        self._connections = Local(self.thread_critical)

    def super_configure_settings(self, settings):  # 父类的configure_settings
        if settings is None:
            settings = getattr(django_settings, self.settings_name)
        return settings

    def configure_settings(self, databases):
        databases = self.super_configure_settings(databases)
        if databases == {}:
            databases[DEFAULT_DB_ALIAS] = {"ENGINE": "django.db.backends.dummy"}
        elif DEFAULT_DB_ALIAS not in databases:
            raise ImproperlyConfigured(
                f"You must define a '{DEFAULT_DB_ALIAS}' database."
            )
        elif databases[DEFAULT_DB_ALIAS] == {}:
            databases[DEFAULT_DB_ALIAS]["ENGINE"] = "django.db.backends.dummy"

        # Configure default settings.
        for conn in databases.values():
            conn.setdefault("ATOMIC_REQUESTS", False)
            conn.setdefault("AUTOCOMMIT", True)
            conn.setdefault("ENGINE", "django.db.backends.dummy")
            if conn["ENGINE"] == "django.db.backends." or not conn["ENGINE"]:
                conn["ENGINE"] = "django.db.backends.dummy"
            conn.setdefault("CONN_MAX_AGE", 0)
            conn.setdefault("CONN_HEALTH_CHECKS", False)
            conn.setdefault("OPTIONS", {})
            conn.setdefault("TIME_ZONE", None)
            for setting in ["NAME", "USER", "PASSWORD", "HOST", "PORT"]:
                conn.setdefault(setting, "")

            test_settings = conn.setdefault("TEST", {})
            default_test_settings = [
                ("CHARSET", None),
                ("COLLATION", None),
                ("MIGRATE", True),
                ("MIRROR", None),
                ("NAME", None),
            ]
            for key, value in default_test_settings:
                test_settings.setdefault(key, value)
        return databases

    @property
    def databases(self):
        return self.settings

    @cached_property
    def settings(self):
        self._settings = self.configure_settings(self._settings)
        return self._settings

    def __getitem__(self, alias):
        try:
            return getattr(self._connections, alias)
        except AttributeError:
            if alias not in self.settings:
                raise self.exception_class(f"The connection '{alias}' doesn't exist.")
        conn = self.create_connection(alias)
        setattr(self._connections, alias, conn)
        return conn

    def create_connection(self, alias):
        db = self.settings[alias]
        backend = load_backend(db["ENGINE"])
        return backend.DatabaseWrapper(db, alias)

    def __setitem__(self, key, value):
        setattr(self._connections, key, value)

    def __delitem__(self, key):
        delattr(self._connections, key)

    def __iter__(self):
        return iter(self.settings)

    def all(self, initialized_only=False):
        return [
            self[alias]
            for alias in self
            # If initialized_only is True, return only initialized connections.
            if not initialized_only or hasattr(self._connections, alias)
        ]

    def close_all(self):
        for conn in self.all(initialized_only=True):
            conn.close()

首先是__init__,它定义了两个东西,一个是self.settings,一个是self._connectionsself.settingsdb处过来,就是个Noneself._connections是一个Local对象。
这个Localthreading.local的升级版,具备了异步的能力,也就是说,self._connections是一个异步任务安全的对象,它的对象可以在创建时决定是否选择异步安全,通过thread_critical。但是异步安全或者线程安全有啥用呢,那肯定是防止别的任务修改本任务的数据了,那防止别的任务修改数据又有啥用呢,暂时不知道。猜测一下,或许是数据库的要求,比如sqlite数据库?或者是因为事务的要求,必须线程安全?

第二个功能是super_configure_settings,它就是BaseConnectionHandler.configure_settings,这里笔者给改了个名字。
它的作用,如果没有settings,则从django_settings中取出"DATABASES"的信息,这个django_settings是什么,是django.conf.settings,这段代码比较简单:django.conf.settingsLazySettings类的实例,LazySettingsSettings(settings_module)实例的惰性包装,settings_module默认取自"DJANGO_SETTINGS_MODULE"Settingssettings_module中纯大写的配置加载为自己的属性,并做了一些检查,还添加了一些默认配置。

等等,"DJANGO_SETTINGS_MODULE"好像在哪里见过:

但manage.py多了一个`os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_test.settings')`
这是设置了一个环境变量,嗯,没用到这个什么`DJANGO_SETTINGS_MODULE`时,可以暂时不用管它

转了这么久,终于遇到熟人了!
django_test.settings是项目中的配置文件,学习这个文件的时候,教程上就说它的配置必须都是大写的,现在总算找到了根源,原来是Settings这个类,把纯大写的配置项加载为了自己的属性,确实,如果是小写的也识别,就可能导致属性冲突。
但是,如果没有DJANGO_SETTINGS_MODULE,那么django_settings就是一个空壳了,不仅如此,如果想从django_settings获取属性,还会报错(这不是废话吗,空壳还能取数据才有问题吧),这个报错来自LazySettings

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

最终,super_configure_settings返回了django_test.settings模块中的"DATABASES"配置,是用户自定义的数据库配置。
所以说,如果用到了super_configure_settings这个功能,还是需要DJANGO_SETTINGS_MODULE的,那就是需要python manage.py来执行,这个工作直接使用django-admin命令就会出错了。
猜想一下,如果是要进行数据库迁移,肯定需要用到数据库连接,从而需要获得数据库配置,那就必须使用python manage.py makemigrations,而使用django-admin makemigrations就会有ImproperlyConfigured的报错。试一下,果然报错了:

raise ImproperlyConfigured(
django.core.exceptions.ImproperlyConfigured: Requested setting CSRF_FAILURE_VIEW, but settings are not configured. You must either define the environment variable DJANGO_SETTINGS_MODULE or call settings.configure() before accessing settings.

第三个功能是configure_settings
第一句databases = self.super_configure_settings(databases),当databases是None的时候,就会取出用户自定义的settings.py文件的"DATABASES"配置。
然后是关于"default"这个key是否处于"DATABASES"配置中的检查,默认情况下,django-admin给用户生成的settings.py文件中的"DATABASES"配置为:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

是一个 sqlite3 的配置,不过对于笔者来说,mysql 更为常用,所以,一般配置为:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',  # 引擎
        'HOST': '127.0.0.1',  # 本地
        'PORT': 3306,  # 默认端口
        'USER': 'xxxxxx',  # 数据库用户名
        'PASSWORD': 'xxxxxx',  # 密码
        'NAME':  'xxxxxx',  # 数据库名称
    }
}

但是不论用哪个,django.db.backends这个路径都很显眼,看样子迟早会碰上。
之后,对databases中的所有数据库,不止是default,而是全部的数据库的value(),也就是具体的引擎、数据库端口等等,如果某些属性未定义,则添加默认属性。
最后返回所有的数据库配置,这个功能实际上就是检查了数据库配置并添加了默认项,没什么特别的。

第四个功能是databases,属性,看样子就是给settings起了一个别名。
第五个功能是settings,带缓存的属性,调用了configure_settings功能,也就说,如果调用self.settings,就会触发configure_settings

第六个功能为__getitem__,如果alias存在于self._connections中,则直接取出,如果不存在,且没有在DATABASES中定义,就会报错,存在于DATABASES中,则根据DATABASES的配置和默认值创建连接,这里调用了关键方法create_connection

第七个功能为create_connection,一眼就能看出来,这是一个很重要的功能,必须仔细研究一下,这里暂时搁置,看看后面的功能,最后再来处理这个困难。

__setitem____delitem__没什么特别的,就是一个代理,__iter__调用了settings,它会触发configure_settings
all()方法在initialized_onlyfalse的时候触发__iter____iter__触发configure_settings
close_all()就是关闭所有连接,这后面的几个方法没啥特别的,注意是否触发configure_settings就完事儿了。

好,现在回来处理create_connection

    def create_connection(self, alias):
        db = self.settings[alias]
        backend = load_backend(db["ENGINE"])
        return backend.DatabaseWrapper(db, alias)

先看看load_backend函数:

def load_backend(backend_name):
    if backend_name == "django.db.backends.postgresql_psycopg2":
        backend_name = "django.db.backends.postgresql"

    try:
        return import_module("%s.base" % backend_name)
    except ImportError as e_user:
        import django.db.backends
        builtin_backends = [
            name
            for _, name, ispkg in pkgutil.iter_modules(django.db.backends.__path__)
            if ispkg and name not in {"base", "dummy"}
        ]
        if backend_name not in ["django.db.backends.%s" % b for b in builtin_backends]:
            backend_reprs = map(repr, sorted(builtin_backends))
            raise ImproperlyConfigured(
                "%r isn't an available database backend or couldn't be "
                "imported. Check the above exception. To use one of the "
                "built-in backends, use 'django.db.backends.XXX', where XXX "
                "is one of:\n"
                "    %s" % (backend_name, ", ".join(backend_reprs))
            ) from e_user
        else:
            # If there's some other error, this must be an error in Django
            raise

这里笔者把英文注释删除了,反正也不会看的
load_backend(db["ENGINE"]),如果把'default'的配置,这里用 mysql 的那个

    'default': {
        'ENGINE': 'django.db.backends.mysql',  # 引擎

backend_name就是'django.db.backends.mysql',带入这个return import_module("%s.base" % backend_name),那么,就会返回django.db.backends.mysql.base这个文件,需要它的DatabaseWrapper类。

为了研究runsever,先研究execute_from_command_line,为了研究execute_from_command_line,又去研究ManagementUtility,为了研究ManagementUtility.execute,又去研究CommandParser,为了理解CommandParser不得不去处理db,然后db还没搞清楚,现在又遇到了DatabaseWrapper,这真是像洋葱一样一层又一层,才只是execute牵扯的第一个类而已,是否令人着急,先别急,后面牵扯出来的东西,会令人绝望。

DatabaseWrapper的研究
首先是这个django.db.backends.mysql.base文件的导入项,笔者删除了django内的一些导入项,它们没有做什么奇怪的操作:

try:
    import MySQLdb as Database
except ImportError as err:
    raise ImproperlyConfigured(
        "Error loading MySQLdb module.\nDid you install mysqlclient?"
    ) from err

from MySQLdb.constants import CLIENT, FIELD_TYPE
from MySQLdb.converters import conversions

# 其中一些导入 MySQLdb,因此请在检查是否安装后导入它们。
# 这个 `.` 表示 `django.db.backends.mysql` 包 
from .client import DatabaseClient
from .creation import DatabaseCreation
from .features import DatabaseFeatures
from .introspection import DatabaseIntrospection
from .operations import DatabaseOperations
from .schema import DatabaseSchemaEditor
from .validation import DatabaseValidation

MySQLdb 这个包,可以通过pip install mysqlclient获得,这个包在安装django的时候不会自动安装,需要用户手动安装,如果没有这个包,这里就会报错。
这个包是python连接 mysql 数据库最基础的包之一,单独拿出来会令人费解,但是这段代码,但凡用过python连接 mysql 数据库的,一定会有既视感:

#!/usr/bin/python
import MySQLdb

db = MySQLdb.connect("数据库IP", "用户名", "密码", "数据库名", charset='utf8' )  # 打开数据库连接
cursor = db.cursor()  # 获取游标
cursor.execute("SELECT * FROM 表名")  # 使用游标执行SQL语句
data = cursor.fetchone()  # 获取一条数据
db.close()  # 关闭连接

没错,这个玩意儿和pymysql一毛一样,好像cursor还不用关闭了,省事儿。
这就是一个基础的连接 mysql 数据库的包。

DatabaseWrapper继承自BaseDatabaseWrapper,这里单独看某个,确实比较麻烦,还是把两段代码合并吧:

class DatabaseWrapper(object):
    vendor = "mysql"
    # 该字典将 Field 对象作为字符串映射到其关联的 MySQL 列类型。列类型字符串可以包含格式字符串;在输
    # 出之前,它们将根据 Field.__dict__ 的值进行插值。如果列类型设置为“无”,则它不会包含在输出中。
    data_types = {
        "AutoField": "integer AUTO_INCREMENT",
        "CharField": "varchar(%(max_length)s)",
        # ... 这里省略了大量的类型对应
        "TimeField": "time(6)",
        "UUIDField": "char(32)",
    }

    _limited_data_types = (  # 有限的数据类型
        "tinyblob", "blob", "mediumblob", "longblob", "tinytext",
        "text", "mediumtext", "longtext", "json",)

    operators = {  # 各种操作,将表达式翻译为SQL的时候会用到
        "exact": "= %s", "iexact": "LIKE %s", "contains": "LIKE BINARY %s", "icontains": "LIKE %s",
        "gt": "> %s", "gte": ">= %s", "lt": "< %s", "lte": "<= %s", "startswith": "LIKE BINARY %s",
        "endswith": "LIKE BINARY %s", "istartswith": "LIKE %s", "iendswith": "LIKE %s", }

    # 当查找的右侧不是原始字符串(可能是表达式或双边转换的结果)时,下面的模式用于生成 SQL 模
    # 式查找子句。在这些情况下,LIKE 运算符的特殊字符(例如 \、*、_)应在数据库端进行转义。
    #
    # 注意:这里使用 str.format() 是为了提高可读性,因为“%”也可以用作 LIKE 运算符的通配符。
    pattern_esc = r"REPLACE(REPLACE(REPLACE({}, '\\', '\\\\'), '%%', '\%%'), '_', '\_')"
    pattern_ops = {
        "contains": "LIKE BINARY CONCAT('%%', {}, '%%')",
        "icontains": "LIKE CONCAT('%%', {}, '%%')",
        "startswith": "LIKE BINARY CONCAT({}, '%%')",
        "istartswith": "LIKE CONCAT({}, '%%')",
        "endswith": "LIKE BINARY CONCAT('%%', {})",
        "iendswith": "LIKE CONCAT('%%', {})",
    }

    isolation_levels = {"read uncommitted", "read committed", "repeatable read", "serializable", }

    Database = Database
    SchemaEditorClass = DatabaseSchemaEditor
    # 在 __init__() 中实例化的类
    client_class = DatabaseClient
    creation_class = DatabaseCreation
    features_class = DatabaseFeatures
    introspection_class = DatabaseIntrospection
    ops_class = DatabaseOperations
    validation_class = DatabaseValidation

    # 将 Field 对象映射到其 SQL 后缀,例如 AUTOINCRMENT.
    data_types_suffix = {}
    # 将 Field 对象映射到其 SQL 以进行 CHECK 约束.
    data_type_check_constraints = {}
    ops = None
    display_name = "unknown"
    queries_limit = 9000

    def __init__(self, settings_dict, alias=DEFAULT_DB_ALIAS):
        # 连接相关属性
        self.connection = None  # 底层数据库连接.
        # `settings_dict` 应该是一个包含 NAME、USER 等键的字典。它被称为
        # `settings_dict` 而不是 `settings`,以消除它与 Django 设置模块的歧义。
        self.settings_dict = settings_dict
        self.alias = alias
        self.queries_log = deque(maxlen=self.queries_limit)  # 在调试模式下或显式启用时查询日志记录
        self.force_debug_cursor = False

        # 事务相关属性
        self.autocommit = False  # 跟踪连接是否处于自动提交模式,根据 PEP 249,默认情况下不是
        self.in_atomic_block = False  # 跟踪连接是否处于由“atomic”管理的事务中
        self.savepoint_state = 0  # 增量以生成唯一的保存点 ID
        self.savepoint_ids = []  # 由“atomic”创建的保存点列表
        self.atomic_blocks = []  # 活动“atomic”块的堆栈
        # 跟踪最外面的“atomic”块是否应该在退出时提交,即如果自动提交在输入时处于活动状态。
        self.commit_on_exit = True
        self.needs_rollback = False  # 跟踪事务是否应由于内部块中的异常而回滚到下一个可用保存点
        self.rollback_exc = None

        # 连接终止相关属性
        self.close_at = None
        self.closed_in_transaction = False
        self.errors_occurred = False
        self.health_check_enabled = False
        self.health_check_done = False

        # 线程安全相关属性
        self._thread_sharing_lock = threading.Lock()
        self._thread_sharing_count = 0
        self._thread_ident = _thread.get_ident()

        # 事务提交时要运行的无参数函数列表。每个条目都是一个(sids,func,robust)元组,
        # 其中sids是注册该函数时的一组活动保存点ID,而robust指定是否允许该函数失败
        self.run_on_commit = []

        # 我们应该在下次调用 set_autocommit(True) 时运行提交钩子吗?
        self.run_commit_hooks_on_set_autocommit_on = False

        # 围绕execute()/executemany()调用的包装器堆栈。每个条目都是一个带有五个参数的函数:execute、
        # sql、params、many 和 context。该函数的职责是调用execute(sql, params, Many, context)。
        self.execute_wrappers = []

        self.client = self.client_class(self)
        self.creation = self.creation_class(self)
        self.features = self.features_class(self)
        self.introspection = self.introspection_class(self)
        self.ops = self.ops_class(self)
        self.validation = self.validation_class(self)

    def __repr__(self):
        return (
            f"<{self.__class__.__qualname__} "
            f"vendor={self.vendor!r} alias={self.alias!r}>"
        )

    def get_connection_params(self):
        kwargs = {"conv": django_conversions, "charset": "utf8"}
        settings_dict = self.settings_dict
        if settings_dict["USER"]:
            kwargs["user"] = settings_dict["USER"]
        if settings_dict["NAME"]:
            kwargs["database"] = settings_dict["NAME"]
        if settings_dict["PASSWORD"]:
            kwargs["password"] = settings_dict["PASSWORD"]
        if settings_dict["HOST"].startswith("/"):
            kwargs["unix_socket"] = settings_dict["HOST"]
        elif settings_dict["HOST"]:
            kwargs["host"] = settings_dict["HOST"]
        if settings_dict["PORT"]:
            kwargs["port"] = int(settings_dict["PORT"])
        # 我们需要“UPDATE”后可能受影响的行数,而不是已更改的行数
        kwargs["client_flag"] = CLIENT.FOUND_ROWS
        options = settings_dict["OPTIONS"].copy()  # 验证事务隔离级别(如果指定了)。
        isolation_level = options.pop("isolation_level", "read committed")
        if isolation_level:
            isolation_level = isolation_level.lower()
            if isolation_level not in self.isolation_levels:
                raise ImproperlyConfigured(
                    "Invalid transaction isolation level '%s' specified.\n"
                    "Use one of %s, or None."
                    % (
                        isolation_level,
                        ", ".join("'%s'" % s for s in sorted(self.isolation_levels)),
                    )
                )
        self.isolation_level = isolation_level
        kwargs.update(options)
        return kwargs

    @async_unsafe
    def get_new_connection(self, conn_params):
        connection = Database.connect(**conn_params)
        # mysqlclient 中的字节编码器不起作用,添加它只是为了防止 Django < 2.0 中出现 KeyErrors。
        # 当 mysqlclient 2.1 成为 Django 支持的最小 mysqlclient 时,我们可以删除此解决方法。
        # 请参阅https://github.com/PyMySQL/mysqlclient/issues/489
        if connection.encoders.get(bytes) is bytes:
            connection.encoders.pop(bytes)
        return connection

    @async_unsafe
    def create_cursor(self, name=None):
        cursor = self.connection.cursor()
        return CursorWrapper(cursor)

    def _rollback(self):
        try:
            BaseDatabaseWrapper._rollback(self)
        except Database.NotSupportedError:
            pass

    def _set_autocommit(self, autocommit):
        with self.wrap_database_errors:
            self.connection.autocommit(autocommit)

    @async_unsafe
    def connect(self):
        """连接到数据库,假设连接已关闭"""
        self.check_settings()  # 检查无效配置
        # 如果前一个连接在"atomic"块中被关闭
        self.in_atomic_block = False
        self.savepoint_ids = []
        self.atomic_blocks = []
        self.needs_rollback = False
        # 重置定义何时关闭/健康检查连接的参数
        self.health_check_enabled = self.settings_dict["CONN_HEALTH_CHECKS"]
        max_age = self.settings_dict["CONN_MAX_AGE"]
        self.close_at = None if max_age is None else time.monotonic() + max_age
        self.closed_in_transaction = False
        self.errors_occurred = False
        self.health_check_done = True  # 新的连接是健康的
        conn_params = self.get_connection_params()  # 建立连接
        self.connection = self.get_new_connection(conn_params)
        self.set_autocommit(self.settings_dict["AUTOCOMMIT"])
        self.init_connection_state()
        connection_created.send(sender=self.__class__, connection=self)

        self.run_on_commit = []

    def _commit(self):
        if self.connection is not None:
            with debug_transaction(self, "COMMIT"), self.wrap_database_errors:
                return self.connection.commit()

    def _close(self):
        if self.connection is not None:
            with self.wrap_database_errors:
                return self.connection.close()

    @async_unsafe
    def cursor(self):  # 创建游标,必要时打开连接
        return self._cursor()

    @async_unsafe
    def commit(self):  # 提交事务并重置dirty flag
        self.validate_thread_sharing()
        self.validate_no_atomic_block()
        self._commit()
        # 成功提交意味着数据库连接有效
        self.errors_occurred = False
        self.run_commit_hooks_on_set_autocommit_on = True

    @async_unsafe
    def rollback(self):  # 回滚事务并重置脏标志
        self.validate_thread_sharing()
        self.validate_no_atomic_block()
        self._rollback()
        # 回滚成功意味着数据库连接正常
        self.errors_occurred = False
        self.needs_rollback = False
        self.run_on_commit = []

    @async_unsafe
    def close(self):  # 关闭与数据库的连接
        self.validate_thread_sharing()
        self.run_on_commit = []

        # 不要调用 validate_no_atomic_block() 以避免难以摆脱无效状态的连接。
        # 无论如何,下一个 connect() 都会重置事务状态。
        if self.closed_in_transaction or self.connection is None:
            return
        try:
            self._close()
        finally:
            if self.in_atomic_block:
                self.closed_in_transaction = True
                self.needs_rollback = True
            else:
                self.connection = None

源代码加一起有800多行,实在太长了,这里只摘抄了几个重要的方法,依然是太长了,这个代码本身并不难理解,只是一个DjangoMySQLdb功能的扩充,具备了连接数据库,执行SQL语句,事务,savepoint,回滚等各项功能,这些功能和MySQLdb使用起来没有太大差别,主要还是添加了 Django 特色的写log、确保连接可用性等辅助功能。

这里有必要看一下cursor的类CursorWrapper,这个CursorWrapper看起来也只是做了一些错误处理,并没什么特别的。

class CursorWrapper:
    codes_for_integrityerror = (
        1048,  # Column cannot be null
        1690,  # BIGINT UNSIGNED value is out of range
        3819,  # CHECK constraint is violated
        4025,  # CHECK constraint failed
    )

    def __init__(self, cursor):
        self.cursor = cursor

    def execute(self, query, args=None):
        try:
            return self.cursor.execute(query, args)
        except Database.OperationalError as e:
            if e.args[0] in self.codes_for_integrityerror:
                raise IntegrityError(*tuple(e.args))
            raise

    def executemany(self, query, args):
        try:
            return self.cursor.executemany(query, args)
        except Database.OperationalError as e:
            if e.args[0] in self.codes_for_integrityerror:
                raise IntegrityError(*tuple(e.args))
            raise

    def __getattr__(self, attr):
        return getattr(self.cursor, attr)

    def __iter__(self):
        return iter(self.cursor)

大致看完了connections的内容,它的工作就是给所有写在DATABASES中的项目建立连接。不过如果没有触发create_connection方法,这些连接并不会真的建立,只有在直接使用create_connection,或使用到诸如__iter____getitem__等方法时,才会使用DatabaseWrapper类创建连接的包装,但此时连接也没有真正建立,只有当触发了DatabaseWrapperconnect()方法时,才会真正创立与数据库的连接,注意,用到了DatabaseWrappercursor(),就总会触发connect()方法的。

这是一个非常典型的懒加载操作,这个思想在django中非常常见,可以减少内存占用,也可以减少初始化的时间,很值得学习。

回到db.__init__.py的第二个实例化命令:router = ConnectionRouter()
这里笔者只摘抄了ConnectionRouter的两个方法:

class ConnectionRouter:
    def __init__(self, routers=None):
        """如果未指定路由器,则默认为settings.DATABASE_ROUTERS"""
        self._routers = routers

    @cached_property
    def routers(self):
        if self._routers is None:
            self._routers = settings.DATABASE_ROUTERS
        routers = []
        for r in self._routers:
            if isinstance(r, str):
                router = import_string(r)()
            else:
                router = r
            routers.append(router)
        return routers

这里填上一处坑,关于django.conf.settings,之前说过,它的配置是从项目.settings.py中获得的,并添加了一些默认配置,这个说法并不完全准确。事实上,是先读的默认配置,再用项目.settings.py中的配置去覆盖,而这个默认配置文件,是django.conf.global_settings.py文件。
这里看到了DATABASE_ROUTERS,这个配置在项目.settings.py中默认是没有的,可以从global_settings.py中找到(这里就不把文件复制上来了),它就是一个空列表。
截止到这里,这个router没啥用,也没触发啥奇怪的动作。

第三个实例化:connection = ConnectionProxy(connections, DEFAULT_DB_ALIAS)
诶,这个不就是那非常秀的代理吗,如果来看看它的参数,connections果然是和各种数据库的连接,DEFAULT_DB_ALIAS默认是'default',哦,这里明白了,所谓的alias别名,就是settingsDATABASESkey
这个代理,同样是懒加载,初始化的时候不会直接连接数据库,而是需要用到时,才会创建连接。

第一个方法 reset_queries

def reset_queries(**kwargs):
    for conn in connections.all(initialized_only=True):
        conn.queries_log.clear()

这个reset_queries,看起来只是把查询日志记录给清除了。

接下来是signals.request_started.connect(reset_queries),这个signalsdjango.core下的.py文件,内容如下:

from django.dispatch import Signal

request_started = Signal()
request_finished = Signal()
got_request_exception = Signal()
setting_changed = Signal()

Signal的内容为:

def _make_id(target):
    if hasattr(target, "__func__"):
        return (id(target.__self__), id(target.__func__))
    return id(target)


NONE_ID = _make_id(None)
NO_RECEIVERS = object()
logger = logging.getLogger("django.dispatch")


class Signal:
    """ 所有信号的基类 """

    def __init__(self, use_caching=False):
        self.receivers = []
        self.lock = threading.Lock()
        self.use_caching = use_caching
        # 为了方便起见,我们创建空缓存,即使它们没有被使用。关于缓存的注意事项:如果定义了use_caching,
        # 那么对于每个不同的发送方,我们将发送方在“sender_receivers_cache”中缓存接收方。
        # 当调用.connect()或.disconnect()并在send()上填充时,缓存将被清除。
        self.sender_receivers_cache = weakref.WeakKeyDictionary() if use_caching else {}
        self._dead_receivers = False

    def connect(self, receiver, sender=None, weak=True, dispatch_uid=None):
        """
        将接收器连接到发送器以获取信号。 
        receiver : 接收信号的函数或实例方法。 接收者必须是可哈希对象。
                   如果weak为True,则接收者必须是弱可引用的。 接收者必须能够接受关键字参数。
                   如果接收器与dispatch_uid参数连接,则如果另一个接收器已与该dispatch_uid连接,则不会添加该接收器。
        sender : 接收者应响应的发送者。必须是 Python 对象,或者 None 才能从任何发送者接收事件。
        weak : 是否对接收者使用弱引用。默认情况下,模块将尝试使用对接收者对象的弱引用。如果此参数为 false,则将使用强引用。
        dispatch_uid : 用于唯一标识接收器的特定实例的标识符。这通常是一个字符串,尽管它可能是任何可散列的东西。
        """
        from django.conf import settings

        if settings.configured and settings.DEBUG:  # 如果 DEBUG 打开,检查我们是否有一个好的接收器
            if not callable(receiver):
                raise TypeError("Signal receivers must be callable.")
            if not func_accepts_kwargs(receiver):  # 检查 **kwargs
                raise ValueError("Signal receivers must accept keyword arguments (**kwargs).")

        if dispatch_uid:
            lookup_key = (dispatch_uid, _make_id(sender))
        else:
            lookup_key = (_make_id(receiver), _make_id(sender))

        if weak:
            ref = weakref.ref
            receiver_object = receiver
            # 检查绑定的方法
            if hasattr(receiver, "__self__") and hasattr(receiver, "__func__"):
                ref = weakref.WeakMethod
                receiver_object = receiver.__self__
            receiver = ref(receiver)
            weakref.finalize(receiver_object, self._remove_receiver)

        with self.lock:
            self._clear_dead_receivers()
            if not any(r_key == lookup_key for r_key, _ in self.receivers):
                self.receivers.append((lookup_key, receiver))
            self.sender_receivers_cache.clear()

    def disconnect(self, receiver=None, sender=None, dispatch_uid=None):
        """
        断开信号接收器与发送器的连接。 如果使用弱引用,则不需要调用disconnect。 接收器将自动从发送中删除。
        receiver : 注册的接收者断开连接。 如果指定了dispatch_uid,则可能为none。
        sender : 已注册的发件人断开连接
        dispatch_uid : 要断开连接的接收者的唯一标识符
        """
        if dispatch_uid:
            lookup_key = (dispatch_uid, _make_id(sender))
        else:
            lookup_key = (_make_id(receiver), _make_id(sender))

        disconnected = False
        with self.lock:
            self._clear_dead_receivers()
            for index in range(len(self.receivers)):
                (r_key, _) = self.receivers[index]
                if r_key == lookup_key:
                    disconnected = True
                    del self.receivers[index]
                    break
            self.sender_receivers_cache.clear()
        return disconnected

    def has_listeners(self, sender=None):
        return bool(self._live_receivers(sender))

    def send(self, sender, **named):
        """
        将信号从发送器发送到所有连接的接收器。如果任何接收者引发错误,该错误将通过发送传播
        回来,从而终止调度循环。因此,如果出现错误,所有接收器可能都不会被调用。

        sender : 信号的发送者。 可以是特定对象,也可以是“无”。
        named : 将传递给接收者的命名参数。
        返回 : [(receiver, response), ... ]。
        """
        if (
                not self.receivers
                or self.sender_receivers_cache.get(sender) is NO_RECEIVERS
        ):
            return []

        return [
            (receiver, receiver(signal=self, sender=sender, **named))
            for receiver in self._live_receivers(sender)
        ]

    def send_robust(self, sender, **named):
        """
        将信号从发送器发送到所有连接的接收器以捕获错误。

        sender : 信号的发送者。 可以是任何 Python 对象(如果您确实希望发生某些事情,通常是通过连接注册的对象)。
        named : 将传递给接收者的命名参数。
        返回 : 元组对列表 [(receiver, response), ... ]。

        如果任何接收者引发错误(特别是 Exception 的任何子类),则返回错误实例作为该接收者的结果。
        """
        if (
                not self.receivers
                or self.sender_receivers_cache.get(sender) is NO_RECEIVERS
        ):
            return []

        # 使用每个接收器可以接受的参数来调用它。 返回元组对列表 [(receiver, response), ... ]。
        responses = []
        for receiver in self._live_receivers(sender):
            try:
                response = receiver(signal=self, sender=sender, **named)
            except Exception as err:
                logger.error(
                    "Error calling %s in Signal.send_robust() (%s)",
                    receiver.__qualname__, err, exc_info=err,
                )
                responses.append((receiver, err))
            else:
                responses.append((receiver, response))
        return responses

    def _clear_dead_receivers(self):
        # 注意:假设调用者持有 self.lock
        if self._dead_receivers:
            self._dead_receivers = False
            self.receivers = [
                r
                for r in self.receivers
                if not (isinstance(r[1], weakref.ReferenceType) and r[1]() is None)
            ]

    def _live_receivers(self, sender):
        """
        过滤接收器序列以获得已解析的实时接收器。
        这会检查弱引用并解决它们,然后仅返回活动接收器。
        """
        receivers = None
        if self.use_caching and not self._dead_receivers:
            receivers = self.sender_receivers_cache.get(sender)
            # 即使由于并发 .send() 调用而在调用 _live_receivers() 之前在 .send()
            # 中检查了这种情况,我们也可能会得到 NO_RECEIVERS 。
            if receivers is NO_RECEIVERS:
                return []
        if receivers is None:
            with self.lock:
                self._clear_dead_receivers()
                senderkey = _make_id(sender)
                receivers = []
                for (receiverkey, r_senderkey), receiver in self.receivers:
                    if r_senderkey == NONE_ID or r_senderkey == senderkey:
                        receivers.append(receiver)
                if self.use_caching:
                    if not receivers:
                        self.sender_receivers_cache[sender] = NO_RECEIVERS
                    else:
                        # 注意,我们必须缓存弱引用版本。
                        self.sender_receivers_cache[sender] = receivers
        non_weak_receivers = []
        for receiver in receivers:
            if isinstance(receiver, weakref.ReferenceType):
                # 取消引用弱引用。
                receiver = receiver()
                if receiver is not None:
                    non_weak_receivers.append(receiver)
            else:
                non_weak_receivers.append(receiver)
        return non_weak_receivers

    def _remove_receiver(self, receiver=None):
        # 标记 self.receivers 列表有死的 weakrefs。 如果是这样,我们将在持有 self.lock 的同时清理 connect、
        # disconnect 和 _live_receivers 中的那些内容。 请注意,在这里进行清理并不是一个好主意,
        # _remove_receiver() 将被调用作为垃圾收集的副作用,因此当我们已经持有 self.lock 时,可能会发生调用。
        self._dead_receivers = True

Signal类,通过connect来定义接收者和发送者对,如果没定义发送者,则接收者会接收所有发送者的信号。
disconnect用于断开接收者和发送者。has_listeners用于判定发送者对应的接收者,包含监听所有发送者的接收者。
send用于发送信号,从receiver(signal=self, sender=sender, **named)可以看出,接收者必须能够识别关键字或命名关键字参数。
Signal的作用简单看来,也就是一个发送者发出信号,对应的接收者们进行接收,类似于一个转发器。

这段代码里有些比较有意思的东西:
其一,weakref.WeakKeyDictionary:这个类的使用和dict基本一致,但要求key必须是object对象。
它与dict不同的是,dict会阻止作为keyobject被销毁(因为object还在被dict使用,所以引用计数没有归0,故不会触发销毁方法)。
WeakKeyDictionary不会阻止作为keyobject被销毁,从而让object的内存被正常回收。
这在object是大对象,也就是占用了很大的内存时,会很有必要。

这个`WeakKeyDictionary`非常的奇特,它的操作,明显不符合普通python对象的流程:
学习python时,总会学习到python的垃圾回收机制:
	python的对象总是会有一个引用计数,每次对该对象的引用,就会增加这个引用计数,引用计数为0时,会删除该对象。
	或者通过`标记-清除`方法解相互引用后计数为0,也可以删除该对象。
	还有分代回收,这里无关,就不展开了。
可以想象,这里出现了一种新的情况,在引用某个对象时不会增加其引用计数,从而不影响垃圾回收。
这便是*弱引用*,它在上面的注释中已经大量出现过了。
笔者看到过一篇讲解弱引用的优秀文章:https://www.51cto.com/article/694445.html
由于弱引用直接与python底层数据结构挂钩,还需要先说明python底层数据结构中关于弱引用的部分,这里笔者就不展开了。
关于python底层机制,建议本站搜索“python幕后”进行学习,相信读者会获益不少。
python幕后的原网址:https://tenthousandmeters.com/

其二,threading.Lock(),线程锁,考虑如果没有线程锁,当进行多线程操作时,可能出现重复增加发送者和接收者、出现重复删除同一个发送者或接收者、调用不存在的发送者或接收者等操作,导致错误。

回到db.__init__.py文件:
signals.request_started给绑定了俩方法作为接收者,一个是reset_queries,用于清理所有connectqueries_log,一个是close_old_connections,尝试关闭所有的旧连接,如果旧连接发生不可恢复的错误或超过了最大寿命。
signals.request_finished绑定了close_old_connections作为接收者。
request_started顾名思义,是在请求开始时发信号,request_finished是请求结束时,发送的信号。
目前这个request_startedrequest_finished中的request指代哪些方法,暂时不知道。

走到此处,CommandParser命令的导入from django.db import DEFAULT_DB_ALIAS, connections总算弄清楚了
django.db会初始化一个懒加载的connectionsconnection用于连接所有数据库和default数据库。
如果没有真正调用connections去处理连接业务,比如获取某个指定的数据库连接,或者用游标执行命令等,则并不会真正的建立连接。
connection也是同理,并不会在创建时,立即创建与数据库的连接,而是等实际使用时才会创建连接。
另外,给signals中的实例,添加了一些接收者,用于请求初始化和请求结束时使用。

CommandParser这个类,从名字来看,命令解析器。
from argparse import ArgumentParser,这个argparse包是python自带的包,ArgumentParser类用来解析命令。
笔者大致研究了一下,这是一个相当复杂的类,还是直接看一下其实际使用效果:

# 这里使用命令"django-admin startproject django-test"
from argparse import ArgumentParser

subcommand = "startproject"  # 这里暂时没用到
parser = ArgumentParser(
    prog="django-admin",
    usage="%(prog)s subcommand [options] [args]",
    add_help=False,
    allow_abbrev=False,
)
parser.add_argument("--settings")
parser.add_argument("--pythonpath")
parser.add_argument("args", nargs="*")
options, args = parser.parse_known_args(["django_test"])

这里来看一下结果:

parser = ArgumentParser(
    'prog': 'django-admin',
    'usage': '%(prog)s subcommand [options] [args]',
    'description': None,
    'formatter_class': <class 'argparse.HelpFormatter'>,
    'conflict_handler': 'error',
    'add_help': False,
    '_actions': [
        _StoreAction(option_strings=['--settings'], dest='settings'),
        _StoreAction(option_strings=['--pythonpath'], dest='pythonpath'),
        _StoreAction(option_strings=[], dest='args', nargs='*')
    ]
)

# Namespace会将参数变成属性
options = Namespace(settings=None, pythonpath=None, args=['django_test'])

args = []

这个CommandParser命令解析器,看起来只是比ArgumentParser多了一些检查,没有本质的区别。

之后是调用handle_default_options,这个功能仅有两个操作:
一是将options中的settings属性设置为"DJANGO_SETTINGS_MODULE",如果settings属性不是空的话;
二是options.pythonpath放到系统路径的最前面,即每次通过系统路径寻找功能或变量时,先走pythonpath,如果pythonpath不是空。

def handle_default_options(options):
    if options.settings:
        os.environ["DJANGO_SETTINGS_MODULE"] = options.settings
    if options.pythonpath:
        sys.path.insert(0, options.pythonpath)

继续查看源码:

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

尝试从settings中获取INSTALLED_APPS属性,这个settingsLazySettings包装后的Settings对象,如果不调用其中的属性,即便没有某些属性,或者某些属性格式错误,它也不会报错,故而这里显式调用INSTALLED_APPS来检查这个属性是否有误。
INSTALLED_APPSdjango.conf.global_settings.py文件中为空列表,在用户项目的中settings.py文件中,默认为:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

用于表示当前需要注册的应用。

        if settings.configured:
            if subcommand == "runserver" and "--noreload" not in self.argv:
                pass  # 这段儿暂时省略
            else:
                django.setup()

settings.configured代表settings是否已经完成了属性加载,前面说到settings初始化时是不会主动加载的,但是如果加载INSTALLED_APPS的过程出错,settings.configured也不一定就是False,比如,当用户的INSTALLED_APPS是一个数字或者字符串,则settings在加载的过程中,会报出ImproperlyConfigured错误,但是这并不影响settings.configuredTrue,至于为什么是这个逻辑,需要继续往下看。
这里的subcommand不是"runserver",而是startproject,故而先跳过"runserver"的部分(这部分研究runserver时再看),直接来看django.setup()
是否还有印象,setup这个功能定义于django.__init__.py

def setup(set_prefix=True):
    """
    配置设置(这是访问第一个设置的副作用),配置日志记录并填充应用程序注册表。
    如果“set_prefix”为 True,则设置线程本地 urlresolvers 脚本前缀。
    """
    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.apps.__init__.py,需要看看它做了什么:

from .config import AppConfig
from .registry import apps

__all__ = ["AppConfig", "apps"]

功能简单,导入了AppConfigapps
查看了下config.py文件没有特殊的导入,并没有坑。
上面调用了apps这个实例,需要研究一下,首先是导入:

from django.core.exceptions import AppRegistryNotReady, ImproperlyConfigured
from .config import AppConfig

这里也调用了AppConfig,看来迟早还是要研究的。
apps实例的定义为 :apps = Apps(installed_apps=None)
所以这里的关键在于Apps类,这里只摘抄其中的几个方法:

class Apps:
    """
    存储已安装应用程序配置的注册表。 它还跟踪模型,例如 提供反向关系。
    """

    def __init__(self, installed_apps=()):
        # 在创建主注册表时,installed_apps 设置为 None,因为此时无法填充它。
        # 其他注册表必须提供已安装应用程序的列表并立即填充。
        if installed_apps is None and hasattr(sys.modules[__name__], "apps"):
            raise RuntimeError("You must supply an installed_apps argument.")

        # 应用程序标签的映射 => 模型名称 => 模型类。 每次导入模型时,ModelBase.__new__
        # 都会调用 apps.register_model,从而在 all_models 中创建一个条目。 所有导入的模
        # 型都会被注册,无论它们是否在已安装的应用程序中定义以及是否已填充注册表。由于不可能安
        # 全地重新导入模块(它可以重新执行初始化代码),因此 all_models 永远不会被覆盖或重置。
        self.all_models = defaultdict(dict)

        self.app_configs = {}  # 将标签映射到已安装应用程序的 AppConfig 实例。

        # app_configs堆栈。用于将当前状态存储在set_available_apps和set_installed_apps中
        self.stored_app_configs = []

        self.apps_ready = self.models_ready = self.ready = False  # 注册表是否已填充
        self.ready_event = threading.Event()  # 对于自动重新加载器
        self._lock = threading.RLock()  # 线程安全群体的锁
        self.loading = False

        # 将 ("app_label", "modelname") 元组映射到相应模型准备好时要调用的函数列表。
        # 由此类的`lazy_model_operation()`和`do_pending_operations()`方法使用。
        self._pending_operations = defaultdict(list)
        if installed_apps is not None:  # 填充应用程序和模型,除非它是主注册表
            self.populate(installed_apps)

    def populate(self, installed_apps=None):
        """
        加载应用程序配置和模型。 导入每个应用程序模块,然后导入每个模型模块。
        它是线程安全且幂等的,但不可重入。
        """
        if self.ready:  # 不可重复填充
            return

        # populate()可能由服务器上的两个线程并行调用,这些线程在初始化WSGI可调用之前创建线程。
        with self._lock:  # 使用线程锁
            if self.ready:
                return

            # RLock 防止其他线程进入此部分。下面的比较和设置操作是原子的。
            if self.loading:
                # 防止重入调用以避免运行 AppConfig.ready() 方法两次。
                raise RuntimeError("populate() isn't reentrant")
            self.loading = True

            # 第 1 阶段:初始化应用程序配置并导入应用程序模块。
            for entry in installed_apps:
                if isinstance(entry, AppConfig):
                    app_config = entry
                else:
                    app_config = AppConfig.create(entry)
                if app_config.label in self.app_configs:
                    raise ImproperlyConfigured(
                        "Application labels aren't unique, "
                        "duplicates: %s" % app_config.label
                    )

                self.app_configs[app_config.label] = app_config
                app_config.apps = self

            # 检查重复的应用程序名称
            counts = Counter(
                app_config.name for app_config in self.app_configs.values()
            )
            duplicates = [name for name, count in counts.most_common() if count > 1]
            if duplicates:
                raise ImproperlyConfigured(
                    "Application names aren't unique, "
                    "duplicates: %s" % ", ".join(duplicates)
                )

            self.apps_ready = True

            # 第二阶段:导入模型模块
            for app_config in self.app_configs.values():
                app_config.import_models()

            self.clear_cache()
            self.models_ready = True

            # 第三阶段:运行应用程序配置的ready()方法
            for app_config in self.get_app_configs():
                app_config.ready()

            self.ready = True
            self.ready_event.set()

    def get_app_configs(self):
        """导入应用程序并返回应用程序配置的可迭代对象"""
        self.check_apps_ready()
        return self.app_configs.values()

    def check_apps_ready(self):
        """如果尚未导入所有应用程序,则引发异常"""
        if not self.apps_ready:
            from django.conf import settings
            settings.INSTALLED_APPS
            raise AppRegistryNotReady("Apps aren't loaded yet.")

先看看其中有趣的一个类:
threading.RLock()这个类,它和threading.Lock()有什么区别:
区别1,使用threading.Lock()时,锁可以被其它线程释放,比如A线程acquire()了某个锁,而B线程可以release()这个锁。
但是threading.RLock()时,锁只能由acquire()了某个锁的线程释放,不能由其它线程释放。
区别2,使用threading.Lock()时,在同一个线程内,不能嵌套使用acquire().
如果嵌套使用acquire()命令,就会发生死锁,比如:

           my_lock = threading.Lock()
           my_lock.acquire()
           my_lock.acquire()
           my_lock.release()
           my_lock.release()
       而如果将`threading.Lock()`改为`threading.RLock()`,则可以正常运行。
       要注意使用`RLock()`时,`acquire()`和`release()`数目相同。

还有一个实例叫 self.ready_event,这个实例在startproject时没啥用,而在runserver时将会有大用,这里暂时跳过。

更重要的是类AppConfig,这个类看起来比较复杂,且检查较多,就不再粘贴源码,但该类的功能是很清晰的:
通过AppConfig.create(entry)来产生一个AppConfig子类的实例,具体是哪个子类,则是由entry所代表的路径决定的。
这里需要一个例子来说明:使用python manage.py startapp django_test2这个命令来生成一个app,默认情况下,django会在django_test2文件夹下生成一个叫apps.py的文件,这个文件内容如下:

from django.apps import AppConfig

class DjangoTest2Config(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'django_test2'

如果把django_test2注册到INSTALLED_APPS中,那么当django要初始化django_test2这个app的时候,就会调用DjangoTest2Config这个类(作为AppConfig的子类)。
这个类在实例化时将传入两个属性,分别是app_nameapp_moduleapp_nameentry或者做一些修改,存储为self.nameapp_module则是导入的模块,存储为self.module

app_config.import_models()这个功能,通过self.label在所有app中获取self.model,并尝试寻找self.module(注意不是self.model)中的models模块,将它导入为self.models_modulemodels.py文件一般被用作存放模型类。

app_config.ready()这个功能,由AppConfig的子类(上例中的DjangoTest2Config)实现,不实现该方法则没有效果。
Django 官方文档对ready()方法的说明为:

"""
子类可以重写此方法来执行类似注册信号的初始化任务。只要注册表被填满就会调用此方法。
虽然你不能在定义 AppConfig 类的模型层导入模型,但可以在 ready() 中导入,通过 import 语句或 get_model()。
若你正在注册 model signals,你可以通过字符串标签追踪发信者,而不是用模型类。
"""
# 举例:
from django.apps import AppConfig
from django.db.models.signals import pre_save

class RockNRollConfig(AppConfig):
    # ...

    def ready(self):
        # importing model classes
        from .models import MyModel  # or...

        MyModel = self.get_model("MyModel")

        # registering signals with the model's string label
        pre_save.connect(receiver, sender="app_label.MyModel")

看完了导入app,来看看setup实际的工作
configure_logging顾名思义是设置log日志的等级,由于是startproject的分析,这里跳过。
set_script_prefix用于设置脚本前缀,结合代码看,就是设置脚本的前缀是否为/,或者通过FORCE_SCRIPT_NAME来修改。
apps.populate(settings.INSTALLED_APPS),注册apps,上面关于apps这个实例的导入说了一大堆,关键就在说明这个功能。
总结一下populate的功能,就是通过INSTALLED_APPSapp的路径(或者说名称),加载路径中的app.py文件的AppConfig子类,并尝试获取该路径下的model.py文件,将其导入并保存,尝试创建连接。
只是,startproject的时候,INSTALLED_APPS是个空列表,runserver等命令时,apps.populate(settings.INSTALLED_APPS)才会真正由意义。

回到ManagementUtility.execute(),接下来为self.autocomplete():
这个功能要求os.environ中存在DJANGO_AUTO_COMPLETE这个变量,如果没有就什么也不做。
这个DJANGO_AUTO_COMPLETE不知道哪里有,这个功能可以直接跳过。

"help""version"subcommand暂时跳过,这里直接看和startproject相关的代码:
self.fetch_command(subcommand).run_from_argv(self.argv)

fetch_command这个功能较为简单,通过subcommand寻找django.core.management.commands下的文件,并返回其Command()实例。
这里的核心功能为load_command_class

def load_command_class(app_name, name):
    module = import_module("%s.management.commands.%s" % (app_name, name))
    return module.Command()

这个功能代表着一种新的设计模式:命令模式
标准的命令模式,包括三个角色:1、received 真正的命令执行对象 2、Command 3、invoker 使用命令对象的入口
这段关于调用命令的方法,正是一个标准的命令模式。
命令模式的优点是降低了系统耦合,可以很容易在系统中添加新的命令。

关于Command类,比如startproject,会寻找django.core.management.commands.Command类,该Command类的内容为:

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"] = SECRET_KEY_INSECURE_PREFIX + get_random_secret_key()

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

这个Command只定义了handle方法,且最后会调用父类的handle方法,这里简单分析下handle的操作,project_nametarget 获取了options中的一些属性,给options添加了一个"secret_key",它的值是一个预设的SECRET_KEY_INSECURE_PREFIX(默认为"django-insecure-"的字符串)加上一个长度为50的随机字符串,看起来并没有什么重要操作,那么它的重点在于父类的handle方法。

startprojectCommand类继承自TemplateCommandTemplateCommand继承自BaseCommandBaseCommand定义了Command的基础功能和子类应当重定义的功能。

关于各种`Command`的继承,是一种比较标准的模板模式。
模板模式在父类,或者叫基类、超类中,定义好基础的流程和调用方式,子类通过重新定义其中的部分功能,实现不同的实际效果。
模板模式主要适用于各种流程固定,但是处理各有不同的情况。
为了实现模板效果,并符合里氏替代的原则,模板模式的基类往往是不可实例化的。
	实例化后按照正常步骤使用会抛出异常,或者是实例化后没有实际效果,从而强制子类必须重写某些方法。
注意,并不是所有使用基类来统合多个父类的方式,都能称作模板模式,模板模式的关键在于 `父类控制程序流程` 。
也就是说,在形式上,模板模式是父类调用了子类方法,子类不会修改流程只会重写方法。

比如这里的BaseCommand就是一个非常经典的模板方法父类,这里只摘抄其中的部分功能:

class BaseCommand:
    """
     所有 Command 命令最终派生自的基类。
     如果您想要访问解析命令行参数并确定要调用哪些代码作为响应的所有机制,请使用此类;
     如果您不需要更改任何行为,请考虑使用此文件中定义的子类之一。

     如果您有兴趣覆盖/自定义命令解析和执行行为的各个方面,正常流程如下:
     1. ``django-admin`` 或 ``manage.py`` 加载命令类并调用其 ``run_from_argv()`` 方法。
     2. “run_from_argv()”方法调用“create_parser()”来获取参数的“ArgumentParser”,解析它们,
        执行“pythonpath”等选项请求的任何环境更改,然后 调用“execute()”方法,传递解析后的参数。
     3. `execute()`方法尝试通过使用解析后的参数调用`handle()`方法来执行命令;`handle()`生成的任何
        输出都将打印到标准输出,并且如果该命令旨在生成 SQL 语句块,则将被包装在 `BEGIN`和`COMMIT`中。
     4. 如果“handle()”或“execute()”引发任何异常(例如“CommandError”),“run_from_argv()”将向
        “stderr”打印一条错误消息。

     因此,“handle()”方法通常是子类的起点; 许多内置命令和命令类型要么将所有逻辑放在“handle()”中,
     要么在“handle()”中执行一些额外的解析工作,然后根据需要将其委托给更专门的方法。

     有几个属性会影响整个过程中各个步骤的行为:

     ``help``
         命令的简短描述,将打印在帮助消息中。

     ``output_transaction``(输出事务)
         一个布尔值,指示命令是否输出SQL语句;如果为“True”,输出将自动用“BEGIN;”和“COMMIT;”包裹。
         默认值为``False``。

     ``requires_migrations_checks``(需要迁移检查)
         一个布尔值; 如果为“True”,如果磁盘上的迁移集与数据库中的迁移不匹配,该命令将打印警告。

     ``requires_system_checks``(需要系统检查)
         标签列表或元组,例如 [Tags.staticfiles, Tags.models]。在执行命令之前,将检查在所选标签中
         注册的系统检查是否有错误。 值“__all__”可用于指定应执行所有系统检查。 默认值为“__all__”。

         要验证单个应用程序的模型而不是所有应用程序的模型,请从“handle()”调用
         “self.check(app_configs)”,其中“app_configs”是应用程序注册表提供的应用程序配置列表 。

     ``stealth_options``(隐形选项)
         命令使用的任何选项的元组,这些选项未由参数解析器定义。
    """

    # 有关此命令的元数据
    help = ""

    # 改变各种逻辑的配置快捷方式
    _called_from_command_line = False
    output_transaction = False  # 是否将输出包装在“BEGIN; COMMIT;”中
    requires_migrations_checks = False
    requires_system_checks = "__all__"
    # 所有命令共有的参数,不是由参数解析器定义的
    base_stealth_options = ("stderr", "stdout")
    # 参数解析器未定义的特定于命令的选项
    stealth_options = ()
    suppressed_base_arguments = set()

    def __init__(self, stdout=None, stderr=None, no_color=False, force_color=False):
        self.stdout = OutputWrapper(stdout or sys.stdout)
        self.stderr = OutputWrapper(stderr or sys.stderr)
        if no_color and force_color:
            raise CommandError("'no_color' and 'force_color' can't be used together.")
        if no_color:
            self.style = no_style()
        else:
            self.style = color_style(force_color)
            self.stderr.style_func = self.style.ERROR
        if (
                not isinstance(self.requires_system_checks, (list, tuple))
                and self.requires_system_checks != ALL_CHECKS
        ):
            raise TypeError("requires_system_checks must be a list or tuple.")

    def create_parser(self, prog_name, subcommand, **kwargs):
        """
        创建并返回“ArgumentParser”,它将用于解析该命令的参数
        """
        kwargs.setdefault("formatter_class", DjangoHelpFormatter)
        parser = CommandParser(
            prog="%s %s" % (os.path.basename(prog_name), subcommand),
            description=self.help or None,
            missing_args_message=getattr(self, "missing_args_message", None),
            called_from_command_line=getattr(self, "_called_from_command_line", None),
            **kwargs,
        )
        pass  # ... 这里省略了一些 add_base_argument 的操作
        self.add_base_argument(
            parser,
            "--settings",
            help=(
                "The Python path to a settings module, e.g. "
                '"myproject.settings.main". If this isn\'t provided, the '
                "DJANGO_SETTINGS_MODULE environment variable will be used."
            ),
        )
        pass  # ... 这里省略了一些 add_base_argument 的操作
        if self.requires_system_checks:
            parser.add_argument(
                "--skip-checks",
                action="store_true",
                help="Skip system checks.",
            )
        self.add_arguments(parser)
        return parser

    def add_arguments(self, parser):
        """子类化命令添加自定义参数的入口点"""
        pass

    def add_base_argument(self, parser, *args, **kwargs):
        """
        调用解析器的 add_argument() 方法,
        根据 BaseCommand.suppressed_base_arguments 抑制帮助文本。
        """
        for arg in args:
            if arg in self.suppressed_base_arguments:
                kwargs["help"] = argparse.SUPPRESS
                break
        parser.add_argument(*args, **kwargs)

    def run_from_argv(self, argv):
        """
        设置请求的任何环境更改(例如,Python 路径和 Django 设置),然后运行此命令。
        如果命令引发“CommandError”,则拦截它并将其合理地打印到 stderr。
        如果存在“--traceback”选项或者引发的“Exception”不是“CommandError”,则引发它。
        """
        self._called_from_command_line = True
        parser = self.create_parser(argv[0], argv[1])

        options = parser.parse_args(argv[2:])
        cmd_options = vars(options)
        # 将位置参数从选项中移出以模仿传统的 optparse
        args = cmd_options.pop("args", ())
        handle_default_options(options)
        try:
            self.execute(*args, **cmd_options)
        except CommandError as e:
            if options.traceback:
                raise

            # SystemCheckError 负责处理它自己的格式
            if isinstance(e, SystemCheckError):
                self.stderr.write(str(e), lambda x: x)
            else:
                self.stderr.write("%s: %s" % (e.__class__.__name__, e))
            sys.exit(e.returncode)
        finally:
            try:
                connections.close_all()
            except ImproperlyConfigured:
                # 如果此时尚未设置连接(例如,未配置设置),请忽略
                pass

    def execute(self, *args, **options):
        """
        尝试执行此命令,如果需要则执行系统检查(由“requires_system_checks”属性控制,除非强制跳过)
        """
        if options["force_color"] and options["no_color"]:
            raise CommandError(
                "The --no-color and --force-color options can't be used together."
            )
        if options["force_color"]:
            self.style = color_style(force_color=True)
        elif 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"])

        if self.requires_system_checks and not options["skip_checks"]:
            if self.requires_system_checks == ALL_CHECKS:
                self.check()
            else:
                self.check(tags=self.requires_system_checks)
        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)
        return output

    def handle(self, *args, **options):
        """命令的实际逻辑,子类必须实现该方法"""
        raise NotImplementedError("subclasses of BaseCommand must provide a handle() method")

这里的handle是比较典型的,必须由子类实现的方法。
这里的add_arguments方法,就属于如果不实现,则什么都不做的方法。

父类定义的重要流程开始接口,是run_from_argv方法,它会调用create_parserexecute
create_parser除了初始化parser并添加了一堆参数外,剩余的部分,便是add_arguments,可由子类按需实现。
execute方法的核心,是handle方法,handle强制子类实现。

除此之外,BaseCommand实现了有关数据库的相关方法,主要是是否通过事务所需关键字“BEGIN; COMMIT;”进行包裹;以及写输出和log。
分析startproject时,和这些SQL相关流程没什么关系,所以这里的关键在于add_argumentshandle

需要继续分析TemplateCommand来查看add_argumentshandle方法:
TemplateCommandadd_arguments方法:

    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 file names "
            "with commas, or use -n multiple times.",
        )
        parser.add_argument(
            "--exclude",
            "-x",
            action="append",
            default=argparse.SUPPRESS,
            nargs="?",
            const="",
            help=(
                "The directory name(s) to exclude, in addition to .git and "
                "__pycache__. Can be used multiple times."
            ),
        )

加了一堆参数,具体的作用暂时也看不出来。

这里更重要的功能是handle,这个才是各个Command实际的功能
这里笔者以django-admin startproject django_test为例,添加该方法的注释:

    def handle(self, app_or_project, name, target=None, **options):
        # 以 django-admin startproject django_test 为例
        # startproject 子类的 handle 对父类的调用方式为:
        #   super().handle("project", project_name, target, **options)
        # 故 app_or_project 为 "project"
        self.app_or_project = app_or_project

        # 大概猜测一下,"app"和"project"会用到该类,也就是startapp和startproject
        self.a_or_an = "an" if app_or_project == "app" else "a"

        self.paths_to_remove = []
        self.verbosity = options["verbosity"]

        # 检查name:1.不能为None 2.必须是有效的目录名称 3.不能是可导入的模块
        self.validate_name(name)  # 例:这里的 name 为 `django_test`

        # 如果给出了某个目录,请确保它已很好地扩展
        if target is None:  # 一般情况下 startproject 不会指定路径
            # 获取当前工作目录,与 name 组成新的路径
            top_dir = os.path.join(os.getcwd(), name)
            try:
                os.makedirs(top_dir)  # 创建文件夹
            except FileExistsError:
                raise CommandError("'%s' already exists" % top_dir)
            except OSError as e:
                raise CommandError(e)
        else:
            top_dir = os.path.abspath(os.path.expanduser(target))
            if app_or_project == "app":
                self.validate_name(os.path.basename(top_dir), "directory")
            if not os.path.exists(top_dir):
                raise CommandError(
                    "Destination directory '%s' does not "
                    "exist, please create it first." % top_dir
                )

        # 在模板的输入可以潜入路径之前,找到格式化程序(外部可执行文件)
        formatter_paths = find_formatters()

        extensions = tuple(handle_extensions(options["extensions"]))
        extra_files = []
        excluded_directories = [".git", "__pycache__"]  # 需要忽略的文件(夹)
        for file in options["files"]:
            extra_files.extend(map(lambda x: x.strip(), file.split(",")))
        if exclude := options.get("exclude"):  # 把指定忽略的文件加入进来
            for directory in exclude:
                excluded_directories.append(directory.strip())
        if self.verbosity >= 2:
            self.stdout.write(
                "Rendering %s template files with extensions: %s"
                % (app_or_project, ", ".join(extensions))
            )
            self.stdout.write(
                "Rendering %s template files with filenames: %s"
                % (app_or_project, ", ".join(extra_files))
            )
        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(
            {
                **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,
        )

        # 设置模板渲染的存根设置环境
        if not settings.configured:
            settings.configure()
            django.setup()

        # startproject 的 "template" 并不设置,所以取 base_subdir 即 "project_template"
        # template_dir 的值是 'django安装位置\\conf\\project_template',即模板文件夹
        template_dir = self.handle_template(options["template"], base_subdir)
        prefix_length = len(template_dir) + 1

        # 递归的获取 template_dir 文件夹下的文件夹和文件
        for root, dirs, files in os.walk(template_dir):
            path_rest = root[prefix_length:]
            # template_dir下有一个叫'project_name'的文件夹(startapp时是'app_name')
            # 把这个文件夹的名字替换为项目名(startapp时是app名),例:django_test
            relative_dir = path_rest.replace(base_name, name)
            if relative_dir:  # 如果relative_dir是文件夹,就在top_dir下创建文件夹
                target_dir = os.path.join(top_dir, relative_dir)
                os.makedirs(target_dir, exist_ok=True)

            for dirname in dirs[:]:  # 去掉不需要的文件
                if "exclude" not in options:
                    if dirname.startswith(".") or dirname == "__pycache__":
                        dirs.remove(dirname)
                elif dirname in excluded_directories:
                    dirs.remove(dirname)

            for filename in files:
                if filename.endswith((".pyo", ".pyc", ".py.class")):
                    # 忽略某些文件,因为它们会导致各种损坏
                    continue
                old_path = os.path.join(root, filename)
                new_path = os.path.join(
                    top_dir, relative_dir, filename.replace(base_name, name)
                )
                for old_suffix, new_suffix in self.rewrite_template_suffixes:
                    # 这里把`.py-tpl`文件改成了`.py`文件
                    if new_path.endswith(old_suffix):
                        new_path = new_path[: -len(old_suffix)] + new_suffix
                        break  # 只会重写一次

                if os.path.exists(new_path):
                    raise CommandError(
                        "%s already exists. Overlaying %s %s into an existing "
                        "directory won't replace conflicting files."
                        % (
                            new_path,
                            self.a_or_an,
                            app_or_project,
                        )
                    )

                # 仅渲染 .py 文件,因为我们不想意外渲染 Django 模板文件
                if new_path.endswith(extensions) or filename in extra_files:
                    with open(old_path, encoding="utf-8") as template_file:
                        content = template_file.read()
                    # 利用python的模板引擎渲染文件
                    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:  # 不是 .py 文件就直接拷贝
                    shutil.copyfile(old_path, new_path)

                if self.verbosity >= 2:
                    self.stdout.write("Creating %s" % new_path)
                try:  # 修改产生的新文件的权限
                    self.apply_umask(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,
                    )

        if self.paths_to_remove:  # 去掉需要移除的文件(夹)
            if self.verbosity >= 2:
                self.stdout.write("Cleaning up temporary files.")
            for path_to_remove in self.paths_to_remove:
                if os.path.isfile(path_to_remove):
                    os.remove(path_to_remove)
                else:
                    shutil.rmtree(path_to_remove)

        # 对指定文件运行黑色格式化程序,就是使用`PEP 8`风格对文件重新进行排版
        run_formatters([top_dir], **formatter_paths)

这个功能的流程是很清晰的,看一下project_template这个文件夹下有什么:

project_template
        │
        │  manage.py-tpl
        │
        └─project_name
                asgi.py-tpl
                settings.py-tpl
                urls.py-tpl
                wsgi.py-tpl
                __init__.py-tpl

.py-tpl是一种python模板文件,一般常和 Jinja2 配合使用,用于生成 python 代码。
Jinja2 是一个python的功能齐全的模板引擎,属于学习 Django 时比较常见的模板。

这里 Django 默认使用的模板引擎不是Jinja2 ,而是 Django 自带的模板引擎。
Django 自带的模板引擎简化 (注意,不是真正的流程) 来说,就是利用 re.compile(r"({%.*?%}|{{.*?}}|{#.*?#})")这个正则表达式来分割字符串。
比如 "a{{ x }}b" 将被分割成三部分"a""{{ x }}""b",分割出来的每个字符串,称之为token_string
然后,django模板根据token_string的前两个字符,先大致判定该token_string代表的类型。
比如{%会被识别为块,{{会被识别为变量,{#会被识别为注释,其它会被识别为普通文字。
这里有一个特殊情况,就是{% verbatim %}(逐字),当遇到这个块之后,遇到{% endverbatim %}之前,其中的token_string都会被识别为普通文字,或者说是普通字符串,不会做任何转换。
token_string被识别非普通文字时,content = token_string[2:-2].strip(),普通文字则content = token_string
Djangotoken_string的类型,content,以及位置和行号(用于调试和报错),组合成一个token对象(其类为Token)。
之后,就可以根据各个token的类型,和用户自定义的上下文context来确定内容了。
这里的主要难点在如何保证渲染结果的安全, Django 这里使用了一个叫SafeString的类,来确保安全。
不过,笔者没有看懂这个SafeString的类对python文件的渲染有什么用,这里也就不做说明了。

至此,django-admin startproject django_test 这个命令的作用,解析完毕。

总结

无关 Django 的知识点:

  1. 设计模式:代理模式、命令模式、模板模式
  2. 懒加载
  3. 弱引用
  4. ArgumentParser
  5. threading.Lock()threading.RLock()

Django 相关的知识点:

  1. manage.pydjango-admin 多了一个 DJANGO_SETTINGS_MODULE 的导入
  2. settings是一个懒加载的,用于获取配置的实例,加载配置时,默认先加载global_settings.py,再用DJANGO_SETTINGS_MODULE中的配置覆盖,只读大写配置。
  3. 实例connections是一个懒加载的,代表对多个数据库连接的实例, connection 是对 default 数据库的连接。
  4. Signal类,可用于转发命令。
  5. apps 负责组织已安装的应用,apps.populate()功能用于导入所有应用程序,django.setup()功能会调用populate
  6. import_module("%s.management.commands.%s" % (app_name, name))
  7. Django 模板引擎与渲染。
  • 13
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值