diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..21395ed --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..fda4259 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,29 @@ +name: CodeQL + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: "0 6 * * 1" # Mondays at 06:00 UTC + +jobs: + analyze: + name: Analyze (python) + runs-on: ubuntu-latest + permissions: + security-events: write + actions: read + contents: read + + steps: + - uses: actions/checkout@v4 + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: python + - name: Perform CodeQL analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:python" diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..26d22b7 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,18 @@ +name: pre-commit + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: "pip" + - uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 82fdf63..4444122 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -15,25 +15,20 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.10 - uses: actions/setup-python@v2 + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.12" + cache: "pip" - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 pytest pytest-cov - - name: Install package - run: | + pip install pytest pytest-cov pip install . - - name: Run unittests - run: | - python -m pytest - - name: Test coverage - run: | - pytest --cov --cov-report=xml + - 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 }} + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e1a4061..07f1e0d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ fail_fast: false repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -11,38 +11,16 @@ repos: - id: check-docstring-first - id: check-merge-conflict - id: check-symlinks - - id: check-yaml - id: debug-statements - id: requirements-txt-fixer -- repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.9 # Ruff version. - hooks: # Run the linter. - - id: ruff - types_or: [ python, pyi ] - args: [ --fix ] - - id: ruff-format # Run the formatter. - types_or: [ python, pyi ] -- repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.9.0 +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.9.9 hooks: - - id: python-use-type-annotations + - id: ruff + args: [--fix] + - id: ruff-format - repo: https://github.com/adrienverge/yamllint - rev: v1.28.0 + rev: v1.35.1 hooks: - id: yamllint args: ["-d", "{ignore: .pre-commit-config.yaml}"] - language: python - types: [yaml] -- repo: https://github.com/pycqa/pydocstyle - rev: 6.1.1 - hooks: - - id: pydocstyle - language: python - types: [python] -- repo: https://github.com/asottile/blacken-docs - rev: v1.12.1 - hooks: - - id: blacken-docs - language: python - types: [python] - additional_dependencies: [black==20.8b1] diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..196b90a --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,17 @@ +# License — placeholder + +This file is a placeholder. **Replace it with the full text of your chosen +license before publishing the package.** + +Pick a license at and paste its full text here. +Common choices for open-source Python packages: MIT, Apache-2.0, BSD-3-Clause. + +After replacing this file, also: + +- Update the year and copyright holder in the license text. +- Update the `license` field in `pyproject.toml` (e.g., `license = "MIT"` + using an SPDX expression per PEP 639). +- Update the LICENSE link in `docs/index.rst` if needed. + +Until you replace this file, no license is granted and the work is "all +rights reserved" by default. diff --git a/README.md b/README.md index 1133b5b..06f88d5 100644 --- a/README.md +++ b/README.md @@ -2,39 +2,36 @@ [![Documentation Status](https://readthedocs.org/projects/my_package/badge/?version=latest)](https://my_package.readthedocs.io/en/latest/?badge=latest) [![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) -[![DOI](https://zenodo.org/badge/331207062.svg)](https://zenodo.org/badge/latestdoi/) - - - +[![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) +[![DOI](https://zenodo.org/badge/{github_id}.svg)](https://zenodo.org/badge/latestdoi/{github_id}) 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}; if you recieve a 404 error the name has not been taken. -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 this after cloning the repo locally. +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`. -~~~bash -$ for file in $(find . -type f -not -path "./.git/*"); do sed -i "s/my_package/my_awesome_new_package/g" $file; done -~~~ +```bash +for file in $(find . -type f -not -path "./.git/*"); do + sed -i "s/my_package/my_awesome_new_package/g" "$file" +done +``` -You should similarly replace all instances of "mahynski" with your GitHub username. Also, rename the `my_package` directory to match your chosen name. +Similarly, replace all instances of `mahynski` with your GitHub username, and rename the `my_package/` directory to match your chosen name. -~~~bash -$ mv my_package/ my_awesome_new_package/ -$ git add my_awesome_new_package/ -$ git commit -m "created my_awesome_new_package" . -~~~ +```bash +git mv my_package my_awesome_new_package +git commit -m "rename package to my_awesome_new_package" +``` -3. Get coding! Follow the instructions below concerning Documentation, etc. for good coding practices. When you are ready to publish the code, proceed to step 4. -4. Bump the [version](https://semver.org/) appropriately in "my_package/__init__.py" and in the CITATION.cff file. -5. When finished FIRST go to [Zenodo](https://zenodo.org/) and enable preservation of this repo; THEN create a release on GitHub. Zenodo will automatically detect the new release and create a DOI and badge. Update the badge in this `README.md` file and in the `docs/index.rst` file with the new one generated by Zenodo. -6. Finally, follow [these instructions](https://packaging.python.org/en/latest/tutorials/packaging-projects/) to publish to PyPI. -7. Create a "dev" branch from main on which future development should occur (not main); when future releases are ready they can be merged into the main branch. This follows a [gitflow](https://nvie.com/posts/a-successful-git-branching-model/) management model instead of Google's ["live at head"](https://nehckl0.medium.com/googles-live-at-head-approach-and-release-management-4f175b723dae) philosophy. +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. +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`. +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. Best Practices === @@ -42,95 +39,107 @@ Best Practices Environment --- -Create a conda/mamba environment then create an editable install so you can easily test and modify things during development. +Create a conda/mamba environment, then install the package in editable mode so changes are picked up immediately during development. + +```bash +mamba create -n awesome_env python=3.12 +mamba activate awesome_env +cd my_awesome_new_package +pip install -e . # editable install +python -m ipykernel install --user --name=awesome_env # register Jupyter kernel +jupyter notebook --port 4321 # launch on localhost (default auth enabled) +``` -~~~bash -$ mamba create -n awesome_env python=3.10 -$ mamba activate awesome_env -$ cd my_awesome_new_package -$ pip install -e . # Local installation -$ python -m ipykernel install --user --name=awesome_env # Install kernel -$ jupyter notebook --port 4321 --ip='*' --NotebookApp.token='' --NotebookApp.password='' # Launch jupyter server -~~~ +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 is stored in the `docs/` folder and is currently set up to use [sphinx](https://www.sphinx-doc.org/en/master/). - -First create the `requirements.txt` that will be needed to build the documentation. This is done by running `pip-compile` on the `requirements.in` file. +Documentation lives in `docs/` and is built with [Sphinx](https://www.sphinx-doc.org/en/master/). -Be sure to add any additional requirements you add to the `pyproject.toml` file etc. to `requirements.in` also. +The doc build pulls Sphinx extensions from `docs/requirements.in`. Pin them with `pip-compile` whenever you change them: -~~~bash -$ cd docs -$ pip install Sphinx -$ pip install pip-tools -$ pip-compile requirements.in # Modify as needed before executing -~~~ +```bash +cd docs +pip install pip-tools +pip-compile requirements.in # produces requirements.txt +``` -Adjust the `docs/conf.py` as desired. Then run `docs/make_docs.sh` to setup the documentation initially. You can manually add and adjust later. +Tweak `docs/conf.py` as needed, then generate the API reference and HTML output: -~~~bash -$ bash make_docs.sh -~~~ +```bash +cd docs +pip install -r requirements.txt +bash make_docs.sh +``` -This command creates documentation for your package, but you still need to adjust the main landing page (`docs/index.rst`) manually. Here is a [primer](https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html) on reStructured text markup language. +Adjust the landing page in `docs/index.rst` manually — see the [reStructuredText primer](https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html). -Go to [https://about.readthedocs.com/](https://about.readthedocs.com/) to link your repo to build and host the documentation automatically! The `.readthedocs.yml` file contains the configuration for this which you can adjust as needed. +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. -Alternatively, you can start the documentation setup fresh using `sphinx-quickstart` in the `docs/` directory: - -~~~bash -$ sphinx-quickstart -~~~ +To start fresh, run `sphinx-quickstart` in `docs/`. Unittests --- -Build [unittests](https://docs.python.org/3/library/unittest.html) in the `tests/` directory. The `pyproject.toml` automatically configures pytest to look in `tests/`. The following will run all unittests in this directory. +Add [unittests](https://docs.python.org/3/library/unittest.html) in `tests/`. `pyproject.toml` configures pytest to look there automatically. + +```bash +python -m pytest +``` -~~~bash -$ python -m pytest -~~~ +`.github/workflows/python-app.yml` runs these tests and reports coverage on every push and pull request to `main`. Edit the `on:` triggers if you want the workflow to run on other branches too. -The GitHub workflow in `.github/workflows/python-app.yml` will also run these tests and perform coverage checks using this command. This workflow is triggered automatically on the main branch, but you can adjust this file so this is automatically triggered on others as well. +Code coverage is wired to [codecov.io](https://app.codecov.io/). Enable the repo there, add your `CODECOV_TOKEN` as a [GitHub Actions secret](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions), and update the badge URL in this `README.md` and in `docs/index.rst` (find it under *Configuration → Badges & Graphs* on codecov). -Code coverage is configured to work with [codecov.io](https://app.codecov.io/); visit their website to enable this for your new project. Add your codecov *repository* token (`CODECOV_TOKEN`) to this repo [as an "Actions" secret](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions). Then update the badge (click on the repo on codecov.io and navigate to Configuration > Badges & Graphs) in this `README.md` file and in the `docs/index.rst` file. +CI/CD +--- + +Three additional workflows keep the repo healthy with no extra setup: + +- `.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. -Linting +Linting and Formatting --- -Automatic code linting is provided via [pre-commit](https://pre-commit.com/); refer to the `.pre-commit-config.yaml` file for the specific configuration which you can adjust as needed. +Linting and formatting are handled by [ruff](https://docs.astral.sh/ruff/) via [pre-commit](https://pre-commit.com/). See `.pre-commit-config.yaml` for the full hook configuration. + +Install the hooks once, then they'll run on every commit: + +```bash +pre-commit install +``` -Run pre-commit to lint new code, then commit the changes. +To run all hooks against the whole repo on demand: -~~~bash -$ pre-commit run --all-files -~~~ +```bash +pre-commit run --all-files +``` Typing --- -While optional, it is best to include [typing](https://docs.python.org/3/library/typing.html) in your code - see [mypy](https://mypy-lang.org/). +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: -~~~bash -$ mypy --ignore-missing-imports my_new_file.py -~~~ +```bash +mypy --ignore-missing-imports my_new_file.py +``` Demo --- -Consider building a [streamlit](https://streamlit.io/) demo to illustrate the capabilities of the tool. This can be hosted for free in the [community cloud](https://streamlit.io/cloud). Refer to their documentation for easy setup, which allows you to put your `app.py` file right here in a public repo, e.g., `streamlit/app.py`. You may also consider using a [HuggingFace space](https://huggingface.co/spaces) instead. +If your package benefits from a UI, consider a [Streamlit](https://streamlit.io/) demo (free hosting via the [community cloud](https://streamlit.io/cloud)) or a [Hugging Face Space](https://huggingface.co/spaces). Either works directly from a public repo. Logo --- -You can generate a logo or other art using [Google Gemini](https://gemini.google.com/app) or other AI tools. [Note](https://lib.guides.umd.edu/c.php?g=1340355&p=9896961#:~:text=The%20Chicago%20Manual%20of%20Style's,prompt%20that%20generated%20the%20image.) that "the Chicago Manual of Style's website recommends you cite AI-generated images like any other image, while including both the name of the AI tool that generated the image, the company that created the AI, and the prompt that generated the image." Modify the `html_logo` tag in the `docs/conf.py` to point to the logo. +Generate a logo with [Google Gemini](https://gemini.google.com/app) or any other AI tool, save it under `docs/_static/`, and point the `html_logo` setting in `docs/conf.py` at the new file. If you use an AI-generated image, [cite it](https://lib.guides.umd.edu/c.php?g=1340355&p=9896961) — name the tool, the company, and the prompt. -The logo for this repository (`docs/_static/logo.png`) was generated using Google Gemini (Imagen 3) on Nov. 28, 2024 with the prompt "Make a logo inspired by code as a template for python projects." +The logo for this repository (`docs/_static/logo.png`) was generated with Google Gemini (Imagen 3) on Nov. 28, 2024 from the prompt: *"Make a logo inspired by code as a template for python projects."* Citation --- -Update the CITATION.cff, CODEOWNERS, and pyproject.toml files to include all code authors and maintainers appropriately. +Update `CITATION.cff`, `CODEOWNERS`, and `pyproject.toml` to list all authors and maintainers. The CITATION format is documented at [citation-file-format.github.io](https://citation-file-format.github.io/). diff --git a/docs/conf.py b/docs/conf.py index 2c1858e..7bb31dd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -4,6 +4,7 @@ For the full list of built-in configuration values, see the documentation: https://www.sphinx-doc.org/en/master/usage/configuration.html """ + import os import sys @@ -34,13 +35,13 @@ # -- 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 +html_logo = "_static/logo.png" # SPECIFY YOUR LOGO LINK HERE html_context = { - "display_github": True, # Integrate GitHub - "github_user": "mahynski", # Username - "github_repo": "my_package", # Repo name - "github_version": "main", # Version - "conf_py_path": "docs/", # Path in the checkout to the docs root + "display_github": True, # Integrate GitHub + "github_user": "mahynski", # Username + "github_repo": "my_package", # Repo name + "github_version": "main", # Version + "conf_py_path": "docs/", # Path in the checkout to the docs root } html_theme = "sphinx_book_theme" #'sphinx_rtd_theme' html_static_path = ["_static"] @@ -51,4 +52,4 @@ # nbsphinx_thumbnails = { # "jupyter/api/pipelines": "_static/default.png", # "jupyter/api/sharing_models": "_static/default.png", -#} +# } diff --git a/my_package/__init__.py b/my_package/__init__.py index db3291d..922e87f 100644 --- a/my_package/__init__.py +++ b/my_package/__init__.py @@ -3,6 +3,7 @@ Author: Nathan A. Mahynski """ + __version__ = "0.0.0" __all__ = [] diff --git a/pyproject.toml b/pyproject.toml index bd8ae18..c0aff33 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,13 +42,13 @@ dependencies = [ "pytest==7.4.0", "mypy", "sphinx" -] +] [project.optional-dependencies] all = [] [tool.pytest.ini_options] -testpaths = [ +testpaths = [ "tests", ] diff --git a/tests/__init__.py b/tests/__init__.py index cf86066..5eefcbd 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -3,4 +3,3 @@ Author: Nathan A. Mahynski """ - diff --git a/tests/test_example.py b/tests/test_example.py index 4747284..6ecb9ec 100644 --- a/tests/test_example.py +++ b/tests/test_example.py @@ -3,14 +3,16 @@ Author: Nathan A. Mahynski """ + import unittest -from my_package import * # Bad form, but just a placeholder example +import my_package # noqa: F401 + class DummyTest(unittest.TestCase): """Perform dummy tests.""" - - @classmethod + + @classmethod def setUpClass(self): """Set up things for all members of this test class.""" return