Django源码分析 runserver(二)

我们继续接着上一篇进行分析https://www.cnblogs.com/zhoubin50/articles/14361878.html

继续看ManagementUtility的execute最后部分

class ManagementUtility:
    """
    Encapsulate the logic of the django-admin and manage.py utilities.
    """
    def __init__(self, argv=None):
        self.argv = argv or sys.argv[:]
        self.prog_name = os.path.basename(self.argv[0])
        if self.prog_name == '__main__.py':
            self.prog_name = 'python -m django'
        self.settings_exception = Nonedef 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(usage='%(prog)s subcommand [options] [args]', add_help=False, allow_abbrev=False) parser.add_argument('--settings') parser.add_argument('--pythonpath') parser.add_argument('args', nargs='*') # catch-all try: options, args = parser.parse_known_args(self.argv[2:]) handle_default_options(options) except CommandError: pass # Ignore any option errors at this point. try: settings.INSTALLED_APPS except ImproperlyConfigured as exc: self.settings_exception = exc except ImportError as exc: self.settings_exception = exc
if settings.configured: # Start the auto-reloading dev server even if the code is broken. # The hardcoded condition is a code smell but we can't rely on a # flag on the command class because we haven't located it yet. if subcommand == 'runserver' and '--noreload' not in self.argv: try: autoreload.check_errors(django.setup)() except Exception: # The exception will be raised later in the child process # started by the autoreloader. Pretend it didn't happen by # loading an empty list of applications. apps.all_models = defaultdict(OrderedDict) apps.app_configs = OrderedDict() apps.apps_ready = apps.models_ready = apps.ready = True # Remove options not compatible with the built-in runserver # (e.g. options for the contrib.staticfiles' runserver). # Changes here require manually testing as described in # #27522. _parser = self.fetch_command('runserver').create_parser('django', 'runserver') _options, _args = _parser.parse_known_args(self.argv[2:]) for _arg in _args: self.argv.remove(_arg) # In all other cases, django.setup() is required to succeed. else: django.setup() self.autocomplete()
# 上面代码的分析可以看上一篇文章的分析,我们这里开始分析fetch_command
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:
# 获取到runserver对应的Command类,实例化后调用run_from_argv方法 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
        # 获取所有命令{'migrate':'django.core','runserver':'django.contrib.staticfiles',...}
