Django源碼分析系列之manage運行加載settings配置

感悟: 對Django源碼進行分析主要來自兩個方面的原因:
1、想熟悉django整個啓動流程包括request請求到最後response響應以及中間環節的整個流程;
2、源於對一款開源的項目管理工具taiga,該工具採用前後端分離的架構,前端採用Angular Js(版本1.x),後端採用django rest framework框架。在後端代碼的理解中看到了對比之前創建MTV架構的django項目更加方便、高效、合理的理念。因此想基於此項目對django框架有個基本的認識。
簡述:
Django源碼分析系列文章將會對從runserver啓動,到加載用戶配置文件、wsgi協議、url、router、中間件以及符合django rest framework框架的用戶認證、權限管理、序列化、節流等衆多功能模塊進行詳細的分析。本篇文章會暫對啓動流程中加載用戶配置文件以及安裝app進行分析。
一、 manager.py main函數
django項目是從python manager.py runserver ip+port啓動的,那麼我們就深入源碼理解啓動邏輯。

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


def main():
    """
    django服務入口函數:
        1、設置項目配置文件
        2、執行runserver命令
    """
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'taiga_master.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
    """
    1、sys.argv:獲取當前文件外部執行傳遞的參數列表
        argv = ["...manager.py","runserver","ip:port"]
    2、執行命令行參數
    """
    execute_from_command_line(sys.argv)


if __name__ == '__main__':
    main()

在manage.py的main函數主要進行兩個操作:
	1、第一個是將用戶的項目設置路徑,放入項目環境變量;
	2、執行命令行參數(runserver ip+port)

二、execute_from_command_line(sys.argv) 執行命令行

def execute_from_command_line(argv=None):
    """Run a ManagementUtility."""
    "管理模塊參數賦值"
    utility = ManagementUtility(argv)
    "管理模塊執行"
    utility.execute()

三、** utility.execute()** 函數

    def execute(self):
        """
        Given the command-line arguments, figure out which subcommand is being
        run, create a parser appropriate to that command, and run it.
        """
        try:
            subcommand = self.argv[1]
        except IndexError:
            subcommand = 'help'  # Display help if no arguments were given.

        # Preprocess options to extract --settings and --pythonpath.
        # These options could affect the commands that are available, so they
        # must be processed early.
        parser = CommandParser(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)
        except CommandError:
            pass  # Ignore any option errors at this point.

        try:
            "獲取settings裏面的app列表,但是還未加載app"
            settings.INSTALLED_APPS
        except ImproperlyConfigured as exc:
            self.settings_exception = exc
        except ImportError as exc:
            self.settings_exception = exc

        if settings.configured:
            # Start the auto-reloading dev server even if the code is broken.
            # The hardcoded condition is a code smell but we can't rely on a
            # flag on the command class because we haven't located it yet.
            if subcommand == 'runserver' and '--noreload' not in self.argv:
                try:
                    "在django.setup裏面真正加載並檢測app格式"
                    autoreload.check_errors(django.setup)()
                except Exception:
                    # The exception will be raised later in the child process
                    # started by the autoreloader. Pretend it didn't happen by
                    # loading an empty list of applications.
                    apps.all_models = defaultdict(OrderedDict)
                    apps.app_configs = OrderedDict()
                    apps.apps_ready = apps.models_ready = apps.ready = True

                    # Remove options not compatible with the built-in runserver
                    # (e.g. options for the contrib.staticfiles' runserver).
                    # Changes here require manually testing as described in
                    # #27522.
                    _parser = self.fetch_command('runserver').create_parser('django', 'runserver')
                    _options, _args = _parser.parse_known_args(self.argv[2:])
                    for _arg in _args:
                        self.argv.remove(_arg)

            # In all other cases, django.setup() is required to succeed.
            else:
                django.setup()

        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])
        # Special-cases: We want 'django-admin --version' and
        # 'django-admin --help' to work, for backwards compatibility.
        elif subcommand == 'version' or self.argv[1:] == ['--version']:
            sys.stdout.write(django.get_version() + '\n')
        elif self.argv[1:] in (['--help'], ['-h']):
            sys.stdout.write(self.main_help_text() + '\n')
        else:
            self.fetch_command(subcommand).run_from_argv(self.argv)

該函數代碼非常重要,主要完成配置文件加載以及app加載等功能。
1、settings.INSTALLED_APPS 這裏涉及的知識點比較多,類似懶加載等
2、autoreload.check_errors(django.setup)()裏面的django.setup

三、settings.INSTALLED_APPS 導入用戶配置

