diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 21395ed..810dee7 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,9 +5,16 @@ updates: schedule: interval: "weekly" open-pull-requests-limit: 5 + groups: + actions: + patterns: ["*"] - package-ecosystem: "pip" directory: "/" schedule: interval: "weekly" open-pull-requests-limit: 5 + groups: + python-deps: + patterns: ["*"] + update-types: ["minor", "patch"] diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index fda4259..011ffee 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,3 +1,6 @@ +# Third-party actions are pinned to commit SHAs (with version comments) for +# supply-chain safety. See python-app.yml for rationale. + name: CodeQL on: @@ -8,22 +11,27 @@ on: schedule: - cron: "0 6 * * 1" # Mondays at 06:00 UTC +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: analyze: name: Analyze (python) runs-on: ubuntu-latest + timeout-minutes: 30 permissions: security-events: write actions: read contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@0daab03d71ff584ef619d027a3fd9146679c5d84 # v3.35.3 with: languages: python - name: Perform CodeQL analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@0daab03d71ff584ef619d027a3fd9146679c5d84 # v3.35.3 with: category: "/language:python" diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 26d22b7..3812e23 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -1,3 +1,6 @@ +# Third-party actions are pinned to commit SHAs (with version comments) for +# supply-chain safety. See python-app.yml for rationale. + name: pre-commit on: @@ -6,13 +9,21 @@ on: pull_request: branches: [main] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + jobs: pre-commit: runs-on: ubuntu-latest + timeout-minutes: 10 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" cache: "pip" - - uses: pre-commit/action@v3.0.1 + - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 4444122..9e3234e 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -1,34 +1,50 @@ # This workflow will install Python dependencies, run tests and lint with a single version of Python # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions +# +# Third-party actions are pinned to commit SHAs (with version comments) for +# supply-chain safety. Dependabot's `github-actions` ecosystem keeps both the +# SHA and the comment in sync. name: Python application on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read jobs: build: - runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - cache: "pip" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install pytest pytest-cov - pip install . - - name: Run tests with coverage - run: pytest --cov --cov-report=xml - - name: Upload coverage report - uses: codecov/codecov-action@v4 - with: - token: ${{ secrets.CODECOV_TOKEN }} + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: ${{ matrix.python-version }} + cache: "pip" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[test]" + - name: Run tests with coverage + run: pytest --cov --cov-report=xml + - name: Upload coverage report + if: matrix.python-version == '3.12' + uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5.5.4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false diff --git a/.gitignore b/.gitignore index 82f9275..2907992 100644 --- a/.gitignore +++ b/.gitignore @@ -70,6 +70,9 @@ instance/ # Sphinx documentation docs/_build/ +# Generated by sphinx-apidoc; regenerated on each docs build. +docs/modules.rst +docs/my_package*.rst # PyBuilder .pybuilder/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2be4dac..998106a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,20 +1,22 @@ -# If using GitLab instead of GitHub +# Optional GitLab CI mirror of `.github/workflows/python-app.yml`. # https://medium.com/swlh/automate-testing-with-gitlab-pipelines-4d35c72c18a default: - image: python:3.10 + image: python:3.12 before_script: - - apt-get update - - apt-get install -y python3-pip - - pip install --upgrade pip - - pip install flake8 pytest coverage + - python -m pip install --upgrade pip + - pip install -e ".[test]" stages: - - install - test -install: - script: - - pip install . test: + parallel: + matrix: + - PYTHON_VERSION: ["3.10", "3.11", "3.12", "3.13"] + image: python:$PYTHON_VERSION script: - - coverage run -m pytest - - coverage report + - pytest --cov --cov-report=term --cov-report=xml coverage: '/TOTAL.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/' + artifacts: + reports: + coverage_report: + coverage_format: cobertura + path: coverage.xml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 07f1e0d..532779d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,7 @@ --- fail_fast: false +default_language_version: + python: python3.12 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 @@ -14,7 +16,7 @@ repos: - id: debug-statements - id: requirements-txt-fixer - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.9 + rev: v0.15.12 hooks: - id: ruff args: [--fix] diff --git a/.readthedocs.yml b/.readthedocs.yml index 252631b..286c9b5 100755 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -8,14 +8,16 @@ version: 2 build: os: ubuntu-22.04 tools: - python: "3.10" + python: "3.12" # You can also specify other tool versions: # nodejs: "20" # rust: "1.70" # golang: "1.20" - commands: - - python -m pip install --exists-action=w --no-cache-dir -r docs/requirements.txt - - python -m sphinx -T -b html -d docs/_build/doctrees -D language=en docs $READTHEDOCS_OUTPUT/html + jobs: + pre_build: + # Generate API reference pages so the `modules` toctree in + # docs/index.rst resolves on Read the Docs (mirrors make_docs.sh). + - sphinx-apidoc -o docs/ my_package/ # Build documentation in the "docs/" directory with Sphinx sphinx: @@ -31,13 +33,11 @@ sphinx: # - pdf # - epub -# Optional but recommended, declare the Python requirements required -# to build your documentation -# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html -# -# docs/requirements.txt can be generated using pip-tools (see https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html) +# Install the package with its `docs` extra so the Sphinx build matches the +# pinned toolchain declared in pyproject.toml. python: install: - - requirements: docs/requirements.txt - - method: pip - path: . + - method: pip + path: . + extra_requirements: + - docs diff --git a/CITATION.cff b/CITATION.cff index 965aeee..b462f01 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,10 +1,12 @@ -cff-version: 1.2.0 -message: "Please cite this repository as indicated below." -authors: - - family-names: Mahynski - given-names: Nathan - orcid: https://orcid.org/0000-0002-0008-8749 -title: "my_package" -version: v0.0.0 -doi: 10.1234/zenodo.12345678 -date-released: YYYY-MM-DD +cff-version: 1.2.0 +message: "Please cite this repository as indicated below." +authors: + - family-names: Mahynski + given-names: Nathan + orcid: https://orcid.org/0000-0002-0008-8749 +title: "my_package" +version: 0.0.0 +# Populate `doi` and `date-released` once Zenodo mints a DOI for the first +# release. Until then, leave them absent so cff-convert validation passes. +# doi: 10.5281/zenodo.XXXXXXX +# date-released: 2026-01-01 diff --git a/README.md b/README.md index 06f88d5..3c33164 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,10 @@ [![codecov](https://codecov.io/gh/mahynski/my_package/branch/main/graph/badge.svg?token=YSLBQ33C7F)](https://codecov.io/gh/mahynski/my_package) [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) [![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) + + PyPI Package Template Instructions === @@ -11,12 +14,15 @@ PyPI Package Template Instructions 1. Choose a name that does not exist in [pypi](https://pypi.org/). You can check by going to `https://pypi.org/simple/{my_awesome_new_package}`; a 404 means the name is available. -2. Replace all `my_package` names, strings, etc. in this repo with your desired package name (e.g., `my_awesome_new_package`). The command below makes this simple; run it after cloning the repo locally. The `sed` syntax below is for GNU `sed` (Linux); on macOS use `sed -i ''` instead of `sed -i`. +2. Replace all `my_package` names, strings, etc. in this repo with your desired package name (e.g., `my_awesome_new_package`). The command below makes this simple; run it after cloning the repo locally. It restricts the rewrite to plain-text files and skips `.git/` so binaries (logos, etc.) are left alone. The `sed` syntax below is for GNU `sed` (Linux); on macOS use `sed -i ''` instead of `sed -i`. ```bash -for file in $(find . -type f -not -path "./.git/*"); do - sed -i "s/my_package/my_awesome_new_package/g" "$file" -done +find . -type f \ + -not -path "./.git/*" \ + \( -name "*.py" -o -name "*.md" -o -name "*.rst" -o -name "*.toml" \ + -o -name "*.cfg" -o -name "*.yml" -o -name "*.yaml" -o -name "*.in" \ + -o -name "*.cff" -o -name "*.sh" -o -name "Makefile*" -o -name "CODEOWNERS" \) \ + -exec sed -i "s/my_package/my_awesome_new_package/g" {} + ``` Similarly, replace all instances of `mahynski` with your GitHub username, and rename the `my_package/` directory to match your chosen name. @@ -26,12 +32,15 @@ git mv my_package my_awesome_new_package git commit -m "rename package to my_awesome_new_package" ``` -3. Replace the placeholder `LICENSE.md` with the full text of your chosen license (see [choosealicense.com](https://choosealicense.com/)) and update the `license` field in `pyproject.toml` accordingly. +3. Replace the placeholder `LICENSE.md` with the full text of your chosen license (see [choosealicense.com](https://choosealicense.com/)) and update the `license` field in `pyproject.toml` accordingly. Per [PEP 639](https://peps.python.org/pep-0639/), prefer an SPDX expression: `license = "MIT"`. 4. Get coding! Follow the best practices below. When you are ready to publish, proceed to the next step. -5. Bump the [version](https://semver.org/) in `my_package/__init__.py` and in `CITATION.cff`. +5. Before tagging a release, update **all** of: + - `__version__` in `my_package/__init__.py` + - `version:` and `date-released:` in `CITATION.cff` (and the `doi:` once Zenodo mints it) + - the `Development Status` classifier in `pyproject.toml` 6. When finished, first enable preservation of this repo on [Zenodo](https://zenodo.org/), then create a release on GitHub. Zenodo will detect the new release and mint a DOI and badge. Update the `{github_id}` placeholder in this `README.md` and in `docs/index.rst` with the ID generated by Zenodo. 7. Follow [these instructions](https://packaging.python.org/en/latest/tutorials/packaging-projects/) to publish to PyPI. -8. Develop on a `dev` branch and merge into `main` for releases. This template assumes that pattern; adjust to taste. +8. The included GitHub Actions workflows trigger on pushes and PRs to `main`. If you adopt a `dev` → `main` pattern for releases, update the `on:` blocks in `.github/workflows/*.yml` so CI runs on `dev` too. Best Practices === @@ -45,37 +54,34 @@ Create a conda/mamba environment, then install the package in editable mode so c mamba create -n awesome_env python=3.12 mamba activate awesome_env cd my_awesome_new_package -pip install -e . # editable install +pip install -e ".[dev]" # editable install with dev/test/docs/notebook extras python -m ipykernel install --user --name=awesome_env # register Jupyter kernel jupyter notebook --port 4321 # launch on localhost (default auth enabled) ``` +The package declares the following extras in `pyproject.toml`: + +- `test` — `pytest` + `pytest-cov` (what CI installs) +- `docs` — Sphinx and the theme/extensions used by `docs/conf.py` +- `notebook` — `IPython`, `ipywidgets`, `ipykernel` +- `dev` — superset of the above plus `pre-commit` and `mypy` + Note: don't disable Jupyter's token/password or bind it to `0.0.0.0` unless you understand the access implications. The default (localhost + token) is the safe choice. Documentation --- -Documentation lives in `docs/` and is built with [Sphinx](https://www.sphinx-doc.org/en/master/). - -The doc build pulls Sphinx extensions from `docs/requirements.in`. Pin them with `pip-compile` whenever you change them: - -```bash -cd docs -pip install pip-tools -pip-compile requirements.in # produces requirements.txt -``` - -Tweak `docs/conf.py` as needed, then generate the API reference and HTML output: +Documentation lives in `docs/` and is built with [Sphinx](https://www.sphinx-doc.org/en/master/). Sphinx and the extensions enabled in `docs/conf.py` are declared as the `docs` extra in `pyproject.toml`, so a single editable install pulls everything in: ```bash +pip install -e ".[docs]" cd docs -pip install -r requirements.txt bash make_docs.sh ``` Adjust the landing page in `docs/index.rst` manually — see the [reStructuredText primer](https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html). -For hosted docs, link the repo at [Read the Docs](https://about.readthedocs.com/). The `.readthedocs.yml` already installs both `docs/requirements.txt` and the package itself; adjust as needed. +For hosted docs, link the repo at [Read the Docs](https://about.readthedocs.com/). The `.readthedocs.yml` installs the package with its `docs` extra so the build matches the pinned toolchain in `pyproject.toml`. To start fresh, run `sphinx-quickstart` in `docs/`. diff --git a/docs/Makefile.txt b/docs/Makefile similarity index 100% rename from docs/Makefile.txt rename to docs/Makefile diff --git a/docs/conf.py b/docs/conf.py index 7bb31dd..3585953 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -5,16 +5,17 @@ https://www.sphinx-doc.org/en/master/usage/configuration.html """ +import datetime import os import sys -sys.path.insert(0, os.path.abspath("../my_package/")) +sys.path.insert(0, os.path.abspath("..")) # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = "my_package" -copyright = "2024, Nathan A. Mahynski" +copyright = f"{datetime.date.today().year}, Nathan A. Mahynski" author = "Nathan A. Mahynski" # -- General configuration --------------------------------------------------- @@ -29,13 +30,18 @@ "nbsphinx", "sphinx_gallery.load_style", ] +# To enable BibTeX citations, add "sphinxcontrib.bibtex" above, install +# `sphinxcontrib-bibtex` (e.g. add it to the docs extra), and set +# `bibtex_bibfiles = ["refs.bib"]` with at least one *.bib file under docs/. templates_path = ["_templates"] exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_logo = "_static/logo.png" # SPECIFY YOUR LOGO LINK HERE +# `_static/logo.png` is used by the Sphinx theme; `_static/logo_transparent.png` +# is what README.md embeds. Keep both in sync (or trim to one) when replacing. +html_logo = "_static/logo.png" html_context = { "display_github": True, # Integrate GitHub "github_user": "mahynski", # Username @@ -46,7 +52,8 @@ html_theme = "sphinx_book_theme" #'sphinx_rtd_theme' html_static_path = ["_static"] pygments_style = "sphinx" -nbsphinx_execute = "never" # Always pre-run notebooks and save their output - just display on readthedocs +# Notebooks must be pre-executed locally; RTD just renders saved output. +nbsphinx_execute = "never" # Examples for setting thumbnails for jupyter notebook tiles # nbsphinx_kernel_name = 'my_package-kernel' # nbsphinx_thumbnails = { diff --git a/docs/index.rst b/docs/index.rst index a5a84d3..73d3dbb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -17,8 +17,10 @@ documentation for details. :target: https://codecov.io/gh/mahynski/my_package .. image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white :target: https://github.com/pre-commit/pre-commit -.. image:: https://zenodo.org/badge/331207062.svg - :target: https://zenodo.org/badge/latestdoi/331207062 +.. + Uncomment after Zenodo mints a DOI for this repo and replace the badge ID. + .. image:: https://zenodo.org/badge/331207062.svg + :target: https://zenodo.org/badge/latestdoi/331207062 ---- diff --git a/docs/make_docs.sh b/docs/make_docs.sh index c356875..275131b 100644 --- a/docs/make_docs.sh +++ b/docs/make_docs.sh @@ -1,8 +1,12 @@ -#!/bin/bash -sphinx-apidoc -o ./ ../my_package/; -make clean html; -make html; +#!/usr/bin/env bash +# Build the Sphinx HTML docs locally. Mirrors the RTD pre_build / build flow. +set -euo pipefail -# Run these commands the first time to set up -# pip install pip-tools -# pip-compile requirements.in +# Generate API reference pages from my_package/. Output goes alongside conf.py +# in docs/, where they're picked up by the `modules` toctree in index.rst. +sphinx-apidoc -o ./ ../my_package/ + +# Clean and rebuild HTML. Equivalent to `make clean html` but doesn't depend +# on the Makefile being named correctly. +rm -rf _build/html _build/doctrees +sphinx-build -b html -d _build/doctrees . _build/html diff --git a/docs/requirements.in b/docs/requirements.in deleted file mode 100644 index 2f19248..0000000 --- a/docs/requirements.in +++ /dev/null @@ -1,6 +0,0 @@ -sphinx-book-theme==1.0.1 -sphinx -readthedocs-sphinx-search>=0.3.2 -nbsphinx==0.9.2 -sphinx_gallery==0.14.0 -sphinxcontrib-bibtex==2.5.0 diff --git a/pyproject.toml b/pyproject.toml index 9ef37b5..a045abc 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,18 +16,24 @@ maintainers = [ ] keywords = [] classifiers = [ - "Development Status :: 5 - Production/Stable", + "Development Status :: 1 - Planning", "Intended Audience :: Science/Research", "Operating System :: OS Independent", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] readme = "README.md" +# Replace with an SPDX expression (e.g. license = "MIT") once LICENSE.md +# is populated. See PEP 639. license = {file = "LICENSE.md"} requires-python = ">=3.10" dependencies = [ "matplotlib>=3.9.0", "numpy>=1.26.4", - "pandas==2.1.4", # For Colab + "pandas>=2.1.4", "scikit-learn>=1.5.0", "scipy>=1.13.1", "seaborn>=0.13.0", @@ -35,23 +41,62 @@ dependencies = [ "watermark>=2.4.3", "missingno>=0.5.2", "wandb>=0.17.5", +] + +[project.optional-dependencies] +test = [ + "pytest>=7.4.0", + "pytest-cov", +] +docs = [ + "sphinx", + "sphinx-book-theme>=1.0.1", + "readthedocs-sphinx-search>=0.3.2", + "nbsphinx>=0.9.2", + "sphinx_gallery>=0.14.0", +] +notebook = [ "IPython", "ipywidgets", "ipykernel", +] +dev = [ + "my_package[test,docs,notebook]", "pre-commit>=3.3.3", - "pytest>=7.4.0", "mypy", - "sphinx" ] -[project.optional-dependencies] -all = [] - [tool.pytest.ini_options] testpaths = [ "tests", ] +[tool.coverage.run] +source = ["my_package"] +branch = true + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "raise NotImplementedError", + "if TYPE_CHECKING:", + "if __name__ == .__main__.:", +] + +[tool.ruff] +line-length = 100 +target-version = "py310" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "UP", # pyupgrade +] + [tool.setuptools.packages.find] where = ["."] include = ["my_package*"] diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index 5eefcbd..0000000 --- a/tests/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -""" -Unittests - -Author: Nathan A. Mahynski -""" diff --git a/tests/test_example.py b/tests/test_example.py index 6ecb9ec..e76ad8c 100644 --- a/tests/test_example.py +++ b/tests/test_example.py @@ -1,6 +1,10 @@ """ Unittests template example. +TODO: Replace this no-op test with real tests for your package. The dummy +test below exists only to keep the test suite green on a fresh template +checkout — coverage numbers it produces are not meaningful. + Author: Nathan A. Mahynski """ @@ -13,7 +17,7 @@ class DummyTest(unittest.TestCase): """Perform dummy tests.""" @classmethod - def setUpClass(self): + def setUpClass(cls): """Set up things for all members of this test class.""" return