commands = get_commands() try:
# 获取app名字 app_name
= commands[subcommand] except KeyError: if os.environ.get('DJANGO_SETTINGS_MODULE'): # If `subcommand` is missing due to misconfigured settings, the # following line will retrigger an ImproperlyConfigured exception # (get_commands() swallows the original one) so the user is # informed about it. settings.INSTALLED_APPS else: sys.stderr.write("No Django settings specified.\n") possible_matches = get_close_matches(subcommand, commands) sys.stderr.write('Unknown command: %r' % subcommand) if possible_matches: sys.stderr.write('. Did you mean %s?' % possible_matches[0]) sys.stderr.write("\nType '%s help' for usage.\n" % self.prog_name) sys.exit(1) if isinstance(app_name, BaseCommand): # If the command is already loaded, use it directly. klass = app_name else:
# 导入命令对应的模块并实例化模块中的Command类,这里kclass是对应子命令Command类的实例
# 参数app_name值为django.contrib.staticfiles,subcommand值为runserver,
# 字符串凭拼接导入模块则是django.contrib.staticfiles.management.commands.runserver
klass
= load_command_class(app_name, subcommand) return klass @functools.lru_cache(maxsize=None) 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) If a specific version of a command must be loaded (e.g., with the startapp command), the instantiated module can be placed in the dictionary in place of the application name. The dictionary is cached on the first call and reused on subsequent calls. """ # 先获取django.core.management.commands下的所有命令
commands
= {name: 'django.core' for name in find_commands(__path__[0])}
# 如果配置没有,则直接返回上面遍历到的命令,一般是在我们没有创建项目时会出现这种情况
if not settings.configured: return commands
# 如果已经有配置文件,会走到这里,反向遍历所有配置的app,搜索app下management.commands下的所有命令
# 如果我们的app需要自定义一些命令,可以在app下创建management包,再创建commands包,在commands中新建python文件,

# 文件名即为子命令,命令继承django.core.management.base.BaseCommand,文件名称不要和django自带命令重名
# 最终返回类似这样{'migrate':'django.core','runserver':'django.contrib.staticfiles',...}

for app_config in reversed(list(apps.get_app_configs())): path = os.path.join(app_config.path, 'management') commands.update({name: app_config.name for name in find_commands(path)}) return commands

fetch_command最后调用了run_from_argv,即django.contrib.staticfiles.management.commands.runserver.Command这个类实例化后调用run_from_argv,

这个类继承自django.core.management.commands.runserver.Command类,该类继承自django.core.management.base.BaseCommand

代码如下,run_from_argv在BaseCommand中

# django.contrib.staticfiles.management.commands.runserver.Command
from
django.core.management.commands.runserver import ( Command as RunserverCommand, ) class Command(RunserverCommand): help = "Starts a lightweight Web server for development and also serves static files."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 # django.core.management.commands.runserver.Command class Command(BaseCommand): help = "Starts a lightweight Web server for development." # Validation is called explicitly each time the server is reloaded. requires_system_checks = False stealth_options = ('shutdown_message',) default_addr = '127.0.0.1' default_addr_ipv6 = '::1' default_port = '8000' protocol = 'http' server_cls = WSGIServerdef execute(self, *args, **options): if options['no_color']: # We rely on the environment because it's currently the only # way to reach WSGIRequestHandler. This seems an acceptable # compromise considering `runserver` runs indefinitely. os.environ["DJANGO_COLORS"] = "nocolor" super().execute(*args, **options) def get_handler(self, *args, **options): """Return the default WSGI handler for the runner.""" return get_internal_wsgi_application() def handle(self, *args, **options):
# DEBUG不是False时必须设置ALLOWED_HOSTS
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
# 调用run方法运行 self.run(
**options) def run(self, **options): """Run the server, using the autoreloader if needed.""" use_reloader = options['use_reloader']
# 执行命令的时候没加--noreload所以这里use_reloader是True
if use_reloader:
# 运行的时候会自动重新加载 autoreload.run_with_reloader(self.inner_run,
**options) else: self.inner_run(None, **options) 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', '') quit_command = 'CTRL-BREAK' if sys.platform == 'win32' else 'CONTROL-C' self.stdout.write("Performing system checks...\n\n") self.check(display_num_errors=True) # Need to check migrations here, so can't use the # requires_migrations_check attribute. self.check_migrations() now = datetime.now().strftime('%B %d, %Y - %X') self.stdout.write(now) self.stdout.write(( "Django version %(version)s, using settings %(settings)r\n" "Starting development server at %(protocol)s://%(addr)s:%(port)s/\n" "Quit the server with %(quit_command)s.\n" ) % { "version": self.get_version(), "settings": settings.SETTINGS_MODULE, "protocol": self.protocol, "addr": '[%s]' % self.addr if self._raw_ipv6 else self.addr, "port": self.port, "quit_command": quit_command, }) try: handler = self.get_handler(*args, **options) run(self.addr, int(self.port), handler, ipv6=self.use_ipv6, threading=threading, server_cls=self.server_cls) except socket.error 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)
# django.core.management.base.BaseCommand
class BaseCommand: # Metadata about this command. help = '' # Configuration shortcuts that alter various logic. _called_from_command_line = False output_transaction = False # Whether to wrap the output in a "BEGIN; COMMIT;" requires_migrations_checks = False requires_system_checks = True # Arguments, common to all commands, which aren't defined by the argument # parser. base_stealth_options = ('skip_checks', 'stderr', 'stdout') # Command-specific options not defined by the argument parser. stealth_options = () 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.ERRORdef 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 Exception as e: if options.traceback or not isinstance(e, CommandError): 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(1) finally: try: connections.close_all() except ImproperlyConfigured: # Ignore if connections aren't setup at this point (e.g. no # configured settings). pass 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'], self.stderr.style_func)

        if self.requires_system_checks and not options.get('skip_checks'):
            self.check()
        if self.requires_migrations_checks:
            self.check_migrations()
        # 主要起作用的代码,调用handle方法,该方法需要子类来实现
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 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')

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__)
# 在线程中运行主程序inner_run start_django(reloader, main_func,
*args, **kwargs) else: exit_code = restart_with_reloader() sys.exit(exit_code) except KeyboardInterrupt: pass
    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', '')
        quit_command = 'CTRL-BREAK' if sys.platform == 'win32' else 'CONTROL-C'

        self.stdout.write("Performing system checks...\n\n")
        self.check(display_num_errors=True)
        # Need to check migrations here, so can't use the
        # requires_migrations_check attribute.
        self.check_migrations()
        now = datetime.now().strftime('%B %d, %Y - %X')
        self.stdout.write(now)
        self.stdout.write((
            "Django version %(version)s, using settings %(settings)r\n"
            "Starting development server at %(protocol)s://%(addr)s:%(port)s/\n"
            "Quit the server with %(quit_command)s.\n"
        ) % {
            "version": self.get_version(),
            "settings": settings.SETTINGS_MODULE,
            "protocol": self.protocol,
            "addr": '[%s]' % self.addr if self._raw_ipv6 else self.addr,
            "port": self.port,
            "quit_command": quit_command,
        })

        try:
# 获取wsgi处理器,同时加载middleware handler
= self.get_handler(*args, **options)
# 这里真正运行服务 run(self.addr, int(self.port), handler, ipv6
=self.use_ipv6, threading=threading, server_cls=self.server_cls) except socket.error 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)

django.contrib.management.commands.runserver.Command.get_handler代码如下

    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.
        """
