mkapi: Generate documentation

This commit is contained in:
Jeff Epler 2025-08-10 21:14:31 -05:00
parent 90e5da4b29
commit dddbf1d4bc
9 changed files with 293 additions and 105 deletions

29
.github/workflows/docs.yml vendored Normal file
View file

@ -0,0 +1,29 @@
name: Build docs
on:
push:
pull_request:
paths:
- docs/**
- py/**
- tests/cpydiff/**
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- name: Install Python packages
run: pip install -r docs/requirements.txt
- name: Submodules
run: make -C ports/m68kmac submodules
- name: Build unix port
run: source tools/ci.sh && ci_unix_build_helper
- name: Build docs
run: make -C docs/ html

View file

@ -3,7 +3,8 @@
# You can set these variables from the command line.
PYTHON = python3
SPHINXOPTS = -W --keep-going -j auto
#SPHINXOPTS = -W --keep-going -j auto
SPHINXOPTS = --keep-going -j auto
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = build/$(MICROPY_PORT)
@ -58,12 +59,18 @@ clean:
rm -rf $(BUILDDIR)/*
rm -f $(GENRSTDIR)/*
doc-pre: cpydiff apidoc
cpydiff:
@echo "Generating MicroPython Differences."
rm -f $(GENRSTDIR)/*
cd $(CPYDIFFDIR) && $(PYTHON) $(CPYDIFF)
html: cpydiff
apidoc:
@echo "Generating mac docs."
$(MAKE) -C ../ports/m68kmac doc
html: doc-pre
$(SPHINXBUILD) $(FORCE) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
@ -117,20 +124,20 @@ epub:
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex: cpydiff
latex: doc-pre
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf: cpydiff
latexpdf: doc-pre
$(SPHINXBUILD) $(FORCE) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
latexpdfja: cpydiff
latexpdfja: doc-pre
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja

View file

@ -0,0 +1,92 @@
{% if not obj.display %}
:orphan:
{% endif %}
:mod:`{{ obj.name }}`
======={{ "=" * obj.name|length }}
.. py:module:: {{ obj.name }}
{% if obj.docstring %}
.. autoapi-nested-parse::
{{ obj.docstring|prepare_docstring|indent(3) }}
{% endif %}
{% block subpackages %}
{% set visible_subpackages = obj.subpackages|selectattr("display")|list %}
{% if visible_subpackages %}
.. toctree::
:maxdepth: 2
{% for subpackage in visible_subpackages %}
{{ subpackage.short_name }}/index.rst
{% endfor %}
{% endif %}
{% endblock %}
{% block submodules %}
{% set visible_submodules = obj.submodules|selectattr("display")|list %}
{% if visible_submodules %}
.. toctree::
:titlesonly:
:maxdepth: 1
{% for submodule in visible_submodules %}
{{ submodule.short_name }}/index.rst
{% endfor %}
{% endif %}
{% endblock %}
{% block content %}
{% if obj.all is not none %}
{% set visible_children = obj.children|selectattr("short_name", "in", obj.all)|list %}
{% elif obj.type is equalto("package") %}
{% set visible_children = obj.children|selectattr("display")|list %}
{% else %}
{% set visible_children = obj.children|selectattr("display")|rejectattr("imported")|list %}
{% endif %}
{% if visible_children %}
{% set visible_classes = visible_children|selectattr("type", "equalto", "class")|list %}
{% set visible_functions = visible_children|selectattr("type", "equalto", "function")|list %}
{% if "show-module-summary" in autoapi_options and (visible_classes or visible_functions) %}
{% block classes %}
{% if visible_classes %}
Classes
~~~~~~~
.. autoapisummary::
{% for klass in visible_classes %}
{{ klass.id }}
{% endfor %}
{% endif %}
{% endblock %}
{% block functions %}
{% if visible_functions %}
Functions
~~~~~~~~~
.. autoapisummary::
{% for function in visible_functions %}
{{ function.id }}
{% endfor %}
{% endif %}
{% endblock %}
{% endif %}
{% for obj_item in visible_children %}
{{ obj_item.rendered|indent(0) }}
{% endfor %}
{% endif %}
{% endblock %}

View file

@ -49,6 +49,7 @@ micropy_authors = "MicroPython authors and contributors"
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
"autoapi.extension",
"sphinx.ext.autodoc",
"sphinx.ext.doctest",
"sphinx.ext.intersphinx",
@ -105,7 +106,7 @@ include_patterns = [
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ["build", ".venv"]
exclude_patterns = ["build", ".venv", "autoapi"]
# The reST default role (used for this markup: `text`) to use for all
# documents.
@ -337,3 +338,23 @@ texinfo_documents = [
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {"python": ("https://docs.python.org/3.5", None)}
# autoapi
autoapi_type = "python"
# Uncomment this if debugging autoapi
# autoapi_keep_files = True
autoapi_dirs = ["../ports/m68kmac/build/."]
autoapi_add_toctree_entry = False
autoapi_options = [
"members",
"undoc-members",
"private-members",
"show-inheritance",
"special-members",
"show-module-summary",
]
autoapi_template_dir = "autoapi/templates"
autoapi_python_class_content = "both"
autoapi_python_use_implicit_namespaces = False
autoapi_root = "library/mac"
autoapi_file_patterns = ["*.pyi"]

View file

@ -84,6 +84,19 @@ library.
zlib.rst
_thread.rst
.. _micropython_lib_m68kmac:
Macintosh-specific libraries
----------------------------
This section is auto-generated.
.. toctree::
:glob:
:maxdepth: 1
mac/**
.. _micropython_lib_micropython:
MicroPython-specific libraries
@ -108,90 +121,6 @@ the following libraries.
uctypes.rst
vfs.rst
The following libraries provide drivers for hardware components.
.. toctree::
:maxdepth: 1
wm8960.rst
Port-specific libraries
-----------------------
In some cases the following port/board-specific libraries have functions or
classes similar to those in the :mod:`machine` library. Where this occurs, the
entry in the port specific library exposes hardware functionality unique to
that platform.
To write portable code use functions and classes from the :mod:`machine` module.
To access platform-specific hardware use the appropriate library, e.g.
:mod:`pyb` in the case of the Pyboard.
Libraries specific to the pyboard
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The following libraries are specific to the pyboard.
.. toctree::
:maxdepth: 2
pyb.rst
stm.rst
lcd160cr.rst
Libraries specific to the WiPy
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The following libraries and classes are specific to the WiPy.
.. toctree::
:maxdepth: 2
wipy.rst
machine.ADCWiPy.rst
machine.TimerWiPy.rst
Libraries specific to the ESP8266 and ESP32
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The following libraries are specific to the ESP8266 and ESP32.
.. toctree::
:maxdepth: 2
esp.rst
esp32.rst
.. toctree::
:maxdepth: 1
espnow.rst
Libraries specific to the RP2040
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The following libraries are specific to the RP2040, as used in the Raspberry Pi Pico.
.. toctree::
:maxdepth: 2
rp2.rst
Libraries specific to Zephyr
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The following libraries are specific to the Zephyr port.
.. toctree::
:maxdepth: 2
zephyr.rst
.. _micropython_lib_extending:
Extending built-in libraries from Python

View file

@ -30,6 +30,6 @@ the Macintosh ``Rect`` type.
To create a new ``Rect`` on the MicroPython heap, call the rect constructor. You can initialize fields using kwargs: ``r = Rect(top=48, left=8, bottom=308, right=504)``.
To treat a memory region as a ``Rect``, use ``Rect`` as the descriptor argument of :fun:`uctypes.struct`: ``uctypes.struct(addr, Rect)``.
To treat a memory region as a ``Rect``, use ``Rect`` as the descriptor argument of :meth:`uctypes.struct`: ``uctypes.struct(addr, Rect)``.
A copy of a struct can be made with ``dest = src[:]`` and a struct can be updated in place with ``dest[:] = src``. The normal assignment operator ``dest = src`` merely makes ``dest`` refer to the same storage as ``src``. (Note that ``dest[:] = src`` does not currently type-check, but it may be made to do so in the future)

View file

@ -1,2 +1,5 @@
click
sphinx~=7.2.6
sphinx-autoapi
sphinx-rtd-theme
sphinxcontrib.jquery==4.1

View file

@ -5,9 +5,14 @@ $$(BUILD)/mod$(1).c: $(TOP)/tools/mkapi.py $(filter-out -t,$(2)) $$(DEFS)
$$(ECHO) "MKAPI $(1)"
$$(Q)$$(MKDIR) -p $$(BUILD)
$$(Q)$$(PYTHON) $(TOP)/tools/mkapi.py $(MKAPIFORMAT) -o $$@ -m $(1) $$(TDEFS) $(2)
$$(BUILD)/$(1).pyi: $(TOP)/tools/mkapi.py $(filter-out -t,$(2)) $$(DEFS)
$$(ECHO) "MKAPI $(1)"
$$(Q)$$(MKDIR) -p $$(BUILD)
$$(Q)$$(PYTHON) $(TOP)/tools/mkapi.py $(MKAPIFORMAT) --doc -o $$@ -m $(1) $$(TDEFS) $(2)
SRC_C += $(BUILD)/mod$(1).c
SRC_QSTR += $(BUILD)/mod$(1).c
mkapi:: $$(BUILD)/mod$(1).c
doc:: $$(BUILD)/$(1).pyi
endef
SRC_C += extmod/multiverse_support.c

130
tools/mkapi.py Normal file → Executable file
View file

@ -1,3 +1,5 @@
#!/usr/bin/python
import pathlib
import sys
import subprocess
@ -569,6 +571,8 @@ class Processor:
else:
if is_const:
base_type = self.parse_type(self.remove_const(base_type))
if isinstance(base_type, str):
return f"const {base_type}"
return dataclasses.replace(base_type, is_const=True)
return typestr
@ -592,6 +596,91 @@ class Processor:
if isinstance(d, FunPtr):
self.funptrs.add(d.name)
def document(self, defs):
self.body.append(f'"""{self.modname} - Wrapped MacOS APIs"""')
self.body.append("import array")
for d in defs:
try:
self.document_node(d)
except Exception as e:
raise RuntimeError(f"failed to document {d}") from e
def pytype(self, ctype):
if ctype is None:
return None
if ctype in {"LONGINT", "INTEGER"}:
return "int"
if ctype == "INTEGER*":
return "array.array['h']"
if ctype == "LONGINT*":
return "array.array['i']"
parsed = self.parse_type(ctype)
print("pytype", ctype, "->", repr(parsed))
if isinstance(parsed, Ptr):
return parsed.pointee
return ctype
@singledispatchmethod
def document_node(self, node):
if type(node) in self.unknowns:
return
self.unknowns.add(type(node))
raise RuntimeError(f"# Unknown {node!r:.68s}...")
@document_node.register
def document_typedef(self, typedef: Typedef):
print(typedef)
@document_node.register
def document_struct(self, struct: Struct):
self.body.append(f"class {struct.name}:")
self.body.append(' """A uctypes structure"""')
for m in struct.members:
self.body.append(f' {m.name}: "{self.pytype(m.type)}"')
@document_node.register
def document_union(self, union: Union):
self.body.append(f"class {union.name}:")
self.body.append(' """A uctypes union"""')
for m in union.members:
self.body.append(f' {m.name}: "{self.pytype(m.type)}"')
@document_node.register
def document_pyverbatim(self, p: PyVerbatim):
pass
@document_node.register
def document_function(self, f: Function):
self.body.append(f"def {f.name}(")
for i, m in enumerate(f.args):
argname = m.name or f"arg{i}"
self.body.append(f' {argname}: "{self.pytype(m.type)}",')
self.body.append(f") -> {self.pytype(f.return_)}: ...\n")
@document_node.register
def document_enum(self, e: Enum):
for m in e.values:
self.body.append(f"{m.name} : int")
@document_node.register
def document_lm(self, lm: LowMem):
# TODO type of LMGet/Set
self.body.append(f"def LMGet{lm.name}() -> object: ...")
self.body.append(f"def LMSet{lm.name}(value) -> None: ...")
@document_node.register
def document_dispatcher(self, d: Dispatcher):
pass
@document_node.register
def document_funptr(self, f: FunPtr):
pass
@document_node.register
def document_verbatim(self, lm: Verbatim):
pass
def emit(self, defs):
for d in defs:
print("emit", id(d), d)
@ -916,35 +1005,48 @@ class Processor:
"-t", "--typedefs", multiple=True, type=click.Path(path_type=pathlib.Path, exists=True)
)
@click.option("-m", "--modname", type=str)
@click.option("-d/-c", "--doc/--code", "do_doc", is_flag=True)
@click.option("--format/-no-format", "do_format")
def main(defs_files, output, modname, typedefs, do_format=False):
def main(defs_files, output, modname, typedefs, do_format=False, do_doc=False):
if modname is None:
modname = defs_files[0].stem
if output is None:
output = pathlib.Path(f"mod{modname}.c")
if do_doc:
output = pathlib.Path(f"{modname}.pyi")
else:
output = pathlib.Path(f"mod{modname}.c")
processor = Processor(modname)
for t in typedefs:
defs = load_defs(t)
processor.typedefs(defs)
if not do_doc:
processor.typedefs(defs)
defs = []
for f in defs_files:
defs.extend(load_defs(f))
processor.typedefs(defs)
processor.emit(defs)
if not do_doc:
processor.typedefs(defs)
if do_format:
tmpfile = output.with_suffix(".tmp.c")
with open(tmpfile, "w") as f:
processor.make_output(f)
print(f"Formatting {output}: ", [script_dir / "codeformat.py", "-c", tmpfile])
subprocess.check_call([script_dir / "codeformat.py", "-c", tmpfile])
tmpfile.rename(output)
else:
if do_doc:
processor.document(defs)
with open(output, "w") as f:
processor.make_output(f)
for line in processor.body:
print(line, file=f)
else:
processor.emit(defs)
if do_format:
tmpfile = output.with_suffix(".tmp.c")
with open(tmpfile, "w") as f:
processor.make_output(f)
print(f"Formatting {output}: ", [script_dir / "codeformat.py", "-c", tmpfile])
subprocess.check_call([script_dir / "codeformat.py", "-c", tmpfile])
tmpfile.rename(output)
else:
with open(output, "w") as f:
processor.make_output(f)
if __name__ == '__main__':