diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index f680f43..eddb0a9 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -153,3 +153,19 @@ jobs: - name: Run publishing-script unit tests run: python3 -m unittest discover -s tests -v + + docs-site: + # The main documentation project and the shared package it installs, built + # the way Read the Docs builds them: every warning is a failure. + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-python@v7 + with: + python-version: "3.13" + + - run: pip install -r docs-site/requirements.txt + + - name: Build the main project + run: sphinx-build -W --keep-going -b html docs-site docs-site/_build/html diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..0e59418 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,17 @@ +# Read the Docs builds the PyDevices main documentation project from this +# repository (Settings > "Path for .readthedocs.yaml" stays at the default). +# The shared theme and configuration come from docs-config/, in the same tree. +version: 2 + +build: + os: ubuntu-24.04 + tools: + python: "3.13" + +sphinx: + configuration: docs-site/conf.py + fail_on_warning: true + +python: + install: + - requirements: docs-site/requirements.txt diff --git a/assets/css/docs-extra.css b/assets/css/docs-extra.css deleted file mode 100644 index 3152f80..0000000 --- a/assets/css/docs-extra.css +++ /dev/null @@ -1,295 +0,0 @@ -/* - * PyDevices brand design tokens & UI components for Material for MkDocs. - * - * MASTER COPY. This file is the source of truth for the documentation - * theme; scripts/generate_sites.py copies it verbatim into every repo that - * publishes a docs site (pygraphics, pdwidgets, palettes) as - * docs/stylesheets/extra.css. Edit it here, run the generator, and commit - * the synced copies -- a fix made in a repo's copy is silently reverted on - * the next run. - * - * Its palette deliberately mirrors the website chrome's, assets/css/site.css - * (deployed as PyDevices.github.io/assets/chrome/site.css). The two files - * use different token *names* because Material for MkDocs dictates its own - * (--md-*); the underlying colour values are meant to match. Change one, - * change the other. - * - * EVERY color in this file is defined in one of the three token blocks - * below. The component rules further down reference tokens only — never a - * literal — so a palette change is a single edit to this token area. - * - * Channel tokens follow the website convention: `---rgb: R G B;` - * (space-separated channels), consumed as `rgb(var(--name-rgb))` for the - * solid color and `rgb(var(--name-rgb) / 0.12)` for an alpha variant. A - * color gets an `-rgb` token only when something actually composes alpha - * over it; opaque-only colors are plain hex. - */ - -:root > * { - /* Brand channels — the source of every orange in this file. */ - --doc-brand-rgb: 34 199 226; - /* Material paints the header bar with --md-primary-fg-color and sets its - label text in --md-primary-bg-color. The bright accent is far too - luminous to carry near-white text (1.95:1), so the header takes a deeper - cyan of the same family (5.4:1); the bright one stays for links, tints - and accents. */ - --doc-header-rgb: 10 113 134; - --doc-primary-bg-rgb: 240 253 255; - --doc-accent-bg-rgb: 4 22 28; - - --md-primary-fg-color: rgb(var(--doc-header-rgb)); - --md-primary-fg-color--light: #5edcf0; - --md-primary-fg-color--dark: #08657a; - --md-primary-bg-color: rgb(var(--doc-primary-bg-rgb)); - --md-primary-bg-color--light: rgb(var(--doc-primary-bg-rgb) / 0.7); - - --md-accent-fg-color: #5edcf0; - --md-accent-fg-color--transparent: rgb(var(--doc-brand-rgb) / 0.1); - --md-accent-bg-color: rgb(var(--doc-accent-bg-rgb)); - --md-accent-bg-color--light: rgb(var(--doc-accent-bg-rgb) / 0.7); - - /* Semantic status colors. Scheme-independent by design: the badges read - the same in light and dark. */ - --doc-success-rgb: 46 125 50; - --doc-success: rgb(var(--doc-success-rgb)); - --doc-error: #d32f2f; - - /* Neutral black used only at low alpha, for drop shadows and the hairline - outline on a color swatch. */ - --doc-shade-rgb: 0 0 0; - - /* Label color on a filled brand-colored button. */ - --doc-on-primary: #fff; - - /* Live WebAssembly demo canvas. These describe the emulated device screen - rather than the site chrome, so they stay fixed across schemes and need - not follow a brand palette change. */ - --demo-canvas-bg: #000000; - --demo-canvas-frame: #17222c; - --demo-canvas-border: #333333; -} - -[data-md-color-scheme="default"] { - --md-default-bg-color: #f5f9fb; - --md-default-bg-color--light: #e9f1f5; - --md-default-bg-color--lighter: #eff6f9; - --md-default-bg-color--lightest: #ffffff; - --md-default-fg-color: #0c161d; - --md-default-fg-color--light: #47606f; - --md-default-fg-color--lighter: #6f8695; - --md-default-fg-color--lightest: #a7bccc; - --md-typeset-a-color: #08657a; - --md-code-bg-color: #eff6f9; - --md-code-fg-color: #0c161d; - --md-footer-bg-color: #e9f1f5; - --md-footer-bg-color--dark: #0c161d; - --md-footer-fg-color: #0c161d; - --md-footer-fg-color--light: #47606f; - --md-footer-fg-color--lighter: #6f8695; -} - -[data-md-color-scheme="slate"] { - --md-hue: 205; - --md-default-bg-color: #080c10; - --md-default-bg-color--light: #0d131a; - --md-default-bg-color--lighter: #121a22; - --md-default-bg-color--lightest: #17222c; - --md-default-fg-color: #dfeaf2; - --md-default-fg-color--light: #a7bccc; - --md-default-fg-color--lighter: #6f8598; - --md-default-fg-color--lightest: #47606f; - --md-typeset-a-color: #7de8f7; - --md-code-bg-color: #121a22; - --md-code-fg-color: #dfeaf2; - --md-footer-bg-color: #0d131a; - --md-footer-bg-color--dark: #080c10; - --md-footer-fg-color: #dfeaf2; - --md-footer-fg-color--light: #a7bccc; - --md-footer-fg-color--lighter: #6f8598; -} - -/* Hero & Feature Grid Cards */ -.hero-banner { - background: linear-gradient(135deg, rgb(var(--doc-brand-rgb) / 0.08) 0%, rgb(var(--doc-brand-rgb) / 0.02) 100%); - border: 1px solid rgb(var(--doc-brand-rgb) / 0.2); - border-radius: 12px; - padding: 1.75rem; - margin-bottom: 2rem; -} - -.hero-banner h1 { - margin-top: 0; - color: var(--md-primary-fg-color); -} - -.grid.cards { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: 1rem; - margin: 1.5rem 0; -} - -.grid.cards > div, .grid.cards > a { - display: flex; - flex-direction: column; - background: var(--md-default-bg-color--light); - border: 1px solid var(--md-default-fg-color--lightest); - border-radius: 8px; - padding: 1.25rem; - text-decoration: none; - color: var(--md-default-fg-color); - transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; -} - -.grid.cards > div:hover, .grid.cards > a:hover { - transform: translateY(-2px); - border-color: var(--md-primary-fg-color); - box-shadow: 0 4px 12px rgb(var(--doc-brand-rgb) / 0.12); -} - -/* Badge and Swatch utilities */ -.badge { - display: inline-flex; - align-items: center; - gap: 0.35rem; - padding: 0.2rem 0.55rem; - font-size: 0.75rem; - font-weight: 600; - border-radius: 9999px; - background: var(--md-default-bg-color--lighter); - color: var(--md-default-fg-color); - border: 1px solid var(--md-default-fg-color--lightest); -} - -.badge-orange { - background: rgb(var(--doc-brand-rgb) / 0.12); - color: var(--md-primary-fg-color); - border-color: rgb(var(--doc-brand-rgb) / 0.3); -} - -.badge-green { - background: rgb(var(--doc-success-rgb) / 0.12); - color: var(--doc-success); - border-color: rgb(var(--doc-success-rgb) / 0.3); -} - -/* Live direct MicroPython WebAssembly demos */ -.pydevices-live-demo { - display: flex; - flex-wrap: wrap; - gap: 1.25rem; - background: var(--md-code-bg-color); - border: 1px solid var(--md-default-fg-color--lightest); - border-radius: 10px; - padding: 1.25rem; - margin: 1.5rem 0; - box-shadow: 0 2px 8px rgb(var(--doc-shade-rgb) / 0.05); -} - -.demo-editor-pane { - flex: 1 1 340px; - display: flex; - flex-direction: column; - gap: 0.6rem; - min-width: 280px; -} - -.code-editor { - font-family: var(--md-code-font-family, monospace); - font-size: 0.85rem; - line-height: 1.45; - background: var(--md-default-bg-color); - color: var(--md-default-fg-color); - border: 1px solid var(--md-default-fg-color--lighter); - border-radius: 6px; - padding: 0.75rem; - height: 230px; - resize: vertical; - tab-size: 4; -} - -.code-editor:focus { - outline: none; - border-color: var(--md-primary-fg-color); - box-shadow: 0 0 0 2px rgb(var(--doc-brand-rgb) / 0.2); -} - -.demo-controls { - display: flex; - align-items: center; - gap: 0.6rem; -} - -.run-btn { - background: var(--md-primary-fg-color); - color: var(--doc-on-primary); - border: none; - padding: 0.35rem 0.9rem; - font-size: 0.82rem; - font-weight: 600; - border-radius: 4px; - cursor: pointer; - transition: background 0.15s ease; -} - -.run-btn:hover:not(:disabled) { - background: var(--md-primary-fg-color--dark); -} - -.run-btn:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.reset-btn { - background: var(--md-default-bg-color--lighter); - color: var(--md-default-fg-color); - border: 1px solid var(--md-default-fg-color--lighter); - padding: 0.35rem 0.75rem; - font-size: 0.82rem; - border-radius: 4px; - cursor: pointer; -} - -.reset-btn:hover { - background: var(--md-default-bg-color--light); -} - -.demo-status { - font-size: 0.78rem; - color: var(--md-default-fg-color--lighter); - margin-left: auto; - font-weight: 500; -} - -.demo-output { - font-family: var(--md-code-font-family, monospace); - font-size: 0.78rem; - color: var(--doc-error); - margin: 0; - max-height: 80px; - overflow-y: auto; - white-space: pre-wrap; -} - -.demo-canvas-pane { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - background: var(--demo-canvas-bg); - border: 1px solid var(--demo-canvas-frame); - border-radius: 8px; - padding: 0.75rem; -} - -.demo-canvas-pane canvas { - image-rendering: pixelated; - border: 1px solid var(--demo-canvas-border); - outline: none; - background: var(--demo-canvas-bg); -} - -.demo-canvas-pane canvas:focus { - border-color: var(--md-primary-fg-color); -} diff --git a/docs-config/.gitignore b/docs-config/.gitignore new file mode 100644 index 0000000..a8d8cd8 --- /dev/null +++ b/docs-config/.gitignore @@ -0,0 +1,3 @@ +build/ +*.egg-info/ +__pycache__/ diff --git a/docs-config/README.md b/docs-config/README.md new file mode 100644 index 0000000..bbd0bac --- /dev/null +++ b/docs-config/README.md @@ -0,0 +1,103 @@ +# pydevices-docs + +The shared look and configuration of the PyDevices documentation projects: a +Sphinx theme (`sphinx_rtd_theme` with a PyDevices palette), the top bar that +links every project to every other, the Markdown setup, the Google-style +docstring setup, and the reference map between projects. + +Each project's `docs/conf.py` calls `configure()` and sets only its name and +where its code is. This is what `palettes` does: + +```python +from pydevices_docs import configure + +globals().update(configure( + "palettes", __file__, + repo="PyDevices/pydevices", edit_path="palettes/docs", + source_dirs=["../lib"], api_packages=["palettes"], +)) +``` + +## Use it in a project + +Put this line in the project's `docs/requirements.txt`, naming the release you +want: + +``` +pydevices-docs @ git+https://github.com/PyDevices/.github@docs-v0.1.0#subdirectory=docs-config +``` + +Then add `conf.py`, an `index.md`, and a `.readthedocs.yaml` that sets +`fail_on_warning: true`. Put a `reference/index` entry in a toctree to get +the API pages (see below). + +## What `configure()` sets up + +- Extensions: `myst_parser` (Markdown), `sphinx_design` (cards and tabs), + `autodoc`, `napoleon` (Google style only), `intersphinx`, `viewcode`. +- API pages: with `api_packages=[...]`, one page per public module is written + to `reference/` before each build, plus `reference/index.rst`. List + `reference/` in the project's `.gitignore`. +- `mock_imports`: modules autodoc should not import (hardware modules a + desktop build lacks). +- The reference map: the Python and MicroPython manuals, and every other + PyDevices project whose `objects.inv` answers. A project that is not + published yet is left out, so it cannot fail a `-W` build. + `PYDEVICES_DOCS_OFFLINE=1` leaves all of them out. +- The version, read from the file named by `version_file` (default + `../VERSION`). + +## The top bar + +`pydevices_docs/projects.py` lists the projects. A project is in the bar once +its `live` flag is true, which takes a new package release. Links are the Read +the Docs addresses; `pydevices-bar.js` rewrites them to the host serving the +page when that is the main project's site (or a subproject under it), so a +custom domain, a preview or a local server keeps readers where they are. + +## Preview a project + +```bash +python -m venv .venv-docs +.venv-docs/bin/pip install -r docs/requirements.txt +.venv-docs/bin/sphinx-build -W --keep-going -b html docs docs/_build/html +``` + +`sphinx-autobuild docs docs/_build/html` rebuilds as you edit. + +## Live demos + +A page can embed a runnable MicroPython demo: raw HTML in a Markdown page, +using the classes `pydevices-live-demo`, `demo-editor-pane`, `code-editor`, +`run-btn`, `reset-btn`, `demo-status`, `demo-output` and `demo-canvas-pane` +(this package styles them). The theme loads the runner, +`https://PyDevices.github.io/assets/chrome/docs-runtime.js`, on any page that +has one; the page needs no script tag. See the palettes `index.md` for a +complete demo. + +## What moving the three MkDocs sites taught + +- **A flag key is a flag.** In `autodoc_default_options`, `"ignore-module-all": + False` turns the option on; leave the key out. `configure()` does. +- **Re-exports document twice.** A package that re-exports its names + (`pdwidgets`, with lazy widgets) shows every class twice unless pages list + only what a module defines. That is the default here. A package whose public + names live in private modules (`pygraphics`) sets `use_module_all=True`. +- **Property docstrings.** Napoleon reads `Face style: flat or raised.` on a + property as a type and a description, swapped. `configure()` turns that off + for properties. +- **Write `\\n` in a docstring, or use a raw string.** A bare `\n` is a real + newline, and the line after it falls out of the paragraph: a warning that + `-W` turns into a failure. +- **Cross-references.** MkDocs docstrings used `[Name][path]`. Sphinx uses a + single backtick (`default_role` is `py:obj`): `` `Name ` ``. The three + libraries' docstrings were converted. +- **Links in Markdown.** MyST treats a link that starts with `/` as a + cross-reference to a document. Link to another project with its full + `https://` address. +- **Demos need no special headers.** They run with and without + cross-origin isolation (COOP/COEP `credentialless`), which was checked with + both. Setting COEP does block Read the Docs' own analytics beacon, so + leave it off. +- **Docstring-only edits are enough.** The libraries' code did not change; + only docstrings and the docs folder did. diff --git a/docs-config/pyproject.toml b/docs-config/pyproject.toml new file mode 100644 index 0000000..56b9536 --- /dev/null +++ b/docs-config/pyproject.toml @@ -0,0 +1,27 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "pydevices-docs" +version = "0.1.0" +description = "Shared Sphinx theme and configuration for the PyDevices documentation projects." +readme = "README.md" +requires-python = ">=3.10" +license = { text = "MIT" } +dependencies = [ + "sphinx>=7.3", + "sphinx_rtd_theme>=3.0", + "myst-parser>=3.0", + "sphinx-design>=0.6", + "sphinxcontrib-mermaid>=1.0", +] + +[project.entry-points."sphinx.html_themes"] +pydevices = "pydevices_docs" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +pydevices_docs = ["theme/*", "theme/static/*", "templates/*"] diff --git a/docs-config/src/pydevices_docs/__init__.py b/docs-config/src/pydevices_docs/__init__.py new file mode 100644 index 0000000..5796a4f --- /dev/null +++ b/docs-config/src/pydevices_docs/__init__.py @@ -0,0 +1,226 @@ +"""Shared Sphinx configuration and theme for the PyDevices documentation projects. + +A project's ``conf.py`` is five lines: + + from pydevices_docs import configure + + globals().update(configure( + "palettes", __file__, repo="PyDevices/pydevices", edit_path="palettes/docs", + source_dirs=["../lib"], api_packages=["palettes"], + )) + +``configure`` returns the settings; the project sets only its name and where +its code is. The theme, the shared top bar, the cross-project reference map +and the Markdown setup all come from here. +""" + +from __future__ import annotations + +import os +import urllib.request +from pathlib import Path + +from .projects import GITHUB, MAIN, PORTAL, PROJECTS, ROOT, live_projects + +__version__ = "0.1.0" + +THEME_DIR = Path(__file__).parent / "theme" +TEMPLATES_DIR = Path(__file__).parent / "templates" + +__all__ = ["configure", "setup", "live_projects", "__version__"] + + +def _keep_property_docstrings_plain(): + """Napoleon reads "Face style: flat or raised." on a property as a type + ("Face style") and a description, which swaps the two. Our property + docstrings are sentences, so leave them alone. + """ + from sphinx.ext.napoleon.docstring import GoogleDocstring + + original = GoogleDocstring._parse_attribute_docstring + if getattr(original, "_pydevices_plain", False): + return + + def _parse_attribute_docstring(self): + if getattr(self, "_what", None) == "property": + return self._consume_to_end() + return original(self) + + _parse_attribute_docstring._pydevices_plain = True + GoogleDocstring._parse_attribute_docstring = _parse_attribute_docstring + + +def setup(app): + """Sphinx entry point: registers the `pydevices` theme.""" + _keep_property_docstrings_plain() + app.add_html_theme("pydevices", str(THEME_DIR)) + return {"version": __version__, "parallel_read_safe": True, "parallel_write_safe": True} + + +def _reachable(url: str) -> bool: + """True when a published inventory answers, so a project that is not on + the site yet does not turn into an intersphinx warning (and a failed + `-W` build).""" + try: + req = urllib.request.Request(url, method="HEAD") + with urllib.request.urlopen(req, timeout=5) as resp: + return resp.status == 200 + except Exception: + return False + + +def intersphinx_map(skip: str = "") -> dict: + """The reference map: the Python and MicroPython manuals plus every other + live PyDevices project whose inventory is published. + + Set ``PYDEVICES_DOCS_OFFLINE=1`` to leave out every sibling, for a build + with no network. + """ + mapping = { + "python": ("https://docs.python.org/3", None), + "micropython": ("https://docs.micropython.org/en/latest", None), + } + if os.environ.get("PYDEVICES_DOCS_OFFLINE"): + return {} + for p in live_projects(): + if p.name == skip: + continue + key = "pydevices" if p.main else p.name + if _reachable(p.inventory): + mapping[key] = (p.url, None) + return mapping + + +def _read_version(conf_file: str, version_file: str | None) -> str: + if not version_file: + return "" + path = (Path(conf_file).parent / version_file).resolve() + try: + return path.read_text().strip() + except OSError: + return "" + + +def configure( + name: str, + conf_file: str, + *, + repo: str | None = None, + edit_path: str = "docs", + source_dirs: list[str] | tuple[str, ...] = (), + api_packages: list[str] | tuple[str, ...] = (), + api_skip: list[str] | tuple[str, ...] = (), + use_module_all: bool = False, + mock_imports: list[str] | tuple[str, ...] = (), + version_file: str | None = "../VERSION", + main: bool = False, + copyright_from: int = 2024, + extra_extensions: list[str] | tuple[str, ...] = (), +) -> dict: + """Return the Sphinx settings for one PyDevices project. + + name the project's name, as the bar and the title show it + conf_file the project's ``conf.py`` (pass ``__file__``) + repo "PyDevices/", for the Edit on GitHub link (default PyDevices/) + edit_path the folder holding ``conf.py``, relative to the repo root ("palettes/docs") + source_dirs folders, relative to ``conf.py``, put on ``sys.path`` for autodoc + api_packages packages to write API reference pages for (see ``pydevices_docs.apigen``) + api_skip folder names inside those packages to leave out of the API pages ("icons") + use_module_all document what a package lists in ``__all__``, re-exports included, instead + of only what the module defines (for a package whose API is private modules) + mock_imports modules autodoc should treat as importable without importing + version_file file, relative to ``conf.py``, holding the version; None for no version + main True for the organization's main project + """ + import sys + + conf_dir = Path(conf_file).resolve().parent + for d in source_dirs: + p = str((conf_dir / d).resolve()) + if p not in sys.path: + sys.path.insert(0, p) + + repo = repo or f"PyDevices/{name}" + release = _read_version(conf_file, version_file) + conf_path = "/" + edit_path.strip("/") + "/" + + extensions = [ + "myst_parser", + "sphinx_design", + "sphinxcontrib.mermaid", + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "sphinx.ext.intersphinx", + "sphinx.ext.viewcode", + *extra_extensions, + ] + if api_packages: + extensions.append("pydevices_docs.apigen") + + # A flag in autodoc_default_options is on whenever its key is present, even + # with a False value, so add ignore-module-all only when it is wanted. + autodoc_options = {"members": True, "show-inheritance": True} + if not use_module_all: + autodoc_options["ignore-module-all"] = True + + settings = { + "project": name, + "copyright": f"{copyright_from}-2026, Brad Barnett & PyDevices Contributors", + "author": "PyDevices Contributors", + "release": release, + "version": release, + "extensions": extensions, + "exclude_patterns": ["_build", "README.md", "Thumbs.db", ".DS_Store"], + "source_suffix": {".rst": "restructuredtext", ".md": "markdown"}, + "master_doc": "index", + # A single backtick in a docstring is a cross-reference: `Palette` or `text `. + "default_role": "py:obj", + # Markdown (MyST) + "myst_enable_extensions": ["colon_fence", "deflist", "attrs_block", "attrs_inline", "html_image", "tasklist"], + "myst_heading_anchors": 3, + # Docstrings: Google style only. + "mermaid_version": "11.4.1", + "napoleon_google_docstring": True, + "napoleon_numpy_docstring": False, + "napoleon_use_param": True, + "napoleon_use_rtype": False, + "autodoc_member_order": "bysource", + "autodoc_typehints": "none", + "autodoc_default_options": autodoc_options, + "autodoc_mock_imports": list(mock_imports), + "api_packages": list(api_packages), + "api_skip": list(api_skip), + "api_source_dirs": [str((conf_dir / d).resolve()) for d in source_dirs], + "intersphinx_mapping": intersphinx_map(skip="home" if main else name), + "intersphinx_timeout": 10, + # Theme + "html_theme": "pydevices", + "templates_path": [str(TEMPLATES_DIR)], + "html_theme_options": { + "navigation_depth": 3, + "collapse_navigation": False, + "sticky_navigation": True, + "titles_only": False, + "logo_only": False, + }, + "html_title": name if main else f"{name} documentation", + "html_favicon": None, + "html_static_path": [], + "html_show_sphinx": False, + "html_context": { + "display_github": True, + "github_user": repo.split("/")[0], + "github_repo": repo.split("/")[1], + "github_version": "main", + "conf_py_path": conf_path, + "pd_project": name, + "pd_is_main": main, + "pd_projects": [ + {"name": p.name, "title": p.title, "url": p.url, "path": p.path} + for p in live_projects() + ], + "pd_portal": PORTAL, + "pd_github": GITHUB, + }, + } + return settings diff --git a/docs-config/src/pydevices_docs/apigen.py b/docs-config/src/pydevices_docs/apigen.py new file mode 100644 index 0000000..7a34b73 --- /dev/null +++ b/docs-config/src/pydevices_docs/apigen.py @@ -0,0 +1,64 @@ +"""Write the API reference pages for a project from its package folders. + +This stands in for the MkDocs `gen-files` script the three library sites used: +for each package named in `api_packages`, one `reference/.rst` page +per public module, each an `automodule`, plus `reference/index.rst`. Modules +and packages whose name starts with an underscore are skipped. + +The pages are generated into the source tree, so list `reference/` in the +project's .gitignore and put `reference/index` in a toctree. +""" + +from __future__ import annotations + +from pathlib import Path + +OUT = "reference" + + +def _write(path: Path, text: str) -> None: + if path.exists() and path.read_text() == text: + return # leave the mtime alone so Sphinx does not rebuild the page + path.write_text(text) + + +def _modules(source_dirs, package, skip=()): + for base in source_dirs: + pkg_dir = Path(base) / package + if not pkg_dir.is_dir(): + continue + found = [] + for path in sorted(pkg_dir.rglob("*.py")): + rel = path.relative_to(base).with_suffix("") + parts = list(rel.parts) + if parts[-1] == "__init__": + parts = parts[:-1] + if not parts or any(p.startswith("_") or p in skip for p in parts): + continue + found.append(".".join(parts)) + return found + return [] + + +def generate(app): + srcdir = Path(app.srcdir) + out = srcdir / OUT + out.mkdir(exist_ok=True) + names = [] + for package in app.config.api_packages: + for module in _modules(app.config.api_source_dirs, package, app.config.api_skip): + title = f"{module}" + body = f"{title}\n{'=' * len(title)}\n\n.. automodule:: {module}\n" + _write(out / f"{module}.rst", body) + names.append(module) + index = "API reference\n=============\n\n.. toctree::\n :maxdepth: 1\n\n" + index += "".join(f" {n}\n" for n in names) + _write(out / "index.rst", index) + + +def setup(app): + app.add_config_value("api_packages", [], "env") + app.add_config_value("api_source_dirs", [], "env") + app.add_config_value("api_skip", [], "env") + app.connect("builder-inited", generate) + return {"version": "1", "parallel_read_safe": True, "parallel_write_safe": True} diff --git a/docs-config/src/pydevices_docs/projects.py b/docs-config/src/pydevices_docs/projects.py new file mode 100644 index 0000000..a89bc1e --- /dev/null +++ b/docs-config/src/pydevices_docs/projects.py @@ -0,0 +1,60 @@ +"""The projects that share the PyDevices documentation site. + +This list is the shared top bar and the cross-project reference map. A project +with `live = False` is planned: it is not in the bar and not in the +intersphinx map until the package release that flips it. Adding a project is +one entry here and a new package version. +""" + +from dataclasses import dataclass + +# Where the site lives until a custom domain is attached to the main project. +# Every address below hangs off it; the bar rewrites them to whatever host is +# serving the page (see theme/static/pydevices-bar.js). +ROOT = "https://pydevices.readthedocs.io" +PORTAL = "https://pydevices.github.io" +GITHUB = "https://github.com/PyDevices" + + +@dataclass(frozen=True) +class Project: + name: str # the Read the Docs subproject alias, and the name in the bar + title: str + summary: str + repo: str # "PyDevices/" or a repo plus folder + live: bool = False + main: bool = False + + @property + def path(self) -> str: + """Where the project sits under the main project's address.""" + return "/en/latest/" if self.main else f"/projects/{self.name}/en/latest/" + + @property + def url(self) -> str: + return ROOT + self.path + + @property + def inventory(self) -> str: + return self.url + "objects.inv" + + +# The main project is the organization overview; it is not a library. +MAIN = Project( + name="home", + title="PyDevices", + summary="The organization overview: what each project is for and how to start.", + repo="PyDevices/.github", + live=True, + main=True, +) + +PROJECTS = ( + Project("palettes", "palettes", "Color palettes, RGB565 helpers and an HSV wheel.", "PyDevices/pydevices", live=True), + Project("pygraphics", "pygraphics", "2D drawing primitives, fonts and blitting in C and Python.", "PyDevices/pygraphics", live=True), + Project("pdwidgets", "pdwidgets", "A pure-Python widget toolkit for touch displays.", "PyDevices/pydevices", live=True), +) + + +def live_projects(): + return [MAIN, *[p for p in PROJECTS if p.live]] diff --git a/docs-config/src/pydevices_docs/templates/layout.html b/docs-config/src/pydevices_docs/templates/layout.html new file mode 100644 index 0000000..bf734d1 --- /dev/null +++ b/docs-config/src/pydevices_docs/templates/layout.html @@ -0,0 +1,28 @@ +{# The shared top bar, laid over sphinx_rtd_theme. Found through templates_path, so "!layout.html" is the theme's own. #} +{%- extends "!layout.html" %} + +{%- block extrahead %} + {{ super() }} + + + +{%- endblock %} + +{%- block extrabody %} + {{ super() }} + +{%- endblock %} diff --git a/docs-config/src/pydevices_docs/theme/static/logo.svg b/docs-config/src/pydevices_docs/theme/static/logo.svg new file mode 100644 index 0000000..8605012 --- /dev/null +++ b/docs-config/src/pydevices_docs/theme/static/logo.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/docs-config/src/pydevices_docs/theme/static/pydevices-bar.js b/docs-config/src/pydevices_docs/theme/static/pydevices-bar.js new file mode 100644 index 0000000..87516ce --- /dev/null +++ b/docs-config/src/pydevices_docs/theme/static/pydevices-bar.js @@ -0,0 +1,22 @@ +/* Point the shared top bar at whichever host is serving this page. + * + * The links in the HTML are the Read the Docs addresses, which work from + * anywhere. When the page is served from the main project's site (its own + * pages, or a subproject under /projects/), the bar uses that site's origin + * instead, so a custom domain, a preview, or a local server keeps readers on + * the host they came from. A project viewed at its own standalone address + * keeps the Read the Docs links. + */ +(function () { + var bar = document.querySelector(".pd-bar"); + if (!bar) return; + var path = location.pathname; + var root = null; + var i = path.indexOf("/projects/"); + if (i >= 0) root = path.slice(0, i); + else if (bar.getAttribute("data-pd-main") === "true") root = ""; + if (root === null) return; + bar.querySelectorAll("a[data-pd-path]").forEach(function (a) { + a.href = location.origin + root + a.getAttribute("data-pd-path"); + }); +})(); diff --git a/docs-config/src/pydevices_docs/theme/static/pydevices-demo.js b/docs-config/src/pydevices_docs/theme/static/pydevices-demo.js new file mode 100644 index 0000000..1e8e0ea --- /dev/null +++ b/docs-config/src/pydevices_docs/theme/static/pydevices-demo.js @@ -0,0 +1,13 @@ +/* Load the runner for live demos, on pages that have one. + * + * A demo is a `.pydevices-live-demo` block in the page's HTML. The runner is + * the one the PyDevices website and galleries share; it boots MicroPython as + * WebAssembly (the PyDevices stack is built in) and runs each demo's code + * into its canvas. + */ +(function () { + if (!document.querySelector(".pydevices-live-demo")) return; + import("https://PyDevices.github.io/assets/chrome/docs-runtime.js").catch(function (error) { + console.error("Unable to load the PyDevices documentation runtime:", error); + }); +})(); diff --git a/docs-config/src/pydevices_docs/theme/static/pydevices.css b/docs-config/src/pydevices_docs/theme/static/pydevices.css new file mode 100644 index 0000000..a9d5b27 --- /dev/null +++ b/docs-config/src/pydevices_docs/theme/static/pydevices.css @@ -0,0 +1,217 @@ +/* + * The PyDevices documentation look, laid over sphinx_rtd_theme. + * + * Its palette mirrors the website chrome (assets/css/site.css in this + * repository, deployed as PyDevices.github.io/assets/chrome/site.css): + * the same brand cyan and the same dark panels. Every colour is a token in + * the block below, so a palette change is one edit here and one there. + */ + +:root { + --pd-brand-rgb: 34 199 226; + --pd-brand: rgb(var(--pd-brand-rgb)); + --pd-brand-soft: #5edcf0; + --pd-header-rgb: 10 113 134; /* deep cyan: 5.4:1 under near-white text */ + --pd-header: rgb(var(--pd-header-rgb)); + --pd-link: #08657a; + --pd-link-hover: #0a7186; + --pd-ink: #0c161d; + --pd-ink-soft: #47606f; + --pd-bg: #ffffff; + --pd-bg-soft: #f5f9fb; + --pd-bg-code: #eff6f9; + --pd-border: #cfdde6; + --pd-dark: #0c1218; /* the bar and the sidebar */ + --pd-dark-2: #121a22; + --pd-dark-ink: #dfeaf2; + --pd-dark-soft: #a7bccc; + --pd-success-rgb: 46 125 50; + --pd-error: #d32f2f; + --pd-shade-rgb: 0 0 0; + --pd-on-primary: #fff; + /* The emulated device screen in a live demo: fixed in every scheme. */ + --pd-canvas-bg: #000000; + --pd-canvas-frame: #17222c; + --pd-canvas-border: #333333; + + --pd-bar-h: 2.5rem; +} + +/* ---- The shared top bar -------------------------------------------- */ + +.pd-bar { + position: fixed; + top: 0; left: 0; right: 0; + height: var(--pd-bar-h); + z-index: 500; + display: flex; + align-items: center; + gap: 1rem; + padding: 0 1rem; + background: var(--pd-dark); + color: var(--pd-dark-ink); + border-bottom: 2px solid var(--pd-brand); + font-family: Lato, "proxima-nova", "Helvetica Neue", Arial, sans-serif; + font-size: 0.9rem; + box-sizing: border-box; +} +.pd-bar a { color: var(--pd-dark-soft); text-decoration: none; white-space: nowrap; } +.pd-bar a:hover { color: var(--pd-brand-soft); } +.pd-bar .pd-brand { + display: inline-flex; align-items: center; gap: 0.5rem; + color: var(--pd-dark-ink); font-weight: 700; flex: none; +} +.pd-bar .pd-projects { + display: flex; gap: 0.25rem; list-style: none; margin: 0; padding: 0; + overflow-x: auto; flex: 1 1 auto; scrollbar-width: none; +} +.pd-bar .pd-projects::-webkit-scrollbar { display: none; } +.pd-bar .pd-projects li { margin: 0; padding: 0; list-style: none; } +.pd-bar .pd-projects a { display: block; padding: 0.15rem 0.6rem; border-radius: 4px; } +.pd-bar .pd-projects a:hover { background: rgb(var(--pd-brand-rgb) / 0.12); } +.pd-bar .pd-projects a.pd-current { + color: var(--pd-dark-ink); + background: rgb(var(--pd-brand-rgb) / 0.18); + box-shadow: inset 0 -2px 0 var(--pd-brand); +} +.pd-bar .pd-bar-links { display: flex; gap: 1rem; margin-left: auto; flex: none; } +@media (max-width: 640px) { + .pd-bar { gap: 0.5rem; padding: 0 0.5rem; } + .pd-bar .pd-brand span, .pd-bar .pd-bar-links { display: none; } +} + +/* Make room for the bar: the theme pins its sidebar to the top of the window. */ +.wy-grid-for-nav { top: var(--pd-bar-h); height: calc(100% - var(--pd-bar-h)); } +.wy-nav-side { top: var(--pd-bar-h); background: var(--pd-dark); } +.wy-side-nav-search, .wy-nav-top { background: var(--pd-header); } +.wy-nav-side.shift { top: var(--pd-bar-h); } +html { scroll-padding-top: var(--pd-bar-h); } +body.wy-body-for-nav { padding-top: 0; } + +/* ---- Sidebar and page ---------------------------------------------- */ + +.wy-menu-vertical a { color: var(--pd-dark-soft); } +.wy-menu-vertical a:hover { background: var(--pd-dark-2); color: var(--pd-dark-ink); } +.wy-menu-vertical li.current { background: var(--pd-dark-2); } +.wy-menu-vertical li.current > a, +.wy-menu-vertical li.toctree-l1.current > a { background: #17222c; color: var(--pd-dark-ink); border-color: var(--pd-border); } +.wy-menu-vertical li.toctree-l2.current > a, +.wy-menu-vertical li.toctree-l2.current li.toctree-l3 > a { background: var(--pd-dark-2); } +.wy-menu-vertical li.current a { border-right: 1px solid #23323f; } +.wy-menu-vertical li.on a, .wy-menu-vertical li.current > a { color: var(--pd-dark-ink); } +.wy-menu-vertical li.current ul li a, +.wy-menu-vertical li.toctree-l2 a, +.wy-menu-vertical li.toctree-l3 a, +.wy-menu-vertical li.toctree-l4 a { color: var(--pd-dark-soft); } +.wy-menu-vertical li.toctree-l2.current > a, +.wy-menu-vertical li.toctree-l3.current > a { color: var(--pd-dark-ink); } +.wy-menu-vertical li.current ul li a:hover { color: var(--pd-dark-ink); background: #17222c; } +.wy-menu-vertical li.toctree-l1 > a:hover, +.wy-menu-vertical li.toctree-l1.current > a:hover { background: #17222c; } +.wy-menu-vertical li.current > ul li.current > a { background: var(--pd-dark-2); } +.wy-menu-vertical li.toctree-l2.current li.toctree-l3 > a { background: var(--pd-dark-2); } +.wy-menu-vertical li.toctree-l3.current li.toctree-l4 > a { background: var(--pd-dark-2); } +.wy-menu-vertical header, .wy-menu-vertical p.caption { color: var(--pd-brand-soft); } +.wy-side-nav-search input[type=text] { border-color: rgb(var(--pd-shade-rgb) / 0.2); } +.wy-side-nav-search > a, .wy-side-nav-search .wy-dropdown > a { color: #fff; } + +.wy-nav-content-wrap, .wy-nav-content { background: var(--pd-bg); } +.wy-nav-content { color: var(--pd-ink); } +.rst-content a, .rst-content a:visited, .wy-nav-content a { color: var(--pd-link); } +.rst-content a:hover { color: var(--pd-link-hover); } +.rst-content code.literal, .rst-content tt.literal { color: var(--pd-ink); background: var(--pd-bg-code); border-color: var(--pd-border); } +.rst-content div[class^="highlight"], .rst-content pre.literal-block { background: var(--pd-bg-code); border-color: var(--pd-border); } +.rst-content .linenodiv pre { border-color: var(--pd-border); } + +/* Signatures from autodoc. */ +.rst-content dl:not(.docutils):not(.field-list) > dt { + background: rgb(var(--pd-brand-rgb) / 0.08); + border-top: 3px solid var(--pd-brand); + color: var(--pd-ink); +} +.rst-content dl:not(.docutils) dl:not(.field-list):not(.docutils) > dt { + background: var(--pd-bg-soft); + border-left: 3px solid var(--pd-brand); + border-top: 0; +} +.rst-content .viewcode-link, .rst-content .viewcode-back { color: var(--pd-link); } + +/* ---- Hero, badges ---------------------------------------------------- */ + +.hero-banner { + background: linear-gradient(135deg, rgb(var(--pd-brand-rgb) / 0.08) 0%, rgb(var(--pd-brand-rgb) / 0.02) 100%); + border: 1px solid rgb(var(--pd-brand-rgb) / 0.25); + border-radius: 12px; + padding: 1.5rem 1.75rem; + margin-bottom: 1.5rem; +} + +.hero-banner p { margin-bottom: 0; } +.badge-row { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.75rem; } +.badge { + display: inline-flex; align-items: center; gap: 0.35rem; + padding: 0.15rem 0.6rem; + font-size: 0.75rem; font-weight: 600; + border-radius: 9999px; + background: var(--pd-bg-soft); color: var(--pd-ink); + border: 1px solid var(--pd-border); +} +.badge-brand { background: rgb(var(--pd-brand-rgb) / 0.12); color: var(--pd-header); border-color: rgb(var(--pd-brand-rgb) / 0.35); } +.badge-green { background: rgb(var(--pd-success-rgb) / 0.12); color: rgb(var(--pd-success-rgb)); border-color: rgb(var(--pd-success-rgb) / 0.3); } + +/* sphinx-design cards, in the brand colour */ +.sd-card { border-color: var(--pd-border); } +.sd-card:hover { border-color: var(--pd-brand); } +.sd-card-title a, .sd-card .sd-stretched-link { color: var(--pd-link); } +.sd-tab-set > label.sd-tab-label { color: var(--pd-ink-soft); } +.sd-tab-set > input:checked + label.sd-tab-label { color: var(--pd-header); border-color: var(--pd-brand); } + +/* ---- Live WebAssembly demos ----------------------------------------- */ + +.pydevices-live-demo { + display: flex; flex-wrap: wrap; gap: 1.25rem; + background: var(--pd-bg-code); + border: 1px solid var(--pd-border); + border-radius: 10px; + padding: 1.25rem; + margin: 1.5rem 0; + box-shadow: 0 2px 8px rgb(var(--pd-shade-rgb) / 0.05); +} +.demo-editor-pane { flex: 1 1 340px; display: flex; flex-direction: column; gap: 0.6rem; min-width: 280px; } +.code-editor { + font-family: SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; + font-size: 0.85rem; line-height: 1.45; + background: var(--pd-bg); color: var(--pd-ink); + border: 1px solid var(--pd-border); border-radius: 6px; + padding: 0.75rem; height: 230px; resize: vertical; tab-size: 4; +} +.code-editor:focus { outline: none; border-color: var(--pd-brand); box-shadow: 0 0 0 2px rgb(var(--pd-brand-rgb) / 0.2); } +.demo-controls { display: flex; align-items: center; gap: 0.6rem; } +.run-btn { + background: var(--pd-header); color: var(--pd-on-primary); + border: none; padding: 0.35rem 0.9rem; font-size: 0.82rem; font-weight: 600; + border-radius: 4px; cursor: pointer; +} +.run-btn:hover:not(:disabled) { background: #08657a; } +.run-btn:disabled { opacity: 0.5; cursor: not-allowed; } +.reset-btn { + background: var(--pd-bg-soft); color: var(--pd-ink); + border: 1px solid var(--pd-border); padding: 0.35rem 0.75rem; font-size: 0.82rem; + border-radius: 4px; cursor: pointer; +} +.reset-btn:hover { background: var(--pd-bg-code); } +.demo-status { font-size: 0.78rem; color: var(--pd-ink-soft); margin-left: auto; font-weight: 500; } +.demo-output { + font-family: SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; + font-size: 0.78rem; color: var(--pd-error); + margin: 0; max-height: 80px; overflow-y: auto; white-space: pre-wrap; + background: transparent; border: 0; padding: 0; +} +.demo-canvas-pane { + display: flex; flex-direction: column; align-items: center; justify-content: center; + background: var(--pd-canvas-bg); border: 1px solid var(--pd-canvas-frame); + border-radius: 8px; padding: 0.75rem; +} +.demo-canvas-pane { max-width: 100%; box-sizing: border-box; } +.demo-canvas-pane canvas { max-width: 100%; height: auto; image-rendering: pixelated; border: 1px solid var(--pd-canvas-border); outline: none; background: var(--pd-canvas-bg); } +.demo-canvas-pane canvas:focus { border-color: var(--pd-brand); } diff --git a/docs-config/src/pydevices_docs/theme/theme.conf b/docs-config/src/pydevices_docs/theme/theme.conf new file mode 100644 index 0000000..57e0c2b --- /dev/null +++ b/docs-config/src/pydevices_docs/theme/theme.conf @@ -0,0 +1,2 @@ +[theme] +inherit = sphinx_rtd_theme diff --git a/docs-site/.gitignore b/docs-site/.gitignore new file mode 100644 index 0000000..69fa449 --- /dev/null +++ b/docs-site/.gitignore @@ -0,0 +1 @@ +_build/ diff --git a/docs-site/conf.py b/docs-site/conf.py new file mode 100644 index 0000000..04e56df --- /dev/null +++ b/docs-site/conf.py @@ -0,0 +1,10 @@ +"""The PyDevices main documentation project: the organization overview.""" + +from pydevices_docs import configure + +globals().update(configure( + "PyDevices", __file__, + repo="PyDevices/.github", edit_path="docs-site", + version_file=None, main=True, +)) +exclude_patterns = ["_build"] diff --git a/docs-site/index.md b/docs-site/index.md new file mode 100644 index 0000000..303defe --- /dev/null +++ b/docs-site/index.md @@ -0,0 +1,70 @@ +# PyDevices + +```{raw} html +
+

