Django 源码(三)-应用 & 中间件 & 配置文件

Django 源码(三)-应用 & 中间件 & 配置文件

本部分主要是在为程序启动时候加载应用以及中间件的信息;

1. 应用的加载

在程序启动的部分,我们分析到程序执行的时候都会执行一个setup()函数,相关的内容可以看之前的章节的部分;

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  # apps 是一个已经实例化的对象, 是类 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
        )
        """
        from asgiref.local import Local
        简单解释:Local 是 threading.locals 的直接替代品,也适用于 asyncio 任务
        通过 current_task asyncio 方法,并通过 sync_to_async 和 async_to_sync传递局部变量。
        
        
        _prefixes = Local()  
        ...
        def set_script_prefix(prefix):

            if not prefix.endswith('/'):
                prefix += '/'
            _prefixes.value = prefix

        
        """
    # 本部分主要是用来加载相关的 app 下面进行逐步的解析。
    apps.populate(settings.INSTALLED_APPS)  

1.1 源码逐步分析

下面我们将逐步分析函数populate

class Apps:
    """
    A registry that stores the configuration of installed applications.

    It also keeps track of models, e.g. to provide reverse relations.
    """

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

        # Mapping of app labels => model names => model classes. Every time a
        # model is imported, ModelBase.__new__ calls apps.register_model which
        # creates an entry in all_models. All imported models are registered,
        # regardless of whether they're defined in an installed application
        # and whether the registry has been populated. Since it isn't possible
        # to reimport a module safely (it could reexecute initialization code)
        # all_models is never overridden or reset.
        self.all_models = defaultdict(dict)

        # Mapping of labels to AppConfig instances for installed apps.
        self.app_configs = {}  # 设置空字典

        # 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
        # For the autoreloader.
        self.ready_event = threading.Event()

        # 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):
        """
        Load application configurations and models.

        Import each application module and then each model module.

        It is thread-safe and idempotent, but not reentrant.
        """
        if self.ready:  # 默认是 False
            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

            # Phase 1: initialize app configs and import app modules.
            for entry in installed_apps:  # 循环传入的参数, settings.INSTALLED_APPS
                """
                传入的是配置文件中已经配置好的应用信息;
                INSTALLED_APPS = [
                    'django.contrib.admin',
                    'django.contrib.auth',
                    'django.contrib.contenttypes',
                    'django.contrib.sessions',
                    'django.contrib.messages',
                    'django.contrib.staticfiles',
                    'web.apps.WebConfig',
                ]
                
                """
                # 检查类型并赋值
                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)
				# 设置app的字典,键为对象的Label 值为 app_config
                self.app_configs[app_config.label] = app_config
                app_config.apps = self

            # Check for duplicate app names.
            # 统计每个app出现的次数;
            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  # 设置 app 加载完成

            # Phase 2: import models modules. 获取字典的值进行循环
            # 导入的是 app 下的 models.py 文件;
            for app_config in self.app_configs.values():
                app_config.import_models()  # 执行当前对象的 import_models()方法;方法在AppConfig类中

            self.clear_cache()

            self.models_ready = True  # 设置模型加载完成

            # Phase 3: run ready() methods of app configs.
            # AppConfig 类中的ready() 方法, 类中这个方法是没有内容的相当于预留的钩子, 可以在 apps.py 的类中进行扩展
            for app_config in self.get_app_configs():
                app_config.ready()

            self.ready = True  # 初始化完成
            self.ready_event.set()  # 使用线程池的 Event 事件进行释放;

被调用的 create 函数

class AppConfig:
    """Class representing a Django application and its configuration."""

    @classmethod
    def create(cls, entry):
        """
        Factory that creates an app config from an entry in INSTALLED_APPS.
        """
        # create() eventually returns app_config_class(app_name, app_module).
        app_config_class = None
        app_config_name = None
        app_name = None
        app_module = None

        # If import_module succeeds, entry points to the app module.
        try:
            app_module = import_module(entry)  #按照配置文件中的路径进行导入;后面以我们自己创建的app为例;
        except Exception:
            pass
        else:
            # If app_module has an apps submodule that defines a single
            # AppConfig subclass, use it automatically.
            # To prevent this, an AppConfig subclass can declare a class
            # variable default = False.
            # If the apps module defines more than one AppConfig subclass,
            # the default one can declare default = True.
            if module_has_submodule(app_module, APPS_MODULE_NAME):  # 检查模块是否存在于包中
                # 默认 APPS_MODULE_NAME 是 apps;因此默认创建的文件下面有个文件是 apps.py
                mod_path = '%s.%s' % (entry, APPS_MODULE_NAME) # web.apps.WebConfig.apps.py
                mod = import_module(mod_path)
                # Check if there's exactly one AppConfig candidate,
                # excluding those that explicitly define default = False.4
                # 使用列表生成式获取模块 mod(apps.py) 的属性和名称并且做过滤;
                app_configs = [
                    (name, candidate)
                    for name, candidate in inspect.getmembers(mod, inspect.isclass)
                    if (
                        issubclass(candidate, cls) and
                        candidate is not cls and
                        getattr(candidate, 'default', True)
                    )
                ]
                if len(app_configs) == 1:
                    app_config_class = app_configs[0][1]
                    app_config_name = '%s.%s' % (mod_path, app_configs[0][0])
                else:
                    # Check if there's exactly one AppConfig subclass,
                    # among those that explicitly define default = True.
                    app_configs = [
                        (name, candidate)
                        for name, candidate in app_configs
                        if getattr(candidate, 'default', False)
                    ] 
                    if len(app_configs) > 1:
                        candidates = [repr(name) for name, _ in app_configs]
                        raise RuntimeError(
                            '%r declares more than one default AppConfig: '
                            '%s.' % (mod_path, ', '.join(candidates))
                        )
                    elif len(app_configs) == 1:
                        app_config_class = app_configs[0][1]
                        app_config_name = '%s.%s' % (mod_path, app_configs[0][0])

            # If app_module specifies a default_app_config, follow the link.
            # default_app_config is deprecated, but still takes over the
            # automatic detection for backwards compatibility during the
            # deprecation period.
            try:
                new_entry = app_module.default_app_config
            except AttributeError:
                # Use the default app config class if we didn't find anything.
                if app_config_class is None:
                    app_config_class = cls
                    app_name = entry
            else:
                message = (
                    '%r defines default_app_config = %r. ' % (entry, new_entry)
                )
                if new_entry == app_config_name:
                    message += (
                        'Django now detects this configuration automatically. '
                        'You can remove default_app_config.'
                    )
                else:
                    message += (
                        "However, Django's automatic detection %s. You should "
                        "move the default config class to the apps submodule "
                        "of your application and, if this module defines "
                        "several config classes, mark the default one with "
                        "default = True." % (
                            "picked another configuration, %r" % app_config_name
                            if app_config_name
                            else "did not find this configuration"
                        )
                    )
                warnings.warn(message, RemovedInDjango41Warning, stacklevel=2)
                entry = new_entry
                app_config_class = None

        # If import_string succeeds, entry is an app config class.
        if app_config_class is None:
            try:
                app_config_class = import_string(entry)
            except Exception:
                pass
        # If both import_module and import_string failed, it means that entry
        # doesn't have a valid value.
        if app_module is None and app_config_class is None:
            # If the last component of entry starts with an uppercase letter,
            # then it was likely intended to be an app config class; if not,
            # an app module. Provide a nice error message in both cases.
            mod_path, _, cls_name = entry.rpartition('.')
            if mod_path and cls_name[0].isupper():
                # We could simply re-trigger the string import exception, but
                # we're going the extra mile and providing a better error
                # message for typos in INSTALLED_APPS.
                # This may raise ImportError, which is the best exception
                # possible if the module at mod_path cannot be imported.
                mod = import_module(mod_path)
                candidates = [
                    repr(name)
                    for name, candidate in inspect.getmembers(mod, inspect.isclass)
                    if issubclass(candidate, cls) and candidate is not cls
                ]
                msg = "Module '%s' does not contain a '%s' class." % (mod_path, cls_name)
                if candidates:
                    msg += ' Choices are: %s.' % ', '.join(candidates)
                raise ImportError(msg)
            else:
                # Re-trigger the module import exception.
                import_module(entry)

        # Check for obvious errors. (This check prevents duck typing, but
        # it could be removed if it became a problem in practice.)
        if not issubclass(app_config_class, 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.
        if app_name is None:
            try:
                app_name = app_config_class.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,
                    app_config_class.__module__,
                    app_config_class.__qualname__,
                )
            )

        # Entry is a path to an app config class.
        # 
        return app_config_class(app_name, app_module)  # AppConfig 类
    
    # 
    def import_models(self):
        # Dictionary of models for this app, primarily maintained in the
        # 'all_models' attribute of the Apps this AppConfig is attached to.
        self.models = self.apps.all_models[self.label]
		# MODELS_MODULE_NAME = 'models'默认常量
        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)  # 导入 models.py 的文件;
            

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

app_config_class 为每个子 app 那个 Config 类(在子app目录下apps.py中的那个Config类,继承自AppConfig),最终实质是调用AppConfig 的 init 方法返回实例对象

调用的函数如下

def module_has_submodule(package, module_name):
    """See if 'module' is in 'package'."""
    try:
        package_name = package.__name__
        package_path = package.__path__
    except AttributeError:
        # package isn't a package.
        return False

    full_module_name = package_name + '.' + module_name
    try:
        # 检查包是否存在;
        return importlib_find(full_module_name, package_path) is not None
    except (ModuleNotFoundError, AttributeError):
        # When module_name is an invalid dotted path, Python raises
        # ModuleNotFoundError. AttributeError is raised on PY36 (fixed in PY37)
        # if the penultimate part of the path is not a package.
        return False

1.2 钩子函数

我们上述分析源码的时候发现了一个钩子函数ready(),通常是在每个 apps.py 的文件下面的,而且该函数是在 Django 启动的时候被调用的,因此可以用来设置一些全局变量等信息

参考文章:https://deepinout.com/django/django-questions/112_django_appconfigready_is_running_twice_on_django_setup_using_heroku.html

参考文章:https://geek-docs.com/django/django-questions/38_django_redefinition_of_appconfigready.html

from django.apps import AppConfig

class MyappConfig(AppConfig):
    name = 'myapp'

    def ready(self):
        if not self.ready_executed:
            self.ready_executed = True
            # 执行初始化操作

在上面的示例中,我们添加了一个名为 ready_executed 的布尔类型属性。当 ready_executed 为 False 时,我们执行初始化操作,并将ready_executed 设置为 True。这样,当 Django 第二次调用 AppConfig.ready() 时,由于 ready_executed 已经为 True,我们将跳过初始化操作。

2. 中间件的加载

参考文章:https://blog.csdn.net/yuencczzy/article/details/129107042

中间件的加载之前已经接触到很多,本次在进行一次相关的分析。

2.1 源码的分析

依旧进入到我们非常熟悉的 WSGIHandler

class WSGIHandler(base.BaseHandler):
    request_class = WSGIRequest

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.load_middleware()

    def __call__(self, environ, start_response):
        set_script_prefix(get_script_name(environ))
        signals.request_started.send(sender=self.__class__, environ=environ)
        request = self.request_class(environ)
        response = self.get_response(request)

        response._handler_class = self.__class__

        status = '%d %s' % (response.status_code, response.reason_phrase)
        response_headers = [
            *response.items(),
            *(('Set-Cookie', c.output(header='')) for c in response.cookies.values()),
        ]
        start_response(status, response_headers)
        if getattr(response, 'file_to_stream', None) is not None and environ.get('wsgi.file_wrapper'):
            # If `wsgi.file_wrapper` is used the WSGI server does not call
            # .close on the response, but on the file wrapper. Patch it to use
            # response.close instead which takes care of closing all files.
            response.file_to_stream.close = response.close
            response = environ['wsgi.file_wrapper'](response.file_to_stream, response.block_size)
        return response

请求到来的时候执行的是__call__方法,触发的条件对象加括号执行,因此执行之前肯定已经存在了 WSGIHandler 的对象,在对象进行初始化的时候会执行__init__方法,而初始化方法中除了执行父类的初始化方法之外,还执行了load_middleware方法;

方法的代码如下:

def load_middleware(self, is_async=False):
    """
    Populate middleware lists from settings.MIDDLEWARE.

    Must be called after the environment is fixed (see __call__ in subclasses).
    """
    self._view_middleware = []
    self._template_response_middleware = []
    self._exception_middleware = []
	
    # 此处不在进行逐句的分析
    get_response = self._get_response_async if is_async else self._get_response
    handler = convert_exception_to_response(get_response)
    handler_is_async = is_async
    
    # 中间件相关的部分
    """
    MIDDLEWARE = [
        'django.middleware.security.SecurityMiddleware',
        'django.contrib.sessions.middleware.SessionMiddleware',
        'django.middleware.common.CommonMiddleware',
        'django.middleware.csrf.CsrfViewMiddleware',
        'django.contrib.auth.middleware.AuthenticationMiddleware',
        'django.contrib.messages.middleware.MessageMiddleware',
        'django.middleware.clickjacking.XFrameOptionsMiddleware',
    ]
    
    """ 
    # 备注: 执行了一下reversed 函数, 因此导入中间件的顺序是从后向前的顺序;
    for middleware_path in reversed(settings.MIDDLEWARE):  # 相关的列表如上所示
        middleware = import_string(middleware_path)  # 按照字符串进行导包
        # 检查导包的信息中是否存在异步的操作, 继承的父类中的两个关键字都是 True
        middleware_can_sync = getattr(middleware, 'sync_capable', True)
        middleware_can_async = getattr(middleware, 'async_capable', False)
        if not middleware_can_sync and not middleware_can_async:
            raise RuntimeError(
                'Middleware %s must have at least one of '
                'sync_capable/async_capable set to True.' % middleware_path
            )
        elif not handler_is_async and middleware_can_sync:
            middleware_is_async = False
        else:
            middleware_is_async = middleware_can_async
        
        # 设置关键字为 bool 的类型;
        try:
            # Adapt handler, if needed.
            # 返回函数的是否异步
            adapted_handler = self.adapt_method_mode(
                middleware_is_async, handler, handler_is_async,
                debug=settings.DEBUG, name='middleware %s' % middleware_path,
            )
            mw_instance = middleware(adapted_handler)  # 中间件类的实例化
        except MiddlewareNotUsed as exc:
            if settings.DEBUG:
                if str(exc):
                    logger.debug('MiddlewareNotUsed(%r): %s', middleware_path, exc)
                else:
                    logger.debug('MiddlewareNotUsed: %r', middleware_path)
            continue
        else:
            handler = adapted_handler

        if mw_instance is None:
            raise ImproperlyConfigured(
                'Middleware factory %s returned None.' % middleware_path
            )
		# 以下三个函数都是再自定义的中间件中可有可无函数;
        if hasattr(mw_instance, 'process_view'):  # 检查是否存在视图中间件函数
            self._view_middleware.insert(
                0,
                self.adapt_method_mode(is_async, mw_instance.process_view),
            )
        if hasattr(mw_instance, 'process_template_response'):  # 模板中间件响应结果;
            self._template_response_middleware.append(
                self.adapt_method_mode(is_async, mw_instance.process_template_response),
            )
        if hasattr(mw_instance, 'process_exception'):  # 异常响应结果
            # The exception-handling stack is still always synchronous for
            # now, so adapt that way.
            self._exception_middleware.append(
                self.adapt_method_mode(False, mw_instance.process_exception),
            )

        handler = convert_exception_to_response(mw_instance)
        handler_is_async = middleware_is_async

    # Adapt the top of the stack, if needed.
    handler = self.adapt_method_mode(is_async, handler, handler_is_async)
    # We only assign to this when initialization is complete as it is used
    # as a flag for initialization being complete.
    self._middleware_chain = handler

2.2 中间件的扩展

中间的扩展在使用的时候通常会继承类 MiddlewareMixin 重写当中的方法;

class MiddlewareMixin:
    sync_capable = True
    async_capable = True

    # RemovedInDjango40Warning: when the deprecation ends, replace with:
    #   def __init__(self, get_response):
    def __init__(self, get_response=None):
        self._get_response_none_deprecation(get_response)
        self.get_response = get_response
        self._async_check()
        super().__init__()

    def _async_check(self):
        """
        If get_response is a coroutine function, turns us into async mode so
        a thread is not consumed during a whole request.
        """
        if asyncio.iscoroutinefunction(self.get_response):
            # Mark the class as async-capable, but do the actual switch
            # inside __call__ to avoid swapping out dunder methods
            self._is_coroutine = asyncio.coroutines._is_coroutine

    def __call__(self, request):
        # Exit out to async mode, if needed
        if asyncio.iscoroutinefunction(self.get_response):
            return self.__acall__(request)
        response = None
        # 此处可以看到缺少的 process_request 和 process_response 在对象的进行调用的时候就已经执行了
        # 其余三个方法实在 load_midleware方法中实现的调用;
        if hasattr(self, 'process_request'):
            response = self.process_request(request)
        response = response or self.get_response(request)
        if hasattr(self, 'process_response'):
            response = self.process_response(request, response)
        return response

    async def __acall__(self, request):
        """
        Async version of __call__ that is swapped in when an async request
        is running.
        """
        response = None
        if hasattr(self, 'process_request'):
            response = await sync_to_async(
                self.process_request,
                thread_sensitive=True,
            )(request)
        response = response or await self.get_response(request)
        if hasattr(self, 'process_response'):
            response = await sync_to_async(
                self.process_response,
                thread_sensitive=True,
            )(request, response)
        return response

    def _get_response_none_deprecation(self, get_response):
        if get_response is None:
            warnings.warn(
                'Passing None for the middleware get_response argument is '
                'deprecated.',
                RemovedInDjango40Warning, stacklevel=3,
            )

