- 深入理解Django:框架内幕与实现原理
- 沈聪 全树强编著
- 1934字
- 2022-05-06 12:24:00
2.3 shell命令的实现原理
在测试模型(Model)中进行增初改查时经常会用到Django中的shell命令。下面基于前文创建的first_django项目给出一个简单的shell操作示例,之后根据相应的现象提出问题,并通过源码追踪的方式解答这些问题。在first_django项目下运行python manage.py shell命令会报错:
错误提示非常清楚,要求安装相应数据库的客户端模块。由于默认使用SQLite3作为数据库,而在该Linux系统上并没有安装相应的SQLite3客户端模块,所以抛出异常。与第1章一样,笔者使用内部搭建好的一个MySQL数据库,并在本机上安装mysqlclient模块。修改settings.py文件中数据库相关的配置如下:
使用startapp命令创建一个shell_test应用(测试用):
在创建shell_test应用后,会生成若干文件,如下:
其中,在models.py文件中保存的是模型类,这里简单创建一个Django图书的模型类:
为了使Django能管理shell_test应用,需要在settings.py文件的INSTALLED_APPS列表中添加该应用:
然后针对shell_test应用对数据库进行迁移操作,将DjangoBooks类映射到具体的数据库表中:
这时就可以在数据库中看到和DjangoBooks类对应的django_books表了。使用shell命令进入Python交互模式,对这个表进行增初改查,操作如下:
是不是非常简单?接下来读者可以思考以下几个问题,带着这些问题去追踪源码并尝试解答:
◎ 如何通过Python代码实现上述交互模式?
◎ 这样的交互模式和普通的Python交互模式有何区别,为何前者能实现对模型层的增初改查操作,而后者在交互模式下导入DjangoBooks类会报错?报错的原因是什么,应如何解决?以下是直接在Python交互模式下导入Django模型类,报错如下:
下面带着前文提出的问题追踪shell命令的执行过程。我们在前面曾分析过startproject命令的执行过程,根据分析经验,首先在命令目录下查找shell.py文件:
仅看这里的代码就能解决前面提出的第1个问题了。这里定义的Command类只继承了BaseCommand类,所以执行过程与startproject命令相比会简单一些。其执行流程如图2-1所示,只不过最后调用的handle()方法会变成Command类中实现的handle()方法。handle()方法的执行逻辑非常简单:
(1)如果有通过-c选项输入的命令,直接执行命令后返回。
(2)对于非Windows平台、非终端且有select模块的,会通过sys.stdin.read()读取输入数据,并在执行后返回。
(3)通过内置及外部输入得到可用的shell()方法,在遍历后直接调用相应的shell()方法形成交互的样式。
Django内置了三种Python交互模式,分别为ipython、bpython和python。通过代码可以看到,在handle()方法返回后会依次遍历这三种模式并导入相应的模块。如果导入模块出现异常,则继续下一个模式的操作。通常情冴下会使用python模式,因此getattr(self,shell)会得到该Command类中的python()方法。
形成交互模式的代码就在Command类的python()方法中,下面看看python()方法的具体实现:
python()方法是一个非常通用的方法,它主要用来导入code模块,实现python交互模式。此外,它会检查系统中是否有readline模块。readline模块用于给交互模式提供代码补全功能。下面使用python()方法完成一个简单的示例:
在虚拟环境中运行上述Python脚本:
从代码中可以看出,我们成功得到了类似Python命令那样的交互模式。此外,code.interact()方法中的banner参数会被打印到交互模式之前,local参数会作为交互模式下的本地变量被默认导入。因此,前文提出的第1个问题就得到了解答:Django通过code模块实现了类似Python的交互模式,具体代码见shell.py文件中Command类的python()方法。
第2个问题也比较容易解决,首先通过错误输出来定位问题,在抛出异常前,最后一行执行代码如下:
打开Django的源码工程,找到这部分代码:
可以看到,最后抛出的异常正是这里的ImproperlyConfigured异常。抛出异常的原因是刞断条件not settings_module为True,即settings_module的值为False。该if语句的上一句os.environ.get(ENVIRONMENT_VARIABLE)结果为空。不妨在python manage.py shell和当前的shell交互模式下都执行这个获取环境变量的语句,看看有何不同:
很明显,这里需要在环境变量中指定DJANGO_SETTINGS_MODULE的值,该值指定了Django项目的配置模块路径。这个信息非常重要,因为前面设置的数据库相关信息就保存在该模块中。是不是在这里设置DJANGO_SETTINGS_MODULE的值之后,就能实现和python manage.py shell一样的效果呢?测试结果如下:
此时又出现了一个新的报错,出错的原因是没有加载Django项目中的应用信息,导致在调用Apps对象的check_apps_ready()方法时抛出异常:
从上面的注释可以看到,check_apps_ready()方法主要用来检查所有的应用是否被导入。导入应用这一步已经在python manage.py shell中执行过了,所以在其命令中导入DjangoBooks类时才不会报错。那么究竟是在哪一步完成的呢?其实只需反复追踪shell命令调用的代码,找出可能与应用导入相关的语句即可。
首先看ManagementUtility类中的execute()方法,前文在介绍startproject命令时忽略了django.setup()语句,而该语句在这里非常重要。下面是django.setup()语句内部所做的操作:
上面代码的最后一行是不是刚好和应用有关?继续看Apps对象的populate()方法:
上面的代码只需大致浏览一遍即可,无须太追究细节。只需看到,当调用populate()方法处理应用时,每个应用都会得到一个AppConfig对象,除设置self.apps_ready=True外,还会调用每个AppConfig对象的ready()方法。因此,在调用django.setup()后,apps的apps_ready属性值为True,对于check_apps_ready()方法自然不会再抛出异常。再次尝试在普通的Python交互模式下导入DjangoBooks类:
从上面的代码可以看到,在普通的Python命令行中也能成功导入Django中的模型类,只不过需要一些额外的操作,而这些操作在shell命令中已经提前做好了,所以可以直接导入并使用相应的模型类。