diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..2767a3d
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,11 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directories:
+ - "/"
+ schedule:
+ interval: "weekly"
+ groups:
+ actions:
+ patterns:
+ - "*"
diff --git a/.github/workflows/oa_update.yml b/.github/workflows/oa_update.yml
new file mode 100644
index 0000000..246f2e2
--- /dev/null
+++ b/.github/workflows/oa_update.yml
@@ -0,0 +1,78 @@
+name: Automatic Update from OpenAstronomy/packging-guide
+permissions:
+ contents: write
+ pull-requests: write
+
+on:
+ # Allow manual runs through the web UI
+ workflow_dispatch:
+ schedule:
+ # ┌───────── minute (0 - 59)
+ # │ ┌───────── hour (0 - 23)
+ # │ │ ┌───────── day of the month (1 - 31)
+ # │ │ │ ┌───────── month (1 - 12 or JAN-DEC)
+ # │ │ │ │ ┌───────── day of the week (0 - 6 or SUN-SAT)
+ - cron: '0 7 * * 1' # Every Monday at 7am UTC
+
+jobs:
+ update:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: true
+
+ steps:
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
+ with:
+ fetch-depth: 0
+
+ - name: Add OpenAstronomy/packaging-guide remote
+ run: |
+ git remote add openastronomy https://github.com/OpenAstronomy/packaging-guide.git
+ git remote update
+
+ - name: Check if there are new commits in OpenAstronomy/packaging-guide
+ continue-on-error: false
+ id: check
+ run: |
+ CHANGES=0
+ if [ "$(git rev-list HEAD..openastronomy/main --count)" -gt 0 ]; then
+ CHANGES=1
+ fi
+ echo "has_changes=$CHANGES" >> "$GITHUB_OUTPUT"
+
+ - name: Merge in OpenAstronomy/packaging-guide
+ if: steps.check.outputs.has_changes == '1'
+ id: merge
+ continue-on-error: false
+ run: |
+ git config --global user.email "${{ github.actor }}@users.noreply.github.com"
+ git config --global user.name "${{ github.actor }}"
+
+ git merge --no-edit --no-commit openastronomy/main || true
+ if [[ -n $(git ls-files --unmerged) ]]; then
+ CONFLICTS=1
+ # Commit the unresolved conflicts
+ git add .
+ git commit -m "Merge branch 'OpenAstronomy/main' into main (with unresolved conflicts)"
+ else
+ CONFLICTS=0
+ # No conflicts, commit the merge
+ git commit -m "Merge branch 'OpenAstronomy/main' into main"
+ fi
+ echo "has_conflicts=$CONFLICTS" >> "$GITHUB_OUTPUT"
+
+ - name: Create pull request
+ if: steps.check.outputs.has_changes == '1'
+ uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ draft: ${{ steps.merge.outputs.has_conflicts == '1' }}
+ delete-branch: true
+ branch-suffix: timestamp
+ title: "Updates from OpenAstronomy/packaging-guide"
+ body: |
+ This is an autogenerated PR, which merges commits from OpenAstronomy/packaging-guide.
+
+ ${{ steps.merge.outputs.has_conflicts == '1' && '**These changes have conflicts that need to be manually resolved.**' || '' }}
+
+ *Do not squash merge this PR! All commits from upstream should be present in the repos history.*
diff --git a/.github/workflows/pre-commit-update.yaml b/.github/workflows/pre-commit-update.yaml
new file mode 100644
index 0000000..f76a0ed
--- /dev/null
+++ b/.github/workflows/pre-commit-update.yaml
@@ -0,0 +1,45 @@
+name: Update template pre-commit
+permissions:
+ contents: write
+ pull-requests: write
+
+on:
+ # Allow manual runs through the web UI
+ workflow_dispatch:
+ schedule:
+ # ┌───────── minute (0 - 59)
+ # │ ┌───────── hour (0 - 23)
+ # │ │ ┌───────── day of the month (1 - 31)
+ # │ │ │ ┌───────── month (1 - 12 or JAN-DEC)
+ # │ │ │ │ ┌───────── day of the week (0 - 6 or SUN-SAT)
+ - cron: '0 7 1 * *' # First day of the month at 7am UTC
+
+jobs:
+ update:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: true
+
+ steps:
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
+ with:
+ fetch-depth: 0
+
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
+
+ - name: Install pre-commit
+ run: pip install pre-commit
+
+ - name: Run pre-commit update
+ id: check
+ run: pre-commit autoupdate -c '{{ cookiecutter.package_name }}/.pre-commit-config.yaml'
+
+ - name: Create pull request
+ uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ delete-branch: true
+ branch-suffix: timestamp
+ title: "pre-commit updates in the template"
+ body: |
+ This is an autogenerated PR, which updates the `{{ cookiecutter.package_name }}/.pre-commit-config.yaml` file.
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index b2efc55..23e4ea2 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -12,10 +12,18 @@ concurrency:
jobs:
test:
- uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@v1
+ uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@28e947497bed4d6ec3fa1d66d198e95a1d17bc63 # v2.2.1
with:
envs: |
- macos: py311-test
- linux: py312-test
- linux: py313-test
- linux: build_docs
+
+ bake_cookies:
+ uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@28e947497bed4d6ec3fa1d66d198e95a1d17bc63 # v2.2.1
+ with:
+ artifact-path: |
+ cookies
+ envs: |
+ - linux: bake_cookies
diff --git a/.gitignore b/.gitignore
index 5e581a5..0f21137 100644
--- a/.gitignore
+++ b/.gitignore
@@ -143,3 +143,4 @@ dmypy.json
# Just in case you want to test out the defaults in the repo
packagename
None/
+cookies
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 03fc95c..ef1df75 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -14,3 +14,5 @@ repos:
exclude: "{{ cookiecutter.package_name }}"
- id: debug-statements
exclude: "{{ cookiecutter.package_name }}"
+ci:
+ autofix_prs: false
diff --git a/README.rst b/README.rst
index a75ef86..a9d552a 100644
--- a/README.rst
+++ b/README.rst
@@ -1,5 +1,10 @@
-OpenAstronomy Packaging Guide
-=============================
+SunPy Package Template
+======================
-The `OpenAstronomy Python Packaging Guide `__ that contains an embedded `cookiecutter `__ template.
-Please see also the `instructions for using the template `__.
+This repo extends the `OpenAstronomy Python Packaging Guide `__ to add SunPy specific features such as config files which are shared over all of SunPy's packages.
+
+Previewing Rendered Templates
+-----------------------------
+
+To make it easier to preview changes to files when rendering the template there is a tox environment named ``bake_cookies``.
+If you run ``tox -e bake_cookies`` a new directory named ``cookies`` will be present with a variety of test renders of the template in (see ``tests/conftest.py`` for the configuration options).
diff --git a/cookiecutter.json b/cookiecutter.json
index d370543..0a9a1d2 100644
--- a/cookiecutter.json
+++ b/cookiecutter.json
@@ -5,6 +5,12 @@
"author_name": "",
"author_email": "",
"project_url": "",
+ "github_repo": "",
+ "sourcecode_url": "{{ 'https://github.com/' + cookiecutter.github_repo if cookiecutter.github_repo else '' }}",
+ "download_url": "https://pypi.org/project/{{ cookiecutter.package_name }}",
+ "documentation_url": "",
+ "changelog_url": "",
+ "issue_tracker_url": "{{ 'https://github.com/' + cookiecutter.github_repo + '/issues/' if cookiecutter.github_repo else '' }}",
"license": [
"BSD 3-Clause",
"GNU GPL v3+",
@@ -22,12 +28,22 @@
"enable_dynamic_dev_versions": "n",
"include_example_code": "n",
"include_cruft_update_github_workflow": "n",
- "_sphinx_theme": "alabaster",
+ "use_extended_ruff_linting": "n",
+ "_sphinx_theme": "sunpy",
"_parent_project": "",
"_install_requires": "",
"_copy_without_render": [
"docs/_templates",
"docs/_static",
".github/workflows/sub_package_update.yml"
- ]
+ ],
+ "__prompts__" : {
+ "project_url": "Primary website for the project, leave blank for Sunpy Homepage",
+ "github_repo": "user/repo format GitHub repository. Leave blank if project is not on GitHub.",
+ "sourcecode_url": "URL for source code, i.e. https://github.com/...",
+ "download_url": "PyPI address for the project, optional",
+ "documentation_url": "Documentation URL, optional",
+ "changelog_url": "URL to the changelog, optional",
+ "issue_tracker_url": "URL to the issue tracker, optional"
+ }
}
diff --git a/docs/conf.py b/docs/conf.py
index f9336f3..3f0987d 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -36,20 +36,15 @@
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+# Treat everything in single ` as a Python reference.
+default_role = 'py:obj'
+
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
-html_theme = 'alabaster'
-
-html_theme_options = {
- "description": "A generic Python packaging guide and template.",
- "code_font_family": "'Fira Code', monospace",
- "github_user": "OpenAstronomy",
- "github_repo": "packaging-guide",
- "sidebar_width": "300px"
-}
+html_theme = 'sunpy'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
diff --git a/docs/index.rst b/docs/index.rst
index 1c93b13..70ae5eb 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -27,7 +27,7 @@ To create a new package based on the template run:
.. code-block:: console
$ pip install cookiecutter cruft
- $ cruft create https://github.com/OpenAstronomy/packaging-guide
+ $ cruft create https://github.com/sunpy/package-template
and go through the steps offered in the cli naming your package and filling in your details.
Cruft is built on cookiecutter, and enables the updating of the template from the source.
@@ -42,7 +42,7 @@ If you would like to stick to simply the cookiecutter approach, the template sti
.. code-block:: console
$ pip install cookiecutter
- $ cookiecutter gh:OpenAstronomy/packaging-guide -o ./output_directory
+ $ cookiecutter gh:sunpy/package-template -o ./output_directory
This will create a new directory in your current directory named the same as the value of "packagename" you supplied.
Change into this directory and run ``git init`` to make it into a git repository, and make an initial commit.
@@ -54,3 +54,49 @@ The template currently implements the following optional flags, all of which def
* ``include_example_code``: This option will fill your new package with some example functions to allow you to test it.
* ``use_compiled_extensions``: This turns on the features needed to support compiled extensions as described in :ref:`extensions`.
* ``enable_dynamic_dev_versions``: This enables a feature which ensures that ``my_package.__version__`` always returns the current git version as calculated by ``setuptools_scm`` when the package is installed as an editable install. See :ref:`dev-versions` for more details.
+* ``include_cruft_update_github_repo``: This option adds a github workflow with pulls in the latest changes from the template every Monday morning and creates a PR against the repo which can then be accepted or closed.
+* ``use_extended_ruff_linting``: This option flag enables the stricter ruff rules. Recommend `Y` on creation of a new project.
+
+Pre-commit
+==========
+
+Pre-commit is configured through ``.pre-commit-config.yaml`` and can be installed locally and ran:
+
+.. code-block:: bash
+
+ $ pre-commit run --all-files
+
+It also possible to use the tox environment to run it and and is integrated into the CI.
+
+Within ``.pre-commit-config.yaml``, there are several tools and each one is configured either within the ``.pre-commit-config.yaml`` or for larger tools like ruff, it is has a dedicated config file ``.ruff.toml`` and we strongly recommend using the full set of rules when you setup your package.
+
+Updating to a new version of the template
+=========================================
+
+It will be simplest to updating a package to a newer template by waiting for the GitHub workflow to trigger.
+This will trigger a pull request one can review and merge via GitHub's UI.
+
+If you do not want to wait, or want to do it manually, you will have to use cruft's CLI.
+For this you will need to install cruft locally and then you can check the status of the package by running:
+
+.. code-block:: console
+
+ $ cruft check
+
+This will let you know whether the repository is up to date or not.
+From there, you can run:
+
+.. code-block:: console
+
+ $ cruft update
+
+To update the repo and if there is a case conflicting files, ``.rej`` files will be created and you will have to manually deal with them and merge changes.
+
+If you need up update one of the variables in the package ``.cruft.json`` for example, changing a ``n`` to a ``y`` this can be done using:
+
+.. code-block:: console
+
+ $ cruft update --variables-to-update '{"use_extended_ruff_linting": "y"}'
+
+This will work through the codebase and include the desired functionality without any further action.
+Then you can commit and push the resulting changes.
diff --git a/docs/rtd_environment.yaml b/docs/rtd_environment.yaml
index f0fee16..a3b2a37 100644
--- a/docs/rtd_environment.yaml
+++ b/docs/rtd_environment.yaml
@@ -1,8 +1,10 @@
-name: rtd311
+name: rtd313
channels:
- conda-forge
- defaults
dependencies:
- - python=3.11
+ - python=3.13
- pip
- graphviz
+ - pip:
+ - sunpy-sphinx-theme
diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py
index 94ff8ab..c56e114 100644
--- a/hooks/post_gen_project.py
+++ b/hooks/post_gen_project.py
@@ -37,7 +37,7 @@ def process_version(enable_dynamic_dev_versions):
def process_github_workflow(include_cruft_update_github_workflow):
if include_cruft_update_github_workflow != "y":
- remove_dir(os.path.join(PROJECT_DIRECTORY, '.github'))
+ remove_file(os.path.join(PROJECT_DIRECTORY, '.github', 'workflows', 'sub_package_update.yml'))
if __name__ == '__main__':
diff --git a/tests/conftest.py b/tests/conftest.py
index 6f27dfa..6d3f32d 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,8 +1,8 @@
-import py
+import py # noqa: F401
import pytest
-import tox
-from tox.pytest import init_fixture
+import tox # noqa: F401
+from tox.pytest import init_fixture # noqa: F401
@pytest.fixture(params=["bake_default",
@@ -25,7 +25,8 @@ def cookiejar_no_examples(request):
@pytest.fixture(params=["bake_examples_compiled",
"bake_examples",
- "bake_examples_compiled_dev_version"])
+ "bake_examples_compiled_dev_version",
+ "bake_examples_url_extensions"])
def cookiejar_examples(request):
"""
Templates rendered with the examples
@@ -64,8 +65,11 @@ def bake_examples(cookies):
"""
Examples on.
"""
- result = cookies.bake(extra_context={"include_example_code": "y",
- "author_name": "test"})
+ result = cookies.bake(extra_context={"package_name": "example-package",
+ "module_name": "example_package",
+ "include_example_code": "y",
+ "author_name": "test",
+ "use_extended_ruff_linting": "y"})
return _handle_cookiecutter_errors(result)
@@ -76,7 +80,8 @@ def bake_examples_compiled(cookies):
"""
result = cookies.bake(extra_context={"include_example_code": "y",
"use_compiled_extensions": "y",
- "author_name": "test"})
+ "author_name": "test",
+ "use_extended_ruff_linting": "n"})
return _handle_cookiecutter_errors(result)
@@ -88,5 +93,33 @@ def bake_examples_compiled_dev_version(cookies):
result = cookies.bake(extra_context={"include_example_code": "y",
"use_compiled_extensions": "y",
"enable_dynamic_dev_versions": "y",
- "author_name": "test"})
+ "author_name": "test",
+ "include_cruft_update_github_workflow": "y",
+ "use_extended_ruff_linting": "y",
+ "download_url": "https://github.com/sunpy/sunpy/archive/master.zip",
+ "issue_tracker_url": "https://github.com/sunpy/sunpy/issues",
+ })
return _handle_cookiecutter_errors(result)
+
+
+@pytest.fixture
+def bake_examples_url_extensions(cookies):
+ """
+ setting url parameters
+ """
+ result = cookies.bake(extra_context={
+ "author_name": "test",
+ "include_example_code": "y",
+ "project_url": "https://sunpy.org",
+ "github_repo": "sunpy/sunpy",
+ "download_url": "https://github.com/sunpy/sunpy/archive/master.zip",
+ "documentation_url": "https://sunpy.org/docs",
+ "changelog_url": "https://sunpy.org/changelog",
+ "issue_tracker_url": "https://github.com/sunpy/sunpy/issues"
+ })
+ return _handle_cookiecutter_errors(result)
+
+
+def pytest_addoption(parser):
+ # Add support for saving out rendered cookies to a specific location for inspection
+ parser.addoption("--cookie-location", action="store", default=None)
diff --git a/tests/test_build_artifacts.py b/tests/test_build_artifacts.py
new file mode 100644
index 0000000..7ebd631
--- /dev/null
+++ b/tests/test_build_artifacts.py
@@ -0,0 +1,26 @@
+"""
+This test file let's you render the examples we use in the tests to a given
+directory for ease of inspection.
+This means that you can check things like whitespace or other issues in a rendered example.
+"""
+import pytest
+import shutil
+from pathlib import Path
+
+
+@pytest.mark.parametrize("bake_name",
+ ["bake_default",
+ "bake_examples",
+ "bake_examples_compiled",
+ "bake_examples_compiled_dev_version",
+ "bake_examples_url_extensions"])
+def test_render_template(request, bake_name, pytestconfig):
+ bake = request.getfixturevalue(bake_name)
+ cached_dir = pytestconfig.getoption("--cookie-location")
+ if not cached_dir:
+ pytest.skip("No cookie location provided skipping render")
+
+ target_dir = Path(cached_dir) / bake_name.removeprefix("bake_")
+ if target_dir.exists():
+ shutil.rmtree(target_dir)
+ shutil.copytree(bake.project_path, target_dir)
diff --git a/tests/test_hooks.py b/tests/test_hooks.py
index 8c299ad..3b980e9 100644
--- a/tests/test_hooks.py
+++ b/tests/test_hooks.py
@@ -37,7 +37,7 @@ def test_examples_present(cookiejar_examples):
example_files.append("example_c.pyx")
for afile in example_files:
- assert (cj.project_path / ctx['package_name'] / afile).exists()
+ assert (cj.project_path / ctx['module_name'] / afile).exists()
@pytest.mark.parametrize("license, lfile", [
@@ -78,3 +78,33 @@ def test_other_licence(cookies):
for name, lfile in license_files.items():
assert (cj.project_path / "licenses" / lfile).exists()
+
+
+def test_cruft_update_exists(bake_examples_compiled_dev_version):
+ cj = bake_examples_compiled_dev_version
+
+ present_files = [
+ ".github/workflows/sub_package_update.yml",
+ ".github/workflows/ci.yml",
+ ]
+
+ for afile in present_files:
+ assert (cj.project_path / afile).exists()
+
+
+def test_cruft_update_absent(bake_examples_compiled):
+ cj = bake_examples_compiled
+
+ present_files = [
+ ".github/workflows/ci.yml"
+ ]
+
+ absent_files = [
+ ".github/workflows/sub_package_update.yml"
+ ]
+
+ for afile in present_files:
+ assert (cj.project_path / afile).exists()
+
+ for afile in absent_files:
+ assert not (cj.project_path / afile).exists()
diff --git a/tools/batchpr_manual_cruft_update.py b/tools/batchpr_manual_cruft_update.py
new file mode 100755
index 0000000..825c421
--- /dev/null
+++ b/tools/batchpr_manual_cruft_update.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python3
+# /// script
+# requires-python = ">=3.10"
+# dependencies = [
+# "typer",
+# "batchpr@git+https://github.com/astrofrog/batchpr@main",
+# "cruft@git+https://github.com/Cadair/cruft@patch-p1",
+# ]
+# ///
+"""
+This script runs cruft update on all templated sponsored affiliated packages and opens PRs.
+
+The main usecase for this script is when the CI files have been updated as
+Github actions can't modify the Github actions workflow files.
+
+You can run this script with:
+
+ pipx run ./all_update.py --help
+"""
+
+import os
+import json
+from typing import Annotated, Optional
+
+from batchpr import Updater
+from cruft import update
+import typer
+
+
+ALL_REPOS = (
+ "sunpy/sunpy",
+ "sunpy/ndcube",
+ "sunpy/sunkit-magex",
+ "sunpy/streamtracer",
+ "sunpy/sunkit-dem",
+ "sunpy/mpl-animators",
+ "sunpy/sunkit-image",
+ "sunpy/sunkit-pyvista",
+ "sunpy/sunpy-soar",
+ "sunpy/sunkit-instruments",
+ "sunpy/drms",
+ "sunpy/sunraster",
+ "sunpy/sunkit-spex",
+ "sunpy/radiospectra",
+)
+
+
+class CruftUpdater(Updater):
+ def __init__(self, token, author_name=None, author_email=None,
+ dry_run=False, verbose=False, cleanup_remote_branch=False, extra_context=None):
+ super().__init__(token, author_name, author_email, dry_run, verbose)
+ self.cleanup_remote_branch = cleanup_remote_branch
+ self.extra_context = extra_context
+
+ def process_repo(self):
+ if self.cleanup_remote_branch:
+ out = self.run_command(f"git ls-remote --heads origin {self.branch_name}")
+ if out:
+ self.run_command(f'git push https://{self.user.login}:{self.token}@github.com/{self.fork.full_name} :{self.branch_name}')
+ ret = update(skip_apply_ask=True, refresh_private_variables=True, extra_context=self.extra_context)
+ if not ret:
+ self.error(f"Cruft update failed for {self.repo}")
+ return False
+ self.add(".")
+ return True
+
+ @property
+ def commit_message(self):
+ return "Update cruft with batchpr"
+
+ @property
+ def branch_name(self):
+ return 'cruft-manual-update'
+
+ @property
+ def pull_request_title(self):
+ return "Updates from package template"
+
+ @property
+ def pull_request_body(self):
+ return "This PR has been generated by a script, it should update the repo with the latest changes from the package template."
+
+
+def run_multi_updater(
+ github_token: Annotated[str, typer.Option(envvar="GITHUB_TOKEN")],
+ repos: Annotated[list[str], typer.Option()] = ALL_REPOS,
+ cleanup_remote_branch: bool = False,
+ extra_context: str = None,
+ dry_run: bool = False,
+ verbose: bool = False
+):
+ """
+ Run the Cruft Updater script against all repos.
+
+ The GITHUB_TOKEN should be a Personal Access Token (classic) with the workflow permission and public_repo permissions.
+ """
+ extra_context = json.loads(extra_context) if extra_context else extra_context
+ helper = CruftUpdater(token=os.environ["GITHUB_TOKEN"], dry_run=dry_run, verbose=verbose, cleanup_remote_branch=cleanup_remote_branch, extra_context=extra_context)
+ for repo in repos:
+ helper.run(repo)
+
+
+if __name__ == "__main__":
+ typer.run(run_multi_updater)
diff --git a/tox.ini b/tox.ini
index 1d19c54..6cb36d4 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,7 @@
[tox]
envlist =
- py{310,311,312}-test
+ py{310,311,312,313,314}-test
+ bake_cookies
build-docs
[testenv]
@@ -22,7 +23,8 @@ deps =
pytest-mock
commands =
- pytest tests/ {posargs}
+ !bake_cookies: pytest tests/ {posargs}
+ bake_cookies: pytest tests/test_build_artifacts.py --cookie-location cookies
[testenv:build_docs]
description = invoke sphinx-build to build the HTML docs
@@ -30,5 +32,6 @@ change_dir =
docs
deps =
sphinx
+ sunpy-sphinx-theme
commands =
sphinx-build -W -b html . _build/html {posargs}
diff --git a/{{ cookiecutter.package_name }}/.codecov.yaml b/{{ cookiecutter.package_name }}/.codecov.yaml
new file mode 100644
index 0000000..8fe09b7
--- /dev/null
+++ b/{{ cookiecutter.package_name }}/.codecov.yaml
@@ -0,0 +1,11 @@
+comment: off
+coverage:
+ status:
+ project:
+ default:
+ threshold: 0.2%
+
+codecov:
+ require_ci_to_pass: false
+ notify:
+ wait_for_ci: true
diff --git a/{{ cookiecutter.package_name }}/.codespellrc b/{{ cookiecutter.package_name }}/.codespellrc
new file mode 100644
index 0000000..042a14e
--- /dev/null
+++ b/{{ cookiecutter.package_name }}/.codespellrc
@@ -0,0 +1,13 @@
+[codespell]
+skip = *.asdf,*.fits,*.fts,*.header,*.json,*.xsh,*cache*,*egg*,*extern*,.git,.idea,.tox,_build,*truncated,*.svg,.asv_env,.history
+ignore-words-list =
+ alog,
+ nd,
+ nin,
+ observ,
+ ot,
+ te,
+ upto,
+ afile,
+ precessed,
+ precess
diff --git a/{{ cookiecutter.package_name }}/.coveragerc b/{{ cookiecutter.package_name }}/.coveragerc
new file mode 100644
index 0000000..17874e8
--- /dev/null
+++ b/{{ cookiecutter.package_name }}/.coveragerc
@@ -0,0 +1,30 @@
+[run]
+omit =
+ {{ cookiecutter.module_name }}/conftest.py
+ {{ cookiecutter.module_name }}/*setup_package*
+ {{ cookiecutter.module_name }}/extern/*
+ {{ cookiecutter.module_name }}/version*
+ */{{ cookiecutter.module_name }}/conftest.py
+ */{{ cookiecutter.module_name }}/*setup_package*
+ */{{ cookiecutter.module_name }}/extern/*
+ */{{ cookiecutter.module_name }}/version*
+
+[report]
+exclude_lines =
+ # Have to re-enable the standard pragma
+ pragma: no cover
+ # Don't complain about packages we have installed
+ except ImportError
+ # Don't complain if tests don't hit assertions
+ raise AssertionError
+ raise NotImplementedError
+ # Don't complain about script hooks
+ def main(.*):
+ # Ignore branches that don't pertain to this version of Python
+ pragma: py{ignore_python_version}
+ # Don't complain about IPython completion helper
+ def _ipython_key_completions_
+ # typing.TYPE_CHECKING is False at runtime
+ if TYPE_CHECKING:
+ # Ignore typing overloads
+ @overload
diff --git a/{{ cookiecutter.package_name }}/.flake8 b/{{ cookiecutter.package_name }}/.flake8
new file mode 100644
index 0000000..2d17646
--- /dev/null
+++ b/{{ cookiecutter.package_name }}/.flake8
@@ -0,0 +1,27 @@
+[flake8]
+ignore =
+ # missing-whitespace-around-operator
+ E225
+ # missing-whitespace-around-arithmetic-operator
+ E226
+ # line-too-long
+ E501
+ # unused-import
+ F401
+ # undefined-local-with-import-star
+ F403
+ # redefined-while-unused
+ F811
+ # Line break occurred before a binary operator
+ W503,
+ # Line break occurred after a binary operator
+ W504
+max-line-length = 110
+exclude =
+ .git
+ __pycache__
+ docs/conf.py
+ build
+ {{ cookiecutter.package_name }}/__init__.py
+rst-directives =
+ plot
diff --git a/{{ cookiecutter.package_name }}/.github/workflows/ci.yml b/{{ cookiecutter.package_name }}/.github/workflows/ci.yml
new file mode 100644
index 0000000..bfa6c9d
--- /dev/null
+++ b/{{ cookiecutter.package_name }}/.github/workflows/ci.yml
@@ -0,0 +1,115 @@
+# Main CI Workflow {%- set default_python = '3.12' %}
+name: CI
+
+on:
+ push:
+ branches:
+ - 'main'
+ - '*.*'
+ - '!*backport*'
+ tags:
+ - 'v*'
+ - '!*dev*'
+ - '!*pre*'
+ - '!*post*'
+ pull_request:
+ # Allow manual runs through the web UI
+ workflow_dispatch:
+ schedule:
+ # ┌───────── minute (0 - 59)
+ # │ ┌───────── hour (0 - 23)
+ # │ │ ┌───────── day of the month (1 - 31)
+ # │ │ │ ┌───────── month (1 - 12 or JAN-DEC)
+ # │ │ │ │ ┌───────── day of the week (0 - 6 or SUN-SAT)
+ - cron: '0 7 * * 3' # Every Wed at 07:00 UTC
+
+concurrency:
+ group: {{ '${{ github.workflow }}-${{ github.ref }}' }}
+ cancel-in-progress: true
+
+jobs:
+ core:
+ uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@v2
+ with:
+ submodules: false
+ coverage: codecov
+ toxdeps: tox-pypi-filter
+ envs: |
+ - linux: py313
+ secrets:
+ CODECOV_TOKEN: {{ '${{ secrets.CODECOV_TOKEN }}' }}
+
+ sdist_verify:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '{{ default_python }}'
+ - run: python -m pip install -U --user build
+ - run: python -m build . --sdist
+ - run: python -m pip install -U --user twine
+ - run: python -m twine check dist/*
+
+ test:
+ needs: [core, sdist_verify]
+ uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@v2
+ with:
+ submodules: false
+ coverage: codecov
+ toxdeps: tox-pypi-filter
+ posargs: -n auto
+ envs: |
+ - windows: py311
+ - macos: py312
+ - linux: py310-oldestdeps
+ - linux: py314-devdeps
+ secrets:
+ CODECOV_TOKEN: {{ '${{ secrets.CODECOV_TOKEN }}' }}
+
+ docs:
+ needs: [core]
+ uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@v2
+ with:
+ default_python: '{{ default_python }}'
+ submodules: false
+ pytest: false
+ toxdeps: tox-pypi-filter
+ libraries: |
+ apt:
+ - graphviz
+ envs: |
+ - linux: build_docs
+
+ publish:
+ # Build wheels on PRs only when labelled. Releases will only be published if tagged ^v.*
+ # see https://github-actions-workflows.openastronomy.org/en/latest/publish.html#upload-to-pypi
+ if: |
+ github.event_name != 'pull_request' ||
+ (
+ github.event_name == 'pull_request' &&
+ contains(github.event.pull_request.labels.*.name, 'Run publish')
+ )
+ needs: [test, docs]
+{%- if cookiecutter.use_compiled_extensions == 'y' %}
+ uses: OpenAstronomy/github-actions-workflows/.github/workflows/publish.yml@v2
+ with:
+ sdist: true
+ test_extras: 'tests'
+ test_command: 'pytest -p no:warnings --doctest-rst --pyargs {{ cookiecutter.module_name }}'
+ submodules: false
+ targets: |
+ - cp3{10,11,12,13,14}-manylinux*_x86_64
+ - cp3{10,11,12,13,14}-macosx_x86_64
+ - cp3{10,11,12,13,14}-macosx_arm64
+ - cp3{10,11,12,13,14}-win_amd64
+{%- else %}
+ uses: OpenAstronomy/github-actions-workflows/.github/workflows/publish_pure_python.yml@v2
+ with:
+ python-version: '{{ default_python }}'
+ test_extras: 'tests'
+ test_command: 'pytest -p no:warnings --doctest-rst --pyargs {{ cookiecutter.module_name }}'
+ submodules: false
+{%- endif %}
+ secrets:
+ pypi_token: {{ '${{ secrets.pypi_token }}' }}
diff --git a/{{ cookiecutter.package_name }}/.github/workflows/label_sync.yml b/{{ cookiecutter.package_name }}/.github/workflows/label_sync.yml
new file mode 100644
index 0000000..7f21775
--- /dev/null
+++ b/{{ cookiecutter.package_name }}/.github/workflows/label_sync.yml
@@ -0,0 +1,23 @@
+name: Label Sync
+on:
+ workflow_dispatch:
+ schedule:
+ # ┌───────── minute (0 - 59)
+ # │ ┌───────── hour (0 - 23)
+ # │ │ ┌───────── day of the month (1 - 31)
+ # │ │ │ ┌───────── month (1 - 12 or JAN-DEC)
+ # │ │ │ │ ┌───────── day of the week (0 - 6 or SUN-SAT)
+ - cron: '0 0 * * *' # run every day at midnight UTC
+
+# Give permissions to write issue labels
+permissions:
+ issues: write
+
+jobs:
+ label_sync:
+ runs-on: ubuntu-latest
+ name: Label Sync
+ steps:
+ - uses: srealmoreno/label-sync-action@850ba5cef2b25e56c6c420c4feed0319294682fd
+ with:
+ config-file: https://raw.githubusercontent.com/sunpy/.github/main/labels.yml
diff --git a/{{ cookiecutter.package_name }}/.github/workflows/sub_package_update.yml b/{{ cookiecutter.package_name }}/.github/workflows/sub_package_update.yml
index 94a9e7e..3d34eac 100644
--- a/{{ cookiecutter.package_name }}/.github/workflows/sub_package_update.yml
+++ b/{{ cookiecutter.package_name }}/.github/workflows/sub_package_update.yml
@@ -21,28 +21,15 @@ jobs:
runs-on: ubuntu-latest
strategy:
fail-fast: true
- matrix:
- include:
- - add-paths: .
- body: apply the changes to this repo.
- branch: cruft/update
- commit-message: "Automatic package template update"
- title: Updates from the package template
- - add-paths: .cruft.json
- body: reject these changes for this repo.
- branch: cruft/reject
- commit-message: "Reject this package template update"
- title: Reject new updates from package template
-
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install Cruft
- run: python -m pip install cruft
+ run: python -m pip install git+https://github.com/Cadair/cruft@patch-p1
- name: Check if update is available
continue-on-error: false
@@ -60,25 +47,49 @@ jobs:
echo "has_changes=$CHANGES" >> "$GITHUB_OUTPUT"
- name: Run update if available
+ id: cruft_update
if: steps.check.outputs.has_changes == '1'
run: |
git config --global user.email "${{ github.actor }}@users.noreply.github.com"
git config --global user.name "${{ github.actor }}"
- cruft update --skip-apply-ask --refresh-private-variables
+ cruft_output=$(cruft update --skip-apply-ask --refresh-private-variables)
+ echo $cruft_output
git restore --staged .
- - name: Create pull request
+ if [[ "$cruft_output" == *"Failed to cleanly apply the update, there may be merge conflicts."* ]]; then
+ echo merge_conflicts=1 >> $GITHUB_OUTPUT
+ else
+ echo merge_conflicts=0 >> $GITHUB_OUTPUT
+ fi
+
+ - name: Check if only .cruft.json is modified
+ id: cruft_json
if: steps.check.outputs.has_changes == '1'
- uses: peter-evans/create-pull-request@v6
+ run: |
+ git status --porcelain=1
+ if [[ "$(git status --porcelain=1)" == " M .cruft.json" ]]; then
+ echo "Only .cruft.json is modified. Exiting workflow early."
+ echo "has_changes=0" >> "$GITHUB_OUTPUT"
+ else
+ echo "has_changes=1" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Create pull request
+ if: steps.cruft_json.outputs.has_changes == '1'
+ uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
- add-paths: ${{ matrix.add-paths }}
- commit-message: ${{ matrix.commit-message }}
- branch: ${{ matrix.branch }}
+ add-paths: "."
+ commit-message: "Automatic package template update"
+ branch: "cruft/update"
delete-branch: true
- branch-suffix: timestamp
- title: ${{ matrix.title }}
+ draft: ${{ steps.cruft_update.outputs.merge_conflicts == '1' }}
+ title: "Updates from the package template"
+ labels: |
+ No Changelog Entry Needed
body: |
- This is an autogenerated PR, which will ${{ matrix.body }}.
- [Cruft](https://cruft.github.io/cruft/) has detected updates from the Package Template
+ This is an autogenerated PR, which will applies the latest changes from the [SunPy Package Template](https://github.com/sunpy/package-template).
+ If this pull request has been opened as a draft there are conflicts which need fixing.
+
+ **To run the CI on this pull request you will need to close it and reopen it.**
diff --git a/{{ cookiecutter.package_name }}/.gitignore b/{{ cookiecutter.package_name }}/.gitignore
index 8fb9b8a..bf4ec7c 100644
--- a/{{ cookiecutter.package_name }}/.gitignore
+++ b/{{ cookiecutter.package_name }}/.gitignore
@@ -1,13 +1,17 @@
+### Python: https://raw.githubusercontent.com/github/gitignore/master/Python.gitignore
+
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
+tmp/
# C extensions
*.so
# Distribution / packaging
.Python
+pip-wheel-metadata/
build/
develop-eggs/
dist/
@@ -75,6 +79,9 @@ instance/
# Sphinx documentation
docs/_build/
+# automodapi
+docs/api
+docs/sg_execution_times.rst
# PyBuilder
.pybuilder/
@@ -99,13 +106,6 @@ ipython_config.py
# install all needed dependencies.
#Pipfile.lock
-# poetry
-# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
-# This is especially recommended for binary packages to ensure reproducibility, and is more
-# commonly ignored for libraries.
-# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
-#poetry.lock
-
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
@@ -133,10 +133,6 @@ ENV/
env.bak/
venv.bak/
-# Spyder project settings
-.spyderproject
-.spyproject
-
# Rope project settings
.ropeproject
@@ -145,21 +141,127 @@ venv.bak/
# mypy
.mypy_cache/
-.dmypy.json
-dmypy.json
# Pyre type checker
.pyre/
+# IDE
+# PyCharm
+.idea
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+### VScode: https://raw.githubusercontent.com/github/gitignore/master/Global/VisualStudioCode.gitignore
+.vscode/*
+.vs/*
+
+### https://raw.github.com/github/gitignore/master/Global/OSX.gitignore
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must ends with two \r.
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear on external disk
+.Spotlight-V100
+.Trashes
+
+### Linux: https://raw.githubusercontent.com/github/gitignore/master/Global/Linux.gitignore
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
# pytype static type analyzer
.pytype/
-# Cython debug symbols
-cython_debug/
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
-# PyCharm
-# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
-# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
-# and can be added to the global gitignore or merged into this file. For a more nuclear
-# option (not recommended) you can uncomment the following to ignore the entire idea folder.
-#.idea/
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### Windows: https://raw.githubusercontent.com/github/gitignore/master/Global/Windows.gitignore
+
+# Windows thumbnail cache files
+Thumbs.db
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+### Extra Python Items and SunPy Specific
+docs/whatsnew/latest_changelog.txt
+examples/**/*.csv
+figure_test_images*
+tags
+baseline
+
+# Release script
+.github_cache
+
+# Misc Stuff
+.history
+*.orig
+.tmp
+node_modules/
+package-lock.json
+package.json
+.prettierrc
+
+# Log files generated by 'vagrant up'
+*.log
diff --git a/{{ cookiecutter.package_name }}/.isort.cfg b/{{ cookiecutter.package_name }}/.isort.cfg
new file mode 100644
index 0000000..269e6be
--- /dev/null
+++ b/{{ cookiecutter.package_name }}/.isort.cfg
@@ -0,0 +1,16 @@
+[settings]
+balanced_wrapping = true
+skip =
+ docs/conf.py
+ {{ cookiecutter.module_name }}/__init__.py
+default_section = THIRDPARTY
+include_trailing_comma = true
+known_astropy = astropy, asdf
+known_sunpy = sunpy
+known_first_party = {{ cookiecutter.module_name }}
+length_sort = false
+length_sort_sections = stdlib
+line_length = 110
+multi_line_output = 3
+no_lines_before = LOCALFOLDER
+sections = STDLIB, THIRDPARTY, ASTROPY, SUNPY, FIRSTPARTY, LOCALFOLDER
diff --git a/{{ cookiecutter.package_name }}/.pre-commit-config.yaml b/{{ cookiecutter.package_name }}/.pre-commit-config.yaml
new file mode 100644
index 0000000..bc75e3d
--- /dev/null
+++ b/{{ cookiecutter.package_name }}/.pre-commit-config.yaml
@@ -0,0 +1,35 @@
+repos:
+ # This should be before any formatting hooks like isort
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: "v0.12.11"
+ hooks:
+ - id: ruff
+ args: ["--fix"]
+ - repo: https://github.com/PyCQA/isort
+ rev: 6.0.1
+ hooks:
+ - id: isort
+ exclude: ".*(.fits|.fts|.fit|.header|.txt|tca.*|extern.*|{{ cookiecutter.module_name }}/extern)$"
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v6.0.0
+ hooks:
+ - id: check-ast
+ - id: check-case-conflict
+ - id: trailing-whitespace
+ exclude: ".*(.fits|.fts|.fit|.header|.txt)$"
+ - id: check-yaml
+ - id: debug-statements
+ - id: check-added-large-files
+ args: ["--enforce-all", "--maxkb=1054"]
+ - id: end-of-file-fixer
+ exclude: ".*(.fits|.fts|.fit|.header|.txt|tca.*|.json)$|^CITATION.rst$"
+ - id: mixed-line-ending
+ exclude: ".*(.fits|.fts|.fit|.header|.txt|tca.*)$"
+ - repo: https://github.com/codespell-project/codespell
+ rev: v2.4.1
+ hooks:
+ - id: codespell
+ args: [ "--write-changes" ]
+ci:
+ autofix_prs: false
+ autoupdate_schedule: "quarterly"
diff --git a/{{ cookiecutter.package_name }}/.readthedocs.yaml b/{{ cookiecutter.package_name }}/.readthedocs.yaml
new file mode 100644
index 0000000..3d9312d
--- /dev/null
+++ b/{{ cookiecutter.package_name }}/.readthedocs.yaml
@@ -0,0 +1,29 @@
+version: 2
+
+build:
+ os: ubuntu-lts-latest
+ tools:
+ python: "mambaforge-latest"
+ jobs:
+ post_checkout:
+ - git fetch --unshallow || true
+ pre_install:
+ - git update-index --assume-unchanged .rtd-environment.yml docs/conf.py
+
+conda:
+ environment: .rtd-environment.yml
+
+sphinx:
+ builder: html
+ configuration: docs/conf.py
+ fail_on_warning: false
+
+formats:
+ - htmlzip
+
+python:
+ install:
+ - method: pip
+ extra_requirements:
+ - docs
+ path: .
diff --git a/{{ cookiecutter.package_name }}/.rtd-environment.yml b/{{ cookiecutter.package_name }}/.rtd-environment.yml
new file mode 100644
index 0000000..5528179
--- /dev/null
+++ b/{{ cookiecutter.package_name }}/.rtd-environment.yml
@@ -0,0 +1,7 @@
+name: {{ cookiecutter.package_name }}
+channels:
+ - conda-forge
+dependencies:
+ - python=3.12
+ - pip
+ - graphviz!=2.42.*,!=2.43.*
diff --git a/{{ cookiecutter.package_name }}/.ruff.toml b/{{ cookiecutter.package_name }}/.ruff.toml
new file mode 100644
index 0000000..df0e2c8
--- /dev/null
+++ b/{{ cookiecutter.package_name }}/.ruff.toml
@@ -0,0 +1,83 @@
+target-version = "py310"
+line-length = 120
+exclude = [
+ ".git,",
+ "__pycache__",
+ "build",
+ "{{ cookiecutter.package_name }}/version.py",
+]
+
+[lint]
+select = [
+ "E",
+ "F",
+ "W",
+ "UP",
+ "PT",
+ {%- if cookiecutter.use_extended_ruff_linting == 'y' %}
+ "BLE",
+ "A",
+ "C4",
+ "INP",
+ "PIE",
+ "T20",
+ "RET",
+ "TID",
+ "PTH",
+ "PD",
+ "PLC",
+ "PLE",
+ "FLY",
+ "NPY",
+ "PERF",
+ "RUF",
+ {%- endif %}
+]
+extend-ignore = [
+ # pycodestyle (E, W)
+ "E501", # ignore line length will use a formatter instead
+ # pyupgrade (UP)
+ "UP038", # Use | in isinstance - not compatible with models and is slower
+ # pytest (PT)
+ "PT001", # Always use pytest.fixture()
+ "PT023", # Always use () on pytest decorators
+ # flake8-pie (PIE)
+ "PIE808", # Disallow passing 0 as the first argument to range
+ # flake8-use-pathlib (PTH)
+ "PTH123", # open() should be replaced by Path.open()
+ # Ruff (RUF)
+ "RUF003", # Ignore ambiguous quote marks, doesn't allow ' in comments
+ "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar`
+ "RUF013", # PEP 484 prohibits implicit `Optional`
+ "RUF015", # Prefer `next(iter(...))` over single element slice
+]
+
+[lint.per-file-ignores]
+"setup.py" = [
+ "INP001", # File is part of an implicit namespace package.
+]
+"conftest.py" = [
+ "INP001", # File is part of an implicit namespace package.
+]
+"docs/conf.py" = [
+ "E402" # Module imports not at top of file
+]
+"docs/*.py" = [
+ "INP001", # File is part of an implicit namespace package.
+]
+"examples/**.py" = [
+ "T201", # allow use of print in examples
+ "INP001", # File is part of an implicit namespace package.
+]
+"__init__.py" = [
+ "E402", # Module level import not at top of cell
+ "F401", # Unused import
+ "F403", # from {name} import * used; unable to detect undefined names
+ "F405", # {name} may be undefined, or defined from star imports
+]
+"test_*.py" = [
+ "E402", # Module level import not at top of cell
+]
+
+[lint.pydocstyle]
+convention = "numpy"
diff --git a/{{ cookiecutter.package_name }}/CHANGELOG.rst b/{{ cookiecutter.package_name }}/CHANGELOG.rst
new file mode 100644
index 0000000..e69de29
diff --git a/{{ cookiecutter.package_name }}/README.rst b/{{ cookiecutter.package_name }}/README.rst
index 534e27e..b718bdb 100644
--- a/{{ cookiecutter.package_name }}/README.rst
+++ b/{{ cookiecutter.package_name }}/README.rst
@@ -1,6 +1,13 @@
{{ cookiecutter.short_description }}
{{ '-' * cookiecutter.short_description|length }}
+Usage of Generative AI
+----------------------
+
+We expect authentic engagement in our community.
+Be wary of posting output from Large Language Models or similar generative AI as comments on GitHub or any other platform, as such comments tend to be formulaic and low quality content.
+If you use generative AI tools as an aid in developing code or documentation changes, ensure that you fully understand the proposed changes and can explain why they are the correct approach and an improvement to the current state.
+
License
-------
diff --git a/{{ cookiecutter.package_name }}/changelog/README.rst b/{{ cookiecutter.package_name }}/changelog/README.rst
new file mode 100644
index 0000000..7d388e3
--- /dev/null
+++ b/{{ cookiecutter.package_name }}/changelog/README.rst
@@ -0,0 +1,34 @@
+=========
+Changelog
+=========
+
+.. note::
+
+ This README was adapted from the pytest changelog readme under the terms of the MIT licence.
+
+This directory contains "news fragments" which are short files that contain a small **ReST**-formatted text that will be added to the next ``CHANGELOG``.
+
+The ``CHANGELOG`` will be read by users, so this description should be aimed at SunPy users instead of describing internal changes which are only relevant to the developers.
+
+Make sure to use full sentences with correct case and punctuation, for example::
+
+ Add support for Helioprojective coordinates in `sunpy.coordinates.frames`.
+
+Please try to use Sphinx intersphinx using backticks.
+
+Each file should be named like ``.[.].rst``, where ```` is a pull request number, ``COUNTER`` is an optional number if a PR needs multiple entries with the same type and ```` is one of:
+
+* ``breaking``: A change which requires users to change code and is not backwards compatible. (Not to be used for removal of deprecated features.)
+* ``feature``: New user facing features and any new behavior.
+* ``bugfix``: Fixes a reported bug.
+* ``doc``: Documentation addition or improvement, like rewording an entire session or adding missing docs.
+* ``deprecation``: Feature deprecation
+* ``removal``: Feature removal.
+* ``trivial``: A change which has no user facing effect or is tiny change.
+
+So for example: ``123.feature.rst``, ``456.bugfix.rst``.
+
+If you are unsure what pull request type to use, don't hesitate to ask in your PR.
+
+Note that the ``towncrier`` tool will automatically reflow your text, so it will work best if you stick to a single paragraph, but multiple sentences and links are OK and encouraged.
+You can install ``towncrier`` and then run ``towncrier --draft`` if you want to get a preview of how your change will look in the final release notes.
diff --git a/{{ cookiecutter.package_name }}/docs/conf.py b/{{ cookiecutter.package_name }}/docs/conf.py
index e595c98..9736433 100644
--- a/{{ cookiecutter.package_name }}/docs/conf.py
+++ b/{{ cookiecutter.package_name }}/docs/conf.py
@@ -6,12 +6,23 @@
import datetime
+from packaging.version import Version
+
# -- Project information -----------------------------------------------------
# The full version, including alpha/beta/rc tags
from {{ cookiecutter.module_name }} import __version__
-release = __version__
+_version = Version(__version__)
+version = release = str(_version)
+# Avoid "post" appearing in version string in rendered docs
+if _version.is_postrelease:
+ version = release = _version.base_version
+# Avoid long githashes in rendered Sphinx docs
+elif _version.is_devrelease:
+ version = release = f"{_version.base_version}.dev{_version.dev}"
+is_development = _version.is_devrelease
+is_release = not(_version.is_prerelease or _version.is_devrelease)
project = "{{ cookiecutter.package_name }}"
author = "{{ cookiecutter.author_name }}"
@@ -19,6 +30,9 @@
# -- General configuration ---------------------------------------------------
+# Wrap large function/method signatures
+maximum_signature_line_length = 80
+
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named "sphinx.ext.*") or your custom
# ones.
@@ -34,6 +48,7 @@
"sphinx.ext.mathjax",
"sphinx_automodapi.automodapi",
"sphinx_automodapi.smart_resolver",
+ "sphinx_changelog",
]
# Add any paths that contain templates here, relative to this directory.
@@ -45,14 +60,13 @@
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
# The suffix(es) of source filenames.
-# You can specify multiple suffix as a list of string:
-source_suffix = ".rst"
+source_suffix = {".rst": "restructuredtext"}
# The master toctree document.
master_doc = "index"
# Treat everything in single ` as a Python reference.
-default_role = 'py:obj'
+default_role = "py:obj"
# -- Options for intersphinx extension ---------------------------------------
@@ -65,6 +79,18 @@
# a list of builtin themes.
html_theme = "{{ cookiecutter._sphinx_theme }}"
+# Render inheritance diagrams in SVG
+graphviz_output_format = "svg"
+
+graphviz_dot_args = [
+ "-Nfontsize=10",
+ "-Nfontname=Helvetica Neue, Helvetica, Arial, sans-serif",
+ "-Efontsize=10",
+ "-Efontname=Helvetica Neue, Helvetica, Arial, sans-serif",
+ "-Gfontsize=10",
+ "-Gfontname=Helvetica Neue, Helvetica, Arial, sans-serif",
+]
+
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
diff --git a/{{ cookiecutter.package_name }}/docs/index.rst b/{{ cookiecutter.package_name }}/docs/index.rst
index 7b96581..312a52f 100644
--- a/{{ cookiecutter.package_name }}/docs/index.rst
+++ b/{{ cookiecutter.package_name }}/docs/index.rst
@@ -1,5 +1,6 @@
+{{ '*' * (cookiecutter.package_name + " Documentation")|length }}
{{ cookiecutter.package_name }} Documentation
-{{ '-' * (cookiecutter.package_name + " Documentation")|length }}
+{{ '*' * (cookiecutter.package_name + " Documentation")|length }}
This is the documentation for {{ cookiecutter.package_name }}.
@@ -7,6 +8,8 @@ This is the documentation for {{ cookiecutter.package_name }}.
:maxdepth: 2
:caption: Contents:
+ whatsnew/index
+
Indices and tables
==================
diff --git a/{{ cookiecutter.package_name }}/docs/whatsnew/changelog.rst b/{{ cookiecutter.package_name }}/docs/whatsnew/changelog.rst
new file mode 100644
index 0000000..a3678c4
--- /dev/null
+++ b/{{ cookiecutter.package_name }}/docs/whatsnew/changelog.rst
@@ -0,0 +1,10 @@
+.. _changelog:
+
+**************
+Full Changelog
+**************
+
+.. changelog::
+ :towncrier: ../../
+ :towncrier-skip-if-empty:
+ :changelog_file: ../../CHANGELOG.rst
diff --git a/{{ cookiecutter.package_name }}/docs/whatsnew/index.rst b/{{ cookiecutter.package_name }}/docs/whatsnew/index.rst
new file mode 100644
index 0000000..019bcad
--- /dev/null
+++ b/{{ cookiecutter.package_name }}/docs/whatsnew/index.rst
@@ -0,0 +1,12 @@
+.. _whatsnew:
+
+***************
+Release History
+***************
+
+This page documents the releases for {{ cookiecutter.package_name }}
+
+.. toctree::
+ :maxdepth: 1
+
+ changelog
diff --git a/{{ cookiecutter.package_name }}/licenses/TEMPLATE_LICENSE.rst b/{{ cookiecutter.package_name }}/licenses/TEMPLATE_LICENSE.rst
index f29177b..544a2db 100644
--- a/{{ cookiecutter.package_name }}/licenses/TEMPLATE_LICENSE.rst
+++ b/{{ cookiecutter.package_name }}/licenses/TEMPLATE_LICENSE.rst
@@ -1,10 +1,10 @@
-This project is based upon the Astropy package template
-(https://github.com/astropy/package-template/) which is licenced under the terms
+This project is based upon the OpenAstronomy package template
+(https://github.com/OpenAstronomy/package-template/) which is licensed under the terms
of the following licence.
---
-Copyright (c) 2018, Astropy Developers
+Copyright (c) 2018, OpenAstronomy Developers
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
diff --git a/{{ cookiecutter.package_name }}/pyproject.toml b/{{ cookiecutter.package_name }}/pyproject.toml
index 35f7b0c..5b856e3 100644
--- a/{{ cookiecutter.package_name }}/pyproject.toml
+++ b/{{ cookiecutter.package_name }}/pyproject.toml
@@ -6,8 +6,8 @@ requires = [
{%- if cookiecutter.use_compiled_extensions == 'y' %}
"extension-helpers",
"numpy>=1.25",
- "cython"
-{% endif -%}
+ "cython",
+{%- endif %}
]
build-backend = "setuptools.build_meta"
@@ -33,19 +33,40 @@ open_astronomy_package_template_example = "{{ cookiecutter.module_name }}.exampl
{% endif -%}
[project.optional-dependencies]
-test = [
- "pytest",
- "pytest-doctestplus",
- "pytest-cov",
+tests = [
+ "pytest",
+ "pytest-doctestplus",
+ "pytest-cov",
+ "pytest-xdist",
]
docs = [
- "sphinx",
- "sphinx-automodapi",
+ "sphinx",
+ "sphinx-automodapi",
+ "sphinx-changelog",
+ "sunpy-sphinx-theme",
+ "packaging",
]
-{%- if cookiecutter.project_url %}
[project.urls]
-repository = "{{ cookiecutter.project_url }}"
+{%- if cookiecutter.project_url %}
+Homepage = "{{ cookiecutter.project_url }}"
+{%- else %}
+Homepage = "https://sunpy.org"
+{%- endif %}
+{%- if cookiecutter.sourcecode_url %}
+"Source Code" = "{{ cookiecutter.sourcecode_url }}"
+{%- endif %}
+{%- if cookiecutter.download_url %}
+Download = "{{ cookiecutter.download_url }}"
+{%- endif %}
+{%- if cookiecutter.documentation_url %}
+Documentation = "{{ cookiecutter.documentation_url }}"
+{%- endif %}
+{%- if cookiecutter.changelog_url %}
+Changelog = "{{ cookiecutter.changelog_url }}"
+{%- endif %}
+{%- if cookiecutter.issue_tracker_url %}
+"Issue Tracker" = "{{ cookiecutter.issue_tracker_url }}"
{%- endif %}
[tool.setuptools]
@@ -57,7 +78,10 @@ include = ["{{ cookiecutter.module_name }}*"]
{%- if cookiecutter.enable_dynamic_dev_versions == 'y' %}
exclude = ["{{ cookiecutter.module_name }}._dev*"]
{%- endif %}
-
+{% if cookiecutter.use_compiled_extensions == 'y' %}
+[tool.setuptools.exclude-package-data]
+"*" = ["*.c", "*.h"]
+{% endif %}
[tool.setuptools_scm]
{% if cookiecutter.enable_dynamic_dev_versions == 'y' -%}
version_file = "{{ cookiecutter.module_name }}/_version.py"
@@ -65,51 +89,62 @@ version_file = "{{ cookiecutter.module_name }}/_version.py"
version_file = "{{ cookiecutter.module_name }}/version.py"
{%- endif %}
-[tool.pytest.ini_options]
-testpaths = [
- "{{ cookiecutter.module_name }}",
- "docs",
-]
-doctest_plus = "enabled"
-text_file_format = "rst"
-addopts = "--doctest-rst"
-norecursedirs = ["{{ cookiecutter.module_name }}[\\/]_dev"]
-
-[tool.coverage.run]
-omit = [
- "{{ cookiecutter.module_name }}/_{{ cookiecutter._parent_project }}_init*",
- "{{ cookiecutter.module_name }}/conftest.py",
- "{{ cookiecutter.module_name }}/*setup_package*",
- "{{ cookiecutter.module_name }}/tests/*",
- "{{ cookiecutter.module_name }}/*/tests/*",
- "{{ cookiecutter.module_name }}/extern/*",
- "{{ cookiecutter.module_name }}/version*",
- "*/{{ cookiecutter.module_name }}/_{{ cookiecutter._parent_project }}_init*",
- "*/{{ cookiecutter.module_name }}/conftest.py",
- "*/{{ cookiecutter.module_name }}/*setup_package*",
- "*/{{ cookiecutter.module_name }}/tests/*",
- "*/{{ cookiecutter.module_name }}/*/tests/*",
- "*/{{ cookiecutter.module_name }}/extern/*",
- "*/{{ cookiecutter.module_name }}/version*",
-]
+[tool.gilesbot]
+ [tool.gilesbot.pull_requests]
+ enabled = true
-[tool.coverage.report]
-exclude_lines = [
- # Have to re-enable the standard pragma
- "pragma: no cover",
- # Don't complain about packages we have installed
- "except ImportError",
- # Don't complain if tests don't hit assertions
- "raise AssertionError",
- "raise NotImplementedError",
- # Don't complain about script hooks
- "def main(.*):",
- # Ignore branches that don't pertain to this version of Python
- "pragma: py{ignore_python_version}",
- # Don't complain about IPython completion helper
- "def _ipython_key_completions_",
- # typing.TYPE_CHECKING is False at runtime
- "if TYPE_CHECKING:",
- # Ignore typing overloads
- "@overload",
-]
+ [tool.gilesbot.towncrier_changelog]
+ enabled = true
+ verify_pr_number = true
+ changelog_skip_label = "No Changelog Entry Needed"
+ help_url = "https://github.com/{{ cookiecutter.github_repo | default('sunpy/sunpy') }}/blob/main/changelog/README.rst"
+
+ changelog_missing_long = "There isn't a changelog file in this pull request. Please add a changelog file to the `changelog/` directory following the instructions in the changelog [README](https://github.com/{{ cookiecutter.github_repo | default('sunpy/sunpy') }}/blob/main/changelog/README.rst)."
+
+ type_incorrect_long = "The changelog file you added is not one of the allowed types. Please use one of the types described in the changelog [README](https://github.com/{{ cookiecutter.github_repo | default('sunpy/sunpy') }}/blob/main/changelog/README.rst)"
+
+ number_incorrect_long = "The number in the changelog file you added does not match the number of this pull request. Please rename the file."
+
+# TODO: This should be in towncrier.toml but Giles currently only works looks in
+# pyproject.toml we should move this back when it's fixed.
+[tool.towncrier]
+ package = "{{ cookiecutter.module_name }}"
+ filename = "CHANGELOG.rst"
+ directory = "changelog/"
+ issue_format = "`#{issue} `__"
+ title_format = "{version} ({project_date})"
+
+ [[tool.towncrier.type]]
+ directory = "breaking"
+ name = "Breaking Changes"
+ showcontent = true
+
+ [[tool.towncrier.type]]
+ directory = "deprecation"
+ name = "Deprecations"
+ showcontent = true
+
+ [[tool.towncrier.type]]
+ directory = "removal"
+ name = "Removals"
+ showcontent = true
+
+ [[tool.towncrier.type]]
+ directory = "feature"
+ name = "New Features"
+ showcontent = true
+
+ [[tool.towncrier.type]]
+ directory = "bugfix"
+ name = "Bug Fixes"
+ showcontent = true
+
+ [[tool.towncrier.type]]
+ directory = "doc"
+ name = "Documentation"
+ showcontent = true
+
+ [[tool.towncrier.type]]
+ directory = "trivial"
+ name = "Internal Changes"
+ showcontent = true
diff --git a/{{ cookiecutter.package_name }}/pytest.ini b/{{ cookiecutter.package_name }}/pytest.ini
new file mode 100644
index 0000000..b5fb97a
--- /dev/null
+++ b/{{ cookiecutter.package_name }}/pytest.ini
@@ -0,0 +1,33 @@
+[pytest]
+minversion = 7.0
+testpaths =
+ {{ cookiecutter.module_name }}
+ docs
+norecursedirs =
+ .tox
+ build
+ docs/_build
+ docs/generated
+ *.egg-info
+ examples
+ {{ cookiecutter.module_name }}/_dev
+ .history
+ {{ cookiecutter.module_name }}/extern
+doctest_plus = enabled
+doctest_optionflags =
+ NORMALIZE_WHITESPACE
+ FLOAT_CMP
+ ELLIPSIS
+text_file_format = rst
+addopts =
+ --doctest-rst
+ -p no:unraisableexception
+ -p no:threadexception
+filterwarnings =
+ # Turn all warnings into errors so they do not pass silently.
+ error
+ # Do not fail on pytest config issues (i.e. missing plugins) but do show them
+ always::pytest.PytestConfigWarning
+ # A list of warnings to ignore follows. If you add to this list, you MUST
+ # add a comment or ideally a link to an issue that explains why the warning
+ # is being ignored
diff --git a/{{ cookiecutter.package_name }}/tox.ini b/{{ cookiecutter.package_name }}/tox.ini
index d262ce1..7a4ac72 100644
--- a/{{ cookiecutter.package_name }}/tox.ini
+++ b/{{ cookiecutter.package_name }}/tox.ini
@@ -1,24 +1,23 @@
[tox]
min_version = 4.0
+requires =
+ tox-pypi-filter>=0.14
envlist =
- py{310,311,312}-test
- py10-test-oldestdeps
+ py{310,311,312,313,314}
+ py312-devdeps
+ py310-oldestdeps
+ codestyle
build_docs
[testenv]
-# tox environments are constructed with so-called 'factors' (or terms)
-# separated by hyphens, e.g. test-devdeps-cov. Lines below starting with factor:
-# will only take effect if that factor is included in the environment name. To
-# see a list of example environments that can be run, along with a description,
-# run:
-#
-# tox -l -v
-#
+pypi_filter = https://raw.githubusercontent.com/sunpy/sunpy/main/.test_package_pins.txt
+# Run the tests in a temporary directory to make sure that we don't import
+# the package from the source tree
+change_dir = .tmp/{envname}
description =
run tests
oldestdeps: with the oldest supported version of key dependencies
-
-# Pass through the following environment variables which may be needed for the CI
+ devdeps: with the latest developer version of key dependencies
pass_env =
# A variable to tell tests we are on a CI system
CI
@@ -27,32 +26,46 @@ pass_env =
# Location of locales (needed by sphinx on some systems)
LOCALE_ARCHIVE
# If the user has set a LC override we should follow it
- # (note LANG is automatically passed through by tox)
LC_ALL
-
-# Suppress display of matplotlib plots generated during docs build
set_env =
MPLBACKEND = agg
-
-# Run the tests in a temporary directory to make sure that we don't import
-# the package from the source tree
-change_dir = .tmp/{envname}
-
+ devdeps: PIP_EXTRA_INDEX_URL = https://pypi.anaconda.org/astropy/simple https://pypi.anaconda.org/scientific-python-nightly-wheels/simple
deps =
+ # For packages which publish nightly wheels this will pull the latest nightly
+ devdeps: numpy>=0.0.dev0
+ # Packages without nightly wheels will be built from source like this
+ # devdeps: git+https://github.com/ndcube/ndcube
oldestdeps: minimum_dependencies
- pytest-cov
-
-# The following indicates which extras_require from setup.cfg will be installed
+# The following indicates which extras_require will be installed
extras =
- test
-
+ tests
commands_pre =
oldestdeps: minimum_dependencies {{ cookiecutter.module_name }} --filename requirements-min.txt
oldestdeps: pip install -r requirements-min.txt
- pip freeze
+ pip freeze --all --no-input
+commands =
+ # To amend the pytest command for different factors you can add a line
+ # which starts with a factor like `online: --remote-data=any \`
+ # If you have no factors which require different commands this is all you need:
+ pytest \
+ -vvv \
+ -r fEs \
+ --pyargs {{ cookiecutter.module_name }} \
+ --cov-report=xml \
+ --cov={{ cookiecutter.module_name }} \
+ --cov-config={toxinidir}/.coveragerc \
+ {toxinidir}/docs \
+ {posargs}
+[testenv:codestyle]
+pypi_filter =
+skip_install = true
+description = Run all style and file checks with pre-commit
+deps =
+ pre-commit
commands =
- pytest --pyargs {{ cookiecutter.module_name }} --cov {{ cookiecutter.module_name }} --cov-report xml:coverage.xml --cov-report term-missing {posargs}
+ pre-commit install-hooks
+ pre-commit run --color always --all-files --show-diff-on-failure
[testenv:build_docs]
description = invoke sphinx-build to build the HTML docs
diff --git a/{{ cookiecutter.package_name }}/{{ cookiecutter.module_name }}/_dev/__init__.py b/{{ cookiecutter.package_name }}/{{ cookiecutter.module_name }}/_dev/__init__.py
index 72583c0..e38b3a8 100644
--- a/{{ cookiecutter.package_name }}/{{ cookiecutter.module_name }}/_dev/__init__.py
+++ b/{{ cookiecutter.package_name }}/{{ cookiecutter.module_name }}/_dev/__init__.py
@@ -1,5 +1,5 @@
"""
-This package contains utilities that are only used when developing drms in a
+This package contains utilities that are only used when developing in a
copy of the source repository.
These files are not installed, and should not be assumed to exist at
runtime.
diff --git a/{{ cookiecutter.package_name }}/{{ cookiecutter.module_name }}/_dev/scm_version.py b/{{ cookiecutter.package_name }}/{{ cookiecutter.module_name }}/_dev/scm_version.py
index 1bcf0dd..988debf 100644
--- a/{{ cookiecutter.package_name }}/{{ cookiecutter.module_name }}/_dev/scm_version.py
+++ b/{{ cookiecutter.package_name }}/{{ cookiecutter.module_name }}/_dev/scm_version.py
@@ -1,11 +1,11 @@
# Try to use setuptools_scm to get the current version; this is only used
# in development installations from the git repository.
-import os.path
+from pathlib import Path
try:
from setuptools_scm import get_version
- version = get_version(root=os.path.join('..', '..'), relative_to=__file__)
+ version = get_version(root=Path('../..'), relative_to=__file__)
except ImportError:
raise
except Exception as e:
diff --git a/{{ cookiecutter.package_name }}/{{ cookiecutter.module_name }}/example_mod.py b/{{ cookiecutter.package_name }}/{{ cookiecutter.module_name }}/example_mod.py
index b2eff69..643e725 100644
--- a/{{ cookiecutter.package_name }}/{{ cookiecutter.module_name }}/example_mod.py
+++ b/{{ cookiecutter.package_name }}/{{ cookiecutter.module_name }}/example_mod.py
@@ -1,4 +1,4 @@
-__all__ = ['primes', 'do_primes']
+__all__ = ['do_primes', 'primes']
def primes(imax):
@@ -43,20 +43,19 @@ def do_primes(n, usecython=False):
{% if cookiecutter.use_compiled_extensions != 'y' %}
raise Exception("This template does not have the example C code included.")
{% else %}
- from .example_c import primes as cprimes
- print('Using cython-based primes')
+ from .example_c import primes as cprimes # noqa: PLC0415
+ print('Using cython-based primes') # noqa: T201
return cprimes(n)
{% endif %}
- else:
- print('Using pure python primes')
- return primes(n)
+ print('Using pure python primes') # noqa: T201
+ return primes(n)
def main(args=None):
- from time import time
+ from time import time # noqa: PLC0415
- from astropy.utils.compat import argparse
+ from astropy.utils.compat import argparse # noqa: PLC0415
parser = argparse.ArgumentParser(description='Process some integers.')
parser.add_argument('-c', '--use-cython', dest='cy', action='store_true',
@@ -74,11 +73,11 @@ def main(args=None):
primes = do_primes(res.n, res.cy)
post = time()
- print(f'Found {len(primes)} prime numbers')
- print(f'Largest prime: {primes[-1]}')
+ print(f'Found {len(primes)} prime numbers') # noqa: T201
+ print(f'Largest prime: {primes[-1]}') # noqa: T201
if res.time:
- print(f'Running time: {post - pre} s')
+ print(f'Running time: {post - pre} s') # noqa: T201
if res.prnt:
- print(f'Primes: {primes}')
+ print(f'Primes: {primes}') # noqa: T201
diff --git a/{{ cookiecutter.package_name }}/{{ cookiecutter.module_name }}/tests/test_example.py b/{{ cookiecutter.package_name }}/{{ cookiecutter.module_name }}/tests/test_example.py
index 33d4574..5349f11 100644
--- a/{{ cookiecutter.package_name }}/{{ cookiecutter.module_name }}/tests/test_example.py
+++ b/{{ cookiecutter.package_name }}/{{ cookiecutter.module_name }}/tests/test_example.py
@@ -1,9 +1,9 @@
{% if cookiecutter.use_compiled_extensions == 'y' %}
def test_primes_c():
- from ..example_c import primes as primes_c
+ from {{ cookiecutter.module_name }}.example_c import primes as primes_c # noqa: PLC0415
assert primes_c(10) == [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
{% endif %}
def test_primes():
- from ..example_mod import primes
+ from {{ cookiecutter.module_name }}.example_mod import primes # noqa: PLC0415
assert primes(10) == [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]