odoo-cookbook

第七章 调试模块

全书完整目录请见:Odoo 14开发者指南(Cookbook)第四版

第五章 基本服务端开发中我们学习了如何编写模型方法来实现模块的逻辑。但是如果我们碰到错误或逻辑问题时可能就不知所措了。为处理这些错误,我们需要进行细致的检测,可能会花费大量时间。所幸,Odoo为我们提供了一些调试工具来帮助我们查找各类问题的根由。本章中我们将详细的了解这些调试工具和技术。

本章中,我们将讲解如下小节:

自动重载和--dev 选项

在前面的章节中,我们学习了如何添加模型、字段和视图。每当对Python文件进行修改时,我们需要重启服务来应用这些修改。如果我们对XML文件做出修改的话,需要重启服务并更新模块来在用户界面中反映这些修改。如果你在开发一个大型应用的话,这会非常耗时且令人沮丧。Odoo提供了一个命令行选项–dev来处理这些问题。--dev有一些可选值,本节中,我们就来学习如何使用它们。

准备工作

运行如下shell命令来在开发环境中安装inotify。没有inotify,就无法运行自动重载的功能:

 $ pip3 install inotify

如何实现…

要启用dev选项,我们需要在命令行中使用--dev=value。这一选项可以使用的值有all, reload, pudb wdb ipdb pdb, qweb, werkzeug和xml。查看如下示例来获取更多信息:
$ odoo/odoo-bin -c ~/odoo-dev/my-instance.cfg --dev=all

如果你只想启用几个选项的话,可以使用逗号分隔的值,如下:

$ odoo/odoo-bin -c ~/odoo-dev/my-instance.cfg --dev=reload,qweb

运行原理…

查看如下列表来了解所有--dev选项及其用途:

📝重要:如果对数据库结构做出修改,比如添加了新字段, --dev=reload不会在数据库schema中进行反映。需要手动更新模块,它仅用于Python的业务逻辑。

如果你添加了一个视图或菜单,--dev=xml也不会在用户界面中进行反映。需要手动更新模块。在设计视图结构或网页时这会很有帮助。如果用户通过 GUI 在视图中作出修改,那么--dev=xml不会从文件中加载XML,Odoo会使用所修改的视图结构。

产生服务日志来帮助调试方法

服务日志对于了解崩溃前运行时发生了什么会很有用。也可以添加日志来在调试问题时提供更多的信息。本节展示如何向已有方法添加日志。

准备工作

我们会向如下方法添加一些日志语句,它会将产品的仓储级别保存到一个文件中(读者需要在声明文件中添加product和stock模块的依赖):

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

如何实现…

执行如下步骤在来执行方法时获取一些日志:

  1. 在代码的起始处,导入logging模块:

    import logging
    
  2. 在定义模型类之前,为模块获取一个日志记录器:

    _logger = logging.getLogger(__name__)
    
  3. 修改export_stock_level()的代码如下:

    @api.model
    def 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。

这么做有两个好处:

第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:

$ python odoo.py --log-handler=odoo.addons.my_library:DEBUG

可在命令行中多次指定--log-handler。也可以在Odoo实例的配置文件中配置日志处理器。那样的话,可以使用前缀:日志级别键值对组成的逗号分隔列表。例如,下面的这行配置和此前最小化日志输出的配置相同。默认我们保留最重要的消息及错误消息,例外情况是:werkzeug产生的消息我们仅需要紧急(critical)消息,还有odoo.service.server我们保留包含服务启动消息等信息级别的消息。

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。

一些设置日志级别的快捷方式列出如下:

使用Odoo shell来交互调用方法

Odoo网页界面供终端用户使用,虽然开发者模式解锁了许多强大的功能。但是,通过网页界面测试和调试不是种简单的方式,因为我们需要手动准备数据,导航至对应菜单来执行动作,等等。Odoo shell是一个命令行界面,可通过它来进行调用操作。本节展示如何启动Odoo shell以及在 shell 中执行调用方法等动作。

准备工作

我们将复用前一节中的代码在生成服务日志帮助调试方法。这让product.product模型可以添加新的方法。我们假定你的实例已安装并可使用该插件。本节中,我们预设你的Odoo实例中有一个名为project.conf的配置文件。

如何实现…

