【第二个命令是runserver】

第二个命令是runserver

django项目的第一个命令 python manage.py startproject

django项目的第二个命令: python manage.py runserver

本篇笔记在重点在于ArgumentParser类,对runserver本身的讨论较少。

关于 runserver Command 之前的操作,这里只说明一下大概:

manage.py 会导入 DJANGO_SETTINGS_MODULEsettings 是一个懒加载,加载配置时,默认先加载global_settings.py,再用DJANGO_SETTINGS_MODULE中的配置覆盖,只读大写配置。connections 是一个懒加载的,代表对多个数据库连接的实例, connection 是对 default 数据库的懒加载连接。Signal类,可用于转发命令。apps 负责组织已安装的应用,populate功能用于导入所有应用程序,django.setup()功能会调用populate。通过import_module("%s.management.commands.%s" % (app_name, name))找到 Command

在 startproject 时,BaseCommand 有一个遗留问题没有处理,便是 execute 中关于 "runserver" 的部分,这里拿出来分析一下:

# 即使代码损坏,也启动自动重新加载开发服务器。硬编码条件是一种`Code Smell`(代码异味),

# 但我们不能依赖命令类上的标志,因为我们还没有找到它。

if subcommand == "runserver" and "--noreload" not in self.argv:

# 当runserver,且需要reload时,走这一段儿代码

try:

autoreload.check_errors(django.setup)()

except Exception:

# 稍后将在自动重新加载器启动的子进程中引发异常。

# 通过加载空的应用程序列表来假装它没有发生。

apps.all_models = defaultdict(dict)

apps.app_configs = {}

apps.apps_ready = apps.models_ready = apps.ready = True

# 删除与内置运行服务器不兼容的选项(例如 contrib.staticfiles 的

# 运行服务器的选项)。此处的更改需要手动测试,如 #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)

这一段代码的关键是autoreload.check_errors到底是何方神圣,它的来源是:from django.utils import autoreload autoreload.check_errors的代码如下:

def check_errors(fn):

@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:

filename = traceback.extract_tb(tb)[-1][0] # 从堆栈中的最后一项获取文件名

else:

filename = ev.filename

if filename not in _error_files:

_error_files.append(filename)

raise

return wrapper

这是一个非常典型的装饰器,这个装饰器看起来只是对fn产生的Exception做了一些处理,并抛出错误,这里使用autoreload.check_errors(django.setup)(),和直接使用django.setup()效果是差不多的。 如果django.setup()出现错误,将在自动重新加载器启动的子进程中引发异常,这里暂时认为没有出错,并不加载应用。

这里暂时当作django.setup()不会出错来分析程序,继续进行 runserver Command 类的分析。 题外话,如果django.setup()出错了,十有八九是项目出现了错误,程序员应当考虑主动修复这个错误,而不是寄希望于django自行修复。

不难想象,runserver会是一个复杂的命令,先来看看它的Command头:

class Command(BaseCommand):

help = "Starts a lightweight web server for development."

# 每次重新加载服务器时都会显式调用验证

requires_system_checks = []

stealth_options = ("shutdown_message",)

suppressed_base_arguments = {"--verbosity", "--traceback"}

default_addr = "127.0.0.1"

default_addr_ipv6 = "::1"

default_port = "8000"

protocol = "http"

# django runserver默认使用WSGI服务器,django只是简单包装了一下

server_cls = WSGIServer

runserver 的 Command 的类,继承自BaseCommand,execute方法只是检查了一下no_color,随后便使用了BaseCommand的execute,那就是没有中间商,直接看add_arguments和handle方法。 先来看看WSGIServer:

from wsgiref import simple_server

class WSGIServer(simple_server.WSGIServer):

"""BaseHTTPServer that implements the Python WSGI protocol"""

request_queue_size = 10

def __init__(self, *args, ipv6=False, allow_reuse_address=True, **kwargs):

if ipv6:

self.address_family = socket.AF_INET6

self.allow_reuse_address = allow_reuse_address

super().__init__(*args, **kwargs)

def handle_error(self, request, client_address):

if is_broken_pipe_error():

logger.info("- Broken pipe from %s", client_address)

else:

super().handle_error(request, client_address)

从第三方包wsgiref导入的simple_server,这个wsgiref.simple_server.WSGIServer是http.server.HTTPServer的子类,看到http这个包,就不用再往下走了,这是一个比较常见的python自带包,如果继续往下探究,就可以查到"万物起源"的socket.socket django的WSGIServer看起来只是对wsgiref.simple_server.WSGIServer做了一个简单的包装。

在startproject的笔记中分析过,Command子类中最重要的两个方法是add_arguments和handle: add_arguments负责增加额外的参数:

def add_arguments(self, parser):

parser.add_argument(

"addrport", nargs="?", help="Optional port number, or ipaddr:port"

)

parser.add_argument(

"--ipv6",

"-6",

action="store_true",

dest="use_ipv6",

help="Tells Django to use an IPv6 address.",

)

parser.add_argument(

"--nothreading",

action="store_false",

dest="use_threading",

help="Tells Django to NOT use threading.",

)

parser.add_argument(

"--noreload",

action="store_false",

dest="use_reloader",

help="Tells Django to NOT use the auto-reloader.",

)

parser.add_argument(

"--skip-checks",

action="store_true",

help="Skip system checks.",

)

之前startproject时,简单分析过parser,对startproject来说,parser并没有展示出特别的效果,所以在startproject时只进行了简单的描述。而runserver时,具备了较多的功能,这里需要更详细的分析。 parser是CommandParser的实例,CommandParser是对argparse.ArgumentParser的一个简单包装,这里需要对argparse.ArgumentParser的用法做简单了解,笔者推荐查看官方文档:https://docs.python.org/zh-cn/3/library/argparse.html

不过,作为源码分析的笔记,自然还是需要看看源码说了些什么,这里笔者比较关系的两个问题,一个是add_argument做了什么,一个是parse_args, 注意,这是 argparse.ArgumentParser 的源码,不是django的。 由于第一个重点在add_argument,这里只节选部分代码片段:

class ArgumentParser(_AttributeHolder, _ActionsContainer):

"""用于将命令行字符串解析为Python对象的对象。

关键字参数:

- prog -- 程序的名称(默认:sys.argv[0])

- usage -- 用法消息(默认:从参数自动生成)

- description -- 程序功能的描述

- epilog -- 参数描述后面的文本

- parents -- 其参数应复制到此的解析器

- formatter_class -- 用于打印帮助消息的 HelpFormatter 类

- prefix_chars -- 可选参数前缀的字符

- fromfile_prefix_chars -- 包含附加参数的文件前缀的字符

- argument_default -- 所有参数的默认值

- conflict_handler -- 指示如何处理冲突的字符串

- add_help -- 添加 -h/-help 选项

- allow_abbrev -- 允许明确缩写长选项

- exit_on_error -- 确定发生错误时 ArgumentParser 是否退出并显示错误信息

"""

def __init__(self,

prog=None,

usage=None,

description=None,

epilog=None,

parents=[],

formatter_class=HelpFormatter,

prefix_chars='-',

fromfile_prefix_chars=None,

argument_default=None,

conflict_handler='error',

add_help=True,

allow_abbrev=True,

exit_on_error=True):

superinit = super(ArgumentParser, self).__init__

superinit(description=description,

prefix_chars=prefix_chars,

argument_default=argument_default,

conflict_handler=conflict_handler)

这里以django.core.management.__init__.py文件中,调用CommandParser的流程,来分析superinit传入了什么:

parser = CommandParser(

prog=self.prog_name, # 可以视作"manage.py"

usage="%(prog)s subcommand [options] [args]",

add_help=False,

allow_abbrev=False,

)

class CommandParser(ArgumentParser):

def __init__(

self, *, missing_args_message=None, called_from_command_line=None, **kwargs

):

self.missing_args_message = missing_args_message

self.called_from_command_line = called_from_command_line

super().__init__(**kwargs)

可以看出,superinit需要的几个参数,与调用CommandParser的传入参数无关,故实际效果为:

superinit(description=description, # None

prefix_chars=prefix_chars, # '-'

argument_default=argument_default, # None

conflict_handler=conflict_handler) # 'error'

ArgumentParser的两个父类,只有_ActionsContainer有__init__方法,add_argument这个方法也是由此类来定义的。 先看看_ActionsContainer的__init__方法:

import re as _re

from gettext import gettext as _, ngettext

class _ActionsContainer(object):

def __init__(self, description, prefix_chars,

argument_default, conflict_handler):

super(_ActionsContainer, self).__init__()

self.description = description # None

self.argument_default = argument_default # None

self.prefix_chars = prefix_chars # '-'

self.conflict_handler = conflict_handler # 'error'

# 设置注册表

self._registries = {}

# 注册actions

self.register('action', None, _StoreAction)

self.register('action', 'store', _StoreAction)

self.register('action', 'store_const', _StoreConstAction)

self.register('action', 'store_true', _StoreTrueAction)

self.register('action', 'store_false', _StoreFalseAction)

self.register('action', 'append', _AppendAction)

self.register('action', 'append_const', _AppendConstAction)

self.register('action', 'count', _CountAction)

self.register('action', 'help', _HelpAction)

self.register('action', 'version', _VersionAction)

self.register('action', 'parsers', _SubParsersAction)

self.register('action', 'extend', _ExtendAction)

# 如果冲突处理程序无效,则引发异常

# 寻找 self 的 _handle_conflict_error 方法,没有该方法就报错

self._get_handler()

# action存储

self._actions = []

self._option_string_actions = {}

# groups

self._action_groups = []

self._mutually_exclusive_groups = []

# 默认存储

self._defaults = {}

# 确定“选项”是否看起来像负数

self._negative_number_matcher = _re.compile(r'^-\d+$|^-\d*\.\d+$')

# 是否有任何看起来像负数的选项 -- 使用列表以便可以共享和编辑

self._has_negative_number_optionals = []

def register(self, registry_name, value, object): # 注册methods

registry = self._registries.setdefault(registry_name, {})

registry[value] = object

def _get_handler(self): # 从冲突处理程序字符串确定函数

handler_func_name = '_handle_conflict_%s' % self.conflict_handler

try:

return getattr(self, handler_func_name)

except AttributeError:

msg = _('invalid conflict_resolution value: %r')

raise ValueError(msg % self.conflict_handler)

def _handle_conflict_error(self, action, conflicting_actions):

message = ngettext('conflicting option string: %s',

'conflicting option strings: %s',

len(conflicting_actions))

conflict_string = ', '.join(

[option_string for option_string, action in conflicting_actions])

raise ArgumentError(action, message % conflict_string)

分析_ActionsContainer的__init__操作,主要是把一堆诸如_StoreAction的类,以类似于’store’的名字,注册到self._registries["action"]这个字典中,类似于:

self._registries = {

'action': {

None: "",

'store': "",

'store_const': "",

'store_true': "",

'store_false': "",

# .... 此处省略其它的注册类

}

}

接下来可以研究_ActionsContainer的add_argument方法了:

def add_argument(self, *args, **kwargs):

"""

add_argument(dest, ..., name=value, ...)

add_argument(option_string, option_string, ..., name=value, ...)

"""

# 如果没有提供位置参数或者只提供了一个并且它看起来不像选项字符串,则解析位置参数

chars = self.prefix_chars # '-'

if not args or len(args) == 1 and args[0][0] not in chars:

# len(args)==1 且 args[0]不以"-"开头时,dest 为 args[0]

if args and 'dest' in kwargs: # 不能重复定义 dest

raise ValueError('dest supplied twice for positional argument')

kwargs = self._get_positional_kwargs(*args, **kwargs)

else: # 否则,我们将添加一个可选参数

