django源码分析之app加载(app registry)

应用程序注册表(app registry):

当运行Django项目时,Django需要做的第一件事情是查找与该项目关联的应用程序(apps),以便知道该项目使用的代码。Django使用配置文件里的INSTALLED_APPS设置来查找项目中的所有应用程序,并构建要运行的应用程序列表。Django在此上下文中将应用程序列表称为应用程序注册表app registry

比如下面一段setting.py配置文件中的代码,是刚创建好的项目都会在setting.py文件中默认生成的。Django会循环载入这些apps和相应的models。下面我们来分析Django是如何实现的这些操作。

# project/setting.py

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

代码分析

django.setup入手分析,看到导入apps模块的代码是from django.apps import apps,因此可以确认模块路径是django.apps。而且使用的是apps变量,马上我们就能知道apps是什么东西。
它下面有3个文件,__init__.pyconfig.pyregistry.py,接下来分别来分析。

# django/apps/__init__.py

from .config import AppConfig
from .registry import apps

__all__ = ['AppConfig', 'apps']

定义了__all__变量,也就是当from <module> import *,只会导出'AppConfig'和'apps',接下来我们会发现它们两分别在config.pyregistry.py文件中。

先来看下config.py文件

# django/apps/config.py

class AppConfig:
    """表示Django应用程序及其配置的类。"""

    def __init__(self, app_name, app_module):
        # 到应用程序的完整的Python路径 e.g. 'django.contrib.admin'.
        self.name = app_name
        # 应用程序的Root模块 e.g. <module 'django.contrib.admin'
        # from 'django/contrib/admin/__init__.py'>.
        self.module = app_module
        # 对持有此AppConfig的应用程序注册表的引用。 当注册AppConfig实例时由注册表设置。
        self.apps = None

        # 以下属性可以在子类中的类级定义,因此用了test-and-set模式。
        # 应用程序的Python路径的最后一部分, e.g. 'admin'.这个值在Django项目中必须是唯一的。
        if not hasattr(self, 'label'):
            self.label = app_name.rpartition(".")[2]
        # 应用程序的Human-readable名字 e.g. "Admin".
        if not hasattr(self, 'verbose_name'):
            self.verbose_name = self.label.title()

        # 到应用程序目录的文件系统路径 e.g. '/path/to/django/contrib/admin'.
        if not hasattr(self, 'path'):
            self.path = self._path_from_module(app_module)

        # 包含models的模块 e.g. <module 'django.contrib.admin.models'
        # from 'django/contrib/admin/models.py'>. 由import_models()设置. 如果没有models,则为None
        self.models_module = None

        # 将model名称(小写格式)映射到model类。防止import_models()运行之前被意外访问,一开始设置为None。
        self.models = None

    (此处略过部分不影响理解核心逻辑的代码)...

    @classmethod
    def create(cls, entry):
        """
        用INSTALLED_APPS中的条目来创建AppConfig实例的工厂方法,
        其中条目可以是一个应用程序模块的路径,也可以是一个应用程序配置类的路径。
        """
        try:
            # 如果import_module成功,则INSTALLED_APPS中的条目是应用程序模块的路径,
            # 它可能使用default_app_config指定了一个应用程序配置类(AppConfig类);
            # 否则,该条目可能是指向一个应用程序配置类的路径或者就是条错误的条目。
            module = import_module(entry)

        except ImportError:
            # 发现按应用程序模块导入失败,如果按应用程序配置类(AppConfig类)导入也失败的话,就触发ImportError。
            module = None

            mod_path, _, cls_name = entry.rpartition('.')

            # 安AppConfig类导入也失败的话,抛出异常,即ImportError。
            if not mod_path:
                raise

        else:
            try:
                # 按应用程序模块导入成功,判断是否有指定应用程序配置类。
                entry = module.default_app_config
            except AttributeError:
                #  没有指定app config class, 用默认的应用程序配置类。
                return cls(entry, module)
            else:
                mod_path, _, cls_name = entry.rpartition('.')

        # 如果我们达到了这里,我们必须尝试加载位于<mod_path>.<cls_name>的应用程序配置类。
        mod = import_module(mod_path)
        try:
            cls = getattr(mod, cls_name)
        except AttributeError:
            if module is None:
                # If importing as an app module failed, that error probably
                # contains the most informative traceback. Trigger it again.
                import_module(entry)
            else:
                raise

        # Check for obvious errors. (This check prevents duck typing, but
        # it could be removed if it became a problem in practice.)
        if not issubclass(cls, AppConfig):
            raise ImproperlyConfigured(
                "'%s' isn't a subclass of AppConfig." % entry)

        # Obtain app name here rather than in AppClass.__init__ to keep
        # all error checking for entries in INSTALLED_APPS in one place.
        try:
            app_name = cls.name
        except AttributeError:
            raise ImproperlyConfigured(
                "'%s' must supply a name attribute." % entry)

        # Ensure app_name points to a valid module.
        try:
            app_module = import_module(app_name)
        except ImportError:
            raise ImproperlyConfigured(
                "Cannot import '%s'. Check that '%s.%s.name' is correct." % (
                    app_name, mod_path, cls_name,
                )
            )

        # 条目是一个应用程序配置类的路径。
        return cls(app_name, app_module)

    def get_model(self, model_name, require_ready=True):
        """
        用给定的不区分大小写的model_name返回model。
        如果没有此名称的model存在,请抛出LookupError。
        """
        if require_ready:
            self.apps.check_models_ready()
        else:
            self.apps.check_apps_ready()
        try:
            return self.models[model_name.lower()]
        except KeyError:
            raise LookupError(
                "App '%s' doesn't have a '%s' model." % (self.label, model_name))

    (此处略过部分不影响理解核心逻辑的代码)...

    def import_models(self):对持有此AppConfig的应用程序注册表的引用
        # 此应用程序的models字典,维护在该应用程序的的AppConfig附属的应用程序注册表(变量Apps)的'all_models'属性中。
        self.models = self.apps.all_models[self.label]

        if module_has_submodule(self.module, MODELS_MODULE_NAME):
            models_module_name = '%s.%s' % (self.name, MODELS_MODULE_NAME)
            self.models_module = import_module(models_module_name)

    def ready(self):
        """
        Override this method in subclasses to run code when Django starts.
        """

