如何編寫 Python 文檔生成器

EdisonStuar 8年前發布 | 13K 次閱讀 Python Markdown Python開發

在我剛開始接觸Python的日子里,我最喜歡做的事情之一是坐在解釋器旁使用內置help功能來檢查類和方法,決定下一個要敲的內容。這個功能導入一個對象,遍布它的成員,取出文檔注釋,生成一個類似manpage的輸出,從而幫助你找到如何使用正在檢查的對象的方法。

它被內置成一個標準庫的美妙之處在于通過代碼直接生成輸出,它為我這樣的懶人間接地強調了一個編碼風格,我就想著在盡量少做額外的工作的情況下維護文檔。尤其是如果你已經為你的變量和函數選擇直接的名字。 這種風格涉及到向你的函數和類添加文檔字符串,以及通過用下劃線前綴來正確地識別私有成員和受保護成員。

Help on class list in module builtins:

class list(object)
 |  list() -> new empty list
 |  list(iterable) -> new list initialized from iterable's items
 |
 |  Methods defined here:
 |
 |  __add__(self, value, /)
 |      Return self+value.

...

 |  __iter__(self, /)
 |      Implement iter(self).

...

 |  append(...)
 |      L.append(object) -> None -- append object to end
 |
 |  extend(...)
 |      L.extend(iterable) -> None -- extend list by appending elements from the iterable
 |
 |  index(...)
 |      L.index(value, [start, [stop]]) -> integer -- return first index of value.
 |      Raises ValueError if the value is not present.

 ...

 |  pop(...)
 |      L.pop([index]) -> item -- remove and return item at index (default last).
 |      Raises IndexError if list is empty or index is out of range.
 |
 |  remove(...)
 |      L.remove(value) -> None -- remove first occurrence of value.
 |      Raises ValueError if the value is not present.

 ...

 |  ----------------------------------------------------------------------
 |  Data and other attributes defined here:
 |
 |  __hash__ = None

在Python解釋器上運行help(list)的輸出。

幫助功能實際上是使用 pydoc 模塊生成其輸出,它也可以從命令行運行,以產生您路徑中任何可導入模塊的文本或 html 表示。

不久以前,我需要編寫更細致,更正式的設計文檔。而作為一個 Markdown 的粉絲,我決定玩一玩mkdocs,看看是否能獲得我想要的結果。這個模塊可以很容易地將你的 markdown 文檔轉化為一個樣式美觀的頁面,并且,在發布為官方文檔之前,允許你對其做更改。它提供了一個 readthedocs  模板,深圳還提供了一個簡單的命令行界面,來把你的修改推送到  GitHub Pages  里面。

在完成了我最初的描述設計決策和注意事項的文本后,我想加上一些細節,描述我正在開發的實際接口方法。因為我已經為大部分的功能寫了定義,我想從源代碼自動生成參考頁面,還希望這個能在 markdown 里面。這樣的話,只要我想跑 mkdocs,它就可以連同我文檔的其余部分,一起渲染成 html。

然而,事實上還沒有一種從源碼生成 markdown 的默認方式,除了一些插件。后來,我不斷在谷歌上查詢,我還是不滿意我發現的插件——很多東西都過時了,沒有人維護了,或者輸出的東西不是我需要的——因此,我決定寫一個我自己的項目。我認為這很有意思,這也讓我學到了更多關于構建和調試一個模塊的知識,更多內容可以查看我之前的文章( 設計一個簡單的圖形 Python 調試器 ):inspect 模塊。

“inspect 模塊提供了幾個有用的函數來幫助我們獲取生存著的對象信息...  ” — Python 文檔

檢查!

Inspect,源自于標準程序庫,它不僅允許你查看較低級別的 python 框架和代碼對象,它還提供很多方法來檢查模塊和類,幫你發現可能感興趣的的項目。這個也就是之前提到的用來生成幫助文件的 pydoc。

