全书完整目录请见:Odoo 14开发者指南(Cookbook)第四版
在第五章 基本服务端开发中我们学习了如何编写模型方法来实现模块的逻辑。但是如果我们碰到错误或逻辑问题时可能就不知所措了。为处理这些错误,我们需要进行细致的检测,可能会花费大量时间。所幸,Odoo为我们提供了一些调试工具来帮助我们查找各类问题的根由。本章中我们将详细的了解这些调试工具和技术。
本章中,我们将讲解如下小节:
- 自动重载和
--dev
选项 - 产生服务日志来帮助调试方法
- 使用Odoo shell来交互调用方法
- 使用Python调试器来追踪方法执行
- 理解调试模式选项
自动重载和--dev
选项
在前面的章节中,我们学习了如何添加模型、字段和视图。每当对Python文件进行修改时,我们需要重启服务来应用这些修改。如果我们对XML文件做出修改的话,需要重启服务并更新模块来在用户界面中反映这些修改。如果你在开发一个大型应用的话,这会非常耗时且令人沮丧。Odoo提供了一个命令行选项–dev来处理这些问题。--dev
有一些可选值,本节中,我们就来学习如何使用它们。
准备工作
运行如下shell命令来在开发环境中安装inotify。没有inotify,就无法运行自动重载的功能:
1 |
$ pip3 install inotify |
如何实现…
要启用dev选项,我们需要在命令行中使用--dev=value
。这一选项可以使用的值有all, reload, pudb|wdb|ipdb|pdb, qweb, werkzeug和xml。查看如下示例来获取更多信息:
1 |
$ odoo/odoo-bin -c ~/odoo-dev/my-instance.cfg --dev=all |
如果你只想启用几个选项的话,可以使用逗号分隔的值,如下:
1 |
$ odoo/odoo-bin -c ~/odoo-dev/my-instance.cfg --dev=reload,qweb |
运行原理…
查看如下列表来了解所有--dev
选项及其用途:
- reload:无论何时对Python做出修改,我们需要重启该服务来在Odoo中应用这些修改。
--dev=reload
选项会在Python做出修改时自动重载Odoo服务。如未安装Python的inotify包则无法使用这一功能。此时使用这一选项运行Odoo服务时,会看到这样的日志:’inotify’ module not installed. Code autoreload feature is disabled - qweb:我们可以在Odoo中使用QWeb模板来创建动态网页。在第十四章 CMS网站开发中我们将了解如何使用QWeb模板开发网页。可以通过t-debug 属性在QWeb模板中调试问题。t-debug选项仅在你通过
--dev=qweb
启用dev模式时才会生效。 - werkzeug:Odoo 使用werkzeug来处理HTTP请求。Odoo内部会捕获并控制所有由werkzeug生成的异常。如果使用
--dev=werkzeug
,werkzeug的交互式调试器会在生成异常时在网页中展示。 - xml:每当你在视图结构中做出更改时,都需要重新加载服务并更新模型来应用这些更改。通过
--dev=xml
选项,仅需在浏览器中重载Odoo,无需再重启服务或更新模块。 - pudb|wdb|ipdb|pdb:我们可以通过Python调试器(PDB)来获取错误的更多详情。在使用
--dev=pdb
选项时,它会在Odoo中产生异常时启动PDB。Odoo支持4种Python调试器:pudb, wdb, ipdb和pdb。 - all:如果使用
--dev=all
,会启用上述的所有选项。
📝重要:如果对数据库结构做出修改,比如添加了新字段,
--dev=reload
不会在数据库schema中进行反映。需要手动更新模块,它仅用于Python的业务逻辑。如果你添加了一个视图或菜单,
--dev=xml
也不会在用户界面中进行反映。需要手动更新模块。在设计视图结构或网页时这会很有帮助。如果用户通过 GUI 在视图中作出修改,那么--dev=xml
不会从文件中加载XML,Odoo会使用所修改的视图结构。
产生服务日志来帮助调试方法
服务日志对于了解崩溃前运行时发生了什么会很有用。也可以添加日志来在调试问题时提供更多的信息。本节展示如何向已有方法添加日志。
准备工作
我们会向如下方法添加一些日志语句,它会将产品的仓储级别保存到一个文件中(读者需要在声明文件中添加product和stock模块的依赖):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
from os.path import join from odoo import models, api, exceptions EXPORTS_DIR = '/srv/exports' class ProductProduct(models.Model): _inherit = 'product.product' @api.model def export_stock_level(self, stock_location): products = self.with_context( location=stock_location.id ).search([]) products = products.filtered('qty_available') fname = join(EXPORTS_DIR, 'stock_level.txt') try: with open(fname, 'w') as fobj: for prod in products: fobj.write('%s\t%f\n' % (prod.name, prod.qty_available)) except IOError: raise exceptions.UserError('unable to save file') |
如何实现…
执行如下步骤在来执行方法时获取一些日志:
- 在代码的起始处,导入logging模块:
1import logging - 在定义模型类之前,为模块获取一个日志记录器:
1_logger = logging.getLogger(__name__) - 修改export_stock_level()的代码如下:
12345678910111213141516171819@api.modeldef export_stock_level(self, stock_location):_logger.info('export stock level for %s', stock_location.name)products = self.with_context(location=stock_location.id).search([])products = products.filtered('qty_available')_logger.debug('%d products in the location',len(products))fname = join(EXPORTS_DIR, 'stock_level.txt')try:with open(fname, 'w') as fobj:for prod in products:fobj.write('%s\t%f\n' % (prod.name, prod.qty_available))except IOError:_logger.exception('Error while writing to %s in %s','stock_level.txt', EXPORTS_DIR)raise exceptions.UserError('unable to save file')
运行原理…
第1步从Python标准库中导入了logging模块。Odoo使用这一模块来管理它的日志。
第2步为Python模块设置了一个日志记录器。我们使用了一个Odoo中常用的 __name__来作为日志记录器名称的自动变量,并通过_logger调用。
📝重要:__name__变量由Python解释器在模块导入的时候自动设置,它的名称为模块的全名。因Odoo在导入时做了一些小动作,插件模块被Python视作属于odoo.addons Python包。因此,如果本节的代码放在 my_library/models/book.py中,__name__为odoo.addons.my_library.models.book。
这么做有两个好处:
- odoo日志记录器的全局日志配置会应用到我们日志记录器中,这源于logging模块中日志记录器的层级结构。
- 日志会以完整的模块路径为前缀,这对查找所产生日志的具体代码行会有很大帮助。
第3步中使用日志记录器来生成日志信息。可以使用的方法有(日志级别逐渐升高):debug, info, warning, error和critical。所有这些方法接收一条消息,在其中可以使用%替换符及其它插入到消息中的参数。不应自己进行%替换,在生成日志时logging会足够智能地执行此操作。如果所运行的日志级别为INFO,那么DEBUG日志会避免具有长期CPU开销的替换。
本节中展示的另一个有用的方法是_logger.exception(),它可以在异常处理器中使用。消息会通过ERROR级别进行日志记录,栈的踪迹也会在应用日志中打印。
扩展知识…
可以通过命令行或配置文件来控制应用的日志级别。这么做有两种主要方式:
第一种方式是使用--log-handler
选项。基础语法为:--log-handler=prefix:level
。此时前缀为日志记录器名称的一段路径,级别为DEBUG, INFO, WARNING, ERROR或CRITICAL。如果省去了前缀,则对所有记录器设置默认级别。例如,设置my_library日志记录器的日志级别为DEBUG并让其它插件保持默认日志级别,可以通过如下命令启动Odoo:
1 |
$ python odoo.py --log-handler=odoo.addons.my_library:DEBUG |
可在命令行中多次指定--log-handler
。也可以在Odoo实例的配置文件中配置日志处理器。那样的话,可以使用前缀:日志级别键值对组成的逗号分隔列表。例如,下面的这行配置和此前最小化日志输出的配置相同。默认我们保留最重要的消息及错误消息,例外情况是:werkzeug产生的消息我们仅需要紧急(critical)消息,还有odoo.service.server我们保留包含服务启动消息等信息级别的消息。
1 |
log_handler = :ERROR,werkzeug:CRITICAL,odoo.service.server:INFO |
第二种方式是使用--log-level
选项。要全局控制日志级别,可以使用--log-level
来作为命令行选项。该选项可用的值有critical, error, warn, debug, debug_rpc, debug_rpc_answer, debug_sql和 test。
一些设置日志级别的快捷方式列出如下:
--log-request
是--log-handler=odoo.http.rpc.request:DEBUG
的简写--log-response
是--log-handler=odoo.http.rpc.response:DEBUG
的简写--log-web
是--log-handler=odoo.http:DEBUG
的简写--log-sql
是--log-handler=odoo.sql_db:DEBUG
的简写
使用Odoo shell来交互调用方法
Odoo网页界面供终端用户使用,虽然开发者模式解锁了许多强大的功能。但是,通过网页界面测试和调试不是种简单的方式,因为我们需要手动准备数据,导航至对应菜单来执行动作,等等。Odoo shell是一个命令行界面,可通过它来进行调用操作。本节展示如何启动Odoo shell以及在 shell 中执行调用方法等动作。
准备工作
我们将复用前一节中的代码在生成服务日志帮助调试方法。这让product.product模型可以添加新的方法。我们假定你的实例已安装并可使用该插件。本节中,我们预设你的Odoo实例中有一个名为project.conf的配置文件。
如何实现…
需要执行如下步骤来通过Odoo shell调用export_stock_level()方法:
- 启动Odoo shell并指定项目配置文件:
1$ ./odoo-bin shell -c project.conf --log-level=error - 查看错误消息并读取常规Python命令行输出之前显示的文本:
1234567891011env: <odoo.api.Environment object at 0x10e3277f0>odoo: <module 'odoo' from'/home/parth/community/odoo/__init__.py'>openerp: <module 'odoo' from'/home/parth/community/odoo/__init__.py'>self: res.users(1,)Python 3.6.5 (default, Apr 25 2018, 14:23:58)[GCC 9.3.0] on linuxType "help", "copyright", "credits" or "license" for moreinformation.>>> - 获取product.product记录集:
1>>> product = env['product.product'] - 获取主仓储位置记录:
1>>> location_stock = env.ref('stock.stock_location_stock') - 调用export_stock_level()方法:
1>>> product.export_stock_level(location_stock) - 在退出前执行事务:
1>>> env.cr.commit() - 通过按下Ctrl + D快捷键退出shell。
执行成功后查看结果:
执行前应新建相关目录并对当前用户进行授权,否则会如出现如下的某个错误:
1 2 3 4 5 |
FileNotFoundError: [Errno 2] No such file or directory: '/srv/exports/stock_level.txt' ... odoo.exceptions.UserError: unable to save file ... PermissionError: [Errno 13] Permission denied: '/srv/exports/stock_level.txt' |
运行原理…
第1步使用odoo-bin shell命令启动Odoo shell。所有常规的命令行参数在此处均可使用。我们使用-c来自指定项目配置文件以及--log-level
来减少日志的输出量。在调试时,你可能会需要使用DEBUG日志级别来获取更具体的插件调试信息。
在进行Python命令行输出之前,odoo-bin shell命令启动未监听网络的Odoo实例并初始化了一些全局变量,参见如下的输出:
- env是连接了数据库及在命令行或配置文件中指定的环境。
- odoo是导入的odoo包。可以在该包内访问所有的Python模块并执行你想要的操作。
- openerp是为保持向后兼容保留的odoo包的别名。
- self是包含Odoo超级用户(Administrator)单条记录的res.users的一个记录集,它与env环境关联。
第3和第4步使用env来获取一个空记录集并通过XML ID查找记录。第5步对product.product 记录集调用该方法。这些操作与你在方法中使用的相同,稍有不同的是我们使用env而非self.env (但两者均可使用,因为它们是一样的)。参见第五章 基本服务端开发了解更多相关知识。
第6步执行数据库事务。在这里并非必须,因为我们没有在数据库中修改任何记录,但如果你做了修改并希望这些修改持久化,就需要这么操作了;在通过网页界面使用Odoo时,每个RPC调用会运行它自己的数据库事务,由Odoo代为管理。在shell模式下运行时,则不再这样,你需要自己调用env.cr.commit()或env.cr.rollback()。否则在退出shell时,任何进行中的事务会自动被回滚。做测试时这没有问题,但如果你使用shell编写实例的配置脚本等时,记得要提交事务。
扩展知识…
默认在shell模式下,Odoo打开Python的REPL shell界面。我们可以通过--shell-interface
选项来选择REPL。支持的REPL有ipython, ptpython, bpython和python:
1 |
$ ./odoo-bin shell -c project.conf --shell-interface=ptpython |
译者注:REPL(Read Eval Print Loop)中文译作交互式解释器
使用Python调试器来追踪方法执行
有时,应用日志不足以判断何处出现了问题。所幸我们还有Python调试器。本节展示如何在方法中插入断点、手动跟踪代码的执行。
准备工作
我们将复用本章中使用Odoo shell来交互调用方法一节的export_stock_level() 方法。请确保已准备好相关代码。
如何实现…
执行如下步骤来通过pdb追踪export_stock_level()的执行:
- 编辑该方法的代码,插入高亮的那行代码:
12345678910def export_stock_level(self, stock_location):import pdb; pdb.set_trace()products = self.with_context( location=stock_location.id).search([])fname = join(EXPORTS_DIR, 'stock_level.txt')try:with open(fname, 'w') as fobj:for prod in products.filtered('qty_available'):fobj.write('%s\t%f\n' % (prod.name, prod.qty_available))except IOError:raise exceptions.UserError('unable to save file') - 运行该方法。我们会通过使用Odoo shell来交互调用方法一节中所讲解的Odoo shell:
12345678$ ./odoo-bin shell -c project.cfg --log-level=error[...]>>> product = env['product.product']>>> location_stock = env.ref('stock.stock_location_stock')>>> product.export_stock_level(location_stock)> /home/vagrant/odoo-dev/local-addons/my_library/models/product.py(15)export_stock_level()-> products = self.with_context((Pdb) - 在(Pdb)命令行,输入args命令(简写为 a)来获取传递给该方法的参数值:
123(Pdb) aself = product.product()stock_location = stock.location(8,) - 输入list命令来查看代码运行所在行:
12345678910111213(Pdb) list10 _inherit = 'product.product'1112 @api.model13 def export_stock_level(self, stock_location):14 import pdb; pdb.set_trace()15 -> products = self.with_context(16 location=stock_location.id).search([])17 fname = join(EXPORT_DIR, 'stock_level.txt')18 try:19 with open(fname, 'w') as fobj:20 for prod in products:(Pdb) - 输入next命令共3次来运行方法前几行代码。你可以使用简写形式n:
123456789(Pdb) next> /home/vagrant/odoo-dev/local-addons/my_library/models/product.py(16)export_stock_level()-> location=stock_location.id).search([])(Pdb) n> /home/vagrant/odoo-dev/local-addons/my_library/models/product.py(17)export_stock_level()-> fname = join(EXPORT_DIR, 'stock_level.txt')(Pdb) n> /home/vagrant/odoo-dev/local-addons/my_library/models/product.py(18)export_stock_level()-> try: - 使用命令p来显示变量products和fname的值:
1234(Pdb) p productsproduct.product(15, 16, 17, 18, 19, 20, 21, 23, 24, 12, 13, 14, 25, 30, 26, 36, 31, 34, 5, 8, 29, 33, 32, 6, 27, 35, 28, 7, 2, 1, 4, 3)(Pdb) p fname'/srv/exports/stock_level.txt' - 修改fname的值来指向/tmp目录:
1(Pdb) !fname = '/tmp/stock_level.txt' - 使用命令return(简写形式:r)来执行当前函数:
12345(Pdb) !fanme = '/tmp/stock_level.txt'(Pdb) return--Return--> /home/vagrant/odoo-dev/local-addons/my_library/models/product.py(20)export_stock_level()->None-> for prod in products: - 使用命令cont(简写为c)来恢复程序的执行:
12(Pdb) c>>>
运行原理…
在第1步中,我们通过Python标准库中的pdb模块调用set_trace() 方法来在源代码中硬编码了一个断点。在该方法执行时,程序的正常流程停止,会进入到一个 (Pdb) 命令行,在其中可输入pdb相关命令。
第2步使用shell模式调用了stock_level_export() 方法。也可以正常重启服务并使用网页界面点击相应的用户界面元素对所需追踪方法进行调用。
在需要手动使用Python调试器单步调试代码时,以下几个贴士会让你更为轻松:
- 降低日志级别来避免过多日志行,这会污染调试器的输出。通常可以ERROR级别开始。可能在使用中会需要启用更丰富的指定日志记录器,可通过使用
--log-handler
命令行选项来实现(参见产生服务日志来帮助调试方法一节)。 - 使用
--workers=0
运行服务来避免可能导致在不同进程中两次到达相同断点的多进程问题。 - 通过
--max-cron-threads=0
运行服务来关闭ir.cron定时任务的处理,不关闭它可能会在单步调试方法时触发定时任务,进而产生不必要的日志和负面效果。
第3到8步使用一些pdb命令来单步执行方法。以下是对pdb主要命令的总结。它们的大部分可使用第一个字母来作为简写。下面我们通过在括号中放入可选字母来进行表示:
- h(elp):会显示pdb命令的帮助信息。
- a(rgs):显示当前函数/方法的参数值。
- l(ist):显示11行代码块的执行源码,初始中间位置为当前行。后续调用会在源码文件中不断递进。还可在开头和结尾传递两个整型数字,指定显示的区域。
- p:打印变量。
- pp:格式化打印变量(对于列表和字典非常有用)。
- w(here):显示调用栈,当前行在最底部,而Python解释器在顶部。
- u(p):在调用栈中上移一级。
- d(own)::在调用栈中下移一级。
- n(ext):执行代码的当前行,然后停止。
- s(tep):进入方法调用执行的内部。
- r(eturn):恢复当前方法运行至执行返回语句。
- c(ont(inue)):恢复当前程序的执行至下一个断点。
- b(reak) <args>:创建一个新断点并展示其标识符,args可以为如下内容:
- 留空:列出所有断点。
- 行号:在指定文件的指定行打断点。
- 文件名:行号:在指定文件的指定行打断点(在sys.path中搜索文件)。
- 函数名:在指定函数的首行打断点。
- tbreak <args>:类似于 break,但在到达断点时会取消断点,这样该行的连续执行不会再次触发断点。
- disable bp_id:通过ID禁用某一断点。
- enable bl_id:通过ID启用某禁用断点。
- j(ump) lineno:执行的下一行为所指定行。可用于重新运行或跳过某些行。
- (!) statement:执行一条Python语句。如果命令不和pdb命令相近的话可以省略 ! 字符。例如,在想要为变量名 a 设置值时需要使用它,因为a是args命令的简写形式。
扩展知识…
本节中,我们插入了一个pdb.set_trace()语句断点进入pdb来进行调试。我们还可以在Odoo shell中直接启动pdb,在无法轻易地使用pdb.runcall()修改项目代码时这会非常有用。该函数接收一个方法做为参数,传递给这个方法的其它参数作为后续参数。因此,在 Odoo shell内,可以这么做:
1 2 3 4 5 6 7 |
>>> import pdb >>> product = env['product.product'] >>> location_stock = env.ref('stock.stock_location_stock') >>> pdb.runcall(product.export_stock_level, location_stock) > /home/vagrant/odoo-dev/local-addons/my_library/models/product.py(14)export_stock_level() -> products = self.with_context( (Pdb) |
本节中,我们集中学习了Python标准库中的Python调试器。了解这一工具非常有用,因为它一定会在Python发行版中存在。还有其它的Python调试器供使用,如ipdb和pudb,它们可作为pdb的替代。它们共享相同的API,本节中所学习的大部分命令都无需更改。当然,如果你使用Python IDE开发Odoo的话,还可以使用其内置的调试器。
其它内容
如果想要学习更多有关pdb调试器的知识,可参见pdb的完整文档:https://docs.python.org/3/library/pdb.html
理解调试模式选项
在第一章 安装Odoo开发环境中,我们学习了如何在Odoo中启用调试/开发者选项。这些选项对于调试或暴露一些更进一步的技术信息会很有帮助。本章中我们来深入了解这些选项。
如何实现…
查看第一章 安装Odoo开发环境中的激活Odoo开发者工具一节,并激活开发者模式。在激活了开发者模式之后,可以顶部菜单栏的调试图标下拉菜单中看到如下选项:
图7.1 – 启动调试模式后的选项
在这个菜单中,可以看到不同的选项。可以点击每个选项进行查看。下一部分我们来详细说明这些选项。
运行原理…
让我们通过如下列表理深入地学习这些选项:
- Run JS Tests:这个选项会重定向到JavaScript QUnit测试用例,如下图所示。它会逐一开始运行所有测试用例。这里,可以看到测试用例的进度和状态。在第十八章 自动化测试用例中,我们将学习如何创建我们自己的QUnit JavaScript测试用例:
- Run JS Mobile Tests:类似于前一个选项,它用于对移动环境运行QUnit测试用例。
- Run Click Anywhere Tests:这一选项会对所有菜单进行逐一点击。它会点击所有视图和搜索过滤器。如果出现问题或存在回退,会显示回溯信息。需要重新加载页面来停止这一测试。
- Open View:这个选项会打开一个所有可用视图的列表。通过选择任一视图,可以无需定义任何菜单或动作即打开该视图。
- Disable Tours: Odoo使用导览来帮助新用户上手。如果希望禁用,可以使用该选项。
- Start Tour:Odoo使用导览来进行自动化测试。我们会在第十五章 网页客户端开发中创建一个启动导览。该选项会通过一个所有导览的列表打开一个对话框,如下图所示。通过点击导览右侧的播放按钮,Odoo会自动执行该导览的所有步骤:
- Edit Action:在第三章 创建Odoo插件模块中添加菜单项和视图一节,我们添加了一个菜单项以及在Odoo中打开视图的动作。这些动作的详情以记录存储在数据库中。这一选项会打开我们打开展示当前视图的动作的记录详情。
- View Fields:这个选项会在我们想要从用户界面中查看字段的详情时有所帮助。它会显示当前模型的字段列表。例如,如果打开library.book模型的树状或表单视图,这个选项会显示library.book模型的字段列表。
- Manage Filters:在Odoo中,用户可以在搜索视图中创建自定义过滤器。该选项会为当前模型打开一个自定义过滤器列表。在这里可以修改自定义过滤器。
- Technical Translations:这一选项会打开一个当前模型的翻译术语列表。可以在这里修改模型的技术术语翻译。参见第十一章 国际化来学习翻译的更多知识。
- View Access Rights: 该选项会显示当前模型的访问权限列表。
- View Record Rules: 该选项会显示当前模型权限记录规则列表。
- Fields View Get:可以继承并修改其它插件模块中的已有视图。在一些应用中,这些视图被多个插件模块继承。因此,很难清楚的了解整体视图的定义。通过这一选项,可以获取应用所有继承后的最终视图定义。它在内部使用fields_view_get()方法。
- Edit View: <view type>:这个选项会打开带有当前视图 ir.ui.view 记录的对话框。此选项是动态的,它会显示一个基于当前所打开视图的选项。这表示如果你打开了看板视图,会获取到Edit View: Kanban option,而如果打开了表单视图,则会获取到Edit View: Form option。
📝小贴士:我们可以通过Edit View选项修改视图定义。这一更新的定义会应用于当前数据库并在更新模块时删除这些修改。因此通过模型来修改视图会更好。
- Edit ControlPanelView::这个选项和前面一个相同,但它会在当前模型的搜索视图中打开ir.ui.view记录。
- Activate Assets Debugging:Odoo提供两种类型的开发者模式:开发者模式和带资源的开发者模式。通过这一选项,可以将Developer mode切换为Developer mode with assets mode。查看第一章 安装Odoo开发环境中的激活Odoo开发者工具一节来获取更多详情。
- Activate Test Assets Debugging: 我们已经知道Odoo使用导览进行测试。启动这一模式会在Odoo中加载测试资源。这一选项会在Start tour对话框中显示更多的导览。
- Regenerate Assets Bundles:Odoo通过资源包来管理所有的CSS和JavaScript。这一选项删除老的JavaScript和CSS资源并生成新的。该选项对于因资源缓存产生的问题会很有帮助。我们将在第十四章 CMS网站开发中学习更多资源包相关知识。
- Become Super User:这是版本12中的新选项。通过激活该选项,可以切换为超级用户。此时即便没有访问权限也可访问这些记录。该选项不是所有用户都能用的,仅对拥有Administration: settings访问权限的用户可用。在启用这一模式之后,会看到一个条状的顶部菜单,如下图所示:
- Leave Developer Tools:该选项允许我们退出开发者模式。
我们已经学习了调试菜单下的所有选项。这些选项可以有多种使用方式,如调试、测试或修复问题。还可以用于查看视图的源代码。