一、环境
- 系统: Mac 64位(M2)
- python版本: 3.12
- Django版本: 6.1
- 项目名称: test
- 虚拟环境: conda(django-test)
- IDE : Pycharm2023
准备环境(假设系统已经装好conda):
- 创建虚拟环境
conda create -n django-test python=3.12
- 激活环境变量
conda activate django-test
- 安装django
pip install django
或者 pip . install django=="指定版本"
- Pycharm配置虚拟环境 这里就不写了
注:创建虚拟环境方式有很多种,conda只是其中一种。也可以使用系统环境(如果安装了的话)
二、启动过程
我们以"python manage.py runserver 8000"为例,看看django的启动过程。
(1)manage.py
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.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)
if __name__ == '__main__':
main()
解析:
- os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings') 将字符串"myproject.settings"注入到系统环境变量,名称为"DJANGO_SETTINGS_MODULE"
- from django.core.management import execute_from_command_line 从django.core.management模块中导入execute_from_command_line函数,这里用了try防止你环境没有安装django或者忘记激活虚拟环境。
- execute_from_command_line(sys.argv) 调用execute_from_command_line函数,同时将python后的参数列表(包括文件名)传递给这个函数。
sys.argv: ['manage.py', 'runserver', '8000']
(2)execute_from_command_line()函数
点击execute_from_command_line,进入源码:
def execute_from_command_line(argv=None):
"""Run a ManagementUtility."""
utility = ManagementUtility(argv)
utility.execute()
调用了ManagementUtility类创建实例然后调用了实例的execute()方法。
- ManagementUtility(argv)做了哪些初始化 这个类就定义在execute_from_command_line函数上面,我们拿到他的init方法:
class ManagementUtility:
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
self.argv = argv or sys.argv[:] 如果argv没有传递,那就从sys.argv中深拷贝([:]切片操作属于深拷贝)过来,这里使用深拷贝防止后续的修改影响到全局的 sys.argv
self.prog_name = os.path.basename(self.argv[0]) 通过 os.path.basename 提取执行文件的名称
if self.prog_name == "main.py": 如果你是用 python -m django runserver 这种方式启动的,Python 底层实际执行的是 Django 内部的 main.py 文件
所以utility = ManagementUtility(argv)里只初始化了3个实例属性,没有其他初始化流程。
(3)执行utility.execute()
execute()源码:
class ManagementUtility:
...
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(
prog=self.prog_name,
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(dict)
apps.app_configs = {}
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)
获取子命令
try:
subcommand = self.argv[1]
except IndexError:
subcommand = "help" # Display help if no arguments were given.
获取字命令,没有就赋值为"help",由于我们执行的是"python manager.py runserver 8000",所以这里subcommand="runserver"
创建命令解析器
parser = CommandParser(
prog=self.prog_name,
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
通过 CommandParser类创建一个parser,通过追踪源码,这个命令就是继承自 Python 原生的 argparse.ArgumentParser类。所以parser是一个解析器对象
- parser.add_argument("--settings") 注册 --settings 参数(例如 --settings=myproject.settings.local)。Django 必须优先拿到它,才能知道去哪里加载项目的全局配置。
- parser.add_argument("--pythonpath") 注册 --pythonpath 参数。用于在运行时临时将某个目录加入到 Python 的模块搜索路径(sys.path)中。
- parser.add_argument("args", nargs="") 这是一个**全捕获(Catch-all)**的位置参数。nargs="" 表示接受 0 个或多个剩余的所有参数。因为这第一步只想挑出 --settings 和 --pythonpath,剩下的所有内容(比如子命令 runserver、端口 8000 等)都会被暂时打包塞进这个 args 列表中,留到下一步再去解析。
--settings及--pythonpath参数解析
try:
options, args = parser.parse_known_args(self.argv[2:])
handle_default_options(options)
except CommandError:
pass # Ignore any option errors at this point.
parser解析器对象调用了parse_known_args()方法,并将子命令后面参数列表传进去了(self.argv[0]是文件名manage.py,self.argv[0]是子命令runserver),返回options, args。 parse_known_args方法是CommandParser类继承Python自带模块argparse.ArgumentParser类的方法,自带模块源码就不看了。 这段作用是 通过parse_known_args()解析子命令后的参数,同时忽略未知参数错误,句将认识的参数赋给options,不认识的参数赋给args。 我们在上面注册了--settings及--pythonpath参数,由于我们运行的是"python manage.py runserver 8000",所以这两个默认为None,结果:
- options=Namespace(settings=None, pythonpath=None)
- args=['8000']
接着运行django.core.management.base模块handle_default_options方法并传入options:
from django.core.management.base import (
...
handle_default_options,
)
handle_default_options的源码:
def handle_default_options(options):
"""
Include any default options that all commands should accept here
so that ManagementUtility can handle them before searching for
user commands.
"""
if options.settings:
os.environ["DJANGO_SETTINGS_MODULE"] = options.settings
if options.pythonpath:
sys.path.insert(0, options.pythonpath)
我们在manage.py的main()函数里已经将环境变量DJANGO_SETTINGS_MODULE设置为"myproject.settings":
- if options.settings: 如果我们在命令行写了 --settings="指定值",将环境变量DJANGO_SETTINGS_MODULE的值覆盖为"指定值"
- if options.pythonpath: 动态调整 Python 模块搜索路径,如果用户在命令行中指定了 --pythonpath(例如指定了一个特殊的代码目录),Django 会利用 sys.path.insert(0, ...) 把这个路径强行插入到 Python 搜索路径的最前面(第 0 位)。
配置加载
try:
settings.INSTALLED_APPS
except ImproperlyConfigured as exc:
self.settings_exception = exc
except ImportError as exc:
self.settings_exception = exc
from django.conf import settings
查看settings:
settings = LazySettings()
它是由LazySettings()创建的一个实例对象。 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 (_wrapped := self._wrapped) is empty:
self._setup(name)
_wrapped = self._wrapped
val = getattr(_wrapped, name)
# RemovedInDjango70Warning.
if name in DEPRECATED_EMAIL_SETTINGS:
if hasattr(_wrapped, "MAILERS"):
raise AttributeError(
f"The {name} setting is not available when MAILERS is defined."
)
_show_settings_deprecation_warning(
EMAIL_SETTING_DEPRECATED_MSG.format(name=name), RemovedInDjango70Warning
)
# Special case some settings which require further modification.
# This is done here for performance reasons so the modified value is
# cached.
if name in {"MEDIA_URL", "STATIC_URL"} and val is not None:
val = self._add_script_prefix(val)
elif name == "SECRET_KEY" and not val:
raise ImproperlyConfigured("The SECRET_KEY setting must not be empty.")
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)
# RemovedInDjango70Warning.
if name == "SIGNED_COOKIE_LEGACY_SALT_FALLBACK":
_show_settings_deprecation_warning(
SIGNED_COOKIE_LEGACY_SALT_DEPRECATED_MSG,
RemovedInDjango70Warning,
)
# RemovedInDjango70Warning.
if name == "USE_BLANK_CHOICE_DASH":
_show_settings_deprecation_warning(
USE_BLANK_CHOICE_DASH_DEPRECATED_MSG, RemovedInDjango70Warning
)
# RemovedInDjango70Warning.
if name == "MAILERS":
# When MAILERS is set, clear any cached values of
# deprecated settings so that __getattr__() runs again for them.
for setting in DEPRECATED_EMAIL_SETTINGS:
self.__dict__.pop(setting, None)
if name in DEPRECATED_EMAIL_SETTINGS:
_show_settings_deprecation_warning(
EMAIL_SETTING_DEPRECATED_MSG.format(name=name), RemovedInDjango70Warning
)
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)
# RemovedInDjango70Warning.
def __dir__(self):
attrs = super().__dir__()
if hasattr(self._wrapped, "MAILERS"):
# When MAILERS is defined, filter out deprecated email
# settings that are from the global_settings defaults.
attrs = [
name
for name in attrs
if name not in DEPRECATED_EMAIL_SETTINGS
or self._wrapped.is_overridden(name)
]
return attrs
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.")
# RemovedInDjango70Warning.
_check_email_settings_conflicts(options.keys())
holder = UserSettingsHolder(default_settings)
for name, value in options.items():
if not name.isupper():
raise TypeError("Setting %r must be uppercase." % name)
setattr(holder, name, value)
self._wrapped = holder
@staticmethod
def _add_script_prefix(value):
"""
Add SCRIPT_NAME prefix to relative paths.
Useful when the app is being served at a subpath and manually prefixing
subpath to STATIC_URL and MEDIA_URL in settings is inconvenient.
"""
# Don't apply prefix to absolute paths and URLs.
if value.startswith(("http://", "https://", "/")):
return value
from django.urls import get_script_prefix
return "%s%s" % (get_script_prefix(), value)
@property
def configured(self):
"""Return True if the settings have already been configured."""
return self._wrapped is not empty
empty = object()
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
settings = LazySettings()创建时只初始化了一个属性self._wrapped = empty,这个empty是一个全局的object类实例。 当执行settings.INSTALLED_APPS后由于settings对象没有这个属性,也没有这个方法,于是就触发了__getattr__方法:
def __getattr__(self, name):
"""Return the value of a setting and cache it in self.__dict__."""
if (_wrapped := self._wrapped) is empty:
self._setup(name)
_wrapped = self._wrapped
val = getattr(_wrapped, name)
# RemovedInDjango70Warning.
if name in DEPRECATED_EMAIL_SETTINGS:
if hasattr(_wrapped, "MAILERS"):
raise AttributeError(
f"The {name} setting is not available when MAILERS is defined."
)
_show_settings_deprecation_warning(
EMAIL_SETTING_DEPRECATED_MSG.format(name=name), RemovedInDjango70Warning
)
# Special case some settings which require further modification.
# This is done here for performance reasons so the modified value is
# cached.
if name in {"MEDIA_URL", "STATIC_URL"} and val is not None:
val = self._add_script_prefix(val)
elif name == "SECRET_KEY" and not val:
raise ImproperlyConfigured("The SECRET_KEY setting must not be empty.")
self.__dict__[name] = val
return val
判断是否初次运行
if (_wrapped := self._wrapped) is empty:
self._setup(name)
_wrapped = self._wrapped
val = getattr(_wrapped, name)
这里用了一个还像运算符声明了一个_wrapped变量,在__getattr__内有效,检测是否是第一次运行(第一次运行就是True) 接着运行 self._setup(name),代码如下:
ENVIRONMENT_VARIABLE = "DJANGO_SETTINGS_MODULE"
class LazySettings(LazyObject):
...
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)
代码解析:
-
settings_module = os.environ.get(ENVIRONMENT_VARIABLE) ENVIRONMENT_VARIABLE在全局定义为"DJANGO_SETTINGS_MODULE" 所以这里是获取环境变量DJANGO_SETTINGS_MODULE的值,这个要么就是manage.py里设置的"myproject.settings",要么就是命令行参数--settings指定的。 到这里获取到的还只是一个字符串。
-
if not settings_module: 如果没有配置DJANGO_SETTINGS_MODULE环境变量,直接抛出异常。
-
self._wrapped = Settings(settings_module) 这里settings_module="myproject.settings"
接着找Settings(settings_module)类:
class Settings:
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_HOSTS",
"INSTALLED_APPS",
"TEMPLATE_DIRS",
"LOCALE_PATHS",
"SECRET_KEY_FALLBACKS",
)
self._explicit_settings = set()
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)
# RemovedInDjango70Warning.
if "SIGNED_COOKIE_LEGACY_SALT_FALLBACK" in self._explicit_settings:
warnings.warn(
SIGNED_COOKIE_LEGACY_SALT_DEPRECATED_MSG,
RemovedInDjango70Warning,
skip_file_prefixes=django_file_prefixes(),
)
# RemovedInDjango70Warning.
if "USE_BLANK_CHOICE_DASH" in self._explicit_settings:
warnings.warn(
USE_BLANK_CHOICE_DASH_DEPRECATED_MSG,
RemovedInDjango70Warning,
skip_file_prefixes=django_file_prefixes(),
)
# RemovedInDjango70Warning.
_check_email_settings_conflicts(self._explicit_settings)
for name in DEPRECATED_EMAIL_SETTINGS.intersection(self._explicit_settings):
warnings.warn(
EMAIL_SETTING_DEPRECATED_MSG.format(name=name),
RemovedInDjango70Warning,
skip_file_prefixes=django_file_prefixes(),
)
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()
代码分析:
- 加载 Django 的"出厂默认设置"
from django.conf import global_settings
...
for setting in dir(global_settings):
if setting.isupper():
setattr(self, setting, getattr(global_settings, setting))
global_settings默认配置: 这个定义在from django.conf模块的global_settings.py里
"""
DEBUG = False
# Whether the framework should propagate raw exceptions rather than catching
# them. This is useful under some testing situations and should never be used
# on a live site.
DEBUG_PROPAGATE_EXCEPTIONS = False
# People who get code error notifications. In the format
# ["email@example.com", '"Full Name" <anotheremail@example.com>']
ADMINS = []
...
通过 dir() 捞出所有的变量,并且规定只有大写字母(isupper())的变量才算作配置项。然后通过 setattr 把这些默认配置全复制到自己身上。这就保证了哪怕你的 settings.py 里什么都不写,Django 也能依靠默认值正常运转。这个自己指的是self._wrapped = Settings(settings_module),self._wrapped中的self指的是通过settings = LazySettings()创建的settings对象,所以默认环境会注册到settings对象的_wrapped变量中。
-
self.SETTINGS_MODULE = settings_module 将"myproject.settings"复制给属性SETTINGS_MODULE
-
mod = importlib.import_module(self.SETTINGS_MODULE) 根据字符串"myproject.settings"导入了settings,相当于"from myproject import settings as mod" 这段段就是通过环境变量动态加载项目的配置模块
-
配置清洗与类型强校验
...
tuple_settings = (
"ALLOWED_HOSTS",
"INSTALLED_APPS",
"TEMPLATE_DIRS",
"LOCALE_PATHS",
"SECRET_KEY_FALLBACKS",
)
self._explicit_settings = set()
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)
流程分析:
- 定义了一个元组tuple_settings,内容代码所示
- 定义一个空集合,赋给属性_explicit_settings
- 循环遍历settings.py 里定义的所有东西(包括变量、导入的模块、函数等)。
for setting in dir(mod)
- 筛选以大写字母开头的,并把它的值赋值给setting_value变量
- 如果变量在tuple_settings里且校验值 setting_value不是列表(list)或元组(tuple)类型,抛出异常
- 将声明的变量注册到类属性中
- 只有在settings.py中有定义,添加到self._explicit_settings集合中
所以上面代码作用是:筛选settings.py中大写字母开头的变量,如果这个变量在定义的 tuple_settings中,校验它的值是否是列表或者元组,如果不是就抛出异常,然后把变量注册为类属性及赋值,最后将变量名添加到self._explicit_settings中。_explicit_settings是一个集合,为何去重,settings.py中一个变量可能会反复定义。
- 检测SIGNED_COOKIE_LEGACY_SALT_FALLBACK配置项
if "SIGNED_COOKIE_LEGACY_SALT_FALLBACK" in self._explicit_settings:
warnings.warn(
SIGNED_COOKIE_LEGACY_SALT_DEPRECATED_MSG,
RemovedInDjango70Warning,
skip_file_prefixes=django_file_prefixes(),
)
它的主要目的是:在允许用户继续使用旧配置的同时,通过控制台弹出警告,温柔地催促用户赶紧修改。 如果配置文件写了SIGNED_COOKIE_LEGACY_SALT_FALLBACK这个配置,控制台就会警告,这个会在jango7中移除。
- 检测USE_BLANK_CHOICE_DASH配置项
# RemovedInDjango70Warning.
if "USE_BLANK_CHOICE_DASH" in self._explicit_settings:
warnings.warn(
USE_BLANK_CHOICE_DASH_DEPRECATED_MSG,
RemovedInDjango70Warning,
skip_file_prefixes=django_file_prefixes(),
)
如果配置文件写了USE_BLANK_CHOICE_DASH这个配置,控制台就会警告,这个会在jango7中移除。
- 邮件配置冲突检查
# RemovedInDjango70Warning.
_check_email_settings_conflicts(self._explicit_settings)
for name in DEPRECATED_EMAIL_SETTINGS.intersection(self._explicit_settings):
warnings.warn(
EMAIL_SETTING_DEPRECATED_MSG.format(name=name),
RemovedInDjango70Warning,
skip_file_prefixes=django_file_prefixes(),
)
旧的邮箱配置会在jango7中移除。
- 跨平台兼容的防御性时区校验及时区同步:
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()
把你在 settings.py 里配置的 TIME_ZONE(时区)真正同步到你运行代码的操作系统底层。
到这里我们把Settings类初始化完成了,并把实例赋给 self._wrapped变量,self._wrapped变量从指向object()实例指向Settings类创建的实例。self._setup()也结束调用
注册INSTALLED_APPS
回到__getattr__方法:
class LazySettings(LazyObject):
...
def __getattr__(self, name):
"""Return the value of a setting and cache it in self.__dict__."""
if (_wrapped := self._wrapped) is empty:
self._setup(name)
_wrapped = self._wrapped
val = getattr(_wrapped, name)
# RemovedInDjango70Warning.
if name in DEPRECATED_EMAIL_SETTINGS:
if hasattr(_wrapped, "MAILERS"):
raise AttributeError(
f"The {name} setting is not available when MAILERS is defined."
)
_show_settings_deprecation_warning(
EMAIL_SETTING_DEPRECATED_MSG.format(name=name), RemovedInDjango70Warning
)
# Special case some settings which require further modification.
# This is done here for performance reasons so the modified value is
# cached.
if name in {"MEDIA_URL", "STATIC_URL"} and val is not None:
val = self._add_script_prefix(val)
elif name == "SECRET_KEY" and not val:
raise ImproperlyConfigured("The SECRET_KEY setting must not be empty.")
self.__dict__[name] = val
return val
上面我们已经完成了self._setup(name),self._wrapped=Settings(settings_module)
- 声明_wrapped变量 _wrapped = self._wrapped = Settings(settings_module)
- val = getattr(_wrapped, name) 从刚才已经组装好、装满真实配置数据的真正 Settings 对象(_wrapped)里,把用户具体想要的那个配置项(name)的值拿出来。这里name = "INSTALLED_APPS",因为上面是通过settings.INSTALLED_APPS调用的,_wrapped已经在Settings初始化时注册进_wrapped的(setattr(self, setting, setting_value)) getattr(_wrapped, name)就会拿到settings.py文件里定义的列表:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
由于启动过程name = "INSTALLED_APPS",下面代码都不会执行:
if name in {"MEDIA_URL", "STATIC_URL"} and val is not None:
val = self._add_script_prefix(val)
elif name == "SECRET_KEY" and not val:
raise ImproperlyConfigured("The SECRET_KEY setting must not be empty.")
- 向settings对象注册INSTALLED_APPS
self.__dict__[name] = val
return val
向settings对象的__dict__字典中注册了INSTALLED_APPS,并返回INSTALLED_APPS列的值
上面__getattr__方法调用完成并返回INSTALLED_APPS列的值,我们回到__getattr__调用的地方
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(
prog=self.prog_name,
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(dict)
apps.app_configs = {}
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)
settings.INSTALLED_APPS完成,接着执行:
from django.utils import autoreload
...
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(dict)
apps.app_configs = {}
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()
看一下configured
@property
def configured(self):
"""Return True if the settings have already been configured."""
return self._wrapped is not empty
configured本来是实例方法的,但是通过@property装饰就可以像属性一样调用,由于前面self._wrapped已经指向的是Setting类的实例,所以不在是empty,所以configured返回True 。由于我们写的是"python manage.py runserver 8000","subcommand == "runserver" and "--noreload" not in self.argv"为True
from django.utils import autoreload
...
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(dict)
apps.app_configs = {}
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)
开始执行autoreload.check_errors(django.setup)() autoreload.check_errors代码:
def check_errors(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
global _exception
try:
fn(*args, **kwargs)
except Exception as e:
_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)
if _url_module_exception is not None:
raise e from _url_module_exception
raise e
return wrapper
这是一个装饰器,假设没有错误,autoreload.check_errors(django.setup)()就是执行django.setup()
查看django.setup()源码:
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
)
apps.populate(settings.INSTALLED_APPS)
- 引入apps对象 from django.apps import apps 后apps会首次实例化,apps = Apps(installed_apps=None)
- 引入settings对象 这个settings在前面已经实例化过了,而且已经将settings内定义的变量注入了这个实例对象的实例属性中
- 导入set_script_prefix函数
- 设置日志 configure_logging(settings.LOGGING_CONFIG, settings.LOGGING),在默认配置中有这两个的配置,如果写了就覆盖。
- 设置prefix 如果配置文件中配置了FORCE_SCRIPT_NAME,将这个值传给set_script_prefix函数,否则默认"/" 查看set_script_prefix
def set_script_prefix(prefix):
"""
Set the script prefix for the current thread.
"""
if not prefix.endswith("/"):
prefix += "/"
_prefixes.value = prefix
如果传进来的prefix不是以"/"结尾那就在尾部加上"/",然后赋值给Local类实例_prefixes对象的value属性。
- 调用populate方法 settings.INSTALLED_APPS就是settings.py写的INSTALLED_APPS列表,这里就不会触发__getattr__方法了,因为初始化settings对象时已经将INSTALLED_APPS注册为属性 apps.populate(settings.INSTALLED_APPS)实现:
class Apps:
def __init__(self, installed_apps=()):
# installed_apps is set to None when creating the main registry
# because it cannot be populated at that point. Other registries must
# provide a list of installed apps and are populated immediately.
if installed_apps is None and hasattr(sys.modules[__name__], "apps"):
raise RuntimeError("You must supply an installed_apps argument.")
# Mapping of app labels => model names => model classes. Every time a
# model is imported, ModelBase.__new__ calls apps.register_model which
# creates an entry in all_models. All imported models are registered,
# regardless of whether they're defined in an installed application
# and whether the registry has been populated. Since it isn't possible
# to reimport a module safely (it could reexecute initialization code)
# all_models is never overridden or reset.
self.all_models = defaultdict(dict)
# Mapping of labels to AppConfig instances for installed apps.
self.app_configs = {}
# Stack of app_configs. Used to store the current state in
# set_available_apps and set_installed_apps.
self.stored_app_configs = []
# Whether the registry is populated.
self.apps_ready = self.models_ready = self.ready = False
# For the autoreloader.
self.ready_event = threading.Event()
# Lock for thread-safe population.
self._lock = threading.RLock()
self.loading = False
# Maps ("app_label", "modelname") tuples to lists of functions to be
# called when the corresponding model is ready. Used by this class's
# `lazy_model_operation()` and `do_pending_operations()` methods.
self._pending_operations = defaultdict(list)
# Populate apps and models, unless it's the main registry.
if installed_apps is not None:
self.populate(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()
代码分析:
- apps是否ready apps初始化时,self.apps_ready = self.models_ready = self.ready = False 下面代码不会执行:
if self.ready:
return
- 获取线程锁
- 修改loading apps初始化时,self.loading = False
- 循环遍历installed_apps
python
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
installed_apps是一个字符串列表,肯定不是AppConfig类的实例 AppConfig.create代码(类方法):
@classmethod
def create(cls, entry):
"""
Factory that creates an app config from an entry in INSTALLED_APPS.
"""
# create() eventually returns app_config_class(app_name, app_module).
app_config_class = None
app_name = None
app_module = None
# If import_module succeeds, entry points to the app module.
try:
app_module = import_module(entry)
except Exception:
pass
else:
# If app_module has an apps submodule that defines a single
# AppConfig subclass, use it automatically.
# To prevent this, an AppConfig subclass can declare a class
# variable default = False.
# If the apps module defines more than one AppConfig subclass,
# the default one can declare default = True.
if module_has_submodule(app_module, APPS_MODULE_NAME):
mod_path = "%s.%s" % (entry, APPS_MODULE_NAME)
mod = import_module(mod_path)
# Check if there's exactly one AppConfig candidate,
# excluding those that explicitly define default = False.
app_configs = [
(name, candidate)
for name, candidate in inspect.getmembers(mod, inspect.isclass)
if (
issubclass(candidate, cls)
and candidate is not cls
and getattr(candidate, "default", True)
)
]
if len(app_configs) == 1:
app_config_class = app_configs[0][1]
else:
# Check if there's exactly one AppConfig subclass,
# among those that explicitly define default = True.
app_configs = [
(name, candidate)
for name, candidate in app_configs
if getattr(candidate, "default", False)
]
if len(app_configs) > 1:
candidates = [repr(name) for name, _ in app_configs]
raise RuntimeError(
"%r declares more than one default AppConfig: "
"%s." % (mod_path, ", ".join(candidates))
)
elif len(app_configs) == 1:
app_config_class = app_configs[0][1]
# Use the default app config class if we didn't find anything.
if app_config_class is None:
app_config_class = cls
app_name = entry
# If import_string succeeds, entry is an app config class.
if app_config_class is None:
try:
app_config_class = import_string(entry)
except Exception:
pass
# If both import_module and import_string failed, it means that entry
# doesn't have a valid value.
if app_module is None and app_config_class is None:
# If the last component of entry starts with an uppercase letter,
# then it was likely intended to be an app config class; if not,
# an app module. Provide a nice error message in both cases.
mod_path, _, cls_name = entry.rpartition(".")
if mod_path and cls_name[0].isupper():
# We could simply re-trigger the string import exception, but
# we're going the extra mile and providing a better error
# message for typos in INSTALLED_APPS.
# This may raise ImportError, which is the best exception
# possible if the module at mod_path cannot be imported.
mod = import_module(mod_path)
candidates = [
repr(name)
for name, candidate in inspect.getmembers(mod, inspect.isclass)
if issubclass(candidate, cls) and candidate is not cls
]
msg = "Module '%s' does not contain a '%s' class." % (
mod_path,
cls_name,
)
if candidates:
msg += " Choices are: %s." % ", ".join(candidates)
raise ImportError(msg)
else:
# Re-trigger the module import exception.
import_module(entry)
# Check for obvious errors. (This check prevents duck typing, but
# it could be removed if it became a problem in practice.)
if not issubclass(app_config_class, AppConfig):
raise ImproperlyConfigured("'%s' isn't a subclass of AppConfig." % entry)
# Obtain app name here rather than in AppClass.__init__ to keep
# all error checking for entries in INSTALLED_APPS in one place.
if app_name is None:
try:
app_name = app_config_class.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,
app_config_class.__module__,
app_config_class.__qualname__,
)
)
# Entry is a path to an app config class.
return app_config_class(app_name, app_module)
create解析: (1)声明局部变量
app_config_class = None
app_name = None
app_module = None
(2)导入模块
app_module = import_module(entry)
所以INSTALLED_APPS中需要填写能够导入的模块路径。这步执行后进入else (3)判断 module_has_submodule(app_module, APPS_MODULE_NAME) APPS_MODULE_NAME是一个全局变量,在当前代码的顶部声明为"apps",app_module拿到的是模块对象 <module 'django.contrib.admin' from '/Users/xxx/miniforge3/envs/django-test/lib/python3.12/site-packages/django/contrib/admin/init.py'>
module_has_submodule函数内部
def module_has_submodule(package, module_name):
"""See if 'module' is in 'package'."""
try:
package_name = package.__name__
package_path = package.__path__
except AttributeError:
# package isn't a package.
return False
full_module_name = package_name + "." + module_name
try:
return importlib_find(full_module_name, package_path) is not None
except ModuleNotFoundError:
# When module_name is an invalid dotted path, Python raises
# ModuleNotFoundError.
return False
- 获取package_name,package_path package_name = package.__name__拿到例如"django.contrib.admin" package_path = package.__path__拿到例如" ['/Users/xxx/miniforge3/envs/django-test/lib/python3.12/site-packages/django/contrib/admin']"
- 拼接".apps" full_module_name = package_name + "." + module_name
- 在 package_path路径下找到这个package_name并导入 importlib_find是这样导入的
from importlib.util import find_spec as importlib_find
作用是:“只去硬盘里刺探某个模块(或文件)是否存在、能不能被导入,但绝对不会真正执行(运行)它。” cd到系统package_path目录,查看确实有apps.py 文件。 所以module_has_submodule(app_module, APPS_MODULE_NAME)为True,没有就是False (4)导入apps 从INSTALLED_APPS拿到的字符串表示的模块中的同级目录下有apps.py文件,就拼接这个字符串,尾部添加".apps",然后通过import_module运行一下这个apps.py
mod_path = "%s.%s" % (entry, APPS_MODULE_NAME)
mod = import_module(mod_path)
(5)构建 app_configs
app_configs = [
(name, candidate)
for name, candidate in inspect.getmembers(mod, inspect.isclass)
if (
issubclass(candidate, cls)
and candidate is not cls
and getattr(candidate, "default", True)
)
]
(6)返回AppConfig类实例 整个上面都是在构建app_name,app_module准备,最后返回AppConfig类实例
return app_config_class(app_name, app_module)
create具体在干什么,后面专门一章来讲解,反正就是构建AppConfig实例过程的算法过程。
看下AppConfig实例化过程:
class AppConfig:
"""Class representing a Django application and its configuration."""
def __init__(self, app_name, app_module):
# Full Python path to the application e.g. 'django.contrib.admin'.
self.name = app_name
# Root module for the application e.g. <module 'django.contrib.admin'
# from 'django/contrib/admin/__init__.py'>.
self.module = app_module
# Reference to the Apps registry that holds this AppConfig. Set by the
# registry when it registers the AppConfig instance.
self.apps = None
# The following attributes could be defined at the class level in a
# subclass, hence the test-and-set pattern.
# Last component of the Python path to the application e.g. 'admin'.
# This value must be unique across a Django project.
if not hasattr(self, "label"):
self.label = app_name.rpartition(".")[2]
if not self.label.isidentifier():
raise ImproperlyConfigured(
"The app label '%s' is not a valid Python identifier." % self.label
)
# Human-readable name for the application e.g. "Admin".
if not hasattr(self, "verbose_name"):
self.verbose_name = self.label.title()
# Filesystem path to the application directory e.g.
# '/path/to/django/contrib/admin'.
if not hasattr(self, "path"):
self.path = self._path_from_module(app_module)
# Module containing models e.g. <module 'django.contrib.admin.models'
# from 'django/contrib/admin/models.py'>. Set by import_models().
# None if the application doesn't have a models module.
self.models_module = None
# Mapping of lowercase model names to model classes. Initially set to
# None to prevent accidental access before import_models() runs.
self.models = None
(a)创建实例属性name: self.name = app_name (b)创建实例属性module: self.module = app_module (c)创建实例属性apps,用来放Apps创建的实例 self.apps = None (d)设置label属性(如果没有) 以app_name最后"."后面的作为label;如果没有".",self.label就是app_name
if not hasattr(self, "label"):
self.label = app_name.rpartition(".")[2]
(e)校验self.label是否合法规范
if not self.label.isidentifier():
raise ImproperlyConfigured(
"The app label '%s' is not a valid Python identifier." % self.label
)
(f)设置verbose_name属性 如果开发者没有给应用手动配置一个可读的名称(verbose_name),Django 就会自动将它的应用标签(self.label)转换为‘首字母大写(Title Case)’的形式,作为默认的直观应用名称。
self.verbose_name = self.label.title()
(g)设置verbose_name属性 path=apps.py所在文件夹绝对路径
if not hasattr(self, "path"):
self.path = self._path_from_module(app_module)
(h)设置models_module 这是一个用来存放“包含了数据库模型的 Python 模块对象”的属性。例如:它在内存里存的,就是实实在在被加载运行后的整个 django/contrib/admin/models.py 文件实体。
self.models_module = None
(i)设置models_module 这是一个将"全小写的模型名字字符串”映射到“真正内存模型类(Class)"的账本字典。
self.models = None
例如:
self.models = {
"article": <class 'myapp.models.Article'>,
"comment": <class 'myapp.models.Comment'>,
}
-
回到上面populate中app_config = AppConfig.create(entry) 根据entry(INSTALLED_APPS里的条目),创建了一个AppConfig实例。
-
校验label是否重复 假设我们有一个users应用,里面apps.py是这样的
from django.apps import AppConfig
class UsersConfig(AppConfig):
name = 'users'
label = "label123"
app_config.label就是上面这个label,如果label重复,抛出异常,这个值要唯一
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
将这个AppConfig类实例保存到self.app_configs中,key为label,value为AppConfig类实例。
- 将apps实例赋给app_configs实例的apps属性
app_config.apps = self
- 校验应用name是否唯一
# 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)
)
- 修改apps_ready状态
self.apps_ready = True
- 导入应用的models模块
for app_config in self.app_configs.values():
app_config.import_models()
import_models代码: app_config就是AppConfig实例,在类中找到这个方法:
def import_models(self):
# Dictionary of models for this app, primarily maintained in the
# 'all_models' attribute of the Apps this AppConfig is attached to.
self.models = self.apps.all_models[self.label]
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)
(1)self.models赋值 self.apps指的是apps实例 从self.apps.all_models[self.label]找已经存在赋值给self.models (2)导入models_module 如果在self.module=app_module(apps.py所在目录)目录下找到名为models.py文件 (MODELS_MODULE_NAME = "models") 构造:models_module_name="%s.%s" % (self.name, MODELS_MODULE_NAME) models_module_name="app_name.models" 执行这个文件并将结果赋给self.models_module
- populate剩余部分 剩下的就是apps及app_config状态重置
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()
到这里django.setup()执行完成
回到execute()
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(dict)
apps.app_configs = {}
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)
注意:autoreload.check_errors(django.setup)()异常才会进入except Exception
如果子命令不是"runserver"也会进入 django.setup()过程,说明其他命令依赖 django.setup()
执行self.autocomplete()
def autocomplete(self):
"""
Output completion suggestions for BASH.
The output of this function is passed to BASH's `COMPREPLY` variable
and treated as completion suggestions. `COMPREPLY` expects a space
separated string as the result.
The `COMP_WORDS` and `COMP_CWORD` BASH environment variables are used
to get information about the cli input. Please refer to the BASH
man-page for more information about this variables.
Subcommand options are saved as pairs. A pair consists of
the long option string (e.g. '--exclude') and a boolean
value indicating if the option requires arguments. When printing to
stdout, an equal sign is appended to options which require arguments.
Note: If debugging this function, it is recommended to write the debug
output in a separate file. Otherwise the debug output will be treated
and formatted as potential completion suggestions.
"""
# Don't complete if user hasn't sourced bash_completion file.
if "DJANGO_AUTO_COMPLETE" not in os.environ:
return
cwords = os.environ["COMP_WORDS"].split()[1:]
cword = int(os.environ["COMP_CWORD"])
try:
curr = cwords[cword - 1]
except IndexError:
curr = ""
subcommands = [*get_commands(), "help"]
options = [("--help", False)]
# subcommand
if cword == 1:
print(" ".join(sorted(filter(lambda x: x.startswith(curr), subcommands))))
# subcommand options
# special case: the 'help' subcommand has no options
elif cwords[0] in subcommands and cwords[0] != "help":
subcommand_cls = self.fetch_command(cwords[0])
# special case: add the names of installed apps to options
if cwords[0] in ("dumpdata", "sqlmigrate", "sqlsequencereset", "test"):
try:
app_configs = apps.get_app_configs()
# Get the last part of the dotted path as the app name.
options.extend((app_config.label, 0) for app_config in app_configs)
except ImportError:
# Fail silently if DJANGO_SETTINGS_MODULE isn't set. The
# user will find out once they execute the command.
pass
parser = subcommand_cls.create_parser("", cwords[0])
options.extend(
(min(s_opt.option_strings), s_opt.nargs != 0)
for s_opt in parser._actions
if s_opt.option_strings
)
# filter out previously specified options from available options
prev_opts = {x.split("=")[0] for x in cwords[1 : cword - 1]}
options = (opt for opt in options if opt[0] not in prev_opts)
# filter options by current input
options = sorted((k, v) for k, v in options if k.startswith(curr))
for opt_label, require_arg in options:
# append '=' to options which require args
if require_arg:
opt_label += "="
print(opt_label)
# Exit code of the bash completion function is never passed back to
# the user, so it's safe to always exit with 0.
# For more details see #25420.
sys.exit(0)
这是BASH 命令行自动补全(Tab Completion)引擎,由于环境变量中没有"DJANGO_AUTO_COMPLETE",这里会跳过
继续往下执行:
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)
应用的运行
上面execute()里分析到这里:
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)
由于subcommand == "runserver,所以最后运行self.fetch_command(subcommand).run_from_argv(self.argv)。
fetch_command
找到代码:
def fetch_command(self, subcommand):
"""
Try to fetch the given subcommand, printing a message with the
appropriate command called from the command line (usually
"django-admin" or "manage.py") if it can't be found.
"""
# 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
elif not settings.configured:
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()方法内部实现
@functools.cache
def get_commands():
"""
Return a dictionary mapping command names to their callback applications.
Look for a management.commands package in django.core, and in each
installed application -- if a commands package exists, register all
commands in that package.
Core commands are always included. If a settings module has been
specified, also include user-defined commands.
The dictionary is in the format {command_name: app_name}. Key-value
pairs from this dictionary can then be used in calls to
load_command_class(app_name, command_name)
The dictionary is cached on the first call and reused on subsequent
calls.
"""
commands = {name: "django.core" for name in find_commands(__path__[0])}
if not settings.configured:
return commands
for app_config in reversed(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
构建commands时又用到了find_commands()
def find_commands(management_dir):
"""
Given a path to a management directory, return a list of all the command
names that are available.
"""
command_dir = os.path.join(management_dir, "commands")
return [
name
for _, name, is_pkg in pkgutil.iter_modules([command_dir])
if not is_pkg and not name.startswith("_")
]
get_commands内传给find_commands是__path__[0],__path__是内置高级变量,__path__[0]拿到的是: management 文件夹在操作系统中的绝对物理盘符路径
find_commands返回
[
'runserver',
'migrate',
'startapp',
'makemigrations',
'shell',
'createsuperuser',
'flush',
'showmigrations',
# ... 以及其他所有内置的命令名称
]
get_commands函数内commands此时结果为: 以内置命令为key,'django.core'为value的字典。
{'check': 'django.core', 'compilemessages': 'django.core', ...}
接着:
if not settings.configured:
return commands
for app_config in reversed(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
-
settings.configured 返回True
-
apps.get_app_configs() 查看get_app_configs()方法
def get_app_configs(self):
"""Import applications and return an iterable of app configs."""
self.check_apps_ready()
return self.app_configs.values()
self.apps_ready在上面已经赋值为True,所以self.check_apps_ready()正常执行。 返回self.app_configs.values(),经过执行django.setup(),self.app_configs已经有值了,self.app_configs.values()返回的就是AppConfig实例对象列表
- 更新commands 扫描app_config.path目录下的management/commands/ 文件夹,将扫描到的命令更新到commands中。
for app_config in reversed(apps.get_app_configs()):
path = os.path.join(app_config.path, "management")
commands.update({name: app_config.name for name in find_commands(path)})
扫描规则在find_commands里有描述
return [
name
for _, name, is_pkg in pkgutil.iter_modules([command_dir])
if not is_pkg and not name.startswith("_")
]
所以如果要创建自己的命令,需要在在应用下创建management/commands目录,命令文件名不能以下划线开头
回到fetch_command函数
def fetch_command(self, subcommand):
"""
Try to fetch the given subcommand, printing a message with the
appropriate command called from the command line (usually
"django-admin" or "manage.py") if it can't be found.
"""
# 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
elif not settings.configured:
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
app_name = commands[subcommand]代码执行后进入下面代码执行
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
修改源码测试: app_name打印的是"django.contrib.staticfiles",因为runserver.py 在这个包下。调用的是load_command_class(app_name, subcommand)
查看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()
module为: django.contrib.staticfiles.management.commands.runserver 函数返回module.Command()
在runserver.py 文件中Command类定义如下:
class Command(RunserverCommand):
help = (
"Starts a lightweight web server for development and also serves static files."
)
def add_arguments(self, parser):
super().add_arguments(parser)
parser.add_argument(
"--nostatic",
action="store_false",
dest="use_static_handler",
help="Tells Django to NOT automatically serve static files at STATIC_URL.",
)
parser.add_argument(
"--insecure",
action="store_true",
dest="insecure_serving",
help="Allows serving static files even if DEBUG is False.",
)
def get_handler(self, *args, **options):
"""
Return the static files serving handler wrapping the default handler,
if static files should be served. Otherwise return the default handler.
"""
handler = super().get_handler(*args, **options)
use_static_handler = options["use_static_handler"]
insecure_serving = options["insecure_serving"]
if use_static_handler and (settings.DEBUG or insecure_serving):
return StaticFilesHandler(handler)
return handler
Command类继承django.core.management.commands.runserver.Command类 django.core.management.commands.runserver.Command类源码:
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = "Starts a lightweight web server for development."
stealth_options = ("shutdown_message",)
suppressed_base_arguments = {"--verbosity", "--traceback"}
default_addr = "127.0.0.1"
default_addr_ipv6 = "::1"
default_port = "8000"
protocol = "http"
server_cls = WSGIServer
django.core.management.commands.runserver.Command类继承django.core.management.base.BaseCommand类,但是这里就定义了几个类变量 django.core.management.base.BaseCommand类源码:
class BaseCommand:
def __init__(self, stdout=None, stderr=None, no_color=False, force_color=False):
self.stdout = OutputWrapper(stdout or sys.stdout)
self.stderr = OutputWrapper(stderr or sys.stderr)
if no_color and force_color:
raise CommandError("'no_color' and 'force_color' can't be used together.")
if no_color:
self.style = no_style()
else:
self.style = color_style(force_color)
self.stderr.style_func = self.style.ERROR
if (
not isinstance(self.requires_system_checks, (list, tuple))
and self.requires_system_checks != ALL_CHECKS
):
raise TypeError("requires_system_checks must be a list or tuple.")
BaseCommand类__init__方法里只是对颜色打印初始化。 接下来找到self.fetch_command(subcommand).run_from_argv(self.argv)里run_from_argv方法实现,根据MRO搜索,这个定义在django.core.management.base.BaseCommand类里:
class BaseCommand:
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])
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:
self.execute(*args, **cmd_options)
except CommandError as e:
if options.traceback:
raise
# SystemCheckError takes care of its own formatting.
if isinstance(e, SystemCheckError):
self.stderr.write(str(e), lambda x: x)
else:
self.stderr.write("%s: %s" % (e.__class__.__name__, e))
sys.exit(e.returncode)
finally:
try:
connections.close_all()
except ImproperlyConfigured:
# Ignore if connections aren't setup at this point (e.g. no
# configured settings).
pass
流程解析
- self._called_from_command_line = True 设置了一个标志:是否从命令行执行的命令
- parser = self.create_parser(argv[0], argv[1]) argv是sys.argv,所以这里argv[0]="manage.py",argv[1]="runserver"
- 构建cmd_options argv[2:]=['8000']
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", ())
最终cmd_options类似这样
{'verbosity': 1, 'settings': None, 'pythonpath': None, 'traceback': False, 'no_color': False, 'force_color': False, 'skip_checks': False, 'addrport': '8000', 'use_ipv6': False, 'use_threading': True, 'use_reloader': True, 'use_static_handler': True, 'insecure_serving': False}
- 运行
self.execute(*args, **cmd_options)
查看execute方法:
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 options["force_color"] and options["no_color"]:
raise CommandError(
"The --no-color and --force-color options can't be used together."
)
if options["force_color"]:
self.style = color_style(force_color=True)
elif options["no_color"]:
self.style = no_style()
self.stderr.style_func = None
if options.get("stdout"):
self.stdout = OutputWrapper(options["stdout"])
if options.get("stderr"):
self.stderr = OutputWrapper(options["stderr"])
if self.requires_system_checks and not options["skip_checks"]:
check_kwargs = self.get_check_kwargs(options)
self.check(**check_kwargs)
if self.requires_migrations_checks:
self.check_migrations()
output = self.handle(*args, **options)
if output:
if self.output_transaction:
connection = connections[options.get("database", DEFAULT_DB_ALIAS)]
output = "%s\n%s\n%s" % (
self.style.SQL_KEYWORD(connection.ops.start_transaction_sql()),
output,
self.style.SQL_KEYWORD(connection.ops.end_transaction_sql()),
)
self.stdout.write(output)
return output
核心运行是这个:
output = self.handle(*args, **options)
继续查看handle实现
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"
)
这里就是占位,里面说了BaseCommand子类需要提供handle()方法,不就重写吗,如果子类调用到了这个方法,抛出异常。根据MRO查找,在 django.core.management.commands.runserver.Command类中有这个方法:
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.")
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)
代码解析:
- ALLOWED_HOSTS校验
if not settings.DEBUG and not settings.ALLOWED_HOSTS:
raise CommandError("You must set settings.ALLOWED_HOSTS if DEBUG is False.")
如果settings.py中配置的变量DEBUG为False,如果没有配置ALLOWED_HOSTS,程序抛出异常。
- ipv6设置
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
- 设置默认端口(如果没指定) 如果addrport没有设置,就是用默认值,这个默认值在django.core.management.commands.runserver.Command类中有定义,默认端口8000
if not options["addrport"]:
self.addr = ""
self.port = self.default_port
接着代码进入else
- else {'addrport': '8000'}
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)
self.addr, _ipv4, _ipv6, _fqdn, self.port = m.groups()
(1)通过re.match校验addrport是否与naiveip_re匹配 (2)self.port.isdigit() 校验端口是否是数字字符串 (3)解析出地址端口
后面一段就是跟ipv6相关了,我们直接看最后self.run(**options)
查看run()方法的代码:
def run(self, **options):
"""Run the server, using the autoreloader if needed."""
use_reloader = options["use_reloader"]
if use_reloader:
autoreload.run_with_reloader(self.inner_run, **options)
else:
self.inner_run(None, **options)
options["use_reloader"]里值为True,当执行 python manage.py runserver 时,Django 的参数解析器(argparse)在后台会自动为这个选项注入默认值 True,可以通过"python manage.py runserver --noreload" 所以上面会执行" autoreload.run_with_reloader(self.inner_run, **options)" 查看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
代码解析: 由于首次运行,代码进入else内
else:
exit_code = restart_with_reloader()
sys.exit(exit_code)
查看restart_with_reloader()代码:
def restart_with_reloader():
new_environ = {**os.environ, DJANGO_AUTORELOAD_ENV: "true"}
orig = getattr(sys, "orig_argv", ())
if any(
(arg == "-u")
or (
arg.startswith("-")
and not arg.startswith(("--", "-X", "-W"))
and len(arg) > 2
and arg[1:].isalpha()
and "u" in arg
)
for arg in orig[1:]
):
new_environ.setdefault("PYTHONUNBUFFERED", "1")
args = get_child_arguments()
while True:
p = subprocess.run(args, env=new_environ, close_fds=False)
if p.returncode != 3:
return p.returncode
- 重新构建环境变量 os.environ拿到的是系统环境变量,在这个之上新增了DJANGO_AUTORELOAD_ENV: "true",这就是初次会进入restart_with_reloader()函数原因。
new_environ = {**os.environ, DJANGO_AUTORELOAD_ENV: "true"}
- 获取原始参数列表 ['python', 'manage.py', 'runserver', '8000']
orig = getattr(sys, "orig_argv", ())
- 校验原始参数列表 使用for循环遍历arg(除第一个),满足any里两个外层()条件之一的往新变量字典里塞("PYTHONUNBUFFERED", "1"),我们命令是不满足的,下面循环后new_environ没变化。
if any(
(arg == "-u")
or (
arg.startswith("-")
and not arg.startswith(("--", "-X", "-W"))
and len(arg) > 2
and arg[1:].isalpha()
and "u" in arg
)
for arg in orig[1:]
):
new_environ.setdefault("PYTHONUNBUFFERED", "1")
- 获取args参数
args = get_child_arguments()
get_child_arguments就不解析了,直接拿源码描述:
Return the executable. This contains a workaround for Windows if the
executable is reported to not have the .exe extension which can cause bugs
on reloading.
获取当前运行的 Python 解释器路径,并专门修复 Windows 环境下缺少 .exe 后缀的 Bug
- 开启一个子进程
while True:
p = subprocess.run(args, env=new_environ, close_fds=False)
if p.returncode != 3:
return p.returncode
父进程通过一个 while True 死循环,不断地监视、拉起和重建子进程。只要子进程是因为“代码修改”而退出的,父进程就会立刻重新拉起一个新的子进程。 (1)subprocess.run(args, env=new_environ, close_fds=False) 通过修改源码打印测试,第一次启动args是: ['/Users/xxx/miniforge3/envs/django-test/bin/python', 'manage.py', 'runserver', '8000'] env: 环境变量PATH路径 所以子进程会再次执行我们执行过的启动命令*
- args:就是我们前面讨论的、由 get_executable() 和 get_child_arguments() 组合而成的完整命令行参数列表(例如:['python', 'manage.py', 'runserver'])。
- env=new_environ:这里传入了带有 DJANGO_AUTORELOAD_ENV="true" 的新环境变量,用来告诉子进程:“你现在是子进程了,去启动 Django 业务并监控文件吧”。
- close_fds=False:在 Windows 和类 Unix 系统上运行 subprocess 时,不强制关闭继承的文件描述符(File Descriptors)。这对于某些底层 socket 连接或标准输入输出的继承非常关键。
- 阻塞特性:subprocess.run 是阻塞的。父进程运行到这里会停住,一直等待子进程运行。
(2)if p.returncode != 3 状态码 3 代表"检测到文件修改,请求父进程重启子进程"的信号。
子进程会执行同样的命令,再次来到run_with_reloader()函数,此次if条件满足,因为在父进中已经将环境变量DJANGO_AUTORELOAD_ENV设置为True,回到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
首先通过 get_reloader()获取reloader,get_reloader()里:
def get_reloader():
"""Return the most suitable reloader for this environment."""
try:
WatchmanReloader.check_availability()
except WatchmanUnavailable:
return StatReloader()
return WatchmanReloader()
reloader就是一个文件监控器(Reloader),子进程最后运行到这里: start_django(reloader, main_func, *args, **kwargs)
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)
(1)确保终端回显模式 start_django里首先执行了ensure_echo_on(),其代码如下: 确保终端的“回显模式(Echo Mode)”是开启的,防止因为 PDB(Python 调试器)等工具异常退出或重载,导致你在终端里敲键盘时“看不到自己输入的字
def ensure_echo_on():
"""
Ensure that echo mode is enabled. Some tools such as PDB disable
it which causes usability issues after reload.
"""
if not termios or not sys.stdin.isatty():
return
attr_list = termios.tcgetattr(sys.stdin)
if not attr_list[3] & termios.ECHO:
attr_list[3] |= termios.ECHO
if hasattr(signal, "SIGTTOU"):
old_handler = signal.signal(signal.SIGTTOU, signal.SIG_IGN)
else:
old_handler = None
termios.tcsetattr(sys.stdin, termios.TCSANOW, attr_list)
if old_handler is not None:
signal.signal(signal.SIGTTOU, old_handler)
(2)check_errors装饰被调用的函数 check_errors是一个装饰函数,如果main_func执行失败,会打印相关错误
main_func = check_errors(main_func)
(3)开启线程执行main_func 子进程开启了一个线程django_main_thread并且开启了线程守护,所以如果子进程结束,线程也会结束,如果代码有改动,原来子进程会杀死,线程也会结束,由于外层父进程是在while 循环,除非子进程返回结果代码不是3,父进程会一直开启新的子进程。
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()
在上层 autoreload.run_with_reloader(self.inner_run, options),所以最终执行的是上层传进来的self.inner_run,args是options。 self.inner_run方法在django.core.management.commands.runserver.Command类 中定义:
def inner_run(self, *args, **options):
# If an exception was silenced in ManagementUtility.execute in order
# to be raised in the child process, raise it now.
autoreload.raise_last_exception()
threading = options["use_threading"]
# 'shutdown_message' is a stealth option.
shutdown_message = options.get("shutdown_message", "")
if not options["skip_checks"]:
self.stdout.write("Performing system checks...\n\n")
check_kwargs = super().get_check_kwargs(options)
check_kwargs["display_num_errors"] = True
self.check(**check_kwargs)
# Need to check migrations here, so can't use the
# requires_migrations_check attribute.
self.check_migrations()
# Close all connections opened during migration checking.
for conn in connections.all(initialized_only=True):
conn.close()
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:
# Use helpful error messages instead of ugly tracebacks.
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)
# Need to use an OS exit because sys.exit doesn't work in a thread
os._exit(1)
except KeyboardInterrupt:
if shutdown_message:
self.stdout.write(shutdown_message)
sys.exit(0)
我们直接找到跟运行相关的那段:
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,
)
先解析下几个关键参数: (1) self.addr 这个是地址,默认是127.0.0.1 (2) self.port 运行端口,默认8000,写了就覆盖 (3)handler 暂时称为一个对象或者什么函数(其实就是WSGI的application,往下看) (6)ipv6 ipv6地址 (7)on_bind on_bind是一个函数,服务器在**成功绑定网络端口(如 8000 端口)后,负责在终端打印那段我们最熟悉的“启动欢迎词和警告信息”**的底层函数。
(8)server_cls 服务类型,默认是WSGIServer
run是这样引入的:
from django.core.servers.basehttp import WSGIServer, get_internal_wsgi_application, run
找到run源码:
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 indicates how threads will behave on an
# abrupt shutdown; like quitting the server by the user or restarting
# by the auto-reloader. True means the server will not wait for thread
# termination before it quits. This will make auto-reloader faster
# and will prevent the need to kill the server manually if a thread
# isn't terminating correctly.
httpd.daemon_threads = True
httpd.set_app(wsgi_handler)
httpd.serve_forever()
这个不就是在启动WSGI吗?回顾wsgiref源码分析:
def make_server(
host, port, app, server_class=WSGIServer, handler_class=WSGIRequestHandler
):
"""Create a new WSGI server listening on `host` and `port` for `app`"""
server = server_class((host, port), handler_class)
server.set_app(app)
return server
在make_server函数里,使用server_class=WSGIServer类,handler_class=WSGIRequestHandler 创建了WSGIServer类实例server(同时建立socket端口监听),通过server.set_app(app)注册application,最后server.serve_forever()启动请求监听。
那runfan方法里的wsgi_handler不就是WSGI里那个需要自己定义的application吗?
到这里整个运行流程就完成了
其他问题的探讨
(1) 父进程与子进程是否真的存在?
根据我们上面分析,在run_with_reloader()里,子进程运行时DJANGO_AUTORELOAD_ENV=="true"为True,进入if;父进程则进入else,我们在如下位置写print测试:
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":
print(f"当前pid是:{os.getpid()},父进程pid是:{os.getppid()},我在run_with_reloader函数里")
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
运行结果
当前pid是:19347,父进程pid是:9503,我在else里
当前pid是:19348,父进程pid是:19347,我在if里
说明父进程与子进程是存在,这就是我们在写代码时,有些内容总是打印两次(如果写在启动过程会执行的代码里),因为父进程与子进程启动过程是一样的,用的是同一个命令。启动后打印的信息是由子进程开启的线程打印的,父进程没有启动WSGI,不会打印启动后的信息。
评论
0 条