• django源码简析——后台程序入口


      这一年一直在用云笔记,平时记录一些tips或者问题很方便,所以也就不再用博客进行记录,还是想把最近学习到的一些东西和大家作以分享,也能够对自己做一个总结。工作中主要基于django框架,进行项目的开发,我是主要做后台相关比较多一些,熟悉django的同学知道,django的后台进程通常通过下面这种方式运行:

    python manage.py app [options]

      我们假设当前的项目名为myproject,这里app表示要运行的app名称,具体为django项目中module/management/commands中定义的进程文件名,options表示一些可选的参数。以python manage app为例,看下它的运行原理。manage.py是在项目创建之后,自动生成的一个py文件,它的定义如下:

    if __name__ == "__main__":
        os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")
    
        from django.core.management import execute_from_command_line
    
        execute_from_command_line(sys.argv)

      execute_from_command_line 方法用于读取命令行参数,并执行相应的app程序代码:

    def execute_from_command_line(argv=None):
        """
        A simple method that runs a ManagementUtility.
        """
        utility = ManagementUtility(argv)
        utility.execute()

      从这里可以看出,实际上app程序是通过 ManagementUtility.execute() 方法来执行的。execute方法定义在django.core.manage.__init__.py中:

    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.
    
        no_settings_commands = [
            'help', 'version', '--help', '--version', '-h',
            'compilemessages', 'makemessages',
            'startapp', 'startproject',
        ]
    
        try:
            settings.INSTALLED_APPS
        except ImproperlyConfigured as exc:
            self.settings_exception = exc
            if subcommand in no_settings_commands:
                settings.configure()
    
        if settings.configured:
            if subcommand == 'runserver' and '--noreload' not in self.argv:
                try:
                    autoreload.check_errors(django.setup)()
                except Exception:
                    pass
            else:
                django.setup()
    
        self.autocomplete()
    
        if subcommand == 'help':
            if '--commands' in args:
                sys.stdout.write(self.main_help_text(commands_only=True) + '
    ')
            elif len(options.args) < 1:
                sys.stdout.write(self.main_help_text() + '
    ')
            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() + '
    ')
        elif self.argv[1:] in (['--help'], ['-h']):
            sys.stdout.write(self.main_help_text() + '
    ')
        else:
            self.fetch_command(subcommand).run_from_argv(self.argv)

       我们来分解一下这段程序,subcommnad是python manage.py后的参数,即子程序名,argv[0]表示manage.py。这里如果没有指定,那么子程序默认为help。接着通过CommandParser来解析随后的参数,app子程序名之后的参数,这里我们默认没有其他参数。接着在try语句中执行 settings.INSTALLED_APPS,这句乍看上去很是不解,没有赋值,没有输出,注意settings是django.conf.__init__.py中定义的一个LazySettings对象,LazySettings继承自LazyObject类,它重写了__getattr__和__setattr__方法,那么在调用settings.INSTALLED_APPS时,会通过其自定义的__getattr__方法实现:

    settings = LazySettings()
    
    # django.conf.__init__.py
    class LazySettings(LazyObject):
        # other functions ...
        
        def _setup(self, name=None):
            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 __getattr__(self, name):
            if self._wrapped is empty:
                self._setup(name)
            return getattr(self._wrapped, name)
            
        # other functions ...

      _setup方法从当前环境变量中获取ENVIRONMENT_VARIABLE("DJANGO_SETTINGS_MODULE"),这个值在manage.py文件中已经定义:

    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "my_project.settings")

      通过getter/setter方法,对settings对象的操作转到其私有成员self._wrapped对象的调用上,这里在第一次使用settings对象时,将其私有成员self._wrapped初始化为Settings类实例,其构造函数如下:

    # django.conf.__init__.py

    class
    Settings(BaseSettings): 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 mod = importlib.import_module(self.SETTINGS_MODULE) tuple_settings = ( "ALLOWED_INCLUDE_ROOTS", "INSTALLED_APPS", "TEMPLATE_DIRS", "LOCALE_PATHS", ) self._explicit_settings = set() for setting in dir(mod): if setting.isupper(): setting_value = getattr(mod, setting) if (setting in tuple_settings and isinstance(setting_value, six.string_types)): raise ImproperlyConfigured("The %s setting must be a tuple. Please fix your settings." % 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 ('django.contrib.auth.middleware.AuthenticationMiddleware' in self.MIDDLEWARE_CLASSES and 'django.contrib.auth.middleware.SessionAuthenticationMiddleware' not in self.MIDDLEWARE_CLASSES): warnings.warn("Session verification will become mandatory in Django 1.10. Please add 'django.contrib.auth.middleware.SessionAuthenticationMiddleware' " "to your MIDDLEWARE_CLASSES setting when you are ready to opt-in after reading the upgrade considerations in the 1.8 release notes.", RemovedInDjango110Warning ) if hasattr(time, 'tzset') and self.TIME_ZONE: zoneinfo_root = '/usr/share/zoneinfo' if (os.path.exists(zoneinfo_root) and not os.path.exists(os.path.join(zoneinfo_root, *(self.TIME_ZONE.split('/'))))): raise ValueError("Incorrect timezone setting: %s" % self.TIME_ZONE) os.environ['TZ'] = self.TIME_ZONE time.tzset() # other functions ...

      这里传递给settings_module的参数值为my_project.settings,构造函数会先通过global_settings来设置其属性,接着读取my_project.settings,设置其特定的属性,主要有ALLOWED_INCLUDE_ROOTS、INSTALLED_APPS、TEMPLATE_DIRS、LOCALE_PATHS这几个key,这几个key的解释如下:

    • ALLOWED_INCLUDE_ROOTS, 默认值为 () (即空元组,在global_settings中),它表示嵌入文件根路径的字符串——只有在某字符串存在于该元组的情况下,Django的 {% ssi %} 模板标签才会嵌入以其为前缀的文件。 这样做是出于安全考虑,从而使模板作者不能访问到他们不该访问的文件。
    • INSTALLED_APPS,默认同样为空元组,它表示项目中哪些 app 处于激活状态。元组中的字符串,除了django默认自带的命令之外,就是我们自己定义的app,也就是用python manage.py所启动的app了。
    • TEMPLATE_DIRS,默认同样为空元组,它表示模板文件的处处路径。
    • LOCALE_PATHS,默认同样为空元组,它表示Django将在这些路径中查找包含实际翻译文件的<locale_code>/LC_MESSAGES目录

      代码中使用了importlib.import_module这个方法,它支持程序动态引入以'.'分割的目录层次,比如importlib.import_module('django.core.management.commands.migrate'),这里该方法引入了myproject.settings模块,加载settings配置文件中上述4个key的值。接着校验中间件和时区的配置信息,完成全局实例settings中self._wrapped属性的初始化,最终通过__getattr__方法,将加载到的INSTALLED_APPS信息返回。回到execute函数,这里的全局settings实例以及初始化完毕,我们的subcommand不是runserver(runserver的情况下来之后再分析),接着运行django.setup()方法:

    # django.__init__.py

    def
    setup(): from django.apps import apps from django.conf import settings from django.utils.log import configure_logging configure_logging(settings.LOGGING_CONFIG, settings.LOGGING) apps.populate(settings.INSTALLED_APPS)

       这里setup函数配置日志信息,并且加载settings.INSTALLED_APPS中的自定义模块以及models模块,保存在django.apps中,这是一个全局的Apps类实例,用以注册或者说存储项目中的INSTALLED_APPS模块信息。我们来看下apps.populate方法:

    class Apps(object):
        # other functions ...
        
        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
    
                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():
                    all_models = self.all_models[app_config.label]
                    app_config.import_models(all_models)
    
                self.clear_cache()
                self.models_ready = True
    
                for app_config in self.get_app_configs():
                    app_config.ready()
    
                self.ready = True
                
        # other functions ...

      for循环中,使用AppConfig.create(entry) 加载installed_apps里面的各模块,并保存在app_cofigs中,注意create方法是AppConfig类的classmethod,用以实现工厂模式,它根据installed_apps中的模块构造出 AppConfig(app_name, app_module) 这样的实例,其中app_name表示INSTALLED_APPS中指定的应用字符串,app_module表示根据app_name加载到的module。当加载的模块中有定义default_app_config时,那么会构造其表示的类对象,例如我们在django项目中会用到的用户认证鉴权模块,在INSTALLED_APPS中配置为'django.contrib.auth',当在import_module此模块时,实际django.contrib.auth是一个python的package,在__init__.py文件中有定义了default_app_config = 'django.contrib.auth.apps.AuthConfig',那么最终会构造apps.py中定义的AuthConfig类实例,这些default_app_config对应的类同样继承自AppConfig。在AppConfig实例的初始化方法中,会记录这些应用的标签、文件路径等信息,最终将这些实例会保存在其属性app_configs中。接着每个AppConfig实例会加载其指定模块的models,all_models定义为all_models = defaultdict(OrderedDict),defaultdict会创建表示一个类似dict的实例,在构造时可以指定字典中元素值的默认类型,这里用OrderedDict来指定其默认的类型,OrderedDict是dict的子类,它可以记录元素添加到字典中的顺序,保证元素有序,因此在获取all_models中的元素时,当key不存在时,会创建一个OrderedDict对象,我们来看下models是如何加载的:

    for app_config in self.app_configs.values():
        all_models = self.all_models[app_config.label]
        app_config.import_models(all_models)
    
    
    MODELS_MODULE_NAME = 'models'
    
    
    def import_models(self, all_models):
        self.models = all_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)

       在module指定的目录或者package中,查找是否有定义models模块,并将其import进来。再回到execute方法中,如果python manage.py之后传递的是非help或者version这种帮助信息,那么会执行到语句:

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

       fetch_command方法内部先通过get_commands方法,从全局的apps对象中获取之前加载到的INSTALLED_APPS模块对应的management/commands包:

    # django.core.management.__init__.py
    
    @lru_cache.lru_cache(maxsize=None)
    def get_commands():
        commands = {name: 'django.core' for name in find_commands(upath(__path__[0]))}
    
        if not settings.configured:
            return commands
    
        for app_config in reversed(list(apps.get_app_configs())):
            path = os.path.join(app_config.path, 'management')
            commands.update({name: app_config.name for name in find_commands(path)})
    
        return commands
    
    class ManagementUtility(object):
        # other functions ...
        def fetch_command(self, subcommand):
            commands = get_commands()
            try:
                app_name = commands[subcommand]
            except KeyError:
                # This might trigger ImproperlyConfigured (masked in get_commands)
                settings.INSTALLED_APPS
                sys.stderr.write("Unknown command: %r
    Type '%s help' for usage.
    " %
                    (subcommand, self.prog_name))
                sys.exit(1)
            if isinstance(app_name, BaseCommand):
                # If the command is already loaded, use it directly.
                klass = app_name
            else:
                klass = load_command_class(app_name, subcommand)
            return klass
            
        # other functions ...

      注意方法定义在django.core.management._init_.py文件中,get_commands方法中的__path__[0]是其__init__.py的绝对路径,这里通过find_commands首先将django.core.management.commands目录下的模块引入进来,像我们常用的一些基础模块(通过python manage.py进行调用)比如startpp、migrate、compilemessages、runserver、shell等都在此目录下。加载完这些基础模块之后,接着加载apps中的自定义的commands模块,即INSTALLED_APPS对应的各个模块。再根据subcommand从中这些包中获取到对应的Command,返回Command类对象。django后台服务中的Command继承自BaseCommand,并且实现了各自业务的handle方法。

      接着,通过返回的对象调用其run_from_argv方法,从名称可以看出,这个方法是通过命令行参数,进行函数调用的:

    def run_from_argv(self, argv):
        self._called_from_command_line = True
        parser = self.create_parser(argv[0], argv[1])
    
        if self.use_argparse:
            options = parser.parse_args(argv[2:])
            cmd_options = vars(options)
            # Move positional args out of options to mimic legacy optparse
            args = cmd_options.pop('args', ())
        else:
            options, args = parser.parse_args(argv[2:])
            cmd_options = vars(options)
        handle_default_options(options)
        try:
            self.execute(*args, **cmd_options)
        except Exception as e:
            if options.traceback or not isinstance(e, CommandError):
                raise
    
            if isinstance(e, SystemCheckError):
                self.stderr.write(str(e), lambda x: x)
            else:
                self.stderr.write('%s: %s' % (e.__class__.__name__, e))
            sys.exit(1)
        finally:
            connections.close_all()

       我们知道 fetch_command 返回的Command对象继承自BaseCommand,那么不同的后台任务可能需要不同的参数信息,在run_from_argv方法中,通过调用create_parser方法,Command子类将不同的参数信息进行设置,再通过执行execute方法,最终调用子类Command对象中定义的handle方法,完成自定义项目中业务逻辑的实现。

  • 相关阅读:
    Azure 中 Linux 虚拟机的大小
    排查在 Azure 中创建、重启 Linux VM 或调整其大小时发生的分配故障
    如何在 Azure 中的 Linux 经典虚拟机上设置终结点
    针对通过 SSH 连接到 Azure Linux VM 时发生的失败、错误或被拒绝问题进行故障排除
    Linux 内核超时导致虚拟机无法正常启动
    Java并发编程(十三)同步容器类
    可以开发着玩一下的web项目
    org.tmatesoft.svn.core.SVNCancelException: svn: E200015: authentication canc
    FastDFS单机搭建以及java客户端Demo
    做前端(单纯页面和js)遇到的问题辑录(一)
  • 原文地址:https://www.cnblogs.com/Tour/p/6403833.html
Copyright © 2020-2023  润新知