Package: wurfapi
wurfapiWe wanted to have a configurable and easy-to-use Sphinx API documentation
generator for our C++ projects. To achieve this we leaned on others for
inspiration:Breathe (https://github.com/michaeljones/breathe): Excellent extension
and the default choice for many.Gasp (https://github.com/troelsfr/Gasp): Gasp inspired us by allowing
templates to control the output. Unfortunately development of Gaps
seems to have stopped.So what iswurfapi:Essentially we picked up where Gasp let go. We have
borrowed the idea of templates to make it highly configurable.We made it easy to use by automatically running Doxygen to generate the
initial API documentation.We parse the Doxygen XML into an easy to use Python dictionary. Which can
be consumed in the templates.We prepared the extension for other backends (replacing Doxygen) e.g.https://github.com/foonathan/standardeseonce they become ready.StatusWe currently use wurfapi in the following projects:https://rely.steinwurf.com/docs/10.0.0/https://kodo.steinwurf.com/docs/11.0.0/https://otacast.steinwurf.com/docs/6.0.0/… and many more.UsageWe recommend that you install wurfapi and sphinx in a virtual environment.
To use the extension, the following steps are needed:Create a virtual environment:Follow the https://docs.python.org/3/tutorial/venv.htmlInstall the extension:pip install sphinx
pip install wurfapiGenerate the initialSphinxdocumentation by running:mkdir docs
cd docs
python sphinx-quickstartYou will need to enter some basic information about your project such
as the project name etc.Open theconf.pygenerated bysphinx-quickstartand add the
the following:# 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}
}Notesource_pathIf you separate source and build dir in sphinx your ‘source_path’
should be something like ‘../../src’.recursiveSet recursiveTrueif you want recursively scan thesource_pathsdownloadIf you do not want to automatically download Doxygen, setdownloadtoFalse. In that casewurfapiwill try to invoke
plaindoxygenwithout specifying any path or similar. This means
itdoxygenmust be available in the path.warnings_as_errorIf Doxygen emits many warnings you might want to set warnings_as_error
to False until they have been fixed.To generate the API documentation for a class open a.rstfile
e.g.index.rstif you ransphinx-quickstart. Say we want to
generate docs for a class calledtestin the namespaceproject.To do this we add the following directive to the rst file:.. wurfapi:: class_synopsis.rst
  :selector: project::coffee::machineSuch thatindex.rstbecomes something like: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.Generate the Documentationmake htmlLabels and ReferencesTo reference different elements in the API, we have added a custom Sphinx role:wurfapi:The:wurfapi:role will try to deduce theunique-namefrom the text given.
E.g if you want to reference theunique-namefoo::bar::baz::func(std::stringvar)and there are
no other member functions infoo::bar::baznamedfunc, you can reference it
by writing:wurfapi:`foo::bar::baz::func`.On the other hand if there was a function withunique-namefoo::bar::baz::function(std::stringvar):wurfapi:`foo::bar::baz::func`could match with both func and function and will throw an error. In This
case this can be fixed by adding the left parenthesis::wurfapi:`foo::bar::baz::func(`.You can read more about unique names later in this README.Running on readthedocs.orgTo use this on readthedocs.org you need to have thewurfapiSphinx
extension installed. This can be done by adding arequirements.txtin the
documentation folder. readthedocs.org can be configured to use therequirements.txtwhen building a project. Simply putwurfapiin to therequirements.txt.Doxygen issuesNothing is perfect, neither is Doxygen. Sometimes Doxygen gets it wrong e.g. in
the following example:class foo
{
private:
    class bar;
};Doxygen incorrectly reports thatbarhas public scope (also reported herehttps://bit.ly/2BWPllZ). To deal with such issues, until a fix lands in
Doxygen, you can do the following:Add a list ofpatchesto the API to yourconf.pyfile. Extending the
example from before, we can add the following fix:wurfapi = {
  'source_paths': ['../src'],
  'recursive': True,
  'parser': {
    'type': 'doxygen', 'download': True,  'warnings_as_error': True,
     'patch_api': [
      {'selector': 'foo::bar', 'key': 'access', 'value': 'private'}
    ]
  }
}Thepatch_apiallows you to reach in to the parsed API information and
update certain values. Theselectoris theunique-nameof the
entity you want to update. Check the “Dictionary layout” section further down
for more information.Collapse inline namespacesFor symbol versioning you may useinline namespaces, however typically
you don’t want these to show up in the docs, as these are mostly
invisible for your users.Withwurfapiyou can collapse the inline namespace such that it
is removed form the scopes etc.Example:namespace foo { inline namespace v1_2_3 { struct bar{}; } }The scope to bar isfoo::v1_2_3. If you collapse the inline namespace it will
just befoo.First issue you have to deal with is that Doxygen currently does not
support inline namespaces. So we need to patch the API first: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}
    ]
  }
}After this we can collapse the namespace: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"
    ]
  }
}Now you will be able to refer tobarasfoo::bar. Note, that
collapsing the namespace will affect the selectors you write when
generating the documentation.Custom templatesYou can write your own custom templates for generating the rst output.
To this you simply write a Jinja2 compatible rst template and place
it in some folder. Adding theuser_templateskey to thewurfapiconfiguration dictionary in theconf.pyfile will make it available.For example: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']Now we can use*.rstfiles inside therst_templatesfolder e.g. if
we had aclass_list.rsttemplate we could use it like this:.. wurfapi:: class_list.rst
    :selector: project::coffeeRelease new versionEditNEWS.rst,wscriptandsrc/wurfapi/wurfapi.py(set
correctVERSION)Run./waf uploadSource codeTestsThe tests will run automatically by passing--run_teststo waf:./waf --run_testsThis follows what seems to be “best practice” advice, namely to install the
package in editable mode in a virtualenv.RecordingsA bunch of the tests use a library calledpytest-datarecorder.
The library is used to store the output as files from different parsing and
rendering operations.E.g. say we want to make sure that a parser function returns a certaindictobject. Then we can record thatdict:datarecorder.record_data(
    data={'foo': 2, 'bar': 3},
    recording_file="/tmp/recording/test.json"
)Ifdatachanges compared to a previous recording a mismatch will be
detected. To update a recording simply delete the recording file.Test directoriesYou will also notice that a bunch of the tests take a parameter calledtestdirectory. Thetestdirectoryis a pytest fixture, which
represents a temporary directory on the filesystem. When running the tests
you will notice these temporary test directories pop up under thepytest_tempdirectory in the project root.You can read more about that here:https://github.com/steinwurf/pytest-testdirectoryDeveloper NotesThesphinxdocumentation on creating extensions:http://www.sphinx-doc.org/en/stable/extdev/index.html#dev-extensionsAn extension is a Python module. When an extension loads, Sphinx will import
it and execute itssetup()function.Understanding how to put together docutils nodes seems pretty difficult. One
suggesting form the mailinglist was to look at the following document:https://github.com/docutils-mirror/docutils/blob/master/test/functional/expected/standalone_rst_pseudoxml.txtWhile researching how to do this, there seem to be three potential approaches:Use the standard Sphinx approach and operate with the doctree.Create RST based on jinja templatesCreate HTML based on jinja templatesInspiration - Sphinx extensions that were used as inspiration while
developing this extension.BreatheGasphttps://github.com/Robpol86/sphinxcontrib-imgurhttps://github.com/djungelorm/sphinx-tabsUnderstanding how to write stuff with docutils:
*http://agateau.com/2015/docutils-snippets/Creating a custom directive
*http://www.xavierdupre.fr/blog/2015-06-07_nojs.htmlNice looking Sphinx extensions
*https://github.com/bokeh/bokeh/tree/master/bokeh/sphinxextThis part of the documentation was useful in order to understand the need
for ViewLists etc. in the directives run(…) function.http://www.sphinx-doc.org/en/stable/extdev/markupapi.htmlThis link provided inspiration for the text json format:https://github.com/micnews/html-to-article-jsonMore xml->json for the text:https://www.xml.com/pub/a/2006/05/31/converting-between-xml-and-json.htmlDictionary layoutWe want to support different “backends” like Doxygen to parse the source
code. To make this possible we define an internal source code description
format. We then translate e.g. Doxygen XML to this and use that to render
the API documentation.This way a different “backend” e.g. Doxygen2 could be use used as the source
code parser and the API documentation could be generated.unique-nameIn order to be able to reference the different entities in the API we need
to assign them a name.We use a similar approach here as described instandardese.This means that theunique-nameof an entity is the name with all
scopes e.g.foo::bar::baz.For functions the unique name contains the signature (parameter types and for
member functions cv-qualifier and ref-qualifier) e.g.foo::bar::baz::func()orfoo::bar::baz::func(inta, char*) const. Seecppreferencefor more
information.For class template specializations the unique name includes the specialization
arguments. For example:// Here the unique-name is just 'foo'
template<class T>
class foo {};

