C++ 文档生成器。
项目描述
乌尔法比
我们希望为我们的 C++ 项目提供一个可配置且易于使用的 Sphinx API 文档生成器。为了实现这一点,我们向其他人寻求灵感:
呼吸(https://github.com/michaeljones/breathe):优秀的扩展和许多人的默认选择。
Gasp ( https://github.com/troelsfr/Gasp):Gasp通过允许模板控制输出来启发我们。不幸的是,Gaps 的开发似乎已经停止。
那么什么是wurfapi:
本质上,我们找到了 Gasp 放手的地方。我们借用了模板的想法,使其高度可配置。
我们通过自动运行 Doxygen 来生成初始 API 文档,使其易于使用。
我们将 Doxygen XML 解析为易于使用的 Python 字典。可以在模板中使用。
我们为其他后端(替换 Doxygen)准备了扩展,例如 https://github.com/foonathan/standardese准备就绪后。
地位
我们目前在以下项目中使用 wurfapi:
… 还有很多。
用法
我们建议您在虚拟环境中安装 wurfapi 和 sphinx。要使用扩展程序,需要执行以下步骤:
创建虚拟环境:
Follow the https://docs.python.org/3/tutorial/venv.html
安装扩展:
pip install sphinx pip install wurfapi
通过运行生成初始Sphinx文档:
mkdir docs cd docs python sphinx-quickstart
您将需要输入有关您的项目的一些基本信息,例如项目名称等。
打开sphinx-quickstart生成的conf.py并添加以下内容:
# Append or insert 'wurfapi' in the extensions list extensions = ['wurfapi'] # wurfapi options - relative to your docs dir wurfapi = { 'source_paths': ['../src'], 'recursive': True, 'parser': {'type': 'doxygen', 'download': True, 'warnings_as_error': True} }
要为类生成 API 文档,请打开一个.rst文件,例如index.rst如果您运行sphinx-quickstart。假设我们要为命名空间项目中名为test的类生成文档。
为此,我们将以下指令添加到 rst 文件:
.. wurfapi:: class_synopsis.rst :selector: project::coffee::machine
这样index.rst就变成了这样:
Welcome to Coffee's documentation! =================================== .. toctree:: :maxdepth: 2 :caption: Contents: .. wurfapi:: class_synopsis.rst :selector: project::coffee::machine .. wurfapi:: class_synopsis.rst :selector: project::coffee::recipe Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` To do this we use the ``class_synopsis.rst`` template.
生成文档
制作html
标签和参考
为了引用 API 中的不同元素,我们添加了一个自定义 Sphinx 角色:wurfapi:
: wurfapi:角色将尝试从给定的文本中推断出唯一名称。例如,如果您想引用唯一名称 foo::bar::baz::func(std::string var)并且foo::bar::baz中没有其他名为func的成员函数,您可以通过以下方式引用它写作:wurfapi:`foo::bar::baz::func`。
另一方面,如果有一个具有唯一名称的函数 foo::bar::baz::function(std::string var) :wurfapi:`foo::bar::baz::func`可以与两个 func 匹配和函数,会抛出错误。在这种情况下,这可以通过添加左括号来解决::wurfapi:`foo::bar::baz::func(`。
您可以稍后在本自述文件中阅读有关唯一名称的更多信息。
在 readthedocs.org 上运行
要在 readthedocs.org 上使用它,您需要安装wurfapi Sphinx 扩展。这可以通过在文档文件夹中添加requirements.txt来完成。readthedocs.org 可以配置为 在构建项目时使用requirements.txt 。只需将wurfapi放入 requirements.txt即可。
氧气问题
没有什么是完美的,Doxygen 也不是。有时 Doxygen 会出错,例如在以下示例中:
class foo { private: class bar; };
Doxygen 错误地报告bar具有公共范围(也在此处报告 https://bit.ly/2BWPllZ)。要处理此类问题,在 Doxygen 中修复之前,您可以执行以下操作:
将 API 的补丁列表添加到您的conf.py文件中。扩展之前的示例,我们可以添加以下修复:
wurfapi = { 'source_paths': ['../src'], 'recursive': True, 'parser': { 'type': 'doxygen', 'download': True, 'warnings_as_error': True, 'patch_api': [ {'selector': 'foo::bar', 'key': 'access', 'value': 'private'} ] } }
patch_api允许您访问已解析的 API 信息并更新某些值。选择器是您要更新的实体的唯一名称。进一步查看“字典布局”部分以获取更多信息。
折叠内联命名空间
对于符号版本控制,您可以使用内联命名空间,但通常您不希望这些出现在文档中,因为这些对您的用户来说大多是不可见的。
使用wurfapi您可以折叠内联命名空间,以便将其从范围等中删除。
例子:
namespace foo { inline namespace v1_2_3 { struct bar{}; } }
bar 的范围是foo::v1_2_3。如果您折叠内联命名空间,它将只是foo。
您必须处理的第一个问题是 Doxygen 目前不支持内联命名空间。所以我们需要先给 API 打补丁:
wurfapi = { 'source_paths': ['../src'], 'recursive': True, 'parser': { 'type': 'doxygen', 'download': True, 'warnings_as_error': True, 'patch_api': [ {'selector': 'foo::v1_2_3', 'key': 'inline', 'value': True} ] } }
在此之后,我们可以折叠命名空间:
wurfapi = { 'source_paths': ['../src'], 'recursive': True, 'parser': { 'type': 'doxygen', 'download': True, 'warnings_as_error': True, 'patch_api': [ {'selector': 'foo::v1_2_3', 'key': 'inline', 'value': True} ], 'collapse_inline_namespaces': [ "foo::v1_2_3" ] } }
现在您可以将bar称为foo::bar。请注意,折叠命名空间会影响您在生成文档时编写的选择器。
自定义模板
您可以编写自己的自定义模板来生成第一个输出。为此,您只需编写一个兼容 Jinja2 的 rst 模板并将其放在某个文件夹中。将user_templates键添加到conf.py文件中的wurfapi 配置字典将使其可用。
例如:
wurfapi = { 'source_paths': ['../src', '../examples/header/header.h'], 'recursive': True, 'user_templates': 'rst_templates', 'parser': { 'type': 'doxygen', 'download': True, 'warnings_as_error': True } } exclude_patterns = ['rst_templates/*.rst']
现在我们可以在rst_templates文件夹中使用*.rst文件,例如,如果我们有一个class_list.rst模板,我们可以像这样使用它:
.. wurfapi:: class_list.rst :selector: project::coffee
发布新版本
编辑NEWS.rst、wscript和src/wurfapi/wurfapi.py(设置正确的VERSION)
跑
./waf upload
源代码
测试
通过将--run_tests传递给 waf,测试将自动运行:
./waf --run_tests
这遵循了似乎是“最佳实践”的建议,即在 virtualenv 中以可编辑模式安装包。
录音
一堆测试使用一个名为Record的类,在 ( test/record.py ) 中定义。Record类用于将输出存储为来自不同解析和渲染操作的文件。
例如,假设我们要确保解析器函数返回某个 dict对象。然后我们可以记录那个dict:
recorder = record.Record(filename='test.json', recording_path='/tmp/recording', mismatch_path='/tmp/mismatch') recorder.record(data={'foo': 2, 'bar': 3})
如果与之前的记录相比数据发生变化,则会检测到不匹配。要更新录音,只需删除录音文件。
测试目录
您还会注意到一堆测试采用了一个名为 testdirectory的参数。testdirectory是一个pytest夹具,它代表文件系统上的一个临时目录。运行测试时,您会注意到这些临时测试目录弹出 在项目根目录下的pytest_temp目录下。
您可以在此处阅读更多相关信息:
开发人员说明
关于创建扩展 的sphinx文档: http ://www.sphinx-doc.org/en/stable/extdev/index.html#dev-extensions
扩展是一个 Python 模块。当扩展加载时,Sphinx 将导入它并执行它的setup()函数。
理解如何将 docutils 节点放在一起似乎非常困难。邮件列表的一个建议是查看以下文档: https ://github.com/docutils-mirror/docutils/blob/master/test/functional/expected/standalone_rst_pseudoxml.txt
在研究如何做到这一点时,似乎有三种潜在的方法:
使用标准的 Sphinx 方法并使用 doctree 进行操作。
基于 jinja 模板创建 RST
基于 jinja 模板创建 HTML
灵感 - Sphinx 扩展,在开发此扩展时用作灵感。
了解如何使用 docutils 编写内容:* http://agateau.com/2015/docutils-snippets/
创建自定义指令 * http://www.xavierdupre.fr/blog/2015-06-07_nojs.html
漂亮的狮身人面像扩展 * https://github.com/bokeh/bokeh/tree/master/bokeh/sphinxext
这部分文档有助于理解指令 run(...) 函数中对 ViewLists 等的需求。 http://www.sphinx-doc.org/en/stable/extdev/markupapi.html
此链接为文本 json 格式提供了灵感:https ://github.com/micnews/html-to-article-json
更多 xml->json 文本:https ://www.xml.com/pub/a/2006/05/31/converting-between-xml-and-json.html
字典布局
我们希望支持不同的“后端”,例如 Doxygen 来解析源代码。为了使这成为可能,我们定义了一个内部源代码描述格式。然后我们将例如 Doxygen XML 翻译为此并使用它来呈现 API 文档。
这样,可以使用不同的“后端”(例如 Doxygen2)作为源代码解析器,并且可以生成 API 文档。
唯一名称
为了能够引用 API 中的不同实体,我们需要为它们分配一个名称。
我们在这里使用与standardese中描述的类似方法。
这意味着实体的唯一名称是具有所有范围的名称,例如foo::bar::baz。
对于函数,唯一名称包含签名(参数类型和成员函数 cv-qualifier 和 ref-qualifier),例如foo::bar::baz::func() 或foo::bar::baz::func(int a , 字符*) 常量。有关详细信息,请参阅cppreference。
对于类模板特化,唯一名称包括特化参数。例如:
// Here the unique-name is just 'foo' template<class T> class foo {}; // Here the unique name is foo<int> template<> class foo<int> {};
除了类型之外,我们还有已解析文件的条目。对于文件,唯一名称将是项目根目录的相对路径。
对于定义,我们将使用定义的名称。举个例子:
#define PROJECT_VERSION "1.0.0"
这里唯一名称将是PROJECT_VERSION。
API 字典
内部结构是具有不同 API 实体的字典。实体的 唯一名称是键,实体类型也是 Python 字典的值,例如:
api = { 'unique-name': { ... }, 'unique-name': { ... }, ... }
为了使这一点更具体,请考虑以下代码:
namespace ns1 { class shape { void print(int a) const; }; namespace ns2 { struct box { void hello(); }; void print(); } }
解析上述代码将生成以下 API 字典:
api = { 'ns1': { 'kind': 'namespace', ...}, 'ns1::shape': { 'kind': 'class', ... }, 'ns1::shape::print(int) const': { kind': function' ... }, 'ns1::ns2': { 'kind': 'namespace', ... }, 'ns1::ns2::box': { 'kind': 'struct', ... }, 'ns1::ns2::box::hello()': { kind': function' ... }, 'ns1::ns2::print()': { 'kind': 'function', ...}, 'ns1.hpp': { 'kind': 'file', ...} }
不同的实体类型公开了有关 API 的不同信息。我们将在下面记录不同的类型。
我们将一些键设置为可选的,其标记方式如下:
api = { 'unique-name': { 'some_key': ... Optional('an_optional_key'): ... }, ... }
命名空间种类
表示 C++ 命名空间的 Python 字典:
info = { 'kind': 'namespace', 'name': 'unqualified-name', 'scope': 'unique-name' | None, 'members: [ 'unique-name', 'unique-name' ], 'briefdescription': paragraphs, 'detaileddescription': paragraphs, 'inline': True | False }
注意:目前 Doxygen 不支持解析内联命名空间。因此,您需要使用补丁 API 手动将值从False更改为True 。也许在某个时候https://github.com/doxygen/doxygen/issues/6741 它会被支持。
班级| 结构种类
表示 C++ 类或结构的 Python 字典:
info = { 'kind': 'class' | 'struct', 'name': 'unqualified-name', 'location': location, 'scope': 'unique-name' | None, 'access': 'public' | 'protected' | 'private', Optional('template_parameters'): template_parameters, 'members: [ 'unique-name', 'unique-name' ], 'briefdescription': paragraphs, 'detaileddescription': paragraphs }
枚举| 枚举类种类
表示 C++ 枚举或枚举类的 Python 字典:
info = { 'kind': 'enum', 'name': 'unqualified-name', 'location': location, 'scope': 'unique-name' | None, 'access': 'public' | 'protected' | 'private', 'values: [ { 'name': 'somename', 'briefdescription': paragraphs, 'detaileddescription': paragraphs, Optional('value'): 'some value' } ], 'briefdescription': paragraphs, 'detaileddescription': paragraphs }
类型定义| 使用种类
表示 C++ using 或 typedef 语句的 Python 字典:
info = { 'kind': 'typedef' | 'using', 'name': 'unqualified-name', 'location': location, 'scope': 'unique-name' | None, 'access': 'public' | 'protected' | 'private', 'type': type, 'briefdescription': paragraphs, 'detaileddescription': paragraphs }
定义种类
代表 C/C++ 的 Python 字典定义:
info = { 'kind': 'define', 'name': 'name', 'location': location, Optional('initializer'): 'some_value', Optional('parameters'): [{ 'name': 'somestring', Optional('description'): paragraphs }], 'briefdescription': paragraphs, 'detaileddescription': paragraphs }
定义的内容将在初始化字段中。如果定义采用记录的参数,这些参数将位于参数键下。
例子:
定义初始化器:
#define VERSION "1.0.2"
用参数定义初始化器:
#define min(X, Y) ((X) < (Y) ? (X) : (Y))
文件种类
表示项目中文件的 Python 字典:
info = { 'kind': 'file', 'name': 'somefile.hpp', 'path': 'relative/path/to/somefile.hpp', }
功能种类
表示 C++ 函数的 Python 字典:
info = { 'kind': 'function', 'name': 'unqualified-name', 'location': location, 'scope': 'unique-name' | None, Optional('return'): { 'type': type, 'description': paragraphs } Optional('template_parameters'): template_parameters, 'is_const': True | False, 'is_static': True | False, 'is_virtual': True | False, 'is_explicit': True | False, 'is_inline': True | False, 'is_constructor': True | False, 'is_destructor': True | False, 'trailing_return': True | False, 'access': 'public' | 'protected' | 'private', 'briefdescription: paragraphs, 'detaileddescription: paragraphs, 'parameters': [ { 'type': type, Optional('name'): 'somename', 'description': paragraphs }, ... ] }
如果函数是构造函数或析构函数,则返回键是可选的。
可变种类
表示 C++ 变量的 Python 字典:
info = { 'kind': 'variable', 'name': 'unqualified-name', Optional('value'): 'some value', 'type': type, 'location': location, 'is_static': True | False, 'is_mutable': True | False, 'is_volatile': True | False, 'is_const': True | False, 'is_constexpr': True | False, 'scope': 'unique-name' | None, 'access': 'public' | 'protected' | 'private', 'briefdescription: paragraphs, 'detaileddescription: paragraphs, }
位置项目
表示位置的 Python 字典:
location = { Optional('include'): 'some/header.h', 'path': 'src/project/header.h', 'line': 10 }
包含将与 Sphinx conf.py中wurfapi字典中指定的 任何include_paths相关。
该路径将相对于项目根文件夹。
类型项目
表示 C++ 类型的 Python 列表:
type = [ { 'value': 'sometext', Optional('link'): link }, ... ]
将类型作为项目列表,我们可以创建到嵌套类型的链接,例如说我们有一个std::unique_ptr<impl>并且我们希望将impl设为链接。这可能看起来像:
"type": [ { "value": "std::unique_ptr<" }, { "link": {"url": False, "value": "project::impl"}, "value": "impl" }, { "value": ">" } ]
类型列表中的任何空格都应从 Doxygen 输出一直保留到类型列表中。首先,简单地输出类型的值就足够了。不应注入空格或其他内容。
链接项目
表示链接的 Python 字典:
link = { 'url': True | False, 'value': 'somestring' }
如果url是True我们有一个基本的外部引用,否则我们有一个指向 API 内部类型的链接。
参数项
表示函数参数的字典:
parameter = { 'type': type, Optional('name'): 'somestring', Optional('description'): paragraphs }
对于参数,名称也包含在类型列表中。原因是某些参数可能非常复杂,名称嵌入在类型中,例如:
void function(int (*(*foo)())[3]);
这是一个函数,它接受一个参数foo,它是指针函数,它返回指向 int 数组 3 的指针 - 不错吧?无论如何,在这种情况下,参数名称嵌入在参数的类型中。因此,我们采取了简单的做法,wurfapi将始终在类型中包含参数名称。
例如,函数void test(int b)的参数字典 可以是:
{ 'type': [{'value': 'int '}, {'value': 'b'}], 'name': 'b' }
模板参数项
表示模板参数的 Python 字典列表:
template_parameters = [{ 'type': type, 'name': 'somestring', Optional('default'): type, Optional('description'): paragraphs }]
文字信息
文本信息存储在列表段落中:
paragraphs = [paragraph]
一个段落由一系列段落元素组成:
paragraph = [ { "kind": "text" | "code" | "list" | "bold" | "italic", ... }, ]
段落元素可以是“文本”、“代码”或“列表”三种类型之一:
text = { 'kind': 'text', 'content': 'hello', Optional('link'): link } code = { 'kind': 'code', 'content': 'void print();', 'is_block': true | false } list = { 'kind': 'list', 'ordered': true | false, 'items': [paragraphs] # Each item is a list of paragraphs }
函数的唯一名称问题
问题等效的 C++ 函数签名可以用多种不同的方式编写:
void hello(const int *x); // x is a pointer to const int void hello(int const *x); // x is a pointer to const int
我们还可以将星号 ( * ) 向左移动:
void hello(const int* x); // x is a pointer to const int void hello(int const* x); // x is a pointer to const int
因此,在将函数签名转换为unique-name时,我们需要一些方法来规范化函数签名。我们不能简单地依赖刺痛比较。
根据大量的谷歌搜索,很难为此编写正则表达式。相反,我们将尝试使用解析器:
Python 解析器:https ://github.com/erezsh/lark
C++ 语法:http ://www.externsoft.ch/media/swf/cpp11-iso.html#parameters_and_qualifiers
我们只需要解析表示为 http://www.externsoft.ch/media/swf/cpp11-iso.html#parameters_and_qualifiers的函数参数列表。
生成的输出
由于我们将使用 Doxygen 的 XML 输出作为扩展的输入,我们需要一个地方来存储它。我们将它存储在系统临时文件夹中,例如,如果项目名称在 Linux 上为“foobar”, 则为 /tmp/wurfapi-foobar-123456,其中123456是源目录路径的哈希值。除了 Doxygen 的 XML,我们还为不同的指令存储生成的 rst。这对于调试以查看我们是否生成损坏的 rst 非常有用。
json 格式的 API 可以在_build/.doctree/wurfapi_api.json中找到。
路径和目录
源目录:在 Sphinx 中,源目录是我们的 .rst 文件所在的位置。这是您在构建文档时传递给sphinx-build的内容。我们将在我们的扩展中使用它来查找 C++ 源代码和输出自定义模板。
笔记
为什么使用src文件夹(https://hynek.me/articles/testing-packaging/)。tl;博士您应该在与您的用户运行您的代码相同的环境中运行您的测试。因此,通过将源文件放在不可导入的文件夹中,您可以避免意外访问未添加到您的用户将安装的 Python 包中的资源……
Python 打包指南:https ://packaging.python.org/distributing/