django源码解读django-admin startproject 命令
解析django-admin startproject 命令执行的过程
1. 在django 3版本中,django 源码下有一个bin 文件,这个文件夹下有一个django-admin.py 的 文件,但是在django 4 版本中,去掉这个文件,改成了django-admin.exe , 在 Scripts 这个文件夹下。2. 所以需要创建在 django下面创建一个bin/django-admin.py 文件用来执行命令。
# django-admin.py
#!/usr/bin/env python
from django.core import management
if __name__ == "__main__":
management.execute_from_command_line()
3. 进入 execute_from_command_line()
def execute_from_command_line(argv=None):
"""Run a ManagementUtility."""
utility = ManagementUtility(argv)
utility.execute()
- 这里调用的ManagementUtility中execute() 函数
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] # 例如 startproject
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:]) # 返回一个Namespace对象,和 列表, options = Namespace(settings=None, pythonpath=None, args=['main', 'C:\\Users\\liuzz10\\PycharmProjects\\django4.0源码完全解读\\second'])
handle_default_options(options) # 这里没有执行
except CommandError:
pass # Ignore any option errors at this point.
try:
settings.INSTALLED_APPS # 尝试导入APP
except ImproperlyConfigured as exc:
self.settings_exception = exc
except ImportError as exc:
self.settings_exception = exc
if settings.configured: # 如果settings配置已经设置,返回True
# 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() # 这里不执行,它会去判断 "DJANGO_AUTO_COMPLETE" not in os.environ 就会return 返回
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) # 直接执行 fetch_command()
# 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() # 值使用的django.core 这个是写死的, 进入这个函数就可以看见,还有几个命令,获取settings.py中注册APP中获取路径,基本就是查看注册APP路径下management\commands中是否有文件名和命令相同的文件,就将django.core 改成 导入包settings.py 中APP注册的值。
try:
#commands 是一个字典,例如:{'check':'django.core'} 得到 'django.core'
"""
{'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',
'optimizemigration': 'django.core', 'runserver': 'django.contrib.staticfiles',
'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', 'collectstatic': 'django.contrib.staticfiles',
'findstatic': 'django.contrib.staticfiles', 'clearsessions': 'django.contrib.sessions',
'remove_stale_contenttypes': 'django.contrib.contenttypes', 'changepassword': 'django.contrib.auth',
'createsuperuser': 'django.contrib.auth'}
"""
app_name = commands[subcommand] # 字典获取startproject 对应的 值 django.core
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 # 对应命令文件中的Command 类
# 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)) # 这里就是使用django.core 的地方,这个拼接起来就是导入包的路径
return module.Command() # 返回包中的Command() 对象,返回再看fetch_command().run_from_argv()
# run_from_argv()
def run_from_argv(self, argv): # args ['C:/Users/liuzz10/PycharmProjects/django4.0源码完全解读/django/bin/django-admin.py', 'startproject', 'main', 'C:\\Users\\liuzz10\\PycharmProjects\\django4.0源码完全解读\\second']
"""
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) # 尝试执行命令,这里会执行handle() 命令,这里继承 TemplateCommand 并重写 handle 函数最终还是继承TemplateCommand 中 handle()
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
django\core\management\templates.py
def handle(self, app_or_project, name, target=None, **options):
self.written_files = []
self.app_or_project = app_or_project # project
self.a_or_an = "an" if app_or_project == "app" else "a"
self.paths_to_remove = []
self.verbosity = options["verbosity"]
self.validate_name(name) # 验证项目名称和文件夹
# if some directory is given, make sure it's nicely expanded
if target is None: # 这里的target 是创建项目的位置,这里为写 项目位置,这是创建项目的一个参数,就是最外层的文件夹,如果不存在,就会创建一个和项目名称相同的文件夹。
top_dir = os.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(os.path.expanduser(target)) # 项目最外层的文件夹的绝对地址。
if app_or_project == "app":
self.validate_name(os.path.basename(top_dir), "directory")
if not os.path.exists(top_dir): # 判断文件夹是都存在,如果不存在就会报错,让你去创建
raise CommandError(
"Destination directory '%s' does not "
"exist, please create it first." % top_dir
)
extensions = tuple(handle_extensions(options["extensions"])) # 更改扩展名,handle_extensions返回set类型 例如{.py}
extra_files = []
excluded_directories = [".git", "__pycache__"] # 排除的文件夹
for file in options["files"]:
extra_files.extend(map(lambda x: x.strip(), file.split(",")))
if exclude := options.get("exclude"): # 这里的 := 是赋值的意思
for directory in exclude:
excluded_directories.append(directory.strip())
if self.verbosity >= 2:
self.stdout.write(
"Rendering %s template files with extensions: %s"
% (app_or_project, ", ".join(extensions))
)
self.stdout.write(
"Rendering %s template files with filenames: %s"
% (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 != "_") # app_or_project 是 project ,camel_case_value 是 Main,创建的项目名
context = Context(
{
**options,
base_name: name,
base_directory: top_dir,
camel_case_name: camel_case_value,
"docs_version": get_docs_version(),
"django_version": django.__version__,
},
autoescape=False,
)
# 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) # 获取模板地址 'django\\conf\\project_template'
prefix_length = len(template_dir) + 1 # 这个用来获取最外层的文件夹地址,用来切片
for root, dirs, files in os.walk(template_dir): # os.walk 用来递归文件夹,获取文件,文件夹, root :'django\\conf\\project_template' dirs: ['project_template']# files: ['manage.py-tpl', '__init__.py'] 这是第一层遍历的结果
path_rest = root[prefix_length:] # 获取最外层文件夹地址
relative_dir = path_rest.replace(base_name, name) 将第二层文件名称,替换成项目名称。
if relative_dir: # 第一次为空,第二次为项目名
target_dir = os.path.join(top_dir, relative_dir)
os.makedirs(target_dir, exist_ok=True) # 这时就有项目的第二层文件夹了。
for dirname in dirs[:]: # 遍历所有的文件夹
if "exclude" not in options: # 这里不执行
if dirname.startswith(".") or dirname == "__pycache__":
dirs.remove(dirname)
elif dirname in excluded_directories:
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 = os.path.join(root, filename) # 'django\\conf\\project_template\\manage.py-tpl' 这是旧的模板文件
new_path = os.path.join(
top_dir, relative_dir, filename.replace(base_name, name) # 拼接新的文件地址 'second\\manage.py-tpl'
)
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 # 修改文件后缀名,将 '.py-tpl' 改成 .py
break # Only rewrite once
if os.path.exists(new_path): # 判断文件是否存在
raise CommandError(
"%s already exists. Overlaying %s %s into an existing "
"directory won't replace conflicting files."
% (
new_path,
self.a_or_an,
app_or_project,
)
)
# 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, encoding="utf-8") as template_file:
content = template_file.read()
template = Engine().from_string(content) # django使用模板引擎,将模板中变量替换,
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)
self.written_files.append(new_path)
if self.verbosity >= 2:
self.stdout.write("Creating %s" % new_path)
try:
self.apply_umask(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.")
for path_to_remove in self.paths_to_remove:
if os.path.isfile(path_to_remove):
os.remove(path_to_remove)
else:
shutil.rmtree(path_to_remove)
run_formatters(self.written_files)
manage.py-tpl
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', '{{ project_name }}.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()
就是替换其中的 project_name 这个就是最终生成 manage.py
在 django\conf\project_template 中 ,同级目录还有 app_project ,也是一样的模板替换。
在创建项目的 django4.0 不会报错,但是django4.1 之后的就会报错,但是项目还是创建成功了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix