doctest 工作原理

2022-08-03 17:06 更新

本節(jié)將詳細(xì)介紹doctest如何工作:查看它的文檔字符串,它如何查找交互式示例,它使用的執(zhí)行上下文,它如何處理異常以及如何使用選項(xiàng)標(biāo)志來控制其行為。這是編寫doctest示例時(shí)需要了解的信息; 有關(guān)在這些示例上實(shí)際運(yùn)行doctest的信息,請(qǐng)參閱以下各節(jié)。

哪些Docstrings被檢查?

模塊docstring,以及所有函數(shù),類和方法文檔字符串被搜索。導(dǎo)入到模塊中的對(duì)象不被搜索。

另外,如果M.__test__存在且“為真”,則它必須是字典,并且每個(gè)條目將(字符串)名稱映射到函數(shù)對(duì)象,類對(duì)象或字符串。從中找到的函數(shù)和類對(duì)象文檔字符串M.__test__被搜索,字符串被視為文檔字符串。在輸出,一鍵K在M.__test__出現(xiàn)與名稱

<name of M>.__test__.K

找到的任何類都以相似的方式遞歸搜索,以測(cè)試其包含的方法和嵌套類中的文檔字符串。

在版本2.4中進(jìn)行了更改:“專用名稱”概念已被棄用且不再有記錄。

Docstring示例如何被認(rèn)可?

在大多數(shù)情況下,交互式控制臺(tái)會(huì)話的復(fù)制和粘貼工作正常,但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!!!
>>>

任何期望的輸出必須緊跟在包含代碼的最后一行'>>> '或'... '一行之后,并且預(yù)期的輸出(如果有的話)擴(kuò)展到下一行'>>> '或全空白行。

細(xì)則:

  • 預(yù)期的輸出不能包含全空白行,因?yàn)檫@樣的行被用來表示預(yù)期輸出的結(jié)束。如果預(yù)期的輸出包含空白行,請(qǐng)<BLANKLINE>在doctest示例中輸入空行。新的2.4版本:<BLANKLINE>加入; 沒有辦法在以前的版本中使用包含空行的預(yù)期輸出。
  • 所有硬標(biāo)簽字符都被擴(kuò)展為空格,使用8列制表位。測(cè)試代碼生成的輸出中的選項(xiàng)卡不會(huì)被修改。由于示例輸出中的任何硬標(biāo)簽都是展開的,這意味著如果代碼輸出包含硬標(biāo)簽,則doctest可以通過的唯一方式是如果NORMALIZE_WHITESPACE選項(xiàng)或指令有效?;蛘撸梢灾貙憸y(cè)試以捕獲輸出并將其作為測(cè)試的一部分與預(yù)期值進(jìn)行比較。源代碼中對(duì)制表符的處理是通過反復(fù)試驗(yàn)得出的,并且已被證明是處理它們的最不容易出錯(cuò)的方式。通過編寫自定義DocTestParser類,可以使用不同的算法來處理選項(xiàng)卡。

在2.4版本中進(jìn)行了更改:將制表符擴(kuò)展為空格是新的; 以前的版本試圖保留硬標(biāo)簽,結(jié)果令人困惑。

  • 輸出到標(biāo)準(zhǔn)輸出被捕獲,但不輸出到標(biāo)準(zhǔn)錯(cuò)誤(異常追溯通過不同的方式捕獲)。
  • 如果在交互式會(huì)話中通過反斜線繼續(xù)行,或者出于任何其他原因使用反斜杠,則應(yīng)該使用原始文檔字符串,該字符串將按照鍵入時(shí)的方式保存反斜杠:
def f(x): ... r'''Backslashes in a raw docstring: m\n''' >>> print f.__doc__ Backslashes in a raw docstring: m\n

否則,反斜杠將被解釋為字符串的一部分。例如,\n以上將被解釋為一個(gè)換行符?;蛘撸梢栽赿octest版本中將每個(gè)反斜杠加倍(并且不使用原始字符串):

