Skip to content
Merged
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
79 changes: 79 additions & 0 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: publish
concurrency:
group: ${{ github.workflow }}
on: # yamllint disable-line rule:truthy
release:
types: [published]

jobs:
build-wheels:
if: ${{ github.repository == 'Dandelion-Science/python-evdev' }}
name: wheels on ${{ matrix.os }}
# evdev is Linux-only (needs <linux/input.h>); 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:
os: [ubuntu-latest, ubuntu-24.04-arm]
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: pypa/cibuildwheel@v4.1.0
- uses: actions/upload-artifact@v4
with:
name: cibw-wheels-${{ matrix.os }}
path: ./wheelhouse/*.whl

build-sdist:
if: ${{ github.repository == 'Dandelion-Science/python-evdev' }}
runs-on: ubuntu-slim
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: astral-sh/setup-uv@v7
- run: uv build --sdist
- uses: actions/upload-artifact@v4
with:
name: cibw-sdist
path: dist/*.tar.gz

publish:
needs: [build-wheels, build-sdist]
runs-on: ubuntu-slim
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
pattern: cibw-*
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_PYPI_STORE_REPOSITORY_URL }}
34 changes: 34 additions & 0 deletions .github/workflows/pytest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.13", "3.13t", "3.14", "3.14t"]
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: astral-sh/setup-uv@v7
with:
activate-environment: true
python-version: ${{ matrix.python-version }}
- run: uv sync -q --no-default-groups --group test
# 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
22 changes: 22 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,34 @@ classifiers = [
[project.urls]
"Homepage" = "https://github.com/gvalkov/python-evdev"

[dependency-groups]
test = ["pytest"]

[tool.ruff]
line-length = 120

[tool.ruff.lint]
ignore = ["E265", "E241", "F403", "F401", "E401", "E731"]

[tool.cibuildwheel]
# evdev is Linux-only (needs <linux/input.h>): 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 <linux/input.h>;
# 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
Expand Down
27 changes: 27 additions & 0 deletions tests/test_free_threaded.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import os
import subprocess
import sys

import pytest


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=env,
)
assert result.returncode == 0, f"evdev re-enabled the GIL:\n{result.stderr}"