doctest 工作原理

2022-08-03 17:06 更新

本节将详细介绍doctest如何工作:查看它的文档字符串,它如何查找交互式示例,它使用的执行上下文,它如何处理异常以及如何使用选项标志来控制其行为。这是编写doctest示例时需要了解的信息; 有关在这些示例上实际运行doctest的信息,请参阅以下各节。

哪些Docstrings被检查?

模块docstring,以及所有函数,类和方法文档字符串被搜索。导入到模块中的对象不被搜索。

另外,如果M.__test__存在且“为真”,则它必须是字典,并且每个条目将(字符串)名称映射到函数对象,类对象或字符串。从中找到的函数和类对象文档字符串M.__test__被搜索,字符串被视为文档字符串。在输出,一键K在M.__test__出现与名称

<name of M>.__test__.K

找到的任何类都以相似的方式递归搜索,以测试其包含的方法和嵌套类中的文档字符串。

在版本2.4中进行了更改:“专用名称”概念已被弃用且不再有记录。

Docstring示例如何被认可?

在大多数情况下,交互式控制台会话的复制和粘贴工作正常,但doctest并不试图精确模拟任何特定的Python shell。

>>> # comments are ignored
>>> x = 12
>>> x
12
>>> if x == 13:
...     print "yes"
... else:
...     print "no"
...     print "NO"
...     print "NO!!!"
...
no
NO
NO!!!
>>>

任何期望的输出必须紧跟在包含代码的最后一行'>>> '或'... '一行之后,并且预期的输出(如果有的话)扩展到下一行'>>> '或全空白行。

细则:

  • 预期的输出不能包含全空白行,因为这样的行被用来表示预期输出的结束。如果预期的输出包含空白行,请<BLANKLINE>在doctest示例中输入空行。新的2.4版本:<BLANKLINE>加入; 没有办法在以前的版本中使用包含空行的预期输出。
  • 所有硬标签字符都被扩展为空格,使用8列制表位。测试代码生成的输出中的选项卡不会被修改。由于示例输出中的任何硬标签都是展开的,这意味着如果代码输出包含硬标签,则doctest可以通过的唯一方式是如果NORMALIZE_WHITESPACE选项或指令有效。或者,可以重写测试以捕获输出并将其作为测试的一部分与预期值进行比较。源代码中对制表符的处理是通过反复试验得出的,并且已被证明是处理它们的最不容易出错的方式。通过编写自定义DocTestParser类,可以使用不同的算法来处理选项卡。

在2.4版本中进行了更改:将制表符扩展为空格是新的; 以前的版本试图保留硬标签,结果令人困惑。

  • 输出到标准输出被捕获,但不输出到标准错误(异常追溯通过不同的方式捕获)。
  • 如果在交互式会话中通过反斜线继续行,或者出于任何其他原因使用反斜杠,则应该使用原始文档字符串,该字符串将按照键入时的方式保存反斜杠:
def f(x): ... r'''Backslashes in a raw docstring: m\n''' >>> print f.__doc__ Backslashes in a raw docstring: m\n

否则,反斜杠将被解释为字符串的一部分。例如,\n以上将被解释为一个换行符。或者,您可以在doctest版本中将每个反斜杠加倍(并且不使用原始字符串):

def f(x): ... '''Backslashes in a raw docstring: m\n''' >>> print f.__doc__ Backslashes in a raw docstring: m\n
  • 起始栏无关紧要:
>>> assert "Easy!"
      >>> import math
          >>> math.floor(1.9)
          1

并且从开始示例的初始行中出现的预期输出中删除了许多主要的空白字符'>>>'。

什么是执行上下文?

默认情况下,每次doctest发现一个文档字符串进行测试,它采用的是 浅拷贝的M的全局,使运行测试不会改变模块真实的全局,因此,在一个测试M不能离开屑不小心让另外一个背后测试工作。这意味着示例可以自由使用任何在顶层定义的M名称,以及在运行的文档字符串中定义的名称。示例无法看到其他文档中定义的名称。

你可以通过强制使用自己的字典作为执行上下文 globs=your_dict来testmod()testfile()替代。

什么是例外?

没问题,只要回溯是该示例生成的唯一输出:只需粘贴回溯。[1]由于回溯包含可能快速变化的细节(例如,确切的文件路径和行号),所以这是doctest很难灵活接受的一种情况。

简单的例子:

>>>

>>> [1, 2, 3].remove(42)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: list.remove(x): x not in list

该文档测试成功,如果ValueError提出,详情如图所示。list.remove(x): x not in list

预期的异常输出必须以追溯标题开头,该标题可以是以下两行中的任一行,缩写与示例的第一行相同:

Traceback (most recent call last):
Traceback (innermost last):

traceback头后面跟着一个可选的traceback堆栈,其内容被doctest忽略。回溯堆栈通常被忽略,或者从交互式会话逐字复制。

跟踪堆栈后面是最有趣的部分:包含异常类型和细节的行。这通常是追溯的最后一行,但如果异常具有多行详细信息,则可以跨越多行:

>>>

>>> raise ValueError('multi\n    line\ndetail')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: multi
    line
detail

最后三行(以开始ValueError)与异常的类型和细节进行比较,其余部分将被忽略。

最佳做法是省略追溯堆栈,除非它为示例增加了重要的文档值。所以最后一个例子可能更好,因为:

>>>

>>> raise ValueError('multi\n    line\ndetail')
Traceback (most recent call last):
    ...
ValueError: multi
    line
detail

请注意,回溯处理非常特别。特别是,在改写的例子中,使用...独立于doctest的 ELLIPSIS选项。这个例子中的省略号可以省略,或者可以是三个(或三百个)逗号或数字,或者Monty Python skit的缩进记录。

一些细节你应该阅读一次,但不需要记住:

  • Doctest无法猜测您的预期输出是来自异常追溯还是来自普通打印。因此,例如,预计ValueError: 42 is prime会传递一个示例,无论是否ValueError实际提出,或者该示例仅打印该追溯文本。实际上,普通输出很少以追溯标题行开始,所以这不会产生实际问题。
  • 回溯堆栈的每一行(如果存在)必须比示例的第一行缩进得更远,或者以非字母数字字符开始。追溯标题后面的第一行缩写相同,并以字母数字开头,作为异常详细信息的开始。当然这对于真正的回溯来说是正确的。
  • 当IGNORE_EXCEPTION_DETAIL指定doctest选项时,将忽略最左侧冒号后面的所有内容以及异常名称中的所有模块信息。
  • 交互式shell省略了一些SyntaxErrors 的追溯标题行。但doctest使用traceback标题行来区分异常和非异常。因此,在极少数情况下,如果您需要测试一个SyntaxError省略traceback头的测试,则需要手动将traceback头行添加到测试示例中。
  • 对于某些SyntaxErrors,Python使用^标记来显示语法错误的字符位置:
1 1 File "", line 1 1 1 ^ SyntaxError: invalid syntax

由于显示错误位置的行出现在异常类型和细节之前,因此它们不会被doctest检查。例如,即使将^标记放在错误的位置,也会通过以下测试:

1 1 File "", line 1 1 1 ^ SyntaxError: invalid syntax

 Option Flags

许多选项标志控制着doctest行为的各个方面。这些标志的符号名称作为模块常量提供,可以按位或运算并传递给各种函数。这些名称也可以在doctest指令中使用。

第一组选项定义测试语义,控制doctest如何确定实际输出是否与示例预期输出相匹配的方面:

doctest.DONT_ACCEPT_TRUE_FOR_1

默认情况下,如果预期的输出块只包含1,只是含有实际输出块1或仅True被认为是一个匹配,并类似地用于0对False。当DONT_ACCEPT_TRUE_FOR_1指定时,不允许替换。缺省行为迎合了Python将许多函数的返回类型从整数更改为布尔值; 希望“小整数”输出的doctests在这些情况下仍然有效。这个选项可能会消失,但不会持续数年。

doctest.DONT_ACCEPT_BLANKLINE

默认情况下,如果预期的输出块包含仅包含字符串的行<BLANKLINE>,则该行将匹配实际输出中的空行。由于真正的空行界定了预期的输出,因此这是沟通预期空行的唯一方式。什么时候DONT_ACCEPT_BLANKLINE被指定,这个替代是不允许的。

doctest.NORMALIZE_WHITESPACE

指定时,所有空白(空格和换行符)都被视为相等。预期输出中的任何空白序列都将与实际输出中的任何空白序列相匹配。默认情况下,空白必须完全匹配。NORMALIZE_WHITESPACE当预期输出的行很长时,并且您想要在源代码中的多行中包装它时,它特别有用。

doctest.ELLIPSIS

指定时,...预期输出中的省略号标记()可以匹配实际输出中的任何子字符串。这包括跨越行边界的子字符串和空的子字符串,所以最好保持简单的使用。复杂的用途可能会导致相同类型的“oops,它匹配得太多了!” .*在正则表达式中很容易出现意外。

doctest.IGNORE_EXCEPTION_DETAIL

指定时,即使异常详细信息不匹配,如果引发了期望类型的异常,那么期望异常的示例也会通过。例如,ValueError: 42如果引发的实际异常是预期的例子ValueError: 3*14,但会失败,例如,如果TypeError引发。

它也会忽略Python 3 doctest报告中使用的模块名称。因此,无论测试是在Python 2.7还是Python 3.2(或更高版本)下运行,这两种变体都可以与指定的标志一起使用:

>>> raise CustomError('message')
Traceback (most recent call last):
CustomError: message

>>> raise CustomError('message')
Traceback (most recent call last):
my_module.CustomError: message

请注意,ELLIPSIS也可以用于忽略异常消息的详细信息,但根据是否将模块详细信息作为异常名称的一部分进行打印,此类测试可能仍会失败。使用IGNORE_EXCEPTION_DETAIL和来自Python 2.3的细节也是编写文档测试的唯一明确方式,它不关心异常细节,但仍然在Python 2.3或更低版本中继续传递(这些版本不支持doctest指令并将它们忽略为不相关的注释) 。例如:

>>> (1, 2)[3] = 'moo'
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: object doesn't support item assignment

虽然Python 2.4中的细节更改为“不”而不是“不”,但在Python 2.3以及更高版本的Python版本中通过了指定的标志。

在版本2.7中更改:IGNORE_EXCEPTION_DETAIL现在也忽略了与包含被测异常的模块有关的任何信息

doctest.SKIP

指定时,请不要运行该示例。这在doctest示例既可用作文档也可用作测试用例的情况下非常有用,应将其用于文档目的,但不应进行检查。例如,该示例的输出可能是随机的; 或者该示例可能依赖于测试驱动程序无法使用的资源。

SKIP标志也可用于临时“注释”示例。

2.5版本中的新功能。

doctest.COMPARISON_FLAGS

将上面的所有比较标志掩盖起来。

第二组选项控制如何报告测试失败:

doctest.REPORT_UDIFF

指定时,涉及多行预期和实际输出的故障将使用统一差异显示。

doctest.REPORT_CDIFF

指定时,涉及多行预期输出和实际输出的故障将使用上下文差异显示。

doctest.REPORT_NDIFF

指定时,difflib.Differ使用与常用ndiff.py实用程序相同的算法计算差异。这是标记线内和线间差异的唯一方法。例如,如果预期输出的一行包含数字1,其中实际输出包含字母l,则会插入一行,并在其中插入用于标记不匹配列位置的插入符号。

doctest.REPORT_ONLY_FIRST_FAILURE

指定时,显示每个doctest中的第一个失败示例,但禁止所有其他示例的输出。这将防止doctest报告因早期故障而中断的正确示例; 但它也可能隐藏不正确的例子,不依靠第一次失败而失败。当REPORT_ONLY_FIRST_FAILURE指定时,剩余的示例仍在运行,并仍然计入报告的故障总数; 只有输出被抑制。

doctest.REPORTING_FLAGS

将上面的所有报告标记掩盖起来。

新的2.4版本:常数DONT_ACCEPT_BLANKLINE,NORMALIZE_WHITESPACE,ELLIPSIS,IGNORE_EXCEPTION_DETAIL,REPORT_UDIFF,REPORT_CDIFF,REPORT_NDIFF,REPORT_ONLY_FIRST_FAILURE,COMPARISON_FLAGS和REPORTING_FLAGS添加。

还有一种方法可以注册新的选项标志名称,但除非您打算doctest通过子类扩展内部函数,否则这种方法并不有用。

doctest.register_optionflag(name)

用给定名称创建一个新选项标志,并返回新标志的整数值。register_optionflag()可用于子类化OutputChecker或DocTestRunner创建您的子类支持的新选项。register_optionflag()应该总是使用以下习惯用法来调用:

MY_FLAG = register_optionflag('MY_FLAG')

New in version 2.4.

Directives

Doctest指令可用于修改单个示例的选项标志。Doctest指令是遵循示例源代码的特殊Python注释:

directive             ::=  "#" "doctest:" directive_options
directive_options     ::=  directive_option ("," directive_option)\*
directive_option      ::=  on_or_off directive_option_name
on_or_off             ::=  "+" \| "-"
directive_option_name ::=  "DONT_ACCEPT_BLANKLINE" \| "NORMALIZE_WHITESPACE" \| ...

+or -和指令选项名称之间不允许有空格。指令选项名称可以是上面解释的任何选项标志名称。

一个例子的doctest指令修改了doctest的这个例子的行为。使用+启用这个名字的行为,或-将其禁用。

例如,这个测试通过:

>>> print range(20) # doctest: +NORMALIZE_WHITESPACE
[0,   1,  2,  3,  4,  5,  6,  7,  8,  9,
10,  11, 12, 13, 14, 15, 16, 17, 18, 19]

如果没有指令,它会失败,这是因为实际输出在单个数字列表元素之前没有两个空格,并且因为实际输出在单行上。这个测试也通过了,并且还需要一个指令来做到这一点:

>>> print range(20) # doctest: +ELLIPSIS
[0, 1, ..., 18, 19]

多条指令可用于单条物理线路,用逗号分隔:

>>> print range(20) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
[0,    1, ...,   18,    19]

如果单个示例使用多个指令注释,则将它们合并:

>>> print range(20) # doctest: +ELLIPSIS
...                 # doctest: +NORMALIZE_WHITESPACE
[0,    1, ...,   18,    19]

如前例所示,您可以将...行添加到仅包含指令的示例中。当一个例子对于指令很容易适合同一行时太长了,这会很有用:

>>> print range(5) + range(10,20) + range(30,40) + range(50,60)
... # doctest: +ELLIPSIS
[0, ..., 4, 10, ..., 19, 30, ..., 39, 50, ..., 59]

请注意,由于默认情况下所有选项都被禁用,并且指令仅适用于它们出现的示例,因此启用选项(通过+指令)通常是唯一有意义的选择。但是,选项标志也可以传递给运行doctests的函数,建立不同的默认值。在这种情况下,通过-指令禁用选项可能很有用。

2.4版新增功能:增加了对doctest指令的支持。

警告

doctest严格要求在预期产出中要求完全匹配。如果即使单个字符不匹配,测试也会失败。这可能会让你感到惊讶,因为你确切地知道Python做了什么,并且不能保证输出。例如,在打印字典时,Python不保证键值对将以任何特定的顺序打印,因此像

>>> foo()
{"Hermione": "hippogryph", "Harry": "broomstick"}

很脆弱!一种解决方法是做

>>> foo() == {"Hermione": "hippogryph", "Harry": "broomstick"}
True

代替。另一个是要做的

>>> d = foo().items()
>>> d.sort()
>>> d
[('Harry', 'broomstick'), ('Hermione', 'hippogryph')]

还有其他的,但你明白了。

另一个不好的想法是打印嵌入对象地址的东西,比如

>>> id(1.0) # certain to fail some of the time
7948648
>>> class C: pass
>>> C()   # the default repr() for instances embeds an address
<__main__.C instance at 0x00AC18F0>

ELLIPSIS指令为最后一个示例提供了一个很好的方法:

>>> C() #doctest: +ELLIPSIS
<__main__.C instance at 0x...>

浮点数也受到跨平台的小输出变化的影响,因为Python遵循平台C库进行浮点格式化,而C库在质量上差别很大。

>>> 1./7  # risky
0.14285714285714285
>>> print 1./7 # safer
0.142857142857
>>> print round(1./7, 6) # much safer
0.142857

表格I/2.**J中的数字在所有平台上都是安全的,而且我通常会编写一些doctest的例子来生成这种格式的数字:

>>> 3./4  # utterly safe
0.75

简单的分数对于人们来说也更容易理解,并且这使得更好的文档。


以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号