django---加载INSTALLED_APPS的源码分析

Posted 进击的小猿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了django---加载INSTALLED_APPS的源码分析相关的知识,希望对你有一定的参考价值。

运行django项目,我们除了可以通过django图形界面启动,我们也可以通过命令行方式启动,启动方式如下:

python manage.py runserver

当我们创建django项目时候,会生成如下目录

mysite/
├── manage.py  # 管理文件
└── mysite  # 项目目录
    ├── __init__.py
    ├── settings.py  # 配置
    ├── urls.py  # 路由 --> URL和函数的对应关系
    └── wsgi.py  # runserver命令就使用wsgiref模块做简单的web server

我们来看下python manage.py runserver中的manage.py做了什么操作,可以用来启动django项目

import os
import sys

if __name__ == "__main__":
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "s8day88.settings")
    try:
        from django.core.management import execute_from_command_line
    except ImportError:
        try:
            import django
        except ImportError:
            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?"
            )
        raise
    execute_from_command_line(sys.argv)

上述代码中setdefault是不存在添加一个键值对,存在则返回该键对应的值
然后执行try内部代码from django.core.management import execute_from_command_line
导入management模块,最后执行该模块下的如下方法

execute_from_command_line(sys.argv)

我们来看下上述一行代码中的参数sys.argv是什么东东?经过测试输出如下的结果

['E:/python/django_pro/manage.py', 'runserver', '8000']

参数意思就是manage.py的绝对路径,命令,端口
接下来我们看下方法execute_from_command_line(sys.argv)方法吧

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

上面代码实例了一个类ManagementUtility对象,然后调用了一个对象方法execute
在构造对象时,在__init__方法中初始化如下代码

    def __init__(self, argv=None):
        self.argv = argv or sys.argv[:]
        self.prog_name = os.path.basename(self.argv[0])
        self.settings_exception = None

赋值self.argv=argvself.prog_name="manage.py"路径,接下来我们看下execute方法吧!

代码有些长,我先把代码贴出来,然后在一点点看

    def execute(self):
        try:
            subcommand = self.argv[1]
        except IndexError:
            subcommand = 'help'  # Display help if no arguments were given.
        parser = CommandParser(None, usage="%(prog)s subcommand [options] [args]", add_help=False)
        parser.add_argument('--settings')
        parser.add_argument('--pythonpath')
        parser.add_argument('args', nargs='*')  # catch-all
        try:
            options, args = parser.parse_known_args(self.argv[2:])
            handle_default_options(options)
        except CommandError:
            pass  # Ignore any option errors at this point.
        try:
            settings.INSTALLED_APPS
        except ImproperlyConfigured as exc:
            self.settings_exception = exc

        if settings.configured:
            if subcommand == 'runserver' and '--noreload' not in self.argv:
                try:
                    autoreload.check_errors(django.setup)()
                except Exception:
                    apps.all_models = defaultdict(OrderedDict)
                    apps.app_configs = OrderedDict()
                    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()

        self.autocomplete()

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

首先parser实例化一个CommandParser类对象解析器,并且add_argument添加如下的代码块

 parser.add_argument('--settings')
 parser.add_argument('--pythonpath')
 parser.add_argument('args', nargs='*')  # catch-all

我们具体也不知道到底是如何一步步解析的,也没有太大的必要去看CommandParser类代码
我们继续看代码进入try语句,我么看到有如下一行代码

options, args = parser.parse_known_args(self.argv[2:])
handle_default_options(options)

我们将options, args打印出来,看下输出结果

Namespace(args=['8000'], pythonpath=None, settings=None) [] @@@@@@@@@@@@@@@

看到上述打印,我们大概猜出parser对象,就是获取传递过来的['E:/python/django_pro/manage.py', 'runserver', '8000']参数,然后自己在封装add_argument需要的数据格式

接下来我们继续看代码,接下来继续执行如下代码

 settings.INSTALLED_APPS

这里就熟悉了,这不是我们settings.py文件中的如下配置么,是我们熟悉的admin模块、用户认真auth模块、sessions模块等,获取了一个字符串的列表数据

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app01.apps.App01Config',
    "rbac.apps.RbacConfig"
]

我们知道subcommand = self.argv[1]截取的字符串是runserver
接下来继续执行代码subcommand == 'runserver' and '--noreload' not in self.argv:
继而进入try语句代码autoreload.check_errors(django.setup)()
我们来看下check_errors函数做了什么操作处理?

def check_errors(fn):
    def wrapper(*args, **kwargs):
        global _exception
        try:
            fn(*args, **kwargs)
        except Exception:
            _exception = sys.exc_info()

            et, ev, tb = _exception

            if getattr(ev, 'filename', None) is None:
                # get the filename from the last item in the stack
                filename = traceback.extract_tb(tb)[-1][0]
            else:
                filename = ev.filename

            if filename not in _error_files:
                _error_files.append(filename)

            raise

    return wrapper

哦原来check_errors是一个装饰器函数,该方法接受一个参数,该参数类型是一个函数django.setup,然后返回一个函数wrapperautoreload.check_errors(django.setup)()代码意思就是最后执行了装饰器函数中的fn(*args, **kwargs),也就是传递的参数django.setup,好我们稍后django.setup做了什么操作?

基本上execute函数也快执行完毕,我们看下最后的代码是做了什么收尾操作?
紧接着执行代码 self.autocomplete()如下

def autocomplete(self):
、、、、
、、、、
、、、、
sys.exit(0)

大概意思就是sys模块使用完毕,去退出该模块,从而做一些sys模块的功能,这里就不去细看了
接下来继续执行execute方法,也就是execute方法的最后一行代码

self.fetch_command(subcommand).run_from_argv(self.argv)

上一行代码,我们通过代码run_from_argv字面含义,大概能猜到应该是需要某个模块类对象,然后启动某些东西

ok我们在分析execute方法时候,autoreload.check_errors(django.setup)()接下来我们就回过头来,去分析它,看看它里面是如何进行加载配置的
如下是django.setup代码部分

from __future__ import unicode_literals

from django.utils.version import get_version

VERSION = (1, 11, 9, 'final', 0)

__version__ = get_version(VERSION)
def setup(set_prefix=True):
    from django.apps import apps
    from django.conf import settings
    from django.urls import set_script_prefix
    from django.utils.encoding import force_text
    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 force_text(settings.FORCE_SCRIPT_NAME)
        )
    apps.populate(settings.INSTALLED_APPS)

代码中configure_logging大概是进行log配置,set_script_prefix以及设置一些前缀补充完整,这两个方法直接过,我们看如下有价值的代码,是调用了模块from django.apps import apps的类方法populate

apps.populate(settings.INSTALLED_APPS)

我也是先贴出populate代码,然后在逐行分析

    def populate(self, installed_apps=None):
        if self.ready:
            return
        with self._lock:
            if self.ready:
                return
            if self.app_configs:
                raise RuntimeError("populate() isn't reentrant")

            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

代码中通过with函数开启了self._lock = threading.Lock()锁,然后循环installed_apps,如果是AppConfig实例类型就返回,否则就去加载app和实例化该app的类对象app_config = AppConfig.create(entry)

 @classmethod
    def create(cls, entry):
        try:
            module = import_module(entry)
        except ImportError:
            module = None
            mod_path, _, cls_name = entry.rpartition('.')
            if not mod_path:
                raise
        else:
            try:
                entry = module.default_app_config
            except AttributeError:
                # Otherwise, it simply uses the default app config class.
                return cls(entry, module)
            else:
                mod_path, _, cls_name = entry.rpartition('.')
        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
        if not issubclass(cls, AppConfig):
            raise ImproperlyConfigured(
                "'%s' isn't a subclass of AppConfig." % entry)
        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,
                )
            )

        # Entry is a path to an app config class.
        return cls(app_name, app_module)

首先module = import_module(entry)先加载模块,然后entry = module.default_app_config获取各个app的配置,我就举一个admin的例子把

class AdminConfig(SimpleAdminConfig):
    """The default AppConfig for admin which does autodiscovery."""
    def ready(self):
        super(AdminConfig, self).ready()
        self.module.autodiscover()

接下来mod_path, _, cls_name = entry.rpartition('.')进行分割,我们来看下分割后的结果集

mod_path django.contrib.admin.apps
mod_path django.contrib.auth.apps
mod_path django.contrib.contenttypes.apps
mod_path django.contrib.sessions.apps
mod_path django.contrib.messages.apps
mod_path django.contrib.staticfiles.apps
mod_path app01.apps
mod_path rbac.apps
mod_path django.contrib.admin.apps
mod_path django.contrib.auth.apps
mod_path django.contrib.contenttypes.apps
mod_path django.contrib.sessions.apps
mod_path django.contrib.messages.apps
mod_path django.contrib.staticfiles.apps
mod_path app01.apps
mod_path rbac.apps
、、、、
、、、、

最后就app_module = import_module(app_name)加载,并且return cls(app_name, app_module)返回实例对象

当加载完毕后,进行导入各个app模块

 for app_config in self.app_configs.values():
     app_config.import_models()

然后修改其状态

 for app_config in self.get_app_configs():
    app_config.ready()
self.ready = True

最后看下构造的结果集

app_configs odict_values([<AdminConfig: admin>, <AuthConfig: auth>, <ContentTypesConfig: contenttypes>, <SessionsConfig: sessions>, <MessagesConfig: messages>, <StaticFilesConfig: staticfiles>, <App01Config: app01>, <RbacConfig: rbac>])

总结如下:
django调用python manage.py runserver启动项目时候,利用CommandParser构造并且解析数据,然后执行django.setup方法,调用django.apps模块来读取项目文件中的settings.py拿到这面这几个app,然后交给django.appsregistry.pyconfig.py来进行统一的配置加载,然后最后self.fetch_command(subcommand).run_from_argv(self.argv)加载

仓促完成,如有问题,评论留言,感谢~_~

以上是关于django---加载INSTALLED_APPS的源码分析的主要内容,如果未能解决你的问题,请参考以下文章

Django脆表单不加载CSS

在运行时动态加载 django 应用程序

如何解决 Django 中的“请求设置 INSTALLED_APPS,但未配置设置。”错误?

加载 Django runserver 提供的静态文件时如何修复 cors 错误

django.core.exceptions.ImproperlyConfigured:请求设置 INSTALLED_APPS,但未配置设置。请帮帮我

Django1.10 错误:django.contrib.contenttypes.models.ContentType 未声明显式 app_label 且不在 INSTALLED_APPS 中的应用