def f(x): ... '''Backslashes in a raw docstring: m\n''' >>> print f.__doc__ Backslashes in a raw docstring: m\n
  • 起始欄無關(guān)緊要:
>>> assert "Easy!"
      >>> import math
          >>> math.floor(1.9)
          1

并且從開始示例的初始行中出現(xiàn)的預(yù)期輸出中刪除了許多主要的空白字符'>>>'。

什么是執(zhí)行上下文?

默認(rèn)情況下,每次doctest發(fā)現(xiàn)一個(gè)文檔字符串進(jìn)行測(cè)試,它采用的是 淺拷貝的M的全局,使運(yùn)行測(cè)試不會(huì)改變模塊真實(shí)的全局,因此,在一個(gè)測(cè)試M不能離開屑不小心讓另外一個(gè)背后測(cè)試工作。這意味著示例可以自由使用任何在頂層定義的M名稱,以及在運(yùn)行的文檔字符串中定義的名稱。示例無法看到其他文檔中定義的名稱。

你可以通過強(qiáng)制使用自己的字典作為執(zhí)行上下文 globs=your_dict來testmod()testfile()替代。

什么是例外?

沒問題,只要回溯是該示例生成的唯一輸出:只需粘貼回溯。[1]由于回溯包含可能快速變化的細(xì)節(jié)(例如,確切的文件路徑和行號(hào)),所以這是doctest很難靈活接受的一種情況。

簡(jiǎn)單的例子:

>>>

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

該文檔測(cè)試成功,如果ValueError提出,詳情如圖所示。list.remove(x): x not in list

預(yù)期的異常輸出必須以追溯標(biāo)題開頭,該標(biāo)題可以是以下兩行中的任一行,縮寫與示例的第一行相同:

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

traceback頭后面跟著一個(gè)可選的traceback堆棧,其內(nèi)容被doctest忽略?;厮荻褩Mǔ1缓雎?,或者從交互式會(huì)話逐字復(fù)制。

跟蹤堆棧后面是最有趣的部分:包含異常類型和細(xì)節(jié)的行。這通常是追溯的最后一行,但如果異常具有多行詳細(xì)信息,則可以跨越多行:

>>>

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

最后三行(以開始ValueError)與異常的類型和細(xì)節(jié)進(jìn)行比較,其余部分將被忽略。

最佳做法是省略追溯堆棧,除非它為示例增加了重要的文檔值。所以最后一個(gè)例子可能更好,因?yàn)椋?/p>

>>>

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

請(qǐng)注意,回溯處理非常特別。特別是,在改寫的例子中,使用...獨(dú)立于doctest的 ELLIPSIS選項(xiàng)。這個(gè)例子中的省略號(hào)可以省略,或者可以是三個(gè)(或三百個(gè))逗號(hào)或數(shù)字,或者M(jìn)onty Python skit的縮進(jìn)記錄。

一些細(xì)節(jié)你應(yīng)該閱讀一次,但不需要記?。?/p>

  • Doctest無法猜測(cè)您的預(yù)期輸出是來自異常追溯還是來自普通打印。因此,例如,預(yù)計(jì)ValueError: 42 is prime會(huì)傳遞一個(gè)示例,無論是否ValueError實(shí)際提出,或者該示例僅打印該追溯文本。實(shí)際上,普通輸出很少以追溯標(biāo)題行開始,所以這不會(huì)產(chǎn)生實(shí)際問題。
  • 回溯堆棧的每一行(如果存在)必須比示例的第一行縮進(jìn)得更遠(yuǎn),或者以非字母數(shù)字字符開始。追溯標(biāo)題后面的第一行縮寫相同,并以字母數(shù)字開頭,作為異常詳細(xì)信息的開始。當(dāng)然這對(duì)于真正的回溯來說是正確的。
  • 當(dāng)IGNORE_EXCEPTION_DETAIL指定doctest選項(xiàng)時(shí),將忽略最左側(cè)冒號(hào)后面的所有內(nèi)容以及異常名稱中的所有模塊信息。
  • 交互式shell省略了一些SyntaxErrors 的追溯標(biāo)題行。但doctest使用traceback標(biāo)題行來區(qū)分異常和非異常。因此,在極少數(shù)情況下,如果您需要測(cè)試一個(gè)SyntaxError省略traceback頭的測(cè)試,則需要手動(dòng)將traceback頭行添加到測(cè)試示例中。
  • 對(duì)于某些SyntaxErrors,Python使用^標(biāo)記來顯示語(yǔ)法錯(cuò)誤的字符位置:
1 1 File "", line 1 1 1 ^ SyntaxError: invalid syntax

由于顯示錯(cuò)誤位置的行出現(xiàn)在異常類型和細(xì)節(jié)之前,因此它們不會(huì)被doctest檢查。例如,即使將^標(biāo)記放在錯(cuò)誤的位置,也會(huì)通過以下測(cè)試:

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

 Option Flags

許多選項(xiàng)標(biāo)志控制著doctest行為的各個(gè)方面。這些標(biāo)志的符號(hào)名稱作為模塊常量提供,可以按位或運(yùn)算并傳遞給各種函數(shù)。這些名稱也可以在doctest指令中使用。

第一組選項(xiàng)定義測(cè)試語(yǔ)義,控制doctest如何確定實(shí)際輸出是否與示例預(yù)期輸出相匹配的方面:

doctest.DONT_ACCEPT_TRUE_FOR_1

默認(rèn)情況下,如果預(yù)期的輸出塊只包含1,只是含有實(shí)際輸出塊1或僅True被認(rèn)為是一個(gè)匹配,并類似地用于0對(duì)False。當(dāng)DONT_ACCEPT_TRUE_FOR_1指定時(shí),不允許替換。缺省行為迎合了Python將許多函數(shù)的返回類型從整數(shù)更改為布爾值; 希望“小整數(shù)”輸出的doctests在這些情況下仍然有效。這個(gè)選項(xiàng)可能會(huì)消失,但不會(huì)持續(xù)數(shù)年。

doctest.DONT_ACCEPT_BLANKLINE

默認(rèn)情況下,如果預(yù)期的輸出塊包含僅包含字符串的行<BLANKLINE>,則該行將匹配實(shí)際輸出中的空行。由于真正的空行界定了預(yù)期的輸出,因此這是溝通預(yù)期空行的唯一方式。什么時(shí)候DONT_ACCEPT_BLANKLINE被指定,這個(gè)替代是不允許的。

doctest.NORMALIZE_WHITESPACE

指定時(shí),所有空白(空格和換行符)都被視為相等。預(yù)期輸出中的任何空白序列都將與實(shí)際輸出中的任何空白序列相匹配。默認(rèn)情況下,空白必須完全匹配。NORMALIZE_WHITESPACE當(dāng)預(yù)期輸出的行很長(zhǎng)時(shí),并且您想要在源代碼中的多行中包裝它時(shí),它特別有用。

doctest.ELLIPSIS

指定時(shí),...預(yù)期輸出中的省略號(hào)標(biāo)記()可以匹配實(shí)際輸出中的任何子字符串。這包括跨越行邊界的子字符串和空的子字符串,所以最好保持簡(jiǎn)單的使用。復(fù)雜的用途可能會(huì)導(dǎo)致相同類型的“oops,它匹配得太多了!” .*在正則表達(dá)式中很容易出現(xiàn)意外。

doctest.IGNORE_EXCEPTION_DETAIL

指定時(shí),即使異常詳細(xì)信息不匹配,如果引發(fā)了期望類型的異常,那么期望異常的示例也會(huì)通過。例如,ValueError: 42如果引發(fā)的實(shí)際異常是預(yù)期的例子ValueError: 3*14,但會(huì)失敗,例如,如果TypeError引發(fā)。

