From 36d240feefde0edc5d0c2593bc015e8b73d6cac5 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Mon, 7 Sep 2026 15:52:40 -0400 Subject: [PATCH 1/6] Add a Cython project template with native wheel checks Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com> --- .github/workflows/build.yaml | 1 + Makefile | 10 + README.md | 2 +- copier.yaml | 2 + examples/cython.yaml | 10 + python/cython/.gitattributes | 9 + .../cython/.github/CODE_OF_CONDUCT.md.jinja | 76 +++++++ .../ISSUE_TEMPLATE/bug_report.md.jinja | 29 +++ .../.github/ISSUE_TEMPLATE/feature_request.md | 19 ++ .../cython/.github/ISSUE_TEMPLATE/question.md | 16 ++ python/cython/.github/dependabot.yaml | 26 +++ .../cython/.github/pull_request_template.md | 20 ++ .../.github/scripts/test-distributions.py | 64 ++++++ .../cython/.github/workflows/build.yaml.jinja | 124 +++++++++++ ...% if add_docs %}docs.yaml{% endif %}.jinja | 59 +++++ ...% if add_wiki %}wiki.yaml{% endif %}.jinja | 34 +++ python/cython/.gitignore.jinja | 166 +++++++++++++++ python/cython/LICENSE.jinja | 201 ++++++++++++++++++ python/cython/Makefile.jinja | 92 ++++++++ python/cython/README.md.jinja | 16 ++ python/cython/docs/development.md.jinja | 40 ++++ python/cython/pyproject.toml.jinja | 189 ++++++++++++++++ .../wiki/Installation.md.jinja | 19 ++ .../wiki/_Footer.md.jinja | 1 + .../wiki/_Sidebar.md.jinja | 16 ++ .../contribute/Build-from-Source.md.jinja | 103 +++++++++ .../wiki/contribute/Contribute.md.jinja | 15 ++ .../Local-Development-Setup.md.jinja | 55 +++++ .../{{_copier_conf.answers_file}}.jinja | 2 + python/cython/{{module}}/__init__.py | 4 + python/cython/{{module}}/_compiled.py | 2 + python/cython/{{module}}/_native.pyx | 2 + python/cython/{{module}}/tests/__init__.py | 0 .../{{module}}/tests/test_extensions.py.jinja | 19 ++ 34 files changed, 1442 insertions(+), 1 deletion(-) create mode 100644 examples/cython.yaml create mode 100644 python/cython/.gitattributes create mode 100644 python/cython/.github/CODE_OF_CONDUCT.md.jinja create mode 100644 python/cython/.github/ISSUE_TEMPLATE/bug_report.md.jinja create mode 100644 python/cython/.github/ISSUE_TEMPLATE/feature_request.md create mode 100644 python/cython/.github/ISSUE_TEMPLATE/question.md create mode 100644 python/cython/.github/dependabot.yaml create mode 100644 python/cython/.github/pull_request_template.md create mode 100644 python/cython/.github/scripts/test-distributions.py create mode 100644 python/cython/.github/workflows/build.yaml.jinja create mode 100644 python/cython/.github/workflows/{% if add_docs %}docs.yaml{% endif %}.jinja create mode 100644 python/cython/.github/workflows/{% if add_wiki %}wiki.yaml{% endif %}.jinja create mode 100644 python/cython/.gitignore.jinja create mode 100644 python/cython/LICENSE.jinja create mode 100644 python/cython/Makefile.jinja create mode 100644 python/cython/README.md.jinja create mode 100644 python/cython/docs/development.md.jinja create mode 100644 python/cython/pyproject.toml.jinja create mode 100644 python/cython/{% if add_wiki %}docs{% endif %}/wiki/Installation.md.jinja create mode 100644 python/cython/{% if add_wiki %}docs{% endif %}/wiki/_Footer.md.jinja create mode 100644 python/cython/{% if add_wiki %}docs{% endif %}/wiki/_Sidebar.md.jinja create mode 100644 python/cython/{% if add_wiki %}docs{% endif %}/wiki/contribute/Build-from-Source.md.jinja create mode 100644 python/cython/{% if add_wiki %}docs{% endif %}/wiki/contribute/Contribute.md.jinja create mode 100644 python/cython/{% if add_wiki %}docs{% endif %}/wiki/contribute/Local-Development-Setup.md.jinja create mode 100644 python/cython/{{_copier_conf.answers_file}}.jinja create mode 100644 python/cython/{{module}}/__init__.py create mode 100644 python/cython/{{module}}/_compiled.py create mode 100644 python/cython/{{module}}/_native.pyx create mode 100644 python/cython/{{module}}/tests/__init__.py create mode 100644 python/cython/{{module}}/tests/test_extensions.py.jinja diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 12a7c93..f258686 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -25,6 +25,7 @@ jobs: matrix: template: - python + - cython - cpp - js - jupyter diff --git a/Makefile b/Makefile index b549ee3..dca728b 100644 --- a/Makefile +++ b/Makefile @@ -37,6 +37,16 @@ gen-python: ## regenerate the python template from scratch copier copy -w . ../python-template --data-file examples/python.yaml cd ../python-template && $(SED) 's#_src_path: .#_src_path: https://github.com/python-project-templates/base.git#g' ./.copier-answers.yaml +.PHONY: gen-cython test-cython +gen-cython: ## regenerate the Cython template + copier copy --defaults --overwrite --vcs-ref=HEAD . ../python-template-cython --data-file examples/cython.yaml + cd ../python-template-cython && $(SED) 's#_src_path: .#_src_path: https://github.com/python-project-templates/base.git#g' ./.copier-answers.yaml + +test-cython: ## test the generated Cython project + git -C ../python-template-cython init + git -C ../python-template-cython add . + $(MAKE) -C ../python-template-cython develop lint checks coverage dist test-dist + gen-cpp: ## regenerate the c++ template from scratch mkdir -p ../python-template-cpp && cd ../python-template-cpp && rm -rf ./* && rm -rf .copier-answers.yaml .gitignore .github .gitattributes copier copy -w . ../python-template-cpp --data-file examples/cpp.yaml diff --git a/README.md b/README.md index 513d45c..f31cc4f 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ copier copy https://github.com/python-project-templates/base.git path/to/new/pro One selector chooses a template: -- `Python / …` options generate pure Python, C++, JavaScript, Jupyter, Rust, and WebAssembly-backed Python packages from `python/`. +- `Python / …` options generate pure Python, Cython, C++, JavaScript, Jupyter, Rust, and WebAssembly-backed Python packages from `python/`. - `JavaScript / UI Toolkit / Svelte` generates a Svelte package and showcase from `javascript/uitk-svelte/`. - `JavaScript / UI Toolkit / Lit + Web Awesome` generates custom elements and a showcase from `javascript/uitk-webawesome/`. - `JavaScript / Site / React` generates a React site with Vite, Vitest, and Playwright from `javascript/site-react/`. diff --git a/copier.yaml b/copier.yaml index 5cd5885..6e204fe 100644 --- a/copier.yaml +++ b/copier.yaml @@ -42,6 +42,8 @@ add_extension: choices: Python / Pure: value: python + Python / Cython: + value: cython Python / C++: value: cpp Python / JavaScript: diff --git a/examples/cython.yaml b/examples/cython.yaml new file mode 100644 index 0000000..029da65 --- /dev/null +++ b/examples/cython.yaml @@ -0,0 +1,10 @@ +--- +add_docs: true +add_wiki: true +add_extension: cython +email: 3105306+timkpaine@users.noreply.github.com +github: python-project-templates +project_description: A Python project template with compiled Cython extensions +project_name: python template cython +python_version_primary: '3.11' +team: Python Project Template Authors diff --git a/python/cython/.gitattributes b/python/cython/.gitattributes new file mode 100644 index 0000000..b40d312 --- /dev/null +++ b/python/cython/.gitattributes @@ -0,0 +1,9 @@ +examples/* linguist-documentation +docs/* linguist-documentation +*.ipynb linguist-documentation +Makefile linguist-documentation + +*.md text=auto eol=lf +*.py text=auto eol=lf +*.toml text=auto eol=lf +*.yaml text=auto eol=lf diff --git a/python/cython/.github/CODE_OF_CONDUCT.md.jinja b/python/cython/.github/CODE_OF_CONDUCT.md.jinja new file mode 100644 index 0000000..9eab626 --- /dev/null +++ b/python/cython/.github/CODE_OF_CONDUCT.md.jinja @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, 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. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers 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, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at {{ email }}. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/python/cython/.github/ISSUE_TEMPLATE/bug_report.md.jinja b/python/cython/.github/ISSUE_TEMPLATE/bug_report.md.jinja new file mode 100644 index 0000000..1e4c149 --- /dev/null +++ b/python/cython/.github/ISSUE_TEMPLATE/bug_report.md.jinja @@ -0,0 +1,29 @@ +--- +name: Bug Report +about: Report a bug to help us improve +title: '[BUG] ' +labels: 'type: bug' +assignees: '' +--- + +**Description** +A clear and concise description of the bug. + +**Steps to Reproduce** +1. +2. +3. + +**Expected Behavior** +What you expected to happen. + +**Actual Behavior** +What actually happened. Include full error messages or tracebacks if available. + +**Environment** +- OS: [e.g. Ubuntu 22.04, macOS 14.0, Windows 11] +- Python version: [e.g. 3.11.5] (`python --version`) +- Package version: (`pip show {{project_name_formatted}} | grep Version`) + +**Additional Context** +Add any other relevant context, logs, or screenshots. diff --git a/python/cython/.github/ISSUE_TEMPLATE/feature_request.md b/python/cython/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..c3e422e --- /dev/null +++ b/python/cython/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Feature Request +about: Suggest a new feature or improvement +title: '[FEATURE] ' +labels: 'type: enhancement' +assignees: '' +--- + +**Problem Statement** +A clear description of the problem this feature would solve. Ex. "I'm always frustrated when [...]" + +**Proposed Solution** +A clear description of the desired behavior or feature. + +**Alternatives Considered** +Any alternative solutions or workarounds you've considered. + +**Additional Context** +Add any other context, mockups, or examples. diff --git a/python/cython/.github/ISSUE_TEMPLATE/question.md b/python/cython/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 0000000..ca52f7e --- /dev/null +++ b/python/cython/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,16 @@ +--- +name: Question +about: Ask a question about usage or behavior +title: '[QUESTION] ' +labels: 'tag: question' +assignees: '' +--- + +**Question** +A clear and concise description of your question. + +**Context** +What are you trying to accomplish? Include relevant code snippets, configuration, or links to documentation you've already consulted. + +**Environment** +If relevant, include your environment details (OS, language versions, package version). diff --git a/python/cython/.github/dependabot.yaml b/python/cython/.github/dependabot.yaml new file mode 100644 index 0000000..8fb3966 --- /dev/null +++ b/python/cython/.github/dependabot.yaml @@ -0,0 +1,26 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + groups: + github-actions: + patterns: + - "*" + labels: + - "part: github_actions" + + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "monthly" + cooldown: + default-days: 7 + groups: + python: + patterns: + - "*" + labels: + - "lang: python" + - "part: dependencies" diff --git a/python/cython/.github/pull_request_template.md b/python/cython/.github/pull_request_template.md new file mode 100644 index 0000000..35c3de1 --- /dev/null +++ b/python/cython/.github/pull_request_template.md @@ -0,0 +1,20 @@ +## Description + +Brief description of the changes in this PR. + +## Type of Change + +- [ ] Bug fix +- [ ] New feature +- [ ] Documentation update +- [ ] Refactor / code cleanup +- [ ] CI / build configuration +- [ ] Other (describe below) + +## Checklist + +- [ ] Linting passes (`make lint`) +- [ ] Tests pass (`make test`) +- [ ] New tests added for new functionality +- [ ] Documentation updated (if applicable) +- [ ] Changelog / version bump (if applicable) diff --git a/python/cython/.github/scripts/test-distributions.py b/python/cython/.github/scripts/test-distributions.py new file mode 100644 index 0000000..504012f --- /dev/null +++ b/python/cython/.github/scripts/test-distributions.py @@ -0,0 +1,64 @@ +"""Verify installed native wheels, including wheels rebuilt from each sdist.""" + +import json +import subprocess +import sys +import venv +from pathlib import Path +from tempfile import TemporaryDirectory +from zipfile import ZipFile + +from packaging.tags import sys_tags +from packaging.utils import parse_wheel_filename + + +def test_wheel(wheel): + with ZipFile(wheel) as archive: + modules = [ + name.split(".", 1)[0].replace("/", ".") for name in archive.namelist() if name.endswith((".so", ".pyd")) and ".dist-info/" not in name + ] + if not modules: + raise RuntimeError(f"No compiled extensions in {wheel.name}") + with TemporaryDirectory() as directory: + env = Path(directory) / "venv" + venv.EnvBuilder(with_pip=True).create(env) + python = env / ("Scripts/python.exe" if sys.platform == "win32" else "bin/python") + subprocess.run([str(python), "-m", "pip", "install", str(wheel)], check=True, cwd=directory) + subprocess.run( + [ + str(python), + "-I", + "-c", + ( + "import importlib, importlib.machinery, json, sys; " + "modules = [importlib.import_module(name) for name in json.loads(sys.argv[1])]; " + "assert all(any(m.__file__.endswith(s) for s in importlib.machinery.EXTENSION_SUFFIXES) for m in modules)" + ), + json.dumps(modules), + ], + check=True, + cwd=directory, + ) + print(f"Verified compiled imports from {wheel.name}", flush=True) + + +def main(): + compatible_tags = set(sys_tags()) + wheels = [wheel for wheel in Path("dist").glob("*.whl") if parse_wheel_filename(wheel.name)[3] & compatible_tags] + if not wheels: + raise RuntimeError("No compatible wheels in dist/") + for wheel in wheels: + test_wheel(wheel.resolve()) + for sdist in Path("dist").glob("*.tar.gz"): + with TemporaryDirectory() as directory: + subprocess.run( + [sys.executable, "-m", "uv", "build", "--wheel", "--out-dir", directory, str(sdist.resolve())], + check=True, + cwd=directory, + ) + for wheel in Path(directory).glob("*.whl"): + test_wheel(wheel) + + +if __name__ == "__main__": + main() diff --git a/python/cython/.github/workflows/build.yaml.jinja b/python/cython/.github/workflows/build.yaml.jinja new file mode 100644 index 0000000..96d31bc --- /dev/null +++ b/python/cython/.github/workflows/build.yaml.jinja @@ -0,0 +1,124 @@ +name: Build Status + +on: + push: + branches: + - main + tags: + - v* + paths-ignore: + - LICENSE + - README.md + pull_request: + branches: + - main + workflow_dispatch: + +concurrency: + group: {% raw %}${{ github.workflow }}-${{ github.head_ref || github.run_id }}{% endraw %} + cancel-in-progress: true + +permissions: + contents: read + checks: write + pull-requests: write + +jobs: + build: + name: build ({% raw %}${{ matrix.platform }}-${{ matrix.python.version }}{% endraw %}) + runs-on: {% raw %}${{ matrix.platform }}{% endraw %} + strategy: + fail-fast: false + matrix: + platform: + - ubuntu-latest + - ubuntu-24.04-arm + - macos-latest + - windows-latest + python: + {% if python_version_primary == "3.10" -%} + - version: "3.10" + cibuildwheel: "cp310" + primary: true + {% endif -%} + {% if python_version_primary in ["3.10", "3.11"] -%} + - version: "3.11" + cibuildwheel: "cp311" + primary: {{ (python_version_primary == "3.11") | lower }} + {% endif -%} + {% if python_version_primary in ["3.10", "3.11", "3.12"] -%} + - version: "3.12" + cibuildwheel: "cp312" + primary: {{ (python_version_primary == "3.12") | lower }} + {% endif -%} + - version: "3.13" + cibuildwheel: "cp313" + primary: {{ (python_version_primary == "3.13") | lower }} + - version: "3.14" + cibuildwheel: "cp314" + primary: false + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup Python + uses: actions-ext/python/setup@ec67f82b3bd863948308a41ecb9e70547cbb19c7 + with: + version: {% raw %}${{ matrix.python.version }}{% endraw %} + + - name: Setup compiler + uses: actions-ext/cpp/setup@22ebafc6f57971978e964f9977333ac98a7c49d9 + with: + clang_format: 'false' + + - name: Install dependencies + run: make develop + + - name: Lint + run: make lint + if: matrix.platform == 'ubuntu-latest' && matrix.python.primary + + - name: Checks + run: make checks + if: matrix.platform == 'ubuntu-latest' && matrix.python.primary + + - name: Test + run: make coverage + + - name: Upload test results (Python) + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: {% raw %}test-results-${{ matrix.platform }}-${{ matrix.python.version }}{% endraw %} + path: junit.xml + if: always() + + - name: Publish test results + uses: EnricoMi/publish-unit-test-result-action@d0a4676d0e0b938bc201470d88276b7c74c712b3 # v2.24.0 + with: + files: '**/junit.xml' + if: matrix.platform == 'ubuntu-latest' && matrix.python.primary && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) + + - name: Upload coverage + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + with: + token: {% raw %}${{ secrets.CODECOV_TOKEN }}{% endraw %} + + - name: Build distributions + run: | + make clean + make dist-py-wheel + make dist-check + env: + CIBW_BUILD: {% raw %}"${{ matrix.python.cibuildwheel }}-*"{% endraw %} + + - name: Test distributions + run: make test-dist + + - name: Upload distributions + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: {% raw %}dist-${{ matrix.platform }}-${{ matrix.python.version }}{% endraw %} + path: dist diff --git a/python/cython/.github/workflows/{% if add_docs %}docs.yaml{% endif %}.jinja b/python/cython/.github/workflows/{% if add_docs %}docs.yaml{% endif %}.jinja new file mode 100644 index 0000000..f3352a7 --- /dev/null +++ b/python/cython/.github/workflows/{% if add_docs %}docs.yaml{% endif %}.jinja @@ -0,0 +1,59 @@ +name: Publish Docs + +on: + workflow_run: + workflows: ["Build Status"] + branches: [main] + types: [completed] + workflow_dispatch: + +permissions: + actions: read + contents: write + +jobs: + docs: + runs-on: ubuntu-latest + if: github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup Python + uses: actions-ext/python/setup@ec67f82b3bd863948308a41ecb9e70547cbb19c7 + + - name: Download distributions + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + pattern: dist-ubuntu-latest-{{python_version_primary}} + merge-multiple: true + path: dist + run-id: {% raw %}${{ github.event.workflow_run.id }}{% endraw %} + github-token: {% raw %}${{ secrets.GITHUB_TOKEN }}{% endraw %} + repository: {% raw %}${{ github.repository }}{% endraw %} + if: github.event_name == 'workflow_run' + + - name: Install from wheel + run: | + uv pip install dist/*.whl + uv pip install dist/*.whl --target . --no-deps + uv pip install "yardang[themes]" + if: github.event_name == 'workflow_run' + + - name: Install from source (manual trigger) + run: | + uv pip install .[develop] + uv pip install "yardang[themes]" + if: github.event_name == 'workflow_dispatch' + + - name: Build documentation + run: yardang build + + - name: Publish documentation + uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4.1.0 + with: + publish_branch: gh-pages + github_token: {% raw %}${{ secrets.GITHUB_TOKEN }}{% endraw %} + publish_dir: docs/html diff --git a/python/cython/.github/workflows/{% if add_wiki %}wiki.yaml{% endif %}.jinja b/python/cython/.github/workflows/{% if add_wiki %}wiki.yaml{% endif %}.jinja new file mode 100644 index 0000000..831f804 --- /dev/null +++ b/python/cython/.github/workflows/{% if add_wiki %}wiki.yaml{% endif %}.jinja @@ -0,0 +1,34 @@ +name: Publish Wiki + +on: + push: + branches: + - main + paths: + - "docs/**" + - "README.md" + workflow_dispatch: + +concurrency: + group: docs + cancel-in-progress: true + +permissions: + contents: write + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Prepare wiki + run: cp README.md docs/wiki/Home.md + + - name: Publish wiki + uses: Andrew-Chen-Wang/github-wiki-action@1bbb4280446f9630e8e21a18012cbacf3b0f992e # v5.0.6 + with: + path: docs/wiki diff --git a/python/cython/.gitignore.jinja b/python/cython/.gitignore.jinja new file mode 100644 index 0000000..2a26300 --- /dev/null +++ b/python/cython/.gitignore.jinja @@ -0,0 +1,166 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.a +*.o +*.dSYM +*.so +*.pyd +*.dylib +{{ module }}/*.c +{{ module }}/*.cpp +*.obj +*.dll +*.exp +*.lib + +# vcpkg +vcpkg/ +vcpkg_installed/ + +# Rust +target +target-capi + +# 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 +*.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 +junit.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Django +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +Pipfile.lock + +# Celery +celerybeat-schedule +celerybeat.pid + +# Airspeed Velocity +.asv + +# 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/ + +# Documentation +/site +index.md +docs/_build/ +docs/api +docs/html +docs/index.md +docs/jupyter_execute +docs/src/_build/ +docs/superpowers +index.md + +# JS +js/coverage +js/dist +js/lib +js/node_modules +js/test-results +js/playwright-report +js/*.tgz + +# Jupyter +.ipynb_checkpoints +.autoversion +Untitled*.ipynb +{{ module }}/extension +{{ module }}/nbextension +{{ module }}/labextension + +# Emscripten SDK (locally installed) +emsdk +.pyodide_build/ + +# Mac +.DS_Store + +# Hydra +/outputs/ +/multirun/ + +# AI +ROADMAP.md +AGENTS.md +.github/hooks/sdlc.json +.superpowers diff --git a/python/cython/LICENSE.jinja b/python/cython/LICENSE.jinja new file mode 100644 index 0000000..3d7a96a --- /dev/null +++ b/python/cython/LICENSE.jinja @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 {{ team }} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/python/cython/Makefile.jinja b/python/cython/Makefile.jinja new file mode 100644 index 0000000..9d05b3b --- /dev/null +++ b/python/cython/Makefile.jinja @@ -0,0 +1,92 @@ +.DEFAULT_GOAL := help +.PHONY: develop requirements build install lint-py lint-docs lint lints fix-py fix-docs fix format check-dist check-types checks check test tests coverage show-version patch minor major dist dist-build dist-py-wheel dist-py-sdist dist-check test-dist publish clean help + +develop: ## install dependencies and build library + uv pip install -e '.[develop]' + +requirements: ## install prerequisite Python build requirements + uv pip install -r pyproject.toml --extra develop + +build: ## build the Python library + python -m build -n + +install: ## install library + uv pip install . + +lint-py: ## lint Python with ruff + python -m ruff check {{ module }} .github/scripts + python -m ruff format --check {{ module }} .github/scripts + +lint-docs: ## lint documentation + python -m mdformat --check README.md docs/development.md + python -m codespell_lib README.md docs/development.md + +fix-py: ## autoformat Python code + python -m ruff check --fix {{ module }} .github/scripts + python -m ruff format {{ module }} .github/scripts + +fix-docs: ## autoformat documentation + python -m mdformat README.md docs/development.md + python -m codespell_lib --write README.md docs/development.md + +lint: lint-py lint-docs ## run all linters +lints: lint +fix: fix-py fix-docs ## run all autoformatters +format: fix + +check-dist: ## check sdist and wheel contents + check-dist -v --rebuild + +check-types: ## check Python types (advisory) + ty check {{ module }} + +checks: check-dist ## run distribution checks +check: checks + +test: ## run Python tests + python -m pytest {{ module }}/tests + +tests: test + +coverage: ## run tests with coverage + python -m pytest {{ module }}/tests --cov={{ module }} --cov-report term-missing --cov-report xml + +show-version: ## show current library version + @bump-my-version show current_version + +patch: ## bump a patch version + @bump-my-version bump patch + +minor: ## bump a minor version + @bump-my-version bump minor + +major: ## bump a major version + @bump-my-version bump major + +dist-build: ## build local Python distributions + python -m build -w -s + +dist-py-wheel: dist-py-sdist ## build portable native wheels from the sdist + python -m cibuildwheel --output-dir dist dist/*.tar.gz + +dist-py-sdist: ## build a source distribution + python -m build --sdist --outdir dist + +dist-check: ## check distribution metadata + python -m twine check dist/* + +test-dist: ## test installed wheels and rebuild source distributions + python .github/scripts/test-distributions.py + +dist: ## build and check local distributions + $(MAKE) clean + $(MAKE) dist-build + $(MAKE) dist-check + +publish: dist + +clean: ## remove distribution build output + rm -rf build dist {{ module }}.egg-info + +help: + @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "%-24s %s\n", $$1, $$2}' $(MAKEFILE_LIST) diff --git a/python/cython/README.md.jinja b/python/cython/README.md.jinja new file mode 100644 index 0000000..ab20868 --- /dev/null +++ b/python/cython/README.md.jinja @@ -0,0 +1,16 @@ +# {{ project_name }} + +{{ project_description }} + +[![Build Status](https://github.com/{{ github }}/{{ project_name_formatted }}/actions/workflows/build.yaml/badge.svg?branch=main&event=push)](https://github.com/{{ github }}/{{ project_name_formatted }}/actions/workflows/build.yaml) +[![License](https://img.shields.io/github/license/{{ github }}/{{ project_name_formatted }})](https://github.com/{{ github }}/{{ project_name_formatted }}) + +This project demonstrates compiling Python (`_compiled.py`) and Cython (`_native.pyx`) +modules with Hatchling and hatch-cython. CI builds native wheels for Linux x86_64, +Linux ARM64, macOS ARM64, and Windows x86_64. + +See [How to build and test extensions](docs/development.md) for local development, +compilation targets, and distribution checks. + +Generated with [Copier](https://copier.readthedocs.io/en/stable/) from the +[Cython variant of the base template](https://github.com/python-project-templates/base). diff --git a/python/cython/docs/development.md.jinja b/python/cython/docs/development.md.jinja new file mode 100644 index 0000000..cf0e46b --- /dev/null +++ b/python/cython/docs/development.md.jinja @@ -0,0 +1,40 @@ +# How to build and test extensions + +Install a C compiler, Python {{ python_version_primary }}, and [uv](https://docs.astral.sh/uv/). +Use Xcode Command Line Tools on macOS or Visual Studio Build Tools on Windows. + +```bash +uv venv --python {{ python_version_primary }} +source .venv/bin/activate +make develop +make lint +make checks +make coverage +make dist +make test-dist +``` + +On Windows, activate with `.venv\Scripts\activate` instead. `make test-dist` installs +compatible wheels in fresh environments and verifies compiled imports outside the +checkout. It also rebuilds each sdist into a wheel and repeats those checks. + +To compile another `.py` or `.pyx` module, add its path to +`[tool.hatch.build.targets.wheel.hooks.cython.options.files].targets` in +`pyproject.toml`, then run `make develop` again. Only explicitly selected modules +are compiled. Keep package initialization and tests as Python sources. + +Run `make test` after changing an extension. Tests check that both example modules +load native binaries. Coverage measures Python code; the compiled `_compiled.py` +is excluded because builds do not enable Cython line tracing. + +For portable wheels, run `make dist-py-wheel` on the target platform. Linux wheel +builds require Docker. Run `make dist-py-sdist` to build an sdist without compiling +extensions. CI runs tests against each installed wheel through cibuildwheel. + +To update the template from a clean branch: + +```bash +copier update --answers-file .copier-answers.yaml --trust +``` + +Review the diff and rerun the checks above before committing. diff --git a/python/cython/pyproject.toml.jinja b/python/cython/pyproject.toml.jinja new file mode 100644 index 0000000..27e39c8 --- /dev/null +++ b/python/cython/pyproject.toml.jinja @@ -0,0 +1,189 @@ +[build-system] +requires = [ + "hatchling", + "hatch-cython", + "Cython>=3", + "setuptools", +] +build-backend = "hatchling.build" + +[project] +name = "{{ project_name_formatted }}" +authors = [ + {name = "{{ team }}", email = "{{ email }}"}, +] +description = "{{ project_description }}" +readme = "README.md" +license = { text = "Apache-2.0" } +version = "0.1.0" +requires-python = ">={{ python_version_primary }}" +keywords = [] + +classifiers = [ + "Development Status :: 3 - Alpha", + "Programming Language :: Python", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: 3", + {% if python_version_primary == "3.10" -%} + "Programming Language :: Python :: 3.10", + {% endif -%} + {% if python_version_primary in ["3.10", "3.11"] -%} + "Programming Language :: Python :: 3.11", + {% endif -%} + {% if python_version_primary in ["3.10", "3.11", "3.12"] -%} + "Programming Language :: Python :: 3.12", + {% endif -%} + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", +] + +dependencies = [] + +[project.optional-dependencies] +develop = [ + "build", + "bump-my-version", + "check-dist", + "cibuildwheel", + "codespell", + "Cython>=3", + "hatch-cython", + "hatchling", + "mdformat", + "mdformat-tables>=1", + "packaging", + "pytest", + "pytest-cov", + "ruff", + "setuptools", + "twine", + "ty", + "uv", + "wheel", +] + +[project.scripts] + +[project.urls] +Repository = "https://github.com/{{ github }}/{{ project_name_formatted }}" +Homepage = "https://github.com/{{ github }}/{{ project_name_formatted }}" + +[tool.bumpversion] +current_version = "0.1.0" +commit = true +tag = true +commit_args = "-s" + +[[tool.bumpversion.files]] +filename = "{{ module }}/__init__.py" +search = '__version__ = "{current_version}"' +replace = '__version__ = "{new_version}"' + +[[tool.bumpversion.files]] +filename = "pyproject.toml" +search = 'version = "{current_version}"' +replace = 'version = "{new_version}"' + +[tool.coverage.run] +branch = true +omit = [ + "{{ module }}/tests/integration/", + "{{ module }}/_compiled.py", +] + +[tool.coverage.report] +exclude_also = [ + "raise NotImplementedError", + "if __name__ == .__main__.:", + "@(abc\\.)?abstractmethod", +] +ignore_errors = true +fail_under = 50 + +[tool.hatch.build.targets.sdist] +include = ["/{{ module }}", "/LICENSE", "/README.md", "/pyproject.toml"] +exclude = ["*.so", "*.pyd", "*.dylib", "*.c", "*.cpp"] + +[tool.hatch.build.targets.wheel] +packages = [ + "{{ module }}", +] +artifacts = ["{{ module }}/*.so", "{{ module }}/*.pyd", "{{ module }}/*.dylib"] + +[tool.hatch.build.targets.wheel.hooks.cython.options] +src = "{{ module }}" + +[tool.hatch.build.targets.wheel.hooks.cython.options.files] +targets = ["*/_compiled.py", "*/_native.pyx"] + +[tool.setuptools] +packages = ["{{ module }}"] + +[tool.check-dist.sdist] +present = ["{{ module }}/_compiled.py", "{{ module }}/_native.pyx", "LICENSE", "README.md", "pyproject.toml"] +absent = ["{{ module }}/*.so", "{{ module }}/*.pyd", "{{ module }}/*.dylib"] + +[tool.check-dist.wheel] +present = ["{{ module }}/_compiled*.so", "{{ module }}/_native*.so"] + +[tool.cibuildwheel] +test-command = "python -m pytest --pyargs {{ module }}.tests" +test-requires = ["pytest"] + +[tool.cibuildwheel.linux] +manylinux-aarch64-image = "manylinux_2_28" +manylinux-x86_64-image = "manylinux_2_28" +archs = "native" +skip = "*i686* *musllinux*" + +[tool.cibuildwheel.macos] +environment = {MACOSX_DEPLOYMENT_TARGET="11.0"} +archs = "arm64" + +[tool.cibuildwheel.windows] +archs = "AMD64" +skip = "*win32 *arm64" + +[tool.pytest.ini_options] +addopts = [ + "-vvv", + "--junitxml=junit.xml", +] +testpaths = "{{ module }}/tests" + +[tool.ruff] +line-length = 150 + +[tool.ruff.lint] +extend-select = [ + "I", +] + +[tool.ruff.lint.isort] +combine-as-imports = true +default-section = "third-party" +known-first-party = [ + "{{ module }}", +] +section-order = [ + "future", + "standard-library", + "third-party", + "first-party", + "local-folder", +] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = [ + "F401", + "F403", +] + +{%- if add_docs %} + +[tool.yardang] +title = "{{ project_name }}" +root = "README.md" +pages = ["docs/development.md"] +use-autoapi = true +{%- endif %} diff --git a/python/cython/{% if add_wiki %}docs{% endif %}/wiki/Installation.md.jinja b/python/cython/{% if add_wiki %}docs{% endif %}/wiki/Installation.md.jinja new file mode 100644 index 0000000..89b2612 --- /dev/null +++ b/python/cython/{% if add_wiki %}docs{% endif %}/wiki/Installation.md.jinja @@ -0,0 +1,19 @@ +## Pre-requisites + +You need Python >=3.10 on your machine to install `{{ project_name_formatted }}`. + +## Install with `pip` + +```bash +pip install {{ project_name_formatted }} +``` + +## Install with `conda` + +```bash +conda install {{ project_name_formatted }} --channel conda-forge +``` + +## Source installation + +For other platforms and for development installations, [build `{{ project_name_formatted }}` from source](Build-from-Source). diff --git a/python/cython/{% if add_wiki %}docs{% endif %}/wiki/_Footer.md.jinja b/python/cython/{% if add_wiki %}docs{% endif %}/wiki/_Footer.md.jinja new file mode 100644 index 0000000..bd81a19 --- /dev/null +++ b/python/cython/{% if add_wiki %}docs{% endif %}/wiki/_Footer.md.jinja @@ -0,0 +1 @@ +_This wiki is autogenerated. To made updates, open a PR against the original source file in [`docs/wiki`](https://github.com/{{ github }}/{{ project_name_formatted }}/tree/main/docs/wiki)._ diff --git a/python/cython/{% if add_wiki %}docs{% endif %}/wiki/_Sidebar.md.jinja b/python/cython/{% if add_wiki %}docs{% endif %}/wiki/_Sidebar.md.jinja new file mode 100644 index 0000000..988b1ac --- /dev/null +++ b/python/cython/{% if add_wiki %}docs{% endif %}/wiki/_Sidebar.md.jinja @@ -0,0 +1,16 @@ + + +**[Home](Home)** + +**Get Started** + +- [Installation](Installation) +- [Contributing](Contribute) +- [Development Setup](Local-Development-Setup) +- [Build from Source](Build-from-Source) diff --git a/python/cython/{% if add_wiki %}docs{% endif %}/wiki/contribute/Build-from-Source.md.jinja b/python/cython/{% if add_wiki %}docs{% endif %}/wiki/contribute/Build-from-Source.md.jinja new file mode 100644 index 0000000..d1afac6 --- /dev/null +++ b/python/cython/{% if add_wiki %}docs{% endif %}/wiki/contribute/Build-from-Source.md.jinja @@ -0,0 +1,103 @@ +`{{ project_name_formatted }}` is written in Python. While prebuilt wheels are provided for end users, it is also straightforward to build `{{ project_name_formatted }}` from either the Python [source distribution](https://packaging.python.org/en/latest/specifications/source-distribution-format/) or the GitHub repository. + +- [Make commands](#make-commands) +- [Prerequisites](#prerequisites) +- [Clone](#clone) +- [Install Python dependencies](#install-python-dependencies) +- [Build](#build) +- [Lint and Autoformat](#lint-and-autoformat) +- [Testing](#testing) + +## Make commands + +As a convenience, `{{ project_name_formatted }}` uses a `Makefile` for commonly used commands. You can print the main available commands by running `make` with no arguments + +```bash +> make + +build build the library +clean clean the repository +fix run autofixers +install install library +lint run lints +test run the tests +``` + +## Prerequisites + +`{{ project_name_formatted }}` has a few system-level dependencies which you can install from your machine package manager. Other package managers like `conda`, `nix`, etc, should also work fine. + +## Clone + +Clone the repo with: + +```bash +git clone https://github.com/{{ github }}/{{ project_name_formatted }}.git +cd {{ project_name_formatted }} +``` + +## Install Python dependencies + +Python build and develop dependencies are specified in the `pyproject.toml`, but you can manually install them: + +```bash +make requirements +``` + +Note that these dependencies would otherwise be installed normally as part of [PEP517](https://peps.python.org/pep-0517/) / [PEP518](https://peps.python.org/pep-0518/). + +## Build + +Build the python project in the usual manner: + +```bash +make build +``` + +## Lint and Autoformat + +`{{ project_name_formatted }}` has linting and auto formatting. + +| Language | Linter | Autoformatter | Description | +| :------- | :---------- | :------------ | :---------- | +| Python | `ruff` | `ruff` | Style | +| Markdown | `mdformat` | `mdformat` | Style | +| Markdown | `codespell` | | Spelling | + +**Python Linting** + +```bash +make lint-py +``` + +**Python Autoformatting** + +```bash +make fix-py +``` + +**Documentation Linting** + +```bash +make lint-docs +``` + +**Documentation Autoformatting** + +```bash +make fix-docs +``` + +## Testing + +`{{ project_name_formatted }}` has extensive Python tests. The tests can be run via `pytest`. First, install the Python development dependencies with + +```bash +make develop +``` + +**Python** + +```bash +make test +``` diff --git a/python/cython/{% if add_wiki %}docs{% endif %}/wiki/contribute/Contribute.md.jinja b/python/cython/{% if add_wiki %}docs{% endif %}/wiki/contribute/Contribute.md.jinja new file mode 100644 index 0000000..463b117 --- /dev/null +++ b/python/cython/{% if add_wiki %}docs{% endif %}/wiki/contribute/Contribute.md.jinja @@ -0,0 +1,15 @@ +Contributions are welcome on this project. We distribute under the terms of the [Apache 2.0 license](https://github.com/{{ github }}/{{ project_name_formatted }}/blob/main/LICENSE). + +> [!NOTE] +> +> `{{ project_name_formatted }}` requires [Developer Certificate of Origin](https://en.wikipedia.org/wiki/Developer_Certificate_of_Origin) for all contributions. +> This is enforced by a [Probot GitHub App](https://probot.github.io/apps/dco/), which checks that commits are "signed". +> Read [instructions to configure commit signing](Local-Development-Setup#configure-commit-signing). + +For **bug reports** or **small feature requests**, please open an issue on our [issues page](https://github.com/{{ github }}/{{ project_name_formatted }}/issues). + +For **questions** or to discuss **larger changes or features**, please use our [discussions page](https://github.com/{{ github }}/{{ project_name_formatted }}/discussions). + +For **contributions**, please see our [developer documentation](Local-Development-Setup). We have `help wanted` and `good first issue` tags on our issues page, so these are a great place to start. + +For **documentation updates**, make PRs that update the pages in `/docs/wiki`. The documentation is pushed to the GitHub wiki automatically through a GitHub workflow. Note that direct updates to this wiki will be overwritten. diff --git a/python/cython/{% if add_wiki %}docs{% endif %}/wiki/contribute/Local-Development-Setup.md.jinja b/python/cython/{% if add_wiki %}docs{% endif %}/wiki/contribute/Local-Development-Setup.md.jinja new file mode 100644 index 0000000..abf413b --- /dev/null +++ b/python/cython/{% if add_wiki %}docs{% endif %}/wiki/contribute/Local-Development-Setup.md.jinja @@ -0,0 +1,55 @@ +## Table of Contents + +- [Table of Contents](#table-of-contents) +- [Step 1: Build from Source](#step-1-build-from-source) +- [Step 2: Configuring Git and GitHub for Development](#step-2-configuring-git-and-github-for-development) + - [Create your fork](#create-your-fork) + - [Configure remotes](#configure-remotes) + - [Authenticating with GitHub](#authenticating-with-github) +- [Guidelines](#guidelines) + +## Step 1: Build from Source + +To work on `{{ project_name_formatted }}`, you are going to need to build it from source. See +[Build from Source](Build-from-Source) for +detailed build instructions. + +Once you've built `{{ project_name_formatted }}` from a `git` clone, you will also need to +configure `git` and your GitHub account for `{{ project_name_formatted }}` development. + +## Step 2: Configuring Git and GitHub for Development + +### Create your fork + +The first step is to create a personal fork of `{{ project_name_formatted }}`. To do so, click +the "fork" button at https://github.com/{{ github }}/{{ project_name_formatted }}, or just navigate +[here](https://github.com/{{ github }}/{{ project_name_formatted }}/fork) in your browser. Set the +owner of the repository to your personal GitHub account if it is not +already set that way and click "Create fork". + +### Configure remotes + +Next, you should set some names for the `git` remotes corresponding to +main {{ github }} repository and your fork. See the [GitHub Docs](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/configuring-a-remote-repository-for-a-fork) for more information. + +### Authenticating with GitHub + +If you have not already configured `ssh` access to GitHub, you can find +instructions to do so +[here](https://docs.github.com/en/authentication/connecting-to-github-with-ssh), +including instructions to create an SSH key if you have not done +so. Authenticating with SSH is usually the easiest route. If you are working in +an environment that does not allow SSH connections to GitHub, you can look into +[configuring a hardware +passkey](https://docs.github.com/en/authentication/authenticating-with-a-passkey/about-passkeys) +or adding a [personal access +token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) +to avoid the need to type in your password every time you push to your fork. + +## Guidelines + +After developing a change locally, ensure that both [lints](Build-from-Source#lint-and-autoformat) and [tests](Build-from-Source#testing) pass. Commits should be squashed into logical units, and all commits must be signed (e.g. with the `-s` git flag). We require [Developer Certificate of Origin](https://en.wikipedia.org/wiki/Developer_Certificate_of_Origin) for all contributions. + +If your work is still in-progress, open a [draft pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests#draft-pull-requests). Otherwise, open a normal pull request. It might take a few days for a maintainer to review and provide feedback, so please be patient. If a maintainer asks for changes, please make said changes and squash your commits if necessary. If everything looks good to go, a maintainer will approve and merge your changes for inclusion in the next release. + +Please note that non substantive changes, large changes without prior discussion, etc, are not accepted and pull requests may be closed. diff --git a/python/cython/{{_copier_conf.answers_file}}.jinja b/python/cython/{{_copier_conf.answers_file}}.jinja new file mode 100644 index 0000000..a96840d --- /dev/null +++ b/python/cython/{{_copier_conf.answers_file}}.jinja @@ -0,0 +1,2 @@ +# Changes here will be overwritten by Copier +{{ _copier_answers|to_nice_yaml -}} diff --git a/python/cython/{{module}}/__init__.py b/python/cython/{{module}}/__init__.py new file mode 100644 index 0000000..22e626c --- /dev/null +++ b/python/cython/{{module}}/__init__.py @@ -0,0 +1,4 @@ +from ._compiled import square +from ._native import add + +__version__ = "0.1.0" diff --git a/python/cython/{{module}}/_compiled.py b/python/cython/{{module}}/_compiled.py new file mode 100644 index 0000000..0e14806 --- /dev/null +++ b/python/cython/{{module}}/_compiled.py @@ -0,0 +1,2 @@ +def square(value: int) -> int: + return value * value diff --git a/python/cython/{{module}}/_native.pyx b/python/cython/{{module}}/_native.pyx new file mode 100644 index 0000000..2418837 --- /dev/null +++ b/python/cython/{{module}}/_native.pyx @@ -0,0 +1,2 @@ +def add(int left, int right): + return left + right diff --git a/python/cython/{{module}}/tests/__init__.py b/python/cython/{{module}}/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/cython/{{module}}/tests/test_extensions.py.jinja b/python/cython/{{module}}/tests/test_extensions.py.jinja new file mode 100644 index 0000000..20acbfa --- /dev/null +++ b/python/cython/{{module}}/tests/test_extensions.py.jinja @@ -0,0 +1,19 @@ +from importlib.machinery import EXTENSION_SUFFIXES + +import pytest + +from {{ module }} import _compiled, _native, add, square + + +@pytest.mark.parametrize("module", [_compiled, _native]) +def test_compiled_extension(module): + assert any(module.__file__.endswith(suffix) for suffix in EXTENSION_SUFFIXES) + + +@pytest.mark.parametrize("value", [-3, 0, 4]) +def test_square(value): + assert square(value) == value * value + + +def test_add(): + assert add(2, 3) == 5 From a85ca0b8c35a4ad5b2435d2841377f277016e0fe Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Mon, 7 Sep 2026 15:55:42 -0400 Subject: [PATCH 2/6] Keep the Cython instance docs-only and simplify extension checks Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com> --- examples/cython.yaml | 2 +- python/cython/.github/scripts/test-distributions.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/cython.yaml b/examples/cython.yaml index 029da65..2620a1d 100644 --- a/examples/cython.yaml +++ b/examples/cython.yaml @@ -1,6 +1,6 @@ --- add_docs: true -add_wiki: true +add_wiki: false add_extension: cython email: 3105306+timkpaine@users.noreply.github.com github: python-project-templates diff --git a/python/cython/.github/scripts/test-distributions.py b/python/cython/.github/scripts/test-distributions.py index 504012f..e2d734a 100644 --- a/python/cython/.github/scripts/test-distributions.py +++ b/python/cython/.github/scripts/test-distributions.py @@ -14,9 +14,10 @@ def test_wheel(wheel): with ZipFile(wheel) as archive: - modules = [ - name.split(".", 1)[0].replace("/", ".") for name in archive.namelist() if name.endswith((".so", ".pyd")) and ".dist-info/" not in name - ] + modules = [] + for name in archive.namelist(): + if name.endswith((".so", ".pyd")) and ".dist-info/" not in name: + modules.append(name.split(".", 1)[0].replace("/", ".")) if not modules: raise RuntimeError(f"No compiled extensions in {wheel.name}") with TemporaryDirectory() as directory: From 4dfea452f571851575f8652694a96095ee8d59da Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Mon, 7 Sep 2026 15:57:10 -0400 Subject: [PATCH 3/6] Generate Cython instances from a reproducible template revision Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com> --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index dca728b..41d1dc3 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,7 @@ gen-python: ## regenerate the python template from scratch .PHONY: gen-cython test-cython gen-cython: ## regenerate the Cython template - copier copy --defaults --overwrite --vcs-ref=HEAD . ../python-template-cython --data-file examples/cython.yaml + copier copy --defaults --overwrite --vcs-ref=$$(git rev-parse HEAD) . ../python-template-cython --data-file examples/cython.yaml cd ../python-template-cython && $(SED) 's#_src_path: .#_src_path: https://github.com/python-project-templates/base.git#g' ./.copier-answers.yaml test-cython: ## test the generated Cython project From 3ab9bd27a5022fc80257494b2bb898c1df04c9a9 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Mon, 7 Sep 2026 17:01:29 -0400 Subject: [PATCH 4/6] Align Cython scaffolding with the shared templates Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com> --- python/cython/.gitignore.jinja | 4 ++-- python/cython/Makefile.jinja | 18 ++++++++++++------ python/cython/README.md.jinja | 14 +++++++------- python/cython/pyproject.toml.jinja | 4 ++-- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/python/cython/.gitignore.jinja b/python/cython/.gitignore.jinja index 2a26300..9cdff91 100644 --- a/python/cython/.gitignore.jinja +++ b/python/cython/.gitignore.jinja @@ -10,8 +10,8 @@ __pycache__/ *.so *.pyd *.dylib -{{ module }}/*.c -{{ module }}/*.cpp +{{ module }}/_compiled.c +{{ module }}/_native.c *.obj *.dll *.exp diff --git a/python/cython/Makefile.jinja b/python/cython/Makefile.jinja index 9d05b3b..6a723fe 100644 --- a/python/cython/Makefile.jinja +++ b/python/cython/Makefile.jinja @@ -1,5 +1,5 @@ .DEFAULT_GOAL := help -.PHONY: develop requirements build install lint-py lint-docs lint lints fix-py fix-docs fix format check-dist check-types checks check test tests coverage show-version patch minor major dist dist-build dist-py-wheel dist-py-sdist dist-check test-dist publish clean help +.PHONY: develop requirements build install lint-py lint-docs lint lints fix-py fix-docs fix format check-dist check-types checks check test tests coverage show-version patch minor major dist dist-build dist-py-wheel dist-py-sdist dist-check test-dist publish deep-clean clean help develop: ## install dependencies and build library uv pip install -e '.[develop]' @@ -18,16 +18,16 @@ lint-py: ## lint Python with ruff python -m ruff format --check {{ module }} .github/scripts lint-docs: ## lint documentation - python -m mdformat --check README.md docs/development.md - python -m codespell_lib README.md docs/development.md + python -m mdformat --check README.md docs/development.md{% if add_wiki %} docs/wiki/{% endif %} + python -m codespell_lib README.md docs/development.md{% if add_wiki %} docs/wiki/{% endif %} fix-py: ## autoformat Python code python -m ruff check --fix {{ module }} .github/scripts python -m ruff format {{ module }} .github/scripts fix-docs: ## autoformat documentation - python -m mdformat README.md docs/development.md - python -m codespell_lib --write README.md docs/development.md + python -m mdformat README.md docs/development.md{% if add_wiki %} docs/wiki/{% endif %} + python -m codespell_lib --write README.md docs/development.md{% if add_wiki %} docs/wiki/{% endif %} lint: lint-py lint-docs ## run all linters lints: lint @@ -85,8 +85,14 @@ dist: ## build and check local distributions publish: dist +deep-clean: ## clean everything from the repository + git clean -fdx + clean: ## remove distribution build output rm -rf build dist {{ module }}.egg-info help: - @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "%-24s %s\n", $$1, $$2}' $(MAKEFILE_LIST) + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +print-%: + @echo '$*=$($*)' diff --git a/python/cython/README.md.jinja b/python/cython/README.md.jinja index ab20868..ce66543 100644 --- a/python/cython/README.md.jinja +++ b/python/cython/README.md.jinja @@ -3,14 +3,14 @@ {{ project_description }} [![Build Status](https://github.com/{{ github }}/{{ project_name_formatted }}/actions/workflows/build.yaml/badge.svg?branch=main&event=push)](https://github.com/{{ github }}/{{ project_name_formatted }}/actions/workflows/build.yaml) +[![codecov](https://codecov.io/gh/{{ github }}/{{ project_name_formatted }}/branch/main/graph/badge.svg)](https://codecov.io/gh/{{ github }}/{{ project_name_formatted }}) [![License](https://img.shields.io/github/license/{{ github }}/{{ project_name_formatted }})](https://github.com/{{ github }}/{{ project_name_formatted }}) +[![PyPI](https://img.shields.io/pypi/v/{{ project_name_formatted }}.svg)](https://pypi.python.org/pypi/{{ project_name_formatted }}) -This project demonstrates compiling Python (`_compiled.py`) and Cython (`_native.pyx`) -modules with Hatchling and hatch-cython. CI builds native wheels for Linux x86_64, -Linux ARM64, macOS ARM64, and Windows x86_64. +## Overview -See [How to build and test extensions](docs/development.md) for local development, -compilation targets, and distribution checks. +> [!NOTE] +> This library was generated using [copier](https://copier.readthedocs.io/en/stable/) from the [Base Python Project Template repository](https://github.com/python-project-templates/base). -Generated with [Copier](https://copier.readthedocs.io/en/stable/) from the -[Cython variant of the base template](https://github.com/python-project-templates/base). +The examples compile Python (`_compiled.py`) and Cython (`_native.pyx`) modules with hatch-cython. +See the [development guide](docs/development.md) for build and test instructions. diff --git a/python/cython/pyproject.toml.jinja b/python/cython/pyproject.toml.jinja index 27e39c8..2a3cfb2 100644 --- a/python/cython/pyproject.toml.jinja +++ b/python/cython/pyproject.toml.jinja @@ -101,8 +101,8 @@ ignore_errors = true fail_under = 50 [tool.hatch.build.targets.sdist] -include = ["/{{ module }}", "/LICENSE", "/README.md", "/pyproject.toml"] -exclude = ["*.so", "*.pyd", "*.dylib", "*.c", "*.cpp"] +packages = ["{{ module }}"] +exclude = ["*.so", "*.pyd", "*.dylib", "{{ module }}/_compiled.c", "{{ module }}/_native.c"] [tool.hatch.build.targets.wheel] packages = [ From 6a0f1bb6436cdd8cc04324ab9ae39bb54543e9be Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Mon, 7 Sep 2026 17:11:17 -0400 Subject: [PATCH 5/6] Use the shared README without Cython-specific prose Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com> --- python/cython/README.md.jinja | 3 --- 1 file changed, 3 deletions(-) diff --git a/python/cython/README.md.jinja b/python/cython/README.md.jinja index ce66543..7e11b1c 100644 --- a/python/cython/README.md.jinja +++ b/python/cython/README.md.jinja @@ -11,6 +11,3 @@ > [!NOTE] > This library was generated using [copier](https://copier.readthedocs.io/en/stable/) from the [Base Python Project Template repository](https://github.com/python-project-templates/base). - -The examples compile Python (`_compiled.py`) and Cython (`_native.pyx`) modules with hatch-cython. -See the [development guide](docs/development.md) for build and test instructions. From 4d84a6b472c9fe1d5f65b97740fd59a0d5573eda Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Mon, 7 Sep 2026 18:03:28 -0400 Subject: [PATCH 6/6] Fix Jupyter template builds with the standalone builder Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com> --- python/jupyter/js/package.json.jinja | 9 ++++++--- python/jupyter/pyproject.toml.jinja | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/python/jupyter/js/package.json.jinja b/python/jupyter/js/package.json.jinja index 531bb31..b4fdf0a 100644 --- a/python/jupyter/js/package.json.jinja +++ b/python/jupyter/js/package.json.jinja @@ -33,7 +33,7 @@ "build:babel": "babel src/ --source-maps --out-dir lib/", "build:extension": "mkdirp ../{{ module }}/extension/ && cpy --flat 'src/extension/*' '../{{ module }}/extension/'", "build:nbextension": "mkdirp ../{{ module }}/nbextension/static/ && cpy --flat 'src/notebook.js' '../{{ module }}/nbextension/static/'", - "build:labextension": "rimraf ../{{ module }}/labextension && jupyter labextension build .", + "build:labextension": "rimraf ../{{ module }}/labextension && jupyter-builder build .", "build": "pnpm clean && pnpm build:babel && pnpm build:extension && pnpm build:labextension && pnpm build:nbextension", "clean": "rimraf lib", "fix": "oxlint --fix src/ tests/ && oxfmt --write \"src/**/*.js\" \"tests/**/*.js\"", @@ -53,9 +53,11 @@ "@babel/cli": "^8.0.4", "@babel/core": "^8.0.1", "@babel/preset-env": "^8.0.2", - "@jupyterlab/builder": "^4.5.10", + "@jupyter/builder": "^1.2.3", + "@jupyterlab/core-meta": "^4.6.3", "babel-jest": "^30.5.1", "cpy-cli": "^7.0.0", + "css-loader": "^7.1.4", "isomorphic-fetch": "^3.0.0", "jest": "^30.5.1", "jest-environment-jsdom": "^30.5.1", @@ -64,6 +66,7 @@ "mkdirp": "^3.0.1", "oxfmt": "^0.66.0", "oxlint": "^1.81.0", - "rimraf": "^6.1.3" + "rimraf": "^6.1.3", + "style-loader": "^4.0.0" } } diff --git a/python/jupyter/pyproject.toml.jinja b/python/jupyter/pyproject.toml.jinja index 7bd9162..ee13312 100644 --- a/python/jupyter/pyproject.toml.jinja +++ b/python/jupyter/pyproject.toml.jinja @@ -2,7 +2,7 @@ requires = [ "hatchling", "hatch-js", - "jupyterlab>=4,<5", + "jupyter-builder>=1.2.3,<2", ] build-backend = "hatchling.build" @@ -52,6 +52,7 @@ develop = [ "codespell", "hatch-js", "hatchling", + "jupyter-builder>=1.2.3,<2", "mdformat", "mdformat-tables>=1", "jupyterlab>=4,<5",