// Here the unique name is foo<int>
template<>
class foo<int> {};In addition to types, we also have entries for the parsed files. For files
the unique name will be the relative path from the project root.For defines we will use the name of the define. As an example:#define PROJECT_VERSION "1.0.0"Hereunique-namewill bePROJECT_VERSION.The API dictionaryThe internal structure is a dicts with the different API entities. Theunique-nameof the entity is the key and the entity type also a
Python dictionary is the value e.g:api = {
  'unique-name': { ... },
  'unique-name': { ... },
  ...
}To make this a bit more concrete consider the following code:namespace ns1
{
  class shape
  {
    void print(int a) const;
  };

  namespace ns2
  {
    struct box
    {
      void hello();
    };

    void print();
  }
}Parsing the above code would produce the following API dictionary: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', ...}
}The different entity kinds expose different information about the
API. We will document the different kinds in the following.We make some keysoptionalthis is marked in the following way:api = {
  'unique-name': {
    'some_key': ...
    Optional('an_optional_key'): ...
  },
  ...
}namespaceKindPython dictionary representing a C++ namespace:info = {
  'kind': 'namespace',
  'name': 'unqualified-name',
  'scope': 'unique-name' | None,
  'members: [ 'unique-name', 'unique-name' ],
  'briefdescription': paragraphs,
  'detaileddescription': paragraphs,
  'inline': True | False
}Note: Currently Doxygen does not support parsinginline namespaces. So
you need to use the patch API to change the value fromFalsetoTruemanually. Maybe at some pointhttps://github.com/doxygen/doxygen/issues/6741it will be supported.class|structKindPython dictionary representing a C++ class or struct: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
}enum|enum classKindPython dictionary representing a C++ enum or enum class: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
}typedef|usingKindPython dictionary representing a C++ using or typedef statement:info = {
  'kind': 'typedef' | 'using',
  'name': 'unqualified-name',
  'location': location,
  'scope': 'unique-name' | None,
  'access': 'public' | 'protected' | 'private',
  'type': type,
  'briefdescription': paragraphs,
  'detaileddescription': paragraphs
}defineKindPython dictionary representing a C/C++ define:info = {
  'kind': 'define',
  'name': 'name',
  'location': location,
  Optional('initializer'): 'some_value',
  Optional('parameters'): [{
      'name': 'somestring',
      Optional('description'): paragraphs
  }],
  'briefdescription': paragraphs,
  'detaileddescription': paragraphs
}The content of the define will be in theinitializerfield. If the define
takes documented paremeters these will be under theparameterkey.Examples:Define initializer:#define VERSION "1.0.2"Define initalizer with parameters:#define min(X, Y)  ((X) < (Y) ? (X) : (Y))fileKindPython dictionary representing a file in the project:info = {
  'kind': 'file',
  'name': 'somefile.hpp',
  'path': 'relative/path/to/somefile.hpp',
}functionKindPython dictionary representing a C++ function: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 },
      ...
    ]
}Thereturnkey is optional if the function is either a constructor or
destructor.variableKindPython dictionary representing a C++ variable: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,
}locationitemPython dictionary representing a location:location = {
  Optional('include'): 'some/header.h',
  'path': 'src/project/header.h',
  'line': 10
  }Theincludewill be relative to anyinclude_pathsspecified in thewurfapidictionary in your Sphinxconf.py.Thepathwill be relative to the project root folder.typeitemPython list representing a C++ type:type = [
  {
    'value': 'sometext',
    Optional('link'): link
  }, ...
]Having the type as a list of items we can create links to nested types e.g.
say we have astd::unique_ptr<impl>and we would like to makeimpla link.
This could look like:"type": [
  {
    "value": "std::unique_ptr<"
  },
  {
    "link": {"url": False, "value": "project::impl"},
    "value": "impl"
  },
  {
    "value": ">"
  }
]Any spaces in the type list should be preserved all the way from the Doxygen
output and into the type list. In the rst it should be sufficient to simply
output the values of the type. No spaces or other stuff should be injected.linkitemPython dictionary representing a link:link = { 'url': True | False, 'value': 'somestring' }IfurlisTruewe have a basic extrenal reference otherwise we have a
link to an internal type in the API.parameteritemDictionary representing a function parameter:parameter = {
  'type': type,
  Optional('name'): 'somestring',
  Optional('description'): paragraphs
}For the parameter, the name is also included in the type list. The reason
is that some parameters can be pretty complex, with the name embedded
inside the type e.g.:void function(int (*(*foo)())[3]);This is a function that takes one parameterfoowhich is a pointer
function returning a pointer to array 3 of int - nice right? Anyway, in
such cases the parameter name is embedded inside the type of the parameter.
We therefore took the easy way out andwurfapiwill always include the
parameter name in the type.As an example the parameter dictionary for a functionvoid test(int b)could be:{
   'type': [{'value': 'int '}, {'value': 'b'}],
   'name': 'b'
}template_parametersitemPython list of dictionaries representing template parameters:template_parameters = [{
  'type': type,
  'name': 'somestring',
  Optional('default'): type,
  Optional('description'): paragraphs
}]Text InformationText information is stored in a list of paragraphs:paragraphs = [paragraph]A paragraph consists of a list of paragraph elements:paragraph = [
      {
        "kind": "text" | "code" | "list" | "bold" | "italic",
        ...
      },
    ]Paragraph elements can be one of three kinds, “text”, “code” or “list”: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
}Problem withunique-namefor functionsIssue equivalent C++ function signatures can be written in a number of
different ways:void hello(const int *x); // x is a pointer to const int
void hello(int const *x); // x is a pointer to const intWe can also move the asterisk (*) to the left:void hello(const int* x); // x is a pointer to const int
void hello(int const* x); // x is a pointer to const intSo we need some way to normalize the function signature when transforming it
tounique-name. We cannot simply rely on sting comparisons.According to the numerous google searches it is hard to write a regex for this.
Instead we will try to use a parser:Python parser:https://github.com/erezsh/larkC++ Grammar:http://www.externsoft.ch/media/swf/cpp11-iso.html#parameters_and_qualifiersWe only need to parse the function parameter list denoted as thehttp://www.externsoft.ch/media/swf/cpp11-iso.html#parameters_and_qualifiers.Generated outputSince we are going to be using Doxygen’s XML output as input to the
extension we need a place to store it. We store it system temporary folder e.g.
if the project name is “foobar” on Linux this would be/tmp/wurfapi-foobar-123456where123456is a hash of the source
directory paths. In addition to Doxygen’s XML we also store the generated rst
for the different directives there. This is nice for debugging to see whether
we generate broken rst.The API in json format can be found in the_build/.doctree/wurfapi_api.json.Paths and directoriesSource directory: In Sphinx the source directory is where our .rst files are
located. This is what you pass tosphinx-buildwhen building your
documentation. We will use this in our extension to find the C++ source code
and output customization templates.NotesWhy use ansrcfolder (https://hynek.me/articles/testing-packaging/).
tl;dr you should run your tests in the same environment as your users would
run your code. So by placing the source files in a non-importable folder you
avoid accidentally having access to resources not added to the Python
package your users will install…Python packaging guide:https://packaging.python.org/distributing/