Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: setup cython example #5

Draft
wants to merge 18 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"vscode": {
"extensions": [
"ms-python.python",
"ms-python.black-formatter"
"ms-python.black-formatter",
"ms-vscode.cpptools-extension-pack"
],
"settings": {
"python.analysis.typeCheckingMode": "strict",
Expand Down
102 changes: 71 additions & 31 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,51 +17,91 @@ jobs:
with:
release-type: python

- uses: actions/checkout@v3
if: ${{ steps.release.outputs.release_created }}
build_wheels:
name: Build wheels for ${{ matrix.arch }} ${{ matrix.os }} py3.${{ matrix.python }}
needs: release
runs-on: ${{ matrix.os }}-latest
if: ${{ needs.release.outputs.release_created }}
strategy:
fail-fast: false
matrix:
python: [9, 10, 11, 12, 13]
os: [ubuntu, macos]
arch: [x86_64, aarch64, universal2]
exclude:
- os: macos
arch: universal2
python: 7
- os: ubuntu
arch: universal2
- os: macos
arch: x86_64
- os: macos
arch: aarch64

steps:
- uses: actions/checkout@v4

- name: Set up QEMU for aarch64 emulation
if: runner.os == 'Linux' && matrix.arch == 'aarch64'
uses: docker/setup-qemu-action@v3
with:
fetch-depth: 2
platforms: all

- name: Build wheels
uses: pypa/[email protected]
env:
CIBW_SKIP: "*-musllinux_* pp3*-manylinux_aarch64"
CIBW_BUILD: ${{ format('*p3{0}-*', matrix.python) }}
CIBW_ARCHS: ${{ matrix.arch }}
CIBW_TEST_REQUIRES: pytest
CIBW_TEST_COMMAND: "pytest {package}/tests"

- name: Set up Python
if: ${{ steps.release.outputs.release_created }}
uses: actions/setup-python@v4
- uses: actions/upload-artifact@v4
with:
python-version: '3.9' # keep synced with dev-env.yml
name: wheels-${{ matrix.os }}-${{ matrix.arch }}-3${{ matrix.python }}
path: ./wheelhouse/*.whl

- name: Upgrade pip
if: ${{ steps.release.outputs.release_created }}
run: |
pip install --upgrade pip
pip --version
merge_wheels:
runs-on: ubuntu-latest
needs: build_wheels
steps:
- name: Merge wheel artifacts into a single artifact
uses: actions/upload-artifact/merge@v4
with:
name: wheels
pattern: wheels-*
delete-merged: true

- name: Install Poetry
if: ${{ steps.release.outputs.release_created }}
run: |
pip install 'poetry==1.8.3' # keep version synced with dev-env.yml
poetry --version
build_sdist:
needs: release
runs-on: ubuntu-latest
if: ${{ needs.release.outputs.release_created }}
env:
job_python_version: "3.9" # keep synced with dev-env.yml

- name: Bump version for developmental release
if: ${{ steps.release.outputs.release_created }}
env:
version: ${{ steps.release.outputs.tag_name }}
run: |
poetry version $version
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python ${{ env.job_python_version }}
uses: actions/setup-python@v5
with:
python-version: ${{ env.job_python_version }}

- name: Build package
if: ${{ steps.release.outputs.release_created }}
- name: Create source distribution
run: |
poetry build --ansi
pip install build
python -m build --sdist .

- uses: actions/upload-artifact@v4
if: ${{ steps.release.outputs.release_created }}
with:
name: dist
path: dist/
name: sdist
path: dist/panct-*.tar.gz

upload_pypi:
needs: release
needs: [build_wheels, build_sdist]
runs-on: ubuntu-latest
if: ${{ needs.release.outputs.release_created }}
environment: release
permissions:
# IMPORTANT: this permission is mandatory for trusted publishing
Expand Down
11 changes: 8 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ jobs:
auto-activate-base: false
miniforge-version: latest
use-mamba: true
conda-remove-defaults: "true"

- name: Get Date
id: get-date
Expand All @@ -48,15 +49,19 @@ jobs:
with:
path: ${{ env.CONDA }}/envs
key:
conda-${{ runner.os }}--${{ runner.arch }}--${{ steps.get-date.outputs.today }}-${{ hashFiles('dev-env.yml') }}-${{ env.CACHE_NUMBER }}
conda-${{ runner.os }}--${{ runner.arch }}--${{ steps.get-date.outputs.today }}-${{ hashFiles('dev-env.yml') }}-${{ matrix.python }}-${{ env.CACHE_NUMBER }}
env:
# Increase this value to reset cache if dev-env.yml has not changed
CACHE_NUMBER: 0
id: cache

- name: Install dev environment
run:
mamba env update -n panct -f dev-env.yml
env:
PYTHON_VERSION: ${{ matrix.python }}
run: |
# sync the python version in the dev-env.yml file
sed s'/python=3.[[:digit:]]\+ /python='"$PYTHON_VERSION"' /' dev-env.yml > dev-env.new.yml
mamba env update -n panct -f dev-env.new.yml
if: steps.cache.outputs.cache-hit != 'true'

- name: Try to build panct
Expand Down
35 changes: 35 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,38 @@ fil-result/

# OSX
*.DS_Store*

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so
*.pyd

# 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

# Cython
panct/*.c
panct/*.cpp
panct/*.html
2 changes: 2 additions & 0 deletions NOTES.md
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: delete this file before merging

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
1. How do we want to include the gbz-base source code in the repo? Options: git submodule or install as setup step
2. Do we want to automatically skip compilation of the cython module if it doesn't work? Or should we just fail/error explicitly. The former could be helpful if we want to implement some kind of slower alternative
85 changes: 85 additions & 0 deletions build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# type: ignore
import os
import shutil
from pathlib import Path

# This file was adapted from https://github.com/BrianPugh/python-template/blob/main/build.py

# Uncomment if library can still function if extensions fail to compile (e.g. slower, python fallback).
# Don't allow failure if cibuildwheel is running.
# allowed_to_fail = os.environ.get("CIBUILDWHEEL", "0") != "1"
allowed_to_fail = False


def build_cython_extensions():
# when using setuptools, you should import setuptools before Cython,
# otherwise, both might disagree about the class to use.
from setuptools import Extension # noqa: I001
from setuptools.dist import Distribution # noqa: I001
import Cython.Compiler.Options # pyright: ignore [reportMissingImports]
from Cython.Build import (
build_ext,
cythonize,
) # pyright: ignore [reportMissingImports]

Cython.Compiler.Options.annotate = True

if os.name == "nt": # Windows
extra_compile_args = [
"/O2",
]
else: # UNIX-based systems
extra_compile_args = [
"-O3",
"-Werror",
"-Wno-unreachable-code-fallthrough",
"-Wno-deprecated-declarations",
"-Wno-parentheses-equality",
]
extra_compile_args.append("-UNDEBUG") # Cython disables asserts by default.
# Relative to project root director
include_dirs = [
"panct/",
"panct/_c_src",
]

c_files = [str(x) for x in Path("panct/_c_src").rglob("*.c")]
extensions = [
Extension(
# Your .pyx file will be available to cpython at this location.
"panct._c_extension",
[
# ".c" and ".pyx" source file paths
"panct/_c_extension.pyx",
*c_files,
],
include_dirs=include_dirs,
extra_compile_args=extra_compile_args,
language="c",
),
]

include_dirs = set()
for extension in extensions:
include_dirs.update(extension.include_dirs)
include_dirs = list(include_dirs)

ext_modules = cythonize(
extensions, include_path=include_dirs, language_level=3, annotate=True
)
dist = Distribution({"ext_modules": ext_modules})
cmd = build_ext(dist)
cmd.ensure_finalized()
cmd.run()

for output in cmd.get_outputs():
output = Path(output)
relative_extension = output.relative_to(cmd.build_lib)
shutil.copyfile(output, relative_extension)


try:
build_cython_extensions()
except Exception:
if not allowed_to_fail:
raise
2 changes: 1 addition & 1 deletion dev-env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ channels:
dependencies:
- conda-forge::python=3.9 # the lowest version of python that we formally support; keep in sync with release.yml, .readthedocs.yaml, and noxfile.py
- conda-forge::pip==24.0
- conda-forge::poetry==1.8.3 # should keep this in sync with version in release.yml
- conda-forge::poetry==1.8.3
- conda-forge::nox==2024.4.15
- conda-forge::poetry-plugin-export==1.8.0
- pip:
Expand Down
15 changes: 14 additions & 1 deletion noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ def docs(session: Session) -> None:
if build_dir.exists():
shutil.rmtree(build_dir)

session.install(".")
session.install(
"sphinx",
"sphinx-autodoc-typehints",
"sphinx-rtd-theme",
"numpydoc",
"sphinx-click",
)
session.run("sphinx-build", *args)


Expand Down Expand Up @@ -63,10 +71,12 @@ def install_handle_python_numpy(session):
@session(venv_backend=conda_cmd, venv_params=conda_args, python=python_versions)
def tests(session: Session) -> None:
"""Run the test suite."""
# first, delete any existing envs to avoid
# https://github.com/cjolowicz/nox-poetry/issues/1188
session.run("poetry", "env", "remove", "--all")
session.conda_install(
"coverage[toml]",
"pytest",
"numpy>=1.20.0",
channel="conda-forge",
)
install_handle_python_numpy(session)
Expand All @@ -83,6 +93,9 @@ def tests(session: Session) -> None:
@session(python=python_versions)
def tests(session: Session) -> None:
"""Run the test suite."""
# first, delete any existing envs to avoid
# https://github.com/cjolowicz/nox-poetry/issues/1188
session.run("poetry", "env", "remove", "--all")
session.install("coverage[toml]", "pytest")
install_handle_python_numpy(session)
try:
Expand Down
6 changes: 1 addition & 5 deletions panct/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
try:
from importlib.metadata import version, PackageNotFoundError
except ImportError:
# handles py3.7, since importlib.metadata was introduced in py3.8
from importlib_metadata import version, PackageNotFoundError
from importlib.metadata import version, PackageNotFoundError

try:
__version__ = version(__name__)
Expand Down
5 changes: 5 additions & 0 deletions panct/_c_extension.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class Foo:
def __init__(self): ...
def __call__(self): ...

def divide(x: float, y: float) -> float: ...
40 changes: 40 additions & 0 deletions panct/_c_extension.pyx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
cimport cpythontemplate # See cpythontemplate.pxd
# Invoke PyErr_CheckSignals() occasionally if your C code runs long.
# This allows your code to be interrupted via ctrl+c.
from cpython.exc cimport PyErr_CheckSignals
from cpython.mem cimport PyMem_Malloc, PyMem_Free
from libc.stddef cimport size_t


cdef class Foo:
"""Pythonic interface to the C "foo" struct."""
cdef cpythontemplate.foo_t * _object

def __cinit__(self):
# Automatically called before __init__.
# All arguments passed to __init__ are also passed to __cinit__.
# * As a convenience, if __cinit__() takes no arguments (other than self), it will
# ignore arguments passed to the constructor without complaining about signature mismatch.
# Allocate memory for C objects here.
self._object = <cpythontemplate.foo_t *>PyMem_Malloc(sizeof(cpythontemplate.foo_t))
if self._object is NULL:
raise MemoryError

def __dealloc__(self):
# Should "undo" __cinit__
PyMem_Free(self._object)

def __init__(self):
cpythontemplate.foo_init(self._object)

def __call__(self):
# invoke increment
cpythontemplate.foo_increment(self._object)

# Functions declared with cpdef are visible to both cython and python.
# https://cython.readthedocs.io/en/latest/src/userguide/language_basics.html#python-functions-vs-c-functions
# https://cython.readthedocs.io/en/latest/src/userguide/language_basics.html#error-return-values
cpdef float divide(float x, float y) except? 1.23:
if y == 0.0:
raise ZeroDivisionError
return x / y
Loading
Loading