diff --git a/.gitignore b/.gitignore index c0c542e6..f3f339c4 100644 --- a/.gitignore +++ b/.gitignore @@ -148,7 +148,6 @@ docs/assets/js/repo-review-app.min.js.map # NodeJS stuff, just in case (developer tooling) node_modules/ package-lock.json -package.json # readthedocs _readthedocs diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index caa7ca73..ea91221e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,7 +32,7 @@ repos: rev: "v0.15.14" hooks: - id: ruff-check - args: ["--fix", "--show-fixes"] + args: ["--fix"] - id: ruff-format - repo: https://github.com/pre-commit/pygrep-hooks @@ -63,8 +63,13 @@ repos: rev: "v3.8.3" hooks: - id: prettier - types_or: [yaml, markdown, html, css, scss, javascript, json] - args: [--prose-wrap=always] + types_or: [yaml, html, css, scss, javascript, json] + + - repo: https://github.com/rvben/rumdl-pre-commit + rev: v0.2.0 + hooks: + - id: rumdl + args: [--no-exclude] # Disable all exclude patterns - repo: https://github.com/crate-ci/typos rev: "v1.46.3" @@ -92,5 +97,5 @@ repos: name: Cog the pages language: python entry: cog -P -r -I ./helpers - files: "^docs/pages/guides/(packaging_compiled|docs|tasks|gha_basic).md|^copier.yml|^docs/_includes/pyproject.md" + files: "^docs/pages/guides/(packaging_compiled|docs|tasks|gha_basic).md|^copier.yml|^docs/_partials/pyproject.md" additional_dependencies: [cogapp, cookiecutter, tomlkit] diff --git a/.readthedocs.yaml b/.readthedocs.yaml index eb3d433f..d9961999 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,21 +1,20 @@ -# .readthedocs.yaml -# Read the Docs configuration file +# Read the Docs configuration file for myst # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 -# Set the version of Python and other tools you might need build: os: ubuntu-24.04 - tools: - ruby: "3.4" - + nodejs: "24" commands: - - ./helpers/fetch_repo_review_app.sh - - bundle install - - > - JEKYLL_ENV=production bundle exec jekyll build --destination - _readthedocs/html --baseurl $(echo -n "$READTHEDOCS_CANONICAL_URL" | cut - -d '/' -f 4-) + # Install myst + - npm install -g mystmd + # Build the site + - cd docs && myst build --html + # Copy the output to Read the Docs expected location + - mkdir -p $READTHEDOCS_OUTPUT/html/ + - cp -r docs/_build/html/. "$READTHEDOCS_OUTPUT/html" + # Clean up build artifacts + - rm -rf docs/_build diff --git a/.ruby-version b/.ruby-version deleted file mode 100644 index 47b322c9..00000000 --- a/.ruby-version +++ /dev/null @@ -1 +0,0 @@ -3.4.1 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..1633652d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,88 @@ +# Agent Guide for scientific-python/cookie + +This repo has three distinct concerns: a **cookiecutter/copier template** for +new Python projects (`{{cookiecutter.project_name}}/`), a **repo-review plugin** +(`src/sp_repo_review/`), and a **Jekyll-based developer guide** (`docs/`). + +## Key commands + +### sp-repo-review (the package) + +- Run tests: `uv run pytest` (or `uvx nox -s rr_tests`) +- Run linting (pre-commit via prek): `uvx nox -s rr_lint` +- Run pylint: `uvx nox -s rr_pylint` +- Run the CLI: `uvx nox -s rr_run -- ` +- Regenerate README check list via cog: `uvx nox -s readme` +- Build wheel/sdist: `uvx nox -s rr_build` + +Important: tests run with `PYTHONWARNDEFAULTENCODING=1`. + +### Cookie template validation + +The noxfile generates temporary projects for **all 9 backends** × **vcs on/off** +× **3 docs engines** (sphinx/mkdocs/zensical). These are slow. + +- `nox -s "tests(hatch)"` — run generated project tests for a single backend +- `nox -s "lint(hatch)"` — run pre-commit (`prek`) on generated project +- `nox -s "dist(hatch)"` — verify build output includes LICENSE +- `nox -s "native(hatch)"` — test hatch/pdm/poetry native test runners +- `nox -s compare_copier` — verify cookiecutter and copier produce identical + files +- `nox -s compare_cruft` — verify cookiecutter and cruft produce identical files +- `nox -s gha_bump` — bump GitHub Actions versions across docs and templates +- `nox -s pc_bump` — bump pre-commit hook versions across docs and templates + +## Architecture notes + +- **Template directory**: `{{cookiecutter.project_name}}/` contains the + cookiecuttter template. Copier reads `copier.yml`; cookiecutter reads + `cookiecutter.json`. Keep them in sync; `compare_copier` checks this. +- **Entry points**: `sp_repo_review` registers `repo-review` + checks/families/fixtures via `[project.entry-points]` in `pyproject.toml`. +- **Generated docs**: The README check list (line ~300+) is a cog block. Do not + edit it by hand; run `nox -s readme` or cog will fail in CI. +- **Cookie template `.pre-commit-config.yaml`** uses `prek` (a Rust-based + pre-commit alternative), not `pre-commit`. +- **Ruff hook ID**: `.pre-commit-config.yaml` uses `ruff-check` as the hook id + (not `ruff`), for pre-commit 4.x compatibility. + +## Style and conventions + +- `tool.pytest.ini_options.norecursedirs` excludes + `{{cookiecutter.project_name}}` so pytest does not descend into the template + directory. +- `tool.ruff.extend-exclude` also excludes `\{\{cookiecutter.project_name\}\}` + (double-escaped in TOML). +- `tool.mypy.python_version = "3.10"`; the `sp_repo_review.*` override enforces + `disallow_untyped_defs=True`. +- `tool.pylint.master.ignore-paths` ignores `src/sp_repo_review/_version.py` + (auto-generated by hatch-vcs). +- Ruff selects `ALL` with many ignores; notable: `S101` (assert) and `D` + (docstrings) are globally disabled. + +## CI quirks + +- CI uses change detection to decide whether to run cookie tests or rr-tests. + Both are required to pass for the `pass` job. +- rr-tests matrix runs on Python 3.10, 3.12, 3.14 across ubuntu/macos/windows. +- Cookie tests reuse the same `reusable-cookie.yml` workflow. + +## Docs site (MyST) + +- Migrated from Jekyll to [MyST](https://mystmd.org) (JupyterBook 2.0) using the + `scientific-python-myst-theme`. +- Node/Bun-based; from the repo root, run `bun install` then `bun run build` to + build the site. +- Config: `docs/myst.yml` (TOC, project settings), + `docs/config/scientific-python.yml` (theme options). +- Custom plugin: `docs/rr-role.mjs` — provides `{rr}` inline role for + repo-review badge spans. +- Custom CSS: `docs/assets/css/site.css` — only `.rr-btn` badge styling remains. +- Docs pages in `docs/pages/` contain cog blocks that auto-generate config + examples from the template. +- The repo-review interactive page uses an `{iframe}` pointing to the WASM app + at `https://scientific-python.github.io/repo-review/`. +- Tab-sets use `:sync: ` for cross-page tab synchronization, where the + sync key is the tab label itself (e.g., `sphinx`, `mkdocs`, + `trusted-publishing`, `scikit-build-core`). +- TOML code bloacks use "ini" to get syntax highlighting for now. diff --git a/Gemfile b/Gemfile deleted file mode 100644 index 01fee0f6..00000000 --- a/Gemfile +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true - -source 'https://rubygems.org' - -# Hello! This is where you manage which Jekyll version is used to run. -# When you want to use a different version, change it below, save the -# file and run `bundle install`. Run Jekyll with `bundle exec`, like so: -# -# bundle exec jekyll serve -# -# This will help ensure the proper Jekyll version is running. -# Happy Jekylling! - -gem 'jekyll' - -# This is the theme -gem 'just-the-docs' - -# This is needed for GitHub Flavored Markdown -gem 'kramdown-parser-gfm' - -# Used to be in the stdlib -gem 'logger' - -# If you have any plugins, put them here! -group :jekyll_plugins do - gem 'jekyll-feed' - gem 'jekyll-seo-tag' -end - -group :development do - # Verify good coding practices in Ruby files - gem 'rubocop', '~>1.52', require: false - - # Check links. Use: - # bundle exec jekyll build - # bundle exec htmlproofer --assume_extension '.html' ./_site - gem 'html-proofer' -end - -# Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem -# and associated library. -platforms :mingw, :x64_mingw, :mswin, :jruby do - gem 'tzinfo' - gem 'tzinfo-data' -end - -gem 'wdm', install_if: Gem.win_platform? diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 40ecaa71..00000000 --- a/Gemfile.lock +++ /dev/null @@ -1,243 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - Ascii85 (2.0.1) - addressable (2.8.9) - public_suffix (>= 2.0.2, < 8.0) - afm (1.0.0) - ast (2.4.3) - async (2.38.1) - console (~> 1.29) - fiber-annotation - io-event (~> 1.11) - metrics (~> 0.12) - traces (~> 0.18) - base64 (0.3.0) - benchmark (0.5.0) - bigdecimal (3.3.1) - colorator (1.1.0) - concurrent-ruby (1.3.6) - console (1.34.3) - fiber-annotation - fiber-local (~> 1.1) - json - csv (3.3.5) - em-websocket (0.5.3) - eventmachine (>= 0.12.9) - http_parser.rb (~> 0) - ethon (0.18.0) - ffi (>= 1.15.0) - logger - eventmachine (1.2.7) - ffi (1.17.4-aarch64-linux-gnu) - ffi (1.17.4-aarch64-linux-musl) - ffi (1.17.4-arm-linux-gnu) - ffi (1.17.4-arm-linux-musl) - ffi (1.17.4-arm64-darwin) - ffi (1.17.4-x86_64-darwin) - ffi (1.17.4-x86_64-linux-gnu) - ffi (1.17.4-x86_64-linux-musl) - fiber-annotation (0.2.0) - fiber-local (1.1.0) - fiber-storage - fiber-storage (1.0.1) - forwardable-extended (2.6.0) - google-protobuf (4.34.1) - bigdecimal - rake (~> 13.3) - google-protobuf (4.34.1-aarch64-linux-gnu) - bigdecimal - rake (~> 13.3) - google-protobuf (4.34.1-aarch64-linux-musl) - bigdecimal - rake (~> 13.3) - google-protobuf (4.34.1-arm64-darwin) - bigdecimal - rake (~> 13.3) - google-protobuf (4.34.1-x86_64-darwin) - bigdecimal - rake (~> 13.3) - google-protobuf (4.34.1-x86_64-linux-gnu) - bigdecimal - rake (~> 13.3) - google-protobuf (4.34.1-x86_64-linux-musl) - bigdecimal - rake (~> 13.3) - hashery (2.1.2) - html-proofer (5.2.1) - addressable (~> 2.3) - async (~> 2.1) - benchmark (~> 0.5) - nokogiri (~> 1.13) - pdf-reader (~> 2.11) - rainbow (~> 3.0) - typhoeus (~> 1.3) - yell (~> 2.0) - zeitwerk (~> 2.5) - http_parser.rb (0.8.1) - i18n (1.14.8) - concurrent-ruby (~> 1.0) - io-event (1.14.5) - jekyll (4.4.1) - addressable (~> 2.4) - base64 (~> 0.2) - colorator (~> 1.0) - csv (~> 3.0) - em-websocket (~> 0.5) - i18n (~> 1.0) - jekyll-sass-converter (>= 2.0, < 4.0) - jekyll-watch (~> 2.0) - json (~> 2.6) - kramdown (~> 2.3, >= 2.3.1) - kramdown-parser-gfm (~> 1.0) - liquid (~> 4.0) - mercenary (~> 0.3, >= 0.3.6) - pathutil (~> 0.9) - rouge (>= 3.0, < 5.0) - safe_yaml (~> 1.0) - terminal-table (>= 1.8, < 4.0) - webrick (~> 1.7) - jekyll-feed (0.17.0) - jekyll (>= 3.7, < 5.0) - jekyll-include-cache (0.2.1) - jekyll (>= 3.7, < 5.0) - jekyll-sass-converter (3.1.0) - sass-embedded (~> 1.75) - jekyll-seo-tag (2.8.0) - jekyll (>= 3.8, < 5.0) - jekyll-watch (2.2.1) - listen (~> 3.0) - json (2.19.3) - just-the-docs (0.12.0) - jekyll (>= 3.8.5) - jekyll-include-cache - jekyll-seo-tag (>= 2.0) - rake (>= 12.3.1) - kramdown (2.5.2) - rexml (>= 3.4.4) - kramdown-parser-gfm (1.1.0) - kramdown (~> 2.0) - language_server-protocol (3.17.0.5) - lint_roller (1.1.0) - liquid (4.0.4) - listen (3.10.0) - logger - rb-fsevent (~> 0.10, >= 0.10.3) - rb-inotify (~> 0.9, >= 0.9.10) - logger (1.7.0) - mercenary (0.4.0) - metrics (0.15.0) - nokogiri (1.19.2-aarch64-linux-gnu) - racc (~> 1.4) - nokogiri (1.19.2-aarch64-linux-musl) - racc (~> 1.4) - nokogiri (1.19.2-arm-linux-gnu) - racc (~> 1.4) - nokogiri (1.19.2-arm-linux-musl) - racc (~> 1.4) - nokogiri (1.19.2-arm64-darwin) - racc (~> 1.4) - nokogiri (1.19.2-x86_64-darwin) - racc (~> 1.4) - nokogiri (1.19.2-x86_64-linux-gnu) - racc (~> 1.4) - nokogiri (1.19.2-x86_64-linux-musl) - racc (~> 1.4) - parallel (1.28.0) - parser (3.3.11.1) - ast (~> 2.4.1) - racc - pathutil (0.16.2) - forwardable-extended (~> 2.6) - pdf-reader (2.15.1) - Ascii85 (>= 1.0, < 3.0, != 2.0.0) - afm (>= 0.2.1, < 2) - hashery (~> 2.0) - ruby-rc4 - ttfunk - prism (1.9.0) - public_suffix (7.0.5) - racc (1.8.1) - rainbow (3.1.1) - rake (13.3.1) - rb-fsevent (0.11.2) - rb-inotify (0.11.1) - ffi (~> 1.0) - regexp_parser (2.11.3) - rexml (3.4.4) - rouge (4.7.0) - rubocop (1.86.0) - json (~> 2.3) - language_server-protocol (~> 3.17.0.2) - lint_roller (~> 1.1.0) - parallel (~> 1.10) - parser (>= 3.3.0.2) - rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.49.0, < 2.0) - ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.49.1) - parser (>= 3.3.7.2) - prism (~> 1.7) - ruby-progressbar (1.13.0) - ruby-rc4 (0.1.5) - safe_yaml (1.0.5) - sass-embedded (1.98.0-aarch64-linux-gnu) - google-protobuf (~> 4.31) - sass-embedded (1.98.0-aarch64-linux-musl) - google-protobuf (~> 4.31) - sass-embedded (1.98.0-arm-linux-gnueabihf) - google-protobuf (~> 4.31) - sass-embedded (1.98.0-arm-linux-musleabihf) - google-protobuf (~> 4.31) - sass-embedded (1.98.0-arm64-darwin) - google-protobuf (~> 4.31) - sass-embedded (1.98.0-x86_64-darwin) - google-protobuf (~> 4.31) - sass-embedded (1.98.0-x86_64-linux-gnu) - google-protobuf (~> 4.31) - sass-embedded (1.98.0-x86_64-linux-musl) - google-protobuf (~> 4.31) - terminal-table (3.0.2) - unicode-display_width (>= 1.1.1, < 3) - traces (0.18.2) - ttfunk (1.8.0) - bigdecimal (~> 3.1) - typhoeus (1.6.0) - ethon (>= 0.18.0) - unicode-display_width (2.6.0) - wdm (0.2.0) - webrick (1.9.2) - yell (2.2.2) - zeitwerk (2.7.5) - -PLATFORMS - aarch64-linux - aarch64-linux-gnu - aarch64-linux-musl - arm-linux-gnu - arm-linux-gnueabihf - arm-linux-musl - arm-linux-musleabihf - arm64-darwin - x86_64-darwin - x86_64-linux - x86_64-linux-gnu - x86_64-linux-musl - -DEPENDENCIES - html-proofer - jekyll - jekyll-feed - jekyll-seo-tag - just-the-docs - kramdown-parser-gfm - logger - rubocop (~> 1.52) - tzinfo - tzinfo-data - wdm - -BUNDLED WITH - 2.6.5 diff --git a/README.md b/README.md index 005b5878..2aca5c4a 100644 --- a/README.md +++ b/README.md @@ -42,30 +42,30 @@ hand, from `{{cookiecutter.project_name}}/`). During generation you can select from the following backends for your package: -1. [hatch][]: This uses hatchling, a modern builder with nice file inclusion, - extendable via plugins, and good error messages. **(Recommended for pure - Python projects)** -2. [uv][]: The `uv_build` backend is written in Rust and is integrated into' - `uv`, meaning it can build without downloading anything extra and can even - avoid running Python at all when building, making it the fastest backend for - simple packages. No dynamic metadata support. -3. [flit][]: A modern, lightweight [PEP 621][] build system for pure Python - projects. Replaces setuptools, no MANIFEST.in, setup.py, or setup.cfg. Low - learning curve. Easy to bootstrap into new distributions. Difficult to get - the right files included, little dynamic metadata support. -4. [pdm][]: A modern, less opinionated all-in-one solution to pure Python - projects supporting standards. Replaces setuptools, venv/pipenv, pip, wheel, - and twine. Supports [PEP 621][]. -5. [poetry][]: An all-in-one solution to pure Python projects. Replaces - setuptools, venv/pipenv, pip, wheel, and twine. Higher learning curve, but - is all-in-one. Makes some bad default assumptions for libraries. -6. [setuptools][]: The classic build system, but with the new standardized - configuration. -7. [pybind11][]: This is setuptools but with an C++ extension written in - [pybind11][] and wheels generated by [cibuildwheel][]. -8. [scikit-build][]: A scikit-build (CMake) project also using pybind11, using - scikit-build-core. **(Recommended for C++ projects)** -9. [meson-python][]: A Meson project also using pybind11. (No VCS versioning) +1. [hatch][]: This uses hatchling, a modern builder with nice file inclusion, + extendable via plugins, and good error messages. **(Recommended for pure + Python projects)** +2. [uv][]: The `uv_build` backend is written in Rust and is integrated into' + `uv`, meaning it can build without downloading anything extra and can even + avoid running Python at all when building, making it the fastest backend for + simple packages. No dynamic metadata support. +3. [flit][]: A modern, lightweight [PEP 621][] build system for pure Python + projects. Replaces setuptools, no MANIFEST.in, setup.py, or setup.cfg. Low + learning curve. Easy to bootstrap into new distributions. Difficult to get + the right files included, little dynamic metadata support. +4. [pdm][]: A modern, less opinionated all-in-one solution to pure Python + projects supporting standards. Replaces setuptools, venv/pipenv, pip, wheel, + and twine. Supports [PEP 621][]. +5. [poetry][]: An all-in-one solution to pure Python projects. Replaces + setuptools, venv/pipenv, pip, wheel, and twine. Higher learning curve, but + is all-in-one. Makes some bad default assumptions for libraries. +6. [setuptools][]: The classic build system, but with the new standardized + configuration. +7. [pybind11][]: This is setuptools but with an C++ extension written in + [pybind11][] and wheels generated by [cibuildwheel][]. +8. [scikit-build][]: A scikit-build (CMake) project also using pybind11, using + scikit-build-core. **(Recommended for C++ projects)** +9. [meson-python][]: A Meson project also using pybind11. (No VCS versioning) 10. [maturin][]: A [PEP 621][] builder for Rust binary extensions. (No VCS versioning) **(Recommended for Rust projects)** @@ -73,7 +73,7 @@ Currently, the best choice is probably hatch for pure Python projects, and scikit-build (such as the scikit-build-core + pybind11 choice) for binary projects. -#### To use (copier version) +### To use (copier version) Install `copier` and `copier-templates-extensions`. Using [uv][], that's: @@ -132,7 +132,7 @@ There are a few example dependencies and a minimum Python version of 3.10, feel free to change it to whatever you actually need/want. There is also a basic backports structure with a small typing example. -#### Contained components: +#### Contained components - GitHub Actions runs testing for the generation itself - Uses nox so cookie development can be checked locally @@ -160,7 +160,7 @@ backports structure with a small typing example. - A README - Code coverage reporting with automatic uploads to Codecov after tests run -#### For developers: +#### For developers You can test locally with [nox][]: @@ -196,8 +196,6 @@ A lot of the guide, cookiecutter, and repo-review started out as part of [Scikit-HEP][]. These projects were merged, generalized, and combined with the [NSLS-II][] guide during the 2023 Scientific-Python Developers Summit. - - [actions-badge]: https://github.com/scientific-python/cookie/actions/workflows/ci.yml/badge.svg [actions-link]: https://github.com/scientific-python/cookie/actions [cibuildwheel]: https://cibuildwheel.readthedocs.io @@ -227,8 +225,6 @@ A lot of the guide, cookiecutter, and repo-review started out as part of [setuptools]: https://setuptools.readthedocs.io [uv]: https://docs.astral.sh/uv - - --- ## sp-repo-review @@ -302,7 +298,7 @@ CLI requirements are included. ## List of checks - + + diff --git a/_config.yml b/_config.yml deleted file mode 100644 index f55fae1d..00000000 --- a/_config.yml +++ /dev/null @@ -1,57 +0,0 @@ -title: Scientific Python Development Guide -email: guide@scientific-python.org -description: >- - This guide is maintained by the scientific Python community for the benefit of - fellow scientists and research software engineers. -github_username: scientific-python -source: docs - -# Build settings -markdown: kramdown -theme: "just-the-docs" -plugins: - - jekyll-feed - -# Theme settings - -logo: "/assets/images/logo.svg" - -# Enable or disable the site search -search_enabled: true - -# Aux links for the upper right navigation -aux_links: - Scientific Python: - - https://scientific-python.org - Learn: - - https://learn.scientific-python.org - Cookie: - - https://github.com/scientific-python/cookie - -gh_edit_link: true -gh_edit_link_text: "View source for this page on GitHub." -gh_edit_repository: "https://github.com/scientific-python/cookie" -gh_edit_branch: "main" -gh_edit_source: "docs" -gh_edit_view_mode: "tree" # "tree" or "edit" - -callouts: - warning: - color: red - title: Warning - note: - color: yellow - title: Note - important: - color: green - title: Important - highlight: - color: blue - -# Workaround for dep warnings -sass: - quiet_deps: true - silence_deprecations: - - import - - color-functions - - global-builtin diff --git a/action.yml b/action.yml index 8649d4cb..007365a7 100644 --- a/action.yml +++ b/action.yml @@ -26,7 +26,6 @@ runs: python-version: "3.13" update-environment: false - # prettier-ignore - name: Run repo-review shell: bash run: > diff --git a/bun.lock b/bun.lock new file mode 100644 index 00000000..1b32acea --- /dev/null +++ b/bun.lock @@ -0,0 +1,15 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "scientific-python-development-guide", + "dependencies": { + "mystmd": "^1.0", + }, + }, + }, + "packages": { + "mystmd": ["mystmd@1.9.1", "", { "bin": { "myst": "dist/myst.cjs" } }, "sha512-ya8u48V7Hn2EtE6DJTEtrWCZEnpIboiN5Ed6lDlvMikiSk8NHUxpocR2okf3BF8vNlC7Auj5s3T/Ulj3taN6bQ=="], + } +} diff --git a/docs/_includes/head.html b/docs/_includes/head.html deleted file mode 100644 index 732e0399..00000000 --- a/docs/_includes/head.html +++ /dev/null @@ -1,74 +0,0 @@ - - - - - {% unless site.plugins contains "jekyll-seo-tag" %} - {{ page.title }} - {{ site.title }} - - {% if page.description %} - - {% endif %} {% endunless %} - - - - {% if false %} - - {% endif %} - - - - - - - {% if site.ga_tracking != nil %} - - - - {% endif %} {% if site.search_enabled != false %} - - {% endif %} - - - - - {% seo %} {% include head_custom.html %} - diff --git a/docs/_includes/head_custom.html b/docs/_includes/head_custom.html deleted file mode 100644 index 96fd84d7..00000000 --- a/docs/_includes/head_custom.html +++ /dev/null @@ -1,20 +0,0 @@ - - -{%- if page.interactive_repo_review %} - - - - - - - -{%- endif %} diff --git a/docs/_includes/interactive_repo_review.html b/docs/_includes/interactive_repo_review.html deleted file mode 100644 index ce4ff0dc..00000000 --- a/docs/_includes/interactive_repo_review.html +++ /dev/null @@ -1,25 +0,0 @@ -
Loading (requires javascript and WebAssembly)...
- - diff --git a/docs/_includes/toc.html b/docs/_includes/toc.html deleted file mode 100644 index fb09be8b..00000000 --- a/docs/_includes/toc.html +++ /dev/null @@ -1,7 +0,0 @@ - -
- Table of contents - - * TOC - {:toc .m-1} -
diff --git a/docs/_includes/pyproject.md b/docs/_partials/pyproject.md similarity index 95% rename from docs/_includes/pyproject.md rename to docs/_partials/pyproject.md index 66bd8bd7..55b8cf65 100644 --- a/docs/_includes/pyproject.md +++ b/docs/_partials/pyproject.md @@ -1,4 +1,4 @@ -## pyproject.toml: project table +# pyproject.toml: project table - -```toml + +```ini [project] name = "package" version = "0.1.0" @@ -50,7 +50,7 @@ Homepage = "https://github.com/org/package" Discussions = "https://github.com/org/package/discussions" Changelog = "https://github.com/org/package/releases" ``` - + In this example, `"package"` is the name of the thing you are working on. You @@ -64,7 +64,7 @@ special, and replaces the old url setting. If you use the above configuration, you need `README.md` and `LICENSE` files, since they are explicitly specified. -### License +## License The license can be done one of two ways. @@ -82,14 +82,14 @@ other tools often did the wrong thing (such as load the entire file into the metadata's free-form one line text field that was intended to describe deviations from the classifier license(s)). -```toml +```ini classifiers = [ "License :: OSI Approved :: BSD License", ] ``` You should not include the `License ::` classifiers if you use the `license` -field {% rr PP007 %}. +field {rr}`PP007`. ### Extras @@ -100,7 +100,7 @@ package or wheel name when installing, like `package[cli,mpl]`. Here is an example of a simple extras: -```toml +```ini [project.optional-dependencies] cli = [ "click", @@ -118,7 +118,7 @@ Self dependencies can be used by using the name of the package, such as If you want to ship an "app" that a user can run from the command line, you need to add a `script` entry point. The form is: -```toml +```ini [project.scripts] cliapp = "package.__main__:main" ``` @@ -138,15 +138,15 @@ forward) and they are more composable. In contrast with extras, dependency-groups are not available when installing your package via PyPI, but they are available for local installation (and can be installed separately from your package); the `dev` group is even installed, by default, when using `uv`'s -high level commands like `uv run` and `uv sync`. {% rr PP0086 %} Here is an +high level commands like `uv run` and `uv sync`. {rr}`PP006` Here is an example: - -```toml + +```ini [dependency-groups] test = [ "pytest >=9", @@ -163,7 +163,7 @@ docs = [ "furo>=2023.08.17", ] ``` - + You can include one dependency group in another. Most tools allow you to install diff --git a/docs/_plugins/details.rb b/docs/_plugins/details.rb deleted file mode 100644 index 25d4d35e..00000000 --- a/docs/_plugins/details.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -module SP - # Sets up a details / summary block - class Details < Liquid::Block - def initialize(tag_name, markup, tokens) - super - @title = markup.strip - end - - def render(context) - <<~RETURN -
- #{@title} - #{super} -
- RETURN - end - end -end - -Liquid::Template.register_tag('details', SP::Details) diff --git a/docs/_plugins/repo_review.rb b/docs/_plugins/repo_review.rb deleted file mode 100644 index 82882f56..00000000 --- a/docs/_plugins/repo_review.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -module SP - # Sets up a repo review badge - class RepoReview < Liquid::Tag - def initialize(tag_name, markup, tokens) - super - @code = markup.strip - raise SyntaxError, 'RepoReview requires a code' unless @code - raise SyntaxError, 'RepoReview code must not contain a space' if @code.include? ' ' - end - - def render(_context) - %(#{@code}) - end - end -end - -Liquid::Template.register_tag('rr', SP::RepoReview) diff --git a/docs/_plugins/tabs.rb b/docs/_plugins/tabs.rb deleted file mode 100644 index b182746d..00000000 --- a/docs/_plugins/tabs.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -module SP - # Sets up a tabs with top switcher bar - class Tabs < Liquid::Block - def initialize(tag_name, markup, tokens) - super - @group = markup.strip.empty? ? 'default' : markup.strip - end - - def render(context) - tab_bar_content = '' - context['tabs'] = [] - context['tab_group'] = @group - result = super - - context['tabs'].each_with_index do |(label, title), index| - res = index.zero? ? ' btn-purple' : '' - tab_bar_content += <<~CONTENT - - CONTENT - end - - <<~RETURN -
- #{tab_bar_content} -
- #{result} - RETURN - end - end - - # Sets up tabs without the top switcher bar - class TabBodies < Liquid::Block - def initialize(tag_name, markup, tokens) - super - @group = markup.strip.empty? ? 'default' : markup.strip - end - - def render(context) - context['tabs'] = [] - context['tab_group'] = @group - super - end - end - - # This is the content of each tab - class Tab < Liquid::Block - def initialize(tag_name, markup, tokens) - super - @label, @title = markup.strip.split(/ /, 2) - end - - def render(context) - raise SyntaxError, "'tab' be in 'tabs' or 'tabbodies'" unless context.key?('tabs') - - group = context['tab_group'] || 'default' - res = context['tabs'].empty? ? '' : ' style="display:none;"' - context['tabs'] << [@label, @title] - <<~RETURN -
- #{super} -
- RETURN - end - end -end - -Liquid::Template.register_tag('tabs', SP::Tabs) -Liquid::Template.register_tag('tabbodies', SP::TabBodies) -Liquid::Template.register_tag('tab', SP::Tab) diff --git a/docs/_sass/color_schemes/skhep.scss b/docs/_sass/color_schemes/skhep.scss deleted file mode 100644 index acf9f5b4..00000000 --- a/docs/_sass/color_schemes/skhep.scss +++ /dev/null @@ -1 +0,0 @@ -$link-color: #7092be; diff --git a/docs/_sass/custom/custom.scss b/docs/_sass/custom/custom.scss deleted file mode 100644 index 05d5b483..00000000 --- a/docs/_sass/custom/custom.scss +++ /dev/null @@ -1,155 +0,0 @@ -@media (min-width: 50rem) { - div.site-header { - height: 264px; - max-height: 264px; - } -} - -details > summary { - font-weight: bold; -} - -div.site-logo { - background-position: center center; -} - -a.site-title { - padding: 0px 6px 0px 6px; -} - -div.noted { - color: #ff6347; - font-weight: bold; - float: right; -} - -a.package { - font-size: larger; - font-weight: bold; -} - -div.project { - display: flex; - flex-direction: row; - flex-wrap: wrap; - justify-content: center; - box-shadow: 0px 5px 5px #88888840; - margin: 10px 0px; - padding: 2px; - border-radius: 5px; - background-color: #f5f6fa; - @media (prefers-color-scheme: dark) { - background-color: #000000; - box-shadow: 0px 0px 5px rgb(106, 143, 188); - } -} - -div.project.affiliated { -} - -div.project.deprecated { - background-color: #ffdad9; - @media (prefers-color-scheme: dark) { - background-color: #501109; - } -} - -div.project .nameorlogo { - flex-basis: 200px; - flex-shrink: 0; - padding: 2px; - text-align: center; -} - -div.project .nameorlogo a img { - vertical-align: middle; -} - -div.project .description { - flex-basis: 150px; - flex-shrink: 0; - flex-grow: 1; - padding: 2px; -} - -div.description .inner p { - display: inline; -} - -pre.highlight { - line-height: 1.2; -} - -details { - padding: 4px; - margin-bottom: 1.5rem; - border-radius: 4px; - box-shadow: - 0 2px 3px rgba(0, 0, 0, 0.12), - 0 3px 10px rgba(0, 0, 0, 0.08); - @media (prefers-color-scheme: dark) { - box-shadow: 0 0 4px rgba(255, 255, 255, 0.4); - background-color: black; - } -} - -details:not([open]) > summary::after { - content: "(expand)"; - float: right; -} -details[open] > summary::after { - content: "(close)"; - float: right; -} - -details[open] { - summary { - margin-bottom: 1em; - } - padding-bottom: 0.5em; -} - -blockquote { - display: block; - margin-block-start: 0; - margin-block-end: 0; - margin-inline-start: 0; - margin-inline-end: 0; - - padding: 4px 1em; - margin-bottom: 1.5rem; - border-radius: 4px; - box-shadow: - 0 2px 3px rgba(0, 0, 0, 0.12), - 0 3px 10px rgba(0, 0, 0, 0.08); - @media (prefers-color-scheme: dark) { - box-shadow: 0 0 4px rgba(255, 255, 255, 0.4); - background-color: black; - } -} - -blockquote > h2 { - font-size: 16px !important; - text-transform: uppercase; - line-height: 1.25; - font-weight: 300; - letter-spacing: 2px; -} - -.rr-btn { - border-radius: 4px; - background-color: #808080; - color: white; - padding: 1px 4px; - font-size: 9pt; - font-family: sans-serif; - display: inline-block; - text-align: center; - line-height: 1; -} -.rr-btn::before { - content: "sp-repo-review"; - font-size: 5pt; - display: block; - text-align: center; -} diff --git a/docs/assets/css/site.css b/docs/assets/css/site.css new file mode 100644 index 00000000..4b366d63 --- /dev/null +++ b/docs/assets/css/site.css @@ -0,0 +1,110 @@ +/* Scientific Python theme base + * Adapted from https://github.com/scientific-python/scientific-python-myst-theme + */ + +body { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +main { + min-height: 0; + flex: 1 0 auto; +} + +.article.content { + min-height: 0; +} + +.footer { + flex-shrink: 0; + background: #013243; + color: white; + padding-left: 2rem; + padding-right: 2rem; + padding-left: 3.5rem; + padding-right: 3.5rem; + + & .outer-grid { + grid-template-columns: 3fr 3fr 4fr; + align-items: center; + margin-bottom: 0rem; + + & li { + list-style: none; + } + } + + @media (max-width: 640px) { + & .outer-grid { + grid-template-columns: 1fr; + justify-items: start; + } + } + + & a, + h1, + h2, + h3, + h4, + h5, + h6 { + color: white; + } + + & h1 { + font-size: 1.25rem; + font-weight: bold; + } +} + +.myst-fm-downloads-button { + display: none; +} + +.myst-home-link { + font-weight: bold; +} + +.bd-main .bd-content .bd-article-container { + max-width: 900px; +} + +h1, +h2, +h3, +h4 { + letter-spacing: -0.01em; +} + +.bd-header { + box-shadow: none; +} + +.bd-sidebar-primary { + border-right: 0; +} + +/* ------------------------------------------------------------------ */ +/* rr-btn: inline repo-review check badge, rendered by {rr} MyST role */ +/* ------------------------------------------------------------------ */ + +.rr-btn { + border-radius: 4px; + background-color: #808080; + color: white; + padding: 1px 4px; + font-size: 9pt; + font-family: sans-serif; + display: inline-block; + text-align: center; + line-height: 1; +} + +.rr-btn::before { + content: "sp-repo-review"; + font-size: 5pt; + display: block; + text-align: center; +} diff --git a/docs/assets/js/tabs.js b/docs/assets/js/tabs.js deleted file mode 100644 index 41b87fdb..00000000 --- a/docs/assets/js/tabs.js +++ /dev/null @@ -1,36 +0,0 @@ -function openTab(tabName, groupName = "default") { - var tab = document.getElementsByClassName("skhep-tab"); - for (const t of tab) { - if (t.classList.contains(`${groupName}-${tabName}-tab`)) { - t.style.display = "block"; - } else if ( - Array.from(t.classList).some( - (c) => c.startsWith(`${groupName}-`) && c.endsWith("-tab"), - ) - ) { - t.style.display = "none"; - } - } - var btn = document.getElementsByClassName("skhep-bar-item"); - for (const b of btn) { - if (b.classList.contains(`${groupName}-${tabName}-btn`)) { - b.classList.add("btn-purple"); - } else if ( - Array.from(b.classList).some( - (c) => c.startsWith(`${groupName}-`) && c.endsWith("-btn"), - ) - ) { - b.classList.remove("btn-purple"); - } - } -} -function ready() { - const urlParams = new URLSearchParams(window.location.search); - const tabs = urlParams.getAll("tabs"); - - for (const tab of tabs) { - openTab(tab); - } -} - -document.addEventListener("DOMContentLoaded", ready, false); diff --git a/docs/config/scientific-python.yml b/docs/config/scientific-python.yml new file mode 100644 index 00000000..00bf1391 --- /dev/null +++ b/docs/config/scientific-python.yml @@ -0,0 +1,9 @@ +version: 1 + +site: + template: book-theme + options: + hide_toc: false + hide_footer_links: true + folders: true + hide_myst_branding: true diff --git a/docs/myst.yml b/docs/myst.yml new file mode 100644 index 00000000..2774e1be --- /dev/null +++ b/docs/myst.yml @@ -0,0 +1,60 @@ +version: 1 +extends: config/scientific-python.yml + +project: + title: Scientific Python Development Guide + github: https://github.com/scientific-python/cookie + plugins: + - rr-role.mjs + + toc: + - file: pages/index.md + - file: pages/tutorials/index.md + children: + - file: pages/tutorials/dev-environment.md + - file: pages/tutorials/module.md + - file: pages/tutorials/packaging.md + - file: pages/tutorials/test.md + - file: pages/tutorials/docs.md + - file: pages/guides/index.md + children: + - file: pages/guides/pytest.md + - file: pages/guides/coverage.md + - file: pages/guides/docs.md + - file: pages/guides/packaging_simple.md + - file: pages/guides/packaging_compiled.md + - file: pages/guides/packaging_classic.md + - file: pages/guides/style.md + - file: pages/guides/mypy.md + - file: pages/guides/gha_basic.md + - file: pages/guides/gha_pure.md + - file: pages/guides/gha_wheels.md + - file: pages/guides/tasks.md + - file: pages/principles/index.md + children: + - file: pages/principles/process.md + - file: pages/principles/design.md + - file: pages/principles/testing.md + - file: pages/patterns/index.md + children: + - file: pages/patterns/exports.md + - file: pages/patterns/backports.md + - file: pages/patterns/data_files.md + - file: pages/guides/repo_review.md + +site: + template: book-theme + nav: + - title: Scientific Python + url: https://scientific-python.org + - title: Learn + url: https://learn.scientific-python.org + actions: + - title: src + url: https://github.com/scientific-python/cookie + options: + favicon: assets/favicon.ico + logo: assets/images/logo.svg + logo_text: Scientific Python Development Guide + style: assets/css/site.css + hide_footer_links: false diff --git a/docs/pages/guides/coverage.md b/docs/pages/guides/coverage.md index d1469897..18ca0be8 100644 --- a/docs/pages/guides/coverage.md +++ b/docs/pages/guides/coverage.md @@ -1,14 +1,8 @@ --- -layout: page title: "Code coverage" -permalink: /guides/coverage/ -nav_order: 3 -parent: Topical Guides --- -{% include toc.html %} - -# 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 @@ -27,20 +21,16 @@ Tools and libraries used to calculate, read, and visualize coverage reports: - `GitHub Actions`: allows users to automatically upload coverage reports to `Codecov` -{: .note-title } - -> Are there any alternatives? -> -> Coveralls is an alternative coverage platform, but we recommend using Codecov -> because of its ease of use and integration with GitHub Actions. - -{: .highlight-title } +:::{note} Are there any alternatives? +Coveralls is an alternative coverage platform, but we recommend using Codecov +because of its ease of use and integration with GitHub Actions. +::: -> Should increasing the coverage value be my top priority? -> -> A low coverage percentage will definitely motivate you to add more tests, but -> 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. +:::{tip} Should increasing the coverage value be my top priority? +A low coverage percentage will definitely motivate you to add more tests, but +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 @@ -53,8 +43,9 @@ directly is hidden away from normal use, so we recommend that, but will show both methods below. If you are not running `pytest`, but instead are running an example or a script, you have to use `coverage` directly. -{% tabs %} {% tab cov coverage %} - +::::{tab-set} +:::{tab-item} coverage +:sync: coverage Make sure you install `coverage[toml]`. `coverage` has several commands; the most important one is `coverage run`. This @@ -75,9 +66,9 @@ coverage report This looks for a `.coverage` file and displays the result. There are many output formats for reports. - -{% endtab %} {% tab pycov pytest-cov %} - +::: +:::{tab-item} pytest-cov +:sync: pytest-cov Make sure you install `pytest-cov`. `pytest` allows users to pass the `--cov` option to automatically invoke @@ -98,16 +89,16 @@ See the [docs](https://pytest-cov.readthedocs.io/en/latest/) for more options. Coverage pytest arguments can be placed in your pytest configuration file or in your task runner. It also will (mostly) respect the coverage configuration, shown below. +::: +:::: -{% endtab %} {% endtabs %} - -### Configuring coverage +#### Configuring coverage There is a configuration section in `pyproject.toml` for coverage. Here are some common options [(see the docs for more)](https://coverage.readthedocs.io/en/latest/config.html): -```toml +```ini [tool.coverage] run.core = "sysmon" run.disable_warnings = ["no-sysmon"] @@ -135,7 +126,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. @@ -144,7 +135,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, either with `pytest-xdist`, you can set `run.parallel` to `true`, which will add a unique suffix to the coverage file(s) @@ -158,42 +149,39 @@ manually produce multiple files from task runner jobs. Here's an example nox job: -{% tabs %} {% tab cov coverage %} +::::{tab-set} +:::{tab-item} coverage +:sync: coverage ```python -@nox.session(python=ALL_PYTHONS) -def tests(session: nox.Session) -> None: - coverage_file = f".coverage.{sys.platform}.{session.python}" - session.install("-e.", "--group=cov") - session.run( - "coverage", - "run", - "-m", - "pytest", - *session.posargs, - env={"COVERAGE_FILE": coverage_file}, - ) +session.run( + "coverage", + "run", + "-m", + "pytest", + *session.posargs, + env={"COVERAGE_FILE": coverage_file}, +) ``` -{% endtab %} {% tab pycov pytest-cov %} +::: +:::{tab-item} pytest-cov +:sync: pytest-cov ```python -@nox.session(python=ALL_PYTHONS) -def tests(session: nox.Session) -> None: - coverage_file = f".coverage.{sys.platform}.{session.python}" - session.install("-e.", "--group=cov") - session.run( - "pytest", - "--cov=", - "--cov-config=pyproject.toml", - *session.posargs, - env={"COVERAGE_FILE": coverage_file}, - ) +session.run( + "pytest", + "--cov=", + "--cov-config=pyproject.toml", + *session.posargs, + env={"COVERAGE_FILE": coverage_file}, +) ``` -{% endtab %} {% endtabs %} +::: +:::: -#### 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 @@ -213,7 +201,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 @@ -225,8 +213,6 @@ uploading coverage reports easy for users. A minimal working example for uploading coverage reports through your workflow, which should be more than enough for a simple testing suite, can be written as follows: -{% raw %} - ```yaml - name: Upload coverage report uses: codecov/codecov-action@v6 @@ -234,14 +220,12 @@ enough for a simple testing suite, can be written as follows: token: ${{ secrets.CODECOV_TOKEN }} ``` -{% endraw %} - The lines above should be added after the step that runs your tests with the `--cov` option. See the [docs](https://github.com/codecov/codecov-action#usage) 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 @@ -280,5 +264,3 @@ loss of coverage to fail. See the TODO --> [codecov/codecov-action]: https://github.com/codecov/codecov-action - - diff --git a/docs/pages/guides/docs.md b/docs/pages/guides/docs.md index bceb5281..99184e1a 100644 --- a/docs/pages/guides/docs.md +++ b/docs/pages/guides/docs.md @@ -1,61 +1,51 @@ --- -layout: page title: Writing documentation -permalink: /guides/docs/ -nav_order: 4 -parent: Topical Guides --- -{% include toc.html %} - -# 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, 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 } - -> 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: -> -> - [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), -> [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). - -{: .warning-title } - -> The Future of MkDocs -> -> The creators of `mkdocs-material` and `mkdocstrings` have come together to -> create a new documentation package called -> [Zensical](https://zensical.org/about/). The framework is still in alpha -> development, but aims to simplify the documentation process, be blazing fast, -> and move away from the limitations of MkDocs. This also means MkDocs's future -> is uncertain, and mkdocs-material will be minimally maintained until -> late 2026. - -## What to include +:::{note} 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: + +- [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), + [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). +::: + +:::{warning} The Future of MkDocs +The creators of `mkdocs-material` and `mkdocstrings` have come together to +create a new documentation package called +[Zensical](https://zensical.org/about/). The framework is still in alpha +development, but aims to simplify the documentation process, be blazing fast, +and move away from the limitations of MkDocs. This also means MkDocs's future +is uncertain, and mkdocs-material will be minimally maintained until +late 2026. +::: + +### What to include Ideally, software documentation should include: @@ -67,12 +57,10 @@ Ideally, software documentation should include: - **Explanations** to convey deeper understanding of why and how the software operates the way it does. -{: .note-title } - -> The Diátaxis framework -> -> This overall framework has a name, [Diátaxis][], and you can read more about -> it if you are interested. +:::{note} The Diátaxis framework +This overall framework has a name, [Diátaxis][], and you can read more about +it if you are interested. +::: -## Hand-written docs +### Hand-written docs Create `docs/` directory within your project (next to `src/`). From here, Sphinx and MkDocs diverge. -{% tabs %}{% tab sphinx Sphinx %} +::::{tab-set} +:::{tab-item} Sphinx +:sync: sphinx -### pyproject.toml additions +#### pyproject.toml additions Setting a `docs` dependency group looks like this: -```toml +```ini [dependency-groups] docs = [ "furo", @@ -117,13 +107,13 @@ 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 - + ```python from __future__ import annotations @@ -190,7 +180,7 @@ nitpick_ignore = [ always_document_param_types = True ``` - + We start by setting some configuration values, but most notably we are getting @@ -237,7 +227,7 @@ docstrings, even if the parameter isn't documented yet. Feel free to check [sphinx-autodoc-typehints](https://github.com/tox-dev/sphinx-autodoc-typehints) for more options. -### index.md +#### index.md Your `index.md` file can start out like this: @@ -245,7 +235,7 @@ Your `index.md` file can start out like this: with code_fence("md", width=4): print(docs_index_md) ]]] --> - + ````md # package @@ -265,7 +255,7 @@ with code_fence("md", width=4): - {ref}`modindex` - {ref}`search` ```` - + You can put your project name in as the title. The `toctree` directive houses @@ -277,9 +267,9 @@ docs, so you can add a expression to your README (`` above) to mark where you want the docs portion to start. You can add the standard indices and tables at the end. - -{% endtab %} {% tab mkdocs MkDocs %} - +::: +:::{tab-item} MkDocs +:sync: 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/) @@ -289,7 +279,7 @@ 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 +```ini [dependency-groups] docs = [ "markdown>=3.9", @@ -321,7 +311,7 @@ Here's the whole file for completeness. We'll break it into sections underneath. with code_fence("yaml"): print(mkdocs_conf_yaml) ]]] --> - + ```yaml site_name: package site_url: https://package.readthedocs.io/ @@ -376,7 +366,7 @@ nav: - Home: index.md - Python API: api.md ``` - + First, the basic site metadata contains authors, repository details, URLs, etc: @@ -478,21 +468,23 @@ nav: - Python API: api.md ``` -{% endtab %} {% endtabs %} +::: +:::: -### .readthedocs.yaml +#### .readthedocs.yaml In order to use to build, host, and preview your -documentation, you must have a `.readthedocs.yaml` file {% rr RTD100 %} like +documentation, you must have a `.readthedocs.yaml` file {rr}`RTD100` like this: -{% tabs %} {% tab sphinx Sphinx %} - +::::{tab-set} +:::{tab-item} Sphinx +:sync: sphinx - + ```yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details @@ -514,16 +506,16 @@ python: groups: - docs ``` - + - -{% endtab %} {% tab mkdocs MkDocs %} - +::: +:::{tab-item} MkDocs +:sync: mkdocs - + ```yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details @@ -542,31 +534,32 @@ build: - 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 %}. +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 %} and 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). 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 +#### noxfile.py additions Add a session to your `noxfile.py` to generate docs: -{% tabs %} {% tab sphinx Sphinx %} - +::::{tab-set} +:::{tab-item} Sphinx +:sync: sphinx - + ```python @nox.session(reuse_venv=True, default=False) def docs(session: nox.Session) -> None: @@ -599,7 +592,7 @@ def docs(session: nox.Session) -> None: else: session.run("sphinx-build", "--keep-going", *shared_args) ``` - + This is a more complex Nox job just because it's taking some options (the @@ -614,14 +607,14 @@ links, and doesn't really produce output. Finally, we collect some useful args, and run either the autobuild (for `--serve`) or regular build. We could have just added `python -m http.server` pointing at the built documentation, but autobuild will rebuild if you change a file while serving. - -{% endtab %} {% tab mkdocs MkDocs %} - +::: +:::{tab-item} MkDocs +:sync: mkdocs - + ```python @nox.session(reuse_venv=True, default=False) def docs(session: nox.Session) -> None: @@ -637,7 +630,7 @@ def docs(session: nox.Session) -> None: else: session.run("mkdocs", "build", "--clean", *session.posargs) ``` - + This Nox job will invoke MkDocs to serve a live copy of your documentation under @@ -646,25 +639,26 @@ 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 %} +### API docs +::::{tab-set} +:::{tab-item} Sphinx +:sync: 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. -### noxfile.py additions +#### noxfile.py additions - + ```python @nox.session(default=False) def build_api_docs(session: nox.Session) -> None: @@ -683,13 +677,13 @@ def build_api_docs(session: nox.Session) -> None: "src/", ) ``` - + And you'll need this added to your `docs/index.md`: ````md -```{toctree} +```text {toctree} :maxdepth: 2 :hidden: :caption: API @@ -699,9 +693,9 @@ api/ ```` Note that your docstrings are still parsed as reStructuredText. - -{% endtab %} {% tab mkdocs MkDocs %} - +::: +:::{tab-item} MkDocs +:sync: 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 @@ -724,13 +718,14 @@ 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 %} +### Notebooks in docs +::::{tab-set} +:::{tab-item} Sphinx +:sync: 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 @@ -757,9 +752,9 @@ 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 %} - +::: +:::{tab-item} MkDocs +:sync: 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 @@ -784,16 +779,11 @@ and notebooks. If you have a directory of example python files to run, consider 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/ [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/ [`sphinx.ext.napoleon`]: https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html - - - diff --git a/docs/pages/guides/gha_basic.md b/docs/pages/guides/gha_basic.md index a161cd96..31524989 100644 --- a/docs/pages/guides/gha_basic.md +++ b/docs/pages/guides/gha_basic.md @@ -1,17 +1,11 @@ --- -layout: page title: "GHA: GitHub Actions intro" -permalink: /guides/gha-basic/ -nav_order: 10 -parent: Topical Guides -custom_title: GitHub Actions introduction +short_title: GitHub Actions introduction --- -{% include toc.html %} +## GitHub Actions: Intro -# GitHub Actions: Intro - -{% rr GH100 %} The recommended CI for scientific Python projects is GitHub +{rr}`GH100` The recommended CI for scientific Python projects is GitHub Actions (GHA), although its predecessor Azure is also in heavy usage, and other popular services (Travis, Appveyor, and Circle CI) may be found in a few packages. GHA is preferred due to the flexible, extensible design and the tight @@ -31,7 +25,7 @@ with render_cookie() as package: ]]] --> -## Header +### Header Your main CI workflow file should begin something like this: @@ -47,20 +41,20 @@ on: jobs: ``` -This gives the workflow a nice name {% rr GH101 %}, and defines the conditions +This gives the workflow a nice name {rr}`GH101`, and defines the conditions under which it runs. This will run on all pull requests, or pushes to main. If 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 world usage and supports the same configuration and hooks. -{% tabs runner %} {% tab prek Prek %} - +::::{tab-set} +:::{tab-item} Prek Prek can run using the official action: ```yaml @@ -72,8 +66,8 @@ lint: - uses: j178/prek-action@v2 ``` -{% endtab %} {% tab pre-commit Pre-commit %} - +::: +:::{tab-item} Pre-commit Pre-commit can run using the official action: ```yaml @@ -88,7 +82,8 @@ lint: - uses: pre-commit/action@v3.0.1 ``` -{% endtab %} {% endtabs %} +::: +:::: If you do use [pre-commit.ci](https://pre-commit.ci), but you need this job to run a manual check, like check-manifest, then you can keep it but just use @@ -96,7 +91,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 @@ -105,8 +100,6 @@ adjust the Python versions to suit your taste; you can also test on different OS's if you'd like by adding them to the matrix and inputting them into `runs-on`. -{% raw %} - ```yaml tests: runs-on: ubuntu-latest @@ -136,8 +129,6 @@ tests: run: uv run pytest ``` -{% endraw %} - A few things to note from above: The matrix should contain the versions you are interested in. You can also test @@ -167,9 +158,9 @@ 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 +{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 up to date. GitHub provided a way to do this with dependabot. Just add the following file as `.github/dependabot.yml`: @@ -192,16 +183,16 @@ This will check to see if there are updates to the action weekly, and will make a PR if there are updates, including the changelog and commit summary in the PR. If you select a name like `v1`, this should only look for updates of the same form (since April 2022) - there is no need to restrict updates for "moving tag" -updates anymore {% rr GH211 %}. You can also use SHA's and dependabot will -respect that too. And `groups` will combine actions updates {% rr GH212 %}, +updates anymore {rr}`GH211`. You can also use SHA's and dependabot will +respect that too. And `groups` will combine actions updates {rr}`GH212`, which is both cleaner and sometimes required for dependent actions, like `upload-artifact`/`download-artifact`. 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`: @@ -213,7 +204,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: @@ -224,7 +215,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: @@ -234,16 +225,12 @@ actions have outputs, and bash actions can manually write to output: run: echo "something=true" >> $GITHUB_OUTPUT ``` -{% raw %} - You can now refer to this step in a later step with `${{ steps.someid.something }}`. You also can get it from another job by using `${{ needs..outputs.something }}`. The `toJson()` function is useful for inputting JSON - you can even generate matrices dynamically this way! -{% endraw %} - -### Pretty output +#### Pretty output You can write GitHub flavored markdown to `$GITHUB_STEP_SUMMARY`, and it will be shown on the summary page. @@ -260,7 +247,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: @@ -338,31 +325,27 @@ 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 +{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: -{% raw %} - ```yaml concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true ``` -{% endraw %} - Anything with a matching group name will count in the same group - the ref is 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 @@ -373,8 +356,6 @@ pass). As an example, if you had `lint` and `checks` jobs, use this: -{% raw %} - ```yaml pass: if: always() @@ -386,8 +367,6 @@ pass: jobs: ${{ toJSON(needs) }} ``` -{% endraw %} - We want the job to always run, so we set `if: always()`. Otherwise, it might be skipped if any job it depends on is skipped, and skipped jobs count as "passing" to GitHub's automerge (yikes!). The important part of the job is the `needs:` @@ -403,7 +382,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) @@ -446,8 +425,6 @@ ideally shouldn't change the user's environment; suddenly changing the active Python version might come as a surprise. You can do that, though, using `update-environment: false` with `setup-python` and `pipx`: -{% raw %} - ```yaml - uses: actions/setup-python@v6 id: python @@ -462,23 +439,21 @@ Python version might come as a surprise. You can do that, though, using github.action_path }}' ${{ inputs.some-input }} ``` -{% endraw %} - You use the `python-path` output from `setup-python` to get the Python you activated. You use `github.action_path` to get the path to the checked-out action. -{: .highlight } +:::{tip} +Examples of custom composite actions include: -> Examples of custom composite actions include: -> -> - [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel/blob/main/action.yml) -> - [wntrblm/nox](https://github.com/wntrblm/nox/blob/main/action.yml) -> - [scientific-python/repo-review](https://github.com/scientific-python/repo-review/blob/main/action.yml) -> - [scientific-python/cookie](https://github.com/scientific-python/cookie/blob/main/action.yml) -> (This repo) +- [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel/blob/main/action.yml) +- [wntrblm/nox](https://github.com/wntrblm/nox/blob/main/action.yml) +- [scientific-python/repo-review](https://github.com/scientific-python/repo-review/blob/main/action.yml) +- [scientific-python/cookie](https://github.com/scientific-python/cookie/blob/main/action.yml) + (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 @@ -495,7 +470,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 @@ -515,8 +490,6 @@ on: Otherwise, they look like normal workflows. Then you need another reusable workflow file to decide when to run a specific situation. -{% raw %} - ```yaml # reusable-change-detection.yml on: @@ -527,8 +500,6 @@ on: # More here if you have more situations to detect ``` -{% endraw %} - You start by specifying outputs when running this. You'll want one output per situation you want to detect. The value will be output from our `change-detection` job below, and defaults to "false" if we don't output @@ -536,8 +507,6 @@ anything. Now, we need our job: -{% raw %} - ```yaml jobs: change-detection: @@ -571,8 +540,6 @@ jobs: # Add 2 more steps per situation you have to detect ``` -{% endraw %} - This has a bit of boilerplate (mostly around passing variables around), but what it's doing is fairly simple. Instead of stepping through it, let's look at what it's trying to do. First, you need to find a list of all changed files in the @@ -588,21 +555,18 @@ Everything else in the job is about getting the output from the step `changed-tests-files` to `tests-changes`, then from there into the reusable workflow output as `run-tests`. -{: .note } - +:::{note} Someone probably could write an action (maybe even an composite action using either `gh` or `shell: python`) that could directly report changes true/false instead of a file list, saving the two step process and greatly simplifying this. - +::: If you have more situations, you just repeat these two steps with different `id`s and inputs. Finally, you write the overarching CI workflow that combines the reusable workflows, something like `ci.yml`: -{% raw %} - ```yaml on: workflow_dispatch: @@ -648,17 +612,15 @@ If you have more situations, add another `${{ ... }}` above, after the first one, and add them to the needs list. This is really just injecting "tests" only if the "tests" job is being skipped into `allowed-skips`. -{% endraw %} - -{: .highlight } +:::{tip} +Some examples of repos using this method are: -> Some examples of repos using this method are: -> -> - [pypa/build](https://github.com/pypa/build/tree/main/.github/workflows) -> - [scientific-python/cookie](https://github.com/scientific-python/cookie/tree/main/.github/workflows) -> (this repo) +- [pypa/build](https://github.com/pypa/build/tree/main/.github/workflows) +- [scientific-python/cookie](https://github.com/scientific-python/cookie/tree/main/.github/workflows) + (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/) @@ -679,7 +641,7 @@ permissions: id-token: write ``` -{% rr GH103 %} You probably only want one deployment at a time, so you can use: +{rr}`GH103` You probably only want one deployment at a time, so you can use: ```yaml concurrency: @@ -696,16 +658,12 @@ configure Pages. uses: actions/configure-pages@v6 ``` -{% raw %} - Notice this action sets an `id:`; this will allow you to use the outputs from this action later; specifically, may want to use `${{ steps.pages.outputs.base_path }}` when building (you can also get `origin`, `base_url`, or `host` - see the action [config](https://github.com/actions/configure-pages/blob/main/action.yml)). -{% endraw %} - ```yaml - name: Upload artifact uses: actions/upload-pages-artifact@v5 @@ -718,8 +676,6 @@ Finally, you'll need to deploy the artifact (named `github-pages`) to Pages. You can make this a custom job with `needs:` pointing at your previous job (in this example, the previous job is called `build`): -{% raw %} - ```yaml deploy: environment: @@ -733,22 +689,20 @@ deploy: uses: actions/deploy-pages@v5 ``` -{% endraw %} - The deploy-pages job gives a `page_url`, which is the same as `base_url` on the configure step, and can be set in the `environment`. If you want to do everything in one job, you only need one of these. -{: .highlight } +:::{tip} +See the +[official starter workflows](https://github.com/actions/starter-workflows/tree/main/pages) +for examples. Some other examples include: -> See the -> [official starter workflows](https://github.com/actions/starter-workflows/tree/main/pages) -> for examples. Some other examples include: -> -> - [CLIUtils.github.io/CLI11](https://github.com/CLIUtils/CLI11/blob/main/.github/workflows/docs.yml) -> - [iris-hep.org](https://github.com/iris-hep/iris-hep.github.io/blob/master/.github/workflows/deploy.yml) +- [CLIUtils.github.io/CLI11](https://github.com/CLIUtils/CLI11/blob/main/.github/workflows/docs.yml) +- [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 @@ -759,7 +713,7 @@ pre-commit-ci PRs for you: with code_fence("yaml"): print(github_release_yaml) ]]] --> - + ```yaml changelog: exclude: @@ -767,15 +721,9 @@ changelog: - dependabot[bot] - pre-commit-ci[bot] ``` - + - - [pre-commit]: https://pre-commit.com [prek]: https://github.com/j178/prek [gh-changelog]: https://docs.github.com/en/repositories/releasing-projects - - - - diff --git a/docs/pages/guides/gha_pure.md b/docs/pages/guides/gha_pure.md index 37d72572..d80a857d 100644 --- a/docs/pages/guides/gha_pure.md +++ b/docs/pages/guides/gha_pure.md @@ -1,15 +1,9 @@ --- -layout: page title: "GHA: Pure Python wheels" -permalink: /guides/gha-pure/ -nav_order: 11 -parent: Topical Guides -custom_title: GitHub Actions for pure Python wheels +short_title: GitHub Actions for pure Python wheels --- -{% include toc.html %} - -# 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 @@ -17,25 +11,25 @@ procedure to make a "built" wheel is simple. At the end of this page, there is a recipe that can often be used exactly for pure Python wheels (if the previous recommendations were followed). -{: .note } +:::{note} +Why make a wheel when there is nothing to compile? There are a multitude of +reasons that a wheel is better than only providing an sdist: -> Why make a wheel when there is nothing to compile? There are a multitude of -> reasons that a wheel is better than only providing an sdist: -> -> - Wheels do not run `setup.py`, but simply install files into locations -> - Lower install requirements - users don't need your setup tools -> - Faster installs -> - Safer installs - no arbitrary code execution -> - Highly consistent installs -> - Wheels pre-compile bytecode when they install -> - Initial import is not slower than subsequent import -> - Less chance of a permission issue -> - You can look in the `.whl` (it's a `.zip`, really) and see where everything -> is going to go +- Wheels do not run `setup.py`, but simply install files into locations + - Lower install requirements - users don't need your setup tools + - Faster installs + - Safer installs - no arbitrary code execution + - Highly consistent installs +- Wheels pre-compile bytecode when they install + - Initial import is not slower than subsequent import + - Less chance of a permission issue +- You can look in the `.whl` (it's a `.zip`, really) and see where everything + is going to go +::: -[on the next page]: {% link pages/guides/gha_wheels.md %} +[on the next page]: pages/guides/gha_wheels -## Job setup +### Job setup ```yaml name: CD @@ -62,9 +56,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 - -{% raw %} +### Distribution: Pure Python wheels ```yaml dist: @@ -86,8 +78,6 @@ dist: run: pipx run twine check dist/* ``` -{% endraw %} - We use [PyPA-Build](https://pypa-build.readthedocs.io/en/latest/), a new build tool designed to make building wheels and SDists easy. It run a [PEP 517][] backend and can get [PEP 518][] requirements even for making SDists. @@ -105,38 +95,36 @@ GitHub Actions (in fact, they use it to setup other applications). We upload the artifact just to make it available via the GitHub PR/Checks API. You can download a file to test locally if you want without making a release. -{: .warning } - -> As of `upload-artifact@v4`, the artifact name must be unique. Extending an -> existing artifact is no longer supported. +:::{warning} +As of `upload-artifact@v4`, the artifact name must be unique. Extending an +existing artifact is no longer supported. +::: We also add an optional check using twine for the metadata (it will be tested later in the upload action for the release job, as well). -{: .highlight-title } - -> All-in-one action -> -> There is an -> [all-in-one action](https://github.com/hynek/build-and-inspect-python-package) -> that does all the work for you for a pure Python package, including extra -> pre-upload checks & nice GitHub summaries. -> -> ```yaml -> steps: -> - uses: actions/checkout@v6 -> - uses: hynek/build-and-inspect-python-package@v2 -> ``` -> -> The artifact it produces is named `Packages`, so that's what you need to use -> later to publish. This will be used instead of the manual steps below. +:::{tip} All-in-one action +There is an +[all-in-one action](https://github.com/hynek/build-and-inspect-python-package) +that does all the work for you for a pure Python package, including extra +pre-upload checks & nice GitHub summaries. -And then, you need a release job. Trusted Publishing is more secure and -recommended {% rr GH105 %}: +```yaml +steps: + - uses: actions/checkout@v6 + - uses: hynek/build-and-inspect-python-package@v2 +``` -{% tabs %} {% tab oidc Trusted Publishing (recommended) %} +The artifact it produces is named `Packages`, so that's what you need to use +later to publish. This will be used instead of the manual steps below. +::: -{% raw %} +And then, you need a release job. Trusted Publishing is more secure and +recommended {rr}`GH105`: + +::::{tab-set} +:::{tab-item} Trusted Publishing (recommended) +:sync: trusted-publishing ```yaml publish: @@ -162,8 +150,6 @@ publish: - uses: pypa/gh-action-pypi-publish@release/v1 ``` -{% endraw %} - When you make a GitHub release in the web UI, we publish to PyPI. You'll just need to tell PyPI which org, repo, workflow, and set the `pypi` environment to allow pushes from GitHub. If it's the first time you've published a package, go @@ -172,10 +158,9 @@ accept your initial package publish. We are also generating artifact attestations, which can allow users to verify that the artifacts were built on your actions. - -{% endtab %} {% tab token Token %} - -{% raw %} +::: +:::{tab-item} Token +:sync: token ```yaml publish: @@ -193,25 +178,22 @@ publish: password: ${{ secrets.pypi_password }} ``` -{% endraw %} - If you cannot use Trusted Publishing, this publishes to PyPI with a token. You'll need to go to PyPI, generate a token for your user, and put it into `pypi_password` on your repo's secrets page. Once you have a project, you should delete your user-scoped token and generate a new project-scoped token. +::: +:::: -{% endtab %} {% endtabs %} - -{% details Complete recipe %} - +:::::{dropdown} Complete recipe This can be used on almost any package with a standard `.github/workflows/cd.yml` recipe. This works because `pyproject.toml` describes exactly how to build your package, hence all packages build exactly via the same interface: -{% tabbodies %} {% tab oidc Trusted Publishing (recommended) %} - -{% raw %} +::::{tab-set} +:::{tab-item} Trusted Publishing (recommended) +:sync: trusted-publishing ```yaml name: CD @@ -260,11 +242,9 @@ jobs: - uses: pypa/gh-action-pypi-publish@release/v1 ``` -{% endraw %} - -{% endtab %} {% tab token Token %} - -{% raw %} +::: +:::{tab-item} Token +:sync: token ```yaml name: CD @@ -305,25 +285,15 @@ jobs: password: ${{ secrets.pypi_password }} ``` -{% endraw %} - If you cannot use Trusted Publishing, this publishes to PyPI with a token. You'll need to go to PyPI, generate a token for your user, and put it into `pypi_password` on your repo's secrets page. Once you have a project, you should delete your user-scoped token and generate a new project-scoped token. - -{% endtab %} {% endtabbodies %} - -{% enddetails %} - - +::: +:::: +::::: [pep 517]: https://www.python.org/dev/peps/pep-0517/ [pep 518]: https://www.python.org/dev/peps/pep-0518/ [pypi trusted publisher docs]: https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/ [`workflow_dispatch`]: https://github.blog/changelog/2020-07-06-github-actions-manual-triggers-with-workflow_dispatch/ - - - - - diff --git a/docs/pages/guides/gha_wheels.md b/docs/pages/guides/gha_wheels.md index 6d4e6582..e3492cd7 100644 --- a/docs/pages/guides/gha_wheels.md +++ b/docs/pages/guides/gha_wheels.md @@ -1,21 +1,15 @@ --- -layout: page title: "GHA: Binary wheels" -permalink: /guides/gha-wheels/ -nav_order: 12 -parent: Topical Guides -custom_title: GitHub Actions for Binary Wheels +short_title: GitHub Actions for Binary Wheels --- -{% include toc.html %} - -# 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 @@ -43,18 +37,15 @@ wheels before making a release; you can download them from the "artifacts". You can even define variables that you can set in the GUI and access in the CI! 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: -{: .no_toc } - +#### 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 Linux and Windows): -```toml +```ini [tool.cibuildwheel] test-groups = ["test"] test-command = "pytest {project}/tests" @@ -69,7 +60,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: @@ -98,12 +89,10 @@ 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: -{% raw %} - ```yaml build_wheels: name: Wheel on ${{ matrix.os }} @@ -136,8 +125,6 @@ build_wheels: path: wheelhouse/*.whl ``` -{% endraw %} - There are several things to note here. First, one of the reasons this works is because you followed the suggestions in the previous sections, and your package builds nicely into a wheel without strange customizations (if you _really_ need @@ -166,13 +153,13 @@ 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 - -Trusted Publishing is more secure and recommended {% rr GH105 %}: +### Publishing -{% tabs %} {% tab oidc Trusted Publishing (recommended) %} +Trusted Publishing is more secure and recommended {rr}`GH105`: -{% raw %} +::::{tab-set} +:::{tab-item} Trusted Publishing (recommended) +:sync: trusted-publishing ```yaml upload_all: @@ -200,8 +187,6 @@ upload_all: - uses: pypa/gh-action-pypi-publish@release/v1 ``` -{% endraw %} - When you make a GitHub release in the web UI, we publish to PyPI. You'll just need to tell PyPI which org, repo, workflow, and set the `pypi` environment to allow pushes from GitHub. If it's the first time you've published a package, go @@ -210,10 +195,9 @@ accept your initial package publish. We are also generating artifact attestations, which can allow users to verify that the artifacts were built on your actions. - -{% endtab %} {% tab token Token %} - -{% raw %} +::: +:::{tab-item} Token +:sync: token ```yaml upload_all: @@ -232,14 +216,12 @@ upload_all: password: ${{ secrets.pypi_password }} ``` -{% endraw %} - If you cannot use Trusted Publishing, this publishes to PyPI with a token. You'll need to go to PyPI, generate a token for your user, and put it into `pypi_password` on your repo's secrets page. Once you have a project, you should delete your user-scoped token and generate a new project-scoped token. - -{% endtab %} {% endtabs %} +::: +:::: If you have multiple jobs, you will want to collect your artifacts from above. If you only have one job, you can combine this into a single job like we did for @@ -248,25 +230,17 @@ multiple places, you can set `skip_existing` (but generally it's better to not try to upload the same file from two places - you can trick Travis into avoiding the sdist, for example). -{: .note-title } - -> Other architectures -> -> GitHub Actions supports ARM on Linux and Windows as well. On Travis, -> `cibuildwheel` even has the ability to create rarer architectures like PowerPC -> builds natively. IBM Z builds are also available but in beta. However, due to -> Travis CI's recent dramatic reduction on open source support, emulating these -> architectures on GHA or Azure is probably better. Maybe look into Cirrus CI, -> which has some harder-to-find architectures. - - +:::{note} Other architectures +GitHub Actions supports ARM on Linux and Windows as well. On Travis, +`cibuildwheel` even has the ability to create rarer architectures like PowerPC +builds natively. IBM Z builds are also available but in beta. However, due to +Travis CI's recent dramatic reduction on open source support, emulating these +architectures on GHA or Azure is probably better. Maybe look into Cirrus CI, +which has some harder-to-find architectures. +::: [`cibw_before_build`]: https://cibuildwheel.readthedocs.io/en/stable/options/#before-build [`cibw_environment`]: https://cibuildwheel.readthedocs.io/en/stable/options/#environment [cibw custom]: https://cibuildwheel.readthedocs.io/en/stable/options/#build-skip [cibuildwheel]: https://cibuildwheel.readthedocs.io/en/stable/ [pypi trusted publisher docs]: https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/ - - - - diff --git a/docs/pages/guides/index.md b/docs/pages/guides/index.md index 33e935ff..c79bc775 100644 --- a/docs/pages/guides/index.md +++ b/docs/pages/guides/index.md @@ -1,12 +1,8 @@ --- -layout: page title: Topical Guides -permalink: /guides/ -nav_order: 2 -has_children: true --- -# 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. @@ -28,40 +24,33 @@ and one for [compiled extensions][gha_wheels]. You can read about setting up good tests on the [pytest page][pytest], with [coverage][]. There's also a page on setting up [docs][], as well. -{: .highlight-title } +:::{tip} New project template +Once you have completed the guidelines, there is a +[copier][]/[cookiecutter][]/[cruft][] project, [scientific-python/cookie][], +that implements these guidelines and lets you setup a new package from a +template in less than 60 seconds! Nine build backends including compiled +backends, generation tested in Nox, and kept in-sync with the guide. +::: -> New project template -> -> Once you have completed the guidelines, there is a -> [copier][]/[cookiecutter][]/[cruft][] project, [scientific-python/cookie][], -> that implements these guidelines and lets you setup a new package from a -> template in less than 60 seconds! Nine build backends including compiled -> backends, generation tested in Nox, and kept in-sync with the guide. +:::{important} Checking an existing project +We provide [sp-repo-review][], a set of [repo-review][] checks for comparing +your repository with the guidelines, runnable [right in the guide][] via +WebAssembly! All checks point to a linked badge in the guide. +::: -{: .important-title } - -> Checking an existing project -> -> We provide [sp-repo-review][], a set of [repo-review][] checks for comparing -> your repository with the guidelines, runnable [right in the guide][] via -> WebAssembly! All checks point to a linked badge in the guide. - - - -[tutorials]: {% link pages/tutorials/index.md %} -[style]: {% link pages/guides/style.md %} -[mypy]: {% link pages/guides/mypy.md %} -[docs]: {% link pages/guides/docs.md %} -[simple packaging]: {% link pages/guides/packaging_simple.md %} -[compiled packaging]: {% link pages/guides/packaging_compiled.md %} -[classic packaging]: {% link pages/guides/packaging_classic.md %} -[coverage]: {% link pages/guides/coverage.md %} -[gha_basic]: {% link pages/guides/gha_basic.md %} -[gha_pure]: {% link pages/guides/gha_pure.md %} -[gha_wheels]: {% link pages/guides/gha_wheels.md %} -[pytest]: {% link pages/guides/pytest.md %} -[task runners]: {% link pages/guides/tasks.md %} -[right in the guide]: {% link pages/guides/repo_review.md %} +[tutorials]: pages/tutorials/index +[style]: pages/guides/style +[mypy]: pages/guides/mypy +[docs]: pages/guides/docs +[simple packaging]: pages/guides/packaging_simple +[compiled packaging]: pages/guides/packaging_compiled +[classic packaging]: pages/guides/packaging_classic +[coverage]: pages/guides/coverage +[gha_basic]: pages/guides/gha_basic +[gha_pure]: pages/guides/gha_pure +[gha_wheels]: pages/guides/gha_wheels +[pytest]: pages/guides/pytest +[right in the guide]: pages/guides/repo_review [cookiecutter]: https://cookiecutter.readthedocs.io [copier]: https://copier.readthedocs.io @@ -70,4 +59,6 @@ on setting up [docs][], as well. [sp-repo-review]: https://pypi.org/project/sp-repo-review [scientific-python/cookie]: https://github.com/scientific-python/cookie - + +```text {tableofcontents} +``` diff --git a/docs/pages/guides/mypy.md b/docs/pages/guides/mypy.md index 7adb3543..494699b6 100644 --- a/docs/pages/guides/mypy.md +++ b/docs/pages/guides/mypy.md @@ -1,16 +1,10 @@ --- -layout: page title: "Static type checking" -permalink: /guides/mypy/ -nav_order: 9 -parent: Topical Guides --- -{% include toc.html %} +## 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, @@ -42,7 +36,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. @@ -66,7 +60,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: @@ -80,7 +74,7 @@ everything. Try to turn on as much as possible, and increase it until you can run with full `strict` checking. See the [style page][] for configuration suggestions. -[style page]: {% link pages/guides/style.md %} +[style page]: pages/guides/style For a library to support typing, it has to a) add types using any of the three methods, and b) add a `py.typed` empty file to indicate that it's okay to look @@ -90,9 +84,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. @@ -122,7 +116,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 @@ -174,7 +168,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: @@ -186,9 +180,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: @@ -213,7 +207,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 @@ -243,11 +237,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: @@ -295,7 +289,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 cbd613fa..c32b361d 100644 --- a/docs/pages/guides/packaging_classic.md +++ b/docs/pages/guides/packaging_classic.md @@ -1,14 +1,8 @@ --- -layout: page title: Classic packaging -permalink: /guides/packaging-classic/ -nav_order: 7 -parent: Topical Guides --- -{% include toc.html %} - -# 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 @@ -18,31 +12,31 @@ outlined as well. There are several popular packaging systems. This guide covers the old configuration style for [Setuptools][]. Unless you really need it, you should be using the modern style described in [Simple -Packaging]({% link pages/guides/packaging_simple.md %}). The modern style is +Packaging](pages/guides/packaging_simple). The modern style is guided by Python Enhancement Proposals (PEPs), and is more stable than the setuptools-specific mechanisms that evolve over the years. This page is kept to help users with legacy code (and hopefully upgrade it). -{: .note } - -> Raw source lives in git and has a `pyproject.toml` and/or a `setup.py`. You -> _can_ install directly from git via pip, but normally users install from -> distributions hosted on PyPI. There are three options: **A)** A source -> package, called an SDist and has a name that ends in `.tar.gz`. This is a copy -> of the GitHub repository, stripped of a few specifics like CI files, and -> possibly with submodules included (if there are any). **B)** A pure python -> wheel, which ends in `.whl`; this is only possible if there are no compiled -> extensions in the library. This does _not_ contain a setup.py, but rather a -> `PKG_INFO` file that is rendered from setup.py (or from another build system). -> **C)** If not pure Python, a collection of wheels for every binary platform, -> generally one per supported Python version and OS as well. -> -> Developer requirements (users of A or git) are generally higher than the -> requirements to use B or C. Poetry and optionally flit create SDists that -> include a `setup.py`, and all alternate packing systems produce "normal" -> wheels. - -## Package structure (medium priority) +:::{note} +Raw source lives in git and has a `pyproject.toml` and/or a `setup.py`. You +_can_ install directly from git via pip, but normally users install from +distributions hosted on PyPI. There are three options: **A)** A source +package, called an SDist and has a name that ends in `.tar.gz`. This is a copy +of the GitHub repository, stripped of a few specifics like CI files, and +possibly with submodules included (if there are any). **B)** A pure python +wheel, which ends in `.whl`; this is only possible if there are no compiled +extensions in the library. This does _not_ contain a setup.py, but rather a +`PKG_INFO` file that is rendered from setup.py (or from another build system). +**C)** If not pure Python, a collection of wheels for every binary platform, +generally one per supported Python version and OS as well. + +Developer requirements (users of A or git) are generally higher than the +requirements to use B or C. Poetry and optionally flit create SDists that +include a `setup.py`, and all alternate packing systems produce "normal" +wheels. +::: + +### 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 @@ -52,11 +46,11 @@ 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: -```toml +```ini [build-system] requires = ["setuptools>=42"] build-backend = "setuptools.build_meta" @@ -85,16 +79,16 @@ these "[hypermodern][]" packaging tools is growing in scientific Python packages. All tools build the same wheels (and they often build setuptools compliant SDists, as well). -{% rr PP003 %} Note that `"wheel"` is never required; it was injected +{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 Python packages supporting older versions of NumPy: -```toml +```ini requires = [ "oldest-supported-numpy", ``` @@ -104,32 +98,33 @@ package. Whether you build the wheel locally or on CI, you can transfer it to someone else and it will work on any supported NumPy. The `oldest-supported-numpy` package is a SciPy metapackage from the NumPy developers that tracks the -[correct version of NumPy to build wheels against for each version of Python and for each OS/implementation](https://github.com/scipy/oldest-supported-numpy/blob/master/setup.cfg). +[correct version of NumPy to build wheels against for each version of Python +and for each OS/implementation][oldest-supported-numpy]. Otherwise, you would have to list the earliest version of NumPy that had support for each Python version here. -{: .note } +:::{note} +Modern versions of NumPy (1.25+) allow you to target older versions when +building, which is _highly_ recommended, and this will become required in +NumPy 2.0. Now you add: -> Modern versions of NumPy (1.25+) allow you to target older versions when -> building, which is _highly_ recommended, and this will become required in -> NumPy 2.0. Now you add: -> -> ```cpp -> #define NPY_TARGET_VERSION NPY_1_22_API_VERSION -> ``` -> -> (Where that number is whatever version you support as a minimum) then make -> sure you build with NumPy 1.25+ (or 2.0+ when it comes out). +```cpp +#define NPY_TARGET_VERSION NPY_1_22_API_VERSION +``` -## Versioning (medium/high priority) +(Where that number is whatever version you support as a minimum) then make +sure you build with NumPy 1.25+ (or 2.0+ when it comes out). +::: + +### 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: -```toml +```ini requires = [ "setuptools>=42", "setuptools_scm[toml]>=3.4", @@ -189,8 +184,6 @@ computed correctly from a checkout that is too shallow. For GitHub Actions, use For GitHub actions, you can add a few lines that will enable you to manually trigger builds with custom versions: -{% raw %} - ```yaml on: workflow_dispatch: @@ -201,16 +194,14 @@ env: SETUPTOOLS_SCM_PRETEND_VERSION: ${{ github.event.inputs.overrideVersion }} ``` -{% endraw %} - If you fill in the override version setting when triggering a manual workflow run, that version will be forced, otherwise, it works as normal. -{: .note } - -> Make sure you have a good gitignore, probably starting from -> [GitHub's Python one](https://github.com/github/gitignore/blob/main/Python.gitignore) -> or using a [generator site](https://www.toptal.com/developers/gitignore). +:::{note} +Make sure you have a good gitignore, probably starting from +[GitHub's Python one](https://github.com/github/gitignore/blob/main/Python.gitignore) +or using a [generator site](https://www.toptal.com/developers/gitignore). +::: You should also add these two files: @@ -232,7 +223,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 @@ -253,7 +244,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 @@ -324,7 +315,7 @@ where = src # extern ``` -{% rr SCFG001 %} Note that all keys use underscores; using a dash will cause +{rr}`SCFG001` Note that all keys use underscores; using a dash will cause warnings and eventually failures. And, a possible `setup.py`; though in recent versions of pip, there no longer is @@ -357,13 +348,13 @@ SDist structure that shows up in another place in the package, then replace With the exception of flake8, all package configuration should be possible via `pyproject.toml`, such as pytest (6+): -```toml +```ini [tool.pytest] 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 @@ -407,7 +398,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 @@ -435,7 +426,7 @@ be all of git][setuptools_scm file]; if you do not, the default is a few common files, like any `.py` files and standard tooling. Here is a useful default, though be sure to update it to include any files that need to be included: -``` +```text graft src graft tests @@ -443,7 +434,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: @@ -470,15 +461,11 @@ the app. default behavior is changing, though, as there's much less reason today to have a legacy `setup.py`. - - [flit]: https://flit.readthedocs.io [poetry]: https://python-poetry.org -[hatch]: https://hatch.pypa.io [hypermodern]: https://cjolowicz.github.io/posts/hypermodern-python-01-setup/ [setuptools_scm file]: https://setuptools-scm.readthedocs.io/en/latest/usage/#file-finders-hook-makes-most-of-manifestin-unnecessary [manifest.in]: https://packaging.python.org/guides/using-manifest-in/ [setuptools]: https://setuptools.readthedocs.io/en/latest/userguide/index.html [setuptools cfg]: https://setuptools.readthedocs.io/en/latest/userguide/declarative_config.html - - +[oldest-supported-numpy]: https://github.com/scipy/oldest-supported-numpy/blob/master/setup.cfg diff --git a/docs/pages/guides/packaging_compiled.md b/docs/pages/guides/packaging_compiled.md index 049df61b..bfdc6c65 100644 --- a/docs/pages/guides/packaging_compiled.md +++ b/docs/pages/guides/packaging_compiled.md @@ -1,13 +1,7 @@ --- -layout: page title: Compiled packaging -permalink: /guides/packaging-compiled/ -nav_order: 6 -parent: Topical Guides --- -{% include toc.html %} - -{: .highlight-title } - -> Quick start -> -> Once you've done this at least once, feel free to use -> [our cookiecutter/copier template](https://github.com/scientific-python/cookie), -> or `uv init` to get started quickly on new packages! +:::{tip} Quick start +Once you've done this at least once, feel free to use +[our cookiecutter/copier template](https://github.com/scientific-python/cookie), +or `uv init` to get started quickly on new packages! +::: -# Packaging Compiled Projects +## Packaging Compiled Projects There are a variety of ways to package compiled projects. In the past, the only way to do it was to use setuptools/distutils, which required using lots of @@ -50,135 +42,136 @@ The most exciting developments have been new native build backends: - [enscons][]: Builds C/C++ using SCONs. (Aging now, but this was the first native backend!) -{: .note } - +:::{note} You should be familiar with [packing a pure Python -project]({% link pages/guides/packaging_compiled.md %}) - the metadata +project](pages/guides/packaging_compiled) - the metadata configuration is the same. - +::: There are also classic setuptools plugins: - [scikit-build][]: Builds C/C++/Fortran using CMake. - [setuptools-rust][]: Builds Rust using Cargo. -{: .highlight-title } - -> Selecting a backend -> -> Selecting a backend: If you are using Rust, use maturin. If you are using -> CUDA, use scikit-build-core. If you are using a classic language (C, C++, -> Fortran), then you can use either scikit-build-core or meson-python, depending -> on whether you prefer writing CMake or Meson. Meson is a lot more opinionated; -> it requires you use version control, it requires a README.md and LICENSE file. -> It requires your compiler be properly set up. Etc. While CMake can be as -> elegant as Meson, there are a lot of historical examples of poorly written -> CMake. +:::{tip} Selecting a backend +Selecting a backend: If you are using Rust, use maturin. If you are using +CUDA, use scikit-build-core. If you are using a classic language (C, C++, +Fortran), then you can use either scikit-build-core or meson-python, depending +on whether you prefer writing CMake or Meson. Meson is a lot more opinionated; +it requires you use version control, it requires a README.md and LICENSE file. +It requires your compiler be properly set up. Etc. While CMake can be as +elegant as Meson, there are a lot of historical examples of poorly written +CMake. +::: -## pyproject.toml: build-system +### pyproject.toml: build-system -{% rr PY001 %} Packages must have a `pyproject.toml` file {% rr PP001 %} that +{rr}`PY001` Packages must have a `pyproject.toml` file {rr}`PP001` that selects the backend: -{% tabs %} {% tab skbc Scikit-build-core %} - +::::{tab-set} +:::{tab-item} Scikit-build-core +:sync: scikit-build-core - -```toml + +```ini [build-system] requires = ["pybind11", "scikit-build-core>=0.12"] build-backend = "scikit_build_core.build" ``` - + - -{% endtab %} {% tab meson Meson-python %} - +::: +:::{tab-item} Meson-python +:sync: meson-python - -```toml + +```ini [build-system] requires = ["meson-python>=0.18", "pybind11"] build-backend = "mesonpy" ``` - + - -{% endtab %} {% tab maturin Maturin %} - +::: +:::{tab-item} Maturin +:sync: maturin - -```toml + +```ini [build-system] requires = ["maturin>=1.9,<2"] build-backend = "maturin" ``` - + +::: +:::: -{% endtab %} {% endtabs %} - -{% include pyproject.md %} +```{include} ../../_partials/pyproject.md +``` -## Tool section in pyproject.toml +### Tool section in pyproject.toml These tools all read the project table. They also have extra configuration options in `tool.*` settings. -{% tabs %} {% tab skbc Scikit-build-core %} - +::::{tab-set} +:::{tab-item} Scikit-build-core +:sync: scikit-build-core - -```toml + +```ini [tool.scikit-build] minimum-version = "build-system.requires" build-dir = "build/{wheel_tag}" ``` - + These options are not required, but can improve your experience. - -{% endtab %} {% tab meson Meson-python %} - +::: +:::{tab-item} Meson-python +:sync: meson-python No `tool.meson-python` configuration required for this example. - -{% endtab %} {% tab maturin Maturin %} - +::: +:::{tab-item} Maturin +:sync: maturin - -```toml + +```ini [tool.maturin] module-name = "package._core" python-source = "src" sdist-generator = "git" # default is cargo ``` - + Maturin assumes you follow Rust's package structure, so we need a little bit of configuration here to follow the convention of the other tools here. +::: +:::: -{% endtab %} {% endtabs %} - -## Backend specific files - -{% tabs %} {% tab skbc Scikit-build-core %} +### Backend specific files +::::{tab-set} +:::{tab-item} Scikit-build-core +:sync: scikit-build-core Example `CMakeLists.txt` file (using pybind11, so include `pybind11` in `build-system.requires` too): @@ -186,7 +179,7 @@ Example `CMakeLists.txt` file (using pybind11, so include `pybind11` in with code_fence("cmake"): print(skbuild_cmakelists_txt) ]]] --> - + ```cmake cmake_minimum_required(VERSION 3.15...3.26) project(${SKBUILD_PROJECT_NAME} LANGUAGES CXX) @@ -197,15 +190,15 @@ find_package(pybind11 CONFIG REQUIRED) pybind11_add_module(_core MODULE src/main.cpp) install(TARGETS _core DESTINATION ${SKBUILD_PROJECT_NAME}) ``` - + Scikit-build-core will use your `.gitignore` to help it avoid adding ignored files to your distributions; it also has a default ignore for common cache files, so you can get started without one, but it's recommended. - -{% endtab %} {% tab meson Meson-python %} - +::: +:::{tab-item} Meson-python +:sync: meson-python Example `meson.build` file (using pybind11, so include `pybind11` in `build-system.requires` too): @@ -213,7 +206,7 @@ Example `meson.build` file (using pybind11, so include `pybind11` in with code_fence("meson"): print(mesonpy_meson_build) ]]] --> - + ```meson project( 'package', @@ -238,23 +231,23 @@ py.extension_module('_core', install_subdir('src/package', install_dir: py.get_install_dir() / 'package', strip_directory: true) ``` - + Meson requires that your source be tracked by version control. In a real project, you will likely be doing this, but when trying out a build backend you might not think to set up a git repo to build it. - -{% endtab %} {% tab maturin Maturin %} - +::: +:::{tab-item} Maturin +:sync: maturin Example `Cargo.toml` file: - -```toml + +```ini [package] name = "package" version = "0.1.0" @@ -274,27 +267,28 @@ version = "0.27.2" # "abi3-py310" tells pyo3 (and maturin) to build using the stable ABI with minimum Python version 3.10 features = ["extension-module", "abi3-py310"] ``` - + +::: +:::: -{% endtab %} {% endtabs %} - -## Example compiled file +### Example compiled file This example will make a `_core` extension inside your package; this pattern allows you to easily provide both Python files and compiled extensions, and keeping the details of your compiled extension private. You can select whatever name you wish, though, or even make your compiled extension a top level module. -{% tabs %} {% tab skbc Scikit-build-core %} - +::::{tab-set} +:::{tab-item} Scikit-build-core +:sync: scikit-build-core Example `src/main.cpp` file: - + ```cpp #include @@ -324,18 +318,18 @@ PYBIND11_MODULE(_core, m) { )pbdoc"); } ``` - + - -{% endtab %} {% tab meson Meson-python %} - +::: +:::{tab-item} Meson-python +:sync: meson-python Example `src/main.cpp` file: - + ```cpp #include @@ -365,18 +359,18 @@ PYBIND11_MODULE(_core, m) { )pbdoc"); } ``` - + - -{% endtab %} {% tab maturin Maturin %} - +::: +:::{tab-item} Maturin +:sync: maturin Example `src/lib.rs` file: - + ```rs use pyo3::prelude::*; @@ -405,37 +399,37 @@ mod _core { } } ``` - + +::: +:::: -{% endtab %} {% endtabs %} - -## Package structure +### Package structure The recommendation (followed above) is to have source code in `/src`, and the Python package files in `/src/`. The compiled files also can go in `/src`. -## Versioning +### Versioning Check the documentation for the tools above to see what forms of dynamic versioning the tool supports. -## Including/excluding files in the SDist +### Including/excluding files in the SDist Each tool uses a different mechanism to include or remove files from the SDist, though the defaults are reasonable. -## Distributing +### Distributing Unlike pure Python, you'll need to build redistributable wheels for each platform and supported Python version if you want to avoid compilation on the user's system using cibuildwheel. See [the CI page on wheels][gha_wheels] for a suggested workflow. -## Special considerations +### Special considerations -### NumPy +#### NumPy Modern versions of NumPy (1.25+) allow you to target older versions when building, which is _highly_ recommended, and this became required in NumPy 2.0. @@ -453,24 +447,14 @@ for those versions. If using pybind11, you don't need NumPy at build-time in the first place. -{: .important } - +:::{important} Python 3.13.4 is broken on Windows for compiling code - it always reports that it is free-threaded. 3.13.5 was rushed out to fix it. - - +::: [scikit-build-core]: https://scikit-build-core.readthedocs.io [scikit-build]: https://scikit-build.readthedocs.io [meson-python]: https://meson-python.readthedocs.io -[cmake]: https://cmake.org -[meson]: https://mesonbuild.com -[enscons]: https://pypi.org/project/enscons -[scons]: https://scons.org/ [setuptools-rust]: https://setuptools-rust.readthedocs.io/en/latest/ [maturin]: https://www.maturin.rs -[gha_wheels]: {% link pages/guides/gha_wheels.md %} - - - - +[gha_wheels]: pages/guides/gha_wheels diff --git a/docs/pages/guides/packaging_simple.md b/docs/pages/guides/packaging_simple.md index b22b7766..499ca354 100644 --- a/docs/pages/guides/packaging_simple.md +++ b/docs/pages/guides/packaging_simple.md @@ -1,22 +1,14 @@ --- -layout: page title: Simple packaging -permalink: /guides/packaging-simple/ -nav_order: 5 -parent: Topical Guides --- -{% include toc.html %} +:::{tip} Quick start +Once you've done this at least once, feel free to use +[our cookiecutter/copier template](https://github.com/scientific-python/cookie), +or `uv init` to get started quickly on new packages! +::: -{: .highlight-title } - -> Quick start -> -> Once you've done this at least once, feel free to use -> [our cookiecutter/copier template](https://github.com/scientific-python/cookie), -> or `uv init` to get started quickly on new packages! - -# Simple packaging +## Simple packaging Python packages can now use a modern build system instead of the classic but verbose setuptools and `setup.py`. The one you select doesn't really matter that @@ -30,80 +22,83 @@ and compiled backends (see the next page). Also see the [Python packaging guide][], especially the [Python packaging tutorial][]. -{: .note-title } - -> Classic files -> -> These systems do not use or require `setup.py`, `setup.cfg`, or `MANIFEST.in`. -> Those are for setuptools. Unless you are using setuptools, of course, which -> still uses `MANIFEST.in`. You can convert the old files using -> `pipx run hatch new --init` or with -> [ini2toml](https://ini2toml.readthedocs.io/en/latest/). +:::{note} Classic files +These systems do not use or require `setup.py`, `setup.cfg`, or `MANIFEST.in`. +Those are for setuptools. Unless you are using setuptools, of course, which +still uses `MANIFEST.in`. You can convert the old files using +`pipx run hatch new --init` or with +[ini2toml](https://ini2toml.readthedocs.io/en/latest/). +::: -{: .important-title } +:::{important} Selecting a backend +Backends handle metadata the same way, so the choice comes down to how you +specify what files go into an SDist and extra features, like getting a version +from VCS. If you don't have an existing preference, hatchling is an excellent +choice, balancing speed, configurability, and extendability. +::: -> Selecting a backend -> -> Backends handle metadata the same way, so the choice comes down to how you -> specify what files go into an SDist and extra features, like getting a version -> from VCS. If you don't have an existing preference, hatchling is an excellent -> choice, balancing speed, configurability, and extendability. +### pyproject.toml: build-system -## pyproject.toml: build-system - -{% rr PY001 %} Packages must have a `pyproject.toml` file {% rr PP001 %} that +{rr}`PY001` Packages must have a `pyproject.toml` file {rr}`PP001` that selects the backend: -{% tabs %} {% tab hatch Hatchling %} +::::{tab-set} +:::{tab-item} Hatchling -```toml +```ini [build-system] requires = ["hatchling"] build-backend = "hatchling.build" ``` -{% endtab %} {% tab uv uv_build %} +::: +:::{tab-item} uv_build -```toml +```ini [build-system] requires = ["uv_build>=0.7.19"] build-backend = "uv_build" ``` -{% endtab %} {% tab flit Flit-core %} +::: +:::{tab-item} Flit-core -```toml +```ini [build-system] requires = ["flit_core>=3.12"] build-backend = "flit_core.buildapi" ``` -{% endtab %} {% tab pdm PDM-backend %} +::: +:::{tab-item} PDM-backend -```toml +```ini [build-system] requires = ["pdm-backend"] build-backend = "pdm.backend" ``` -{% endtab %} {% tab setuptools Setuptools %} +::: +:::{tab-item} Setuptools -```toml +```ini [build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" ``` -{% endtab %} {% endtabs %} +::: +:::: -{% include pyproject.md %} +```{include} ../../_partials/pyproject.md +``` For `requires-python`, you should specify the minimum you require, and you -should not put an upper cap on it {% rr PY004 %}, as this field is used to +should not put an upper cap on it {rr}`PY004`, as this field is used to back-solve for old package versions that pass this check, allowing you to safely drop Python versions. -## Package structure +### Package structure 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 @@ -120,11 +115,11 @@ detection. If you don't match your package name and import name (which you should except for very special cases), you will likely need extra configuration here. -You should have a `README` {% rr PY002 %} and a `LICENSE` {% rr PY003 %} file. -You should have a `docs/` folder {% rr PY004 %}. You should have a `/tests` -folder {% rr PY005 %} (recommended) and/or a `src//tests` folder. +You should have a `README` {rr}`PY002` and a `LICENSE` {rr}`PY003` file. +You should have a `docs/` folder {rr}`PY004`. You should have a `/tests` +folder {rr}`PY005` (recommended) and/or a `src//tests` folder. -## Versioning +### Versioning You can specify the version manually (as shown in the example), but the backends usually provide some automatic features to help you avoid this. Flit will pull @@ -134,18 +129,17 @@ in a file or use git. You will always need to specify that the version will be supplied dynamically with: -```toml +```ini dynamic = ["version"] ``` Then you'll configure your backend to compute the version. -{% details Hatchling dynamic versioning %} - +:::{dropdown} Hatchling dynamic versioning You can tell hatchling to get the version from VCS. Add `hatch-vcs` to your `build-backend.requires`, then add the following configuration: -```toml +```ini [tool.hatch] version.source = "vcs" build.hooks.vcs.version-file = "src//version.py" @@ -153,7 +147,7 @@ build.hooks.vcs.version-file = "src//version.py" Or you can tell it to look for it in a file (see docs for arbitrary regex's): -```toml +```ini [tool.hatch] version.path = "src//__init__.py" ``` @@ -178,10 +172,9 @@ And `.gitattributes` (or add this line if you are already using this file): This will allow git archives (including the ones generated from GitHub) to also support versioning. +::: -{% enddetails %} - -## Including/excluding files in the SDist +### Including/excluding files in the SDist This is tool specific. @@ -194,25 +187,24 @@ This is tool specific. - [PDM info here](https://pdm-backend.fming.dev/build_config/#include-or-exclude-files). - Setuptools still uses `MANIFEST.in`. -{: .warning } - -> Flit will not use VCS (like git) to populate the SDist if you use standard -> tooling, even if it can do that using its own tooling. So make sure you list -> explicit include/exclude rules, and test the contents: -> -> ```bash -> # Show SDist contents -> tar -tvf dist/*.tar.gz -> # Show wheel contents -> unzip -l dist/*.whl -> ``` +:::{warning} +Flit will not use VCS (like git) to populate the SDist if you use standard +tooling, even if it can do that using its own tooling. So make sure you list +explicit include/exclude rules, and test the contents: -{: .note-title } +```bash +# Show SDist contents +tar -tvf dist/*.tar.gz +# Show wheel contents +unzip -l dist/*.whl +``` -> Flit _requires_ `license.file` to be set in your `[project]` section to ensure -> it finds the license file. +::: - +:::{note} +Flit _requires_ `license.file` to be set in your `[project]` section to ensure +it finds the license file. +::: [flit]: https://flit.readthedocs.io [poetry]: https://python-poetry.org @@ -224,7 +216,4 @@ This is tool specific. [meson-python]: https://meson-python.readthedocs.io [python packaging guide]: https://packaging.python.org [python packaging tutorial]: https://packaging.python.org/tutorials/packaging-projects/ - - - - +[metadata]: https://packaging.python.org/en/latest/specifications/core-metadata/ diff --git a/docs/pages/guides/pytest.md b/docs/pages/guides/pytest.md index 6c0612de..97441e87 100644 --- a/docs/pages/guides/pytest.md +++ b/docs/pages/guides/pytest.md @@ -1,14 +1,8 @@ --- -layout: page title: "Testing with pytest" -permalink: /guides/pytest/ -nav_order: 2 -parent: Topical Guides --- -{% include toc.html %} - -# Testing with pytest +## Testing with pytest Tests are crucial to writing reliable software. A good test suite allows you to: @@ -29,17 +23,15 @@ in using pytest. The goals of writing good tests are: - Reporting: when things break, you should get good information about what broke. -{: .note-title } - -> What about other choices? -> -> The alternative library, `nose`, has been abandoned in favor of `pytest`, -> which can run nose-style tests. The standard library has a test suite as well, -> but it's extremely verbose and complex; and since "developers" run tests, your -> test requirements don't affect users. And `pytest` can run stdlib style -> testing too. So just use `pytest`. All major packages use it too, including -> `NumPy`. Most other choices, like [Hypothesis][], are related to `pytest` and -> just extend it. +:::{note} What about other choices? +The alternative library, `nose`, has been abandoned in favor of `pytest`, +which can run nose-style tests. The standard library has a test suite as well, +but it's extremely verbose and complex; and since "developers" run tests, your +test requirements don't affect users. And `pytest` can run stdlib style +testing too. So just use `pytest`. All major packages use it too, including +`NumPy`. Most other choices, like [Hypothesis][], are related to `pytest` and +just extend it. +::: ### Basic test structure @@ -63,17 +55,18 @@ This looks simple, but it is doing several things: happens. If it fails, you will get a clear, detailed report on what each value was. -### Configuring pytest +#### Configuring pytest pytest supports configuration in `pytest.ini`, `setup.cfg`, or, since version 6, -`pyproject.toml` {% rr PP301 %}, or, since version 9, `pytest.toml` or +`pyproject.toml` {rr}`PP301`, or, since version 9, `pytest.toml` or `.pytest.toml`. Remember, pytest is a developer requirement, not a user one, so always require 6+ (or 9+) and use `pyproject.toml` or the pytest TOML ones. This is an example configuration: -{% tabs %} {% tab conf-modern Pytest 9+ %} +::::{tab-set} +:::{tab-item} Pytest 9+ -```toml +```ini [tool.pytest] minversion = "9.0" addopts = ["-ra", "--showlocals"] @@ -85,9 +78,10 @@ testpaths = [ ] ``` -{% endtab %} {% tab conf-classic Pytest 6+ %} +::: +:::{tab-item} Pytest 6+ -```toml +```ini [tool.pytest.ini_options] minversion = "6.0" addopts = ["-ra", "--showlocals", "--strict-markers", "--strict-config"] @@ -99,24 +93,25 @@ testpaths = [ ] ``` -{% endtab %} {% endtabs %} +::: +:::: -{% rr PP302 %} The `minversion` will print a nicer error if your `pytest` is too +{rr}`PP302` The `minversion` will print a nicer error if your `pytest` is too old (though, ironically, it won't read this if the version is too old, so setting "6" or less in `pyproject.toml` is rather pointless, similarly for 9 if using the new config location). The `addopts` setting will add whatever you put -there to the command line when you run; {% rr PP308 %} `-ra` will print a +there to the command line when you run; {rr}`PP308` `-ra` will print a summary "r"eport of "a"ll results, which gives you a quick way to review what tests failed and were skipped, and why. `--showlocals` will print locals in tracebacks - depending on your tests, you might or might not like this one. -{% rr PP307 %} `--strict-markers` will make sure you don't try to use an -unspecified fixture. {% rr PP306 %} And `--strict-config` will error if you make -a mistake in your config. {% rr PP305 %} `xfail_strict` will change the default +{rr}`PP307` `--strict-markers` will make sure you don't try to use an +unspecified fixture. {rr}`PP306` And `--strict-config` will error if you make +a mistake in your config. {rr}`PP305` `xfail_strict` will change the default for `xfail` to fail the tests if it doesn't fail - you can still override -locally in a specific xfail for a flaky failure. {% rr PP309 %} +locally in a specific xfail for a flaky failure. {rr}`PP309` `filter_warnings` will cause all warnings to be errors (you can add allowed -warnings here too, see below). {% rr PP304 %} `log_level` will report `INFO` and -above log messages on a failure. {% rr PP303 %} Finally, `testpaths` will limit +warnings here too, see below). {rr}`PP304` `log_level` will report `INFO` and +above log messages on a failure. {rr}`PP303` Finally, `testpaths` will limit `pytest` to just looking in the folders given - useful if it tries to pick up things that are not tests from other directories. [See the docs](https://docs.pytest.org/en/stable/customize.html) for more @@ -144,7 +139,7 @@ becoming errors using the syntax tends to be `default` (show the first time) or `ignore` (never show). The regex matches at the beginning of the error unless you prefix it with `.*`. -### Running pytest +#### Running pytest You can run pytest directly with `pytest` or `python -m pytest`. You can optionally give a directory or file to run on. You can also select just some @@ -161,13 +156,13 @@ start out in your debugger at the beginning of the last failed test with `--trace --lf`. [See the docs](https://docs.pytest.org/en/stable/usage.html) for more running tips. -## Guidelines for writing good tests +### Guidelines for writing good tests Time spent learning all the powerful tools pytest has to offer will be well spent! You can make your tests more granular, mock things that aren't available (or are slow to run), parametrize, and much more. -### Tests should be easy +#### Tests should be easy Always use pytest. The built-in unittest is _very_ verbose; the simpler the writing of tests, the more tests you will write! @@ -210,7 +205,7 @@ This natively works with NumPy arrays, too! Always prefer `array1 == approx(array2)` over the functions in the `numpy.testing` module if you can, it is simpler and the reporting is better. -### Tests should test for failures too +#### Tests should test for failures too You should make sure that expected errors are thrown: @@ -226,7 +221,7 @@ def test_raises(): You can check for warnings as well, with `pytest.warns` or `pytest.deprecated_call`. -### Tests should stay easy when scaling out +#### Tests should stay easy when scaling out pytest [uses fixtures](https://docs.pytest.org/en/stable/fixture.html) to represent complex ideas, like setup/teardown, temporary resources, or @@ -289,7 +284,7 @@ conftest) will run three times, and each time will identify as a different `platform.system()`! Leave `autouse` off, and it becomes opt-in; adding `platform_system` to the list of arguments will opt in. -### Tests should be organized +#### Tests should be organized You can use `pytest.mark.*` to [mark](https://docs.pytest.org/en/stable/mark.html) tests, so you can easily @@ -321,7 +316,7 @@ Many pytest plugins support new marks too, like `pytest-parametrize`. You can also use custom marks to enable/disable groups of tests, or to pass data into fixtures. -### Tests should test the installed version, not the local version +#### Tests should test the installed version, not the local version Your tests should run against an _installed_ version of your code. Testing against the _local_ version might work while the installed version does not (due @@ -332,7 +327,7 @@ directories and `pytest` does not. Also, there may come a time when someone a build system, and if you are unable to test against an installed version, you won't be able to run your tests! (It happens more than you might think). -### Mock expensive or tricky calls +#### Mock expensive or tricky calls If you have to call something that is expensive or hard to call, it is often better to mock it. To isolate parts of your own code for "unit" testing, mocking @@ -392,5 +387,3 @@ redirects to the standard library [unittest.mock][]. [pytest]: https://docs.pytest.org [pytest-mock]: https://pypi.org/project/pytest-mock/ [unittest.mock]: https://docs.python.org/3/library/unittest.mock.html - - diff --git a/docs/pages/guides/repo_review.md b/docs/pages/guides/repo_review.md index b68913c7..b8f27090 100644 --- a/docs/pages/guides/repo_review.md +++ b/docs/pages/guides/repo_review.md @@ -1,12 +1,8 @@ --- -layout: page title: Repo-Review -permalink: /guides/repo-review/ -nav_order: 110 -interactive_repo_review: true --- -# Repo-Review +## Repo-Review You can check the style of a GitHub repository below. Enter any repository, such as `scikit-hep/hist`, and the branch you want to check, such as `main` (it must @@ -25,6 +21,14 @@ pipx run 'sp-repo-review[cli]' --- -{% include interactive_repo_review.html %} - -[Open in new page](https://scientific-python.github.io/repo-review/). +:::{anywidget} +{ + "url_sync": true, + "deps": [ + "repo-review~=1.1.0", + "sp-repo-review==2026.04.04", + "validate-pyproject[all]~=0.25.0", + "validate-pyproject-schema-store==2026.03.29", + ] +} +::: diff --git a/docs/pages/guides/style.md b/docs/pages/guides/style.md index 9f4878e7..1f982135 100644 --- a/docs/pages/guides/style.md +++ b/docs/pages/guides/style.md @@ -1,32 +1,25 @@ --- -layout: page title: Style & static checks -permalink: /guides/style/ -nav_order: 8 -parent: Topical Guides -custom_title: Style and static checks +short_title: Style and static checks --- -{% include toc.html %} +## Style and static checks -# Style and static checks +### Pre-commit -## Pre-commit - -{% rr PY006 %} Scientific Python projects often use [pre-commit][] (or [prek][]) +{rr}`PY006` Scientific Python projects often use [pre-commit][] (or [prek][]) to check code style. The original, `pre-commit`, has support for more languages, but `prek` is a faster Rust rewrite that supports most real world usage. -{% tabs runner %} {% tab prek Prek %} - +::::{tab-set} +:::{tab-item} Prek Prek can be installed through `brew` (macOS) or `pipx/uv` (anywhere). There are two modes to use it locally; you can check manually with `prek run` (changes only) or `prek run -a` (all). You can omit the `run`, as well; such as `prek -a`. You can also run `prek install` to add checks as a git pre-commit hook. - -{% endtab %} {% tab pre-commit Pre-commit %} - +::: +:::{tab-item} Pre-commit Pre-commit can be installed through `brew` (macOS) or `pipx/uv` (anywhere). There are two modes to use it locally; you can check manually with `pre-commit run` (changes only) or `pre-commit run -a` (all). You can also run @@ -35,8 +28,8 @@ gets its name). Pre-commit's setup is much slower than prek, but you can install `pre-commit-uv` with pre-commit to speed up the setup time quite a bit. - -{% endtab %} {% endtabs %} +::: +:::: Local runs (with `-a`) are the standard way to use it. That will run all the checks in optimized, isolated environments. After the first run, it's quite fast @@ -47,7 +40,7 @@ a custom pre-commit hook before; it's quite elegant and does not add or commit the changes, it just makes the changes and allows you to check and add them. You can always override the hook with `-n`. -{% rr PC100 %} Here is a minimal `.pre-commit-config.yaml` file with some handy +{rr}`PC100` Here is a minimal `.pre-commit-config.yaml` file with some handy options: ```yaml @@ -94,7 +87,7 @@ docker), you cannot enable a `--manual` flag, so extra checks will not run, and jobs should not download packages (use `additional-dependencies:` to add what you need). -{% rr PC901 %} {% rr PC902 %} {% rr PC903 %} You can customize the pre-commit +{rr}`PC901` {rr}`PC902` {rr}`PC903` You can customize the pre-commit message with: ```yaml @@ -124,9 +117,9 @@ updates: default-days: 7 ``` -## Format +### Format -{% rr PC110 %} [Black](https://black.readthedocs.io/en/latest/) is a popular +{rr}`PC110` [Black](https://black.readthedocs.io/en/latest/) is a popular auto-formatter from the Python Software Foundation. One of the main features of Black is that it is "opinionated"; that is, it is almost completely unconfigurable. Instead of allowing you to come up with your own format, it @@ -147,8 +140,8 @@ There are a _few_ options, mostly to enable/disable certain files, remove string normalization, and to change the line length, and those go in your `pyproject.toml` file. -{% tabs formatters %} {% tab ruff Ruff-format %} - +:::::{tab-set} +::::{tab-item} Ruff-format Ruff, the powerful Rust-based linter, has a formatter that is designed with the help of some of the Black authors to look 99.9% like Black, but run 30x faster. Here is the snippet to add the formatter to your `.pre-commit-config.yml` @@ -165,23 +158,21 @@ Here is the snippet to add the formatter to your `.pre-commit-config.yml` As you likely will be using Ruff if you follow this guide, the formatter is recommended as well. -{% details You can add a Ruff badge to your repo as well %} - +:::{dropdown} You can add a Ruff badge to your repo as well [![Code style: Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/format.json)](https://github.com/astral-sh/ruff) ```md [![Code style: Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/format.json)](https://github.com/astral-sh/ruff) ``` -``` +```text .. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/format.json :target: https://github.com/astral-sh/ruff ``` -{% enddetails %} - -{% endtab %} {% tab black Black %} - +::: +:::: +::::{tab-item} Black Here is the snippet to add Black to your `.pre-commit-config.yml`: ```yaml @@ -191,22 +182,21 @@ Here is the snippet to add Black to your `.pre-commit-config.yml`: - id: black ``` -{% details You can add a Black badge to your repo as well %} - +:::{dropdown} You can add a Black badge to your repo as well [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) ```md [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) ``` -``` +```text .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black ``` -{% enddetails %} - -{% endtab %} {% endtabs %} +::: +:::: +::::: In _very_ specific situations, like when making a 2D array, you may want to retain special formatting. After carefully deciding that it is a special use @@ -216,9 +206,8 @@ option! Most of the time, you can find a way to make the Blacked code look better by rewriting your code; factor out long unreadable portions into a variable, avoid writing matrices as 1D lists, etc. -{% details Documentation / README snippets support %} - -{% rr PC111 %} If you want Black used in your documentation, you can use +:::{dropdown} Documentation / README snippets support +{rr}`PC111` If you want Black used in your documentation, you can use blacken-docs. This can even catch syntax errors in code snippets! It supports markdown and restructured text. Note that because black is in `additional_dependencies`, you'll have to keep it up to date manually. @@ -231,11 +220,11 @@ markdown and restructured text. Note that because black is in additional_dependencies: [black==24.*] ``` -{% enddetails %} +::: -## Ruff +### Ruff -{% rr PC190 %} [Ruff][] [(docs)][ruff docs] is a Python code linter and +{rr}`PC190` [Ruff][] [(docs)][ruff docs] is a Python code linter and autofixer that replaces many other tools in the ecosystem with a ultra-fast (written in Rust), single zero-dependency package. All plugins are compiled in, so you can't get new failures from plugins updating without updating your @@ -252,16 +241,17 @@ pre-commit hook. args: ["--fix", "--show-fixes"] ``` -{% rr PC192 %} The hook is named `ruff-check`. {% rr PC191 %} The `--fix` +{rr}`PC192` The hook is named `ruff-check`. {rr}`PC191` The `--fix` argument is optional, but recommended, since you can inspect and undo changes in git. `--show-fixes` is highly recommended if `--fix` is present, otherwise it won't tell you what or why it fixed things. -{% rr RF001 %} Ruff is configured in your `pyproject.toml`. Here's an example: +{rr}`RF001` Ruff is configured in your `pyproject.toml`. Here's an example: -{% tabs ruff-config %} {% tab ruff-simple Simple config %} +::::{tab-set} +:::{tab-item} Simple config -```toml +```ini [tool.ruff.lint] extend-select = [ "B", # flake8-bugbear @@ -271,9 +261,10 @@ extend-select = [ ] ``` -{% endtab %} {% tab ruff-full Full config %} +::: +:::{tab-item} Full config -```toml +```ini [tool.ruff.lint] extend-select = [ "ARG", # flake8-unused-arguments @@ -323,9 +314,10 @@ typing-modules = ["mypackage._compat.typing"] "tests/**" = ["T20"] ``` -{% endtab %} {% tab ruff-ignore Ignore-based config %} +::: +:::{tab-item} Ignore-based config -```toml +```ini [tool.ruff.lint] select = ["ALL"] ignore = [ @@ -353,7 +345,8 @@ typing-modules = ["mypackage._compat.typing"] "tests/**" = ["T20"] ``` -{% endtab %} {% endtabs %} +::: +:::: Ruff [provides dozens of rule sets](https://beta.ruff.rs/docs/rules/); you can select what you want from these. Like Flake8, plugins match by whole letter @@ -373,31 +366,31 @@ entirely from pre-commit (default excludes include "build", so if you have a this). The `src` list which tells it where to look for top level packages is no longer -needed if just set to `["src"]` in Ruff 0.6+. {% rr RF003 %} +needed if just set to `["src"]` in Ruff 0.6+. {rr}`RF003` -{: .warning } +:::{warning} +If you don't use a `[project]` table (older setuptools or Poetry), then you +should also set: -> If you don't use a `[project]` table (older setuptools or Poetry), then you -> should also set: -> -> ```toml -> target-version = "py39" -> ``` -> -> This selects the minimum version you want to target (primarily for `"UP"` and -> `"I"`) {% rr RF002 %} +```ini +target-version = "py39" +``` + +This selects the minimum version you want to target (primarily for `"UP"` and +`"I"`) {rr}`RF002` +::: Here are some good error codes to enable on most (but not all!) projects: - `E`, `F`, `W`: These are the standard flake8 checks, classic checks that have stood the test of time. Not required if you use `extend-select` (`W` not needed if you use a formatter) -- `B`: This finds patterns that are very bug-prone. {% rr RF101 %} +- `B`: This finds patterns that are very bug-prone. {rr}`RF101` - `I`: This sorts your includes. There are multiple benefits, such as smaller diffs, fewer conflicts, a way to auto-inject `__future__` imports, and easier for readers to tell what's built-in, third-party, and local. It has a lot of configuration options, but defaults to a Black-compatible style. - {% rr RF102 %} + {rr}`RF102` - `ARG`: This looks for unused arguments. You might need to `# noqa: ARG001` occasionally, but it's overall pretty useful. - `C4`: This looks for places that could use comprehensions, and can autofix @@ -419,7 +412,7 @@ Here are some good error codes to enable on most (but not all!) projects: - `RUF`: Codes specific to Ruff, including removing noqa's that aren't used. - `T20`: Disallow `print` in your code (built on the assumption that it's a common debugging tool). -- `UP`: Upgrade old Python syntax to your `target-version`. {% rr RF103 %} +- `UP`: Upgrade old Python syntax to your `target-version`. {rr}`RF103` - `FURB`: From the refurb tool, a collection of helpful cleanups. - `PYI`: Typing related checks @@ -428,8 +421,7 @@ Ruff. You can use `ALL` to get them all, then ignore the ones you want to ignore. New checks go into `--preview` before being activated in a minor release. -{% details You can add a Ruff badge to your repo as well %} - +:::{dropdown} You can add a Ruff badge to your repo as well [![Code style: Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json))](https://github.com/astral-sh/ruff) ```md @@ -441,11 +433,11 @@ release. :target: https://github.com/astral-sh/ruff ``` -{% enddetails %} +::: -{% details Separate tools that Ruff replaces %} +::::{dropdown} Separate tools that Ruff replaces -### PyCln +#### PyCln [PyCln][] will clean up your imports if you have any that are not needed. There is a Flake8 check for this, but it's usually nicer to automatically do the @@ -461,7 +453,7 @@ use the manual stage, it's opt-in instead of automatic. stages: [manual] ``` -### Flake8 +#### Flake8 [Flake8][] can check a collection of good practices for you, ranging from simple style to things that might confuse or detract users, such as unused imports, @@ -520,8 +512,7 @@ enable more checks. A few interesting plugins: - [`flake8-print`](https://pypi.org/project/pep8-naming/): Makes sure you don't have print statements that sneak in. Code: `T` -{% details Flake8-print details %} - +:::{dropdown} Flake8-print details Having something verify you don't add a print statement by mistake is _very_ useful. A common need for the print checker would be to add it to a single directory (`src` if you are following the convention recommended). You can do @@ -535,9 +526,9 @@ per-file-ignores = examples/*: T ``` -{% enddetails %} +::: -### YesQA +#### YesQA Over time, you can end up with extra "noqa" comments that are no longer needed. This is a flake8 helper that removes those comments when they are no longer @@ -554,7 +545,7 @@ You need to have the same extra dependencies as flake8. In YAML, you can save the list given to yesqa and repeat it in flake8 using `&flake8-dependencies` and `*flake8-dependencies` after the colon. -### isort +#### isort You can have your imports sorted automatically by [isort][]. This will sort your imports, and is black compatible. One reason to have sorted imports is to reduce @@ -580,9 +571,7 @@ In order to use it, you need to add some configuration. You can add it to profile = "black" ``` -[isort]: https://pycqa.github.io/isort/ - -### PyUpgrade +#### PyUpgrade Another useful tool is [PyUpgrade][], which monitors your codebase for "old" style syntax. Most useful to keep Python 2 outdated constructs out, it can even @@ -600,23 +589,22 @@ when clearly better (please always use them, they are faster) if you set [pyupgrade]: https://github.com/asottile/pyupgrade -{: .note } +:::{note} +If you set this to at least `--py37-plus`, you can add the annotations import +by adding the following line to your isort pre-commit hook configuration: -> If you set this to at least `--py37-plus`, you can add the annotations import -> by adding the following line to your isort pre-commit hook configuration: -> -> ```yaml -> args: ["-a", "from __future__ import annotations"] -> ``` -> -> Also make sure isort comes before pyupgrade. Now when you run pre-commit, it -> will clean up your annotations to 3.7+ style, too! +```yaml +args: ["-a", "from __future__ import annotations"] +``` -{% enddetails %} +Also make sure isort comes before pyupgrade. Now when you run pre-commit, it +will clean up your annotations to 3.7+ style, too! +::: +:::: -## Type checking +### Type checking -{% rr PC140 %} One of the most exciting advancements in Python in the last 10 +{rr}`PC140` One of the most exciting advancements in Python in the last 10 years has been static type hints. Scientific Python projects vary in the degree to which they are type-hint ready. One of the challenges for providing static type hints is that it was developed in the Python 3 era and it really shines in @@ -652,7 +640,7 @@ add items to the virtual environment setup for MyPy by pre-commit, for example: additional_dependencies: [attrs==23.1.0] ``` -{% rr MY100 %} MyPy has a config section in `pyproject.toml` that looks like +{rr}`MY100` MyPy has a config section in `pyproject.toml` that looks like this: ```ini @@ -679,23 +667,23 @@ enable `check_untyped_defs` first, followed by `disallow_untyped_defs` then `disallow_incomplete_defs`. You can add these _per file_ by adding a `# mypy: