From a441150bf5a4d7428a7c2485cb3cccd6be04743c Mon Sep 17 00:00:00 2001 From: Olaf Conradi Date: Sun, 28 Jun 2026 13:14:46 +0200 Subject: [PATCH 01/14] Add tox and pyenv helper --- .gitignore | 3 ++- scripts/run_with_pyenv.sh | 23 +++++++++++++++++++++++ tox.ini | 17 +++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100755 scripts/run_with_pyenv.sh create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index 01925c5..1c353da 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ MANIFEST *.so *.swp venv/ -.venv/ \ No newline at end of file +.venv/ +.python-version diff --git a/scripts/run_with_pyenv.sh b/scripts/run_with_pyenv.sh new file mode 100755 index 0000000..184eddf --- /dev/null +++ b/scripts/run_with_pyenv.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail + +if ! command -v pyenv >/dev/null 2>&1; then + echo "pyenv is not installed. Install it first: https://github.com/pyenv/pyenv" >&2 + exit 1 +fi + +version="${1:-3.13}" + +if [[ "$version" == "2.7" ]]; then + version="2.7.18" +fi + +if ! pyenv versions --bare | grep -Fxq "$version"; then + echo "Python $version is not installed under pyenv yet. Install it first, for example: pyenv install $version" >&2 + exit 1 +fi + +pyenv local "$version" +python -m pip install --upgrade pip setuptools wheel +python -m pip install . +python -m unittest discover -s test -p 'test_*.py' diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..f015ed6 --- /dev/null +++ b/tox.ini @@ -0,0 +1,17 @@ +[tox] +envlist = py39,py310,py311,py312,py313,py314,py27 +skipsdist = False + +[testenv] +basepython = + py39: python3.9 + py310: python3.10 + py311: python3.11 + py312: python3.12 + py313: python3.13 + py314: python3.14 + py27: python2.7 +setenv = + PIP_DISABLE_PIP_VERSION_CHECK = 1 +commands = + python -m unittest discover -s test -p 'test_*.py' From 48083552b0c5706692563005774fa7b047da5d33 Mon Sep 17 00:00:00 2001 From: Olaf Conradi Date: Sun, 28 Jun 2026 13:18:19 +0200 Subject: [PATCH 02/14] Add Github actions based workflow --- .github/workflows/ci.yml | 145 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..090a122 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,145 @@ +name: CI + +on: + push: + branches: + - master + - main + tags: + - 'v*' + pull_request: + + +jobs: + test: + name: Python ${{ matrix.python-version }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + python-version: '3.9' + - os: macos-latest + python-version: '3.9' + - os: windows-latest + python-version: '3.9' + - os: ubuntu-latest + python-version: '3.10' + - os: macos-latest + python-version: '3.10' + - os: windows-latest + python-version: '3.10' + - os: ubuntu-latest + python-version: '3.11' + - os: macos-latest + python-version: '3.11' + - os: windows-latest + python-version: '3.11' + - os: ubuntu-latest + python-version: '3.12' + - os: macos-latest + python-version: '3.12' + - os: windows-latest + python-version: '3.12' + - os: ubuntu-latest + python-version: '3.13' + - os: macos-latest + python-version: '3.13' + - os: windows-latest + python-version: '3.13' + - os: ubuntu-latest + python-version: '3.14' + - os: macos-latest + python-version: '3.14' + - os: windows-latest + python-version: '3.14' + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip setuptools wheel + + - name: Install package + run: | + python -m pip install . + + - name: Run tests + run: | + python -m unittest discover -s test -p 'test_*.py' + + test-py27: + name: Python 2.7 on Ubuntu + runs-on: ubuntu-latest + container: + image: python:2.7.18-buster + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Build extension in place + run: | + python setup.py build_ext --inplace + + - name: Run tests + run: | + python -m unittest discover -s test -p 'test_*.py' + + build: + name: Build distributions + runs-on: ubuntu-latest + needs: [ test, test-py27 ] + if: startsWith(github.ref, 'refs/tags/') + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: pip + + - name: Build source and wheel distributions + run: | + python -m pip install --upgrade pip build + python -m build + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/* + + publish: + name: Publish to PyPI + runs-on: ubuntu-latest + needs: build + if: startsWith(github.ref, 'refs/tags/') + environment: + name: pypi + permissions: + id-token: write + + steps: + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: dist + path: dist + + - name: Publish package + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist/ From 43d1fc5e3655853659e834d05b5c38d1df2e07b5 Mon Sep 17 00:00:00 2001 From: Olaf Conradi Date: Sun, 28 Jun 2026 13:19:59 +0200 Subject: [PATCH 03/14] Remove Travis and Appveyor --- .appveyor/appveyor-bootstrap.py | 162 --------------- .appveyor/appveyor-with-compiler.cmd | 50 ----- .travis.yml | 184 ----------------- .travis/travis-release.py | 289 --------------------------- appveyor.yml | 170 ---------------- 5 files changed, 855 deletions(-) delete mode 100644 .appveyor/appveyor-bootstrap.py delete mode 100644 .appveyor/appveyor-with-compiler.cmd delete mode 100644 .travis.yml delete mode 100644 .travis/travis-release.py delete mode 100644 appveyor.yml diff --git a/.appveyor/appveyor-bootstrap.py b/.appveyor/appveyor-bootstrap.py deleted file mode 100644 index 0468e1b..0000000 --- a/.appveyor/appveyor-bootstrap.py +++ /dev/null @@ -1,162 +0,0 @@ -""" -AppVeyor will at least have few Pythons around so there's no point of -implementing a bootstrapper in PowerShell. - -This is a port of -https://github.com/pypa/python-packaging-user-guide/blob/master/source/code/install.ps1 -with various fixes and improvements that just weren't feasible to -implement in PowerShell. -""" -from __future__ import print_function - -import logging - -from os import environ -from os.path import exists -from subprocess import check_call - -try: - from urllib.request import urlretrieve -except ImportError: - from urllib import urlretrieve - - -log = logging.getLogger(__name__) - - -BASE_URL = "https://www.python.org/ftp/python/" -GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" -GET_PIP_PATH = "C:\get-pip.py" -URLS = { - ("2.6", "64"): BASE_URL + "2.6.6/python-2.6.6.amd64.msi", - ("2.6", "32"): BASE_URL + "2.6.6/python-2.6.6.msi", - ("2.7", "64"): BASE_URL + "2.7.18/python-2.7.18.amd64.msi", - ("2.7", "32"): BASE_URL + "2.7.18/python-2.7.18.msi", - ("3.3", "64"): BASE_URL + "3.3.3/python-3.3.5.amd64.msi", - ("3.3", "32"): BASE_URL + "3.3.3/python-3.3.5.msi", - ("3.4", "64"): BASE_URL + "3.4.4/python-3.4.4.amd64.msi", - ("3.4", "32"): BASE_URL + "3.4.4/python-3.4.4.msi", - # NOTE: no .msi installer since 3.5 - ("3.5", "64"): BASE_URL + "3.5.4/python-3.5.4-amd64.exe", - ("3.5", "32"): BASE_URL + "3.5.4/python-3.5.4.exe", - ("3.6", "64"): BASE_URL + "3.6.8/python-3.6.8-amd64.exe", - ("3.6", "32"): BASE_URL + "3.6.8/python-3.6.8.exe", - ("3.7", "64"): BASE_URL + "3.7.8/python-3.7.8-amd64.exe", - ("3.7", "32"): BASE_URL + "3.7.8/python-3.7.8.exe", - ("3.8", "64"): BASE_URL + "3.8.10/python-3.8.10-amd64.exe", - ("3.8", "32"): BASE_URL + "3.8.10/python-3.8.10.exe", - ("3.9", "64"): BASE_URL + "3.9.13/python-3.9.13-amd64.exe", - ("3.9", "32"): BASE_URL + "3.9.13/python-3.9.13.exe", - ("3.10", "64"): BASE_URL + "3.10.11/python-3.10.11-amd64.exe", - ("3.10", "32"): BASE_URL + "3.10.11/python-3.10.11.exe", - ("3.11", "64"): BASE_URL + "3.11.7/python-3.11.7-amd64.exe", - ("3.11", "32"): BASE_URL + "3.11.7/python-3.11.7.exe", - ("3.12", "64"): BASE_URL + "3.12.1/python-3.12.1-amd64.exe", - ("3.12", "32"): BASE_URL + "3.12.1/python-3.12.1.exe", -} -INSTALL_CMD = { - # Commands are allowed to fail only if they are not the last command. - # Eg: uninstall (/x) allowed to fail. - "2.6": [["msiexec.exe", "/L*+!", "install.log", "/qn", "/x", "{path}"], - ["msiexec.exe", "/L*+!", "install.log", "/qn", "/i", "{path}", - "TARGETDIR={home}"]], - "2.7": [["msiexec.exe", "/L*+!", "install.log", "/qn", "/x", "{path}"], - ["msiexec.exe", "/L*+!", "install.log", "/qn", "/i", "{path}", - "TARGETDIR={home}"]], - "3.3": [["msiexec.exe", "/L*+!", "install.log", "/qn", "/x", "{path}"], - ["msiexec.exe", "/L*+!", "install.log", "/qn", "/i", "{path}", - "TARGETDIR={home}"]], - "3.4": [["msiexec.exe", "/L*+!", "install.log", "/qn", "/x", "{path}"], - ["msiexec.exe", "/L*+!", "install.log", "/qn", "/i", "{path}", - "TARGETDIR={home}"]], - "3.5": [["{path}", "/quiet", "TargetDir={home}"]], - "3.6": [["{path}", "/quiet", "TargetDir={home}"]], - "3.7": [["{path}", "/quiet", "TargetDir={home}"]], - "3.8": [["{path}", "/quiet", "TargetDir={home}"]], - "3.9": [["{path}", "/quiet", "TargetDir={home}"]], - "3.10": [["{path}", "/quiet", "TargetDir={home}"]], - "3.11": [["{path}", "/quiet", "TargetDir={home}"]], - "3.12": [["{path}", "/quiet", "TargetDir={home}"]], -} - - -def download_file(url, path): - log.info("Downloading: %s (into %s).", url, path) - progress = [0, 0] - - def report(count, size, total): - progress[0] = count * size - if progress[0] - progress[1] > 1000000: - progress[1] = progress[0] - log.info("Downloaded %s/%s ...", progress[1], total) - - dest, _ = urlretrieve(url, path, reporthook=report) - return dest - - -def install_python(version, arch, home): - log.info("Installing Python %s for %s bit architecture to '%s'.", - version, arch, home) - if exists(home): - return - - path = download_python(version, arch) - log.info("Installing '%s' to '%s'.", path, home) - success = False - for cmd in INSTALL_CMD[version]: - cmd = [part.format(home=home, path=path) for part in cmd] - log.info("Running '%s'.", " ".join(cmd)) - try: - check_call(cmd) - except Exception: - log.exception("Failed command '%s'.", " ".join(cmd)) - if exists("install.log"): - with open("install.log") as fh: - log.error(fh.read()) - else: - success = True - if success: - log.info("Installation complete.") - else: - log.error("Installation failed.") - - -def download_python(version, arch): - for _ in range(3): - try: - return download_file(URLS[version, arch], "installer.exe") - except Exception: - log.exception("Failed to download.") - log.info("Retrying ...") - - -def install_pip(home): - pip_path = home + "/Scripts/pip.exe" - python_path = home + "/python.exe" - if exists(pip_path): - log.info("pip already installed, try to upgrade it.") - cmd = [python_path, "-m", "pip", "install", "--upgrade", "pip"] - check_call(cmd) - else: - log.info("Installing pip.") - download_file(GET_PIP_URL, GET_PIP_PATH) - log.info("Executing: %s %s", python_path, GET_PIP_PATH) - check_call([python_path, GET_PIP_PATH]) - - -def install_packages(home, *packages): - cmd = [home + "/Scripts/pip.exe", "install"] - cmd.extend(packages) - check_call(cmd) - - -if __name__ == '__main__': - logging.basicConfig(level=logging.DEBUG, - format="%(message)s") - - install_python(environ['PYTHON_VERSION'], - environ['PYTHON_ARCH'], - environ['PYTHON_HOME']) - install_pip(environ['PYTHON_HOME']) - install_packages(environ['PYTHON_HOME'], - "setuptools>=36.4.0", "wheel") diff --git a/.appveyor/appveyor-with-compiler.cmd b/.appveyor/appveyor-with-compiler.cmd deleted file mode 100644 index a3e5d22..0000000 --- a/.appveyor/appveyor-with-compiler.cmd +++ /dev/null @@ -1,50 +0,0 @@ -:: To build extensions for 64 bit Python 3, we need to configure environment -:: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of: -:: MS Windows SDK for Windows 7 and .NET Framework 4 (SDK v7.1) -:: -:: To build extensions for 64 bit Python 2, we need to configure environment -:: variables to use the MSVC 2008 C++ compilers from GRMSDKX_EN_DVD.iso of: -:: MS Windows SDK for Windows 7 and .NET Framework 3.5 (SDK v7.0) -:: -:: 32 bit builds do not require specific environment configurations. -:: -:: Note: this script needs to be run with the /E:ON and /V:ON flags for the -:: cmd interpreter, at least for (SDK v7.0) -:: -:: More details at: -:: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows -:: http://stackoverflow.com/a/13751649/163740 -:: -:: Author: Olivier Grisel -:: License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/ -@ECHO OFF -SET COMMAND_TO_RUN=%* -SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows -SET WIN_WDK="c:\Program Files (x86)\Windows Kits\10\Include\wdf" -ECHO SDK: %WINDOWS_SDK_VERSION% ARCH: %PYTHON_ARCH% - - -IF "%PYTHON_VERSION%"=="3.5" GOTO main -IF "%PYTHON_VERSION%"=="3.6" GOTO main -IF "%PYTHON_VERSION%"=="3.7" GOTO main -IF "%PYTHON_VERSION%"=="3.8" GOTO main -IF "%PYTHON_VERSION%"=="3.9" GOTO main -IF "%PYTHON_VERSION%"=="3.10" GOTO main -IF "%PYTHON_VERSION%"=="3.11" GOTO main -IF "%PYTHON_VERSION%"=="3.12" GOTO main -IF "%PYTHON_ARCH%"=="32" GOTO main - - -SET DISTUTILS_USE_SDK=1 -SET MSSdk=1 -"%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION% -CALL "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release - -:main -IF EXIST %WIN_WDK% ( - REM See: https://connect.microsoft.com/VisualStudio/feedback/details/1610302/ - REN %WIN_WDK% 0wdf -) - -ECHO Executing: %COMMAND_TO_RUN% -CALL %COMMAND_TO_RUN% || EXIT 1 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 72b74e8..0000000 --- a/.travis.yml +++ /dev/null @@ -1,184 +0,0 @@ -language: python -cache: pip - -env: - global: - # GITHUB_API_TOKEN - - secure: YNLKYudc4YRn+i89r20lB8oDoigXqiBWHpBi+QVNUCuzbHcp1rtK5/uwvbMEzERPTcRsFcqp0BHOaXf8PzWFVYVVlLCag9eX69HuOopHakW6zvZgWEv5ZVUbzLatJmOJ6BXIP+b6o83lgrO1khG2e8VRPVVwO8D0kadr2zwpuH4= - # APPVEYOR_API_TOKEN - - secure: UaLfn9kWVK+EyYxvjZzoV3m3ODs+iD0F57kCF4Z+YqXdo3oPxd+hIoEpo9VtkXAvhNLbj3kvCyiv+J+lqEwuR6uoAFaiz70LaAsxIX1CuHzNhJRxUglK4TdTdoGXHo2bbiTPZRnPkYwFYip5ehx8q7tPnL+xgu3AiTTLvSr0R6g= - # TWINE_USERNAME - - secure: Od+iImlaOP2qzkGstfj3TTmhqWDdtZ1+CGFPJnHzZ9mj9l3Pg1Aek369f1MsGBXZeJqojiTx1xg/XzW3xH39QzbuynMGEFYOAZDnQMJsT990H1vKyHsoQ86t4n0T7FEIs6B5BzP1oDROdADHcxmRQ1BoSvWumbq/isGewArGO0M= - # TWINE_PASSWORD - - secure: cMN061O5PjB3B8+iSEgyo/qe3QPSDW8GaAupYtHKLnxT2M6JiepuVzZL40y9I1QTnZQMf8VIKH7XExLkgyVvbKZyQCctp9tioBBiAafp6uhAQMLSzj1+03gBzaJQJySPMLX4Snfa/TuhOqym5m4kYQTX/p3qCImzkgxWW1SV+rc= - - TWINE_TEST_REPOSITORY=pypitest - - TWINE_TEST_REPOSITORY_URL=https://test.pypi.org/legacy/ - - TWINE_PROD_REPOSITORY=pypi - - TWINE_PROD_REPOSITORY_URL=https://upload.pypi.org/legacy/ - -stages: - - name: test - - name: deploy - if: type = push AND tag IS present - -os: - - linux -dist: jammy - -addons: - apt: - packages: - - pandoc - -python: - # Each commented python version has a custom include for a different distro - # - "2.7" - # - "pypy" - - "pypy3" - # - "3.3" - # - "3.4" - # - "3.5" - # - "3.6" - - "3.7" - - "3.8" - - "3.9" - - "3.10" - - "3.11" - - "3.12" - -before_install: - - if [[ $TRAVIS_OS_NAME == "osx" ]]; then - echo "Updating brew packages."; - brew update 1>&2 || travis_terminate 1; - if [[ $TRAVIS_PYTHON_VERSION == "2.7" ]]; then - echo "Installing python 2."; - brew install python@2 1>&2 || travis_terminate 1; - pip install virtualenv || travis_terminate 1; - virtualenv venv -p python2; - elif [[ $TRAVIS_PYTHON_VERSION == "3.12" ]]; then - echo "Installing python 3."; - brew install python@3.12 1>&2 || travis_terminate 1; - pip3 install virtualenv || travis_terminate 1; - virtualenv venv -p python3; - fi; - source venv/bin/activate || travis_terminate 1; - fi - - if [[ $TRAVIS_PULL_REQUEST == false && -n $TRAVIS_TAG ]]; then - if [[ $TRAVIS_OS_NAME == "osx" ]]; then - echo "Installing pandoc."; - brew install pandoc 1>&2 || travis_terminate 1; - fi; - echo "Installing pypandoc."; - pip install pypandoc || travis_terminate 1; - fi - -install: - - pip install . - -script: - - python --version || travis_terminate 1 - - python setup.py test || travis_terminate 1 - # Need to pick a Python version to create the source distribution. - - if [[ $TRAVIS_PYTHON_VERSION == "3.12" && $TRAVIS_OS_NAME == linux && $TRAVIS_PULL_REQUEST == false && -n $TRAVIS_TAG ]]; then - echo "Creating source distribution."; - python setup.py sdist || travis_terminate 1; - else - echo "Skip creation of source distribution."; - fi - # Binary wheels for Linux that are too platform specific are not allowed to - # be uploaded on PyPI. As we need to compile C code, we only create them on OSX. - - if [[ $TRAVIS_OS_NAME == osx && $TRAVIS_PULL_REQUEST == false && -n $TRAVIS_TAG ]]; then - echo "Creating binary distribution wheel."; - python setup.py bdist_wheel || travis_terminate 1; - else - echo "Skip creation of binary distribution wheel."; - fi - -deploy: - provider: releases - token: - secure: Oy5f4YjhoPd1tAFK0nlD9ZmWYH1cpnl5R3erPinemU4S/8l0jyTb4e5gdYVhluzSTZGrvNKJREwTSsq6yOriNc9EgTc/YwkFczkFuPokGzNO+VNfaEfsm9v/zcPH+Tpc51mFWXYnhFzBkJeIeRPNoVXCifFL+WxoCyWg6ljpVHg= - file_glob: true - file: - - dist/Whirlpool-*.tar.gz - - dist/Whirlpool-*.whl - on: - tags: true - -jobs: - fast_finish: true - include: - # Python 3.3 is available on distro Trusty - - stage: test - language: python - python: 3.3 - dist: trusty - env: TRAVIS_PYTHON_VERSION=3.3 - # Python 2.7, pypy, 3.4, 3.5 and 3.6 are available on distro Xenial - - stage: test - language: python - python: 2.7 - dist: xenial - env: TRAVIS_PYTHON_VERSION=2.7 - - stage: test - language: python - python: pypy - dist: xenial - env: TRAVIS_PYTHON_VERSION=pypy - - stage: test - language: python - python: 3.4 - dist: xenial - env: TRAVIS_PYTHON_VERSION=3.4 - - stage: test - language: python - python: 3.5 - dist: xenial - env: TRAVIS_PYTHON_VERSION=3.5 - - stage: test - language: python - python: 3.6 - dist: xenial - env: TRAVIS_PYTHON_VERSION=3.6 - # Manually include OSX in the job matrix, but only for - # Python 2.7 and 3.12. - - stage: test - language: generic - python: 2.7 - os: osx - osx_image: xcode12.2 - env: TRAVIS_PYTHON_VERSION=2.7 - - stage: test - language: generic - python: 3.12 - os: osx - osx_image: xcode14.2 - env: TRAVIS_PYTHON_VERSION=3.12 - # As the deploy script fetches all build artifacts uploaded to GitHub of - # all stages, we only need to run it once. - # So pinning it to Python 3.12 on Linux. - - stage: deploy - python: 3.12 - os: linux - cache: false - before_install: skip - install: skip - script: - - echo "Check build status." - - python .travis/travis-release.py || travis_terminate 1 - - echo "Installing Twine." - - pip install twine || travis_terminate 1 - - if [[ $TRAVIS_PULL_REQUEST == false && -n $TRAVIS_TAG ]]; then - if [[ $TRAVIS_TAG =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "Deploy to production PyPI."; - twine upload -r $TWINE_PROD_REPOSITORY --repository-url $TWINE_PROD_REPOSITORY_URL dist/* --skip-existing || travis_terminate 1; - else - echo "Deploy to test PyPI."; - twine upload -r $TWINE_TEST_REPOSITORY --repository-url $TWINE_TEST_REPOSITORY_URL dist/* --skip-existing || travis_terminate 1; - fi; - fi - -notifications: - email: - on_success: never - on_failure: change diff --git a/.travis/travis-release.py b/.travis/travis-release.py deleted file mode 100644 index 8b518b9..0000000 --- a/.travis/travis-release.py +++ /dev/null @@ -1,289 +0,0 @@ -import errno -import json -import logging -import os -import re -import shutil -import stat - -from time import sleep - -try: - from urllib.request import urlretrieve, urlopen, Request -except ImportError: - from urllib import urlretrieve - from urllib2 import urlopen, Request - - -log = logging.getLogger(__name__) - - -DIST_DIR = 'dist' - -BASE_GITHUB_API = 'https://api.github.com/repos' -GITHUB_RELEASES_TAGS = '{}/{}/releases/tags/{}' - -BASE_APPVEYOR_API = 'https://ci.appveyor.com/api' -APPVEYOR_BUILD_HISTORY = '{}/projects/{}/history?recordsNumber=20' -APPVEYOR_BUILD_VERSION = '{}/projects/{}/build/{}' - - -class ReleaseException(Exception): - """Base exception class for release errors.""" - - -class ReleaseVersionException(ReleaseException): - """Version mismatch between code and tagged release.""" - - -class ApiTimeout(ReleaseException): - """Could not reach API endpoint.""" - - -class AppVeyorBuildFailed(ReleaseException): - """Exception raised on failed build runs.""" - - -class AppVeyorBuildCancelled(ReleaseException): - """Exception raised on cancelled build.""" - - -class AppVeyorBuildTimeout(ReleaseException): - """Exception raised on build timeout.""" - - -def download_file(url, path): - """Download the target of url and store as local file in path.""" - log.info("Downloading: %s (into %s).", url, path) - progress = [0, 0] - - def report(count, size, total): - progress[0] = count * size - if progress[0] - progress[1] > 1000000: - progress[1] = progress[0] - log.info("Downloaded %s/%s ...", progress[1], total) - - dest, _ = urlretrieve(url, path, reporthook=report) - return dest - - -def api_request(url, token): - """Send a request to API at url and decode the response as json.""" - response = None - for i in range(3): - try: - log.info("Sending API request to '%s'.", url) - request = Request(url) - if token is not None: - request.add_header('Authorization', 'token {}'.format(token)) - response = urlopen(request) - break - except Exception: - log.exception("Failed to call API.") - log.info("Retrying ...") - sleep(i) - if response is None: - raise ApiTimeout("Could not reach API at '{}'.".format(url)) - - try: - charset = response.info().get_content_charset() - except AttributeError: - charset = response.info().getparam('charset') - if not charset: - charset = 'utf-8' - data = json.loads(response.read().decode(charset)) - return data - - -def is_regular_dir(path): - """Return non-zero if path is a directory.""" - try: - mode = os.lstat(path).st_mode - except os.error: - mode = 0 - return stat.S_ISDIR(mode) - - -def force_remove_file_or_symlink(path): - """Try to remove the file referenced by path. If it fails try again by - setting write permission. - """ - try: - os.remove(path) - except OSError: - os.lchmod(path, stat.S_IWRITE) - os.remove(path) - - -def remove_readonly(func, path, excinfo): - """Error handling function for shutil.rmtree to try the file or directory - remove operation again by setting write permission. - - Function taken from: https://stackoverflow.com/a/1889686 - """ - if func is os.rmdir: - os.chmod(path, stat.S_IWRITE) - os.rmdir(path) - elif func is os.remove: - os.lchmod(path, stat.S_IWRITE) - os.remove(path) - - -def clear_dir(path): - """Remove all files and directories from path. The path itself remains - untouched. - - Function taken from: https://stackoverflow.com/a/24844618 - """ - if is_regular_dir(path): - for name in os.listdir(path): - fullpath = os.path.join(path, name) - if is_regular_dir(fullpath): - shutil.rmtree(fullpath, onerror=remove_readonly) - else: - force_remove_file_or_symlink(fullpath) - else: - raise OSError(errno.ENOTDIR, os.strerror(errno.ENOTDIR), path) - - -def download_github_tagged_release(path, url, tag, token): - """Download all assets that belong to GitHub url for given tag. - - The local directory path is cleared of any existing files - before downloading takes place. - """ - filenames = [] - if os.path.exists(path): - log.info("Clearing existing download path '%s'.", path) - clear_dir(path) - else: - log.info("Creating download path '%s'.", path) - os.makedirs(path) - - log.info("Retrieving GitHub assets for tag '%s'.", tag) - data = api_request(url, token) - for asset in data['assets']: - download_file(asset['browser_download_url'], - os.path.join(path, asset['name'])) - filenames.append(asset['name']) - sleep(5) - return filenames - - -def check_appveyor_build_status(url, token): - """Poll url for final build status for max 20 times with a three minute - interval. - """ - status = 'queued' - for _ in range(20): - data = api_request(url, token) - build = data['build'] - status = build['status'] - if status in ('success', 'failed', - 'cancelled', 'cancelling'): - return status - log.info("Build status of build with version '%s' and tag '%s' " - "is %s.", build['version'], build['tag'], status) - sleep(180) - return status - - -def check_appveyor_tagged_build(url, tag, token): - """Examine the AppVeyor build history for any build with the specified - tag. When the build still needs to finish keep polling for updates - until done or timed out. - - Returns true when build is successful or raises an exception otherwise. - """ - log.info("Retrieving AppVeyor build history.") - data = api_request(url, token) - status = 'unknown' - for build in data['builds']: - if build['isTag'] and build['tag'] == tag: - status = build['status'] - if status in ('success', 'failed'): - # Found a final state, no need to check prior builds. - break - elif status in ('cancelled', 'cancelling'): - # Try to see if a prior build was successful. - continue - elif status in ('queued', 'running'): - av_build_url = APPVEYOR_BUILD_VERSION.format( - BASE_APPVEYOR_API, - os.environ['TRAVIS_REPO_SLUG'], - build['version']) - status = check_appveyor_build_status(av_build_url, token) - break - # Done checking build history, examine final status. - if status == 'unknown': - raise AppVeyorBuildTimeout("No build found for tag '{}'".format(tag)) - elif status == 'success': - log.info("Build version '%s' with tag '%s' " - "was successful.", build['version'], tag) - return True - elif status in ('cancelled', 'cancelling'): - raise AppVeyorBuildCancelled("Build version '{}' with tag '{}' was " - "cancelled.".format(build['version'], - tag)) - elif status == 'failed': - raise AppVeyorBuildFailed("Build version '{}' with tag '{}' " - "failed.".format(build['version'], - tag)) - elif status in ('queued', 'running'): - raise AppVeyorBuildTimeout("Build version '{}' with tag '{}' " - "timed out.".format(build['version'], - tag)) - else: - raise ReleaseException("Unrecognized status '{}' for build version " - "'{}' with tag '{}'.".format(status, - build['version'], - tag)) - - -def check_code_version(filenames, tag): - """Check if the asset filenames relate to the tag version. - - When tag is in the form v0.1.2 the first character is stripped. - - Raises ReleaseVersionException when the tag is not found in any - of the asset file names. - """ - if re.match(r"v\d+\.\d+\.\d+", tag) is not None: - file_tag = tag[1:] - else: - file_tag = tag - wheel_part = "-{}-".format(file_tag) - targz_part = "-{}.tar.gz".format(file_tag) - for filename in filenames: - if (filename[-4:] == '.whl') and (wheel_part not in filename): - log.error("Filename '%s' does not correspond " - "to tag '%s'.", filename, tag) - raise ReleaseVersionException("Version mismatch " - "between code and tag") - if (filename[-7:] == '.tar.gz') and (targz_part not in filename): - log.error("Filename '%s' does not correspond " - "to tag '%s'.", filename, tag) - raise ReleaseVersionException("Version mismatch " - "between code and tag") - return True - - -if __name__ == '__main__': - logging.basicConfig(level=logging.DEBUG, - format="%(message)s") - - tag = os.environ['TRAVIS_TAG'] - gh_token = os.environ.get('GITHUB_API_TOKEN') - av_token = os.environ.get('APPVEYOR_API_TOKEN') - gh_url = GITHUB_RELEASES_TAGS.format(BASE_GITHUB_API, - os.environ['TRAVIS_REPO_SLUG'], - tag) - av_url = APPVEYOR_BUILD_HISTORY.format(BASE_APPVEYOR_API, - os.environ['TRAVIS_REPO_SLUG']) - if check_appveyor_tagged_build(av_url, tag, av_token): - log.info("Download assets for tagged release '%s'.", tag) - filenames = download_github_tagged_release(DIST_DIR, - gh_url, tag, - gh_token) - log.info("All assets downloaded for tagged release '%s'.", tag) - check_code_version(filenames, tag) diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 59184f9..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,170 +0,0 @@ -version: '{branch}-{build}' -cache: - - '%LOCALAPPDATA%\pip\Cache' -environment: - global: - # SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the - # /E:ON and /V:ON options are not enabled in the batch script interpreter - # See: http://stackoverflow.com/a/13751649/163740 - CMD_WITH_COMPILER: 'cmd /E:ON /V:ON /C .\.appveyor\appveyor-with-compiler.cmd' - - matrix: - - PYTHON_VERSION: '2.7' - PYTHON_ARCH: '32' - PYTHON_HOME: C:\Python27 - PYTHON_EXE: C:\Python27\python.exe - - PYTHON_VERSION: '2.7' - PYTHON_ARCH: '64' - WINDOWS_SDK_VERSION: v7.0 - PYTHON_HOME: C:\Python27-x64 - PYTHON_EXE: C:\Python27-x64\python.exe - - PYTHON_VERSION: '3.3' - PYTHON_ARCH: '32' - PYTHON_HOME: C:\Python33 - PYTHON_EXE: C:\Python33\python.exe - - PYTHON_VERSION: '3.3' - PYTHON_ARCH: '64' - WINDOWS_SDK_VERSION: v7.1 - PYTHON_HOME: C:\Python33-x64 - PYTHON_EXE: C:\Python33-x64\python.exe - - PYTHON_VERSION: '3.4' - PYTHON_ARCH: '32' - PYTHON_HOME: C:\Python34 - PYTHON_EXE: C:\Python34\python.exe - - PYTHON_VERSION: '3.4' - PYTHON_ARCH: '64' - WINDOWS_SDK_VERSION: v7.1 - PYTHON_HOME: C:\Python34-x64 - PYTHON_EXE: C:\Python34-x64\python.exe - - PYTHON_VERSION: '3.5' - PYTHON_ARCH: '32' - PYTHON_HOME: C:\Python35 - PYTHON_EXE: C:\Python35\python.exe - - PYTHON_VERSION: '3.5' - PYTHON_ARCH: '64' - PYTHON_HOME: C:\Python35-x64 - PYTHON_EXE: C:\Python35-x64\python.exe - - PYTHON_VERSION: '3.6' - PYTHON_ARCH: '32' - PYTHON_HOME: C:\Python36 - PYTHON_EXE: C:\Python36\python.exe - - PYTHON_VERSION: '3.6' - PYTHON_ARCH: '64' - PYTHON_HOME: C:\Python36-x64 - PYTHON_EXE: C:\Python36-x64\python.exe - - PYTHON_VERSION: '3.7' - PYTHON_ARCH: '32' - PYTHON_HOME: C:\Python37 - PYTHON_EXE: C:\Python37\python.exe - - PYTHON_VERSION: '3.7' - PYTHON_ARCH: '64' - PYTHON_HOME: C:\Python37-x64 - PYTHON_EXE: C:\Python37-x64\python.exe - - PYTHON_VERSION: '3.8' - PYTHON_ARCH: '32' - PYTHON_HOME: C:\Python38 - PYTHON_EXE: C:\Python38\python.exe - - PYTHON_VERSION: '3.8' - PYTHON_ARCH: '64' - PYTHON_HOME: C:\Python38-x64 - PYTHON_EXE: C:\Python38-x64\python.exe - - PYTHON_VERSION: '3.9' - PYTHON_ARCH: '32' - PYTHON_HOME: C:\Python39 - PYTHON_EXE: C:\Python39\python.exe - - PYTHON_VERSION: '3.9' - PYTHON_ARCH: '64' - PYTHON_HOME: C:\Python39-x64 - PYTHON_EXE: C:\Python39-x64\python.exe - - PYTHON_VERSION: '3.10' - PYTHON_ARCH: '32' - PYTHON_HOME: C:\Python310 - PYTHON_EXE: C:\Python310\python.exe - - PYTHON_VERSION: '3.10' - PYTHON_ARCH: '64' - PYTHON_HOME: C:\Python310-x64 - PYTHON_EXE: C:\Python310-x64\python.exe - - PYTHON_VERSION: '3.11' - PYTHON_ARCH: '32' - PYTHON_HOME: C:\Python311 - PYTHON_EXE: C:\Python311\python.exe - - PYTHON_VERSION: '3.11' - PYTHON_ARCH: '64' - PYTHON_HOME: C:\Python311-x64 - PYTHON_EXE: C:\Python311-x64\python.exe - - PYTHON_VERSION: '3.12' - PYTHON_ARCH: '32' - PYTHON_HOME: C:\Python312 - PYTHON_EXE: C:\Python312\python.exe - - PYTHON_VERSION: '3.12' - PYTHON_ARCH: '64' - PYTHON_HOME: C:\Python312-x64 - PYTHON_EXE: C:\Python312-x64\python.exe - -matrix: - fast_finish: true - -init: - - ECHO "Installed Python versions:" - - ps: ls C:\Python* - - ECHO "Installed SDKs:" - - ps: ls "$env:ProgramFiles\Microsoft SDKs\Windows" - -install: - # If there is a newer build queued for the same PR, cancel this one. - # The AppVeyor 'rollout builds' option is supposed to serve the same - # purpose but it is problematic because it tends to cancel builds pushed - # directly to master instead of just PR builds (or the converse). - # credits: JuliaLang developers. - - ps: | - if ($env:APPVEYOR_PULL_REQUEST_NUMBER -and - $env:APPVEYOR_BUILD_NUMBER -ne - ( - (Invoke-RestMethod https://ci.appveyor.com/api/projects/$env:APPVEYOR_ACCOUNT_NAME/$env:APPVEYOR_PROJECT_SLUG/history?recordsNumber=50 - ).builds | Where-Object pullRequestId -eq $env:APPVEYOR_PULL_REQUEST_NUMBER - )[0].buildNumber - ) - { - throw "There are newer queued builds for this pull request, failing early." - } - - python -u .appveyor\appveyor-bootstrap.py - -build: false - -test_script: - - "%CMD_WITH_COMPILER% %PYTHON_EXE% setup.py test" - -after_test: - - ps: | - if ($env:APPVEYOR_REPO_TAG -eq $true) { - Write-Host "Installing pandoc." - cinst --no-progress pandoc - if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } - Write-Host "Installing pypandoc." - Invoke-Expression "$env:CMD_WITH_COMPILER $env:PYTHON_EXE -m pip install pypandoc" - if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } - Write-Host "Creating binary distribution wheel." - Invoke-Expression "$env:CMD_WITH_COMPILER $env:PYTHON_EXE setup.py bdist_wheel" - if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } - } - -artifacts: - - path: dist\Whirlpool-*.whl - name: wheel - -deploy: - - provider: GitHub - auth_token: - secure: XL2Y+p18A7mTWLwutrqVVb73ZPibgTB+hZD9oP6iK8qylOur1VafOBjCi30K6+1Q - artifact: wheel - draft: false - prerelease: false - force_update: true - on: - appveyor_repo_tag: true - -notifications: - - provider: Email - on_build_success: false - on_build_failure: true - on_build_status_changed: false From 1e4f95f44cde4ceecdff14a7f6f26a3564555d52 Mon Sep 17 00:00:00 2001 From: Olaf Conradi Date: Sun, 28 Jun 2026 13:36:39 +0200 Subject: [PATCH 04/14] Add pypy2 and pypy3 --- .github/workflows/ci.yml | 16 ++++++++++++++++ tox.ini | 5 ++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 090a122..7629638 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,17 +54,33 @@ jobs: python-version: '3.14' - os: windows-latest python-version: '3.14' + - os: ubuntu-latest + python-version: 'pypy2' + - os: ubuntu-latest + python-version: 'pypy3' steps: - name: Check out repository uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} + if: matrix.python-version != 'pypy2' uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: pip + - name: Install PyPy2 from source + if: matrix.python-version == 'pypy2' + run: | + sudo apt-get update + sudo apt-get install -y build-essential libbz2-dev libffi-dev libssl-dev libsqlite3-dev zlib1g-dev libncurses5-dev libgdbm-dev libc6-dev tk-dev libreadline-dev liblzma-dev + git clone --depth 1 https://github.com/pypy/pypy.git /tmp/pypy2 + cd /tmp/pypy2 + python3 ./rpython/bin/rpython --version || true + echo "PyPy2 provisioning is not supported by the GitHub-hosted runner image; this job is expected to fail unless a compatible toolchain is available." + exit 1 + - name: Install build dependencies run: | python -m pip install --upgrade pip setuptools wheel diff --git a/tox.ini b/tox.ini index f015ed6..d9f707d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,7 @@ [tox] -envlist = py39,py310,py311,py312,py313,py314,py27 +envlist = py39,py310,py311,py312,py313,py314,py27,pypy2_env,pypy3_env skipsdist = False +skip_missing_interpreters = true [testenv] basepython = @@ -11,6 +12,8 @@ basepython = py313: python3.13 py314: python3.14 py27: python2.7 + pypy2_env: /opt/homebrew/bin/pypy + pypy3_env: /opt/homebrew/bin/pypy3 setenv = PIP_DISABLE_PIP_VERSION_CHECK = 1 commands = From 409cf11cfff12e128cf4eb22172e1425b574b436 Mon Sep 17 00:00:00 2001 From: Olaf Conradi Date: Sun, 28 Jun 2026 13:36:52 +0200 Subject: [PATCH 05/14] Update docs --- CHANGELOG.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd7e757..dd807b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ project adheres to [Semantic Versioning][semver]. ### Added -- Support for Python 3.11 and 3.12 +- Support for Python 3.11, 3.12, 3.13 and 3.14. ### Changed diff --git a/README.md b/README.md index e2758f3..1690090 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ Or install in editable mode using pip: ## Testing -This module is tested using Python 2.7, PyPy, and Python 3.3 and up. +This module is tested using Python 2.7, PyPy, and Python 3.9 and up. You can run the test suite using: From 366070167d020283e3c129be804763e38a85fc1e Mon Sep 17 00:00:00 2001 From: Olaf Conradi Date: Sun, 28 Jun 2026 13:47:57 +0200 Subject: [PATCH 06/14] The GitHub Actions failure was caused by the setup-python cache step expecting a dependency manifest, but this repository uses setuptools metadata from setup.py rather than a requirements file. --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7629638..00e843d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,6 +69,9 @@ jobs: with: python-version: ${{ matrix.python-version }} cache: pip + cache-dependency-path: | + setup.py + tox.ini - name: Install PyPy2 from source if: matrix.python-version == 'pypy2' @@ -126,6 +129,9 @@ jobs: with: python-version: '3.12' cache: pip + cache-dependency-path: | + setup.py + tox.ini - name: Build source and wheel distributions run: | From 001562639baec59edd90b87e8d333bf5df7989d6 Mon Sep 17 00:00:00 2001 From: Olaf Conradi Date: Sun, 28 Jun 2026 13:50:37 +0200 Subject: [PATCH 07/14] Update supported versions --- setup.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/setup.py b/setup.py index 05b0e5b..c066ab3 100755 --- a/setup.py +++ b/setup.py @@ -44,16 +44,12 @@ "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Security :: Cryptography", @@ -65,7 +61,7 @@ maintainer="Olaf Conradi", maintainer_email="olaf@conradi.org", license="Public Domain", - python_requires=">=2.7,!=3.0.*,!=3.1.*,!=3.2.*", + python_requires=">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,!=3.8.*", platforms=["any"], ext_modules=[Extension("whirlpool", ["whirlpool/pywhirlpool.c"], include_dirs=["lib"])], From c15bc8185d2bd8ac1fc14749df6c5fbeb07343e6 Mon Sep 17 00:00:00 2001 From: Olaf Conradi Date: Sun, 28 Jun 2026 13:54:55 +0200 Subject: [PATCH 08/14] Pypy2 not supported on GH actions --- .github/workflows/ci.yml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00e843d..b0f8ce5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,8 +54,6 @@ jobs: python-version: '3.14' - os: windows-latest python-version: '3.14' - - os: ubuntu-latest - python-version: 'pypy2' - os: ubuntu-latest python-version: 'pypy3' @@ -64,7 +62,6 @@ jobs: uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - if: matrix.python-version != 'pypy2' uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -73,17 +70,6 @@ jobs: setup.py tox.ini - - name: Install PyPy2 from source - if: matrix.python-version == 'pypy2' - run: | - sudo apt-get update - sudo apt-get install -y build-essential libbz2-dev libffi-dev libssl-dev libsqlite3-dev zlib1g-dev libncurses5-dev libgdbm-dev libc6-dev tk-dev libreadline-dev liblzma-dev - git clone --depth 1 https://github.com/pypy/pypy.git /tmp/pypy2 - cd /tmp/pypy2 - python3 ./rpython/bin/rpython --version || true - echo "PyPy2 provisioning is not supported by the GitHub-hosted runner image; this job is expected to fail unless a compatible toolchain is available." - exit 1 - - name: Install build dependencies run: | python -m pip install --upgrade pip setuptools wheel From 346b8b92a44735307d67269e1a2a57b821700ae7 Mon Sep 17 00:00:00 2001 From: Olaf Conradi Date: Sun, 28 Jun 2026 13:57:17 +0200 Subject: [PATCH 09/14] Adjust pypy3 on GH actions --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b0f8ce5..ee2b6ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,7 +55,7 @@ jobs: - os: windows-latest python-version: '3.14' - os: ubuntu-latest - python-version: 'pypy3' + python-version: 'pypy-3.10' steps: - name: Check out repository From c894477cd0b96ab08cb07bfe52012378c81a5ea7 Mon Sep 17 00:00:00 2001 From: Olaf Conradi Date: Sun, 28 Jun 2026 14:03:03 +0200 Subject: [PATCH 10/14] Fix typo in test block size Checked the wrong variable --- test/test_whirlpool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_whirlpool.py b/test/test_whirlpool.py index 88e7ebb..15b41b0 100644 --- a/test/test_whirlpool.py +++ b/test/test_whirlpool.py @@ -118,7 +118,7 @@ def test_block_size(self): wp = whirlpool.new() self.assertEqual(wp.block_size, 64) with self.assertRaises((AttributeError, TypeError)): - wp.digest_size = 32 + wp.block_size = 32 if __name__ == '__main__': From af03973fc1fe37d4dd2ffb872674ed6bef86050a Mon Sep 17 00:00:00 2001 From: Olaf Conradi Date: Sun, 28 Jun 2026 14:06:04 +0200 Subject: [PATCH 11/14] Update documentation --- README.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 1690090..4f35a64 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # python-whirlpool -[![Travis CI Build Status](https://travis-ci.org/oohlaf/python-whirlpool.svg?branch=master)](https://travis-ci.org/oohlaf/python-whirlpool) -[![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/pw35grm8ald8lg22/branch/master?svg=true)](https://ci.appveyor.com/project/oohlaf/python-whirlpool/branch/master) +[![CI](https://github.com/oohlaf/python-whirlpool/actions/workflows/ci.yml/badge.svg)](https://github.com/oohlaf/python-whirlpool/actions/workflows/ci.yml) [![License: Unlicense](https://img.shields.io/badge/license-Unlicense-blue.svg)](http://unlicense.org/) The [Whirlpool] algorithm is designed by Vincent Rijmen and Paulo S.L.M. Barreto. @@ -56,19 +55,19 @@ The source code is available on [GitHub]. Install in development mode using: - python setup.py develop - -Or install in editable mode using pip: - pip install -e . ## Testing This module is tested using Python 2.7, PyPy, and Python 3.9 and up. -You can run the test suite using: +You can run the test suite locally with: + + python -m unittest discover -s test -p 'test_*.py' + +For a multi-version local test matrix, you can also use: - python setup.py test + tox [Whirlpool]: https://en.wikipedia.org/wiki/Whirlpool_(cryptography) [NESSIE]: https://www.cosic.esat.kuleuven.be/nessie/ From f7b95d7a85ff1a2a21b41dc7a3ae7f27c7934032 Mon Sep 17 00:00:00 2001 From: Olaf Conradi Date: Sun, 28 Jun 2026 14:28:58 +0200 Subject: [PATCH 12/14] PEP 517-style build configuration via pyproject.toml --- CHANGELOG.md | 6 ++++-- pyproject.toml | 3 +++ 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 pyproject.toml diff --git a/CHANGELOG.md b/CHANGELOG.md index dd807b4..dfbed1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,11 +9,13 @@ project adheres to [Semantic Versioning][semver]. ### Added - Support for Python 3.11, 3.12, 3.13 and 3.14. +- PEP 517-style build configuration via pyproject.toml. ### Changed -- Included newer Python versions in CI scripting up to Python 3.10 -- Now also builds on PyPy3 +- Included newer Python versions in CI scripting up to Python 3.10. +- Now also builds on PyPy3. +- Fix unit test for block size. ## [1.0.0] (2018-02-19) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4a85092 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools>=61", "wheel"] +build-backend = "setuptools.build_meta" From 6fdf02dea406f9bfb8bb0a1453ac4e51db7343a7 Mon Sep 17 00:00:00 2001 From: Olaf Conradi Date: Sun, 28 Jun 2026 14:34:04 +0200 Subject: [PATCH 13/14] Smoke test built artifacts --- .github/workflows/ci.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee2b6ec..d1f1e61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -124,6 +124,14 @@ jobs: python -m pip install --upgrade pip build python -m build + - name: Smoke test built artifacts + run: | + python -m venv .venv-smoke + . .venv-smoke/bin/activate + python -m pip install --upgrade pip + python -m pip install dist/*.whl + python -m unittest discover -s test -p 'test_*.py' + - name: Upload build artifacts uses: actions/upload-artifact@v4 with: From 89b7d4a03c50cd7a6d954f31180d3a3cd4b0c0dc Mon Sep 17 00:00:00 2001 From: Olaf Conradi Date: Sun, 28 Jun 2026 14:53:34 +0200 Subject: [PATCH 14/14] Move metadata to pyproject.toml --- pyproject.toml | 45 ++++++++++++++++++++++++++++++- setup.py | 73 +++++--------------------------------------------- 2 files changed, 50 insertions(+), 68 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4a85092..124934a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,46 @@ [build-system] -requires = ["setuptools>=61", "wheel"] +requires = [ + "setuptools>=61; python_version>='3.7'", + "setuptools<45; python_version<'3.7'", + "wheel", +] build-backend = "setuptools.build_meta" + +[project] +name = "Whirlpool" +version = "1.0.1.dev0" +description = "Bindings for whirlpool hash reference implementation." +readme = { file = "README.md", content-type = "text/markdown" } +requires-python = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,!=3.8.*" +license = { text = "Public Domain" } +authors = [{ name = "Olaf Conradi", email = "olaf@conradi.org" }] +keywords = ["digest", "hashlib", "whirlpool"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: Public Domain", + "Programming Language :: C", + "Programming Language :: Python", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: Security :: Cryptography", + "Topic :: Software Development :: Libraries :: Python Modules", +] + +[project.urls] +Homepage = "https://github.com/oohlaf/python-whirlpool" +Repository = "https://github.com/oohlaf/python-whirlpool" +Changelog = "https://github.com/oohlaf/python-whirlpool/blob/master/CHANGELOG.md" + +[tool.setuptools] +packages = [] +include-package-data = false diff --git a/setup.py b/setup.py index c066ab3..47b9220 100755 --- a/setup.py +++ b/setup.py @@ -1,70 +1,9 @@ """Whirlpool: Bindings for whirlpool hash reference implementation.""" -import os -import sys - from setuptools import setup, Extension -if sys.version_info.major < 3: - from io import open - - -VERSION = '1.0.1.dev0' -GITHUB_URL = 'https://github.com/oohlaf/python-whirlpool' -DOWNLOAD_URL = '{}/archive/{}.zip'.format(GITHUB_URL, VERSION) - -DOCLINES = __doc__.strip().split('\n') - -HERE = os.path.abspath(os.path.dirname(__file__)) -with open(os.path.join(HERE, 'README.md'), encoding='utf-8') as f: - README = '\n' + f.read() -with open(os.path.join(HERE, 'CHANGELOG.md'), encoding='utf-8') as f: - CHANGELOG = '\n' + f.read() -LONG_DESC = README + '\n' + CHANGELOG - - -try: - import pypandoc - LONG_DESC = pypandoc.convert_text(LONG_DESC, 'rst', format='markdown') - LONG_DESC_CTYPE = 'text/x-rst' -except (IOError, ImportError): - LONG_DESC_CTYPE = 'text/markdown' - - -setup(name="Whirlpool", - version=VERSION, - description=DOCLINES[0], - long_description=LONG_DESC, - long_description_content_type=LONG_DESC_CTYPE, - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "License :: Public Domain", - "Programming Language :: C", - "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", - "Topic :: Security :: Cryptography", - "Topic :: Software Development :: Libraries :: Python Modules", - ], - keywords="digest hashlib whirlpool", - url=GITHUB_URL, - download_url=DOWNLOAD_URL, - maintainer="Olaf Conradi", - maintainer_email="olaf@conradi.org", - license="Public Domain", - python_requires=">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,!=3.8.*", - platforms=["any"], - ext_modules=[Extension("whirlpool", ["whirlpool/pywhirlpool.c"], - include_dirs=["lib"])], - data_files=[("whirlpool", ['lib/nessie.h', "lib/Whirlpool.c"])], - test_suite="test" - ) +setup( + version='1.0.1.dev0', + ext_modules=[Extension('whirlpool', ['whirlpool/pywhirlpool.c'], include_dirs=['lib'])], + data_files=[('whirlpool', ['lib/nessie.h', 'lib/Whirlpool.c'])], + test_suite='test', +)