瀏覽一下在線文檔,你會發現許多跟我們所做的嘗試相關的方法。最重要的幾個是getmembers(),getdoc() 和 signature(),還有許多給 getmembers 做濾波器的 is... 功能。擁有這些,我們可以輕易地循環訪問很多功能,包括區分生成器和協同程序,并可以按需要遞歸到任何一個類以及內部。

導入編碼

如果我們要去檢查一個對象,不管它是什么,第一步要做的是提供一個導入進我們的命名空間的原理。為何要討論導入呢?這取決于你想要做什么,還有很多需要擔心的,包括虛擬環境,自定義代碼,標準模塊和重命名。情況會容易混淆,搞錯的話會需要一些時間去整理清楚。

我們當然還有些選擇,更復雜的是直接從pydoc重用 safeimport (),當出現問題時,為我們解決很多特例和ErrorDuringImport類的特別條款。然而,如果我們對我們的環境需要更高的控制,我們自己簡單地運行__import__(modulename)也是可能的。

另一個需要記住的是每一個代碼的執行路徑。可能會用到 sys.path.append() 的一個目錄來進入我們尋找的模塊。我的用例 我的用例是從命令行和被檢查的模塊的路徑中的目錄執行,所以,我將當前目錄添加到 sys.path,這足以解決典型的導入路徑問題。

按照上述方式,我們的導入函數會如下所示:

def generatedocs(module):
    try:
        sys.path.append(os.getcwd())        # Attempt import
        mod = safeimport(module)        if mod is None:
           print("Module not found")        
        # Module imported correctly, let's create the docs
        return getmarkdown(mod)    except ErrorDuringImport as e:
        print("Error while trying to import " + module)

決定輸出

在繼續之前,你需要一個關于如何組織生成 markdown 輸出的心理圖像。思考:你需要一個不遞歸到自定義類的淺的引用嗎?我們想要包含哪些方法?內置功能會怎么樣?是用_還是__方法?我們應該如何呈現函數簽名?我們應該拉注釋嗎?

