diff --git a/.gitignore b/.gitignore index c5fe006..e97c540 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ _ignore bin/ dev/ +_temp/ # Temporary files *.tmp diff --git a/.vscode/settings.json b/.vscode/settings.json index fc0bb11..346d266 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -30,6 +30,7 @@ "python.languageServer": "Pylance", "files.insertFinalNewline": true, "files.associations": { + "justfile.jinja": "plaintext", "*.yml.jinja": "jinja-yaml", "*.cff.jinja": "jinja-yaml", "*.toml.jinja": "jinja-toml", diff --git a/copier.yaml b/copier.yaml new file mode 100644 index 0000000..7bcd79b --- /dev/null +++ b/copier.yaml @@ -0,0 +1,109 @@ +_subdirectory: template + +# Post-copy commands: +_tasks: + # Add dev dependencies + - command: "git init -b main; uv add --dev pre-commit ruff typos pytest bandit commitizen genbadge jupyter pytest-cov quartodoc types-tabulate mypy vulture" + when: "{{ _copier_operation == 'copy' }}" + +# Message to show after generating or regenerating the project successfully +_message_after_copy: | + + Your project "{{ package_name }}" has been created successfully! + + Next steps: + + 1. Change directory to the project root: + + $ cd {{ _copier_conf.dst_path }} + + 2. Install the pre-commit hooks: + + $ just install-precommit + + 3. Install [`spaid`](https://github.com/seedcase-project/spaid) and run these commands to upload and configure your project on GitHub: + + $ spaid_gh_create_repo_from_local -h + $ spaid_gh_set_repo_settings -h + $ spaid_gh_ruleset_basic_protect_main -h + + 4. Configure GitHub following this + [guide](https://guidebook.seedcase-project.org/operations/security#using-github-apps-to-generate-tokens): + + - Install the [auto-release-token](https://github.com/apps/auto-release-token) + and [add-to-board-token](https://github.com/apps/add-to-board-token) GitHub Apps + - Create an `UPDATE_VERSION_TOKEN` and `ADD_TO_BOARD_TOKEN` secret for the GitHub Apps + - Create an `UPDATE_VERSION_APP_ID` and `ADD_TO_BOARD_APP_ID` variable of the GitHub Apps' IDs + + 5. List and complete all TODO items in the repository: + + $ just list-todos + +# Questions: +package_github_repo: + type: str + help: "What is or will be the GitHub repository spec for the project?" + placeholder: "user/repo" + validator: | + {% if package_github_repo and not (package_github_repo | regex_search('^[\w.-]+\/[\w.-]+$')) %} + Must be in the format `user/repo` and contain only alphanumeric characters and `_`, `-`, or `.`. + {% endif %} + +github_user: + type: str + default: "{{ package_github_repo.split('/')[0] if package_github_repo else '' }}" + when: false + +package_name: + type: str + help: "What is the name of the package?" + default: "{{ _copier_conf.dst_path | basename }}" + validator: | + {% if package_name and not (package_name | regex_search('^[\w.-]+$')) %} + Must contain only alphanumeric characters and `_`, `-`, or `.`. + {% endif %} + +package_name_snake_case: + type: str + default: "{{package_name | replace('-', '_') | replace('.', '_')}}" + when: false + +is_seedcase_project: + type: bool + help: "Is this package part of the Seedcase Project?" + default: "{{ github_user == 'seedcase-project' }}" + +homepage: + type: str + help: "What is the homepage of your project?" + default: "{{ 'https://%s.seedcase-project.org' % package_name if is_seedcase_project else '' }}" + +author_given_name: + type: str + help: "What is your first/given name?" + +author_family_name: + type: str + help: "What is your last/family name?" + +author_email: + type: str + help: "What is your email address?" + +review_team: + type: str + help: What GitHub team is responsible for reviewing pull requests? + default: "{{ '@%s/developers' % github_user if github_user else '' }}" + +github_board_number: + type: str + help: "What is the GitHub project board number to add issues and PRs to?" + validator: | + {% if github_board_number and not github_board_number.isdigit() %} + The board number must be an integer. + {% endif %} + +copyright_year: + type: str + default: "{{ copyright_year | default('%Y' | strftime) }}" + when: false diff --git a/docs/guide.qmd b/docs/guide.qmd index c1c60fb..57c42a3 100644 --- a/docs/guide.qmd +++ b/docs/guide.qmd @@ -136,6 +136,8 @@ Project, run: just update-quarto-theme ``` +Then set `seedcase-theme` as your project `type` in `_quarto.yml`. + This adds the `seedcase-theme` Quarto theme to the website, which provides a consistent look and feel across all Seedcase Project websites, including for Python package websites. diff --git a/justfile b/justfile index 0849692..9fdf690 100644 --- a/justfile +++ b/justfile @@ -2,10 +2,12 @@ just --list --unsorted @_checks: check-spelling check-commits +# Test Seedcase and non-Seedcase projects +@_tests: (test "true") (test "false") @_builds: build-contributors build-website build-readme # Run all build-related recipes in the justfile -run-all: update-quarto-theme update-template _checks test _builds +run-all: update-quarto-theme update-template _checks _tests _builds # Install the pre-commit hooks install-precommit: @@ -26,7 +28,7 @@ update-template: mkdir -p template/tools cp tools/get-contributors.sh template/tools/ cp .github/pull_request_template.md template/.github/ - cp .github/workflows/build-website.yml .github/workflows/dependency-review.yml template/.github/workflows/ + cp .github/workflows/dependency-review.yml template/.github/workflows/ # Check the commit messages on the current branch that are not on the main branch check-commits: @@ -45,9 +47,59 @@ check-spelling: uvx typos # Test and check that a Python package can be created from the template -# TODO: add test for copier -test: - echo "copier test" +test is_seedcase_project: + #!/bin/zsh + test_name="test-python-package" + test_dir="$(pwd)/_temp/{{ is_seedcase_project }}/$test_name" + template_dir="$(pwd)" + commit=$(git rev-parse HEAD) + rm -rf $test_dir + # vcs-ref means the current commit/head, not a tag. + uvx copier copy $template_dir $test_dir \ + --vcs-ref=$commit \ + --defaults \ + --trust \ + --data package_github_repo="first-last/repo" \ + --data is_seedcase_project={{ is_seedcase_project }} \ + --data author_given_name="First" \ + --data author_family_name="Last" \ + --data author_email="first.last@example.com" \ + --data review_team="@first-last/developers" \ + --data github_board_number=22 + # Run checks in the generated test Python package + cd $test_dir + git add . + git commit -m "test: initial copy" + just check-python check-spelling + # TODO: Find some way to test the `update` command + # Check that recopy works + echo "Testing recopy command -----------" + rm .cz.toml + git add . + git commit -m "test: preparing to recopy from the template" + uvx copier recopy \ + --vcs-ref=$commit \ + --defaults \ + --overwrite \ + --trust + # Check that copying onto an existing Python package works + echo "Using the template in an existing package command -----------" + rm .cz.toml .copier-answers.yml LICENSE.md + git add . + git commit -m "test: preparing to copy onto an existing package" + uvx copier copy \ + $template_dir $test_dir \ + --vcs-ref=$commit \ + --defaults \ + --trust \ + --overwrite \ + --data package_github_repo="first-last/repo" \ + --data is_seedcase_project={{ is_seedcase_project }} \ + --data author_given_name="First" \ + --data author_family_name="Last" \ + --data author_email="first.last@example.com" \ + --data review_team="@first-last/developers" \ + --data github_board_number=22 # Clean up any leftover and temporary build files cleanup: @@ -64,4 +116,4 @@ build-readme: # Generate a Quarto include file with the contributors build-contributors: - sh ./tools/get-contributors.sh seedcase-project/template-workshop + sh ./tools/get-contributors.sh seedcase-project/template-python-project diff --git a/template/.github/CODEOWNERS.jinja b/template/.github/CODEOWNERS.jinja new file mode 100644 index 0000000..9681e53 --- /dev/null +++ b/template/.github/CODEOWNERS.jinja @@ -0,0 +1,2 @@ +# All members on Developers team get added to review PRs +* {{ review_team }} diff --git a/template/.github/dependabot.yml b/template/.github/dependabot.yml new file mode 100644 index 0000000..3a50f8a --- /dev/null +++ b/template/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: uv + directory: / + schedule: + interval: monthly + versioning-strategy: increase-if-necessary + commit-message: + prefix: build + include: scope diff --git a/template/.github/workflows/add-to-project.yml.jinja b/template/.github/workflows/add-to-project.yml.jinja new file mode 100644 index 0000000..955b8d5 --- /dev/null +++ b/template/.github/workflows/add-to-project.yml.jinja @@ -0,0 +1,27 @@ +name: Add to project board + +on: + issues: + types: + - opened + - reopened + - transferred + pull_request: + types: + - reopened + - opened + +# Limit token permissions for security +permissions: read-all + +jobs: + add-to-project: + uses: seedcase-project/.github/.github/workflows/reusable-add-to-project.yml@main + permissions: + pull-requests: write + with: + board-number: {{ github_board_number }} + app-id: {{ '${{ vars.ADD_TO_BOARD_APP_ID }}' }} + secrets: + add-to-board-token: {{ '${{ secrets.ADD_TO_BOARD }}' }} + gh-token: {{ '${{ secrets.GITHUB_TOKEN }}' }} diff --git a/template/CITATION.cff.jinja b/template/CITATION.cff.jinja new file mode 100644 index 0000000..c03cb5b --- /dev/null +++ b/template/CITATION.cff.jinja @@ -0,0 +1,24 @@ +# TODO: Add title of Python package. +title: "" +# TODO: Add abstract of Python package. +abstract: "" +authors: + - family-names: {{ author_family_name }} + given-names: {{ author_given_name }} + # TODO: Add ORCID and affiliation for the author. + orcid: "" + affiliation: "" + # TODO: Add more authors as needed. + # - family-names: "" + # given-names: "" + # orcid: "" + # affiliation: "" +cff-version: 1.2.0 +# doi: +keywords: + # TODO: Add keywords relevant to the project. + - "" +license: MIT +message: "If you use this Python package, please cite it using these metadata." +repository-code: "https://github.com/{{ package_github_repo }}" +url: "{{ homepage }}" diff --git a/template/LICENSE.md.jinja b/template/LICENSE.md.jinja new file mode 100644 index 0000000..8b1ba0a --- /dev/null +++ b/template/LICENSE.md.jinja @@ -0,0 +1,21 @@ +# MIT License + +Copyright (c) {{ copyright_year }} {{ package_name }} authors + +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/template/README.md.jinja b/template/README.md.jinja new file mode 100644 index 0000000..ba17175 --- /dev/null +++ b/template/README.md.jinja @@ -0,0 +1,3 @@ + + +# {{ package_name }} diff --git a/template/README.qmd.jinja b/template/README.qmd.jinja new file mode 100644 index 0000000..117f38b --- /dev/null +++ b/template/README.qmd.jinja @@ -0,0 +1,3 @@ +# {{ package_name }} + + diff --git a/template/_quarto.yml.jinja b/template/_quarto.yml.jinja new file mode 100644 index 0000000..cf7c3ab --- /dev/null +++ b/template/_quarto.yml.jinja @@ -0,0 +1,75 @@ +project: + type: website + # Delete auto-generated files from `quartodoc` + post-render: rm -f docs/reference/*.qmd + render: + - "docs/*" + - "index.qmd" + +website: + # TODO: Fill in the title of the website. + title: "" + site-url: "{{ homepage }}" + repo-url: "https://github.com/{{ package_github_repo }}" + page-navigation: true + navbar: + pinned: true + title: false + {%- if is_seedcase_project %} + logo: "_extensions/seedcase-project/seedcase-theme/logos/navbar-logo-{{ package_name }}.svg" + logo-alt: "{{ package_name }} logo: Main page" + {%- else %} + # TODO: add logo + logo: "" + logo-alt: "{{ package_name }} logo: Main page" + {%- endif %} + left: + - text: "Guide" + href: docs/guide/index.qmd + - text: "Design" + href: docs/design/index.qmd + tools: + - icon: github + href: "https://github.com/{{ package_github_repo }}" + aria-label: "GitHub icon: Source code" + {% if is_seedcase_project -%} + - icon: house + href: "https://seedcase-project.org" + aria-label: "House icon: Main website for the Seedcase Project" + {%- endif %} + sidebar: + - id: design + pinned: true + style: "floating" + contents: + - text: "Design" + href: docs/design/index.qmd + - id: guide + contents: + - section: "Guide" + href: docs/guide/index.qmd + +quartodoc: + sidebar: "docs/reference/_sidebar.yml" + style: "pkgdown" + dir: "docs/reference" + package: "{{ package_name_snake_case }}" + parser: google + dynamic: true + renderer: + style: _renderer.py + table_style: description-list + show_signature_annotations: true + +metadata-files: + - docs/reference/_sidebar.yml + +format: + {{ "seedcase-theme-html" if is_seedcase_project else "html" }}: + include-before-body: + - "docs/site-counter.html" + +editor: + markdown: + wrap: 72 + canonical: true diff --git a/template/docs/site-counter.html.jinja b/template/docs/site-counter.html.jinja new file mode 100644 index 0000000..b92291e --- /dev/null +++ b/template/docs/site-counter.html.jinja @@ -0,0 +1,3 @@ + + diff --git a/template/index.qmd b/template/index.qmd new file mode 100644 index 0000000..2769a9f --- /dev/null +++ b/template/index.qmd @@ -0,0 +1,3 @@ +--- +title: "Welcome!" +--- diff --git a/template/justfile b/template/justfile.jinja similarity index 73% rename from template/justfile rename to template/justfile.jinja index 279c94f..ab72553 100644 --- a/template/justfile +++ b/template/justfile.jinja @@ -1,8 +1,16 @@ @_default: just --list --unsorted +@_checks: check-python check-unused check-security check-spelling check-commits +@_tests: test-python +@_builds: build-contributors build-website build-readme + # Run all build-related recipes in the justfile -run-all: install-deps format-python check-python check-unused test-python check-security check-spelling check-commits build-website +{%- if is_seedcase_project %} +run-all: install-deps update-quarto-theme format-python _checks _tests _builds +{%- else %} +run-all: install-deps format-python _checks _tests _builds +{%- endif %} # List all TODO items in the repository list-todos: @@ -16,7 +24,11 @@ install-precommit: uvx pre-commit run --all-files # Update versions of pre-commit hooks uvx pre-commit autoupdate - +{% if is_seedcase_project %} +# Update the Quarto seedcase-theme extension +update-quarto-theme: + quarto add seedcase-project/seedcase-theme --no-prompt +{% endif %} # Install Python package dependencies install-deps: uv sync --all-extras --dev @@ -92,3 +104,19 @@ check-unused: # There are some things should be ignored though, with the allowlist. # Create an allowlist with `vulture --make-allowlist` uv run vulture src/ tests/ **/vulture-allowlist.py + +# Re-build the README file from the Quarto version +build-readme: + uvx --from quarto quarto render README.qmd --to gfm + +# Generate a Quarto include file with the contributors +build-contributors: + sh ./tools/get-contributors.sh {{ package_github_repo }} + +# Check for and apply updates from the template +update-from-template: + uvx copier update --trust --defaults + +# Reset repo changes to match the template +reset-from-template: + uvx copier recopy --trust --defaults diff --git a/template/pyproject.toml.jinja b/template/pyproject.toml.jinja new file mode 100644 index 0000000..a12e4c8 --- /dev/null +++ b/template/pyproject.toml.jinja @@ -0,0 +1,30 @@ +[project] +name = "{{ package_name }}" +version = "0.1.0" +# TODO: Add a description of the package. +description = "" +authors = [ + {name = "{{ author_given_name }} {{ author_family_name }}", email = "{{ author_email }}" }, + # TODO: Add more authors as needed. +] +maintainers = [ + {name = "{{ author_given_name }} {{ author_family_name }}", email = "{{ author_email }}" }, + # TODO: Add more maintainers as needed. +] +readme = "README.md" +license = "MIT" +license-files = ["LICENSE.md"] +requires-python = ">=3.12" +dependencies = [] + +[project.urls] +homepage = "{{ homepage }}" +{%- if package_github_repo %} +repository = "https://github.com/{{ package_github_repo }}" +changelog = "https://github.com/{{ package_github_repo }}/blob/main/CHANGELOG.md" +issues = "https://github.com/{{ package_github_repo }}/issues" +{% endif %} + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/template/src/{{package_name_snake_case}}/__init__.py b/template/src/{{package_name_snake_case}}/__init__.py new file mode 100644 index 0000000..89c3d35 --- /dev/null +++ b/template/src/{{package_name_snake_case}}/__init__.py @@ -0,0 +1 @@ +"""Module containing all source code.""" diff --git a/template/src/{{package_name_snake_case}}/py.typed b/template/src/{{package_name_snake_case}}/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/template/tools/get-contributors.sh b/template/tools/get-contributors.sh new file mode 100644 index 0000000..40aea37 --- /dev/null +++ b/template/tools/get-contributors.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +# Get a list of contributors to this repository and save it to +# _contributors.qmd file (overwritten if it exists). It also: +# +# - Formats users into Markdown links to their GitHub profiles. +# - Removes any usernames with the word "bot" in them. +# - Removes the trailing comma from the list. +repo_spec=${1} +echo "These are the people who have contributed by submitting changes through pull requests :tada:\n\n" > _contributors.qmd +gh api \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /repos/$repo_spec/contributors \ + --template '{{range .}} [\@{{.login}}]({{.html_url}}){{"\n"}}{{end}}' | \ + grep -v "\[bot\]" | \ + tr '\n' ', ' | \ + sed -e 's/,$//' >> _contributors.qmd diff --git a/template/{{_copier_conf.answers_file}}.jinja b/template/{{_copier_conf.answers_file}}.jinja new file mode 100644 index 0000000..a8c521e --- /dev/null +++ b/template/{{_copier_conf.answers_file}}.jinja @@ -0,0 +1,2 @@ +# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY +{{ dict(_copier_answers, copyright_year=copyright_year) | to_nice_yaml -}}