diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 1e95c06..7e42dad 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -31,6 +31,8 @@ jobs:
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
with:
languages: python
+ # Use the broader query suite (default + maintainability/style).
+ queries: security-and-quality
- name: Perform CodeQL analysis
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
with:
diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml
index 1ed8739..ade5ed0 100644
--- a/.github/workflows/python-app.yml
+++ b/.github/workflows/python-app.yml
@@ -1,5 +1,7 @@
-# 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
+# KEEP IN SYNC with .gitlab-ci.yml. This template ships CI for both GitHub and
+# GitLab so the package can move between them without rewriting pipelines.
+# When you change the test matrix, install command, or pytest invocation here,
+# mirror the change in .gitlab-ci.yml.
#
# Third-party actions are pinned to commit SHAs (with version comments) for
# supply-chain safety. Dependabot's `github-actions` ecosystem keeps both the
@@ -40,6 +42,11 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install -e ".[test]"
+ - name: Type-check with mypy
+ if: matrix.python-version == '3.12'
+ run: |
+ pip install mypy
+ mypy my_package
- name: Run tests with coverage
run: pytest --cov --cov-report=xml
- name: Upload coverage report
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 998106a..5182dd7 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,22 +1,54 @@
-# Optional GitLab CI mirror of `.github/workflows/python-app.yml`.
-# https://medium.com/swlh/automate-testing-with-gitlab-pipelines-4d35c72c18a
+# KEEP IN SYNC with .github/workflows/python-app.yml and pre-commit.yml.
+# This template ships CI for both GitHub and GitLab. The test stage below is
+# behaviorally equivalent to python-app.yml: same Python matrix (3.10–3.13),
+# same install command (pip install -e ".[test]"), same pytest invocation.
+# Coverage is exported as Cobertura XML (GitLab's native format) instead of
+# uploaded to Codecov.
+#
+# CodeQL has no GitLab equivalent here; if you host on GitLab, consider
+# GitLab's built-in SAST template instead:
+# https://docs.gitlab.com/ee/user/application_security/sast/
+
default:
- image: python:3.12
- before_script:
- - python -m pip install --upgrade pip
- - pip install -e ".[test]"
+ interruptible: true # cancel superseded pipelines, mirroring GH `concurrency`
+
stages:
- - test
+ - lint
+ - test
+
+pre-commit:
+ stage: lint
+ image: python:3.12
+ timeout: 10m
+ script:
+ - python -m pip install --upgrade pip
+ - pip install pre-commit
+ - pre-commit run --all-files
+
+mypy:
+ stage: lint
+ image: python:3.12
+ timeout: 10m
+ script:
+ - python -m pip install --upgrade pip
+ - pip install -e ".[test]" mypy
+ - mypy my_package
+
test:
- parallel:
- matrix:
- - PYTHON_VERSION: ["3.10", "3.11", "3.12", "3.13"]
- image: python:$PYTHON_VERSION
- script:
- - 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
+ stage: test
+ timeout: 15m
+ parallel:
+ matrix:
+ - PYTHON_VERSION: ["3.10", "3.11", "3.12", "3.13"]
+ image: python:$PYTHON_VERSION
+ before_script:
+ - python -m pip install --upgrade pip
+ - pip install -e ".[test]"
+ script:
+ - pytest --cov --cov-report=term --cov-report=xml
+ coverage: '/TOTAL.*? (\d+(?:\.\d+)?\%)$/'
+ artifacts:
+ reports:
+ coverage_report:
+ coverage_format: cobertura
+ path: coverage.xml
diff --git a/.readthedocs.yml b/.readthedocs.yml
index 286c9b5..6a46b8d 100755
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -21,12 +21,12 @@ build:
# Build documentation in the "docs/" directory with Sphinx
sphinx:
- fail_on_warning: false
+ # Fail on warnings so dangling cross-references don't silently rot. Flip to
+ # `false` if you intentionally ship docs with known warnings.
+ fail_on_warning: true
configuration: docs/conf.py
# You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
# builder: "dirhtml"
- # Fail on all warnings to avoid broken references
- # fail_on_warning: true
# Optionally build your docs in additional formats such as PDF and ePub
# formats:
diff --git a/CITATION.cff b/CITATION.cff
index b462f01..3e71aea 100644
--- a/CITATION.cff
+++ b/CITATION.cff
@@ -5,7 +5,7 @@ authors:
given-names: Nathan
orcid: https://orcid.org/0000-0002-0008-8749
title: "my_package"
-version: 0.0.0
+version: 0.0.0.dev0
# 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
diff --git a/README.md b/README.md
index 8c9132e..45a3e4c 100644
--- a/README.md
+++ b/README.md
@@ -14,33 +14,30 @@ 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. 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`.
+2. Run the personalization script. It rewrites every place the original author's identity is baked in (package name, GitHub username, author name, email, ORCID, codecov token) and renames the `my_package/` directory in one shot:
```bash
-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" {} +
+scripts/personalize.sh \
+ --name my_awesome_new_package \
+ --username my-github-handle \
+ --author "Jane Doe" \
+ --email jane@example.com \
+ --orcid 0000-0001-2345-6789 # optional; line is dropped from CITATION.cff if omitted
```
-Similarly, replace all instances of `mahynski` with your GitHub username, and rename the `my_package/` directory to match your chosen name.
-
-```bash
-git mv my_package my_awesome_new_package
-git commit -m "rename package to my_awesome_new_package"
-```
+Review the diff (`git diff`), then commit. Once you're satisfied you can delete the script with `rm -r scripts/`.
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. Before tagging a release, update **all** of:
+4. Replace `docs/_static/logo.png` and `logo_transparent.png` with your own art (or remove `html_logo` from `docs/conf.py`). The committed art is AI-generated `my_package` placeholder.
+5. Once codecov is enabled for the new repo, replace `REPLACE_WITH_CODECOV_TOKEN` in `README.md` and `docs/index.rst` with your project's token.
+6. Get coding! Follow the best practices below. When you are ready to publish, proceed to the next step.
+7. 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. 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.
+8. 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.
+9. Follow [these instructions](https://packaging.python.org/en/latest/tutorials/packaging-projects/) to publish to PyPI.
+10. 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
===
@@ -70,8 +67,12 @@ python -m ipykernel install --user --name=awesome_env # register Jupyter kerne
jupyter notebook --port 4321 # launch on localhost (default auth enabled)
```
-The package declares the following extras in `pyproject.toml`:
+The package declares the following extras in `pyproject.toml`. The default
+`dependencies` list is **empty** on purpose — add only what your package
+actually imports. The `science` extra is opt-in so users who don't need it
+aren't forced to install hundreds of MB of wheels.
+- `science` — `matplotlib`, `numpy`, `pandas`, `scikit-learn`, `scipy`, `seaborn` with loose floors
- `test` — `pytest` + `pytest-cov` (what CI installs)
- `docs` — Sphinx and the theme/extensions used by `docs/conf.py`
- `notebook` — `IPython`, `ipywidgets`, `ipykernel`
@@ -112,11 +113,23 @@ Code coverage is wired to [codecov.io](https://app.codecov.io/). Enable the repo
CI/CD
---
-Three additional workflows keep the repo healthy with no extra setup:
+The template ships CI for **both GitHub and GitLab** so you can host on either
+without rewriting pipelines. The two configs are kept behaviorally equivalent
+for the test stage (same Python matrix, same install command, same coverage
+report). When you change one, **mirror the change in the other**; both files
+have a `KEEP IN SYNC` header pointing at the other.
+
+GitHub-specific workflows (with no GitLab equivalent in this template):
-- `.github/workflows/pre-commit.yml` runs the pre-commit hooks on every push and PR to `main`, so lint/format failures are caught in CI rather than only locally.
-- `.github/workflows/codeql.yml` runs GitHub's [CodeQL](https://codeql.github.com/) static-analysis scanner on every push, every PR, and weekly on Mondays. Findings appear under the repo's *Security → Code scanning* tab.
-- `.github/dependabot.yml` opens weekly PRs to update GitHub Actions versions and pip dependencies. Tune the `interval` or `open-pull-requests-limit` if the volume is too high.
+- `.github/workflows/python-app.yml` — test matrix on 3.10–3.13, coverage to Codecov.
+- `.github/workflows/pre-commit.yml` — runs pre-commit hooks on push and PR to `main`.
+- `.github/workflows/codeql.yml` — [CodeQL](https://codeql.github.com/) static analysis on push, PR, and weekly Mondays. Findings appear under *Security → Code scanning*.
+- `.github/dependabot.yml` — weekly PRs to update GitHub Actions and pip dependencies. Tune `interval` or `open-pull-requests-limit` if volume is too high.
+
+GitLab equivalent:
+
+- `.gitlab-ci.yml` — mirrors the test matrix and adds `pre-commit` and `mypy` jobs. CodeQL has no GitLab analogue here; if you host on GitLab, consider GitLab's built-in [SAST](https://docs.gitlab.com/ee/user/application_security/sast/) instead.
+- **No Dependabot on GitLab.** If you host on GitLab, consider [Renovate](https://docs.renovatebot.com/) (works on both platforms) or replace `.github/dependabot.yml` with a manual update cadence.
Linting and Formatting
---
@@ -138,12 +151,17 @@ pre-commit run --all-files
Typing
---
-Type hints are optional but recommended — see the [typing docs](https://docs.python.org/3/library/typing.html) and [mypy](https://mypy-lang.org/). Mypy is not run by pre-commit in this template; invoke it manually as needed:
+Type hints are optional but recommended — see the [typing docs](https://docs.python.org/3/library/typing.html) and [mypy](https://mypy-lang.org/). [Mypy](https://mypy-lang.org/) is configured under `[tool.mypy]` in `pyproject.toml` and runs in CI on every push/PR (the GitHub `python-app.yml` test job runs it on Python 3.12; the GitLab `mypy` job runs in the `lint` stage). The package ships a [PEP 561](https://peps.python.org/pep-0561/) `py.typed` marker so downstream consumers see your annotations.
+
+Run mypy locally the same way CI does:
```bash
-mypy --ignore-missing-imports my_new_file.py
+pip install -e ".[dev]"
+mypy my_package
```
+The default config is permissive (`ignore_missing_imports`, `check_untyped_defs`, no `disallow_untyped_defs`) so a fresh template checkout passes without effort. Tighten it as your code grows — e.g., flip on `disallow_untyped_defs = true` once you're ready to require annotations on public APIs.
+
Demo
---
diff --git a/docs/conf.py b/docs/conf.py
index 3585953..34e9d1b 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -5,17 +5,13 @@
https://www.sphinx-doc.org/en/master/usage/configuration.html
"""
-import datetime
-import os
-import sys
-
-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 = f"{datetime.date.today().year}, Nathan A. Mahynski"
+# Hardcoded for reproducible builds. Bump the year manually (or replace with
+# a "{start}–{current}" range) when appropriate.
+copyright = "2026, Nathan A. Mahynski"
author = "Nathan A. Mahynski"
# -- General configuration ---------------------------------------------------
diff --git a/docs/index.rst b/docs/index.rst
index 73d3dbb..6ebb4a1 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -32,11 +32,6 @@ License Information
* Any mention of commercial products is for information only; it does not imply recommendation or endorsement by `NIST `_.
-.. toctree::
- :maxdepth: 2
- :caption: Contents:
-
-
.. toctree::
:maxdepth: 3
:caption: API Reference
diff --git a/my_package/__init__.py b/my_package/__init__.py
index 922e87f..bcb27dc 100644
--- a/my_package/__init__.py
+++ b/my_package/__init__.py
@@ -4,6 +4,6 @@
Author: Nathan A. Mahynski
"""
-__version__ = "0.0.0"
+__version__ = "0.0.0.dev0"
__all__ = []
diff --git a/my_package/py.typed b/my_package/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/pyproject.toml b/pyproject.toml
index 1007fb9..60f7db4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -30,20 +30,22 @@ readme = "README.md"
# is populated. See PEP 639.
license = {file = "LICENSE.md"}
requires-python = ">=3.10"
-dependencies = [
- "matplotlib>=3.9.0",
- "numpy>=2.2.6",
- "pandas>=2.1.4",
- "scikit-learn>=1.7.2",
- "scipy>=1.13.1",
- "seaborn>=0.13.0",
- "tqdm>=4.67.3",
- "watermark>=2.6.0",
- "missingno>=0.5.2",
- "wandb>=0.26.1",
-]
+# Intentionally empty: this is a template. Add only what your package actually
+# imports at runtime. Heavy science deps live in the `science` extra below so
+# users opt in.
+dependencies = []
[project.optional-dependencies]
+science = [
+ # Loose floors chosen to match Python 3.13 wheel availability on PyPI.
+ # Tighten only when your code actually depends on a newer API.
+ "matplotlib>=3.9",
+ "numpy>=2.1",
+ "pandas>=2.2",
+ "scikit-learn>=1.5",
+ "scipy>=1.14",
+ "seaborn>=0.13",
+]
test = [
"pytest>=7.4.0",
"pytest-cov",
@@ -97,10 +99,26 @@ select = [
"UP", # pyupgrade
]
+[tool.mypy]
+# Permissive defaults so a fresh template checkout passes. Tighten as your
+# code grows: turn on `disallow_untyped_defs = true` once you're ready to
+# require type annotations on public APIs.
+python_version = "3.10"
+ignore_missing_imports = true
+warn_unused_ignores = true
+warn_redundant_casts = true
+warn_unused_configs = true
+strict_equality = true
+check_untyped_defs = true
+
[tool.setuptools.packages.find]
where = ["."]
include = ["my_package*"]
+[tool.setuptools.package-data]
+# Ship the PEP 561 marker so downstream consumers see the package's type hints.
+my_package = ["py.typed"]
+
[tool.setuptools.dynamic]
version = {attr = "my_package.__version__"}
diff --git a/scripts/personalize.sh b/scripts/personalize.sh
new file mode 100755
index 0000000..f5c81a6
--- /dev/null
+++ b/scripts/personalize.sh
@@ -0,0 +1,149 @@
+#!/usr/bin/env bash
+# Personalize this template for your project.
+#
+# Replaces every place the original author's identity is baked in
+# (package name, GitHub username, author name, email, ORCID, codecov token)
+# and renames the package directory. Run once, after cloning.
+#
+# Usage:
+# scripts/personalize.sh \
+# --name my_awesome_package \
+# --username my-github-handle \
+# --author "Jane Doe" \
+# --email jane@example.com \
+# [--orcid 0000-0001-2345-6789]
+#
+# Flags --name, --username, --author, --email are required. --orcid is
+# optional; if omitted, the orcid line is removed from CITATION.cff.
+#
+# After running, manually:
+# 1. Replace LICENSE.md with your chosen license and update the `license`
+# field in pyproject.toml to its SPDX expression.
+# 2. Replace docs/_static/logo*.png with your own art (or remove
+# `html_logo` from docs/conf.py).
+# 3. Once codecov is enabled for the new repo, replace the placeholder
+# token (the script leaves it as REPLACE_WITH_CODECOV_TOKEN).
+# 4. Once Zenodo mints a DOI, fill in CITATION.cff and the badge in
+# README.md / docs/index.rst.
+# 5. Update pyproject.toml's `description` and the package docstring
+# in /__init__.py.
+#
+# When you're satisfied, delete this script: `rm -r scripts/`.
+
+set -euo pipefail
+
+# ---- defaults (these are the strings to replace) -------------------------
+OLD_NAME="my_package"
+OLD_USERNAME="mahynski"
+OLD_AUTHOR="Nathan A. Mahynski"
+OLD_EMAIL="nathan.mahynski@gmail.com"
+OLD_ORCID="0000-0002-0008-8749"
+OLD_CODECOV_TOKEN="YSLBQ33C7F"
+
+NEW_NAME=""
+NEW_USERNAME=""
+NEW_AUTHOR=""
+NEW_EMAIL=""
+NEW_ORCID=""
+
+# ---- arg parse -----------------------------------------------------------
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --name) NEW_NAME="$2"; shift 2 ;;
+ --username) NEW_USERNAME="$2"; shift 2 ;;
+ --author) NEW_AUTHOR="$2"; shift 2 ;;
+ --email) NEW_EMAIL="$2"; shift 2 ;;
+ --orcid) NEW_ORCID="$2"; shift 2 ;;
+ -h|--help)
+ sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'
+ exit 0
+ ;;
+ *) echo "Unknown arg: $1" >&2; exit 2 ;;
+ esac
+done
+
+for var in NEW_NAME NEW_USERNAME NEW_AUTHOR NEW_EMAIL; do
+ if [[ -z "${!var}" ]]; then
+ echo "Missing required flag for ${var#NEW_} (lowercased). See --help." >&2
+ exit 2
+ fi
+done
+
+# CITATION.cff stores given/family names as separate fields. Split the new
+# author on the last space: "Jane Doe" -> given="Jane", family="Doe".
+# "Jane A. Doe" -> given="Jane A.", family="Doe".
+NEW_FAMILY="${NEW_AUTHOR##* }"
+NEW_GIVEN="${NEW_AUTHOR% *}"
+OLD_FAMILY="Mahynski"
+OLD_GIVEN="Nathan"
+
+# ---- platform-aware sed -i ----------------------------------------------
+if [[ "$(uname)" == "Darwin" ]]; then
+ sedi=(sed -i '')
+else
+ sedi=(sed -i)
+fi
+
+# ---- collect target files (plain text, exclude .git, _build, this script) -
+mapfile -t files < <(find . -type f \
+ -not -path "./.git/*" \
+ -not -path "./docs/_build/*" \
+ -not -path "./scripts/personalize.sh" \
+ \( -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" \))
+
+# ---- rewrite -------------------------------------------------------------
+# Use | as sed delimiter for fields that may contain / (none here, but
+# author/email are safer that way too).
+for f in "${files[@]}"; do
+ "${sedi[@]}" \
+ -e "s|${OLD_AUTHOR}|${NEW_AUTHOR}|g" \
+ -e "s|${OLD_EMAIL}|${NEW_EMAIL}|g" \
+ -e "s|${OLD_CODECOV_TOKEN}|REPLACE_WITH_CODECOV_TOKEN|g" \
+ -e "s/${OLD_NAME}/${NEW_NAME}/g" \
+ -e "s/${OLD_USERNAME}/${NEW_USERNAME}/g" \
+ "$f"
+done
+
+# CITATION.cff name fields (given/family stored separately).
+"${sedi[@]}" \
+ -e "s/family-names: ${OLD_FAMILY}/family-names: ${NEW_FAMILY}/" \
+ -e "s/given-names: ${OLD_GIVEN}/given-names: ${NEW_GIVEN}/" \
+ CITATION.cff
+
+# ORCID: rewrite if provided, otherwise drop the orcid line from CITATION.cff.
+if [[ -n "$NEW_ORCID" ]]; then
+ "${sedi[@]}" -e "s|${OLD_ORCID}|${NEW_ORCID}|g" CITATION.cff
+else
+ "${sedi[@]}" -e "/orcid:.*${OLD_ORCID}/d" CITATION.cff
+fi
+
+# ---- rename package directory -------------------------------------------
+if [[ -d "$OLD_NAME" && "$OLD_NAME" != "$NEW_NAME" ]]; then
+ if git ls-files --error-unmatch "$OLD_NAME" >/dev/null 2>&1; then
+ git mv "$OLD_NAME" "$NEW_NAME"
+ else
+ mv "$OLD_NAME" "$NEW_NAME"
+ fi
+fi
+
+cat <