需要执行如下步骤来通过Odoo shell调用export_stock_level()方法:

  1. 启动Odoo shell并指定项目配置文件:

    $ ./odoo-bin shell -c project.conf --log-level=error
    
  2. 查看错误消息并读取常规Python命令行输出之前显示的文本:

    env: <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 linux
    Type "help", "copyright", "credits" or "license" for more
    information.
    >>>
    
  3. 获取product.product记录集:

    >>> product = env['product.product']
    
  4. 获取主仓储位置记录:

    >>> location_stock = env.ref('stock.stock_location_stock')
    
  5. 调用export_stock_level()方法:

    >>> product.export_stock_level(location_stock)
    
  6. 在退出前执行事务:

    >>> env.cr.commit()
    
  7. 通过按下Ctrl + D快捷键退出shell。

使用Odoo shell来交互调用方法

执行成功后查看结果:

使用Odoo shell来交互调用方法

执行前应新建相关目录并对当前用户进行授权,否则会如出现如下的某个错误:

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实例并初始化了一些全局变量,参见如下的输出:

第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:

$ ./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()的执行:

  1. 编辑该方法的代码,插入高亮的那行代码:

    def 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')
    
  2. 运行该方法。我们会通过

    使用Odoo shell来交互调用方法

    一节中所讲解的Odoo shell:

    $ ./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)
    
  3. 在(Pdb)命令行,输入args命令(简写为 a)来获取传递给该方法的参数值:

    (Pdb) a
    self = product.product()
    stock_location = stock.location(8,)
    
  4. 输入list命令来查看代码运行所在行:

    (Pdb) list
     10  	    _inherit = 'product.product'
     11
     12  	    @api.model
     13  	    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)
    
  5. 输入next命令共3次来运行方法前几行代码。你可以使用简写形式n:

    (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:
    
  6. 使用命令p来显示变量products和fname的值:

    (Pdb) p products
    product.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'
    
  7. 修改fname的值来指向/tmp目录:

    (Pdb) !fname = '/tmp/stock_level.txt'
    
  8. 使用命令return(简写形式:r)来执行当前函数:

    (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:
    
  9. 使用命令cont(简写为c)来恢复程序的执行:

    (Pdb) c
    >>>
    

运行原理…

在第1步中,我们通过Python标准库中的pdb模块调用set_trace() 方法来在源代码中硬编码了一个断点。在该方法执行时,程序的正常流程停止,会进入到一个 (Pdb) 命令行,在其中可输入pdb相关命令。

第2步使用shell模式调用了stock_level_export() 方法。也可以正常重启服务并使用网页界面点击相应的用户界面元素对所需追踪方法进行调用。

在需要手动使用Python调试器单步调试代码时,以下几个贴士会让你更为轻松:

第3到8步使用一些pdb命令来单步执行方法。以下是对pdb主要命令的总结。它们的大部分可使用第一个字母来作为简写。下面我们通过在括号中放入可选字母来进行表示:

扩展知识…

本节中,我们插入了一个pdb.set_trace()语句断点进入pdb来进行调试。我们还可以在Odoo shell中直接启动pdb,在无法轻易地使用pdb.runcall()修改项目代码时这会非常有用。该函数接收一个方法做为参数,传递给这个方法的其它参数作为后续参数。因此,在 Odoo shell内,可以这么做:

>>> 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调试器供使用,如ipdbpudb,它们可作为pdb的替代。它们共享相同的API,本节中所学习的大部分命令都无需更改。当然,如果你使用Python IDE开发Odoo的话,还可以使用其内置的调试器。

其它内容

如果想要学习更多有关pdb调试器的知识,可参见pdb的完整文档:https://docs.python.org/3/library/pdb.html

理解调试模式选项

第一章 安装Odoo开发环境中,我们学习了如何在Odoo中启用调试/开发者选项。这些选项对于调试或暴露一些更进一步的技术信息会很有帮助。本章中我们来深入了解这些选项。

如何实现…

查看第一章 安装Odoo开发环境中的激活Odoo开发者工具一节,并激活开发者模式。在激活了开发者模式之后,可以顶部菜单栏的调试图标下拉菜单中看到如下选项:

Odoo 14启动调试模式后的选项

图7.1 – 启动调试模式后的选项

在这个菜单中,可以看到不同的选项。可以点击每个选项进行查看。下一部分我们来详细说明这些选项。

运行原理…

让我们通过如下列表理深入地学习这些选项:

我们已经学习了调试菜单下的所有选项。这些选项可以有多种使用方式,如调试、测试或修复问题。还可以用于查看视图的源代码。