From db399f7708f41ebab36531324796ffe431e7315a Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Mon, 15 Jun 2026 12:21:30 -0400 Subject: [PATCH 1/2] fix(docs): remove duplicate page titles after mystmd conversion The mystmd migration (#792) gave every page a frontmatter `title:` and demoted the original `#` page heading to `##`. The book-theme renders the frontmatter title as a title-block H1, so each page showed its title twice and the landing page rendered a bare "Home" heading above the content. Drop the redundant frontmatter `title` and promote body headings back one level so MyST extracts the single `#` H1 as the page title. `short_title` still drives the nav on the GHA/style pages; the index keeps a `short_title: Home` nav label. The two packaging guides had a tip/cog block before the heading (which blocks title extraction), so their `#` heading moves to the top. Resolves the "extra Home header" and "duplicated headers" review notes from https://github.com/scientific-python/cookie/pull/801 Assisted-by: ClaudeCode:claude-opus-4.8 --- docs/pages/guides/coverage.md | 20 +++----- docs/pages/guides/docs.md | 26 ++++------ docs/pages/guides/gha_basic.md | 39 +++++++------- docs/pages/guides/gha_pure.md | 7 ++- docs/pages/guides/gha_wheels.md | 13 +++-- docs/pages/guides/index.md | 6 +-- docs/pages/guides/mypy.md | 32 +++++------- docs/pages/guides/packaging_classic.md | 26 ++++------ docs/pages/guides/packaging_compiled.md | 26 ++++------ docs/pages/guides/packaging_simple.md | 14 ++--- docs/pages/guides/pytest.md | 26 ++++------ docs/pages/guides/repo_review.md | 6 +-- docs/pages/guides/style.md | 51 +++++++++---------- docs/pages/guides/tasks.md | 30 +++++------ docs/pages/index.md | 6 +-- docs/pages/patterns/backports.md | 16 +++--- docs/pages/patterns/data_files.md | 14 ++--- docs/pages/patterns/exports.md | 8 +-- docs/pages/patterns/index.md | 6 +-- docs/pages/principles/design.md | 26 ++++------ docs/pages/principles/index.md | 6 +-- docs/pages/principles/process.md | 12 ++--- docs/pages/principles/testing.md | 68 ++++++++++++------------- docs/pages/tutorials/dev-environment.md | 14 ++--- docs/pages/tutorials/docs.md | 14 ++--- docs/pages/tutorials/index.md | 6 +-- docs/pages/tutorials/module.md | 8 +-- docs/pages/tutorials/packaging.md | 10 ++-- docs/pages/tutorials/test.md | 10 ++-- 29 files changed, 223 insertions(+), 323 deletions(-) diff --git a/docs/pages/guides/coverage.md b/docs/pages/guides/coverage.md index 82e873fb..6a0a4c73 100644 --- a/docs/pages/guides/coverage.md +++ b/docs/pages/guides/coverage.md @@ -1,8 +1,4 @@ ---- -title: "Code coverage" ---- - -## Code Coverage +# Code Coverage The "Code coverage" value of a codebase indicates how much of the production/development code is covered by the running unit tests. Maintainers @@ -32,7 +28,7 @@ adding weak tests just for coverage's sake is not a good idea. The tests should test your codebase thoroughly and should not be unreliable. ::: -### Running your tests with coverage +## Running your tests with coverage There are two common ways to calculate coverage: using `coverage` or using `pytest-cov`. While `pytest-cov` is simpler on the command line, and it promises @@ -92,7 +88,7 @@ shown below. ::: :::: -#### Configuring coverage +### Configuring coverage There is a configuration section in `pyproject.toml` for coverage. Here are some common options @@ -126,7 +122,7 @@ There are also useful reporting options. `report.exclude_lines = [...]` allows you to exclude lines from coverage. `report.fail_under` can trigger a failure if coverage is below a percent (like 100). -#### Calculating code coverage in your workflows +### Calculating code coverage in your workflows Your workflows should produce a `.coverage` file as outlined above. This file can be uploaded to `Codecov` using the [codecov/codecov-action][] action. @@ -135,7 +131,7 @@ If you would rather do it yourself, you should collect coverage files from all your jobs and combine them into one `.coverage` file before running `coverage report`, so that you get a combined score. -##### Manually combining coverage +#### Manually combining coverage If you are running in parallel, such as with `pytest-xdist`, you can set `run.parallel` to `true`, which will add a unique suffix to the coverage file(s) @@ -199,7 +195,7 @@ def tests(session: nox.Session) -> None: ::: :::: -##### Merging and reporting +#### Merging and reporting If you are running in multiple jobs, you should use upload/download artifacts so they are all available in a single combine job at the end. Each one should have @@ -219,7 +215,7 @@ def coverage(session: nox.Session) -> None: session.run("coverage", "erase") ``` -##### Configuring Codecov and uploading coverage reports +#### Configuring Codecov and uploading coverage reports Interestingly, `Codecov` does not require any initial configurations for your project, given that you have already signed up for the same using your GitHub @@ -243,7 +239,7 @@ The lines above should be added after the step that runs your tests with the for all the optional options. You'll need to specify a `CODECOV_TOKEN` secret, as well. -##### Using codecov.yml +#### Using codecov.yml One can also configure `Codecov` and coverage reports passed to `Codecov` using `codecov.yml`. `codecov.yml` should be placed inside the `.github` folder, along diff --git a/docs/pages/guides/docs.md b/docs/pages/guides/docs.md index c2e76f67..4777454d 100644 --- a/docs/pages/guides/docs.md +++ b/docs/pages/guides/docs.md @@ -1,8 +1,4 @@ ---- -title: Writing documentation ---- - -## Writing documentation +# Writing documentation Documentation used to require learning reStructuredText (sometimes referred to as reST / rST), but today we have great choices for documentation in markdown, @@ -45,7 +41,7 @@ is uncertain, and mkdocs-material will be minimally maintained until late 2026. ::: -### What to include +## What to include Ideally, software documentation should include: @@ -76,7 +72,7 @@ with render_cookie(backend="hatch", docs="mkdocs") as package: ]]] --> -### Hand-written docs +## Hand-written docs Create `docs/` directory within your project (next to `src/`). From here, Sphinx and MkDocs diverge. @@ -85,7 +81,7 @@ and MkDocs diverge. :::{tab-item} Sphinx :sync: sphinx -#### pyproject.toml additions +### pyproject.toml additions Setting a `docs` dependency group looks like this: @@ -107,7 +103,7 @@ 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 +### conf.py -### Header +## Header Your main CI workflow file should begin something like this: @@ -47,7 +46,7 @@ you use a develop branch, you probably will want to include that. You can also specify specific branches for pull requests instead of running on all PRs (will run on PRs targeting those branches only). -### Prek / Pre-commit +## Prek / Pre-commit If you use [prek][] or [pre-commit][] in CI, you can run it directly in GitHub Actions. Prek is a faster Rust rewrite of pre-commit that supports most real @@ -91,7 +90,7 @@ run a manual check, like check-manifest, then you can keep it but just use this one check. You can also use `needs: lint` in your other jobs to keep them from running if the lint check does not pass. -### Unit tests +## Unit tests Implementing unit tests is also easy. Since you should be following best practices listed in the previous sections, this becomes an almost directly @@ -158,7 +157,7 @@ Note that while versioned images are available, like `ubuntu-24.04`, these are all rolling images; selecting a specific image will not make your CI completely static. And old versioned images are decommissioned. -### Updating +## Updating {rr}`GH200` {rr}`GH210` If you use non-default actions in your repository (you will see some in the following pages), then it's a good idea to keep them @@ -190,9 +189,9 @@ which is both cleaner and sometimes required for dependent actions, like You can use this for other ecosystems too, including Python. -### Common needs +## Common needs -#### Single OS steps +### Single OS steps If you need to have a step run only on a specific OS, use an if on that step with `runner.os`: @@ -204,7 +203,7 @@ if: runner.os != 'Windows' # also 'macOS' and 'Linux' Using `runner.os` is better than `matrix.`. You also have an environment variable `$RUNNER_OS` as well. Single quotes are required here. -#### Changing the environment in a step +### Changing the environment in a step If you need to change environment variables for later steps, such combining with an if condition for only for one OS, then you add it to a special file: @@ -215,7 +214,7 @@ an if condition for only for one OS, then you add it to a special file: Later steps will see this environment variable. -#### Communicating between steps +### Communicating between steps You can also directly communicate between steps, by setting `id:`'s. Some actions have outputs, and bash actions can manually write to output: @@ -230,7 +229,7 @@ You can now refer to this step in a later step with using `${{ needs..outputs.something }}`. The `toJson()` function is useful for inputting JSON - you can even generate matrices dynamically this way! -#### Pretty output +### Pretty output You can write GitHub flavored markdown to `$GITHUB_STEP_SUMMARY`, and it will be shown on the summary page. @@ -247,7 +246,7 @@ You can also do this which tell GitHub to look for certain patterns. Do keep in mind you can only see up to 10 matches per type per step, and a total of 50 matchers. -#### Common useful actions +### Common useful actions There are a variety of useful actions. There are GitHub supplied ones: @@ -325,11 +324,11 @@ You can also run GitHub Actions locally: - [act](https://github.com/nektos/act): Run GitHub Actions in a docker image locally. -### Advanced usage +## Advanced usage These are some things you might need. -#### Cancel existing runs +### Cancel existing runs {rr}`GH102` If you add the following, you can ensure only one run per PR/branch happens at a time, cancelling the old run when a new one starts: @@ -345,7 +344,7 @@ the "from" name for the PR. If you want, you can replace `github.ref` with `github.event.pull_request.number || github.sha`; this will still cancel on PR pushes but will build each commit on `main`. -#### Pass job +### Pass job If you want support GitHub's "merge when pass" feature, you should set up a pass job instead of listing every job you wand to require. Besides making it much @@ -382,7 +381,7 @@ allowed to be skipped (`allowed-skips:`) too. Just set this `pass` job in your required checks for your main branch. Then you'll be able to use GitHub's auto merge functionality. -#### Custom actions +### Custom actions You can [write your own actions](https://docs.github.com/en/actions/creating-actions) @@ -453,7 +452,7 @@ Examples of custom composite actions include: (This repo) ::: -#### Reusable workflows +### Reusable workflows You can also make reusable workflows. One reason to do this is it allows you to use `needs` or communicate values between workflows. It's an easy way to make @@ -470,7 +469,7 @@ If you add a `outputs:` table to the workflow call table, you can specify outputs for other workflows to read. See other options [in the docs](https://docs.github.com/en/actions/using-workflows/reusing-workflows). -#### Conditional workflows +### Conditional workflows Sometimes you have jobs that depend on certain files in our repository. Maybe you only want to run tests if code or tests files are changed, docs if @@ -620,7 +619,7 @@ Some examples of repos using this method are: (this repo) ::: -#### GitHub pages +### GitHub pages GitHub has finished moving their pages build infrastructure to Actions, and they [now provide](https://github.blog/changelog/2022-07-27-github-pages-custom-github-actions-workflows-beta/) @@ -702,7 +701,7 @@ for examples. Some other examples include: - [iris-hep.org](https://github.com/iris-hep/iris-hep.github.io/blob/master/.github/workflows/deploy.yml) ::: -#### Changelog generation +### Changelog generation Not directly part of Actions, but also in `.github` is `.github/release.yml`, which lets you [configure the changelog generation][gh-changelog] button when diff --git a/docs/pages/guides/gha_pure.md b/docs/pages/guides/gha_pure.md index db8fbde0..50fd495a 100644 --- a/docs/pages/guides/gha_pure.md +++ b/docs/pages/guides/gha_pure.md @@ -1,9 +1,8 @@ --- -title: "GHA: Pure Python wheels" short_title: GitHub Actions for pure Python wheels --- -## GitHub Actions: Pure Python wheels +# GitHub Actions: Pure Python wheels We will cover binary wheels [on the next page][], but if you do not have a compiled extension, this is called a universal (pure Python) package, and the @@ -29,7 +28,7 @@ reasons that a wheel is better than only providing an sdist: [on the next page]: pages/guides/gha-wheels -### Job setup +## Job setup ```yaml name: CD @@ -56,7 +55,7 @@ releases(-only). You will also need to change the event filter below. You can merge the CI job and the CD job if you want. To do that, preferably with the name "CI/CD", you can just combine the two `on` dicts. -### Distribution: Pure Python wheels +## Distribution: Pure Python wheels ```yaml dist: diff --git a/docs/pages/guides/gha_wheels.md b/docs/pages/guides/gha_wheels.md index b2e6b299..ddfe1eeb 100644 --- a/docs/pages/guides/gha_wheels.md +++ b/docs/pages/guides/gha_wheels.md @@ -1,15 +1,14 @@ --- -title: "GHA: Binary wheels" short_title: GitHub Actions for Binary Wheels --- -## GitHub Actions: Binary wheels +# GitHub Actions: Binary wheels Building binary wheels is a bit more involved, but can still be done effectively with GHA. This document will introduce [cibuildwheel][] for use in your project. We will focus on GHA below. -### Header +## Header Wheel building should only happen rarely, so you will want to limit it to releases, and maybe a rarely moving branch or other special tag (such as @@ -39,7 +38,7 @@ Finally, if you change the workflow itself in a PR, then rebuild the wheels too. [workflow_dispatch]: https://github.blog/changelog/2020-07-06-github-actions-manual-triggers-with-workflow_dispatch/ -#### Useful suggestion +### Useful suggestion Since these variables will be used by all jobs, you could make them available in your `pyproject.toml` file, so they can be used everywhere (even locally for @@ -60,7 +59,7 @@ will cause the pip install to use the dependency-group(s) specified. The `test-command` will use pytest to run your tests. You can also set the build verbosity (`-v` in pip) if you want to. -### Making an SDist +## Making an SDist You probably should not forget about making an SDist! A simple job, like before, will work: @@ -89,7 +88,7 @@ Instead of using `uv`, you can also run `pipx run build --sdist`, or install build via pip and use `python -m build --sdist`. You can also pin the version with `pipx run build==`. -### The core job (3 main OS's) +## The core job (3 main OS's) The core of the work is down here: @@ -153,7 +152,7 @@ set that in the `pyproject.toml` file instead. You can skip specifying the `build[uv]` build-frontend option and pre-installing `uv` on the runners, but it will be a slower. -### Publishing +## Publishing Trusted Publishing is more secure and recommended {rr}`GH105`: diff --git a/docs/pages/guides/index.md b/docs/pages/guides/index.md index e0af86c3..96b733ef 100644 --- a/docs/pages/guides/index.md +++ b/docs/pages/guides/index.md @@ -1,8 +1,4 @@ ---- -title: Topical Guides ---- - -## Topical Guides +# Topical Guides The pages here are intended for developers who are making or maintaining a package and want to follow modern best practices in Python. diff --git a/docs/pages/guides/mypy.md b/docs/pages/guides/mypy.md index 1b93f72e..0a4466db 100644 --- a/docs/pages/guides/mypy.md +++ b/docs/pages/guides/mypy.md @@ -1,10 +1,6 @@ ---- -title: "Static type checking" ---- +# Static type checking -## Static type checking - -### Basics +## Basics The most exciting thing happening right now in Python development is static typing. Since Python 3.0, we've had function annotations, and since 3.6, @@ -36,7 +32,7 @@ Your tests cannot test every possible branch, every line of code. MyPy can runs rarely, that requires remote resources, that is slow, etc. All those can be checked by MyPy. It also keeps you (too?) truthful in your types. -#### Adding types +### Adding types There are three ways to add types. @@ -60,7 +56,7 @@ it for parameters and returns from functions. When running MyPy, you can use print statement but at type-checking time, or `reveal_locals()` to see all local types. -#### Configuration +### Configuration By default, MyPy does as little as possible, so that you can add it iteratively to a code base. By default: @@ -84,9 +80,9 @@ type hints for (mostly) the standard library. Third party libraries that are typed sometimes forget this last step, by the way! -### Features +## Features -#### Type narrowing +### Type narrowing One of the key features of type checking is type narrowing. The type checker monitors the types of a variable, and "narrows" it when something restricts it. @@ -116,7 +112,7 @@ reveal_type(x) This will print `A` because you removed B via the type narrowing using the `assert`. -#### Protocols +### Protocols One of the best features of MyPy is support for structural subtyping via Protocols - formalized duck-typing, basically. This allows cross library @@ -168,7 +164,7 @@ There are lots of built-in Protocols, most of which pre-date typing and are available in an Abstract Base Class form. Most of them check for one or more special methods, like `Iterable`, `Iterator`, etc. -#### Other features +### Other features Static typing has some great features worth checking out: @@ -180,9 +176,9 @@ Static typing has some great features worth checking out: - MyPy validates with the Python version you ask for, regardless of what version you are actually running. -### Complete example +## Complete example -#### Runtime compatible types +### Runtime compatible types Here's the classic syntax, which you need to use if you want to access the type annotations at runtime and you need to support Python < 3.10: @@ -207,7 +203,7 @@ def g(x: Union[str, int]) -> None: # Calling x.lower() is invalid here! ``` -#### Types as strings +### Types as strings If you don't access the types at runtime, or if you use Python 3.10+ only, then you can use a much nicer syntax. The `annotations` future feature causes the @@ -237,11 +233,11 @@ annotations at runtime. You can use the above in earlier Python versions if you use strings manually, with the same caveats. -### Tips for good types +## Tips for good types These are some guidelines to help you in writing good type hints. -#### Loose vs. specific types +### Loose vs. specific types When you have a function, you should take as generic a type as possible, and return as specific a type as possible. For example: @@ -289,7 +285,7 @@ Also note that the best place to get these in modern Python is `collections.abc`, but if you need to subscript them at runtime, you'll need Python 3.9+ or the versions in `typing`. -### Final words +## Final words When run alongside a good linter like flake8, this can catch a huge number of issues before tests or they are discovered in the wild! It also prompts _better diff --git a/docs/pages/guides/packaging_classic.md b/docs/pages/guides/packaging_classic.md index f28bc8f9..84a601bb 100644 --- a/docs/pages/guides/packaging_classic.md +++ b/docs/pages/guides/packaging_classic.md @@ -1,8 +1,4 @@ ---- -title: Classic packaging ---- - -## Classic packaging +# Classic packaging The libraries in the scientific Python ecosytem have a variety of different packaging styles, but this document is intended to outline a recommended style @@ -36,7 +32,7 @@ include a `setup.py`, and all alternate packing systems produce "normal" wheels. ::: -### Package structure (medium priority) +## Package structure (medium priority) All packages _should_ have a `src` folder, with the package code residing inside it, such as `src//`. This may seem like extra hassle; after all, you @@ -46,7 +42,7 @@ common bugs, such as running `pytest` and getting the local version instead of the installed version - this obviously tends to break if you build parts of the library or if you access package metadata. -### PEP 517/518 support (high priority) +## PEP 517/518 support (high priority) Packages should provide a `pyproject.toml` file that _at least_ looks like this: @@ -82,7 +78,7 @@ compliant SDists, as well). {rr}`PP003` Note that `"wheel"` is never required; it was injected automatically by setuptools in older versions, and is no longer used at all. -#### Special additions: NumPy +### Special additions: NumPy You may want to build against NumPy (mostly for Cython packages, pybind11 does not need to access the NumPy headers). This is the recommendation for scientific @@ -116,11 +112,11 @@ NumPy 2.0. Now you add: sure you build with NumPy 1.25+ (or 2.0+ when it comes out). ::: -### Versioning (medium/high priority) +## Versioning (medium/high priority) Scientific Python packages should use one of the following systems: -#### Git tags: official PyPA method +### Git tags: official PyPA method One more section is very useful in your `pyproject.toml` file: @@ -223,7 +219,7 @@ This will allow git archives (including the ones generated from GitHub) to also support versioning. This will only work with `setuptools_scm>=7` (though adding the files won't hurt older versions). -#### Classic in-source versioning +### Classic in-source versioning Recent versions of `setuptools` have improved in-source versioning. If you have a simple file that includes a line with a simple PEP 440 style version, like @@ -244,7 +240,7 @@ requirements only, this will fail. Flit will always look for `package.__version__`, and so will always import your package; you just have to deal with that if you use Flit. -### Setup configuration (medium priority) +## Setup configuration (medium priority) You should put as much as possible in your `setup.cfg`, and leave `setup.py` for _only_ parts that need custom logic or binary building. This keeps your @@ -354,7 +350,7 @@ junit_family = "xunit2" testpaths = ["tests"] ``` -### Extras (low/medium priority) +## Extras (low/medium priority) It is recommended to use extras instead of or in addition to making requirement files. These extras a) correctly interact with install requires and other @@ -398,7 +394,7 @@ Self dependencies can be placed in `setup.cfg` using the name of the package, such as `dev = package[test,examples]`, but this requires Pip 21.2 or newer. We recommend providing at least `test`, `docs`, and `dev`. -### Including/excluding files in the SDist +## Including/excluding files in the SDist Python packaging goes through a 3-stage procedure if you have the above recommended `pyproject.toml` file. If you type `pip install .`, then @@ -434,7 +430,7 @@ include LICENSE README.md pyproject.toml setup.py setup.cfg global-exclude __pycache__ *.py[cod] .venv ``` -### Command line +## Command line If you want to ship an "app" that a user can run from the command line, you need to add a `console_scripts` entry point. The form is: diff --git a/docs/pages/guides/packaging_compiled.md b/docs/pages/guides/packaging_compiled.md index 6eefe377..c72dc2cc 100644 --- a/docs/pages/guides/packaging_compiled.md +++ b/docs/pages/guides/packaging_compiled.md @@ -1,6 +1,4 @@ ---- -title: Compiled packaging ---- +# Packaging Compiled Projects -##### Tests +#### Tests -##### Docs +#### Docs + +## pyproject.toml: project table