# 调用父类get_handler获取默认wsgi应用 handler = super().get_handler(*args, **options) use_static_handler = options['use_static_handler'] insecure_serving = options['insecure_serving']
# 这里我们命令没有加参数--nostatic和--insecure,开发环境下DEBUG为True,下面的if判断为true,最终返回StaticFilesHandler
# 多了对静态文件的请求支持
if use_static_handler and (settings.DEBUG or insecure_serving):
return StaticFilesHandler(handler) return handler

最后看下run(self.addr, int(self.port), handler,ipv6=self.use_ipv6, threading=threading, server_cls=self.server_cls),

django.core.servers.basehttp.run代码如下,这里服务拉起来了

def run(addr, port, wsgi_handler, ipv6=False, threading=False, 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 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()

 

现在梳理一下整个流程

在我们执行python manage.py runserver 8000的时候

1.manage.py调用execute_from_command_line,

2.execute_from_command_line中实例化ManagementUtility,调用execute方法

3.execute方法对命令行进行解析,发现是runserver子命令,会调用django.setup, django.setup先设置日志参数,然后调用Apps实例的populate方法,初始化INSTALLED_APPS中所有的app(app_config)并导入app模块;2.导入app下的模型即models;3.执行app_config的ready方法

4.调用fetch_command().run_from_argv(),找到子命令对应的Command类,执行BaseCommand类的run_from_argv,先获取wsgi处理器并加载midleware,最后启动服务

 

小结:

启动源码暂时先分析到这里,通过分析源码,了解了一些之前不知道的东西。1.apps文件中中可以通过定义AppConfig类的ready方法,在app加载后做一些我们想做的事情。2.app可以自定义命令,在app下创建management包,再在management包中创建commands包,在commands中创建python文件来定义我们的命令,子命令名为文件名,类名必须是Command,且要继承django.core.management.base.BaseCommand

下一篇分析下django是如何实现wsgi应用的

第一次分析源码,还有许多不足之处,欢迎指正

posted @ 2021-02-04 14:44  小麦香茶  阅读(240)  评论(0)    收藏  举报