diff --git a/.github/workflows/reusable-cookie.yml b/.github/workflows/reusable-cookie.yml index fcd8b8a4..343ed718 100644 --- a/.github/workflows/reusable-cookie.yml +++ b/.github/workflows/reusable-cookie.yml @@ -46,47 +46,49 @@ jobs: run: uv tool install nox - name: Test pybind11 - run: nox -s 'tests(pybind11, novcs)' -s 'tests(pybind11, vcs)' + run: nox -s 'tests(pybind11, novcs)' -s 'tests(pybind11, vcs, sphinx)' - name: Test scikit-build - run: nox -s 'tests(skbuild, novcs)' -s 'tests(skbuild, vcs)' + run: nox -s 'tests(skbuild, novcs)' -s 'tests(skbuild, vcs, sphinx)' - name: Test poetry - run: nox -s 'tests(poetry, novcs)' -s 'tests(poetry, vcs)' + run: nox -s 'tests(poetry, novcs)' -s 'tests(poetry, vcs, sphinx)' - name: Test flit - run: nox -s 'tests(flit, novcs)' -s 'tests(flit, vcs)' + run: nox -s 'tests(flit, novcs)' -s 'tests(flit, vcs, mkdocs)' - name: Test uv - run: nox -s 'tests(uv, novcs)' + run: nox -s 'tests(uv, novcs, sphinx)' - name: Test pdm - run: nox -s 'tests(pdm, novcs)' -s 'tests(pdm, vcs)' + run: nox -s 'tests(pdm, novcs)' -s 'tests(pdm, vcs, sphinx)' - name: Test maturin - run: nox -s 'tests(maturin, novcs)' + run: nox -s 'tests(maturin, novcs, sphinx)' - name: Test hatch - run: nox -s 'tests(hatch, novcs)' -s 'tests(hatch, vcs)' + run: nox -s 'tests(hatch, novcs, sphinx)' -s 'tests(hatch, vcs, sphinx)' - name: Test setuptools PEP 621 - run: nox -s 'tests(setuptools, novcs)' -s 'tests(setuptools, vcs)' + run: + nox -s 'tests(setuptools, novcs, sphinx)' -s 'tests(setuptools, vcs, + sphinx)' - name: Native poetry tooling if: matrix.python-version != 'pypy-3.11' run: | - nox -s 'native(poetry, novcs)' - nox -s 'native(poetry, vcs)' + nox -s 'native(poetry, novcs, sphinx)' + nox -s 'native(poetry, vcs, sphinx)' - name: Native pdm tooling - run: nox -s 'native(pdm, novcs)' -s 'native(pdm, vcs)' + run: nox -s 'native(pdm, novcs, sphinx)' -s 'native(pdm, vcs, sphinx)' - name: Activate MSVC for Meson if: runner.os == 'Windows' uses: ilammy/msvc-dev-cmd@v1 - name: Test meson-python - run: nox -s 'tests(mesonpy, novcs)' + run: nox -s 'tests(mesonpy, novcs, sphinx)' - name: Compare copier template generation run: nox -s compare_copier @@ -114,43 +116,43 @@ jobs: - name: Test pybind11 run: | - nox -s 'nox(pybind11, vcs)' - nox -s 'nox(pybind11, vcs)' -- docs + nox -s 'nox(pybind11, vcs, sphinx)' + nox -s 'nox(pybind11, vcs, sphinx)' -- docs - name: Test scikit-build run: | - nox -s 'nox(skbuild, vcs)' - nox -s 'nox(skbuild, vcs)' -- docs + nox -s 'nox(skbuild, vcs, sphinx)' + nox -s 'nox(skbuild, vcs, mkdocs)' -- docs - name: Test poetry run: | - nox -s 'nox(poetry, novcs)' - nox -s 'nox(poetry, novcs)' -- docs + nox -s 'nox(poetry, novcs, sphinx)' + nox -s 'nox(poetry, novcs, mkdocs)' -- docs - name: Test flit run: | - nox -s 'nox(flit, novcs)' - nox -s 'nox(flit, novcs)' -- docs + nox -s 'nox(flit, novcs, mkdocs)' + nox -s 'nox(flit, novcs, mkdocs)' -- docs - name: Test pdm run: | - nox -s 'nox(pdm, vcs)' - nox -s 'nox(pdm, vcs)' -- docs + nox -s 'nox(pdm, vcs, sphinx)' + nox -s 'nox(pdm, vcs, sphinx)' -- docs - name: Test maturin run: | - nox -s 'nox(maturin, novcs)' - nox -s 'nox(maturin, novcs)' -- docs + nox -s 'nox(maturin, novcs, sphinx)' + nox -s 'nox(maturin, novcs, sphinx)' -- docs - name: Test hatch run: | - nox -s 'nox(hatch, vcs)' - nox -s 'nox(hatch, vcs)' -- docs + nox -s 'nox(hatch, vcs, mkdocs)' + nox -s 'nox(hatch, vcs, mkdocs)' -- docs - name: Test setuptools PEP 621 run: | - nox -s 'nox(setuptools, vcs)' - nox -s 'nox(setuptools, vcs)' -- docs + nox -s 'nox(setuptools, vcs, sphinx)' + nox -s 'nox(setuptools, vcs, sphinx)' -- docs - name: Activate MSVC for Meson if: runner.os == 'Windows' @@ -158,8 +160,8 @@ jobs: - name: Test meson-python run: | - nox -s 'nox(mesonpy, novcs)' - nox -s 'nox(mesonpy, novcs)' -- docs + nox -s 'nox(mesonpy, novcs, sphinx)' + nox -s 'nox(mesonpy, novcs, sphinx)' -- docs dist: name: Distribution build diff --git a/cookiecutter.json b/cookiecutter.json index 558ecc07..7379fec5 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -18,6 +18,7 @@ "mesonpy", "maturin" ], + "docs": ["sphinx", "mkdocs"], "vcs": true, "__year": "{% now 'utc', '%Y' %}", "__project_slug": "{{ cookiecutter.project_name | lower | replace('-', '_') | replace('.', '_') }}", @@ -45,6 +46,11 @@ "mesonpy": "Meson-python - Compiled C++ (also good)", "maturin": "Maturin - Compiled Rust (recommended)" }, + "docs": { + "__prompt__": "Choose your documentation tool", + "sphinx": "Sphinx", + "mkdocs": "MkDocs" + }, "vcs": "Use version control for versioning" } } diff --git a/copier.yml b/copier.yml index 488cf8fa..ae1f36f9 100644 --- a/copier.yml +++ b/copier.yml @@ -82,6 +82,14 @@ backend: "Maturin - Compiled Rust (recommended)": maturin # [[[end]]] +# [[[cog print(cc.docs.yaml()) ]]] +docs: + help: Choose your documentation tool + choices: + "Sphinx": sphinx + "MkDocs": mkdocs + # [[[end]]] + # [[[cog print(cc.vcs.yaml()) ]]] vcs: type: bool diff --git a/docs/README.md b/docs/README.md index 0c23adc0..12be755d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,7 +11,7 @@ scientists and research software engineers. The repository contains: ## Contributing to the documentation To build locally, install rbenv (remember to run `rbenv init` after installing, -and `rbenv install 3.1.2`). Then: +and `rbenv install 3.4.1`). Then: ```bash bundle install diff --git a/docs/pages/guides/docs.md b/docs/pages/guides/docs.md index cd5571e4..c42a9a0b 100644 --- a/docs/pages/guides/docs.md +++ b/docs/pages/guides/docs.md @@ -12,28 +12,36 @@ parent: Topical Guides Documentation used to require learning reStructuredText (sometimes referred to as reST / rST), but today we have great choices for documentation in markdown, -the same format used by GitHub, Wikipedia, and others. This guide covers Sphinx, -and uses the modern MyST plugin to get Markdown support. +the same format used by GitHub, Wikipedia, and others. This guide covers Sphinx +and Mkdocs, and uses the modern MyST plugin to get Markdown support. {: .note-title } -> Other frameworks +> Popular frameworks > > There are other frameworks as well; these often are simpler, but are not as > commonly used, and have somewhat fewer examples and plugins. They are: > -> - [JupyterBook](https://jupyterbook.org): A powerful system for rendering a -> collection of notebooks using Sphinx internally. Can also be used for docs, -> though, see [echopype](https://echopype.readthedocs.io). -> - [MkDocs](https://www.mkdocs.org): a from-scratch new documentation system +> - [Sphinx](https://www.sphinx-doc.org/en/master/): A popular documentation +> framework for scientific libraries with a history of close usage with +> scientific tools like LaTeX. Examples include +> [astropy](https://docs.astropy.org/en/stable/index_user_docs.html) and +> [corner](https://docs.astropy.org/en/stable/index_user_docs.html). +> - [MkDocs](https://www.mkdocs.org): A from-scratch new documentation system > based on markdown and HTML. Less support for man pages & PDFs than Sphinx, > since it doesn't use docutils. Has over > [200 plugins](https://github.com/mkdocs/catalog) - they are much easier to > write than Sphinx. Example sites include [hatch](https://hatch.pypa.io), > [PDM](https://pdm.fming.dev), > [cibuildwheel](https://cibuildwheel.readthedocs.io), -> [Textual](https://textual.textualize.io), and -> [pipx](https://pypa.github.io/pipx/). +> [Textual](https://textual.textualize.io), +> [pipx](https://pypa.github.io/pipx/), +> [Pydantic](https://docs.pydantic.dev/latest/), +> [Polars](https://docs.pola.rs/), and +> [FastAPI](https://fastapi.tiangolo.com/) +> - [JupyterBook](https://jupyterbook.org): A powerful system for rendering a +> collection of notebooks using Sphinx internally. Can also be used for docs, +> though, see [echopype](https://echopype.readthedocs.io). ## What to include @@ -56,20 +64,28 @@ Ideally, software documentation should include: ## Hand-written docs -Create `docs/` directory within your project (i.e. next to `src/`). There is a -sphinx-quickstart tool, but it creates unnecessary files (make/bat, we recommend -a cross-platform noxfile instead), and uses rST instead of Markdown. Instead, -this is our recommended starting point for `conf.py`: +Create `docs/` directory within your project (next to `src/`). From here, Sphinx +and MkDocs diverge. + +{% tabs %}{% tab sphinx Sphinx %} + +There is a sphinx-quickstart tool, but it creates unnecessary files (make/bat, +we recommend a cross-platform noxfile instead), and uses rST instead of +Markdown. Instead, this is our recommended starting point for `conf.py`: ### conf.py @@ -249,12 +265,217 @@ docs = [ You should use `--group=docs` when using uv or pip to install. +{% endtab %} {% tab mkdocs MkDocs %} + +While the cookie cutter creates a basic structure for your MkDocs (a top level +`mkdocs.yml` file and the `docs` directory), you can also follow the official +[Getting started](https://squidfunk.github.io/mkdocs-material/getting-started/) +guide instead. Note, however, instead of the `pip` install, it is better +practice install your documentation dependencies via `pyproject.toml` and then +when you run your `uv sync` to install dependencies, you can explicitly ask for +the `docs` group to be installed via `uv sync --group=docs` or +`uv sync --all-groups`. + +If you selected the `mkdocs` option when using the template cookie-cutter +repository, you will already have this group. Otherwise, add to your +`pyproject.toml`: + +```toml +[dependency-groups] +docs = [ + "markdown>=3.9", + "mdx-include>=1.4.2", + "mkdocs-material>=9.1.19", + "mkdocs>=1.1.2,", + "mkdocstrings-python>=1.18.2", + "pyyaml>=6.0.1", +] +``` + +These dependencies include several common plugins---such as generating reference +API documentation from docstrings---to make life easier. + +Similar to Sphinx, MkDocs puts your written documentation into the `/docs` +directory, but also has a top-level `mkdocs.yml` configuration file. You can see +the +[minimal configuration for the file here](https://squidfunk.github.io/mkdocs-material/creating-your-site/#minimal-configuration), +which is only four lines. However, the `mkdocs.yml` file bundled with the +template repository have many options pre-configured. Let's run through an +example configuration now. + +Here's the whole file for completeness. We'll break it into sections underneath. + + + +```yaml +site_name: package +site_url: https://package.readthedocs.io/ +site_author: "My Name" + +repo_name: "org/package" +repo_url: "https://github.com/org/package" + +theme: + name: material + icon: + repo: fontawesome/brands/github + features: + - search.suggest + - search.highlight + - navigation.expand + - navigation.tracking + - toc.follow + palette: + # See options to customise your color scheme here: + # https://squidfunk.github.io/mkdocs-material/setup/changing-the-colors/ + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: + icon: material/weather-sunny + name: Switch to light mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/weather-night + name: Switch to dark mode + +plugins: + autorefs: {} + mkdocstrings: + handlers: + python: + paths: [.] + inventories: + - https://docs.python.org/3/objects.inv + - https://docs.pydantic.dev/latest/objects.inv + options: + members_order: source + separate_signature: true + filters: ["!^_"] + show_root_heading: true + show_if_no_docstring: true + show_signature_annotations: true + search: {} + +nav: + - Home: index.md + - Python API: api.md +``` + + + +First, the basic site metadata contains authors, repository details, URLs, etc: + +```yaml +site_name: some_project +site_url: https://some_project.readthedocs.io/ +site_author: "Bruce Wayne" +repo_name: "wayne_industries/some_project" +repo_url: "https://github.com/wayne_industries/some_project" +``` + +After that, we can configure the visual theming for the the site. The repo icon +is what appears in the top-right of the site next to the link to your +GitHub/GitLab/etc, and you can peruse +[other FontAwesome icons here](https://fontawesome.com/icons) if the default +GitHub or GitLab brand is unwanted. + +Extra search features are documented +[here](https://squidfunk.github.io/mkdocs-material/setup/setting-up-site-search/), +and the three enabled are autocomplete for search suggestions (`search.suggest`) +and highlighting search terms after a user clicks on a search result +(`search.highlight`). + +For navigation plugins (documented +[here](https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/)), +we request the side navigation to be expanded by default (`navigation.expand`) +and that the URL autoupdate to the latest anchor as a user scrolls through the +page (`navigation.tracking`). Finally, we request that the current user section +is always shown and highlight in the sidebar via `toc.follow`. + +In the palette section (documented +[here](https://squidfunk.github.io/mkdocs-material/setup/changing-the-colors/)) +you can easily modify the scheme, icons, primary colors, and accents for both +light and dark themes. + +```yaml +theme: + name: material + icon: + repo: fontawesome/brands/github + features: + - search.suggest + - search.highlight + - navigation.expand + - navigation.tracking + - toc.follow + palette: + # See options to customise your color scheme here: + # https://squidfunk.github.io/mkdocs-material/setup/changing-the-colors/ + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: + icon: material/weather-sunny + name: Switch to light mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/weather-night + name: Switch to dark mode +``` + +Onto the best part of MkDocs: it's many plugins! + +- `search` enabled search functionality. +- [`autorefs`](https://mkdocstrings.github.io/autorefs/) allows easier linking + across pages and anchors. +- [`mkdocstrings`](https://mkdocstrings.github.io/) lets you generate reference + API documentation from your docstring. + +```yaml +plugins: + autorefs: {} + mkdocstrings: + handlers: + python: + paths: [.] + inventories: + - https://docs.python.org/3/objects.inv + - https://docs.pydantic.dev/latest/objects.inv + options: + members_order: source + separate_signature: true + filters: ["!^_"] + show_root_heading: true + show_if_no_docstring: true + show_signature_annotations: true + search: {} +``` + +Finally, we have to define the actual structure of our site by providing the +primary navigation sidebar layout. Here we have three top-level links, one for +the home page and one where all the generated API documentation from +`mkdocstrings` will live. + +```yaml +nav: + - Home: index.md + - Python API: api.md +``` + +{% endtab %} {% endtabs %} + ### .readthedocs.yaml In order to use to build, host, and preview your documentation, you must have a `.readthedocs.yaml` file {% rr RTD100 %} like this: +{% tabs %} {% tab sphinx Sphinx %} + +{% endtab %} {% tab mkdocs MkDocs %} + + + +```yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +version: 2 + +build: + os: ubuntu-24.04 + tools: + python: "3.13" + commands: + - asdf plugin add uv + - asdf install uv latest + - asdf global uv latest + - uv sync --group docs + - uv run mkdocs build --site-dir $READTHEDOCS_OUTPUT/html +``` + + + +{% endtab %} {% endtabs %} + This sets the Read the Docs config version (2 is required) {% rr RTD101 %}. The `build` table is the modern way to specify a runner. You need an `os` (a -modern Ubuntu should be fine) {% rr RTD102 %}, a `tools` table (we'll use Python -{% rr RTD103 %}, several languages are supported here). +modern Ubuntu should be fine) {% rr RTD102 %} and a `tools` table (we'll use +Python {% rr RTD103 %}, several languages are supported here). -Adding a `sphinx` table tells Read the Docs to enable Sphinx integration. MkDocs -is supported too. You must include one of these unless you use build commands -{% rr RTD104 %}. - -Finally, we have a `python` table with an `install` key to describe how to -install our project. This will enable our "docs" extra. +Finally, we have a `commands` table which describes how to install our +dependencies and build the documentation into the ReadTheDocs output directory. ### noxfile.py additions Add a session to your `noxfile.py` to generate docs: +{% tabs %} {% tab sphinx Sphinx %} + + +```python +@nox.session(reuse_venv=True, default=False) +def docs(session: nox.Session) -> None: + """ + Make or serve the docs. Pass --non-interactive to avoid serving. + """ + + doc_deps = nox.project.dependency_groups(PROJECT, "docs") + session.install("-e.", *doc_deps) + + if session.interactive: + session.run("mkdocs", "serve", "--clean", *session.posargs) + else: + session.run("mkdocs", "build", "--clean", *session.posargs) +``` + + + +This Nox job will invoke MkDocs to serve a live copy of your documentation under +a local endpoint, such as `http://localhost:8080` (the link will be in the job +output). By requesting a `serve` instead of a `build`, any time documentation or +the source code is changed, the documentation will automatically update. For +documentation on how to configure what directories are watched for changes, +[consult the MkDocs configuration page](https://www.mkdocs.org/user-guide/configuration/#live-reloading). + +{% endtab %} {% endtabs %} + ## API docs +{% tabs %} {% tab sphinx Sphinx %} + To build API docs, you need to add the following Nox job. It will rerun `sphinx-apidoc` to generate the sphinx autodoc pages for each of your public modules. @@ -400,8 +684,37 @@ api/ Note that your docstrings are still parsed as reStructuredText. +{% endtab %} {% tab mkdocs MkDocs %} + +API documentation can be built from your docstring using the `mkdocstrings` +plugin, as referenced previously. Unlike with Sphinx, which requires a direct +invocation of `sphinx-apidoc`, MkDocs plugins are integrated into the MkDocs +build. + +All `mkdocstrings` requires is your markdown files to specify what module, +class, or function you would like documented in said file. See the +[`mkdocstring` Usage page](https://mkdocstrings.github.io/usage/) for more +details, but for a minimal example, if you add an `api.md` file and set its +contents to: + +```markdown +# Documentation for `my_package.my_module` + +::: my_package.my_module +``` + +Where the triple colon syntax is used to specify what documentation you would +like built. In this case, we are asking to document the entire module +`my_module` (and all classes and functions within it) which is located in +`my_package`. You could instead ask for only a single component inside your +module by being more specific, like `::: my_package.my_module.MyClass`. + +{% endtab %} {% endtabs %} + ## Notebooks in docs +{% tabs %} {% tab sphinx Sphinx %} + You can combine notebooks into your docs. The tool for this is `nbsphinx`. If you want to use it, add `nbsphinx` and `ipykernel` to your documentation requirements, add `"nbsphinx"` to your `conf.py`'s `extensions =` list, and add @@ -429,6 +742,35 @@ for this to work. CI services like readthedocs usually have it installed. If you want to use Markdown instead of notebooks, you can use jupytext (see [here](https://nbsphinx.readthedocs.io/en/0.9.2/a-markdown-file.html)). +{% endtab %} {% tab mkdocs MkDocs %} + +You can combine notebooks into your docs. The plugin for this is +`mkdocs-jupyter`, and configuration is detailed +[here](https://github.com/danielfrg/mkdocs-jupyter) and you can find examples +[here](https://mkdocs-jupyter.danielfrg.com/). + +Once you have a notebook (which has been run and populated with results, as the +plugin will not execute your notebooks for you), you simply need to add a link +to the notebook in your `mkdocs.yml` navigation. + +```yaml +nav: + - Home: index.md + - Notebook page: notebook.ipynb + - Python file: python_script.py +plugins: + - mkdocs-jupyter +``` + +Note that the `mkdocs-jupyter` plugin allows you to include both python scripts +and notebooks. If you have a directory of example python files to run, consider +[`mkdocs-gallery`](https://smarie.github.io/mkdocs-gallery/) as an alternative. +For an external example, the +[ChainConsumer docs](https://samreay.github.io/ChainConsumer/generated/gallery/) +show `mkdocs-gallery` in action. + +{% endtab %} {% endtabs %} + [diátaxis]: https://diataxis.fr/ [sphinx]: https://www.sphinx-doc.org/ diff --git a/docs/pages/tutorials/docs.md b/docs/pages/tutorials/docs.md index 670bba88..336aedd5 100644 --- a/docs/pages/tutorials/docs.md +++ b/docs/pages/tutorials/docs.md @@ -34,11 +34,19 @@ Some **bold** or _italicized_ text! This documentation "source code" is then _built_ into a formats like HTML or PDF to be displayed to the user. -There are a variety of tools that can do this. In this guide we will present an -approach that is mainstream in the scientific Python community: the [Sphinx][] -documentation generator with the [MyST][] plugin. Refer to the MyST -documentation for more information on the Markdown syntax in general and MyST's -flavor of Markdown in particular. +There are a variety of tools that can do this. In this guide we will present two +approaches that are mainstream in the scientific Python community: the +[Sphinx][] documentation generator with the [MyST][] plugin, and the [MkDocs][] +generator via [mkdocs-material][]. + +For more details, examples to help you pick between Sphinx and MkDocs (and +instructions for the latter), see the [documentation guide][]. For this simple +introduction, we will use Sphinx as it is still more popular with scientific +libraries, even though MkDocs is simpler to set up and more popular in general +with Python documentation. + +Please refer to the MyST documentation for more information on the Markdown +syntax in general and MyST's flavor of Markdown in particular. We'll start with a very basic template. Start by create a `docs/` directory within your project (i.e. next to `src/`). @@ -202,4 +210,7 @@ integrate this into a package, and setup for nox. [myst]: https://myst-parser.readthedocs.io/ [organizing content]: https://myst-parser.readthedocs.io/en/latest/syntax/organising_content.html [sphinx-autodoc2]: https://sphinx-autodoc2.readthedocs.io/ +[mkdocs]: https://www.mkdocs.org/ +[mkdocs-material]: https://squidfunk.github.io/mkdocs-material/ +[documentation guide]: {% link pages/guides/docs.md %} diff --git a/noxfile.py b/noxfile.py index f60c0be1..f0d0104f 100755 --- a/noxfile.py +++ b/noxfile.py @@ -15,6 +15,7 @@ import difflib import email.parser import email.policy +import enum import functools import json import os @@ -38,6 +39,11 @@ nox.options.default_venv_backend = "uv|virtualenv" +class Docs(enum.Enum): + Sphinx = "sphinx" + MkDocs = "mkdocs" + + DIR = Path(__file__).parent.resolve() with DIR.joinpath("cookiecutter.json").open() as f: BACKENDS = json.load(f)["backend"] @@ -47,6 +53,7 @@ project_name: cookie-{backend} backend: {backend} vcs: {vcs} + docs: {docs} """ @@ -66,7 +73,7 @@ def get_expected_version(backend: str, vcs: bool) -> str: return "0.2.3" if vcs and backend not in {"maturin", "mesonpy", "uv"} else "0.1.0" -def make_copier(session: nox.Session, backend: str, vcs: bool) -> Path: +def make_copier(session: nox.Session, backend: str, vcs: bool, docs: Docs) -> Path: package_dir = Path(f"copy-{backend}") if package_dir.exists(): rmtree_ro(package_dir) @@ -86,6 +93,7 @@ def make_copier(session: nox.Session, backend: str, vcs: bool) -> Path: "--data=email=me@email.com", "--data=license=BSD", f"--data=vcs={vcs}", + f"--data=docs={docs.value}", ) init_git(session, package_dir) @@ -93,13 +101,13 @@ def make_copier(session: nox.Session, backend: str, vcs: bool) -> Path: return package_dir -def make_cookie(session: nox.Session, backend: str, vcs: bool) -> Path: +def make_cookie(session: nox.Session, backend: str, vcs: bool, docs: Docs) -> Path: package_dir = Path(f"cookie-{backend}") if package_dir.exists(): rmtree_ro(package_dir) Path("input.yml").write_text( - JOB_FILE.format(backend=backend, vcs=vcs), encoding="utf-8" + JOB_FILE.format(backend=backend, vcs=vcs, docs=docs.value), encoding="utf-8" ) session.run( @@ -114,7 +122,7 @@ def make_cookie(session: nox.Session, backend: str, vcs: bool) -> Path: return package_dir -def make_cruft(session: nox.Session, backend: str, vcs: bool) -> Path: +def make_cruft(session: nox.Session, backend: str, vcs: bool, docs: Docs) -> Path: package_dir = Path(f"cruft-{backend}") if package_dir.exists(): rmtree_ro(package_dir) @@ -124,7 +132,8 @@ def make_cruft(session: nox.Session, backend: str, vcs: bool) -> Path: session.cd(tmp_dir) Path("input.yml").write_text( - JOB_FILE.format(backend=backend, pkg=package_dir, vcs=vcs), encoding="utf-8" + JOB_FILE.format(backend=backend, pkg=package_dir, vcs=vcs, docs=docs.value), + encoding="utf-8", ) session.run( "cruft", @@ -189,14 +198,15 @@ def diff_files(p1: Path, p2: Path) -> bool: @nox.session(default=False) +@nox.parametrize("docs", list(Docs), ids=[d.value for d in Docs]) @nox.parametrize("vcs", [False, True], ids=["novcs", "vcs"]) @nox.parametrize("backend", BACKENDS, ids=BACKENDS) -def lint(session: nox.Session, backend: str, vcs: bool) -> None: +def lint(session: nox.Session, backend: str, vcs: bool, docs: Docs) -> None: session.install("cookiecutter", "pre-commit") tmp_dir = session.create_tmp() session.cd(tmp_dir) - cookie = make_cookie(session, backend, vcs) + cookie = make_cookie(session, backend, vcs, docs) session.chdir(cookie) session.run( @@ -215,7 +225,7 @@ def autoupdate(session: nox.Session, backend: str) -> None: tmp_dir = session.create_tmp() session.cd(tmp_dir) - cookie = make_cookie(session, backend, True) + cookie = make_cookie(session, backend, True, Docs.Sphinx) session.chdir(cookie) session.run("pre-commit", "autoupdate") @@ -223,14 +233,15 @@ def autoupdate(session: nox.Session, backend: str) -> None: @nox.session(default=False) +@nox.parametrize("docs", list(Docs), ids=[d.value for d in Docs]) @nox.parametrize("vcs", [False, True], ids=["novcs", "vcs"]) @nox.parametrize("backend", BACKENDS, ids=BACKENDS) -def tests(session: nox.Session, backend: str, vcs: bool) -> None: +def tests(session: nox.Session, backend: str, vcs: bool, docs: Docs) -> None: session.install("cookiecutter") tmp_dir = session.create_tmp() session.cd(tmp_dir) - cookie = make_cookie(session, backend, vcs) + cookie = make_cookie(session, backend, vcs, docs) session.chdir(cookie) name = f"cookie-{backend}" @@ -249,14 +260,15 @@ def tests(session: nox.Session, backend: str, vcs: bool) -> None: @nox.session(default=False) +@nox.parametrize("docs", list(Docs), ids=[d.value for d in Docs]) @nox.parametrize("vcs", [False, True], ids=["novcs", "vcs"]) @nox.parametrize("backend", ("poetry", "pdm", "hatch"), ids=("poetry", "pdm", "hatch")) -def native(session: nox.Session, backend: str, vcs: bool) -> None: +def native(session: nox.Session, backend: str, vcs: bool, docs: Docs) -> None: session.install("cookiecutter", backend) tmp_dir = session.create_tmp() session.cd(tmp_dir) - cookie = make_cookie(session, backend, vcs) + cookie = make_cookie(session, backend, vcs, docs) session.chdir(cookie) if backend == "hatch": @@ -270,14 +282,15 @@ def native(session: nox.Session, backend: str, vcs: bool) -> None: @nox.session(default=False) +@nox.parametrize("docs", list(Docs), ids=[d.value for d in Docs]) @nox.parametrize("vcs", [False, True], ids=["novcs", "vcs"]) @nox.parametrize("backend", BACKENDS, ids=BACKENDS) -def dist(session: nox.Session, backend: str, vcs: bool) -> None: +def dist(session: nox.Session, backend: str, vcs: bool, docs: Docs) -> None: session.install("cookiecutter", "build", "twine") tmp_dir = session.create_tmp() session.cd(tmp_dir) - cookie = make_cookie(session, backend, vcs) + cookie = make_cookie(session, backend, vcs, docs) session.chdir(cookie) session.run("python", "-m", "build", silent=True) @@ -333,14 +346,15 @@ def dist(session: nox.Session, backend: str, vcs: bool) -> None: @nox.session(name="nox", default=False) +@nox.parametrize("docs", list(Docs), ids=[d.value for d in Docs]) @nox.parametrize("vcs", [False, True], ids=["novcs", "vcs"]) @nox.parametrize("backend", BACKENDS, ids=BACKENDS) -def nox_session(session: nox.Session, backend: str, vcs: bool) -> None: +def nox_session(session: nox.Session, backend: str, vcs: bool, docs: Docs) -> None: session.install("cookiecutter", "nox") tmp_dir = session.create_tmp() session.cd(tmp_dir) - cookie = make_cookie(session, backend, vcs) + cookie = make_cookie(session, backend, vcs, docs) session.chdir(cookie) if session.posargs: @@ -358,13 +372,14 @@ def compare_copier(session): for backend in BACKENDS: for vcs in (False, True): - cookie = make_cookie(session, backend, vcs) - copier = make_copier(session, backend, vcs) + for docs in Docs: + cookie = make_cookie(session, backend, vcs, docs) + copier = make_copier(session, backend, vcs, docs) - if diff_files(cookie, copier): - session.log(f"{backend} {vcs=} passed") - else: - session.error(f"{backend} {vcs=} files are not the same!") + if diff_files(cookie, copier): + session.log(f"{backend} {vcs=} passed") + else: + session.error(f"{backend} {vcs=} {docs=} files are not the same!") @nox.session(default=False) @@ -376,13 +391,14 @@ def compare_cruft(session): for backend in BACKENDS: for vcs in (False, True): - cookie = make_cookie(session, backend, vcs) - cruft = make_cruft(session, backend, vcs) - - if diff_files(cookie, cruft): - session.log(f"{backend} {vcs=} passed") - else: - session.error(f"{backend} {vcs=} files are not the same!") + for docs in Docs: + cookie = make_cookie(session, backend, vcs, docs) + cruft = make_cruft(session, backend, vcs, docs) + + if diff_files(cookie, cruft): + session.log(f"{backend} {vcs=} passed") + else: + session.error(f"{backend} {vcs=} {docs=} files are not the same!") PC_VERS = re.compile( diff --git a/pyproject.toml b/pyproject.toml index f6af1144..a72047f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,6 +83,7 @@ scikit-hep = "sp_repo_review.families:get_families" [dependency-groups] dev = [ { include-group = "test" }, + { include-group = "cog" }, "repo-review[cli]", "validate-pyproject-schema-store[all]", ] @@ -90,6 +91,10 @@ test = [ "pytest >=7", "repo-review >=0.10.6", ] +cog = [ + "cogapp", + "cookiecutter", +] [tool.hatch] version.source = "vcs" diff --git a/tests/test_readthedocs.py b/tests/test_readthedocs.py index 9a4575b7..7b6a11e2 100644 --- a/tests/test_readthedocs.py +++ b/tests/test_readthedocs.py @@ -67,34 +67,3 @@ def test_rtd103_false() -> None: os: ubuntu-22.04 """) assert not compute_check("RTD103", readthedocs=readthedocs).result - - -def test_rtd104_commands() -> None: - readthedocs = yaml.safe_load(""" - build: - commands: [] - """) - assert compute_check("RTD104", readthedocs=readthedocs).result - - -def test_rtd104_sphinx() -> None: - readthedocs = yaml.safe_load(""" - sphinx: - configuration: docs/conf.py - """) - assert compute_check("RTD104", readthedocs=readthedocs).result - - -def test_rtd104_mkdocs() -> None: - readthedocs = yaml.safe_load(""" - mkdocs: - configuration: docs/mkdocs.yml - """) - assert compute_check("RTD104", readthedocs=readthedocs).result - - -def test_rtd104_false() -> None: - readthedocs = yaml.safe_load(""" - build: - """) - assert not compute_check("RTD104", readthedocs=readthedocs).result diff --git a/{{cookiecutter.project_name}}/.readthedocs.yaml b/{{cookiecutter.project_name}}/.readthedocs.yaml index 172d2d8b..173da630 100644 --- a/{{cookiecutter.project_name}}/.readthedocs.yaml +++ b/{{cookiecutter.project_name}}/.readthedocs.yaml @@ -11,6 +11,10 @@ build: - asdf plugin add uv - asdf install uv latest - asdf global uv latest - - uv sync --group docs + - uv sync --group docs + {%- if cookiecutter.docs == 'sphinx' %} - uv run python -m sphinx -T -b html -d docs/_build/doctrees -D language=en docs $READTHEDOCS_OUTPUT/html + {%- elif cookiecutter.docs == 'mkdocs' %} + - uv run mkdocs build --site-dir $READTHEDOCS_OUTPUT/html + {%- endif %} diff --git a/{{cookiecutter.project_name}}/README.md b/{{cookiecutter.project_name}}/README.md index ff45a1c0..8fab55ce 100644 --- a/{{cookiecutter.project_name}}/README.md +++ b/{{cookiecutter.project_name}}/README.md @@ -11,7 +11,10 @@ {%- if cookiecutter.org | lower == "scikit-hep" %} [![Scikit-HEP][sk-badge]](https://scikit-hep.org/) {%- endif %} +{%- if cookiecutter.docs == "sphinx" %} + +{%- endif %} [actions-badge]: {{cookiecutter.url}}/workflows/CI/badge.svg diff --git a/{{cookiecutter.project_name}}/docs/{% if cookiecutter.docs == 'mkdocs' %}api.md{% endif %} b/{{cookiecutter.project_name}}/docs/{% if cookiecutter.docs == 'mkdocs' %}api.md{% endif %} new file mode 100644 index 00000000..aa2f887b --- /dev/null +++ b/{{cookiecutter.project_name}}/docs/{% if cookiecutter.docs == 'mkdocs' %}api.md{% endif %} @@ -0,0 +1 @@ +# ::: {{cookiecutter.__project_slug}}.example diff --git a/{{cookiecutter.project_name}}/docs/{% if cookiecutter.docs == 'mkdocs' %}index.md{% endif %} b/{{cookiecutter.project_name}}/docs/{% if cookiecutter.docs == 'mkdocs' %}index.md{% endif %} new file mode 100644 index 00000000..96c94cb2 --- /dev/null +++ b/{{cookiecutter.project_name}}/docs/{% if cookiecutter.docs == 'mkdocs' %}index.md{% endif %} @@ -0,0 +1,18 @@ +# {{ cookiecutter.project_name }} + +Here you can document whatever you'd like on your main page. Common choices +include installation instructions, a minimal usage example, BibTex citations, +and contribution guidelines. + +See [this link](https://squidfunk.github.io/mkdocs-material/reference/) for all +the easy references and components you can use with mkdocs-material, or feel +free to go through through +[from the top](https://squidfunk.github.io/mkdocs-material/). + +## Installation + +You can install this package via running: + +```bash +pip install {{ cookiecutter.__project_slug }} +``` diff --git a/{{cookiecutter.project_name}}/docs/conf.py b/{{cookiecutter.project_name}}/docs/{% if cookiecutter.docs == 'sphinx' %}conf.py{% endif %} similarity index 100% rename from {{cookiecutter.project_name}}/docs/conf.py rename to {{cookiecutter.project_name}}/docs/{% if cookiecutter.docs == 'sphinx' %}conf.py{% endif %} diff --git a/{{cookiecutter.project_name}}/docs/index.md b/{{cookiecutter.project_name}}/docs/{% if cookiecutter.docs == 'sphinx' %}index.md{% endif %} similarity index 100% rename from {{cookiecutter.project_name}}/docs/index.md rename to {{cookiecutter.project_name}}/docs/{% if cookiecutter.docs == 'sphinx' %}index.md{% endif %} diff --git a/{{cookiecutter.project_name}}/noxfile.py b/{{cookiecutter.project_name}}/noxfile.py index d56b538a..2c139679 100644 --- a/{{cookiecutter.project_name}}/noxfile.py +++ b/{{cookiecutter.project_name}}/noxfile.py @@ -1,6 +1,8 @@ from __future__ import annotations +{% if cookiecutter.docs == 'sphinx' -%} import argparse +{% endif -%} import shutil from pathlib import Path @@ -45,6 +47,9 @@ def tests(session: nox.Session) -> None: session.run("pytest", *session.posargs) +{%- if cookiecutter.docs == 'sphinx' %} + + @nox.session(reuse_venv=True, default=False) def docs(session: nox.Session) -> None: """ @@ -95,6 +100,27 @@ def build_api_docs(session: nox.Session) -> None: ) +{%- elif cookiecutter.docs == 'mkdocs' %} + + +@nox.session(reuse_venv=True, default=False) +def docs(session: nox.Session) -> None: + """ + Make or serve the docs. Pass --non-interactive to avoid serving. + """ + + doc_deps = nox.project.dependency_groups(PROJECT, "docs") + session.install("{% if cookiecutter.backend != "mesonpy" %}-e{% endif %}.", *doc_deps) + + if session.interactive: + session.run("mkdocs", "serve", "--clean", *session.posargs) + else: + session.run("mkdocs", "build", "--clean", *session.posargs) + + +{%- endif %} + + @nox.session(default=False) def build(session: nox.Session) -> None: """ diff --git a/{{cookiecutter.project_name}}/pyproject.toml b/{{cookiecutter.project_name}}/pyproject.toml index 3f0e52fc..ab699889 100644 --- a/{{cookiecutter.project_name}}/pyproject.toml +++ b/{{cookiecutter.project_name}}/pyproject.toml @@ -125,6 +125,7 @@ test = [ dev = [ { include-group = "test" }, ] +{%- if cookiecutter.docs == 'sphinx' %} docs = [ "sphinx>=7.0", "myst_parser>=0.13", @@ -132,6 +133,16 @@ docs = [ "sphinx_autodoc_typehints", "furo>=2023.08.17", ] +{%- elif cookiecutter.docs == "mkdocs" %} +docs = [ + "markdown>=3.9", + "mdx-include>=1.4.2", + "mkdocs-material>=9.1.19", + "mkdocs>=1.1.2", + "mkdocstrings-python>=1.18.2", + "pyyaml>=6.0.1", +] +{%- endif %} {%- if cookiecutter.backend == "skbuild" %} @@ -312,6 +323,9 @@ ignore = [ [tool.ruff.lint.per-file-ignores] "tests/**" = ["T20"] "noxfile.py" = ["T20"] +{%- if cookiecutter.docs == "mkdocs" %} +"docs/examples/**" = ["T20"] +{%- endif %} [tool.pylint] diff --git a/{{cookiecutter.project_name}}/src/{{cookiecutter.__project_slug}}/example.py b/{{cookiecutter.project_name}}/src/{{cookiecutter.__project_slug}}/example.py new file mode 100644 index 00000000..99f366b4 --- /dev/null +++ b/{{cookiecutter.project_name}}/src/{{cookiecutter.__project_slug}}/example.py @@ -0,0 +1,32 @@ +"""Behold, module level docstring.""" + + +class Example: + """This class exists to show off autodocstring generation.""" + + def add(self, a: int, b: int) -> int: + """Add two integers. + + Notes: + Docstring can be useful. I promise. + + Parameters: + a: First integer to add. + b: Second integer to add. + + Returns: + The sum of the two integers. + """ + return a + b + + def subtract(self, a: int, b: int) -> int: + """Subtract two integers. + + Parameters: + a: Integer to subtract from. + b: Integer to subtract. + + Returns: + The difference of the two integers. + """ + return a - b diff --git a/{{cookiecutter.project_name}}/{% if cookiecutter.docs=='mkdocs' %}mkdocs.yml{% endif %} b/{{cookiecutter.project_name}}/{% if cookiecutter.docs=='mkdocs' %}mkdocs.yml{% endif %} new file mode 100644 index 00000000..979d0fa3 --- /dev/null +++ b/{{cookiecutter.project_name}}/{% if cookiecutter.docs=='mkdocs' %}mkdocs.yml{% endif %} @@ -0,0 +1,52 @@ +site_name: {{ cookiecutter.project_name }} +site_url: https://{{cookiecutter.project_name}}.readthedocs.io/ +site_author: "{{ cookiecutter.full_name }}" + +repo_name: "{{ cookiecutter.org }}/{{ cookiecutter.__project_slug }}" +repo_url: "{{ cookiecutter.url }}" + +theme: + name: material + icon: + repo: fontawesome/brands/github + features: + - search.suggest + - search.highlight + - navigation.expand + - navigation.tracking + - toc.follow + palette: + # See options to customise your color scheme here: + # https://squidfunk.github.io/mkdocs-material/setup/changing-the-colors/ + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: + icon: material/weather-sunny + name: Switch to light mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/weather-night + name: Switch to dark mode + +plugins: + autorefs: {} + mkdocstrings: + handlers: + python: + paths: [.] + inventories: + - https://docs.python.org/3/objects.inv + - https://docs.pydantic.dev/latest/objects.inv + options: + members_order: source + separate_signature: true + filters: ["!^_"] + show_root_heading: true + show_if_no_docstring: true + show_signature_annotations: true + search: {} + +nav: + - Home: index.md + - Python API: api.md