因此中间件的调用顺序是有父类中的__call__方法和 WSGIHandler 中的load_middleware方法进行的控制;

3. 配置文件的懒加载

当我们在 django 的项目中使用配置文件的时候

from django.conf import settings

我们在项目中使用的时候直接使用的是conf中的配置,而不是直接使用form pro import settings

接下来我们就研究一下 django 配置文件的加载机制;

""" 导入的配置文件类如下所示;
"""

class LazySettings(LazyObject):
    """
    A lazy proxy for either global Django settings or a custom settings object.
    The user can manually configure settings prior to using them. Otherwise,
    Django uses the settings module pointed to by DJANGO_SETTINGS_MODULE.
    """
    def _setup(self, name=None):
        """
        Load the settings module pointed to by the environment variable. This
        is used the first time settings are needed, if the user hasn't
        configured settings manually.
        """
        # 在 manage 的时候写入到环境变量的路径字典;
        settings_module = os.environ.get(ENVIRONMENT_VARIABLE)
        # 从系统的环境变量中获取失败之后, 主动抛出异常;
        if not settings_module:
            desc = ("setting %s" % name) if name else "settings"
            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))
		
        # 实例化的私有配置文件的对象, 将路径作为初始化的参数传入;
        self._wrapped = Settings(settings_module)

    def __repr__(self):
        # Hardcode the class name as otherwise it yields 'Settings'.
        if self._wrapped is empty:
            return '<LazySettings [Unevaluated]>'
        return '<LazySettings "%(settings_module)s">' % {
            'settings_module': self._wrapped.SETTINGS_MODULE,
        }

    def __getattr__(self, name):
        # 取值的时候会执行该方法;
        """Return the value of a setting and cache it in self.__dict__."""
        if self._wrapped is empty:
            self._setup(name)  # 执行对象的 setup 方法;
        val = getattr(self._wrapped, name)

        # Special case some settings which require further modification.
        # This is done here for performance reasons so the modified value is cached.
        if name in {'MEDIA_URL', 'STATIC_URL'} and val is not None:
            val = self._add_script_prefix(val)  # 设置前缀;
        elif name == 'SECRET_KEY' and not val:
            raise ImproperlyConfigured("The SECRET_KEY setting must not be empty.")

        self.__dict__[name] = val  # 设置相关的值
        return val  # 进行返回;

    def __setattr__(self, name, value):
        """
        Set the value of setting. Clear all cached values if _wrapped changes
        (@override_settings does this) or clear single values when set.
        """
        if name == '_wrapped':
            self.__dict__.clear()
        else:
            self.__dict__.pop(name, None)
        super().__setattr__(name, value)

    def __delattr__(self, name):
        """Delete a setting and clear it from cache if needed."""
        super().__delattr__(name)
        self.__dict__.pop(name, None)

    def configure(self, default_settings=global_settings, **options):
        """
        Called to manually configure the settings. The 'default_settings'
        parameter sets where to retrieve any unspecified values from (its
        argument must support attribute access (__getattr__)).
        """
        if self._wrapped is not empty:
            raise RuntimeError('Settings already configured.')
        holder = UserSettingsHolder(default_settings)
        for name, value in options.items():
            if not name.isupper():
                raise TypeError('Setting %r must be uppercase.' % name)
            setattr(holder, name, value)
        self._wrapped = holder

    @staticmethod
    def _add_script_prefix(value):
        """
        Add SCRIPT_NAME prefix to relative paths.

        Useful when the app is being served at a subpath and manually prefixing
        subpath to STATIC_URL and MEDIA_URL in settings is inconvenient.
        """
        # Don't apply prefix to absolute paths and URLs.
        if value.startswith(('http://', 'https://', '/')):
            return value
        from django.urls import get_script_prefix
        return '%s%s' % (get_script_prefix(), value)

    @property
    def configured(self):
        """Return True if the settings have already been configured."""
        return self._wrapped is not empty

    @property
    def PASSWORD_RESET_TIMEOUT_DAYS(self):
        stack = traceback.extract_stack()
        # Show a warning if the setting is used outside of Django.
        # Stack index: -1 this line, -2 the caller.
        filename, _, _, _ = stack[-2]
        if not filename.startswith(os.path.dirname(django.__file__)):
            warnings.warn(
                PASSWORD_RESET_TIMEOUT_DAYS_DEPRECATED_MSG,
                RemovedInDjango40Warning,
                stacklevel=2,
            )
        return self.__getattr__('PASSWORD_RESET_TIMEOUT_DAYS')

    
settings = LazySettings()

通过注释可以理解配置文件的加载机制是懒加载,当使用的时候才进行加载;我们使用的时候直接是通过 settings 进行的取值,但是取值我们可以发现类中没有相关的实例变量,因此会执行到魔法方法__getattr__

_setup中引用的类代码如下;

from django.conf import global_settings  # 包中的信息是 django 的默认配置;

class Settings:
    def __init__(self, settings_module):
        # update this dict from global settings (but only for ALL_CAPS settings)
        for setting in dir(global_settings):  # 返回模块中的信息,获取函数、类、变量
            if setting.isupper():  # 将每一个的变量全部变换成为大写的字母信息
                # 使用反射,为当前对象, 设置变量
                setattr(self, setting, getattr(global_settings, setting))

        # store the settings module in case someone later cares
        self.SETTINGS_MODULE = settings_module  # 设置当前的路径;
		
        # 使用 import_module 导入配置文件;
        mod = importlib.import_module(self.SETTINGS_MODULE)
		
        # 初始化元组的配置
        tuple_settings = (
            "INSTALLED_APPS",
            "TEMPLATE_DIRS",
            "LOCALE_PATHS",
        )
        
        # 设置空的集合
        self._explicit_settings = set()
        for setting in dir(mod):  # 将django 中的配置的变量进行循环
            if setting.isupper():
                # 使用反射获取 import_module 导入信息的配置;
                setting_value = getattr(mod, setting)

                if (setting in tuple_settings and
                        not isinstance(setting_value, (list, tuple))):
                    raise ImproperlyConfigured("The %s setting must be a list or a tuple. " % setting)
                setattr(self, setting, setting_value)
                self._explicit_settings.add(setting)  # 添加到集合的信息

        if self.is_overridden('PASSWORD_RESET_TIMEOUT_DAYS'):  # 检查配置的信息是否在集合中
            if self.is_overridden('PASSWORD_RESET_TIMEOUT'):  
                raise ImproperlyConfigured(
                    'PASSWORD_RESET_TIMEOUT_DAYS/PASSWORD_RESET_TIMEOUT are '
                    'mutually exclusive.'
                )
            setattr(self, 'PASSWORD_RESET_TIMEOUT', self.PASSWORD_RESET_TIMEOUT_DAYS * 60 * 60 * 24)
            warnings.warn(PASSWORD_RESET_TIMEOUT_DAYS_DEPRECATED_MSG, RemovedInDjango40Warning)

        if self.is_overridden('DEFAULT_HASHING_ALGORITHM'):
            warnings.warn(DEFAULT_HASHING_ALGORITHM_DEPRECATED_MSG, RemovedInDjango40Warning)

        if hasattr(time, 'tzset') and self.TIME_ZONE:
            # When we can, attempt to validate the timezone. If we can't find
            # this file, no check happens and it's harmless.
            zoneinfo_root = Path('/usr/share/zoneinfo')
            zone_info_file = zoneinfo_root.joinpath(*self.TIME_ZONE.split('/'))
            if zoneinfo_root.exists() and not zone_info_file.exists():
                raise ValueError("Incorrect timezone setting: %s" % self.TIME_ZONE)
            # Move the time zone info into os.environ. See ticket #2315 for why
            # we don't do this unconditionally (breaks Windows).
            os.environ['TZ'] = self.TIME_ZONE
            time.tzset()

    def is_overridden(self, setting):
        return setting in self._explicit_settings

    def __repr__(self):
        return '<%(cls)s "%(settings_module)s">' % {
            'cls': self.__class__.__name__,
            'settings_module': self.SETTINGS_MODULE,
        }

image-20231214145810123

上述的类继承的类如下:

empty = object()

class LazyObject:
    """
    A wrapper for another class that can be used to delay instantiation of the
    wrapped class.

    By subclassing, you have the opportunity to intercept and alter the
    instantiation. If you don't need to do that, use SimpleLazyObject.
    """

    # Avoid infinite recursion when tracing __init__ (#19456).
    _wrapped = None

    def __init__(self):
        # Note: if a subclass overrides __init__(), it will likely need to
        # override __copy__() and __deepcopy__() as well.
        self._wrapped = empty  # 空对象;

    __getattr__ = new_method_proxy(getattr)

    def __setattr__(self, name, value):
        if name == "_wrapped":
            # Assign to __dict__ to avoid infinite __setattr__ loops.
            self.__dict__["_wrapped"] = value
        else:
            if self._wrapped is empty:
                self._setup()
            setattr(self._wrapped, name, value)

    def __delattr__(self, name):
        if name == "_wrapped":
            raise TypeError("can't delete _wrapped.")
        if self._wrapped is empty:
            self._setup()
        delattr(self._wrapped, name)

    def _setup(self):
        """
        Must be implemented by subclasses to initialize the wrapped object.
        """
        # 子类必须要实现该方法;
        raise NotImplementedError('subclasses of LazyObject must provide a _setup() method')

    # Because we have messed with __class__ below, we confuse pickle as to what
    # class we are pickling. We're going to have to initialize the wrapped
    # object to successfully pickle it, so we might as well just pickle the
    # wrapped object since they're supposed to act the same way.
    #
    # Unfortunately, if we try to simply act like the wrapped object, the ruse
    # will break down when pickle gets our id(). Thus we end up with pickle
    # thinking, in effect, that we are a distinct object from the wrapped
    # object, but with the same __dict__. This can cause problems (see #25389).
    #
    # So instead, we define our own __reduce__ method and custom unpickler. We
    # pickle the wrapped object as the unpickler's argument, so that pickle
    # will pickle it normally, and then the unpickler simply returns its
    # argument.
    def __reduce__(self):
        if self._wrapped is empty:
            self._setup()
        return (unpickle_lazyobject, (self._wrapped,))

    def __copy__(self):
        if self._wrapped is empty:
            # If uninitialized, copy the wrapper. Use type(self), not
            # self.__class__, because the latter is proxied.
            return type(self)()
        else:
            # If initialized, return a copy of the wrapped object.
            return copy.copy(self._wrapped)

    def __deepcopy__(self, memo):
        if self._wrapped is empty:
            # We have to use type(self), not self.__class__, because the
            # latter is proxied.
            result = type(self)()
            memo[id(self)] = result
            return result
        return copy.deepcopy(self._wrapped, memo)

    __bytes__ = new_method_proxy(bytes)
    __str__ = new_method_proxy(str)
    __bool__ = new_method_proxy(bool)

    # Introspection support
    __dir__ = new_method_proxy(dir)

    # Need to pretend to be the wrapped class, for the sake of objects that
    # care about this (especially in equality tests)
    __class__ = property(new_method_proxy(operator.attrgetter("__class__")))
    __eq__ = new_method_proxy(operator.eq)
    __lt__ = new_method_proxy(operator.lt)
    __gt__ = new_method_proxy(operator.gt)
    __ne__ = new_method_proxy(operator.ne)
    __hash__ = new_method_proxy(hash)

    # List/Tuple/Dictionary methods support
    __getitem__ = new_method_proxy(operator.getitem)
    __setitem__ = new_method_proxy(operator.setitem)
    __delitem__ = new_method_proxy(operator.delitem)
    __iter__ = new_method_proxy(iter)
    __len__ = new_method_proxy(len)
    __contains__ = new_method_proxy(operator.contains)

综上,我们在读取 settings 某个配置时,会触发 __getattr__ 方法,如果 _wrapped 为空,则调用 _setup 方法,这个方法内部获取配置文件模块,_wrapped 属性指向 Settings 类的实例,这个类在实例化的时候,构造函数先读取 global_settings 来设置一些默认属性,接着通过动态导入模块的形式 importlib.import_module 加载配置模块的属性,继而读取的属性从 _wrapped 中获取。

继续努力,终成大器!

posted @ 2024-01-14 22:51  紫青宝剑  阅读(53)  评论(0编辑  收藏  举报