doctest 工作原理

2022-08-03 17:06 更新

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

哪些Docstrings被檢查?

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

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

<name of M>.__test__.K

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

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

Docstring示例如何被認可?

在大多數(shù)情況下,交互式控制臺會話的復制和粘貼工作正常,但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版本中進行了更改:將制表符擴展為空格是新的; 以前的版本試圖保留硬標簽,結果令人困惑。

  • 輸出到標準輸出被捕獲,但不輸出到標準錯誤(異常追溯通過不同的方式捕獲)。
  • 如果在交互式會話中通過反斜線繼續(xù)行,或者出于任何其他原因使用反斜杠,則應該使用原始文檔字符串,該字符串將按照鍵入時的方式保存反斜杠:
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

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

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

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

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

什么是例外?

沒問題,只要回溯是該示例生成的唯一輸出:只需粘貼回溯。[1]由于回溯包含可能快速變化的細節(jié)(例如,確切的文件路徑和行號),所以這是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堆棧,其內(nèi)容被doctest忽略?;厮荻褩Mǔ1缓雎?,或者從交互式會話逐字復制。

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

>>>

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

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

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

>>>

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

請注意,回溯處理非常特別。特別是,在改寫的例子中,使用...獨立于doctest的 ELLIPSIS選項。這個例子中的省略號可以省略,或者可以是三個(或三百個)逗號或數(shù)字,或者Monty Python skit的縮進記錄。

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

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

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

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

 Option Flags

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

第一組選項定義測試語義,控制doctest如何確定實際輸出是否與示例預期輸出相匹配的方面:

doctest.DONT_ACCEPT_TRUE_FOR_1

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

doctest.DONT_ACCEPT_BLANKLINE

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

doctest.NORMALIZE_WHITESPACE

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

doctest.ELLIPSIS

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

doctest.IGNORE_EXCEPTION_DETAIL

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

它也會忽略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也可以用于忽略異常消息的詳細信息,但根據(jù)是否將模塊詳細信息作為異常名稱的一部分進行打印,此類測試可能仍會失敗。使用IGNORE_EXCEPTION_DETAIL和來自Python 2.3的細節(jié)也是編寫文檔測試的唯一明確方式,它不關心異常細節(jié),但仍然在Python 2.3或更低版本中繼續(xù)傳遞(這些版本不支持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中的細節(jié)更改為“不”而不是“不”,但在Python 2.3以及更高版本的Python版本中通過了指定的標志。

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

doctest.SKIP

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

SKIP標志也可用于臨時“注釋”示例。

2.5版本中的新功能。

doctest.COMPARISON_FLAGS

將上面的所有比較標志掩蓋起來。

第二組選項控制如何報告測試失?。?/p>

doctest.REPORT_UDIFF

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

doctest.REPORT_CDIFF

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

doctest.REPORT_NDIFF

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

doctest.REPORT_ONLY_FIRST_FAILURE

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

doctest.REPORTING_FLAGS

將上面的所有報告標記掩蓋起來。

新的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添加。

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

doctest.register_optionflag(name)

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

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]

如果沒有指令,它會失敗,這是因為實際輸出在單個數(shù)字列表元素之前沒有兩個空格,并且因為實際輸出在單行上。這個測試也通過了,并且還需要一個指令來做到這一點:

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

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

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

警告

doctest嚴格要求在預期產(chǎn)出中要求完全匹配。如果即使單個字符不匹配,測試也會失敗。這可能會讓你感到驚訝,因為你確切地知道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...>

浮點數(shù)也受到跨平臺的小輸出變化的影響,因為Python遵循平臺C庫進行浮點格式化,而C庫在質(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ù)字在所有平臺上都是安全的,而且我通常會編寫一些doctest的例子來生成這種格式的數(shù)字:

>>> 3./4  # utterly safe
0.75

簡單的分數(shù)對于人們來說也更容易理解,并且這使得更好的文檔。


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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號