diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c827810..d18ebfa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,6 @@ on: push: branches: [ main ] pull_request: - branches: [ main ] jobs: test: @@ -39,7 +38,12 @@ jobs: - name: Test source distribution build run: | uv run python -m build --sdist - + + - name: Build manylinux wheel and test prebuilt install + run: bash scripts/verify_wheel_install.sh + env: + CIBW_BUILD: cp311-manylinux_x86_64 + - name: Test installation from source run: | # Test that the source distribution can be installed diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index f589c61..6f7074c 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.11" @@ -32,25 +32,69 @@ jobs: - uses: actions/upload-artifact@v4 with: - name: python-package-distributions + name: cibw-sdist path: dist/*.tar.gz + build_wheels: + name: Build wheels (${{ matrix.os }}) + runs-on: ${{ matrix.runs-on }} + strategy: + fail-fast: false + matrix: + include: + - os: linux + runs-on: ubuntu-latest + - os: windows + runs-on: windows-latest + - os: macos-intel + runs-on: macos-15-intel + - os: macos-arm + runs-on: macos-latest + + steps: + - uses: actions/checkout@v4 + + - name: Build wheels + uses: pypa/cibuildwheel@v3.4.1 + + - uses: actions/upload-artifact@v4 + with: + name: cibw-wheels-${{ matrix.os }} + path: ./wheelhouse/*.whl + + verify_wheel_install: + name: Verify Linux wheel before PyPI + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Build manylinux wheel and test prebuilt install + run: bash scripts/verify_wheel_install.sh + env: + CIBW_BUILD: cp311-manylinux_x86_64 + upload_pypi: - needs: [build_sdist] + needs: [build_sdist, build_wheels, verify_wheel_install] runs-on: ubuntu-latest if: github.event_name == 'release' && github.event.action == 'published' environment: name: pypi url: https://pypi.org/p/python-uuidv47 - + steps: - - name: Download all artifacts + - name: Download distributions uses: actions/download-artifact@v4 with: - name: python-package-distributions - path: dist/ + pattern: cibw-* + path: dist + merge-multiple: true - - name: Publish distribution 📦 to PyPI + - name: Publish distribution to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: - password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.gitignore b/.gitignore index a25df5f..5fff847 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ parts/ sdist/ var/ wheels/ +wheelhouse/ share/python-wheels/ *.egg-info/ .installed.cfg diff --git a/pyproject.toml b/pyproject.toml index 6f0f42b..8458a24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,4 +80,7 @@ strict = true warn_return_any = true warn_unused_configs = true +[tool.cibuildwheel] +# Match Trove classifiers (CPython 3.9–3.12 only) +build = "cp39-* cp310-* cp311-* cp312-*" diff --git a/scripts/verify_wheel_install.sh b/scripts/verify_wheel_install.sh new file mode 100644 index 0000000..8351d64 --- /dev/null +++ b/scripts/verify_wheel_install.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Build one manylinux wheel (selector via CIBW_BUILD) and run tests against a clean venv. +# Used on PR CI and in the release workflow before PyPI upload. +set -euxo pipefail + +: "${CIBW_BUILD:=cp311-manylinux_x86_64}" + +python -m pip install --upgrade pip +pip install cibuildwheel +cibuildwheel --platform linux --output-dir wheelhouse . + +WHEEL="$(ls wheelhouse/python_uuidv47-*.whl | head -n1)" +test -n "${WHEEL}" + +python -m venv .venv-wheeltest +.venv-wheeltest/bin/pip install --upgrade pip +.venv-wheeltest/bin/pip install pytest "${WHEEL}" +.venv-wheeltest/bin/python -m pytest \ + tests/test_wheel_package.py \ + tests/test_uuidv47.py \ + tests/test_error_handling.py \ + -v --tb=short diff --git a/tests/test_wheel_package.py b/tests/test_wheel_package.py new file mode 100644 index 0000000..7490459 --- /dev/null +++ b/tests/test_wheel_package.py @@ -0,0 +1,55 @@ +"""Assertions for installs that ship a precompiled extension (wheels / binary installs). + +These tests pass for a normal editable or ``pip install -e .`` build as long as the +Cython extension is compiled to a native module. They fail loudly if the accelerator +is missing or only source artifacts are visible at import time. +""" + +from __future__ import annotations + +from pathlib import Path + +import python_uuidv47._uuidv47 as _uuidv47 + +import python_uuidv47 +from python_uuidv47 import decode, encode, has_keys, set_keys, uuid_parse + + +def _extension_file() -> str | None: + return getattr(_uuidv47, "__file__", None) + + +def _is_native_extension_file(path: str) -> bool: + """True if ``path`` looks like a loaded binary extension module.""" + if not path: + return False + p = Path(path) + if p.suffix.lower() == ".pyd": + return True + # Linux/macOS: ``*.cpython-XY-…so`` or ``*.so`` + return p.suffix.lower() == ".so" + + +def test_native_extension_module_file_points_to_binary() -> None: + path = _extension_file() + assert path is not None, "_uuidv47 should define __file__" + assert _is_native_extension_file(path), ( + "Expected compiled extension (.so / .pyd); " + f"got __file__={path!r}. Is the Cython module built?" + ) + assert not path.endswith(".pyx"), "Wheel/runtime should not import .pyx directly" + + +def test_package_version_matches_metadata() -> None: + assert isinstance(python_uuidv47.__version__, str) + assert len(python_uuidv47.__version__.split(".")) >= 2 + + +def test_public_api_roundtrip_on_installed_package() -> None: + set_keys(111, 222) + assert has_keys() is True + u = "550e8400-e29b-71d4-a716-446655440000" + assert uuid_parse(u) is True + facade = encode(u) + assert uuid_parse(facade) is True + assert decode(facade) == u