可以看到AppConfig是应用程序配置类,主要用来管理Django应用程序及其配置信息,所以每个应用都会有一个应用程序配置类。它核心的方法是一个工厂类方法,它接收INSTALLED_APPS中的条目来创建AppConfig实例,接收的条目可以是一个应用程序模块的路径,这时候创建的是默认的AppConfig实例;接收的条目也可以是一个应用程序配置类的路径,这时候创建的就是用户自己定义的AppConfig实例。

接着分析文件registry.py,其实我们也能猜到它就是注册表了,维护项目所有的应用程序配置信息。

# django/apps/registry.py

class Apps:
    """
    存储所有应用程序配置信息的注册表。它还跟踪models,例如 提供反向关系。
    """

    def __init__(self, installed_apps=()):
        # installed_apps is set to None when creating the master registry
        # because it cannot be populated at that point. Other registries must
        # provide a list of installed apps and are populated immediately.
        if installed_apps is None and hasattr(sys.modules[__name__], 'apps'):
            raise RuntimeError("You must supply an installed_apps argument.")

        # model的映射表,app labels => model names => model classes
        self.all_models = defaultdict(OrderedDict)

        # AppConfig实例的映射表
        self.app_configs = OrderedDict()

        # Stack of app_configs. Used to store the current state in
        # set_available_apps and set_installed_apps.
        self.stored_app_configs = []

        # Whether the registry is populated.
        self.apps_ready = self.models_ready = self.ready = False

        # Lock for thread-safe population.
        self._lock = threading.RLock()
        self.loading = False

        # Maps ("app_label", "modelname") tuples to lists of functions to be
        # called when the corresponding model is ready. Used by this class's
        # `lazy_model_operation()` and `do_pending_operations()` methods.
        self._pending_operations = defaultdict(list)

        # Populate apps and models, unless it's the master registry.
        if installed_apps is not None:
            self.populate(installed_apps)

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

        # populate() might be called by two threads in parallel on servers
        # that create threads before initializing the WSGI callable.
        with self._lock:
            if self.ready:
                return

            # An RLock prevents other threads from entering this section. The
            # compare and set operation below is atomic.
            if self.loading:
                # Prevent reentrant calls to avoid running AppConfig.ready()
                # methods twice.
                raise RuntimeError("populate() isn't reentrant")
            self.loading = True

            # 阶段1:遍历installed_apps,初始化每一条条目对应的应用程序配置并导入应用程序模块。
            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

            # 阶段2:导入models模块。
            for app_config in self.app_configs.values():
                # 这里调用的上面分析的AppConfig的import_models方法
                app_config.import_models()

            self.clear_cache()

            self.models_ready = True

            # 阶段3:运行每个应用程序配置的ready()方法。
            for app_config in self.get_app_configs():
                # 这里调用的上面分析的AppConfig的ready方法
                app_config.ready()

            self.ready = True

    (此处略过部分不影响理解核心逻辑的代码)...

apps = Apps(installed_apps=None)

最后的apps = Apps(installed_apps=None),就是__init__.py文件包含的apps变量,是个注册表Apps的实例。以及我们回去看django.setup,会发现那里是调用apps.populate(settings.INSTALLED_APPS)方法的地方,从这时候开始循环载入apps和相应的models。

总结

当Django由wsgi启动或者management命令启动时,会由django.setup()负责填充应用程序注册表。它通过以下方式配置Django:
1)加载配置文件,生成settings对象;2)设置日志;3)如果set_prefix为True,则将URL解析器脚本前缀设置为FORCE_SCRIPT_NAME(如果已定义),否则则为/;4)初始化应用程序注册表。

其中应用程序注册表分为三个阶段初始化。 在每个阶段,Django按照INSTALLED_APPS的顺序处理所有应用程序。1)首先会导入INSTALLED_APPS中所有应用程序(apps);2)尝试导入每个应用程序的models子模块(如果有的话);3)最后运行每个应用程序配置的ready()方法。

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 121,294评论 16 134
  • 切换到创建项目的目录 cd C:\Users\admin\Desktop\DjangoProject创建名为pr...
    在努力中阅读 2,641评论 2 3
  • 版权: https://github.com/haiiiiiyun/awesome-django-cn Aweso...
    若与阅读 21,259评论 3 238
  • 全文链接 第一章 创建一个blog应用第二章 使用高级特性来增强你的blog第三章 扩展你的blog应用第四章上 ...
    夜夜月阅读 6,330评论 25 27
  • 【读经】 撒下14章 【金句】 我们都是必死的,如同水泼在地上,不能收回。神并不夺取人的性命,乃设法使逃亡的人不致...
    chanor阅读 144评论 0 0