Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions .github/workflows/nightly-python-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: Deploy Python API Docs
on:
schedule:
- cron: '0 7 * * *'
workflow_dispatch:
jobs:
build:
name: 'Build Python API Docs (${{ matrix.gazebo_distribution }})'
runs-on: ubuntu-latest
container:
image: ubuntu:${{ matrix.ubuntu_distribution }}
strategy:
fail-fast: false
matrix:
include:
- ubuntu_distribution: jammy
gazebo_distribution: harmonic
gz_math_version: "7"
- ubuntu_distribution: noble
gazebo_distribution: ionic
gz_math_version: "8"
- ubuntu_distribution: noble
gazebo_distribution: jetty
gz_math_version: ""
steps:
- uses: ros-tooling/setup-ros@v0.7
- name: Set up Gazebo
uses: gazebo-tooling/setup-gazebo@v0.3.0
with:
required-gazebo-distributions: ${{ matrix.gazebo_distribution }}
- name: Install Python bindings and Sphinx
run: |
sudo apt-get install -y python3-pip
pip3 install sphinx furo sphinx-autodoc-typehints
sudo apt-get install -y python3-gz-math${{ matrix.gz_math_version }} || true
- name: Checkout docs repo
uses: actions/checkout@v4
- name: Build Python API docs
run: |
python3 tools/build_python_docs.py \
--distro ${{ matrix.gazebo_distribution }} \
--output python-api-build
- uses: actions/upload-artifact@v4
if: always()
with:
name: python-api-docs-${{ matrix.gazebo_distribution }}
path: python-api-build
if-no-files-found: warn
include-hidden-files: 'true'
upload:
name: Upload Python docs to production
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
concurrency:
group: pages
cancel-in-progress: false
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/download-artifact@v4
id: download
with:
path: .python-api-docs
pattern: python-api-docs-*
merge-multiple: false
- name: Install Python
run: |
sudo apt-get install -y python3-yaml
- run: python3 tools/restructure_python_artifacts.py --input ${{steps.download.outputs.download-path}} .python-api-out
- uses: actions/upload-artifact@v4
with:
name: python-api-docs
path: .python-api-out/*
if-no-files-found: warn
include-hidden-files: 'true'
- name: Commit
if: github.ref == 'refs/heads/master'
uses: JamesIves/github-pages-deploy-action@v4
with:
folder: ./.python-api-out
target-folder: api/python
clean: false
101 changes: 101 additions & 0 deletions tools/build_python_docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""
Build Sphinx autodoc documentation for Gazebo Python bindings.
Usage: python3 build_python_docs.py --distro harmonic --output python-api-build
"""

import argparse
import subprocess
import sys
import shutil
from pathlib import Path

# Map distro -> list of (module_name, package_name, lib_short_name)
PYTHON_BINDINGS = {
"harmonic": [
("gz.math7", "gz-math7", "math7"),
],
"ionic": [
("gz.math8", "gz-math8", "math8"),
],
"jetty": [
("gz.math", "gz-math", "math"),
],
}

CONF_PY_TEMPLATE = """\
project = "{project}"
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.napoleon",
"sphinx.ext.viewcode",
]
html_theme = "furo"
autodoc_default_options = {{
"members": True,
"undoc-members": True,
"show-inheritance": True,
}}
"""

INDEX_RST_TEMPLATE = """\
{project}
{underline}

.. automodule:: {module}
:members:
:undoc-members:
:show-inheritance:
"""


def build_docs(module_name, lib_short_name, output_dir: Path):
build_dir = output_dir / lib_short_name
src_dir = build_dir / "src"
src_dir.mkdir(parents=True, exist_ok=True)

project = f"Gazebo {lib_short_name} Python API"

(src_dir / "conf.py").write_text(CONF_PY_TEMPLATE.format(project=project))
(src_dir / "index.rst").write_text(
INDEX_RST_TEMPLATE.format(
project=project,
underline="=" * len(project),
module=module_name,
)
)

html_out = build_dir / "html"
result = subprocess.run(
["sphinx-build", "-b", "html", str(src_dir), str(html_out)],
capture_output=True, text=True
)
print(result.stdout)
if result.returncode != 0:
print("WARN: sphinx-build failed for", module_name, file=sys.stderr)
print(result.stderr, file=sys.stderr)
return False
return True


def main():
parser = argparse.ArgumentParser()
parser.add_argument("--distro", required=True)
parser.add_argument("--output", required=True)
args = parser.parse_args()

output_dir = Path(args.output)
output_dir.mkdir(parents=True, exist_ok=True)

bindings = PYTHON_BINDINGS.get(args.distro, [])
if not bindings:
print(f"No Python bindings configured for distro: {args.distro}")
sys.exit(0)

for module_name, pkg_name, lib_short_name in bindings:
print(f"Building docs for {module_name}...")
build_docs(module_name, lib_short_name, output_dir)


if __name__ == "__main__":
main()
55 changes: 55 additions & 0 deletions tools/restructure_python_artifacts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""
Restructure Python API doc artifacts for deployment.

Input structure:
python-api-docs-harmonic/math7/html/
python-api-docs-ionic/math8/html/

Output structure:
math/7/
math/8/
"""

import argparse
import re
import sys
import shutil
from pathlib import Path


def restructure(input_dir: Path, output_dir: Path):
output_dir.mkdir(parents=True, exist_ok=True)
for distro_dir in input_dir.iterdir():
if not distro_dir.is_dir():
continue
for lib_dir in distro_dir.iterdir():
if not lib_dir.is_dir():
continue
html_dir = lib_dir / "html"
if not html_dir.exists():
print(f"WARN: no html dir in {lib_dir}, skipping")
continue
# lib_dir.name is like "math7" or "math"
m = re.match(r"([a-z_]+)(\d*)", lib_dir.name)
if not m:
continue
lib_name, version = m.groups()
if not version:
version = "latest"
dest = output_dir / lib_name / version
dest.mkdir(parents=True, exist_ok=True)
print(f"{html_dir} -> {dest}")
shutil.copytree(html_dir, dest, dirs_exist_ok=True)


def main():
parser = argparse.ArgumentParser()
parser.add_argument("-i", "--input", dest="input_dir", required=True)
parser.add_argument("output_dir")
args = parser.parse_args()
restructure(Path(args.input_dir), Path(args.output_dir))


if __name__ == "__main__":
sys.exit(main())
Loading