One Python for screens, sound and USB that runs the same on a microcontroller, on your desktop and in a browser tab.

+
+ MicroPython + CircuitPython + CPython + WebAssembly + Alpha +
+
+``` + +PyDevices is a set of libraries and firmware for people who build things with +displays, touch, sound and USB in Python. You write your drawing, widgets and +audio code once and run it on a microcontroller board, as a desktop program, +inside a web page, or on Android. + +This site is the front door to the documentation of every PyDevices project. +Each project has its own manual, and the bar at the top of every page takes +you to any of them. If you are new, start with the +[newcomer's guide](newcomers.md); if you want something on your machine, +[install](install.md) it. + +## The projects + +::::{grid} 1 1 2 3 +:gutter: 3 + +:::{grid-item-card} palettes +:link: https://pydevices.readthedocs.io/projects/palettes/en/latest/ +:link-type: url +Color palettes: an HSV wheel, RGB cubes, Material Design swatches and RGB565 +helpers. +::: + +:::{grid-item-card} pygraphics +:link: https://pydevices.readthedocs.io/projects/pygraphics/en/latest/ +:link-type: url +2D drawing, bitmap fonts and blitting, in C and in Python. +::: + +:::{grid-item-card} pdwidgets +:link: https://pydevices.readthedocs.io/projects/pdwidgets/en/latest/ +:link-type: url +Buttons, sliders, labels and screens for touch displays, in pure Python. +::: +:::: + +Every project, including the ones whose manual is still a README on GitHub, is +listed in [Projects](projects/index.md). + +## Try it before you install it + +The [gallery](https://pydevices.github.io/pydevices-examples/gallery/) runs +the same example programs that run on boards, in your browser, and +[Workbench](https://pydevices.github.io/workbench/) is an editor with a +simulated board built in. The main page of each project here also carries a +live demo you can edit and run. + +```{toctree} +:hidden: +:maxdepth: 2 + +newcomers +install +projects/index +``` diff --git a/docs-site/install.md b/docs-site/install.md new file mode 100644 index 0000000..aeab4e7 --- /dev/null +++ b/docs-site/install.md @@ -0,0 +1,61 @@ +# Install + +How you install PyDevices depends on where your code will run. The exact +commands and the system packages each channel needs are in the +[install workflows](https://github.com/PyDevices/pydevices/blob/main/docs/install-workflows.md) +of the `pydevices` repository; this page is the short version. + +::::{tab-set} + +:::{tab-item} MicroPython board +Install a package with `mip`, pointing it at the PyDevices index: + +```python +import mip +mip.install("palettes", index="https://PyDevices.github.io/mip") +``` + +A board's `board_config` installs the same way and wires up the display and +touch for that board. The board's page in the +[device matrix](https://github.com/PyDevices/pydevices/blob/main/docs/device-matrix.md) +names the package to ask for. +::: + +:::{tab-item} CircuitPython board +Copy the library's folder from its repository into `CIRCUITPY/lib/`. Libraries +that have a C part (pygraphics) are built into a CircuitPython firmware +instead; each project's page says which. +::: + +:::{tab-item} Desktop Python +One command installs the whole desktop stack: + +```bash +pip install -i https://test.pypi.org/simple/ \ + --extra-index-url https://pypi.org/simple/ pydevices-desktop +``` + +Current releases are on TestPyPI, which is why the command names it first. +::: + +:::{tab-item} In the browser +Nothing to install: the [gallery](https://pydevices.github.io/pydevices-examples/gallery/) +and [Workbench](https://pydevices.github.io/workbench/) run MicroPython as +WebAssembly. To use a library from your own page, load it with micropip in +Pyodide or PyScript; each project's page shows the line. +::: + +:::: + +## Checking that it works + +With `pydevices-desktop` installed, this draws a red square in a window: + +```bash +python -c "from board_config import display_drv; display_drv.fill_rect(50, 50, 100, 100, 0xF800); display_drv.show()" +``` + +If a window opens with a red square in it, the stack is working. If it does +not, the +[troubleshooting page](https://github.com/PyDevices/pydevices/blob/main/docs/troubleshooting.md) +is the next stop. diff --git a/docs-site/newcomers.md b/docs-site/newcomers.md new file mode 100644 index 0000000..017f255 --- /dev/null +++ b/docs-site/newcomers.md @@ -0,0 +1,41 @@ +# New here? + +PyDevices has a lot of parts, and you only need a few of them. This is the +order most people meet them in. + +## 1. Watch it run + +Open the [gallery](https://pydevices.github.io/pydevices-examples/gallery/). +Every program there is a real PyDevices example running as MicroPython in your +browser, with the same code a board would run. Pick one that draws, one that +reacts to the mouse and one that uses widgets, and read the code beside it. + +## 2. Run it on your desktop + +[Install](install.md) `pydevices-desktop` and run an example from +[pydevices-examples](https://github.com/PyDevices/pydevices-examples). A window +opens and your program draws into it, which is the quickest way to try an idea. + +## 3. Learn the three layers + +Most programs use three libraries stacked on the display: + +- [pygraphics](https://pydevices.readthedocs.io/projects/pygraphics/en/latest/) draws: lines, shapes, text and images. +- [palettes](https://pydevices.readthedocs.io/projects/palettes/en/latest/) supplies colors in the format the display wants. +- [pdwidgets](https://pydevices.readthedocs.io/projects/pdwidgets/en/latest/) builds a user interface out of buttons, sliders and labels, and routes touch and keys to them. + +Each project's manual has its own newcomer's guide. + +## 4. Move it to a board + +The board you own has a `board_config` that sets up its display and touch, so +the program you ran on the desktop starts the same way on the board. The +[device matrix](https://github.com/PyDevices/pydevices/blob/main/docs/device-matrix.md) +lists the boards, and [mpftp](https://github.com/PyDevices/mpftp) copies files +to them and gives you a REPL from your editor. + +## When something goes wrong + +Open an issue in the project that misbehaves; the links are on the +[projects page](projects/index.md). Say what you ran and what you saw, and +which board or interpreter it was. diff --git a/docs-site/projects/audiocomponents.md b/docs-site/projects/audiocomponents.md new file mode 100644 index 0000000..20153e5 --- /dev/null +++ b/docs-site/projects/audiocomponents.md @@ -0,0 +1,5 @@ +# audiocomponents + +The instrument and effect libraries built on audiodsp: `audioinstruments` (55 modelled classics, from the TR-808 to the DX7) and `audioeffects` (45 effect classes and racks). Pure Python on MicroPython and CPython, each a standalone package. + +[audiocomponents on GitHub](https://github.com/PyDevices/audiocomponents). diff --git a/docs-site/projects/audiodsp.md b/docs-site/projects/audiodsp.md new file mode 100644 index 0000000..bc6e198 --- /dev/null +++ b/docs-site/projects/audiodsp.md @@ -0,0 +1,5 @@ +# audiodsp + +CircuitPython's audio system (`audiocore`, `synthio`, `audiomixer`, effects, `audiomp3`) ported to MicroPython and CPython, and extended with dynamics, routing, convolution and offline rendering. It is on PyPI as `pydevices-audiodsp`. + +[audiodsp on GitHub](https://github.com/PyDevices/audiodsp). diff --git a/docs-site/projects/index.md b/docs-site/projects/index.md new file mode 100644 index 0000000..63d773d --- /dev/null +++ b/docs-site/projects/index.md @@ -0,0 +1,31 @@ +# Projects + +Each project has its own manual. A project listed with its manual here has it on this site; the others are documented in the Markdown of their GitHub repository for now, and move onto this site over time. + +- [pydevices](pydevices.md) +- [pygraphics](pygraphics.md) +- [palettes](palettes.md) +- [pdwidgets](pdwidgets.md) +- [audiodsp](audiodsp.md) +- [audiocomponents](audiocomponents.md) +- [mpftp](mpftp.md) +- [LVGL bindings and ports](lvgl.md) +- [micropython-pydevices and the device modules](micropython-pydevices.md) +- [workbench](workbench.md) +- [pydevices-examples](pydevices-examples.md) + +```{toctree} +:hidden: + +pydevices +pygraphics +palettes +pdwidgets +audiodsp +audiocomponents +mpftp +lvgl +micropython-pydevices +workbench +pydevices-examples +``` diff --git a/docs-site/projects/lvgl.md b/docs-site/projects/lvgl.md new file mode 100644 index 0000000..572d5b0 --- /dev/null +++ b/docs-site/projects/lvgl.md @@ -0,0 +1,7 @@ +# LVGL bindings and ports + +LVGL v9 for Python. `lvgl-bindings` generates the bindings from the LVGL headers; `lvgl-micropython`, `lvgl-circuitpython` and `lvgl-python` use them to bring LVGL to MicroPython boards, CircuitPython boards, and CPython (desktop, Android and the browser). + +[LVGL bindings and ports on GitHub](https://github.com/PyDevices/lvgl-bindings). + +The generator lives in [lvgl-bindings](https://github.com/PyDevices/lvgl-bindings); the ports are [lvgl-micropython](https://github.com/PyDevices/lvgl-micropython), [lvgl-circuitpython](https://github.com/PyDevices/lvgl-circuitpython) and [lvgl-python](https://github.com/PyDevices/lvgl-python). diff --git a/docs-site/projects/micropython-pydevices.md b/docs-site/projects/micropython-pydevices.md new file mode 100644 index 0000000..6fbabba --- /dev/null +++ b/docs-site/projects/micropython-pydevices.md @@ -0,0 +1,5 @@ +# micropython-pydevices and the device modules + +The PyDevices patch queue for MicroPython: an ordered series of patches over a pinned upstream release, plus presets, boards and variants that build the PyDevices C modules into firmware with upstream's own `make`. The device modules `displayif`, `audioif`, `cameraif` and `usbhost` are built here, in its `modules/` folder, and reach you in the firmware. + +[micropython-pydevices and the device modules on GitHub](https://github.com/PyDevices/micropython-pydevices). diff --git a/docs-site/projects/mpftp.md b/docs-site/projects/mpftp.md new file mode 100644 index 0000000..7a72487 --- /dev/null +++ b/docs-site/projects/mpftp.md @@ -0,0 +1,5 @@ +# mpftp + +Connect a MicroPython or CircuitPython board from VS Code, Cursor or the command line, over USB or Wi-Fi: two-pane file transfer, a REPL inside the editor, firmware builds and flashing, and `mip` and `circup` installs. + +[mpftp on GitHub](https://github.com/PyDevices/mpftp). diff --git a/docs-site/projects/palettes.md b/docs-site/projects/palettes.md new file mode 100644 index 0000000..0ec38ec --- /dev/null +++ b/docs-site/projects/palettes.md @@ -0,0 +1,5 @@ +# palettes + +Color for displays: RGB565 and RGB888 output, an HSV color wheel, RGB color cubes, Material Design swatches and Win16 color names, with byte swapping for panels that want it. Pure Python with no C part. + +[Read the palettes manual](https://pydevices.readthedocs.io/projects/palettes/en/latest/). diff --git a/docs-site/projects/pdwidgets.md b/docs-site/projects/pdwidgets.md new file mode 100644 index 0000000..9a88972 --- /dev/null +++ b/docs-site/projects/pdwidgets.md @@ -0,0 +1,5 @@ +# pdwidgets + +A lightweight widget toolkit in pure Python: buttons, sliders, progress bars, labels, screens, themes and event routing for touch, mouse, keys and encoders. It redraws only what changed. + +[Read the pdwidgets manual](https://pydevices.readthedocs.io/projects/pdwidgets/en/latest/). diff --git a/docs-site/projects/pydevices-examples.md b/docs-site/projects/pydevices-examples.md new file mode 100644 index 0000000..407214c --- /dev/null +++ b/docs-site/projects/pydevices-examples.md @@ -0,0 +1,7 @@ +# pydevices-examples + +Example programs for every part of PyDevices, the board configurations they run on, and the browser galleries that run the same examples as MicroPython and as Pyodide. + +[pydevices-examples on GitHub](https://github.com/PyDevices/pydevices-examples). + +Try them in the [gallery](https://pydevices.github.io/pydevices-examples/gallery/). diff --git a/docs-site/projects/pydevices.md b/docs-site/projects/pydevices.md new file mode 100644 index 0000000..9e5d8e9 --- /dev/null +++ b/docs-site/projects/pydevices.md @@ -0,0 +1,7 @@ +# pydevices + +The core of PyDevices: the display engine, the hardware drivers and the board configurations that let one program run on many boards. It holds `displaydev`, `audiodev`, `appdev`, `multimer`, `events` and `keys`, the USB packages, and the sources of palettes and pdwidgets. Start here when you want to put something on a screen. + +[pydevices on GitHub](https://github.com/PyDevices/pydevices). + +Its manual is the set of Markdown pages in its `docs/` folder on GitHub. diff --git a/docs-site/projects/pygraphics.md b/docs-site/projects/pygraphics.md new file mode 100644 index 0000000..48e3eff --- /dev/null +++ b/docs-site/projects/pygraphics.md @@ -0,0 +1,5 @@ +# pygraphics + +Drawing primitives, bitmap font rendering, paletted surface blitting and dirty-area tracking. The core is C for speed on a microcontroller, with a pure-Python version that behaves the same, so a program draws the same on a board, a desktop and a browser tab. + +[Read the pygraphics manual](https://pydevices.readthedocs.io/projects/pygraphics/en/latest/). diff --git a/docs-site/projects/workbench.md b/docs-site/projects/workbench.md new file mode 100644 index 0000000..bcca479 --- /dev/null +++ b/docs-site/projects/workbench.md @@ -0,0 +1,7 @@ +# workbench + +A ViperIDE fork with a built-in WebAssembly MicroPython simulator. Edit, run and debug PyDevices code against a simulated board in your browser, with no hardware. + +[Open workbench](https://pydevices.github.io/workbench/). + +The source is on [GitHub](https://github.com/PyDevices/workbench). diff --git a/docs-site/requirements.txt b/docs-site/requirements.txt new file mode 100644 index 0000000..db0ead4 --- /dev/null +++ b/docs-site/requirements.txt @@ -0,0 +1,3 @@ +# Read the Docs installs this project's shared configuration from the checkout. +# Paths in a requirements file are relative to where pip runs: the repository root. +./docs-config diff --git a/docs/building-docs.md b/docs/building-docs.md index 6f700f4..f76b1c8 100644 --- a/docs/building-docs.md +++ b/docs/building-docs.md @@ -1,182 +1,110 @@ # Building and publishing documentation -How to preview a documentation site on your machine and publish it to -ReadTheDocs. - -**This applies to the three library sites only** — `palettes`, `pdwidgets`, and -`pygraphics`. Every other PyDevices repository documents itself as plain -markdown browsed on github.com; do not add MkDocs to one. - -| Repository | Site | -|---|---| -| [pygraphics](https://github.com/PyDevices/pygraphics) | [pygraphics.readthedocs.io](https://pygraphics.readthedocs.io) | -| [pdwidgets](https://github.com/PyDevices/pdwidgets) | [pdwidgets.readthedocs.io](https://pdwidgets.readthedocs.io) | -| [palettes](https://github.com/PyDevices/palettes) | [palettes.readthedocs.io](https://palettes.readthedocs.io) | +How to preview a PyDevices documentation project on your machine and how it +reaches Read the Docs. + +One Sphinx configuration, in the `pydevices-docs` package +([docs-config/](../docs-config/)), gives every project the same theme, the +same top bar linking every project to every other, and the same Markdown and +docstring setup. The organization overview is the main project, built from +[docs-site/](../docs-site/); each library's manual is a subproject. + +| Project | Source | Site | +|---|---|---| +| PyDevices (the main project) | [docs-site/](../docs-site/) here | `pydevices.readthedocs.io` | +| palettes | [palettes/docs](https://github.com/PyDevices/pydevices/tree/main/palettes/docs) | `/projects/palettes/` under the main site | +| pdwidgets | [pdwidgets/docs](https://github.com/PyDevices/pydevices/tree/main/pdwidgets/docs) | `/projects/pdwidgets/` | +| pygraphics | [docs/](https://github.com/PyDevices/pygraphics/tree/main/docs) in its repo | `/projects/pygraphics/` | Docstring conventions for the generated API pages: [docstrings.md](docstrings.md). ## Preview locally -From the repository root: +From the project's repository root (for palettes and pdwidgets, from the +`pydevices` checkout): ```bash python3 -m venv .venv-docs -.venv-docs/bin/pip install -r docs/requirements.txt -.venv-docs/bin/mkdocs serve +.venv-docs/bin/pip install -r palettes/docs/requirements.txt # or pdwidgets/, or docs/ in pygraphics +.venv-docs/bin/sphinx-build -W --keep-going -b html palettes/docs palettes/docs/_build/html ``` -Open **http://127.0.0.1:8000** in your browser. MkDocs reloads when you edit files under `docs/`. - -One-shot production build (output in `site/`): +`requirements.txt` installs the tagged `pydevices-docs`. To work on the shared +package itself, install your checkout instead: +`.venv-docs/bin/pip install -e ../.github/docs-config`. -```bash -.venv-docs/bin/mkdocs build -``` +Open `_build/html/index.html`, or serve the folder +(`python3 -m http.server -d palettes/docs/_build/html`). Live demos need http, +not `file://`. `sphinx-autobuild palettes/docs palettes/docs/_build/html` +rebuilds as you edit. -> **Already have the venv?** -> -> If `.venv-docs/` exists from a previous session, skip the `venv` and `pip install` lines and run `.venv-docs/bin/mkdocs serve` directly. +`-W` turns every warning into a failure. That is how CI and Read the Docs +(`fail_on_warning: true` in `.readthedocs.yaml`) build, so a build that passes +here passes there. No network? Set `PYDEVICES_DOCS_OFFLINE=1` to skip the +reference map to the Python and MicroPython manuals. ### What runs during a build | File | Role | |------|------| -| `mkdocs.yml` | Site config, theme, navigation | -| `docs/requirements.txt` | Python packages for MkDocs and plugins | -| `.readthedocs.yaml` | ReadTheDocs build settings (same deps) | -| `scripts/mkdocs_gen_ref_pages.py` | Auto-generates API reference stubs from source docstrings | - -Hand-authored pages live under `docs/` and follow a **Try → Quick start → Install → Learn → Reference** structure (see `mkdocs.yml` nav). +| `docs/conf.py` | Five lines: the project's name and where its code is, passed to `pydevices_docs.configure()` | +| `docs/requirements.txt` | The pinned `pydevices-docs` release | +| `.readthedocs.yaml` | Read the Docs build settings | +| `docs/reference/` | API pages, written at build time from the package's modules (git-ignored) | -API reference pages under `reference/` and `reference/utils/` are generated at build time — do not hand-edit them. - -Shared copy-paste blocks: `docs/_snippets/` (included via pymdownx Snippets). - -### Jupyter notebooks - -Interactive Jupyter notebooks are generated on demand from example scripts using `jupyter.py`. See [pydevices/docs/jupyter.md](https://github.com/PyDevices/pydevices/blob/main/docs/jupyter.md). +Hand-authored pages are Markdown (MyST) in `docs/`. Cards and tabs come from +`sphinx-design`, diagrams from `mermaid`, and the live browser demos are raw +HTML (see the [pydevices-docs README](../docs-config/README.md)). ### Troubleshooting -**`ModuleNotFoundError` during build** — use a venv as shown above; do not `pip install` into the system Python on Debian/Ubuntu (externally-managed-environment error). - -**Griffe warnings** — docstring parameter mismatches in source. All three sites -now build under `strict: true`, so these fail the build rather than scrolling -past. Fix them in the **docstring**, not the signature: these packages run on -MicroPython and CircuitPython, where annotations cost bytecode and RAM. See -[docstrings.md](docstrings.md). - -**`Returns` type reported as missing when it is right there** — griffe's default -`returns_named_value: true` expects the type in parentheses, `(Area): text`, -while our convention writes a bare `Area: text` and griffe then reads `Area` as -the return's *name*. All three sites therefore set: - -```yaml -plugins: - - mkdocstrings: - handlers: - python: - options: - docstring_options: - returns_named_value: false -``` - -A new library site must set it too, or it will fail `strict` on warnings that -are not real. If shared mkdocs config is ever centralized in this repo, this -option belongs in it. - -**MkDocs 2.0 warning banner** — harmless; set `DISABLE_MKDOCS_2_WARNING=true` to hide it. +**`ModuleNotFoundError` while importing a module for the API pages** -- autodoc +imports the package. Name modules a desktop build lacks (hardware, interpreter +builtins) in `mock_imports` in `conf.py`. -#### ReadTheDocs: "Builds disabled due to consecutive failures" +**A docstring warning fails the build** -- fix it in the **docstring**, not the +signature: these packages run on MicroPython and CircuitPython, where +annotations cost bytecode and RAM. See [docstrings.md](docstrings.md). -This project was registered on ReadTheDocs before the docs revamp. RTD kept building **`main`**, which had broken MkDocs config (missing nav pages, no `docs/requirements.txt`, wrong mkdocstrings paths). After 25 failures, RTD auto-disabled builds. +**An API page is missing a name** -- the page documents what the module +defines. For a package whose public names live in private modules (pygraphics), +set `use_module_all=True` so the page follows `__all__`. -**Fix:** +## Publish to Read the Docs -1. **Admin** → **Settings** → Advanced → uncheck **Disable builds for this project** → Save. -2. **Push fixes to `main`** — RTD builds from the default branch; it cannot build changes that exist only locally. -3. **Admin** → **Versions** → ensure **`latest`** is active → click **Build version**. -4. Confirm the build log shows MkDocs Material and `docs/requirements.txt` installing — not the old readthedocs theme with missing `test2.md`. +Read the Docs reads `.readthedocs.yaml` from the repository. The main project +and each subproject are separate Read the Docs projects; the main project lists +the others under **Subprojects**, which is what puts them at +`/projects//` and gives every page the same address scheme. -#### ReadTheDocs: "Search indexing has been disabled" - -Harmless for now — RTD pauses search indexing on inactive projects. After docs are live and receiving traffic: - -**Admin** → **Settings** → **Enable search indexing** → Save. - ---- - -## Publish to ReadTheDocs - -ReadTheDocs reads `.readthedocs.yaml` from the repository and runs the same MkDocs build as locally. - -### Org GitHub App (required) +### Org GitHub App PyDevices uses the [Read the Docs Community GitHub App](https://github.com/apps/read-the-docs-community) -installed on the org with access to **all repositories** (see +installed on the org with access to all repositories (see the [org installation](https://github.com/organizations/PyDevices/settings/installations)). -That app delivers push/PR events to RTD — do **not** add a manual -`readthedocs.org/api/v2/webhook/...` hook on the repo. - -Docs projects on the same app: **pygraphics**, -**palettes**, **pdwidgets**. - -### First-time setup (new project) - -1. Go to [readthedocs.org](https://readthedocs.org) and sign in with **GitHub** - (an account that can see the repository). -2. Open the [Read the Docs dashboard](https://app.readthedocs.org/dashboard/) - and click **Add project**. -3. Search for the repository under **`PyDevices/`** and import it. - - If the repo does not appear, confirm the - [GitHub App installation](https://github.com/organizations/PyDevices/settings/installations) - includes this repository, then use **Refresh your repositories** on RTD. -4. On the setup form, confirm: - - **Documentation type:** MkDocs (auto-detected from `.readthedocs.yaml`) - - **Configuration file:** `.readthedocs.yaml` - - Click **Next**, then **This file exists** (the config is already in the repo). -5. **Build `latest` (tracks `main`):** - - Go to **Admin** → **Versions**. - - Ensure **`latest`** is **Active** and set as the **default version**. - - Click **Build** on `latest` (or wait for the next push to `main`). -6. Check the **Builds** tab. A successful build ends with - `Documentation built successfully`. The site appears at: - - `https://.readthedocs.io/en/latest/` - - `https://.readthedocs.io/` when `latest` is the default - -### Migrating a legacy (webhook) project to the GitHub App - -Older imports used a per-repo webhook under GitHub **Settings → Webhooks**. -Those PyDevices docs projects (**pygraphics**, **palettes**, **pdwidgets**) have -been migrated to the org GitHub App via -[Migrate to GitHub App](https://app.readthedocs.org/accounts/migrate-to-github-app/); -legacy webhooks are removed. New projects should use the App from the start -(no manual webhook). - -### Ongoing - -1. RTD rebuilds automatically when you push to `main` (via the GitHub App). -2. Optionally disable obsolete version slugs under **Admin** → **Versions** if any remain from earlier experiments. -3. Enable **search indexing** under **Settings** once the site is live. - -### Optional: pull request previews - -In RTD project **Admin** → **Preview documentation from pull requests**, enable PR builds so each PR gets a preview URL before merge. - -## Check GitHub Actions from the CLI - -Authenticate once (stores credentials for future sessions): +It delivers push and pull-request events; do not add a manual +`readthedocs.org/api/v2/webhook/...` hook. -```bash -gh auth login -``` +### A new project -Then from the repo root: +1. Open the [Read the Docs dashboard](https://app.readthedocs.org/dashboard/) + and **Add project**; pick the repository under `PyDevices/`. +2. If the project's `.readthedocs.yaml` is not at the repository root, set + **Settings > Advanced > Path for .readthedocs.yaml** (palettes and pdwidgets + use `palettes/.readthedocs.yaml` and `pdwidgets/.readthedocs.yaml`). +3. Make sure `latest` (tracking `main`) is active and the default version. +4. On the main project, **Subprojects > Add subproject**, with the project's + name as the alias. +5. Add the project to `projects.py` in `docs-config/` with `live = True` and + release a new `pydevices-docs`, so it appears in every project's top bar. -```bash -gh run list --limit 5 # recent workflow runs -gh run watch # follow the latest run -``` +### Versions + +Readers land on **latest**, built from `main`. Keep only the most recent +release version active beside it (**Admin > Versions**); hide the rest. + +### Pull request previews -Useful after pushing doc changes to confirm the RTD build succeeded. +In the project's **Admin > Preview documentation from pull requests**, enable +PR builds so each PR gets a preview before merge. diff --git a/docs/doc-style.md b/docs/doc-style.md index 8ffe255..7b969b5 100644 --- a/docs/doc-style.md +++ b/docs/doc-style.md @@ -39,7 +39,7 @@ rather than what was hoped: restraint reads better. This is the prescribed convention (a handful of READMEs still carry stray emoji; Phase 4 removes them as each repo gets its pass). -- **Docs-site pages** (mkdocs/portal pages) may use the landmark set +- **Docs-site pages** (Read the Docs and portal pages) may use the landmark set above, sparingly, on headings only — the original rules (one per heading, only the fixed set, never in body text or code) apply there. - A repo using none at all is compliant. The failure mode this document diff --git a/docs/docstrings.md b/docs/docstrings.md index 25082e6..9411ae8 100644 --- a/docs/docstrings.md +++ b/docs/docstrings.md @@ -1,14 +1,20 @@ # Docstring conventions -Public docstrings in the three library repositories — `palettes`, `pdwidgets`, -and `pygraphics` — are rendered on ReadTheDocs via mkdocstrings. Follow these -rules so the generated reference pages stay accurate. See -[building-docs.md](building-docs.md) for the build itself. +Public docstrings in the three library projects — `palettes`, `pdwidgets`, +and `pygraphics` — are rendered on Read the Docs by Sphinx's `autodoc` with the +`napoleon` extension. Follow these rules so the generated reference pages stay +accurate. See [building-docs.md](building-docs.md) for the build itself. ## Style - **Google style** only (`Args`, `Returns`, `Raises`, `Example`). -- Do not mix Sphinx directives (`:return:`, `:param:`). +- Do not mix Sphinx fields (`:return:`, `:param:`) or roles (`:meth:`, `:class:`). +- Cross-reference with single backticks: `` `Palette` `` or + `` `text ` ``. A single backtick in a docstring is a + Python cross-reference. Use double backticks for code that is not a name. +- A blank line before every list, and a raw string or a doubled backslash for + `\n` in prose: a bare `\n` in a normal docstring is a real newline and + breaks the paragraph. - Match **real function signatures** — do not document named parameters on `*args` / `**kwargs` wrappers. ## Module docstrings @@ -43,9 +49,11 @@ See also: https://github.com/PyDevices/pydevices/blob/main/docs/displaydev.md - **`Area` returns:** Drawing helpers that return dirty regions use `pygraphics.Area` (`x`, `y`, `w`, `h`). - **App:** Document poll/subscribe patterns; link to [Events concept](https://github.com/PyDevices/pydevices/blob/main/docs/appdev.md). -- **Private API:** Names starting with `_` are excluded from mkdocstrings output; minimal or no docstrings are fine. +- **Private API:** Names starting with `_` are excluded from the API pages; minimal or no docstrings are fine. Document public methods that live on private implementation bases when they surface via inheritance - (e.g. provider `Timer` classes ← `_TimerCore.init` / `deinit` with `inherited_members: true`). + (set `:inherited-members:` on the page for that class). +- **Properties:** write a sentence. Napoleon does not read `Name: text` on a + property as a type, so the sentence stays a sentence. ## Types @@ -59,13 +67,13 @@ annotations that deliberately are not there. ## Verification -From the repo root: +From the repo root, build the way CI does: ```bash -.venv-docs/bin/mkdocs build 2>&1 | grep -i griffe +.venv-docs/bin/sphinx-build -W --keep-going -b html palettes/docs palettes/docs/_build/html ``` -Griffe warnings mean a docstring parameter does not appear in the signature — fix before merging P0 module changes. +A docstring problem is a warning, and `-W` makes it a failure. Fix it before merging. ## Priority tiers diff --git a/docs/github-presence.md b/docs/github-presence.md index e76e1f2..304769a 100644 --- a/docs/github-presence.md +++ b/docs/github-presence.md @@ -62,20 +62,19 @@ the answer from scratch. ## Read the Docs -MkDocs sites for **pydevices-examples**, **pygraphics**, **palettes**, and **pdwidgets** -publish to `*.readthedocs.io`. GitHub integration uses the org-installed +Sphinx documentation for the organization, **pygraphics**, **palettes**, and **pdwidgets** +publishes to Read the Docs as one site (see [building-docs.md](building-docs.md)). GitHub integration uses the org-installed [Read the Docs Community](https://github.com/apps/read-the-docs-community) app (**all repositories** — see the org's [installed GitHub Apps](https://github.com/organizations/PyDevices/settings/installations)). With the GitHub App, RTD receives push/PR events directly — no per-repo webhook -is required. The docs repos (**pydevices-examples**, **pygraphics**, **palettes**, -**pdwidgets**) use this app; legacy `api/v2/webhook/...` hooks are gone. New +is required. The docs repos (**pygraphics**, **palettes**, **pdwidgets**) use this app; legacy `api/v2/webhook/...` hooks are gone. New projects: import from the RTD dashboard (repo list comes from the app). If an older project still needs migrating: [Migrate to GitHub App](https://app.readthedocs.org/accounts/migrate-to-github-app/). -Build config in each docs repo is `.readthedocs.yaml` + `mkdocs.yml` (no secrets). +Build config in each docs repo is `.readthedocs.yaml` + `docs/conf.py` (no secrets). ## Repos & Pages sites diff --git a/docs/repo-layout.md b/docs/repo-layout.md index b82fbfc..9c91a71 100644 --- a/docs/repo-layout.md +++ b/docs/repo-layout.md @@ -28,7 +28,7 @@ Long-form notes and guides belong under `docs/`. | Kind | Examples | Stay at root? | |------|----------|---------------| | Packaging / build metadata | `pyproject.toml`, `setup.py`, `setup.cfg`, `MANIFEST.in`, `micropython.mk`, `circuitpython.mk` | Yes — tooling discovers these at root | -| Docs site config | `mkdocs.yml` | Yes | +| Docs site config | `.readthedocs.yaml` (`conf.py` lives in `docs/`) | Yes | | Short entry scripts | `apply_*.sh`, `build_*.sh`, `regenerate_*.sh` | Yes when they are the public entry point | | License / community | `LICENSE`, `README.md`, `ROADMAP.md`, `AGENTS.md`, `CONTRIBUTING.md` | Yes | | Pip requirements | `requirements.txt`, `requirements-dev.txt` | Yes (see below) | @@ -74,7 +74,7 @@ Do **not** put a root `requirements.txt` that only lists tooling — that reads “install these to use the app,” which is misleading when the product ships via MIP / TestPyPI / wheels / source trees instead. -MkDocs (and similar) stay under **`docs/requirements.txt`**, not at the repo +Documentation tooling (Sphinx and its extensions) stays under **`docs/requirements.txt`**, not at the repo root. Examples: diff --git a/docs/workflows.md b/docs/workflows.md index 6eda7de..6948dc4 100644 --- a/docs/workflows.md +++ b/docs/workflows.md @@ -131,11 +131,11 @@ those paths. ## Documentation -**No repository builds documentation in Actions.** ReadTheDocs builds -`palettes`, `pdwidgets`, and `pygraphics` on push via the org GitHub App. A -duplicate `mkdocs build` used to run here too and was deleted: it passed while -two of the three sites were failing to publish, because `mkdocs` exits 0 and RTD -then fails the build on output checks `mkdocs` cannot see. +ReadTheDocs builds the main project, `palettes`, `pdwidgets`, and `pygraphics` +on push via the org GitHub App, with `fail_on_warning: true`. Each repo's CI +builds its docs the same way (`sphinx-build -W`, from the repository root with +the paths in `.readthedocs.yaml`), so a warning fails the pull request before it +can fail the published build. ## Maintenance diff --git a/scripts/generate_sites.py b/scripts/generate_sites.py index f6b9aca..1718249 100755 --- a/scripts/generate_sites.py +++ b/scripts/generate_sites.py @@ -569,31 +569,6 @@ def _copy_chrome_into(site_root): shutil.copy2(src_img, os.path.join(img_dir, img_name)) -DOCS_THEME_REPOS = ('pygraphics', 'pdwidgets', 'palettes') - - -def sync_docs_theme(): - """Copy the shared Material for MkDocs stylesheet into the repos that publish docs sites. - - assets/css/docs-extra.css is the master; each repo's - docs/stylesheets/extra.css is a pure sync target, exactly like the - chrome under PyDevices.github.io/assets/. Read the Docs builds those - repos independently, so the synced copies must be committed in their - own repositories for a palette change to reach the published docs. - """ - src = os.path.join(ASSETS_DIR, 'css', 'docs-extra.css') - if not os.path.exists(src): - print("[SKIP] docs theme: assets/css/docs-extra.css is missing") - return - for repo in DOCS_THEME_REPOS: - dest_dir = os.path.join(BASE_DIR, repo, 'docs', 'stylesheets') - if not os.path.isdir(dest_dir): - print(f"[SKIP] {repo}: no docs/stylesheets (checkout missing)") - continue - shutil.copy2(src, os.path.join(dest_dir, 'extra.css')) - print(f"[SYNC] {repo}/docs/stylesheets/extra.css") - - def sync_assets(db): """Copy the shared chrome and apps to the portal repository root (PyDevices.github.io).""" portal_root = os.path.join(BASE_DIR, PORTAL_REPO) @@ -672,7 +647,6 @@ def main(argv=None): if args.only is None: sync_assets(db) - sync_docs_theme() for repo_name, data in repos(db).items(): if args.only is not None and repo_name != args.only: