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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 1 addition & 9 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,6 @@

Open work and open decisions for this repository.

## Work

- **MkDocs backend for the generator:** `src/scene_repo_template/skeleton/mkdocs/`, from the prototype on `cy/scene-docs-mkdocs` (worktree `../CodeAndDocsTemplate-mkdocs`, `api-docs/` uncommitted). It writes `docs/`, with its own docs extra (`docs_dependencies`), docs workflow and SCENE stylesheet from `scene-palette.json`; add it to `BACKENDS` in `cli.py` and to the end-to-end docs test. The logo and favicon are in `skeleton/jupyter-book/docs/images/` because `footer.md` links them relatively; move them to `common/` only if both backends can reach them there.
- **CI on this repository:** its workflows skip private repositories, like those of generated projects. If the new repository is private, delete the `if:` lines in `.github/workflows/` here; the skeleton's keep them.
- **Review and test pass:** `pytest` on Windows and Linux with `SCENE_TEMPLATE_E2E=1`, `pre-commit run --all-files --hook-stage manual`, a strict book build with link checking, `actionlint` and `zizmor` on `.github/` and the skeleton's `dot-github/`, the interactive prompts by hand, and every page checked in light and dark mode.

## Decisions

- **New repository:** this work moves to a new SCENE repository; its name and visibility are open. After the move, update the repository links in `pyproject.toml` (`urls`), `docs/myst.yml` (`github`) and the `uvx --from` command in `docs/guides/using-the-template.md`. `uvx --from git+https://…` works without credentials only on a public repository. Do not mark it as a GitHub template repository: projects come from the generator.
- **Footer funder line:** "Simons Foundation" now; SCENE's own wording is "supported by a Simons Foundation International grant administered by the Simons Foundation".
- **MLAI example headers:** authors are given as initials (C. Benquet, T. Sainsbury); the permission line has no date.
- **New repository:** this work moves to a new SCENE repository; its name and visibility are open. After the move, update the repository links in `pyproject.toml` (`urls`), `docs/myst.yml` (`github`) and `docs/guides/using-the-template.md` (uv, pip and conda tabs). Installing from `git+https://…` works without credentials only on a public repository. Do not mark it as a GitHub template repository: projects come from the generator.
2 changes: 1 addition & 1 deletion docs/guides/development-guide.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Development Guide
# Collaborating on code

Each project sets its own rules, such as who reviews what and how decisions are made.
The guidelines below are a suggested common starting point.
Expand Down
18 changes: 15 additions & 3 deletions docs/guides/using-the-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ You will need to enter:
- A one-line description
- The author
- The GitHub URL
- The documentation: Jupyter Book (guides and notebooks) or MkDocs (API reference from docstrings and notebooks)

This will show a summary, and wait for confirmation.

Expand All @@ -60,9 +61,20 @@ The project then contains:

- `pyproject.toml`, the package folder and a first test example in `tests/`;
- `.pre-commit-config.yaml` and three GitHub Actions workflows: lint, tests and docs;
- `docs/`, a Jupyter Book with SCENE's logo, colours and footer;
- `docs/`, a Jupyter Book, or an MkDocs site with `mkdocs.yml`;
- `LICENSE` (Apache-2.0) and a short `README.md`.

### Which documentation backend?

| | Jupyter Book | MkDocs |
| --- | --- | --- |
| Best for | Guides, tutorials and analyses shown as notebooks | A package with an API |
| Needed | Markdown and notebooks (`.ipynb`), shown with their saved outputs | Markdown for the documentation, plus docstrings on the code |
| Configuration | `docs/myst.yml` | `mkdocs.yml` |
| Preview | `jupyter book start` in `docs/` (needs [Node.js](https://nodejs.org)) | `mkdocs serve` |

A project can only have one. To switch later, generate a project with the other backend and copy its `docs/`, `.github/workflows/docs.yml`, `mkdocs.yml` if any, and the `docs` extra of `pyproject.toml`.

## 2. Set up your environment

In the new project, with your preferred package manager:
Expand Down Expand Up @@ -141,8 +153,8 @@ Commit `uv.lock` with the change.

- Create the repository on GitHub at the URL you decided, then commit and push.
- CI runs as soon as you push, if the repository is public and Actions is enabled (**Settings → Actions**): see *Continuous Integration* in the [Project standards](guidelines.md).
- To publish the docs, enable GitHub Pages once: see [Documentation with Jupyter Book](jupyter-book.md#publish).
- To publish the docs, enable GitHub Pages once: in **Settings → Pages**, set **Source** to **GitHub Actions**. The docs workflow then publishes on every push to `main`, for either backend. More on Jupyter Book: [Documentation with Jupyter Book](jupyter-book.md#publish).

```{important}
No docs needed? Delete `docs/` and `.github/workflows/docs.yml`.
No docs needed? Delete `docs/`, `.github/workflows/docs.yml`, and `mkdocs.yml` if present.
```
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ title: SCENE Code & Data Guidelines

Shared practices for code and data in [SCENE](https://www.project-scene.org/), the Simons Collaboration on Ecological Neuroscience, and a template repository for quick project setup.

See the [SCENE-Collaboration](https://github.com/SCENE-Collaboration) GitHub organisation for more other repositories and resources.
See the [SCENE-Collaboration](https://github.com/SCENE-Collaboration) GitHub organisation for other repositories and resources.

::::{grid} 1 1 2 2

Expand Down
14 changes: 14 additions & 0 deletions src/scene_repo_template/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,20 @@
"`.github/workflows/docs.yml` publishes it to GitHub Pages once Pages is enabled with GitHub Actions as its source."
),
},
"mkdocs": {
"label": "MkDocs: API reference from docstrings, and notebooks",
# MkDocs 2.0 drops the plugin system mkdocstrings relies on.
"docs_dependencies": (
'"mkdocs>=1.6,<2", "mkdocs-api-autonav>=0.1", "mkdocs-autorefs>=1.2", "mkdocs-jupyter>=0.25", '
'"mkdocs-material>=9.5", "mkdocstrings[python]>=0.27", "pymdown-extensions<12.2; python_version < \'3.11\'"'
),
"docs_readme": (
"Built from `docs/` and `mkdocs.yml` with [MkDocs](https://www.mkdocs.org) and Material, "
"the API reference generated from docstrings and notebooks shown as pages: "
'after `pip install -e ".[docs]"` (or `uv sync --extra docs`), `mkdocs serve` serves it locally. '
"`.github/workflows/docs.yml` publishes it to GitHub Pages once Pages is enabled with GitHub Actions as its source."
),
},
}

TEMPLATE_SUFFIX = ".tmpl"
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/scene_repo_template/skeleton/mkdocs/docs/index.md.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# $title

$description

The API reference is generated from the docstrings of `$package`. Add pages, Markdown files or notebooks, next to this one and list them under `nav:` in `mkdocs.yml`.
54 changes: 54 additions & 0 deletions src/scene_repo_template/skeleton/mkdocs/docs/stylesheets/scene.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/* SCENE theme for MkDocs Material, in the colours of the SCENE logo. */

:root {
--md-primary-fg-color: #125069; /* petrol blue */
--md-primary-fg-color--light: #12638e; /* ocean blue */
--md-primary-fg-color--dark: #0d3a4c;
--md-accent-fg-color: #41cfdb; /* cyan */
--md-typeset-a-color: #12638e;
}

[data-md-color-scheme="slate"] {
--md-primary-fg-color: #1f2d36; /* ink */
--md-typeset-a-color: #41cfdb;
}

/* Brand stripe above the header, in the logo's colours. */
.md-header::before {
content: "";
display: block;
height: 4px;
background: linear-gradient(90deg, #41cfdb, #31c5a1, #80c848, #edb71f, #e98724, #d76c26);
}

.md-typeset h2 {
padding-bottom: 0.25em;
border-bottom: 2px solid var(--md-accent-fg-color);
}

.md-copyright {
width: 100%;
text-align: center;
}

/* Footer line: the name with its initials in stripe order (golden yellow skipped), as in the SCENE Jupyter Book,
the logo, then the funder. Markup: `copyright` in mkdocs.yml. */
.scene-logo {
display: inline-block;
width: 1.4em;
height: 1.4em;
margin: 0 0.6em;
vertical-align: middle;
/* A background image, not an <img>: its URL is relative to this file, so it holds on every page. */
background: url("../images/scene-logo.png") center / contain no-repeat;
}

.scene-footer strong {
color: #fff;
}

.scene-footer em b:nth-of-type(1) { color: #41cfdb; }
.scene-footer em b:nth-of-type(2) { color: #31c5a1; }
.scene-footer em b:nth-of-type(3) { color: #80c848; }
.scene-footer em b:nth-of-type(4) { color: #e98724; }
.scene-footer em b:nth-of-type(5) { color: #d76c26; }
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Docs

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

jobs:
build:
# CI runs on public repositories only: private ones draw on the org's Actions quota, which can be billed. Delete this line to opt in.
if: ${{ !github.event.repository.private }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
with:
python-version: "3.13"
- run: uv sync --extra docs
- run: uv run mkdocs build --strict
- uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
path: site

deploy:
# Pull requests only check that the site builds.
if: ${{ github.event_name == 'push' && !github.event.repository.private }}
needs: build
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
concurrency:
group: pages
cancel-in-progress: false
steps:
- uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
- id: deployment
uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
83 changes: 83 additions & 0 deletions src/scene_repo_template/skeleton/mkdocs/mkdocs.yml.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# MkDocs configuration: https://www.mkdocs.org, theme https://squidfunk.github.io/mkdocs-material/
# Preview locally with `mkdocs serve`; build with `mkdocs build --strict`. Pages are in docs/.
site_name: "$title"
site_description: "$description"
repo_url: $github_url

theme:
name: material
logo: images/scene-logo.png
favicon: images/favicon.png
palette:
- media: "(prefers-color-scheme: light)"
scheme: default
primary: custom
accent: custom
toggle:
icon: material/brightness-7
name: Switch to dark mode
- media: "(prefers-color-scheme: dark)"
scheme: slate
primary: custom
accent: custom
toggle:
icon: material/brightness-4
name: Switch to light mode
features:
- content.code.copy
- navigation.indexes
- navigation.path
- navigation.sections
- navigation.top
- search.highlight
- search.suggest
- toc.follow

extra_css:
- stylesheets/scene.css

plugins:
- search
- autorefs
# One page per module of the package, under "API reference".
- api-autonav:
modules: ["$package"]
nav_section_title: "API reference"
exclude_private: true
- mkdocstrings:
default_handler: python
handlers:
python:
options:
show_root_heading: true
show_symbol_type_heading: true
separate_signature: true
show_signature_annotations: true
docstring_style: google
line_length: 120
inventories:
- url: https://docs.python.org/3/objects.inv
domains: [py, std]
# Notebooks (.ipynb) are pages, shown with the outputs saved in them, not re-executed.
# Only notebooks: by default, .py files in docs/ would become pages too.
- mkdocs-jupyter:
include: ["*.ipynb"]

markdown_extensions:
- attr_list
- admonition
- pymdownx.details
- pymdownx.highlight:
anchor_linenums: true
- pymdownx.inlinehilite
- pymdownx.superfences

extra:
generator: false

# The SCENE name with coloured initials, styled in docs/stylesheets/scene.css.
copyright: "<span class='scene-footer'><em><b>S</b>imons <b>C</b>ollaboration on <b>E</b>cological <b>N</b><b>E</b>uroscience</em><span class='scene-logo'></span><strong>Simons Foundation</strong></span>"

nav:
- Home: index.md
- API reference
85 changes: 62 additions & 23 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import importlib
import importlib.metadata
import json
import os
import shutil
import subprocess
Expand Down Expand Up @@ -37,27 +38,51 @@ def refused(tmp_path, *options, folder="my-project"):
return exit_info.value.code == 2


def test_a_project_gets_the_common_files_and_its_docs_backend(tmp_path):
expected = {
".github/workflows/docs.yml",
".github/workflows/lint.yml",
".github/workflows/tests.yml",
".gitignore",
".pre-commit-config.yaml",
"LICENSE",
"README.md",
"docs/_static/footer.md",
"docs/_static/scene.css",
"docs/images/favicon.png",
"docs/images/scene-logo.png",
"docs/index.md",
"docs/myst.yml",
"my_project/__init__.py",
"my_project/version.py",
"pyproject.toml",
"tests/test_version.py",
}
assert expected <= files_of(generate(tmp_path))
COMMON_FILES = {
".github/workflows/docs.yml",
".github/workflows/lint.yml",
".github/workflows/tests.yml",
".gitignore",
".pre-commit-config.yaml",
"LICENSE",
"README.md",
"docs/images/favicon.png",
"docs/images/scene-logo.png",
"docs/index.md",
"my_project/__init__.py",
"my_project/version.py",
"pyproject.toml",
"tests/test_version.py",
}
BACKEND_FILES = {
"jupyter-book": {"docs/myst.yml", "docs/_static/footer.md", "docs/_static/scene.css"},
"mkdocs": {"mkdocs.yml", "docs/stylesheets/scene.css"},
}


def test_every_backend_is_tested():
assert set(BACKEND_FILES) == set(cli.BACKENDS)


@pytest.mark.parametrize("backend", BACKEND_FILES)
def test_a_project_gets_the_common_files_and_only_its_own_docs_backend(tmp_path, backend):
project = files_of(generate(tmp_path, f"--docs={backend}"))
assert COMMON_FILES | BACKEND_FILES[backend] <= project
other_backends = set().union(*(files for name, files in BACKEND_FILES.items() if name != backend))
assert project & other_backends == set()


def test_the_backends_ship_the_same_logo_and_favicon():
for image in ("scene-logo.png", "favicon.png"):
copies = {(cli.SKELETON / backend / "docs" / "images" / image).read_bytes() for backend in cli.BACKENDS}
assert len(copies) == 1, image


def test_the_docs_extra_follows_the_backend(tmp_path):
pyproject = (generate(tmp_path, "--docs=mkdocs") / "pyproject.toml").read_text(encoding="utf-8")
assert '"mkdocstrings[python]>=0.27"' in pyproject
assert '"mkdocs-jupyter>=0.25"' in pyproject
assert "jupyter-book" not in pyproject


def test_no_skeleton_naming_is_left_in_a_project(tmp_path):
Expand Down Expand Up @@ -303,7 +328,21 @@ def test_a_generated_project_installs_lints_and_passes_its_tests(tmp_path):

@E2E
@pytest.mark.skipif(shutil.which("node") is None, reason="Jupyter Book needs Node.js")
def test_a_generated_project_builds_its_docs(tmp_path):
dest = generate(tmp_path)
def test_a_generated_jupyter_book_builds(tmp_path):
dest = generate(tmp_path, "--docs=jupyter-book")
run(dest, "uv", "sync", "--extra", "docs")
run(dest / "docs", "uv", "run", "jupyter", "book", "build", "--html", "--strict")


@E2E
def test_a_generated_mkdocs_site_builds_with_an_api_page_per_module_and_a_notebook(tmp_path):
dest = generate(tmp_path, "--docs=mkdocs")
notebook = {"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# Analysis"]}], "metadata": {}}
notebook |= {"nbformat": 4, "nbformat_minor": 5}
(dest / "docs" / "analysis.ipynb").write_text(json.dumps(notebook), encoding="utf-8")
mkdocs_yml = dest / "mkdocs.yml"
mkdocs_yml.write_text(mkdocs_yml.read_text(encoding="utf-8") + " - analysis.ipynb\n", encoding="utf-8")
run(dest, "uv", "sync", "--extra", "docs")
run(dest, "uv", "run", "mkdocs", "build", "--strict")
assert (dest / "site" / "reference" / "my_project" / "version" / "index.html").is_file()
assert (dest / "site" / "analysis" / "index.html").is_file()
Loading