它也會(huì)忽略Python 3 doctest報(bào)告中使用的模塊名稱。因此,無論測(cè)試是在Python 2.7還是Python 3.2(或更高版本)下運(yùn)行,這兩種變體都可以與指定的標(biāo)志一起使用:

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

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

請(qǐng)注意,ELLIPSIS也可以用于忽略異常消息的詳細(xì)信息,但根據(jù)是否將模塊詳細(xì)信息作為異常名稱的一部分進(jìn)行打印,此類測(cè)試可能仍會(huì)失敗。使用IGNORE_EXCEPTION_DETAIL和來自Python 2.3的細(xì)節(jié)也是編寫文檔測(cè)試的唯一明確方式,它不關(guān)心異常細(xì)節(jié),但仍然在Python 2.3或更低版本中繼續(xù)傳遞(這些版本不支持doctest指令并將它們忽略為不相關(guān)的注釋) 。例如:

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

雖然Python 2.4中的細(xì)節(jié)更改為“不”而不是“不”,但在Python 2.3以及更高版本的Python版本中通過了指定的標(biāo)志。

在版本2.7中更改:IGNORE_EXCEPTION_DETAIL現(xiàn)在也忽略了與包含被測(cè)異常的模塊有關(guān)的任何信息

doctest.SKIP

指定時(shí),請(qǐng)不要運(yùn)行該示例。這在doctest示例既可用作文檔也可用作測(cè)試用例的情況下非常有用,應(yīng)將其用于文檔目的,但不應(yīng)進(jìn)行檢查。例如,該示例的輸出可能是隨機(jī)的; 或者該示例可能依賴于測(cè)試驅(qū)動(dòng)程序無法使用的資源。

SKIP標(biāo)志也可用于臨時(shí)“注釋”示例。

2.5版本中的新功能。

doctest.COMPARISON_FLAGS

將上面的所有比較標(biāo)志掩蓋起來。

第二組選項(xiàng)控制如何報(bào)告測(cè)試失?。?/p>

doctest.REPORT_UDIFF

指定時(shí),涉及多行預(yù)期和實(shí)際輸出的故障將使用統(tǒng)一差異顯示。

doctest.REPORT_CDIFF

指定時(shí),涉及多行預(yù)期輸出和實(shí)際輸出的故障將使用上下文差異顯示。

doctest.REPORT_NDIFF

指定時(shí),difflib.Differ使用與常用ndiff.py實(shí)用程序相同的算法計(jì)算差異。這是標(biāo)記線內(nèi)和線間差異的唯一方法。例如,如果預(yù)期輸出的一行包含數(shù)字1,其中實(shí)際輸出包含字母l,則會(huì)插入一行,并在其中插入用于標(biāo)記不匹配列位置的插入符號(hào)。

doctest.REPORT_ONLY_FIRST_FAILURE

指定時(shí),顯示每個(gè)doctest中的第一個(gè)失敗示例,但禁止所有其他示例的輸出。這將防止doctest報(bào)告因早期故障而中斷的正確示例; 但它也可能隱藏不正確的例子,不依靠第一次失敗而失敗。當(dāng)REPORT_ONLY_FIRST_FAILURE指定時(shí),剩余的示例仍在運(yùn)行,并仍然計(jì)入報(bào)告的故障總數(shù); 只有輸出被抑制。

doctest.REPORTING_FLAGS

將上面的所有報(bào)告標(biāo)記掩蓋起來。

新的2.4版本:常數(shù)DONT_ACCEPT_BLANKLINE,NORMALIZE_WHITESPACE,ELLIPSIS,IGNORE_EXCEPTION_DETAIL,REPORT_UDIFF,REPORT_CDIFF,REPORT_NDIFF,REPORT_ONLY_FIRST_FAILURE,COMPARISON_FLAGS和REPORTING_FLAGS添加。