我的選擇如下:

  • 每個運行一個 .md 文件,其中包含遞歸到正在檢查的對象的任意子類中生成的信息。

  • 只包括我創建的自定義代碼,沒有來自導入的模塊的信息。

  • 每一項的輸出必須用第二級 markdown 標題(##)標識。

  • 所有頭文件必須包含正在描述的項的完整路徑(module.class.subclass.function)。

  • 將完整的函數簽名作為預格式化文本。

  • 為每個標題提供錨點,以便輕松的鏈接到文檔及文檔本身內容。

  • 任何以_或者__開頭的函數都不做文檔記錄。

整合在一起

一旦對象被導入,我們可以開始檢測了。這是一個簡單的例子,重復調用 getmembers(object, filter),過濾器是一個有用的 is 函數。你能夠發現 isclass 和 isfunction,其它相關的方法都是 is開頭的,例如,ismethod ,  isgenerator ,  iscoroutine。這都取決于你是否想寫一些通用的,可以處理所有的特殊情況,或一些更小的和更特殊的源代碼。我堅持前兩點,因為我不用把采用3個不同方法來創建我想要的格式化模塊,類和功能。

def getmarkdown(module):
    output = [ module_header ]
    output.extend(getfunctions(module)
    output.append("***\n")
    output.extend(getclasses(module))
    return "".join(output)def getclasses(item):
    output = list()    for cl in inspect.getmembers(item, inspect.isclass):        if cl[0] != "__class__" and not cl[0].startswith("_"):            # Consider anything that starts with _ private
            # and don't document it
            output.append( class_header )
            output.append(cl[0])   
            # Get the docstring
            output.append(inspect.getdoc(cl[1])            # Get the functions
            output.extend(getfunctions(cl[1]))            # Recurse into any subclasses
            output.extend(getclasses(cl[1])    return outputdef getfunctions(item):    for func in inspect.getmembers(item, inspect.isfunction):
        output.append( function_header )
        output.append(func[0])        # Get the signature
        output.append("\n```python\n)
        output.append(func[0])
        output.append(str(inspect.signature(func[1]))
        # Get the docstring
        output.append(inspect.getdoc(func[1])
    return output

當要格式化大文本和一些編程代碼的混合體時,我傾向于把它作為一個在列表或元組中的獨立項目,用 "".join() 來合并輸出。在寫作的時候,這種方法其實比基于插值的方法(.format 和 %)更快。然而,python 3.6 的新字符串格式化將比這更快,更具可讀性。

你可以看到,getmembers() 返回一個元組與對象的名稱在第一位置和第二位置的實際對象,我們可以用遞歸遍歷對象層次。

對于檢索到的每一個項目,可能使用 getdoc() 或 getcomments() 獲取文檔字符串和注釋。對于每一個功能,我們可以使用 signature() 得到 Signature 對象 ,它表示其位置參數和關鍵字參數的默認值和任何注釋,為我們提供了產生簡單直接的描述和良好風格的文本,有助于我們理解用戶我們寫代碼的意圖。

其他考慮因素和非預期后果

請注意,上面的代碼只是示例代碼,只是讓你大概真的最終產品應該是什么樣子。在最終確定產品之前,還有很多其他注意事項:

  • getfunctions 和 getclasses 將顯示模塊中導入的所有方法和類。包括內置程序包,以及來自外部軟件包的任何東西,所以你必須過濾掉更多的 for 循環。我在檢查過程中使用模塊的 __file__ 屬性,不管它包含什么項。換句話說,如果項在我正在執行的路徑中存在的模塊內定義,則包含它(使用 os.path.commonprefix())。

  • 有一些 gotcha 的文件路徑,導入層次結構和名稱。像通過 __init__.py 將 moduleX 導入到包中時,你可以通過 package.moduleX.function 訪問他的函數方法,但是全稱將會是 package.moduleX.moduleX.function—通過 moduleX.__name__ 返回的名稱。你或許不在乎這個區別,但是我在乎,所以這是在迭代過程中需要記住的事情。

  • 你會從內置程序庫中導入類和任何其他不包含 __file__ 的東西,如果你進行任何如上所述的過濾,那么檢查是必要的。

  • 因為這是 markdown,而我們只是導入 docstrings,你可以在你的 docstrings 中包含 mardown 語法,它會美觀漂亮的呈現在頁面中。然而,這意味著你應該注意正確的轉義 docstrings,這樣他才不會破壞生成的 HTML。

示例輸出

我在 sofi 包-精確的說是 sofi.app 模塊運行生成器,下面是它創建的 markdown 內容。

# sofi<a name="sofi"></a><a name="sofi.__init__"></a>### [sofi](#sofi).\_\_init\_\_
```python
__init__(self)
```
<a name="sofi.addclass"></a>
### [sofi](#sofi).addclass```python
addclass(self, selector, cl)```
Add the given class from all elements matching this selector.

下面是通過 mkdocs 運行它產生 readthedocs 主題頁面后的最終結果(不包括函數注釋)的示例。

我相信你已經知道,使用這些機制自動生成文檔,會生成完整、精確和最新的模塊信息,這些信息在你編寫代碼的時候可以進行維護和編寫,且操作簡單 。我強烈建議每個人都試一試。

在結束之前,我想再補充一點,mkdocs 并不是唯一的文檔包,還有其他一些使用廣泛的系統,如 Sphinx(mkdocs 基于此開發 )和 Doxygen,他們都能實現我們以上討論的事項。然而,我比較通過練習來學習和了解更多關于 Python 內部機制和其隨附的工具。

 

來自:https://www.oschina.net/translate/python-introspection-with-the-inspect-module

 

 本文由用戶 EdisonStuar 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!