如何编写 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 indexof 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 outof 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 的默认方式,除了一些插件。后来,我不断在谷歌上查询,我还是不满意我发现的插件——很多东西都过时了,没有人维护了,或者输出的东西不是我需要的——因此,我决定写一个我自己的项目。

“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 ErrorDuringImportas e:

print("Error while trying to import " + module)

决定输出

  在继续之前,你需要一个关于如何组织生成 markdown 输出的心理图像。思考:你需要一个不递归到自定义类的浅的引用吗?我们想要包含哪些方法?内置功能会怎么样?是用_还是__方法?我们应该如何呈现函数签名?我们应该拉注释吗?

  我的选择如下:

·每个运行一个 .md文件,其中包含递归到正在检查的对象的任意子类中生成的信息。

·只包括我创建的自定义代码,没有来自导入的模块的信息。

·每一项的输出必须用第二级 markdown标题(##)标识。

·所有头文件必须包含正在描述的项的完整路径(module.class.subclass.function)。

·将完整的函数签名作为预格式化文本。

·为每个标题提供锚点,以便轻松的链接到文档及文档本身内容。

·任何以_或者__开头的函数都不做文档记录。

整合在一起

  一旦对象被导入,我们可以开始检测了。这是一个简单的例子,重复调用 getmembers(object, filter),过滤器是一个有用的is函数。你能够发现isclassisfunction,其它相关的方法都是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 funcin 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__.pymoduleX导入到包中时,你可以通过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 并不是唯一的文档包,还有其他一些使用广泛的系统,如 Sphinxmkdocs基于此开发 )和Doxygen,他们都能实现我们以上讨论的事项。然而,我比较通过练习来学习和了解更多关于Python内部机制和其随附的工具。

 

来源:开源中国

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值