django商城项目源码_阅读源码 - django-admin.py startproject是怎么创建项目的?

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新项目建立完成。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值