還有一種方法可以注冊(cè)新的選項(xiàng)標(biāo)志名稱,但除非您打算doctest通過子類擴(kuò)展內(nèi)部函數(shù),否則這種方法并不有用。

doctest.register_optionflag(name)

用給定名稱創(chuàng)建一個(gè)新選項(xiàng)標(biāo)志,并返回新標(biāo)志的整數(shù)值。register_optionflag()可用于子類化OutputChecker或DocTestRunner創(chuàng)建您的子類支持的新選項(xiàng)。register_optionflag()應(yīng)該總是使用以下習(xí)慣用法來調(diào)用:

MY_FLAG = register_optionflag('MY_FLAG')

New in version 2.4.

Directives

Doctest指令可用于修改單個(gè)示例的選項(xiàng)標(biāo)志。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 -和指令選項(xiàng)名稱之間不允許有空格。指令選項(xiàng)名稱可以是上面解釋的任何選項(xiàng)標(biāo)志名稱。

一個(gè)例子的doctest指令修改了doctest的這個(gè)例子的行為。使用+啟用這個(gè)名字的行為,或-將其禁用。

例如,這個(gè)測(cè)試通過:

>>> 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]

如果沒有指令,它會(huì)失敗,這是因?yàn)閷?shí)際輸出在單個(gè)數(shù)字列表元素之前沒有兩個(gè)空格,并且因?yàn)閷?shí)際輸出在單行上。這個(gè)測(cè)試也通過了,并且還需要一個(gè)指令來做到這一點(diǎn):

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

多條指令可用于單條物理線路,用逗號(hào)分隔:

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

如果單個(gè)示例使用多個(gè)指令注釋,則將它們合并:

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

如前例所示,您可以將...行添加到僅包含指令的示例中。當(dāng)一個(gè)例子對(duì)于指令很容易適合同一行時(shí)太長(zhǎng)了,這會(huì)很有用:

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

請(qǐng)注意,由于默認(rèn)情況下所有選項(xiàng)都被禁用,并且指令僅適用于它們出現(xiàn)的示例,因此啟用選項(xiàng)(通過+指令)通常是唯一有意義的選擇。但是,選項(xiàng)標(biāo)志也可以傳遞給運(yùn)行doctests的函數(shù),建立不同的默認(rèn)值。在這種情況下,通過-指令禁用選項(xiàng)可能很有用。

2.4版新增功能:增加了對(duì)doctest指令的支持。

警告

doctest嚴(yán)格要求在預(yù)期產(chǎn)出中要求完全匹配。如果即使單個(gè)字符不匹配,測(cè)試也會(huì)失敗。這可能會(huì)讓你感到驚訝,因?yàn)槟愦_切地知道Python做了什么,并且不能保證輸出。例如,在打印字典時(shí),Python不保證鍵值對(duì)將以任何特定的順序打印,因此像

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

很脆弱!一種解決方法是做

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

代替。另一個(gè)是要做的

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

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

另一個(gè)不好的想法是打印嵌入對(duì)象地址的東西,比如

>>> 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指令為最后一個(gè)示例提供了一個(gè)很好的方法:

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

浮點(diǎn)數(shù)也受到跨平臺(tái)的小輸出變化的影響,因?yàn)镻ython遵循平臺(tái)C庫(kù)進(jìn)行浮點(diǎn)格式化,而C庫(kù)在質(zhì)量上差別很大。

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

表格I/2.**J中的數(shù)字在所有平臺(tái)上都是安全的,而且我通常會(huì)編寫一些doctest的例子來生成這種格式的數(shù)字:

>>> 3./4  # utterly safe
0.75

簡(jiǎn)單的分?jǐn)?shù)對(duì)于人們來說也更容易理解,并且這使得更好的文檔。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)