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()

  1. 这里调用的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 之后的就会报错,但是项目还是创建成功了。

posted @ 2022-11-17 11:17  恰恰的故事  阅读(413)  评论(0编辑  收藏  举报