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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
11 changes: 9 additions & 2 deletions .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
70 changes: 51 additions & 19 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -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
6 changes: 3 additions & 3 deletions .readthedocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
68 changes: 43 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,30 @@ PyPI Package Template Instructions
<img src="docs/_static/logo_transparent.png" align="right" width=200 />

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
===
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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
---
Expand All @@ -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
---

Expand Down
10 changes: 3 additions & 7 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---------------------------------------------------
Expand Down
5 changes: 0 additions & 5 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,6 @@ License Information

* Any mention of commercial products is for information only; it does not imply recommendation or endorsement by `NIST <https://www.nist.gov/>`_.

.. toctree::
:maxdepth: 2
:caption: Contents:


.. toctree::
:maxdepth: 3
:caption: API Reference
Expand Down
2 changes: 1 addition & 1 deletion my_package/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
Author: Nathan A. Mahynski
"""

__version__ = "0.0.0"
__version__ = "0.0.0.dev0"

__all__ = []
Empty file added my_package/py.typed
Empty file.
42 changes: 30 additions & 12 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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__"}

Expand Down
Loading
Loading