From 6becf654b091b797dc6acc029292da235d3de1f7 Mon Sep 17 00:00:00 2001 From: Federico Mengozzi <19249682+fedemengo@users.noreply.github.com> Date: Fri, 26 Jun 2026 15:35:48 +0200 Subject: [PATCH 1/3] setup ci --- .github/workflows/publish.yaml | 95 ++++++++++++++++++++++++++++++++++ .github/workflows/pytest.yaml | 38 ++++++++++++++ pyproject.toml | 3 ++ 3 files changed, 136 insertions(+) create mode 100644 .github/workflows/publish.yaml create mode 100644 .github/workflows/pytest.yaml diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..5cc3cb1 --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,95 @@ +name: publish +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: true +on: # yamllint disable-line rule:truthy + release: + types: [published] + +jobs: + build: + if: ${{ github.repository == 'Dandelion-Science/python-evdev' }} + name: build py${{ matrix.python-version }} + runs-on: codebuild-dandelion-linux-amd64-${{ github.run_id }}-${{ github.run_attempt }} + strategy: + fail-fast: false + matrix: + python-version: ["3.13", "3.13t", "3.14", "3.14t"] + timeout-minutes: 15 + permissions: + id-token: write + contents: read + steps: + - uses: aws-actions/configure-aws-credentials@v6 + with: + aws-region: ${{ vars.AWS_REGION }} + role-to-assume: ${{ vars.ARN_IAM_ROLE_GHA_CORE }} + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - uses: Dandelion-Science/github-actions/setup-uv@main + with: + python-version: ${{ matrix.python-version }} + - run: uv build --wheel + - uses: actions/upload-artifact@v4 + with: + name: dist-${{ matrix.python-version }} + path: dist/ + + build-sdist: + if: ${{ github.repository == 'Dandelion-Science/python-evdev' }} + runs-on: codebuild-dandelion-linux-arm64-${{ github.run_id }}-${{ github.run_attempt }} + timeout-minutes: 15 + permissions: + id-token: write + contents: read + steps: + - uses: aws-actions/configure-aws-credentials@v6 + with: + aws-region: ${{ vars.AWS_REGION }} + role-to-assume: ${{ vars.ARN_IAM_ROLE_GHA_CORE }} + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - uses: Dandelion-Science/github-actions/setup-uv@main + - run: uv build --sdist + - uses: actions/upload-artifact@v4 + with: + name: dist-sdist + path: dist/ + + publish: + if: ${{ github.repository == 'Dandelion-Science/python-evdev' }} + needs: [build, build-sdist] + runs-on: codebuild-dandelion-linux-arm64-${{ github.run_id }}-${{ github.run_attempt }} + timeout-minutes: 15 + permissions: + id-token: write + contents: write + steps: + - uses: aws-actions/configure-aws-credentials@v6 + with: + aws-region: ${{ vars.AWS_REGION }} + role-to-assume: ${{ vars.ARN_IAM_ROLE_GHA_CORE }} + - uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + - uses: softprops/action-gh-release@v3 + with: + files: dist/* + tag_name: ${{ github.event.release.tag_name }} + - name: Get CodeArtifact token + run: | + AWS_CODEARTIFACT_TOKEN=$(aws codeartifact get-authorization-token \ + --domain ${{ vars.CODEARTIFACT_DOMAIN }} \ + --domain-owner ${{ vars.CODEARTIFACT_DOMAIN_OWNER }} \ + --query authorizationToken \ + --output text) + echo "AWS_CODEARTIFACT_TOKEN=$AWS_CODEARTIFACT_TOKEN" >> $GITHUB_ENV + - uses: Dandelion-Science/github-actions/setup-uv@main + - run: uv publish + env: + UV_PUBLISH_USERNAME: aws + UV_PUBLISH_PASSWORD: ${{ env.AWS_CODEARTIFACT_TOKEN }} + UV_PUBLISH_URL: ${{ vars.CODEARTIFACT_DANDELION_PYPI_REPOSITORY_URL }} diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml new file mode 100644 index 0000000..4771194 --- /dev/null +++ b/.github/workflows/pytest.yaml @@ -0,0 +1,38 @@ +name: pytest +concurrency: + group: ${{ github.workflow }}-${{ github.event.number }}-${{ github.event.ref }} + cancel-in-progress: true +on: # yamllint disable-line rule:truthy + pull_request: + push: + branches: [main] + workflow_dispatch: + +jobs: + pytest: + if: ${{ !cancelled() && github.repository == 'Dandelion-Science/python-evdev' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }} + name: py${{ matrix.python-version }} + runs-on: codebuild-dandelion-linux-arm64-${{ github.run_id }}-${{ github.run_attempt }} + strategy: + fail-fast: false + matrix: + python-version: ["3.13", "3.13t", "3.14", "3.14t"] + timeout-minutes: 30 + permissions: + id-token: write + contents: read + steps: + - uses: aws-actions/configure-aws-credentials@v6 + with: + aws-region: ${{ vars.AWS_REGION }} + role-to-assume: ${{ vars.ARN_IAM_ROLE_GHA_CORE }} + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - uses: Dandelion-Science/github-actions/setup-uv@main + with: + activate-environment: true + python-version: ${{ matrix.python-version }} + - run: uv sync -q --no-default-groups --group test + - run: pytest tests/test_ecodes.py tests/test_events.py tests/test_util.py + - run: sudo .venv/bin/pytest tests/test_uinput.py diff --git a/pyproject.toml b/pyproject.toml index d0b4f7c..7b4344e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,9 @@ classifiers = [ [project.urls] "Homepage" = "https://github.com/gvalkov/python-evdev" +[dependency-groups] +test = ["pytest"] + [tool.ruff] line-length = 120 From 689b5d47a4499565b5864650b36df40c81cb6b85 Mon Sep 17 00:00:00 2001 From: Federico Mengozzi <19249682+fedemengo@users.noreply.github.com> Date: Fri, 26 Jun 2026 15:43:54 +0200 Subject: [PATCH 2/3] add test for GIL --- tests/test_free_threaded.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 tests/test_free_threaded.py diff --git a/tests/test_free_threaded.py b/tests/test_free_threaded.py new file mode 100644 index 0000000..ed14b63 --- /dev/null +++ b/tests/test_free_threaded.py @@ -0,0 +1,21 @@ +import os +import subprocess +import sys + +import pytest + + +def _is_freethreaded_build(): + import sysconfig + return bool(sysconfig.get_config_var("Py_GIL_DISABLED")) + + +@pytest.mark.skipif(not _is_freethreaded_build(), reason="requires free-threaded Python build") +def test_evdev_does_not_reenable_gil(): + result = subprocess.run( + [sys.executable, "-c", "import sys; import evdev; assert not sys._is_gil_enabled()"], + capture_output=True, + text=True, + env={**os.environ, "PYTHON_GIL": "0"}, + ) + assert result.returncode == 0, f"evdev re-enabled the GIL:\n{result.stderr}" From 606db096211bcbbc533f468e5e91159c2a26f525 Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Mon, 29 Jun 2026 15:48:30 +0200 Subject: [PATCH 3/3] update CIs --- .github/workflows/publish.yaml | 52 ++++++++++++---------------------- .github/workflows/pytest.yaml | 16 ++++------- pyproject.toml | 19 +++++++++++++ tests/test_free_threaded.py | 10 +++++-- 4 files changed, 51 insertions(+), 46 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 5cc3cb1..ce94348 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -1,67 +1,50 @@ name: publish concurrency: group: ${{ github.workflow }} - cancel-in-progress: true on: # yamllint disable-line rule:truthy release: types: [published] jobs: - build: + build-wheels: if: ${{ github.repository == 'Dandelion-Science/python-evdev' }} - name: build py${{ matrix.python-version }} - runs-on: codebuild-dandelion-linux-amd64-${{ github.run_id }}-${{ github.run_attempt }} + name: wheels on ${{ matrix.os }} + # evdev is Linux-only (needs ); no macOS/Windows wheels are + # possible. Native runners build their own arch (archs = "native"), no QEMU. + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - python-version: ["3.13", "3.13t", "3.14", "3.14t"] - timeout-minutes: 15 - permissions: - id-token: write - contents: read + os: [ubuntu-latest, ubuntu-24.04-arm] + timeout-minutes: 30 steps: - - uses: aws-actions/configure-aws-credentials@v6 - with: - aws-region: ${{ vars.AWS_REGION }} - role-to-assume: ${{ vars.ARN_IAM_ROLE_GHA_CORE }} - uses: actions/checkout@v7 with: fetch-depth: 0 - - uses: Dandelion-Science/github-actions/setup-uv@main - with: - python-version: ${{ matrix.python-version }} - - run: uv build --wheel + - uses: pypa/cibuildwheel@v4.1.0 - uses: actions/upload-artifact@v4 with: - name: dist-${{ matrix.python-version }} - path: dist/ + name: cibw-wheels-${{ matrix.os }} + path: ./wheelhouse/*.whl build-sdist: if: ${{ github.repository == 'Dandelion-Science/python-evdev' }} - runs-on: codebuild-dandelion-linux-arm64-${{ github.run_id }}-${{ github.run_attempt }} + runs-on: ubuntu-slim timeout-minutes: 15 - permissions: - id-token: write - contents: read steps: - - uses: aws-actions/configure-aws-credentials@v6 - with: - aws-region: ${{ vars.AWS_REGION }} - role-to-assume: ${{ vars.ARN_IAM_ROLE_GHA_CORE }} - uses: actions/checkout@v7 with: fetch-depth: 0 - - uses: Dandelion-Science/github-actions/setup-uv@main + - uses: astral-sh/setup-uv@v7 - run: uv build --sdist - uses: actions/upload-artifact@v4 with: - name: dist-sdist - path: dist/ + name: cibw-sdist + path: dist/*.tar.gz publish: - if: ${{ github.repository == 'Dandelion-Science/python-evdev' }} - needs: [build, build-sdist] - runs-on: codebuild-dandelion-linux-arm64-${{ github.run_id }}-${{ github.run_attempt }} + needs: [build-wheels, build-sdist] + runs-on: ubuntu-slim timeout-minutes: 15 permissions: id-token: write @@ -74,6 +57,7 @@ jobs: - uses: actions/download-artifact@v4 with: path: dist + pattern: cibw-* merge-multiple: true - uses: softprops/action-gh-release@v3 with: @@ -92,4 +76,4 @@ jobs: env: UV_PUBLISH_USERNAME: aws UV_PUBLISH_PASSWORD: ${{ env.AWS_CODEARTIFACT_TOKEN }} - UV_PUBLISH_URL: ${{ vars.CODEARTIFACT_DANDELION_PYPI_REPOSITORY_URL }} + UV_PUBLISH_URL: ${{ vars.CODEARTIFACT_PYPI_STORE_REPOSITORY_URL }} diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 4771194..bea29b3 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -12,27 +12,23 @@ jobs: pytest: if: ${{ !cancelled() && github.repository == 'Dandelion-Science/python-evdev' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }} name: py${{ matrix.python-version }} - runs-on: codebuild-dandelion-linux-arm64-${{ github.run_id }}-${{ github.run_attempt }} + runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ["3.13", "3.13t", "3.14", "3.14t"] timeout-minutes: 30 - permissions: - id-token: write - contents: read steps: - - uses: aws-actions/configure-aws-credentials@v6 - with: - aws-region: ${{ vars.AWS_REGION }} - role-to-assume: ${{ vars.ARN_IAM_ROLE_GHA_CORE }} - uses: actions/checkout@v7 with: fetch-depth: 0 - - uses: Dandelion-Science/github-actions/setup-uv@main + - uses: astral-sh/setup-uv@v7 with: activate-environment: true python-version: ${{ matrix.python-version }} - run: uv sync -q --no-default-groups --group test - - run: pytest tests/test_ecodes.py tests/test_events.py tests/test_util.py + # test_uinput.py needs root (opens /dev/uinput) and does not skip without it, + # so the rest of the suite runs as the normal user and uinput runs under sudo. + - run: pytest tests --ignore=tests/test_uinput.py + - run: sudo modprobe uinput - run: sudo .venv/bin/pytest tests/test_uinput.py diff --git a/pyproject.toml b/pyproject.toml index 7b4344e..8a404bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,25 @@ line-length = 120 [tool.ruff.lint] ignore = ["E265", "E241", "F403", "F401", "E401", "E731"] +[tool.cibuildwheel] +# evdev is Linux-only (needs ): no macOS/Windows wheels exist. +# Build CPython 3.11-3.14 incl. the free-threaded 3.13t/3.14t builds; glibc only +# (musllinux skipped), CPython only (no PyPy). +build = ["cp311-*", "cp312-*", "cp313-*", "cp314-*", "cp313t-*", "cp314t-*"] +enable = ["cpython-freethreading"] +skip = ["*-musllinux*", "pp*"] +archs = "native" +build-frontend = "build[uv]" +# Smoke-test that the wheel imports and every compiled extension loads. +test-command = "python -c \"import evdev, evdev.ecodes, evdev._input, evdev._uinput, evdev._ecodes\"" + +[tool.cibuildwheel.linux] +# manylinux images are glibc/AlmaLinux based and normally ship kernel-headers +# (pulled in by glibc-devel), but evdev's build hard-fails without ; +# ensure they are present. No-op when already installed. +before-all = "yum install -y kernel-headers || true" +repair-wheel-command = "auditwheel repair -w {dest_dir} {wheel}" + [tool.bumpversion] current_version = "1.9.3" commit = true diff --git a/tests/test_free_threaded.py b/tests/test_free_threaded.py index ed14b63..333c1ff 100644 --- a/tests/test_free_threaded.py +++ b/tests/test_free_threaded.py @@ -5,17 +5,23 @@ import pytest -def _is_freethreaded_build(): +def _is_freethreaded_build() -> bool: import sysconfig + return bool(sysconfig.get_config_var("Py_GIL_DISABLED")) @pytest.mark.skipif(not _is_freethreaded_build(), reason="requires free-threaded Python build") def test_evdev_does_not_reenable_gil(): + # Run with PYTHON_GIL unset (not 0): PYTHON_GIL=0 force-disables the GIL + # regardless of the module, which would make this pass even for a C extension + # that has not declared Py_MOD_GIL_NOT_USED. We want the default behavior so + # importing a non-free-threaded extension actually re-enables the GIL and fails. + env = {k: v for k, v in os.environ.items() if k != "PYTHON_GIL"} result = subprocess.run( [sys.executable, "-c", "import sys; import evdev; assert not sys._is_gil_enabled()"], capture_output=True, text=True, - env={**os.environ, "PYTHON_GIL": "0"}, + env=env, ) assert result.returncode == 0, f"evdev re-enabled the GIL:\n{result.stderr}"