# 懶加載
settings = LazySettings()
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.
        """
        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)
        val = getattr(self._wrapped, name)
        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():
            setattr(holder, name, value)
        self._wrapped = holder

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

    @property
    def DEFAULT_CONTENT_TYPE(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, _line_number, _function_name, _text = stack[-2]
        if not filename.startswith(os.path.dirname(django.__file__)):
            warnings.warn(
                DEFAULT_CONTENT_TYPE_DEPRECATED_MSG,
                RemovedInDjango30Warning,
                stacklevel=2,
            )
        return self.__getattr__('DEFAULT_CONTENT_TYPE')

    @property
    def FILE_CHARSET(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, _line_number, _function_name, _text = stack[-2]
        if not filename.startswith(os.path.dirname(django.__file__)):
            warnings.warn(
                FILE_CHARSET_DEPRECATED_MSG,
                RemovedInDjango31Warning,
                stacklevel=2,
            )
        return self.__getattr__('FILE_CHARSET')

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)


class Settings:
    def __init__(self, settings_module):
        # update this dict from global settings (but only for ALL_CAPS settings)
        "加載系統的setting配置文件"
        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

        mod = importlib.import_module(self.SETTINGS_MODULE)

        tuple_settings = (
            "INSTALLED_APPS",
            "TEMPLATE_DIRS",
            "LOCALE_PATHS",
        )
        self._explicit_settings = set()
        "加載用戶setting配置文件"
        for setting in dir(mod):
            if setting.isupper():
                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 not self.SECRET_KEY:
            raise ImproperlyConfigured("The SECRET_KEY setting must not be empty.")

        if self.is_overridden('DEFAULT_CONTENT_TYPE'):
            warnings.warn(DEFAULT_CONTENT_TYPE_DEPRECATED_MSG, RemovedInDjango30Warning)
        if self.is_overridden('FILE_CHARSET'):
            warnings.warn(FILE_CHARSET_DEPRECATED_MSG, RemovedInDjango31Warning)

        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,
        }

代碼解讀:settings.INSTALLED_APPS
1、settings是實例化的一個對象,然後進入LazySettings類的__init__函數,發現該類沒有__init__函數,則找其父類LazyObject,執行其裏面的__init__函數,該函數內部發現self.wrapped = empty爲空。到此完成實例化操作;
2、那麼settings實例化操作後.INSTALLED_APPS,查找該類的INSTALLED_APPS的屬性。到這裏我們闡述一下python class裏面的魔法方法。python類的魔法方法包含幾個類別:比如構造初始化(init, new),屬性訪問控制(getattr, setattr,deleteattr,getattribute,),描述器對象(get, set,delete)以及構造自定義的容器,在這裏我們僅僅闡述我們用到的屬性訪問控制裏面的__getattr
_, setattr
3、因爲該類沒有INSTALLED_APPS屬性因此會進入__getattr__方法,在裏面判斷self._wrapped爲空,進入self._setup(name)進行設置;
4、setup函數裏面獲取用戶setting路徑,然後執行self._wrapped = Settings(settings_module)
5、在Settings裏面首先導入系統內部的配置,然後導入用戶配置,重複的配置覆蓋。到此用戶的配置導入完畢,但是還未加載app以及對app進行檢測。

getattr 該方法定義了你試圖訪問一個不存在的屬性時的行爲。因此,重載該方法可以實現捕獲錯誤拼寫然後進行重定向, 或者對一些廢棄的屬性進行警告。
setattr 是實現封裝的解決方案,它定義了你對屬性進行賦值和修改操作時的行爲。
不管對象的某個屬性是否存在,它都允許你爲該屬性進行賦值,因此你可以爲屬性的值進行自定義操作。

四、 autoreload.check_errors(django.setup)() django初始化

from django.utils.version import get_version

VERSION = (2, 2, 11, 'final', 0)

__version__ = get_version(VERSION)


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

    configure_logging(settings.LOGGING_CONFIG, settings.LOGGING)
    if set_prefix:
        set_script_prefix(
            '/' if settings.FORCE_SCRIPT_NAME is None else settings.FORCE_SCRIPT_NAME
        )
    "加載INSTALLED_APPS內app"
    apps.populate(settings.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:
        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:
            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

        # Check for duplicate app names.
        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

        # Phase 2: import models modules.
        for app_config in self.app_configs.values():
            app_config.import_models()

        self.clear_cache()

        self.models_ready = True

        # Phase 3: run ready() methods of app configs.
        for app_config in self.get_app_configs():
            app_config.ready()

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

該函數在for循環中AppConfig.create(entry)加載app,並更改加載狀態。
到此完成用戶配置文件setting的導入以及系統app和用戶app的加載。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章