Django源码阅读(1)浅谈runserver的执行过程

入口manage.py 文件

if __name__ == '__main__':
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.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
    execute_from_command_line(sys.argv)
  • raise A from B 是一种很Pythonic的关于异常处理的写法,这篇文章讲的很清楚。设置环境变量之后,命令参数的列表传到了 execute_from_command_line 中。

命令管理工具

# django\core\management\ __init__.py
def execute_from_command_line(argv=None):
    """Run a ManagementUtility."""
    utility = ManagementUtility(argv)
    utility.execute()
    
class ManagementUtility:
"""
Encapsulate the logic of the django-admin and manage.py utilities.
"""
def __init__(self, argv=None):
    self.argv = argv or sys.argv[:]
    self.prog_name = os.path.basename(self.argv[0])
    if self.prog_name == '__main__.py':
        self.prog_name = 'python -m django'
    self.settings_exception = None
  • 初始化时对命令参数第一次判断(prog_namemanage.py 或者django-admin) ,实例化ManagementUtility类后调用了 execute() 方法,在这个方法中,会对命令参数进行处理。

execute() 方法

# django\core\management\__init__.py
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类对象解析器,并且add_argument添加如下的代码块
    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.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:
                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)
  • 将options, args打印出来,看下输出结果 Namespace(args=[], pythonpath=None, settings=None) [] ,可以看作parser对象自己封装参数的格式。接着获取了一个字符串列表,即在settings.py文件中的 INSTALLED_APPS 配置(admin模块、用户认证auth模块、sessions模块等)。
  • 当解析的的命令是 runserver 时,两个选择,第一个是自动重载的路线,通过 autoreload.check_errors(django.setup)() 。 check_errors是一个装饰器函数,该方法接受一个参数,然后返回一个函数wrapper,autoreload.check_errors(django.setup)()实际上最后执行了装饰器函数中的fn(*args, **kwargs),也就是传递的参数django.setup。
# django\utils\autoreload.py
def check_errors(fn):
    @functools.wraps(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
  • 另一个路线是参数中有 --noreload 时,就用 django.setup() 来启动服务。
  • 接着执行 self.autocomplete(),该函数最后一句显然是退出sys模块,所以这个函数封装的关于sys模块的功能,不是研究的重点,略过?
    def autocomplete(self):
        ...
        sys.exit(0)

动态加载模块

Django源码阅读(1)浅谈runserver的执行过程_第1张图片

  • 如果不是 runserver 而是其他命令,那么会对命令参数 self.argv[1] 进行判断,包括错误处理,是否是 help ,是否是 version ,根据不同的情况展示不同的信息。
  • 最重要的是最后一句,即前面的情况都不是,就进入self.fetch_command(subcommand).run_from_argv(self.argv) ,分两步,一步是获取执行命令所需要的类,其次是将命令参数作为参数传递给执行函数执行:
    def fetch_command(self, subcommand):
        # Get commands outside of try block to prevent swallowing exceptions
        commands = get_commands()
        try:
            app_name = commands[subcommand]
        except KeyError:
            if os.environ.get('DJANGO_SETTINGS_MODULE'):
                # If `subcommand` is missing due to misconfigured settings, the
                # following line will retrigger an ImproperlyConfigured exception
                # (get_commands() swallows the original one) so the user is
                # informed about it.
                settings.INSTALLED_APPS
            else:
                sys.stderr.write("No Django settings specified.\n")
            possible_matches = get_close_matches(subcommand, commands)
            sys.stderr.write('Unknown command: %r' % subcommand)
            if possible_matches:
                sys.stderr.write('. Did you mean %s?' % possible_matches[0])
            sys.stderr.write("\nType '%s help' for usage.\n" % 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
  • get_commands() 是返回是一个命令与模块映射作用的字典:
{ 'check': 'django.core', 
 'compilemessages': 'django.core',
 'createcachetable': 'django.core',
 'dbshell': 'django.core',
 ...
 'runserver': 'django.contrib.staticfiles',
 'sendtestemail': 'django.core', 
 ...}
  • 模块是通过 load_command_class 来动态加载的:
def load_command_class(app_name, name):
    """
    Given a command name and an application name, return the Command
    class instance. Allow all errors raised by the import process
    (ImportError, AttributeError) to propagate.
    """
    module = import_module('%s.management.commands.%s' % (app_name, name))
    return module.Command()
  • runserver 命令的模块实际上是在执行 django.contrib.staticfiles.management.commands.runserver ,然后返回该模块中定义的 Command 类的实例。获得实例后调用了 run_from_argv(self.argv)(直接定义在
    基类BaseCommand中) ,函数最终执行的是execute函数。而在execute中最终执行却又是self.handle(*args, **options),从注释可以知道,这个方法需要由子类实现的,即执行的 runserverhandle 函数。
# django\core\management\base.py
    def run_from_argv(self, argv):
        """
        Set up any environment changes requested (e.g., Python path
        and Django settings), then run this command. If the
        command raises a ``CommandError``, intercept it and print it sensibly
        to stderr. If the ``--traceback`` option is present or the raised
        ``Exception`` is not ``CommandError``, raise it.
        """
        self._called_from_command_line = True
        parser = self.create_parser(argv[0], argv[1])
		# 返回一个Namespace的实例
        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', ())
        # 设置默认参数
        handle_default_options(options)
        try:
        	# 异常捕获包裹的execute
            self.execute(*args, **cmd_options)
        except Exception as e:
            ...

    def execute(self, *args, **options):
        """
        Try to execute this command, performing system checks if needed (as
        controlled by the ``requires_system_checks`` attribute, except if
        force-skipped).
        """
        ...
        if self.requires_system_checks and not options.get('skip_checks'):
            self.check()
        if self.requires_migrations_checks:
            self.check_migrations()
        output = self.handle(*args, **options)
        ...
        
    def handle(self, *args, **options):
        """
        The actual logic of the command. Subclasses must implement
        this method.
        """
        raise NotImplementedError('subclasses of BaseCommand must provide a handle() method')

你可能感兴趣的:(Django源码阅读)