diff --git a/.github/dependabot.yml b/.github/dependabot.yml index be006de9..9142c861 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,3 +11,5 @@ updates: - "*" # Group all Actions updates into a single larger pull request schedule: interval: weekly + cooldown: + default-days: 7 diff --git a/.github/workflows/cibuildwheel.yml b/.github/workflows/cibuildwheel.yml index 999fdbc3..0bc4ced5 100644 --- a/.github/workflows/cibuildwheel.yml +++ b/.github/workflows/cibuildwheel.yml @@ -4,9 +4,9 @@ on: pull_request: branches: - master - push: - tags: - - "v*" + release: + types: + - published jobs: build_bdist: @@ -31,72 +31,59 @@ jobs: arch: ARM64 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - - # For aarch64 support - # https://cibuildwheel.pypa.io/en/stable/faq/#emulation - #- uses: docker/setup-qemu-action@v3 - # with: - # platforms: all - # if: runner.os == 'Linux' && matrix.arch == 'aarch64' + persist-credentials: false - name: Build just oldest and newest on PRs, all on tags - if: ${{ github.event_name }} == pull_request + if: ${{ github.event_name == 'pull_request' }} shell: bash # - On PPs, omit musllinux for speed - # - On PRs, run just oldest and newest Python versions + # - On PRs, run just oldest and newest Python versions (3.11 is the oldest abi3 target) run: | - CIBW_SKIP="cp310-win_arm64 cp311-* cp312-* cp313-* *musllinux*" + CIBW_SKIP="cp310-win_arm64 *musllinux*" echo "CIBW_SKIP=$CIBW_SKIP" >> $GITHUB_ENV echo "Setting CIBW_SKIP=$CIBW_SKIP" + CIBW_TEST_SKIP="cp312-* cp313-*" + echo "CIBW_TEST_SKIP=$CIBW_TEST_SKIP" >> $GITHUB_ENV + echo "Setting CIBW_TEST_SKIP=$CIBW_TEST_SKIP" + - name: "Building ${{ matrix.os }} (${{ matrix.arch }}) wheels" - uses: pypa/cibuildwheel@v3.3.1 + uses: pypa/cibuildwheel@298ed2fb2c105540f5ed055e8a6ad78d82dd3a7e # v3.3.1 env: CIBW_SKIP: ${{ env.CIBW_SKIP }} + CIBW_TEST_SKIP: ${{ env.CIBW_TEST_SKIP }} CIBW_ARCHS: ${{ matrix.arch }} - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: pypi-artifacts-${{ matrix.os }}-${{ matrix.arch }} path: ${{ github.workspace }}/wheelhouse/*.whl - + permissions: + actions: write build_sdist: name: Build source distribution runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 + persist-credentials: false - name: Build sdist run: > pip install build && python -m build --sdist . --outdir dist - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: pypi-artifacts path: ${{ github.workspace }}/dist/*.tar.gz - - show-artifacts: - needs: [build_bdist, build_sdist] - name: "Show artifacts" - runs-on: ubuntu-22.04 - steps: - - uses: actions/download-artifact@v7 - with: - pattern: pypi-artifacts* - path: ${{ github.workspace }}/dist - merge-multiple: true - - - shell: bash - run: | - ls -l ${{ github.workspace }}/dist - + permissions: + actions: write publish-artifacts-pypi: needs: [build_bdist, build_sdist] @@ -105,14 +92,17 @@ jobs: # upload to PyPI for every tag starting with 'v' if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v') steps: - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: pattern: pypi-artifacts* path: ${{ github.workspace }}/dist merge-multiple: true - - uses: pypa/gh-action-pypi-publish@release/v1 + # Move to Trusted Publishing + - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 with: user: __token__ password: ${{ secrets.PYPI_PASSWORD }} print_hash: true + permissions: + id-token: write # IMPORTANT: this permission is mandatory for trusted publishing \ No newline at end of file diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 3891f15c..bc531c8d 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -1,22 +1,32 @@ name: Build and Deploy docs on: + pull_request: push: branches: - master + release: + types: + - published + +# Deny all permissions by default +permissions: {} jobs: build-docs: runs-on: ubuntu-latest + permissions: + contents: write steps: - name: checkout - uses: actions/checkout@v6 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 + persist-credentials: false - name: Setup Mamba - uses: mamba-org/setup-micromamba@v2 + uses: mamba-org/setup-micromamba@add3a49764cedee8ee24e82dfde87f5bc2914462 # v2.0.7 with: environment-name: TEST create-args: >- @@ -43,7 +53,8 @@ jobs: popd - name: Deploy - uses: peaceiris/actions-gh-pages@v4 + if: success() && github.event_name == 'release' + uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: docs/_build/html diff --git a/.github/workflows/tests_conda.yml b/.github/workflows/tests_conda.yml index 49c5e313..cae95de7 100644 --- a/.github/workflows/tests_conda.yml +++ b/.github/workflows/tests_conda.yml @@ -5,6 +5,9 @@ on: push: branches: [master] +# Deny all permissions by default +permissions: {} + jobs: run: runs-on: ${{ matrix.os }} @@ -25,10 +28,12 @@ jobs: experimental: true steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Setup micromamba Env - uses: mamba-org/setup-micromamba@v2 + uses: mamba-org/setup-micromamba@add3a49764cedee8ee24e82dfde87f5bc2914462 # v2.0.7 with: environment-name: TEST create-args: >- diff --git a/.github/workflows/tests_latest.yml b/.github/workflows/tests_latest.yml index e270c2b6..f04e4cfb 100644 --- a/.github/workflows/tests_latest.yml +++ b/.github/workflows/tests_latest.yml @@ -1,5 +1,12 @@ name: Build and test with development python -on: [push, pull_request] + +on: + pull_request: + push: + +# Deny all permissions by default +permissions: {} + jobs: build-linux: name: Python (${{ matrix.python-version }}) @@ -9,10 +16,12 @@ jobs: python-version: ["3.14.0b.1"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: ${{ matrix.python-version }} diff --git a/pyproject.toml b/pyproject.toml index ae720e47..1f452b64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -127,3 +127,4 @@ test-command = [ ] manylinux-x86_64-image = "manylinux2014" manylinux-aarch64-image = "manylinux2014" +environment = { CFTIME_LIMITED_API = "1" } diff --git a/setup.py b/setup.py index eed7048f..48371c37 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,6 @@ import os import sys +import sysconfig import numpy from Cython.Build import cythonize @@ -29,6 +30,24 @@ CFTIME_DIR = os.path.join(SRCDIR, NAME) CYTHON_FNAME = os.path.join(CFTIME_DIR, '_{}.pyx'.format(NAME)) +USE_PY_LIMITED_API = ( + # require opt-in (builds are specialized by default) + os.getenv('CFTIME_LIMITED_API', '0') == '1' + # Cython + numpy + limited API de facto requires Python >=3.11 + and sys.version_info >= (3, 11) + # as of Python 3.14t, free-threaded builds don't support the limited API + and not sysconfig.get_config_var("Py_GIL_DISABLED") +) +ABI3_TARGET_VERSION = "".join(str(_) for _ in sys.version_info[:2]) +ABI3_TARGET_HEX = hex(sys.hexversion & 0xFFFF00F0) + +if USE_PY_LIMITED_API: + DEFINE_MACROS += [(("Py_LIMITED_API", ABI3_TARGET_HEX))] + +if USE_PY_LIMITED_API: + SETUP_OPTIONS = {"bdist_wheel": {"py_limited_api": f"cp{ABI3_TARGET_VERSION}"}} +else: + SETUP_OPTIONS = {} class CleanCython(Command): description = 'Purge artifacts built by Cython' @@ -60,6 +79,7 @@ def run(self): } DEFINE_MACROS += [('CYTHON_TRACE', '1'), ('CYTHON_TRACE_NOGIL', '1')] + if FLAG_COVERAGE in sys.argv: sys.argv.remove(FLAG_COVERAGE) print('enable: "linetrace" Cython compiler directive') @@ -71,7 +91,8 @@ def run(self): extension = Extension('{}._{}'.format(NAME, NAME), sources=[os.path.relpath(CYTHON_FNAME, BASEDIR)], define_macros=DEFINE_MACROS, - include_dirs=[numpy.get_include(),]) + include_dirs=[numpy.get_include()], + py_limited_api=USE_PY_LIMITED_API) ext_modules = cythonize( extension, @@ -82,4 +103,5 @@ def run(self): setup( cmdclass={'clean_cython': CleanCython}, ext_modules=ext_modules, + options=SETUP_OPTIONS, )