![526fea7d92ef24ed3d44f53d801bdcf5.png](https://img-blog.csdnimg.cn/img_convert/526fea7d92ef24ed3d44f53d801bdcf5.png)
示例:
创建一个最小的django demo
$ django-admin.py startproject minidemo
这条命令有3个参数:
- django-admin.py
- startproject
- minidemo
解析参数django-admin.py
既然能执行django-admin.py这条命令,说明它一定在可执行命令目录下
执行which django-admin.py
可以看到django-admin.py在venv/bin/目录下
/Users/xxx/my_project/venv/bin/django-admin.py
查看此目录 ls venv/bin/
__pycache__ activate.csh django-admin easy_install iptest ipython pip pip3.6 python sqlformat
activate activate.fish django-admin.py easy_install-3.6 iptest3 ipython3 pip3 pygmentize python3
可以看到除了django-admin.py以外,还有django-admin也在
既然找到文件了,对比一下两者的代码
# django-admin
import re
import sys
from django.core.management import execute_from_command_line
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script.pyw?|.exe)?$', '', sys.argv[0])
sys.exit(execute_from_command_line())
# django-admin.py
from django.core import management
if __name__ == "__main__":
management.execute_from_command_line()
可以看到两者基本是一样的,除了前者会自动进行后缀补全。这就解释了为什么既可以用django-admin.py startproject xxx
也可以用django-admin startproject xxx
生成项目。
excute_from_command_line()
这个函数名字也非常清晰明了,从命令行开始执行,这里就是我们创建项目的入口
至此,第一个参数django-admin.py解析完毕。
解析execute_from_command_line()
# django-admin.py
from django.core import management
if __name__ == "__main__":
management.execute_from_command_line()
从上面的代码可以看到,当我们执行django-admin.py时,其实执行的是
management的execute_from_command_line()函数
我们进入execute_from_command_line()函数内部查看,函数源码在 django/core/management/__init__ 文件的第378行
def execute_from_command_line(argv=None):
"""Run a ManagementUtility."""
utility = ManagementUtility(argv)
utility.execute()
可以看到,excute_from_command_line()函数实例化了一个ManagementUtility()类,执行了excute()方法,这里代码分为
- 实例化
- 执行excute()
实例化
class ManagementUtility:
"""
Encapsulate the logic of the django-admin and manage.py utilities.
"""
def __init__(self, argv=None):
self.argv = argv or sys.argv[:]
# ['/Users/kaka/PycharmProjects/my_project/kk.py', 'startproject', 'minidemo']
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中,self.prog_name含义???待查
实例化完毕,即开始执行excute()方法
excute()
excute()方法在django/core/management/__init__ 文件的301行,粗略概览,此方法没有返回任何东西,所以它只是执行了IO操作,excute方法代码分为几大块,我将会在代码中进行注释。
def execute(self):
"""
给出命令行参数,识别出正在运行的是什么子命令,创建与命令对应的解析器,然后运行它
"""
# 取出第二个命令行参数,此处为startproject命令
try:
subcommand = self.argv[1]
except IndexError:
subcommand = 'help' # 如果没有找到命令,就显示help指令
# 解析是否有指定settings.py和Python路径
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
# 检查是否已经对settings进行了配置,configured是一个属性,在django.conf中,
# 如果进行了配置,表示已经建立了项目,应该寻找runserver这一类的命令了,此处我们忽略
# @property
# def configured(self):
# """Return True if the settings have already been configured."""
# return self._wrapped is not empty
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()
# 如果命令是help,略
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.
# 如果命令是version或--version,略
elif subcommand == 'version' or self.argv[1:] == ['--version']:
sys.stdout.write(django.get_version() + 'n')
# 如果命令是-h或--help,略
elif self.argv[1:] in (['--help'], ['-h']):
sys.stdout.write(self.main_help_text() + 'n')
# 终于到了我们真正执行IO的地方了
else:
# 此处subcommand是startproject,fetch_command函数是
self.fetch_command(subcommand).run_from_argv(self.argv)
通过对excute()方法的拆解,我们发现,大部分代码和我们这次分析无关,真正有关的是这一行
self.fetch_command(subcommand).run_from_argv(self.argv)
我们再次把这条命令拆成self.fetch_command(subcommand)
和run_from_argv(self.argv)
两部分来看
excute()方法分为两步:
- self.fetch_command(subcommand)
- run_from_argv(self.argv)
一、 self.fetch_command(subcommand)
Fetch_command方法的代码在django/core/management目录下的__init__文件195行。
本方法有返回,为一个类,所以我们查看返回了什么类
def fetch_command(self, subcommand):
"""
查找获取给予的命令
subcommand为startproject
"""
commands = get_commands()
# commands为如下的一个字典
# <class 'dict'>: {
# 'check': 'django.core',
# 'compilemessages': 'django.core',
# 'createcachetable': 'django.core',
# 'dbshell': 'django.core',
# 'diffsettings': 'django.core',
# 'dumpdata': 'django.core',
# 'flush': 'django.core',
# 'inspectdb': 'django.core',
# 'loaddata': 'django.core',
# 'makemessages': 'django.core',
# 'makemigrations': 'django.core',
# 'migrate': 'django.core',
# 'runserver': 'django.core',
# 'sendtestemail': 'django.core',
# 'shell': 'django.core',
# 'showmigrations': 'django.core',
# 'sqlflush': 'django.core',
# 'sqlmigrate': 'django.core',
# 'sqlsequencereset': 'django.core',
# 'squashmigrations': 'django.core',
# 'startapp': 'django.core',
# 'startproject': 'django.core',
# 'test': 'django.core',
# 'testserver': 'django.core'
# }
# 获取获取要安装的django app,如果没传,就是安装django核心部分
try:
app_name = commands[subcommand]
# app_name为从上述字典中取出键为startproject的值,即django.core,
# 具体含义就是安装django核心部分
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): # False,不是实例
# If the command is already loaded, use it directly.
klass = app_name
else:
# 此处是我们本次要执行的,要返回的类在这里
klass = load_command_class(app_name, subcommand)
# klass = load_command_class(django.core, startproject)
return klass
查看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))
# import_module函数的说明是"Import a module."这里知道它是导入了一个模块就行,不深究了,略
# module补全后为'django.core.management.commands.startproject',所以导入的是
# django.core.management.commands目录下的startproject.py模块,此模块下有一个Command()类
return module.Command()
# 返回的是django项目中 django/core/management/commands/startproject.py文件中的Command()类
到这里,我们可以明白self.fetch_command(subcommand)这部分代码返回的是startproject.py中的Command类,所以self.fetch_command(subcommand).run_from_argv(self.argv)
可以直接替换为Command.run_from_argv(self.argv)
参数self.argv还是我们之前分析的那个列表:
<class 'list'>: ['/Users/kaka/PycharmProjects/my_project/kk.py', 'startproject', 'minidjango']
分析完前半部分,我们再来分析run_from_argv函数
二、 run_from_argv(self.argv)
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.
"""
# 创建一个解析器
# CommandParser(prog='kk.py startproject', usage=None, description='Creates a Django project directory structure for the given project name in the current directory or optionally in the given directory.', formatter_class=<class 'django.core.management.base.DjangoHelpFormatter'>, conflict_handler='error', add_help=True)
self._called_from_command_line = True
parser = self.create_parser(argv[0], argv[1])
# 可选项,如 --setting之类,略
options = parser.parse_args(argv[2:])
cmd_options = vars(options)
# option相关,略
# Move positional args out of options to mimic legacy optparse
args = cmd_options.pop('args', ())
handle_default_options(options)
try:
# 此处是关键,又一个excute()方法,这个excute()方法是BaseCommand类的执行方法,
# 我们查看详细代码
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
通过代码分析,我们可以看到,run_from_argv(self.argv)
关键在于 self.execute(*args, **cmd_options)
这一句代码
此处有一个地方需要注意,类的继承关系,self.fetch_command(subcommand).run_from_argv(self.argv)
代码,它可以换为Command.run_from_argv(self.argv)
,Command继承于TemplateCommand,TemplateCommand继承于BaseCommand。
Command类和TemplateCommand类中均没有excute()方法,所以执行的是BaseCommand类中的excute()方法
BaseCommand类的excute()方法
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).
"""
# option可选相关,各种check,略
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(),所有的项目建立代码都在handle()方法里,这里才是核心
# 位于django/core/management/base.py的364行
output = self.handle(*args, **options)
# output也略
if output:
if self.output_transaction:
connection = connections[options.get('database', DEFAULT_DB_ALIAS)]
output = '%sn%sn%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
self.handle(args, *options)
如果你直接跟着Pycharm点进去查看handle方法,会发现啥都没做,只raise了一个错误,因为Pycharm会把你直接带到BaseCommand的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')
真正应该看的是下面的handle()
Command类的handle()
# startproject.py文件中的Command类的handle方法
def handle(self, **options):
project_name = options.pop('name')
# project_name即为我们传的minidemo
target = options.pop('directory')
# target是目标目录,但是此处为None,应该在其他地方会有指定
# Create a random SECRET_KEY to put it in the main settings.
options['secret_key'] = get_random_secret_key()
# 生成一个随机secret_key,我们settings.py中就是从这里来的
# 执行父类的handle方法,即TemplateCommand类的handle()方法
super().handle('project', project_name, target, **options)
TemplateCommand类的handle()
def handle(self, app_or_project, name, target=None, **options):
# 判断是生成app还是project
self.app_or_project = app_or_project
self.paths_to_remove = []
self.verbosity = options['verbosity']
# 校验名称合法性
self.validate_name(name, app_or_project)
# 此处开始指定项目要生成到哪里了,之前没有指定的target在此处指定
# if some directory is given, make sure it's nicely expanded
if target is None:
# top_dir即指定的项目目录
top_dir = path.join(os.getcwd(), name)
try:
# 创建项目目录
os.makedirs(top_dir)
except FileExistsError:
raise CommandError("'%s' already exists" % top_dir)
except OSError as e:
raise CommandError(e)
else:
top_dir = os.path.abspath(path.expanduser(target))
if not os.path.exists(top_dir):
raise CommandError("Destination directory '%s' does not "
"exist, please create it first." % top_dir)
# 额外创建的py文件,此处无,略
extensions = tuple(handle_extensions(options['extensions']))
extra_files = []
for file in options['files']:
extra_files.extend(map(lambda x: x.strip(), file.split(',')))
if self.verbosity >= 2:
self.stdout.write("Rendering %s template files with "
"extensions: %sn" %
(app_or_project, ', '.join(extensions)))
self.stdout.write("Rendering %s template files with "
"filenames: %sn" %
(app_or_project, ', '.join(extra_files)))
base_name = '%s_name' % app_or_project
base_subdir = '%s_template' % app_or_project
base_directory = '%s_directory' % app_or_project
camel_case_name = 'camel_case_%s_name' % app_or_project
camel_case_value = ''.join(x for x in name.title() if x != '_')
# 指定django项目建立时需要的上下文
context = Context({
**options,
base_name: name, # project_name
base_directory: top_dir, # project_directory
camel_case_name: camel_case_value,
'docs_version': get_docs_version(), # 2.2
'django_version': django.__version__, # 2.2.1
}, autoescape=False)
# 如果是新建项目,自动按默认配置configure,然后执行setup()函数
# Setup a stub settings environment for template rendering
if not settings.configured:
settings.configure()
django.setup()
template_dir = self.handle_template(options['template'],
base_subdir)
# template_dir为 'venv/lib/python3.6/site-packages/django/conf/project_template'目录
# 目录结构为:
# project_name
# __init__.py-tpl
# settings.py-tpl
# urls.py-tpl
# wsgi.py-tpl
# manage.py-tpl
# 可以看出,这就是一个初始新项目的结构
prefix_length = len(template_dir) + 1
*********************************** 项目文件最终在此处建立 *********************************************************
# 在project_template模板目录下遍历,创建需要的文件
for root, dirs, files in os.walk(template_dir):
path_rest = root[prefix_length:]
relative_dir = path_rest.replace(base_name, name)
if relative_dir:
target_dir = path.join(top_dir, relative_dir)
if not path.exists(target_dir):
os.mkdir(target_dir)
for dirname in dirs[:]:
if dirname.startswith('.') or dirname == '__pycache__':
dirs.remove(dirname)
for filename in files:
if filename.endswith(('.pyo', '.pyc', '.py.class')):
# Ignore some files as they cause various breakages.
continue
old_path = path.join(root, filename)
new_path = path.join(top_dir, relative_dir,
filename.replace(base_name, name))
for old_suffix, new_suffix in self.rewrite_template_suffixes:
if new_path.endswith(old_suffix):
new_path = new_path[:-len(old_suffix)] + new_suffix
break # Only rewrite once
if path.exists(new_path):
raise CommandError("%s already exists, overlaying a "
"project or app into an existing "
"directory won't replace conflicting "
"files" % new_path)
# Only render the Python files, as we don't want to
# accidentally render Django templates files
if new_path.endswith(extensions) or filename in extra_files:
with open(old_path, 'r', encoding='utf-8') as template_file:
content = template_file.read()
template = Engine().from_string(content)
content = template.render(context)
with open(new_path, 'w', encoding='utf-8') as new_file:
new_file.write(content)
else:
shutil.copyfile(old_path, new_path)
if self.verbosity >= 2:
self.stdout.write("Creating %sn" % new_path)
try:
shutil.copymode(old_path, new_path)
self.make_writeable(new_path)
except OSError:
self.stderr.write(
"Notice: Couldn't set permission bits on %s. You're "
"probably using an uncommon filesystem setup. No "
"problem." % new_path, self.style.NOTICE)
if self.paths_to_remove:
if self.verbosity >= 2:
self.stdout.write("Cleaning up temporary files.n")
for path_to_remove in self.paths_to_remove:
if path.isfile(path_to_remove):
os.remove(path_to_remove)
else:
shutil.rmtree(path_to_remove)
至此,一个新项目建立完成。
总结
Django通过命令交互代码获取我们输入的命令参数,对其进行判断,是建立项目还是建立app,再通过调用django/core/management目录下对应命令模块中Command类进行项目建立,项目建立过程中通过调用Command类的handle()方法和父类TemplateCommand类的handle()方法查找到对应的模板文件,进行IO操作写入初始文件,到这里,一个Django新项目建立完成。