kwargs = self._get_optional_kwargs(*args, **kwargs)

# 如果没有提供默认值,则使用解析器级别的默认值

if 'default' not in kwargs:

dest = kwargs['dest']

if dest in self._defaults:

kwargs['default'] = self._defaults[dest]

elif self.argument_default is not None:

kwargs['default'] = self.argument_default

# 创建操作对象,并将其添加到解析器

# 以kwargs中action="store_true"为例,action_class为_StoreTrueAction

action_class = self._pop_action_class(kwargs)

if not callable(action_class):

raise ValueError('unknown action "%s"' % (action_class,))

action = action_class(**kwargs)

# 如果操作类型不可调用,则会引发错误

type_func = self._registry_get('type', action.type, action.type)

if not callable(type_func):

raise ValueError('%r is not callable' % (type_func,))

if type_func is FileType:

raise ValueError('%r is a FileType class object, instance of it'

' must be passed' % (type_func,))

# 如果元变量与类型不匹配,则会引发错误

if hasattr(self, "_get_formatter"):

try:

self._get_formatter()._format_args(action, None)

except TypeError:

raise ValueError("length of metavar tuple does not match nargs")

return self._add_action(action)

def _get_positional_kwargs(self, dest, **kwargs):

if 'required' in kwargs: # 位置参数不能设定'required'(必需)属性

msg = _("'required' is an invalid argument for positionals")

raise TypeError(msg)

# 如果始终需要至少一个位置参数,则将位置参数标记为必需

# OPTIONAL = '?' ZERO_OR_MORE = '*'

if kwargs.get('nargs') not in [OPTIONAL, ZERO_OR_MORE]:

kwargs['required'] = True

if kwargs.get('nargs') == ZERO_OR_MORE and 'default' not in kwargs:

kwargs['required'] = True

# 返回不带选项字符串的关键字参数

return dict(kwargs, dest=dest, option_strings=[])

def _get_optional_kwargs(self, *args, **kwargs):

# 确定短选项字符串和长选项字符串

option_strings = []

long_option_strings = []

for option_string in args:

# 不以适当前缀开头的字符串将抛出错误

if not option_string[0] in self.prefix_chars:

args = {'option': option_string,

'prefix_chars': self.prefix_chars}

msg = _('invalid option string %(option)r: '

'must start with a character %(prefix_chars)r')

raise ValueError(msg % args)

# 以两个前缀字符开头的字符串是长选项

option_strings.append(option_string)

if len(option_string) > 1 and option_string[1] in self.prefix_chars:

long_option_strings.append(option_string)

# 推断dest,'--foo-bar' -> 'foo_bar' 和 '-x' -> 'x'

dest = kwargs.pop('dest', None)

if dest is None: # 如果没有dest,则进行推断

if long_option_strings:

dest_option_string = long_option_strings[0]

else:

dest_option_string = option_strings[0]

dest = dest_option_string.lstrip(self.prefix_chars)

if not dest:

msg = _('dest= is required for options like %r')

raise ValueError(msg % option_string)

dest = dest.replace('-', '_') # 'foo-bar' -> 'foo_bar'

# 返回更新后的关键字参数

return dict(kwargs, dest=dest, option_strings=option_strings)

def _pop_action_class(self, kwargs, default=None):

action = kwargs.pop('action', default)

return self._registry_get('action', action, action)

def _registry_get(self, registry_name, value, default=None):

return self._registries[registry_name].get(value, default)

def _add_action(self, action):

self._check_conflict(action) # 解决任何冲突

self._actions.append(action) # 添加到操作列表

action.container = self

# 通过具有的任何选项字符串对操作进行索引

for option_string in action.option_strings:

self._option_string_actions[option_string] = action

# 如果任何选项字符串看起来像负数,则设置标志

for option_string in action.option_strings:

if self._negative_number_matcher.match(option_string):

if not self._has_negative_number_optionals:

self._has_negative_number_optionals.append(True)

# 返回创建的action

return action

def _check_conflict(self, action):

confl_optionals = [] # 查找与该选项冲突的所有选项

for option_string in action.option_strings:

if option_string in self._option_string_actions:

confl_optional = self._option_string_actions[option_string]

confl_optionals.append((option_string, confl_optional))

if confl_optionals: # 解决任何冲突

conflict_handler = self._get_handler()

# 如果有冲突,默认的conflict_handler是直接raise error

conflict_handler(action, confl_optionals)

以_StoreTrueAction为例,研究action的属性:

class _StoreTrueAction(_StoreConstAction):

def __init__(self, option_strings, dest, default=False,

required=False, help=None):

super(_StoreTrueAction, self).__init__(

option_strings=option_strings,

dest=dest,

const=True,

default=default, # False 默认为False

required=required, # False 不是必须要传入的

help=help)

需要再向前查看其父类_StoreConstAction:

class _StoreConstAction(Action):

def __init__(self, option_strings, dest, const, default=None,

required=False, help=None, metavar=None):

super(_StoreConstAction, self).__init__(

option_strings=option_strings,

dest=dest,

nargs=0,

const=const, # True

default=default, # False

required=required, # False

help=help)

def __call__(self, parser, namespace, values, option_string=None):

setattr(namespace, self.dest, self.const)

这个_StoreConstAction具备了__call__功能,或者重写了父类的__call__功能,但__init__需要查看父类Action的内容:

class Action(_AttributeHolder):

"""

有关如何将命令行字符串转换为 Python 对象的信息。

ArgumentParser 使用 Action 对象来表示从命令行中的一个或多个字符串解析单个参数所需的信息。

Action 构造函数的关键字参数也是 Action 实例的所有属性。

关键字参数:

- option_strings -- 应与此操作关联的命令行选项字符串列表。

- dest -- 保存所创建对象的属性名称

- nargs -- 应该使用的命令行参数的数量。 默认情况下,将使用一个参数并生成一个值。

其他值包括:

- N(整数)使用 N 个参数(并生成一个列表)

- '?' 消耗零个或一个参数

- '*' 消耗零个或多个参数(并生成一个列表)

- '+' 使用一个或多个参数(并生成一个列表)

请注意,默认值和 nargs=1 之间的区别在于,使用默认值时,将生成单个

值,而使用 nargs=1 时,将生成包含单个值的列表。

- const -- 如果指定了选项并且该选项使用不带值的操作,则要生成的值。

- default -- 如果未指定选项则生成的值。

- type -- 接受单个字符串参数并返回转换后的值的可调用函数。 标准 Python 类型 str、

int、float 和 complex 是此类可调用项的有用示例。 如果没有,则使用 str。

- choices -- 应该允许的值的容器。如果不是 None,则在将命令行参数转换为适当的

类型后,如果它不是此集合的成员,则会引发异常。

- required -- 如果操作必须始终在命令行中指定,则为True。这仅对可选命令行参数有意义。

- help -- 描述参数的帮助字符串。

- metavar -- 用于带有帮助字符串的选项参数的名称。如果没有,则“dest”值将用作名称。

"""

def __init__(self, option_strings, dest, nargs=None,

const=None, default=None, type=None, choices=None,

required=False, help=None, metavar=None):

self.option_strings = option_strings

self.dest = dest

self.nargs = nargs

self.const = const # True

self.default = default # False

self.type = type

self.choices = choices

self.required = required # False

self.help = help

self.metavar = metavar

def _get_kwargs(self): # 将属性和值以元组形式返回

names = ['option_strings', 'dest', 'nargs', 'const', 'default',

'type', 'choices', 'required', 'help', 'metavar']

return [(name, getattr(self, name)) for name in names]

def format_usage(self):

return self.option_strings[0]

def __call__(self, parser, namespace, values, option_string=None):

raise NotImplementedError(_('.__call__() not defined'))

这个Action类的__call__会直接报错,也就是说,此方法是需要子类实现的,这个操作和模板模式是相似的。 这里其实已经可以不用继续看_AttributeHolder了,并没有需要关注的功能了:

class _AttributeHolder(object):

"""提供 __repr__ 的抽象基类。

__repr__ 方法返回以下格式的字符串:

类名(attr=名称, attr=名称, ...)

这些属性由类级属性“_kwarg_names”确定,或者通过检查实例 __dict__ 来确定。

"""

def __repr__(self):

type_name = type(self).__name__

arg_strings = []

star_args = {}

for arg in self._get_args():

arg_strings.append(repr(arg))

for name, value in self._get_kwargs():

if name.isidentifier():

arg_strings.append('%s=%r' % (name, value))

else:

star_args[name] = value

if star_args:

arg_strings.append('**%s' % repr(star_args))

return '%s(%s)' % (type_name, ', '.join(arg_strings))

def _get_kwargs(self):

return list(self.__dict__.items())

def _get_args(self):

return []

当store_true的情况下,如果没有额外的传入,则其中有三个重要的参数如下:

self.const = const # True

self.default = default # False 默认值为False

self.required = required # False 默认并不是必须的

default可以认为是action的默认值,required 为是否必须 传入

至此,add_arguments已经分析完毕,这里结合代码看下实际效果。 分析add_arguments中的参数addrport:

parser.add_argument(

"addrport", nargs="?", help="Optional port number, or ipaddr:port"

)

这个addrport会识别为位置参数,走_get_positional_kwargs这条路线,nargs="?"表示可有可无。 检测出来的Action为:

_StoreAction(

option_strings=[],

dest='addrport',

nargs='?',

required=False, # 不是必须的

help='Optional port number, or ipaddr:port',

const=None,

default=None,

type=None,

choices=None,

metavar=None

)

分析add_arguments中的参数--ipv6:

parser.add_argument(

"--ipv6",

"-6",

action="store_true",

dest="use_ipv6",

help="Tells Django to use an IPv6 address.",

)

分析出来的Action为:

_StoreTrueAction(

option_strings=['--ipv6', '-6'], # 即'--ipv6'和'-6'的效果一样

dest='use_ipv6',

nargs=0,

const=True,

default=False,

required=False,

help='Tells Django to use an IPv6 address.',

type=None,

choices=None,

metavar=None

)

这里剩下的另一个问题,parse_known_args怎么工作:

def parse_known_args(self, args=None, namespace=None):

if args is None: # args 默认为系统参数

args = _sys.argv[1:]

else: # 确保 args 是可变的

args = list(args)

if namespace is None: # 从解析器默认值构建的默认命名空间

namespace = Namespace()

# 添加任何不存在的操作默认值

for action in self._actions:

# SUPPRESS = '==SUPPRESS=='

if action.dest is not SUPPRESS:

if not hasattr(namespace, action.dest):

if action.default is not SUPPRESS:

setattr(namespace, action.dest, action.default)

# 添加任何不存在的解析器默认值

for dest in self._defaults:

if not hasattr(namespace, dest):

setattr(namespace, dest, self._defaults[dest])

# 解析参数并在有任何错误时退出

if self.exit_on_error:

try:

namespace, args = self._parse_known_args(args, namespace)

except ArgumentError:

err = _sys.exc_info()[1]

self.error(str(err))

else:

namespace, args = self._parse_known_args(args, namespace)

if hasattr(namespace, _UNRECOGNIZED_ARGS_ATTR):

args.extend(getattr(namespace, _UNRECOGNIZED_ARGS_ATTR))

delattr(namespace, _UNRECOGNIZED_ARGS_ATTR)

return namespace, args

这里需要先关心一下,self._actions是什么:

[_HelpAction(option_strings=['-h', '--help'], dest='help', nargs=0, const=None, default='==SUPPRESS==', type=None,

choices=None, required=False, help='show this help message and exit', metavar=None),

_VersionAction(option_strings=['--version'], dest='version', nargs=0, const=None, default='==SUPPRESS==', type=None,

choices=None, required=False, help="Show program's version number and exit.", metavar=None),

_StoreAction(option_strings=['-v', '--verbosity'], dest='verbosity', nargs=None, const=None, default=1,

type="", choices=[0, 1, 2, 3], required=False, help='==SUPPRESS==', metavar=None),

_StoreAction(option_strings=['--settings'], dest='settings', nargs=None, const=None, default=None, type=None,

choices=None, required=False, metavar=None,

help='The Python path to a settings module, e.g. "myproject.settings.main". '

'If this isn\'t provided, the DJANGO_SETTINGS_MODULE environment variable will be used.'),

_StoreAction(option_strings=['--pythonpath'], dest='pythonpath', nargs=None, const=None, default=None, type=None,

choices=None, required=False,

help='A directory to add to the Python path, e.g. "/home/djangoprojects/myproject".', metavar=None),

_StoreTrueAction(option_strings=['--traceback'], dest='traceback', nargs=0, const=True, default=False, type=None,

choices=None, required=False, help='==SUPPRESS==', metavar=None),

_StoreTrueAction(option_strings=['--no-color'], dest='no_color', nargs=0, const=True, default=False, type=None,

choices=None, required=False, help="Don't colorize the command output.", metavar=None),

_StoreTrueAction(option_strings=['--force-color'], dest='force_color', nargs=0, const=True, default=False, type=None,

choices=None, required=False, help='Force colorization of the command output.', metavar=None),

_StoreAction(option_strings=[], dest='addrport', nargs='?', const=None, default=None, type=None, choices=None,

required=False, help='Optional port number, or ipaddr:port', metavar=None),

_StoreTrueAction(option_strings=['--ipv6', '-6'], dest='use_ipv6', nargs=0, const=True, default=False, type=None,

choices=None, required=False, help='Tells Django to use an IPv6 address.', metavar=None),

_StoreFalseAction(option_strings=['--nothreading'], dest='use_threading', nargs=0, const=False, default=True,

type=None, choices=None, required=False, help='Tells Django to NOT use threading.', metavar=None),

_StoreFalseAction(option_strings=['--noreload'], dest='use_reloader', nargs=0, const=False, default=True, type=None,

choices=None, required=False, help='Tells Django to NOT use the auto-reloader.', metavar=None),

_StoreTrueAction(option_strings=['--skip-checks'], dest='skip_checks', nargs=0, const=True, default=False, type=None,

choices=None, required=False, help='Skip system checks.', metavar=None),

_StoreFalseAction(option_strings=['--nostatic'], dest='use_static_handler', nargs=0, const=False, default=True,

type=None, choices=None, required=False,

help='Tells Django to NOT automatically serve static files at STATIC_URL.', metavar=None),

_StoreTrueAction(option_strings=['--insecure'], dest='insecure_serving', nargs=0, const=True, default=False, type=None,

choices=None, required=False, help='Allows serving static files even if DEBUG is False.', metavar=None)

]

除了Command中add_arguments添加的几个Action外,还有BaseCommand的create_parser中给出的几个参数,比如"--version"等,这些参数都将在namespace中使用,即setattr(namespace, action.dest, action.default)这里使用。

_parse_known_args这里将default数值修改为真正的数值,这个操作在_parse_known_args的take_action方法里,通过各类Action的__call__来实现,并在其中做了相当多的检查,例如required(强制需要)的属性是否定义,这个函数相当的复杂,但核心的功能并不难理解,这里以'addrport'使用的类_StoreAction为例子,看一下__call__的操作:

class _StoreAction(Action):

def __call__(self, parser, namespace, values, option_string=None):

setattr(namespace, self.dest, values)

很好理解,这就是简单的把namespace的self.dest(这里是addrport)属性改为values,其它的Action各有不同,但也是大差不差,比如_StoreTrueAction,如果没有该参数(比如'--ipv6'),则默认是False,如果有这个参数,就将namespace的self.dest设置为self.const,这里的self.const是True

class _StoreTrueAction(_StoreConstAction):

def __init__(self,

option_strings,

dest,

default=False,

required=False,

help=None):

super(_StoreTrueAction, self).__init__(

option_strings=option_strings,

dest=dest,

const=True, # const是true

default=default, # 默认是False

required=required,

help=help)

class _StoreConstAction(Action):

def __call__(self, parser, namespace, values, option_string=None):

setattr(namespace, self.dest, self.const)

分析一个实际例子:django_test.manage.py runserver 127.0.0.1:8000,则解析出的namespace中存在属性addrport的值为'127.0.0.1:8000';另一个参数'--ipv6'不存在,解析出的namespace中存在属性use_ipv6的值为False,其它参数可以此类推。

这里关于各种Action很好的体现出了编程过程中一个原则:功能单一。也就是封装过程中,不要赋予一个类过多的功能,便于后续维护,实现低耦合。这一系列的Action类,其实只用Action类这一种类,也是可以实现的,比如通过添加一些额外的属性,这样是可以减少类,但会增加耦合程度。反过来说,这种功能单一的设计原则,也会导致需要实现一大堆的类,对于熟悉这套程序的人来说是有利的,对于研究这段程序的新手就不太友好了,至少笔者为了弄明白这套东西,把所有的Action类翻了一遍,结果是功能大差不差,干脆就不在笔记里一一解析了。

这里看完了 Command 的 add_arguments,然后是看看 handle 做些什么:

def handle(self, *args, **options):

if not settings.DEBUG and not settings.ALLOWED_HOSTS:

raise CommandError("You must set settings.ALLOWED_HOSTS if DEBUG is False.")

# 判定是否使用ipv6,现阶段一般还是使用ipv4较多

self.use_ipv6 = options["use_ipv6"]

if self.use_ipv6 and not socket.has_ipv6:

raise CommandError("Your Python does not support IPv6.")

self._raw_ipv6 = False

if not options["addrport"]:

self.addr = ""

self.port = self.default_port

else:

m = re.match(naiveip_re, options["addrport"])

if m is None:

raise CommandError(

'"%s" is not a valid port number '

"or address:port pair." % options["addrport"]

)

self.addr, _ipv4, _ipv6, _fqdn, self.port = m.groups()

if not self.port.isdigit():

raise CommandError("%r is not a valid port number." % self.port)

if self.addr:

if _ipv6:

self.addr = self.addr[1:-1]

self.use_ipv6 = True

self._raw_ipv6 = True

elif self.use_ipv6 and not _fqdn:

raise CommandError('"%s" is not a valid IPv6 address.' % self.addr)

if not self.addr:

self.addr = self.default_addr_ipv6 if self.use_ipv6 else self.default_addr

self._raw_ipv6 = self.use_ipv6

self.run(**options)

def run(self, **options):

"""运行服务器,如果需要的话使用自动重新加载器"""

use_reloader = options["use_reloader"]

if use_reloader:

autoreload.run_with_reloader(self.inner_run, **options)

else:

self.inner_run(None, **options)

handle 看起来就是分析了一下ip地址、是否为ipv6,还是看看 run 的效果: 默认情况下,run 会使用 autoreload.run_with_reloader 进行运转,关于为什么默认是use_reload,是因为这个action:

_StoreFalseAction(option_strings=['--noreload'], dest='use_reloader', nargs=0, const=False, default=True, type=None,

choices=None, required=False, help='Tells Django to NOT use the auto-reloader.', metavar=None),

这里先看看autoreload.run_with_reloader 做了什么:

def run_with_reloader(main_func, *args, **kwargs):

signal.signal(signal.SIGTERM, lambda *args: sys.exit(0))

try:

if os.environ.get(DJANGO_AUTORELOAD_ENV) == "true":

reloader = get_reloader()

logger.info(

"Watching for file changes with %s", reloader.__class__.__name__

)

start_django(reloader, main_func, *args, **kwargs)

else:

exit_code = restart_with_reloader()

sys.exit(exit_code)

except KeyboardInterrupt:

pass

首先是signal.signal(signal.SIGTERM, lambda *args: sys.exit(0)),这里的操作就是将signal.SIGTERM(15,表示正常终止)信号绑定lambda *args: sys.exit(0)这个匿名函数,这里可以解释为收到signal.SIGTERM信号,就让进程终止。 然后是if os.environ.get(DJANGO_AUTORELOAD_ENV) == "true":这里,笔者翻了一遍代码,发现这个DJANGO_AUTORELOAD_ENV只在两个地方用到过,一个是在文件初始化的时候,一个是在restart_with_reloader:

DJANGO_AUTORELOAD_ENV = "RUN_MAIN"

def restart_with_reloader():

new_environ = {**os.environ, DJANGO_AUTORELOAD_ENV: "true"}

args = get_child_arguments()

while True:

p = subprocess.run(args, env=new_environ, close_fds=False)

if p.returncode != 3:

return p.returncode

这里分析一下DJANGO_AUTORELOAD_ENV的作用,它在一开始并没有出现在os.environ中,这样就会走exit_code = restart_with_reloader() 这个流程,然后就会走到restart_with_reloader方法中的p = subprocess.run(args, env=new_environ, close_fds=False)这里,以python manage.py runserver 127.0.0.1:8000为例,这里的args的值可以视作['python.exe', 'manage.py', 'runserver', '127.0.0.1:8000'],这个args由get_child_arguments获得,这个方法就是一个简单的判断,故不对这个方法进行分析。

subprocess.run通过调用子进程来运行args,子进程这里的环境变成了new_environ,也就走到了

if os.environ.get(DJANGO_AUTORELOAD_ENV) == "true":

reloader = get_reloader()

logger.info(

"Watching for file changes with %s", reloader.__class__.__name__

)

start_django(reloader, main_func, *args, **kwargs)

这个判断中。 这里回头看run_with_reloader的流程,就是用主进程打开子进程来进行实际的处理,而主进程只负责监管子进程。

接下来需要看看子进程怎么走,首先是获得reloader,即reloader = get_reloader():

def get_reloader():

"""返回最适合此环境的重新加载器"""

try:

WatchmanReloader.check_availability()

except WatchmanUnavailable:

return StatReloader()

return WatchmanReloader()

重载器默认5秒进行一次重载,这里的WatchmanReloader以pywatchman为基础进行编写,不过目前有些意见认为pywatchman已经出现了过时的情况,应当重新选定一个更好的watchman. 如果WatchmanReloader出错(比如没有安装pywatchman),则选择StatReloader. StatReloader的花费,按一些论坛上的说法,是比WatchmanReloader要大的(笔者这里也不确定,笔者并未进行实测),但作为更常见的方案,这里值得分析一下:

class StatReloader(BaseReloader):

SLEEP_TIME = 1 # 每秒检查一次更改

def tick(self):

mtimes = {}

while True:

for filepath, mtime in self.snapshot_files():

old_time = mtimes.get(filepath)

mtimes[filepath] = mtime

if old_time is None:

logger.debug("File %s first seen with mtime %s", filepath, mtime)

continue

elif mtime > old_time:

logger.debug(

"File %s previous mtime: %s, current mtime: %s",

filepath, old_time, mtime)

self.notify_file_changed(filepath) # 提醒文件发生了变更

time.sleep(self.SLEEP_TIME)

yield

def snapshot_files(self):

# 如果全局重叠,watched_files 可能会产生重复的路径。

seen_files = set()

for file in self.watched_files():

if file in seen_files:

continue

try:

mtime = file.stat().st_mtime

except OSError:

# 当文件不存在时抛出此错误。

continue

seen_files.add(file)

yield file, mtime

@classmethod

def check_availability(cls):

return True

这里能够看出,StatReloader的tick在调用时,会每秒检查一下,如果文件变更,则通过self.notify_file_changed提醒文件发生了变更。 BaseReloader这个类的功能是比较多的,还是先看看start_django会调用到哪个功能,再继续分析:

def start_django(reloader, main_func, *args, **kwargs):

ensure_echo_on()

main_func = check_errors(main_func)

django_main_thread = threading.Thread(

target=main_func, args=args, kwargs=kwargs, name="django-main-thread"

)

django_main_thread.daemon = True

django_main_thread.start()

while not reloader.should_stop:

reloader.run(django_main_thread)

查看ensure_echo_on:

def ensure_echo_on():

"""

确保启用回显模式。某些工具(例如 PDB)会禁

用它,这会导致重新加载后出现可用性问题。

"""

if not termios or not sys.stdin.isatty():

return

pass # ...省略这段代码

termios这里并没有使用,所以也就直接return了,直接往后看: main_func = check_errors(main_func)

def check_errors(fn):

@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:

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这里就是一个简单的装饰器,将错误做了一些处理,可以跳过。 这之后便是创建一个主线程并start(),如果reloader并不需要停止,则使用reloader.run(django_main_thread)

需要查看一下run函数的功能,run函数不在StatReloader中,则说明处于父类里,这里找到BaseReloader的功能:

class BaseReloader:

def __init__(self):

self.extra_files = set()

self.directory_globs = defaultdict(set)

self._stop_condition = threading.Event()

def run(self, django_main_thread):

logger.debug("Waiting for apps ready_event.")

self.wait_for_apps_ready(apps, django_main_thread)

from django.urls import get_resolver

# 通过访问 urlconf_module 属性,防止重新加载器启动时未加载 URL 模块的竞争情况。

try:

get_resolver().urlconf_module

except Exception:

# 加载 urlconf 可能会导致开发过程中出现错误。如果发生这种情况,忽略错误并继续。

pass

logger.debug("Apps ready_event triggered. Sending autoreload_started signal.")

autoreload_started.send(sender=self)

self.run_loop()

def run_loop(self):

ticker = self.tick()

while not self.should_stop:

try:

next(ticker)

except StopIteration:

break

self.stop()

主要的功能在于run,这里有必要看一下wait_for_apps_ready:

def wait_for_apps_ready(self, app_reg, django_main_thread):

"""

等待 Django 报告应用程序已加载。如果给定线程在应用程序准备就绪之前终止,则会引发

SyntaxError 或其他不可恢复的错误。在这种情况下,请停止等待 apps_ready 事件并继续处理。

如果线程处于活动状态并且已触发就绪事件,则返回 True;如果线程在等待事件时终止,则返回 False。

"""

while django_main_thread.is_alive():

if app_reg.ready_event.wait(timeout=0.1):

return True

else:

logger.debug("Main Django thread has terminated before apps are ready.")

return False

这个app_reg正是apps,在startproject的时候分析过App.populate,正是在这里存在self.ready_event.set(),也就是说,如果在0.1秒内完成了初始化,则返回True,否则返回False,这里算是把第一篇中的坑给填了,ready_event用在了这里。

get_resolver().urlconf_module 这里加载了一下 settings.ROOT_URLCONF,这个地方确实没看出实际效果。

run_loop则是对self.tick()的一个包装,但笔者有个疑问并未解决,那便是,这里为什么要使用一个生成器来设计tick(),把tick()和run_loop合并为一个死循环呢?

笔者有一个猜测,这里可能是django自己实现了一个类似于async的流程,也就是利用生成器,实现同步无阻塞,从而在外面看起来就是异步的。

但是,asyncio还有一个调度系统呢,会随着操作系统的不同而发生变更,而这里两个循环,能实现无阻塞吗,这里令人疑惑,笔者也没有想到明确的答案。

这里结束对重载系统的探讨,回到runserver的run方法,重载器的功能总结一下,也就是每过一秒(StatReloader)或五秒(WatchmanReloader),检查一下是否需要重载,不论是有重载器还是没有,这里真正处理业务的,都是这个self.inner_run功能:

class Command(BaseCommand):

# ...

def inner_run(self, *args, **options):

# 如果异常在 ManagementUtility.execute 中被静音以便在子进程中引发,请立即引发。

autoreload.raise_last_exception()

threading = options["use_threading"]

# “shutdown_message”是一个隐秘选项。

shutdown_message = options.get("shutdown_message", "")

if not options["skip_checks"]:

self.stdout.write("Performing system checks...\n\n")

self.check(display_num_errors=True)

# 需要在这里检查迁移,因此不能使用requires_migrations_check属性。

self.check_migrations()

try:

handler = self.get_handler(*args, **options)

run(

self.addr,

int(self.port),

handler,

ipv6=self.use_ipv6,

threading=threading,

on_bind=self.on_bind,

server_cls=self.server_cls,

)

except OSError as e:

# 使用有用的错误消息而不是丑陋的回溯

ERRORS = {

errno.EACCES: "You don't have permission to access that port.",

errno.EADDRINUSE: "That port is already in use.",

errno.EADDRNOTAVAIL: "That IP address can't be assigned to.",

}

try:

error_text = ERRORS[e.errno]

except KeyError:

error_text = e

self.stderr.write("Error: %s" % error_text)

# 需要使用操作系统退出,因为 sys.exit 在线程中不起作用

os._exit(1)

except KeyboardInterrupt:

if shutdown_message:

self.stdout.write(shutdown_message)

sys.exit(0)

分析inner_run,这里的autoreload.raise_last_exception():

def raise_last_exception():

global _exception

if _exception is not None:

raise _exception[1]

如果autoreload有错误,则抛出,看起来好像没啥用。 self.check这里比较麻烦,直接看一下它的注释:

使用系统检查框架来验证整个 Django 项目。

对于任何严重消息(错误或严重错误),引发 CommandError。

如果只有轻微消息(如警告),请将它们打印到 stderr 并且不要引发异常。

check_migrations则是检查迁移文件,如果磁盘上的迁移集与数据库中的迁移不匹配,则打印警告。 这里存在一个操作executor = MigrationExecutor(connections[DEFAULT_DB_ALIAS]),调用了connections中的DEFAULT_DB_ALIAS,故在这里的时候,已经建立了与connections中DEFAULT_DB_ALIAS数据库的连接。

get_handler的作用,最终是获得一个WSGIHandler:

def get_handler(self, *args, **options):

"""返回运行器的默认 WSGI 处理程序"""

return get_internal_wsgi_application()

这里有必要对get_internal_wsgi_application()进行查看:

def get_internal_wsgi_application():

from django.conf import settings

app_path = getattr(settings, "WSGI_APPLICATION")

if app_path is None:

return get_wsgi_application()

try:

return import_string(app_path)

except ImportError as err:

raise ImproperlyConfigured(

"WSGI application '%s' could not be loaded; "

"Error importing module." % app_path

) from err

这个"WSGI_APPLICATION"在项目生成时,默认为:'项目名(例如django_test).wsgi.application' 默认生成的wsgi.py文件内容为:

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', '项目名(例如django_test).settings')

application = get_wsgi_application()

所以默认情况下,绕一圈后,还是同一个WSGIHandler,不同之处在于又重新导入了一次'DJANGO_SETTINGS_MODULE'.

由于这个WSGIHandler是相当复杂的,还是先看看哪里用到了:

def run(

addr,

port,

wsgi_handler,

ipv6=False,

threading=False,

on_bind=None,

server_cls=WSGIServer,

):

server_address = (addr, port)

if threading:

httpd_cls = type("WSGIServer", (socketserver.ThreadingMixIn, server_cls), {})

else:

httpd_cls = server_cls

httpd = httpd_cls(server_address, WSGIRequestHandler, ipv6=ipv6)

if on_bind is not None:

on_bind(getattr(httpd, "server_port", port))

if threading:

# ThreadingMixIn.daemon_threads 指示线程在突然关闭时的行为方式;

# 例如用户退出服务器或自动重新加载器重新启动。 True 意味着服务器

# 在退出之前不会等待线程终止。 这将使自动重新加载速度更快,并且

# 如果线程未正确终止,则无需手动终止服务器。

httpd.daemon_threads = True

httpd.set_app(wsgi_handler)

httpd.serve_forever()

httpd正是WSGIServer这个类,在笔记最开始的时候,就分析了WSGIServer这个类,它本身是wsgiref.WSGIServer类的简单包装,这个set_app是wsgiref.WSGIServer类的功能:

def set_app(self, application):

self.application = application

那么这里反过来看,WSGIHandler类最需要关注__init__方法:

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"

):

# 如果使用“wsgi.file_wrapper”,WSGI 服务器不会在响应上调用 .close,而是在文

# 件包装器上调用 .close。 修补它以使用response.close 来代替它负责关闭所有文件。

response.file_to_stream.close = response.close

response = environ["wsgi.file_wrapper"](

response.file_to_stream, response.block_size

)

return response

WSGIHandler类__init__方法,调用了父类的BaseHandler方法,父类没有直接定义__init__方法,故这里调用的是object的__init__方法,然后调用self.load_middleware(),而WSGIHandler类没有定义该方法,故调用BaseHandler的load_middleware方法:

class BaseHandler:

_view_middleware = None

_template_response_middleware = None

_exception_middleware = None

_middleware_chain = None

def load_middleware(self, is_async=False):

"""

从 settings.MIDDLEWARE 填充中间件列表。

必须在环境修复后调用(参见子类中的 __call__ ).

"""

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

for middleware_path in reversed(settings.MIDDLEWARE):

middleware = import_string(middleware_path)

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

try:

# 如果需要,调整处理程序

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

# 目前,异常处理堆栈仍然始终是同步的,因此请以这种方式进行调整。

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

# 如果需要,调整堆栈顶部。

handler = self.adapt_method_mode(is_async, handler, handler_is_async)

# 我们仅在初始化完成时分配给它,因为它用作初始化完成的标志。

self._middleware_chain = handler

这里不再对load_middleware进行详细的分析,但是这里有一个很有意思的东西: handler = convert_exception_to_response(get_response) 这个方法的源码节选:

def convert_exception_to_response(get_response):

"""

将给定的 get_response 可调用包装在异常到响应的转换中。 所有异常都将被转换。

所有已知的 4xx 异常(Http404、PermissionDenied、 MultiPartParserError、

SuspiciousOperation)将转换为适当的响应,所有其他异常将转换为 500 响应。

该装饰器会自动应用于所有中间件,以确保没有中间件泄漏异常,并且堆栈中

的下一个中间件可以依赖于获取响应而不是异常。

"""

if iscoroutinefunction(get_response):

@wraps(get_response)

async def inner(request):

try:

response = await get_response(request)

except Exception as exc:

response = await sync_to_async(

response_for_exception, thread_sensitive=False

)(request, exc)

return response

return inner

else:

@wraps(get_response)

def inner(request):

try:

response = get_response(request)

except Exception as exc:

response = response_for_exception(request, exc)

return response

return inner

这是一个很有趣的装饰器,但是可以发现,django代码中很少直接使用@来装饰一个功能,反而是handler = convert_exception_to_response(get_response)这种方式使用装饰器更常见,这是一种更加灵活的,使用装饰器的方式,如果有必要,可以模仿这种使用方法。

至此,对于runserver的大致分析完毕,总结一下runserver过程中做过的事情,包括常规的manage.py的工作,以及自己的特性工作。 其中,manage.py的几个重要工作为:

设定环境变量DJANGO_SETTINGS_MODULE创造数据库懒连接,但不进行实际连接组织已安装的应用

runserver的特性工作包括:

重载器创建简单的WSGIServer(这个服务器并不是高可用的,不建议线上使用)检查整个项目和迁移文件与数据库进行实际连接加载中间件(含错误处理,如404等)

与django无关的新知识点:

ArgumentParser装饰器的非@用法