diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..c43490a --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +jgpauloski@uchicago.edu. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..9370017 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,46 @@ +# Contributing Guide +Welcome! We appreciate your interest in contributing. + +## Issues + +Report bugs using GitHub's Issue Tracker. +A useful bug report has detail, background, and sample code. +For example, try to include: + +- A quick summary and/or background. +- Steps to reproduce: + - Be specific! + - Give sample code if you can. +- What you expected would happen. +- What actually happens. +- Any additional information that could help us. + - Why you think this might be happening. + - Things you tried that didn't work. + - Etc. + +## Pull Requests + +We use the [Github Flow](https://docs.github.com/en/get-started/quickstart/github-flow), so all changes happen through pull requests. + +For small bugs, feel free to open a pull request directly. +For larger bugs or enhancements, please open an issue first +Having an associated issue makes it easier to track changes and discuss proposals before you get started. + +1. Fork the repo and create a new branch from `main`. + - We suggest naming your branch `issue-##` if your pull request is addressing an open issue. +2. Make your changes. + - If you've added code that should be tested, add tests. + - If you've changed APIs, update the documentation. +3. Ensure the test suite passes and your code lints. +4. Open the pull request! + +## Coding Style +Be consistent with the coding style of the repository you are contributing to. +Our projects generally have strict code formatters and linters which can fix basic style issues for you. + +## License +Any contributions you make will be under the MIT Software License +Feel free to contact the maintainers if that's a concern. + +## References +This document was adapted from an [adaptation](https://gist.github.com/briandk/3d2e8b3ec8daf5a27a62) of the open-source contribution guidelines for [Facebook's Draft](https://github.com/facebook/draft-js/blob/a9316a723f9e918afde44dea68b5f9f39b7d9b00/CONTRIBUTING.md). diff --git a/.github/ISSUE_TEMPLATE/01_bug.yml b/.github/ISSUE_TEMPLATE/01_bug.yml new file mode 100644 index 0000000..3dae025 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01_bug.yml @@ -0,0 +1,42 @@ +name: Bug Report +description: Report errors or unexpected results. +labels: ["bug"] +assignees: + - foobar-bug-assign-dev +body: + - type: textarea + id: install + attributes: + label: How did you install foobar? + description: > + E.g., install via pip, install from source, etc. **Note:** this will + be rendered as console text automatically. + placeholder: | + $ pip install foobar + Collecting foobar + ... + Successfully installed foobar... + render: console + validations: + required: true + + - type: input + id: version + attributes: + label: What version of foobar are you using? + description: > + Package version if installed via Pip or commit ID if installed + from source. + placeholder: v1.2.3 + validations: + required: true + + - type: textarea + id: freeform + attributes: + label: Describe the problem. + description: > + Please provide sample code and directions for reproducing + your problem and what you expected to happen. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/02_enhancement.yml b/.github/ISSUE_TEMPLATE/02_enhancement.yml new file mode 100644 index 0000000..b324e5b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02_enhancement.yml @@ -0,0 +1,38 @@ +name: Enhancement Request +description: Request a new feature or enhancement. +labels: ["enhancement"] +assignees: + - foobar-enh-assign-dev +body: + - type: textarea + id: request + attributes: + label: Describe the Request + description: > + Please describe your use case and why the current feature set does + not satisfy your needs. + validations: + required: true + + - type: textarea + id: example + attributes: + label: Sample Code + description: > + If relevant, please provide sample code such as the proposed + interface, usage, or results. **Note:** this will be rendered as + Python code automatically. + render: python + validations: + required: false + + - type: textarea + id: additional + attributes: + label: Additional Code or Information + description: > + Optional space for additional code or text. **Note:** this will + be rendered as console text automatically. + render: console + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/03_docs.yml b/.github/ISSUE_TEMPLATE/03_docs.yml new file mode 100644 index 0000000..e6d1d82 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/03_docs.yml @@ -0,0 +1,15 @@ +name: Documentation Improvements +description: Suggest improvements to the documentation. +labels: ["documentation"] +assignees: + - foobar-doc-assignee-dev +body: + - type: textarea + id: freeform + attributes: + label: Describe the Request + description: > + Please describe limitations of the current documentation or + suggested improvements. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..0086358 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: true diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..15946f0 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,14 @@ +# Security Policy + +## Supported Versions + +We currently only support the latest versions of the software with security +updates. I.e., we will not backport new security updates to old versions. + +## Reporting a Vulnerability + +If you believe you have found a security vulnerability in the repository, we +encourage you to let us know right away. We will investigate all legitimate +reports and do our best to quickly fix the problem. + +Please report security issues to the Issue Tracker or Security Advisories. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b608ffc --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 + +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every week + interval: "weekly" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..ee1bae0 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,34 @@ +# Description + + + +### Fixes + + +- Fixes #XX +- Fixes #XX + +### Type of Change + + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Refactoring (internal implementation changes) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] Documentation update (no changes to the code) +- [ ] CI change (changes to CI workflows, packages, templates, etc.) +- [ ] Version changes (changes to the package or dependency versions) + +## Testing + + +N/A + +## Pull Request Checklist + +Please confirm the PR meets the following requirements. +- [ ] Relevant tags are added (breaking, bug, dependencies, documentation, enhancement, refactor). +- [ ] Code changes pass `pre-commit` (e.g., ruff, mypy, etc.). +- [ ] Tests have been added to show the fix is effective or that the new feature works. +- [ ] New and existing unit tests pass locally with the changes. +- [ ] Docs have been updated and reviewed if relevant. diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..c783b72 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,45 @@ +changelog: + exclude: + labels: + - ignore-for-release + authors: + - pre-commit-ci + categories: + # Provide steps for upgrading the package and adjusting for + # breaking changes. No PRs included here. + - title: Upgrade Steps + exclude: + labels: + - "*" + # All PRs tagged as "breaking" + - title: Breaking Changes + labels: + - breaking + # All PRs tagged as "enhancement" + - title: New Features + labels: + - enhancement + # All PRs tagged as "bug" + - title: Bug Fixes + labels: + - bug + # All PRs not tagged as the above or below + - title: Improvements + labels: + - refactor + exclude: + labels: + - dependencies + - documentation + # All PRs tagged as documentation + - title: Improvements + labels: + - documentation + # All PRs tagged "dependencies" + - title: Dependencies + labels: + - dependencies + # All PRs not tagged as the above + - title: Other Changes + labels: + - "*" diff --git a/.github/workflows/cache.yml b/.github/workflows/cache.yml new file mode 100644 index 0000000..4179718 --- /dev/null +++ b/.github/workflows/cache.yml @@ -0,0 +1,34 @@ +# Clean up GitHub Actions caches for closed pull requests. +# Source: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries +name: cache-cleanup + +on: + pull_request: + types: + - closed + +jobs: + cleanup: + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Cleanup + run: | + gh extension install actions/gh-actions-cache + REPO=${{ github.repository }} + BRANCH="refs/pull/${{ github.event.pull_request.number }}/merge" + echo "Fetching list of cache key" + cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 ) + ## Setting this to not fail the workflow while deleting cache keys. + set +e + echo "Deleting caches..." + for cacheKey in $cacheKeysForPR + do + echo "Deleting $REPO $BRANCH $cacheKey" + gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm + done + echo "Done" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/check-docs.yml b/.github/workflows/check-docs.yml new file mode 100644 index 0000000..231f7ab --- /dev/null +++ b/.github/workflows/check-docs.yml @@ -0,0 +1,45 @@ +# This workflow just verifies that the docs build without any +# warnings (as configured in the tox recipe). This is only run on +# the test-me-* branches and PRs as pushes to main will trigger the docs +# workflow. +name: check-docs + +on: + push: + branches: [test-me-*] + pull_request: + workflow_dispatch: + +jobs: + check-docs: + timeout-minutes: 10 + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Get pip cache dir + id: pip-cache-dir + run: echo "PIP_CACHE_DIR=$(pip cache dir)" >> $GITHUB_ENV + + - name: Use pip cache + id: pip-cache + uses: actions/cache@v4 + with: + path: ${{ env.PIP_CACHE_DIR }} + key: docs-ubuntu-latest-pip-3.11-${{ hashFiles('pyproject.toml') }} + restore-keys: | + docs-ubuntu-latest-pip-3.11- + + - name: Install Packages + run: pip install --upgrade setuptools pip tox virtualenv + + - name: Run Tox to build docs + run: tox -e docs diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..dc7614d --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,49 @@ +name: docs + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + docs: + timeout-minutes: 10 + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Get pip cache dir + id: pip-cache-dir + run: echo "PIP_CACHE_DIR=$(pip cache dir)" >> $GITHUB_ENV + + - name: Use pip cache + id: pip-cache + uses: actions/cache@v4 + with: + path: ${{ env.PIP_CACHE_DIR }} + key: docs-ubuntu-latest-pip-3.11-${{ hashFiles('pyproject.toml') }} + restore-keys: | + docs-ubuntu-latest-pip-3.11- + + - name: Install Packages + run: | + pip install --upgrade setuptools pip + pip install .[docs] + + - name: Configure git + run: | + git config --local user.name "GitHub Actions Bot" + git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" + - name: Deploy docs for main:latest to gh-pages branch + run: | + mike deploy --push --update-aliases main diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..164e5d3 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,62 @@ +name: publish + +on: + release: + types: [published] + workflow_dispatch: + +permissions: + contents: write + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Extract package version from pyproject.toml + run: | + echo "PACKAGE_VERSION=$(grep -Po '^version\s*=\s*\"\K.*?(?=\")' pyproject.toml)" >> $GITHUB_ENV + echo "Found version in pyproject.toml: ${{ env.PACKAGE_VERSION }}" + + - name: Check package version is PEP440 compliant + # This is only a partial PEP440 match, it just checks the major, minor + # patch, but not any optional suffixes. + run: echo "${{ env.PACKAGE_VERSION }}" | grep -P "^\d+\.\d+\.\d+.*$" + + - name: Check version matches release tag + run: | + if [ "${{ format('v{0}', env.PACKAGE_VERSION) }}" != "${{ github.event.release.tag_name }}" ] + then + echo "v\$\{PACKAGE_VERSION\} = v${{ env.PACKAGE_VERSION }} does not match tag name: ${{ github.event.release.tag_name }}" + exit 1 + fi + + - name: Setup Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install pypa/build and build + run: | + pip install build + python -m build --sdist --wheel --outdir dist/ . + + - name: Publish to PyPI + if: startsWith(github.ref, 'refs/tags') + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_TOKEN }} + + - name: Configure git + run: | + git config --local user.name "GitHub Actions Bot" + git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" + - name: Deploy docs for release version to gh-pages branch + if: startsWith(github.ref, 'refs/tags/') + run: | + pip install .[docs] + mike deploy --push --update-aliases "${{ env.PACKAGE_VERSION }}" latest diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..5a3b131 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,60 @@ +name: tests + +on: + push: + branches: [main, test-me-*] + tags: + pull_request: + workflow_dispatch: + +jobs: + tests: + timeout-minutes: 10 + + strategy: + matrix: + include: + - os: ubuntu-latest + python: 3.8 + toxenv: py38 + - os: ubuntu-latest + python: 3.9 + toxenv: py39 + - os: ubuntu-latest + python: '3.10' + toxenv: py310 + - os: ubuntu-latest + python: '3.11' + toxenv: py311 + - os: ubuntu-latest + python: '3.12' + toxenv: py312 + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python ${{matrix.python}} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + + - name: Get pip cache dir + id: pip-cache-dir + run: echo "PIP_CACHE_DIR=$(pip cache dir)" >> $GITHUB_ENV + + - name: Use pip cache + id: pip-cache + uses: actions/cache@v4 + with: + path: ${{ env.PIP_CACHE_DIR }} + key: tests-${{ matrix.os }}-pip-${{ matrix.python }}-${{ hashFiles('pyproject.toml') }} + restore-keys: | + tests-${{ matrix.os }}-pip-${{ matrix.python }}- + + - name: Install Packages + run: python -mpip install --upgrade pip tox + + - name: Run Tox + run: tox -e ${{ matrix.toxenv }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6e4761 --- /dev/null +++ b/.gitignore @@ -0,0 +1,129 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..239bf63 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,31 @@ +ci: + autofix_prs: false +repos: + - repo: 'https://github.com/pre-commit/pre-commit-hooks' + rev: v4.5.0 + hooks: + - id: mixed-line-ending + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-added-large-files + - id: check-docstring-first + - id: check-json + - id: check-yaml + - id: check-merge-conflict + - id: name-tests-test + - repo: 'https://github.com/codespell-project/codespell' + rev: v2.2.6 + hooks: + - id: codespell + - repo: 'https://github.com/charliermarsh/ruff-pre-commit' + rev: v0.2.2 + hooks: + - id: ruff + args: + - '--fix' + - id: ruff-format + - repo: 'https://github.com/pre-commit/mirrors-mypy' + rev: v1.8.0 + hooks: + - id: mypy + additional_dependencies: [] diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..7a0f4a2 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,10 @@ +cff-version: 1.2.0 +message: If you use this software, please cite it as below. +authors: + - family-names: Pauloski + given-names: Greg + orcid: https://orcid.org/0000-0002-6547-6902 +license: MIT +repository-code: https://github.com/foobar-author/foobar +title: FooBar +url: https://foobar.dev diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7b7753f --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023-Present FooBar Author + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a101020 --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# Python Package Template Repo + +[![docs](https://github.com/gpauloski/python-template/actions/workflows/docs.yml/badge.svg)](https://github.com/gpauloski/python-template/actions) +[![tests](https://github.com/gpauloski/python-template/actions/workflows/tests.yml/badge.svg)](https://github.com/gpauloski/python-template/actions) +[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/gpauloski/python-template/main.svg)](https://results.pre-commit.ci/latest/github/gpauloski/python-template/main) + +Python package template repo that provides: +- Package, examples, and testing layout. +- GitHub PR and Issue templates. +- Example docs with MKDocs and GitHub Pages. +- CI framework with `pre-commit` and `tox`. +- GitHub actions for running tests and publishing packages. + +This package setup was based on [Anthony Sottile's project setup](https://www.youtube.com/watch?v=q8DkatMZvUs&list=PLWBKAf81pmOaP9naRiNAqug6EBnkPakvY) but deviates in some places (e.g., `pyproject.toml` and `ruff`). + +## Setup Instructions + +1. Click the "Use this template" button at the top right of this page. +2. Delete and directories you will not be using (commonly `docs/` if you do not want to use MKDocs or `examples/` if you will not have example code). +3. Follow the instructions to create the new repo then clone your repo locally. +4. The template uses "foobar" to indicate things that need to be changed. + Start by searching for all instances (`git grep foobar`) and changing them accordingly. +5. Configure pre-commit: + - Go to [https://pre-commit.ci/](https://pre-commit.ci/) and enable pre-commit on your repo. + - Update the pre-commit badge URL in this README with your new badge URL. +6. Configure GitHub pages: + - Go to the "Pages" section of your repository settings. + - Select "Deploy from a branch" and use the "gh-pages" branch. +7. Configure PyPI releases (if relevant): + - Create a new API token for [https://pypi.org/](https://pypi.org/). + - Add the token as a GitHub actions secret (see the instructions [here](https://github.com/pypa/gh-action-pypi-publish)). +8. Delete this boilerplate stuff in the README. +9. Commit and push changes. + +### GitHub Configuration + +I recommend making a few other changes to the repo's setting on GitHub. +- In "General" + - Select/deselect features you need/don't need. + - Select "Automatically delete head branches +- In "Branches": enable branch protection on `main`. + - Check "Require a pull request before merging" + - Check "Require status checks to pass before merging" + - Check "Require branches to be up to date before merging" + - Set required checks (e.g., pre-commit.ci, tests, etc.) + - Check "Do not allow bypassing the above settings" + +## Installation + +Install via pip: +``` +$ pip install foobar +``` + +For local development: +``` +$ tox --devenv venv -e py310 +$ pre-commit install +``` +or +``` +$ pip install -e . +``` + +## Additional README Sections + +... diff --git a/docs/_overrides/base.html b/docs/_overrides/base.html new file mode 100644 index 0000000..0af326a --- /dev/null +++ b/docs/_overrides/base.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block outdated %} + You're not viewing the latest version. + + Click here to go to latest. + +{% endblock %} diff --git a/docs/_templates/python/material/docstring/admonition.html b/docs/_templates/python/material/docstring/admonition.html new file mode 100644 index 0000000..2105eab --- /dev/null +++ b/docs/_templates/python/material/docstring/admonition.html @@ -0,0 +1,5 @@ +{{ log.debug("Rendering admonition") }} +
+ {{ section.title|convert_markdown(heading_level, html_id, strip_paragraph=True) }} + {{ section.value.contents|convert_markdown(heading_level, html_id) }} +
diff --git a/docs/_templates/python/material/function.html b/docs/_templates/python/material/function.html new file mode 100644 index 0000000..04f9fbd --- /dev/null +++ b/docs/_templates/python/material/function.html @@ -0,0 +1,74 @@ +{{ log.debug("Rendering " + function.path) }} + +
+{% with html_id = function.path %} + + {% if root %} + {% set show_full_path = config.show_root_full_path %} + {% set root_members = True %} + {% elif root_members %} + {% set show_full_path = config.show_root_members_full_path or config.show_object_full_path %} + {% set root_members = False %} + {% else %} + {% set show_full_path = config.show_object_full_path %} + {% endif %} + + {% if not root or config.show_root_heading %} + + {% filter heading(heading_level, + role="function", + id=html_id, + class="doc doc-heading", + toc_label=function.name ~ "()") %} + + {% if config.separate_signature %} + {% if show_full_path %}{{ function.path }}{% else %}{{ function.name }}{% endif %}() + {% else %} + {% filter highlight(language="python", inline=True) %} + {% if show_full_path %}{{ function.path }}{% else %}{{ function.name }}{% endif %}() + {% include "signature.html" with context %} + {% endfilter %} + {% endif %} + + {% with labels = function.labels %} + {% include "labels.html" with context %} + {% endwith %} + + {% endfilter %} + + {% if config.separate_signature %} + {% filter highlight(language="python", inline=False) %} + {% filter format_signature(function, config.line_length, crossrefs=config.signature_crossrefs) %} + {% if show_full_path %}{{ function.path }}{% else %}{{ function.name }}{% endif %} + {% include "signature.html" with context %} + {% endfilter %} + {% endfilter %} + {% endif %} + + {% else %} + {% if config.show_root_toc_entry %} + {% filter heading(heading_level, + role="function", + id=html_id, + toc_label=function.path if config.show_root_full_path else function.name, + hidden=True) %} + {% endfilter %} + {% endif %} + {% set heading_level = heading_level - 1 %} + {% endif %} + +
+ {% with docstring_sections = function.docstring.parsed %} + {% include "docstring.html" with context %} + {% endwith %} + + {% if config.show_source and function.source %} +
+ Source code in {{ function.relative_filepath }} + {{ function.source|highlight(language="python", linestart=function.lineno, linenums=True) }} +
+ {% endif %} +
+ +{% endwith %} +
diff --git a/docs/_templates/python/material/module.html b/docs/_templates/python/material/module.html new file mode 100644 index 0000000..682cf0b --- /dev/null +++ b/docs/_templates/python/material/module.html @@ -0,0 +1,67 @@ +{{ log.debug("Rendering " + module.path) }} + +
+{% with html_id = module.path %} + + {% if root %} + {% set show_full_path = config.show_root_full_path %} + {% set root_members = True %} + {% elif root_members %} + {% set show_full_path = config.show_root_members_full_path or config.show_object_full_path %} + {% set root_members = False %} + {% else %} + {% set show_full_path = config.show_object_full_path %} + {% endif %} + + {% if not root or config.show_root_heading %} + + {% filter heading(heading_level, + role="module", + id=html_id, + class="doc doc-heading", + toc_label=module.name) %} + + {% with module_name = module.path if show_full_path else module.name %} + {% if config.separate_signature %} + {{ module_name }} + {% else %} + {{ module_name }} + {% endif %} + {% endwith %} + + {% with labels = module.labels %} + {% include "labels.html" with context %} + {% endwith %} + + {% endfilter %} + + {% else %} + {% if config.show_root_toc_entry %} + {% filter heading(heading_level, + role="module", + id=html_id, + toc_label=module.path if config.show_root_full_path else module.name, + hidden=True) %} + {% endfilter %} + {% endif %} + {% set heading_level = heading_level - 1 %} + {% endif %} + +

+ Source file: {{ module.relative_package_filepath }} +

+ +
+ {% with docstring_sections = module.docstring.parsed %} + {% include "docstring.html" with context %} + {% endwith %} + + {% with obj = module %} + {% set root = False %} + {% set heading_level = heading_level + 1 %} + {% include "children.html" with context %} + {% endwith %} +
+ +{% endwith %} +
diff --git a/docs/contributing/index.md b/docs/contributing/index.md new file mode 100644 index 0000000..956e342 --- /dev/null +++ b/docs/contributing/index.md @@ -0,0 +1,86 @@ +## Getting Started for Local Development + +We recommend using [Tox](https://tox.wiki/en/latest/index.html){target=_blank} +to setup the development environment. This will create a new virtual +environment with all of the required packages installed +and FooBar installed in editable mode with the necessary extras options. + +```bash +$ git clone https://github.com/foobar-author/foobar +$ cd foobar +$ tox --devenv venv -e py310 +$ . venv/bin/activate +``` + +!!! warning + + Running Tox in a Conda environment is possible but it may conflict with + Tox's ability to find the correct Python versions. E.g., if your + Conda environment is Python 3.9, running `#!bash $ tox -e p38` may still use + Python 3.9. + +To install manually: +```bash +$ git clone https://github.com/foobar-author/foobar +$ cd foobar +$ python -m venv venv +$ . venv/bin/activate +$ pip install -e .[dev,docs] +``` + +## Continuous Integration + +FooBar uses [pre-commit](https://pre-commit.com/){target=_blank} and +[Tox](https://tox.wiki/en/latest/index.html){target=_blank} for continuous integration +(test, linting, etc.). + +### Linting and Type Checking (pre-commit) + +To use pre-commit, install the hook and then run against files. + +```bash +$ pre-commit install +$ pre-commit run --all-files +``` + +### Tests (tox) + +The entire CI workflow can be run with `#!bash $ tox`. +This will test against multiple versions of Python and can be slow. + +Module-level unit-test are located in the `tests/` directory and its +structure is intended to match that of `foobar/`. +E.g. the tests for `foobar/x/y.py` are located in +`tests/x/y_test.py`; however, additional test files can be added +as needed. Tests should be narrowly focused and target a single aspect of the +code's functionality, tests should not test internal implementation details of +the code, and tests should not be dependent on the order in which they are run. + +Code that is useful for building tests but is not a test itself belongs in the +`testing/` directory. + +```bash +# Run all tests in tests/ +$ tox -e py39 +# Run a specific test +$ tox -e py39 -- tests/x/y_test.py::test_z +``` + +## Docs + +If code changes require an update to the documentation (e.g., for function +signature changes, new modules, etc.), the documentation can be built using +MKDocs. + +```bash +# Manually +$ pip install -e .[docs] +$ mkdocs build --strict # Build only to site/index.html +$ mkdocs serve # Serve locally + +# With tox (will only build, does not serve) +$ tox -e docs +``` + +Docstrings are automatically generated, but it is recommended to check the +generated docstrings to make sure details/links/etc. are correct. diff --git a/docs/contributing/issues-pull-requests.md b/docs/contributing/issues-pull-requests.md new file mode 100644 index 0000000..27248c9 --- /dev/null +++ b/docs/contributing/issues-pull-requests.md @@ -0,0 +1,36 @@ +## Issues + +[Issue Tracker](https://github.com/foobar-author/foobar/issues){target=_blank} + +We use GitHub issues to report problems, request and track changes, and discuss +future ideas. +If you open an issue for a specific problem, please follow the template guides. + +## Pull Requests + +We use the standard GitHub contribution cycle where all contributions are +made via pull requests (including code owners!). + +1. Fork the repository and clone to your local machine. +2. Create local changes. + - Changes should conform to the style and testing guidelines, referenced + above. + - Preferred commit message format ([source](https://cbea.ms/git-commit/){target=_blank}): + * separate subject from body with a blank line, + * limit subject line to 50 characters, + * capitalize first word of subject line, + * do not end the subject line with a period, + * use the imperative mood for subject lines, + * include related issue numbers at end of subject line, + * wrap body at 72 characters, and + * use the body to explain what/why rather than how. + Example: `Fix concurrency bug in Store (#42)` +3. Push commits to your fork. + - Please squash commits fixing mistakes to keep the git history clean. + For example, if commit "b" follows commit "a" and only fixes a small typo + from "a", please squash "a" and "b" into a single, correct commit. + This keeps the commit history readable and easier to search through when + debugging (e.g., git blame/bisect). +4. Open a pull request in this repository. + - The pull request should include a description of the motivation for the + PR and included changes. A PR template is provided to guide this process. diff --git a/docs/contributing/releases.md b/docs/contributing/releases.md new file mode 100644 index 0000000..13ba765 --- /dev/null +++ b/docs/contributing/releases.md @@ -0,0 +1,33 @@ +## Release Timeline + +Releases are created on an as-needed basis. +Milestones are the [Issue Tracker](https://github.com/foobar-author/foobar/issues){target=_blank} are used to track features to be included in upcoming releases. + +## Creating Releases + +1. Choose the next version number, referred to as `{VERSION}` for the + rest of the instructions. Versioning follows semver + (`major.minor.patch`) with optional [PEP-440](https://peps.python.org/pep-0440){target=_blank} + pre-release/post-release/dev-release segments. Major/minor/patch numbers + start at 0 and pre-release/post-release/dev-release segments start at 1. +2. Update the version in `pyproject.toml` to `{VERSION}`. +3. Commit and merge the version updates/changelogs into main. +4. Tag the release commit and push (typically this is the commit updating the + version numbers). + ```bash + $ git tag -s v{VERSION} -m "FooBar v{VERSION}" + $ git push origin v{VERSION} + ``` + Note the version number is prepended by "v" for the tags so we can + distinguish release tags from non-release tags. +5. Create a new release on GitHub using the tag. The title should be + `FooBar v{VERSION}`. +6. **Official release:** + 1. Use the "Generate release notes" option and set the previous tag as the previous official release tag. E.g., for `v0.4.1`, the previous release tag should be `v0.4.0` and NOT `v0.4.1a1`. + 2. Add an "Upgrade Steps" section at the top (see previous releases for examples). + 3. Review the generated notes and edit as needed. PRs are organized by tag, but some PRs will be missing tags and need to be moved from the "Other Changes" section to the correct section. + 4. Select "Set as the latest release." +7. **Unofficial release:** (alpha/dev builds) + 1. Do NOT generate release notes. The body can be along the lines of "Development pre-prelease for `V{VERSION}`." + 2. Leave the previous tag as "auto." + 3. Select "Set as a pre-release." diff --git a/docs/contributing/style-guide.md b/docs/contributing/style-guide.md new file mode 100644 index 0000000..8dc71a3 --- /dev/null +++ b/docs/contributing/style-guide.md @@ -0,0 +1,22 @@ +The Python code and docstring format mostly follows Google's +[Python Style Guide](https://google.github.io/styleguide/pyguide.html){target=_blank}, +but the pre-commit config is the authoritative source for code format +compliance. + +**Nits:** + +* Avoid imports in `__init__.py` (reduces the likelihood of circular imports). +* Prefer pure functions where possible. +* Define all class attributes inside `__init__` so all attributes are visible + in one place. Attributes that are defined later can be set as `None` + as a placeholder. +* Prefer f-strings (`#!python f'name: {name}`) over string format + (`#!python 'name: {}'.format(name)`). Never use the `%` operator. +* Prefer [typing.NamedTuple][] over [collections.namedtuple][]. +* Use lower-case and no punctuation for log messages, but use upper-case and + punctuation for exception values. + ```python + logger.info(f'new connection opened to {address}') + raise ValueError('Name must contain alphanumeric characters only.') + ``` +* Document all exceptions that may be raised by a function in the docstring. diff --git a/docs/css/mkdocstrings.css b/docs/css/mkdocstrings.css new file mode 100644 index 0000000..eef576e --- /dev/null +++ b/docs/css/mkdocstrings.css @@ -0,0 +1,65 @@ +/* Don't capitalize names. */ +h5.doc-heading { + text-transform: none !important; +} + +/* Avoid breaking parameters name, etc. in table cells. */ +.doc-contents td code { + word-break: normal !important; +} + +/* For pieces of Markdown rendered in table cells. */ +.doc-contents td p { + margin-top: 0 !important; + margin-bottom: 0 !important; +} + +/* Max width for docstring sections tables. */ +.doc .md-typeset__table, +.doc .md-typeset__table table { + display: table !important; + width: 100%; +} +.doc .md-typeset__table tr { + display: table-row; +} + +/* Avoid line breaks in rendered fields. */ +.field-body p { + display: inline; +} + +/* Defaults in Spacy table style. */ +.doc-param-default { + float: right; +} + +span.doc.doc-object-name { + font-weight: 400; + font-family: var(--md-code-font-family); +} + +h2.doc.doc-heading { + border-bottom-style: solid; + border-color: var(--md-default-fg-color--lighter); + border-width: 2px; +} + +h3.doc.doc-heading { + border-bottom-style: dashed; + border-color: var(--md-default-fg-color--lighter); + border-width: 1px; +} + +span.doc.doc-object-name.doc-function-name { + font-style: normal; +} + +/* +div.doc.doc-object.doc-class { + border-bottom-style: solid; + border-top-style: solid; + border-color: var(--md-code-bg-color); + border-width: 1px; +} +*/ diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 0000000..4793c2a --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,12 @@ +# Examples + +## foobar + +foobar can be used as follows. + +```python +from foobar import foo + +foo.bar([1, 2, 3]) +# output: 6 +``` diff --git a/docs/generate_api.py b/docs/generate_api.py new file mode 100644 index 0000000..b32966e --- /dev/null +++ b/docs/generate_api.py @@ -0,0 +1,35 @@ +"""Generate the code reference pages and navigation.""" +from __future__ import annotations + +from pathlib import Path + +import mkdocs_gen_files + +nav = mkdocs_gen_files.Nav() + +PACKAGE_DIR = 'foobar/' + +for path in sorted(Path(PACKAGE_DIR).rglob('**/*.py')): + module_path = path.with_suffix('') + doc_path = path.relative_to(PACKAGE_DIR).with_suffix('.md') + full_doc_path = Path('api', doc_path) + + parts = tuple(module_path.parts) + parts = tuple('.'.join(parts[: i + 1]) for i in range(len(parts))) + + if parts[-1].endswith('__init__'): + parts = parts[:-1] + doc_path = doc_path.with_name('index.md') + full_doc_path = full_doc_path.with_name('index.md') + elif parts[-1].endswith('__main__'): + continue + + nav[parts] = doc_path.as_posix() + + with mkdocs_gen_files.open(full_doc_path, 'w') as fd: + fd.write(f'::: {parts[-1]}') + + mkdocs_gen_files.set_edit_path(full_doc_path, path) + +with mkdocs_gen_files.open('api/SUMMARY.md', 'w') as nav_file: + nav_file.writelines(nav.build_literate_nav()) diff --git a/docs/get-started.md b/docs/get-started.md new file mode 100644 index 0000000..7248f9c --- /dev/null +++ b/docs/get-started.md @@ -0,0 +1,19 @@ +# Quick Start + +foobar brief description. + +## Overview + +foobar detailed overview. + +## Installation + +```bash +$ pip install foobar +``` + +Documentation on installing for local development is provided in [Contributing](contributing/index.md). + +## Usage + +foobar usage example. diff --git a/docs/guides/example.md b/docs/guides/example.md new file mode 100644 index 0000000..cd39a7c --- /dev/null +++ b/docs/guides/example.md @@ -0,0 +1,4 @@ +Example Guide +############# + +Add your example guide here. diff --git a/docs/guides/index.md b/docs/guides/index.md new file mode 100644 index 0000000..e62250f --- /dev/null +++ b/docs/guides/index.md @@ -0,0 +1,4 @@ +# Guides + + +- [Examples](example.md) diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..612c7a5 --- /dev/null +++ b/docs/index.md @@ -0,0 +1 @@ +--8<-- "README.md" diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..9ffd745 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,5 @@ +# Installation + +```bash +$ pip install foobar +``` diff --git a/docs/known-issues.md b/docs/known-issues.md new file mode 100644 index 0000000..f12a337 --- /dev/null +++ b/docs/known-issues.md @@ -0,0 +1,4 @@ +# Known Issues + +- First known issue. +- Second known issue. diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..0a01927 --- /dev/null +++ b/examples/README.md @@ -0,0 +1 @@ +# foobar Examples diff --git a/examples/example.py b/examples/example.py new file mode 100644 index 0000000..7ba4134 --- /dev/null +++ b/examples/example.py @@ -0,0 +1,31 @@ +"""Example script that uses the foobar package.""" +from __future__ import annotations + +import argparse + +from foobar.foo import bar + + +def main() -> int: + """Script entry point.""" + parser = argparse.ArgumentParser() + parser.add_argument( + '-n', + '--numbers', + nargs='+', + type=int, + default=None, + help='List of integers to sum', + ) + args = parser.parse_args() + + result = bar(args.numbers) + + print(f'Input: {args.numbers}') + print(f'Result: {result}') + + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/foobar/__init__.py b/foobar/__init__.py new file mode 100644 index 0000000..776623a --- /dev/null +++ b/foobar/__init__.py @@ -0,0 +1,9 @@ +"""foobar package.""" +# It is recommended to not write code in the __init__.py because it is easy +# to introduce import cycles and code becomes harder to search for. +from __future__ import annotations + +import importlib.metadata as importlib_metadata +import sys + +__version__ = importlib_metadata.version('foobar') diff --git a/foobar/foo.py b/foobar/foo.py new file mode 100644 index 0000000..c75b83c --- /dev/null +++ b/foobar/foo.py @@ -0,0 +1,16 @@ +"""foo module.""" +from __future__ import annotations + + +def bar(data: list[int] | None = None) -> int | None: + """Sum list of integers. + + Args: + data (list[int], None): optional list of integers (default: None). + + Returns: + `None` if `data=None` else the integer sum of `data`. + """ + if data is not None: + return sum(data) + return None diff --git a/foobar/py.typed b/foobar/py.typed new file mode 100644 index 0000000..1c727cb --- /dev/null +++ b/foobar/py.typed @@ -0,0 +1 @@ +# PEP 561 marker file to indicate this package is typed diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..d182bef --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,158 @@ +site_name: Python Template +site_url: https://gpauloski.github.io/python-template +site_author: FooBar Author +site_description: Documentation for FooBar. + +repo_name: gpauloski/python-template +repo_url: https://github.com/gpauloski/python-template + +copyright: Copyright © 2023—Present FooBar Authors + +theme: + name: material + features: + - content.code.annotate + - content.code.copy + - navigation.path + - navigation.sections + - navigation.tabs + - navigation.top + - navigation.tracking + - search.highlight + - search.share + - search.suggest + - toc.follow + font: + text: Open Sans + code: Roboto Mono + palette: + # Palette toggle for automatic mode + - media: "(prefers-color-scheme)" + scheme: default + primary: red + accent: blue + toggle: + icon: material/brightness-auto + name: Switch to light mode + + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: red + accent: blue + toggle: + icon: material/brightness-7 + name: Switch to dark mode + + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: red + accent: blue + toggle: + icon: material/brightness-4 + name: Switch to system preference + overrides: docs/_overrides + # favicon: static/favicon.png + # icon: + # logo: logo + +watch: + - mkdocs.yml + - README.md + - docs/ + - foobar/ + +extra: + version: + default: latest + provider: mike + +extra_css: + - css/mkdocstrings.css + +markdown_extensions: + - admonition + - attr_list + - md_in_html + - toc: + permalink: true + - pymdownx.arithmatex: + generic: true + - pymdownx.betterem: + smart_enable: all + - pymdownx.details + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.keys + - pymdownx.magiclink: + repo_url_shorthand: true + user: foobar-author + repo: foobar + - pymdownx.mark + - pymdownx.smartsymbols + - pymdownx.snippets: + check_paths: true + - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true + - pymdownx.tasklist: + custom_checkbox: true + +nav: + - Home: + - Overview: index.md + - Installation: installation.md + - Get Started: get-started.md + - Examples: examples.md + - Known Issues: known-issues.md + - Changelog (GitHub): https://github.com/gpauloski/python-template/releases + - Guides: + - guides/index.md + - Example: guides/example.md + - API Reference: api/ + - Contributing: + - contributing/index.md + - Style Guide: contributing/style-guide.md + - Issues and Pull Requests: contributing/issues-pull-requests.md + - Releases: contributing/releases.md + +plugins: + - mike: + alias_type: symlink + canonical_version: latest + - gen-files: + scripts: + - docs/generate_api.py + - literate-nav: + nav_file: SUMMARY.md + - mkdocstrings: + custom_templates: docs/_templates + enable_inventory: true + handlers: + python: + setup_commands: + - import pytkdocs_tweaks + - pytkdocs_tweaks.main() + import: + - https://docs.python.org/3/objects.inv + options: + annotations_path: brief + docstring_section_style: list + docstring_style: google + inherited_members: yes + line_length: 60 + members_order: source + merge_init_into_class: yes + separate_signature: yes + show_root_members_full_path: no + show_object_full_path: no + show_root_full_path: yes + show_signature_annotations: yes + show_submodules: no + signature_crossrefs: yes + - search + - section-index diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a9e5aea --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,163 @@ +[build-system] +requires = ["setuptools>=64.0", "setuptools_scm"] +build-backend = "setuptools.build_meta" + +[project] +name = "foobar" +version = "0.1.0" +authors = [ + {name = "foobar author"}, + {email = "foobar@foobar.com"}, +] +description = "Foobar example package." +readme = "README.md" +requires-python = ">=3.8" +license = {text = "MIT"} +classifiers = [ + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: Implementation :: CPython", +] +dependencies = [ + "requests", +] + +[project.urls] +homepage = "https://github.com/foobar-author/foobar" +documentation = "https://foobar.readthedocs.io" +repository = "https://github.com/foobar-author/foobar" + +[project.optional-dependencies] +dev = [ + "covdefaults>=2.2", + "coverage", + "mypy", + "pre-commit", + "pytest", + "pytest-cov", + "ruff>=0.2.0", + "tox", + "virtualenv", +] +docs = [ + "black", + "mkdocs-gen-files", + "mkdocs-literate-nav", + "mkdocs-material==9.4.7", + "mkdocs-section-index", + "mkdocstrings==0.23.0", + "mkdocstrings-python==1.8.0", + "mike", +] + +[tool.codespell] +skip = """ +.git, +.github, +__pycache__, +build, +dist, +.*egg-info +""" + +[tool.coverage.run] +plugins = ["covdefaults"] +omit = ["examples"] + +[tool.mypy] +python_version = "3.10" +check_untyped_defs = true +disallow_any_generics = true +disallow_incomplete_defs = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_configs = true +warn_unused_ignores = true + +[[tool.mypy.overrides]] +module = "testing.*" +allow_incomplete_defs = true +allow_untyped_defs = true + +[[tool.mypy.overrides]] +module = "tests.*" +allow_incomplete_defs = true +allow_untyped_defs = true + +[tool.ruff] +line-length = 79 +target-version = "py38" + +[tool.ruff.format] +indent-style = "space" +quote-style = "single" + +[tool.ruff.lint] +# See all rules here: https://beta.ruff.rs/docs/rules +select = [ + # pyflakes + "F", + # pycodestyle + "E", + # mccabe + "C90", + # isort + "I", + # pep8-naming + "N", + # pydocstyle + "D", + # pyupgrade + "UP", + # flake8-2020 + "YTT", + # flake8-bugbear + "B", + # flake8-builtins + "A", + # flake8-commas + "COM", + # flake8-comprehensions + "C4", + # flake8-implicit-str-concat + "ISC", + # flake8-pytest-style + "PT", + # flake8-quotes + "Q", + # flake8-debugger + "T10", + # flake8-simplify + "SIM", + # PyLint + "PL", + # ruff-specific + "RUF", +] +extend-ignore = [] + +[tool.ruff.lint.flake8-pytest-style] +parametrize-values-type = "tuple" + +[tool.ruff.lint.flake8-quotes] +inline-quotes = "single" +multiline-quotes = "single" + +[tool.ruff.lint.isort] +force-single-line = true +known-first-party = ["foobar", "test", "testing"] +order-by-type = false +required-imports = ["from __future__ import annotations"] + +[tool.ruff.lint.per-file-ignores] +"*/__init__.py" = ["F401"] +"*/*_test.py" = ["D10"] + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.setuptools.packages.find] +exclude = ["tests*", "testing*"] +namespaces = false diff --git a/testing/README.md b/testing/README.md new file mode 100644 index 0000000..5931963 --- /dev/null +++ b/testing/README.md @@ -0,0 +1,4 @@ +# Testing infrastructure for foobar + +Code that is useful for creating and running tests but is not a test itself. +Test infrastructure may include items like mock object, test runners, example data, and more. diff --git a/testing/__init__.py b/testing/__init__.py new file mode 100644 index 0000000..7a15201 --- /dev/null +++ b/testing/__init__.py @@ -0,0 +1,2 @@ +"""Utilities for unit tests.""" +from __future__ import annotations diff --git a/testing/data.py b/testing/data.py new file mode 100644 index 0000000..a8d7b66 --- /dev/null +++ b/testing/data.py @@ -0,0 +1,4 @@ +"""Example data for testing.""" +from __future__ import annotations + +DATA: list[tuple[list[int], int]] = [([1, 2, 3, 4], 10)] diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..5c59416 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,8 @@ +# Tests for foobar + +Test files should take the form `*_test.py` and tests inside files should be top-level functions named `test_*()`. + +Tests should typically: +- test a single aspect of the code (e.g., to test feature `a` and `b`, use two separate test functions), +- only test the interface (e.g., tests should not check internal implementation details), and +- tests should not rely on the order in which they are executed. diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..00e4e8d --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,2 @@ +"""foobar unit tests.""" +from __future__ import annotations diff --git a/tests/bar_test.py b/tests/bar_test.py new file mode 100644 index 0000000..1efd58b --- /dev/null +++ b/tests/bar_test.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +import pytest + +from foobar.foo import bar +from testing.data import DATA + + +def test_bar_none_input() -> None: + assert bar() is None + assert bar(None) is None + + +@pytest.mark.parametrize( + ('data', 'total'), + ( + ([1], 1), + ([1, 2], 3), + ([1, 2, 3], 6), + ), +) +def test_bar_sum(data: list[int], total: int) -> None: + assert bar(data) == total + + d = DATA[0] + assert bar(d[0]) == d[1] diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..00c0db6 --- /dev/null +++ b/tox.ini @@ -0,0 +1,18 @@ +[tox] +envlist = py38, py39, py310, py311, py312, pre-commit, docs + +[testenv] +extras = dev +commands = + coverage erase + coverage run -m pytest {posargs} + coverage report + +[testenv:pre-commit] +skip_install = true +deps = pre-commit +commands = pre-commit run --all-files --show-diff-on-failure + +[testenv:docs] +extras = docs +commands = mkdocs build --strict