Skip to content

Commit

Permalink
Add new command sphinx-c-apidoc
Browse files Browse the repository at this point in the history
`sphinx-c-apidoc` will generate C documentation files for a provided
directory.

closes #5
  • Loading branch information
speedyleion committed Oct 19, 2020
1 parent 31a2c2f commit 4211a01
Show file tree
Hide file tree
Showing 11 changed files with 713 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ This project adheres to `Semantic Versioning <https://semver.org/>`_.
Added
-----

* The ``sphinx-c-apidoc`` command. This command provides users the ability to quickly
build up a set of documentation files for a C directory.

* A
`compilation databases <https://clang.llvm.org/docs/JSONCompilationDatabase.html>`_.
can now be specified with the ``c_autodoc_compilation_database`` configuration value.
Expand Down
103 changes: 103 additions & 0 deletions docs/apidoc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
apidoc
======

The `sphinx-c-apidoc` provides a way to generate documentation files for a C directory.
It is meant to fulfill a similar role to the `sphinx-apidoc`_ command for python
packages.

.. autoprogram:: sphinx_c_autodoc.apidoc:get_parser()
:prog: sphinx-c-apidoc

Generated Documentation Files
-----------------------------

The generated documentation files will follow the same directory structure of the
provide C source directory.

For example:

.. code-block:: text
a_project
├── file_1.h
├── file_2.c
├── some_dir
│ ├── another_dir
│ │ ├── file_3.h
│ │ └── file_4.c
would result in:

.. code-block:: text
doc_dir
├── files.rst
├── file_1_h.rst
├── file_2_c.rst
├── some_dir
│ ├── some_dir.rst
│ ├── another_dir
│ │ ├── another_dir.rst
│ │ ├── file_3_h.rst
│ │ └── file_4_c.rst
Where `a_project` was provided as the ``source_path`` and `doc_dir` was provided as the
``--output-path``. ``files.rst`` is the root index or table of contents file. By
default it only contains references to the other documentation files in the same
directory and any index files in sub directories.

``another_dir.rst`` is the index or table of contents file for the directory
`another_dir` it will only contain references the files in that directory as well as
any index files in subsequent sub directories.

Templates
---------

There are three jinja templates that are utilized for generating the documentation
files. These can be overridden by passing a directory, via the ``--templatedir``
option, containing any of the templates to override.

header.rst.jinja2
^^^^^^^^^^^^^^^^^

Controls the generation of files deemed to be header files, the ``--header-ext``
option.

Will be passed 2 arguments:

- ``filepath`` The relative path to the file. For ``file_3.h``, from the example
above in :ref:`apidoc:Generated Documentation Files`, this would be
``some_dir/another_dir/file_3.h``
- ``filename`` The name of the file, without relative directory. For ``file_3.h``,
from the example above in :ref:`apidoc:Generated Documentation Files`, this would
be ``file_3.h``


source.rst.jinja2
^^^^^^^^^^^^^^^^^

Controls the generation of files deemed to be source files, the ``--source-ext``
option.

Will be passed 2 arguments:

- ``filepath`` The relative path to the file. For ``file_4.c``, from the example
above in :ref:`apidoc:Generated Documentation Files`, this would be
``some_dir/another_dir/file_4.c``
- ``filename`` The name of the file, without relative directory. For ``file_4.c``,
from the example above in :ref:`apidoc:Generated Documentation Files`, this would
be ``file_4.c``

toc.rst.jinja2
^^^^^^^^^^^^^^

Controls the generation of index or table of contents files.

Will be passed 3 arguments:

- ``title`` The name of the index file without extension.
- ``maxdepth`` The ``-d`` option.
- ``doc_names`` The list of documentation files in the directory as well as those
in subdirectories.

.. _sphinx-apidoc: https://www.sphinx-doc.org/en/master/man/sphinx-apidoc.html
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
'sphinx.ext.viewcode',
'sphinx.ext.napoleon',
'sphinx_rtd_theme',
'sphinxcontrib.autoprogram',
'sphinx_c_autodoc',
'sphinx_c_autodoc.napoleon',
'sphinx_c_autodoc.viewcode',
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ C Autodoc Extension For Sphinx.
configuration
napoleon
viewcode
apidoc
developer_notes
contributing
changelog
Expand Down
6 changes: 6 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,16 @@
project_urls={
"Source": "https://github.com/speedyleion/sphinx-c-autodoc",
},
package_data={"": ["templates/*.jinja2"]},
install_requires=[
"sphinx>=3",
"clang>=6",
"beautifulsoup4",
],
entry_points={
"console_scripts": [
"sphinx-c-apidoc = sphinx_c_autodoc.apidoc:main",
]
},
python_requires=">=3.7",
)
222 changes: 222 additions & 0 deletions src/sphinx_c_autodoc/apidoc/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
"""
Provide apidoc like functionality for C projects.
"""
import argparse
import os

from pathlib import Path
from typing import Sequence, Optional

from sphinx.util.template import ReSTRenderer


def get_parser() -> argparse.ArgumentParser:
"""
Gets the argument parser for this module
Returns:
argparse.ArgumentParser: Argument parser to be used with this module.
"""
parser = argparse.ArgumentParser()
parser.add_argument(
"-o",
"--output-path",
help="Directory to place the output files. If it does not exist, it is "
"created",
)
parser.add_argument(
"-f",
"--force",
help="Force overwriting of any existing generated files",
action="store_true",
)
parser.add_argument("source_path", help="Path to C source files to be documented")
parser.add_argument(
"-t", "--templatedir", help="Template directory for template files"
)
parser.add_argument(
"--tocfile",
help="Filename for the root table of contents file (default: %(default)s)",
default="files",
)
parser.add_argument(
"-d",
dest="maxdepth",
help="Maximum depth for the generated table of contents file(s). "
"(default: %(default)s)",
default=4,
type=int,
)
parser.add_argument(
"--header-ext",
help='The extension(s) to use for header files (default: ["h"])',
action="append",
)
parser.add_argument(
"--source-ext",
help='The extension(s) to use for source files (default: ["c"])',
action="append",
)
return parser


def render_doc_file(
source_file: Path, doc_file: Path, template_name: str, user_template_dir: str
) -> None:
"""
Renders a documentation file, `doc_file`, for the provided `source_file`.
Args:
source_file: The source file to be documented.
doc_file: The resultant documentation file that will document `source_file`.
template_name: The name of the template to use to populate `doc_file` with.
user_template_dir: A directory to contain possible user overrides of
`template_name`.
"""
doc_file.parent.mkdir(parents=True, exist_ok=True)
context = {"filepath": "/".join(source_file.parts), "filename": source_file.name}
template_dir = os.path.dirname(__file__) + "/templates"
text = ReSTRenderer([user_template_dir, template_dir]).render(
template_name, context
)
doc_file.write_text(text)


def build_directory_docs(
source_path: Path, output_path: Path, toc_name: str, args: argparse.Namespace
) -> str:
"""
Recursively build the documentation for the `source_path`. Each file to be
documented will be added to the table of contents in `toc_name`. Any sub
directories which contain documented files will also be added to `toc_name`.
Args:
source_path: The path to the source directory to make documentation for.
output_path: The destination path for the generated documentation files.
toc_name: The name of this directories toc(table of contents) or index file.
args: Arguments used for generating the documentation file.
Returns:
str: The reference to `toc_name` that can be used by parent documentation
directories.
"""
doc_names = []
for entry in os.scandir(source_path):
path = Path(entry)
if entry.is_dir():
name = path.name
dir_doc = build_directory_docs(path, output_path / name, name, args)
if dir_doc:
doc_names.append(dir_doc)
if entry.is_file():
file_doc = create_file_documentation(path, output_path, args)
if file_doc:
doc_names.append(file_doc)

if doc_names:
return render_directory_index_file(output_path, toc_name, doc_names, args)
return ""


def create_file_documentation(
source_file: Path, output_path: Path, args: argparse.Namespace
) -> str:
"""
Attempts to create the file documentation for `source_file`.
Args:
source_file: The file to create documentation for.
output_path: The directory to place the generated file documentation.
args: Arguments used for generating the documentation file.
Returns:
str: The short name of the documentation file can be placed in a sibling
index file. Empty if this file shouldn't be documented.
"""
name = source_file.name
normalized_name = name.replace(".", "_")
doc_name = f"{normalized_name}.rst"
doc_file = output_path / doc_name
if not args.force and doc_file.exists():
return normalized_name

template = None

if name.endswith(args.header_ext):
template = "header.rst.jinja2"
elif name.endswith(args.source_ext):
template = "source.rst.jinja2"

if not template:
return ""

relative_path = source_file.relative_to(args.source_path)
render_doc_file(relative_path, doc_file, template, args.templatedir)
return normalized_name


def render_directory_index_file(
output_path: Path, index_name: str, doc_names: Sequence, args: argparse.Namespace
) -> str:
"""
Renders the template for the directory index file.
Args:
output_path: The location to render the directory index file in.
index_name: The name of the directory index file
doc_names: The list of files to provide in the director index file.
args: Arguments used for generating the index file.
Returns:
str: The name of the index file to be placed in parent index files.
"""
index_file = output_path / f"{index_name}.rst"
context = {
"doc_names": doc_names,
"maxdepth": args.maxdepth,
"title": index_name,
}
template_dir = os.path.dirname(__file__) + "/templates"
text = ReSTRenderer([args.templatedir, template_dir]).render(
"toc.rst.jinja2", context
)
index_file.write_text(text)
return f"{output_path.name}/{index_name}"


def main(argv: Optional[Sequence] = None) -> int:
"""
The main entry point for this module. Will parse the provided arguments(`argv`)
and generated the documentation files.
Args:
argv: The arguments to use for documentation generation. Use `--help` to see
the full documentation. If no arguments provided then sys.argv will be
used.
Returns:
int: 0 if success. Other than 0 on failure.
"""
parser = get_parser()
args = parser.parse_args(argv)

output_path = Path(args.output_path)
source_path = Path(args.source_path)

if not args.header_ext:
args.header_ext = ["h"]
args.header_ext = tuple(f".{ext}" for ext in args.header_ext)

if not args.source_ext:
args.source_ext = ["c"]
args.source_ext = tuple(f".{ext}" for ext in args.source_ext)

build_directory_docs(source_path, output_path, args.tocfile, args)
return 0


if __name__ == "__main__":
main()
4 changes: 4 additions & 0 deletions src/sphinx_c_autodoc/apidoc/templates/header.rst.jinja2
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{{ filename | heading }}

.. autocmodule:: {{ filepath }}
:members:
3 changes: 3 additions & 0 deletions src/sphinx_c_autodoc/apidoc/templates/source.rst.jinja2
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{{ filename | heading }}

.. autocmodule:: {{ filepath }}
7 changes: 7 additions & 0 deletions src/sphinx_c_autodoc/apidoc/templates/toc.rst.jinja2
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{{ title | heading }}

.. toctree::
:maxdepth: {{ maxdepth }}
{% for doc_name in doc_names %}
{{ doc_name }}
{%- endfor %}
Loading

0 comments on commit 4211a01

Please sign in to comment.