Skip to content

Add CI workflow with lint + headless test subset across 3 OS - #678

Open
mxschmitt wants to merge 3 commits into
moses-palmer:masterfrom
mxschmitt:ci/add-github-actions
Open

Add CI workflow with lint + headless test subset across 3 OS#678
mxschmitt wants to merge 3 commits into
moses-palmer:masterfrom
mxschmitt:ci/add-github-actions

Conversation

@mxschmitt

Copy link
Copy Markdown

Context

This addresses the gap discussed in #660: the repo has no CI. The hard part of testing pynput is input simulation, which needs real OS events and a graphical session — that's still manual. This PR lands the easy half: linting and the small subset of tests that don't need real input.

What's added

Workflow (.github/workflows/ci.yml):

  • lint job — runs ruff format --check on lib/pynput and tests/ with quote-style=preserve and line-length=80. These are the same options that produce the formatting in commit eeb7eab ("Apply ruff formatting"), so the check is green on master after the test reformat below. No project-level config file is needed; the options are passed via CLI flags so this PR doesn't have to make a project-wide tooling decision.

  • test job — matrixed across {ubuntu-latest, macos-latest, windows-latest} × Python {3.10, 3.11, 3.12, 3.13} (12 cells). Runs an explicit eight-test subset chosen because they don't synthesize OS input and don't wait on self.confirm():

    • keyboard_hotkey_tests: test_parse_valid, test_parse_invalid, test_activate_single, test_activate_combo — pure parser + state machine
    • mouse_controller_tests::test_buttons and keyboard_controller_tests::test_keys — verify each platform's backend exposes every Button/Key in the base enum
    • keyboard_controller_tests::test_press_invalid and test_release_invalid — exception-path tests that never reach the OS

    Linux runs under xvfb-run -a and pip-installs evdev/python-xlib so the Xorg backend can instantiate Display().

Pre-requisites also in this PR

To make the lint job green and the (now-running) tests pass, two small fixes:

  1. Format tests/ with the same formatter as lib/pynput/ (commit "Apply ruff formatting to tests/ and drop u-string prefix in _info"). The May 12 ruff commit only touched lib/pynput/. Running the same ruff format --config 'format.quote-style=preserve' --config 'line-length=80' on tests/ produces the formatting in this commit. Also drops a u'...' prefix in lib/pynput/_info.py that's a Python-2 leftover and was the only remaining diff in lib/.

  2. Update test callbacks for the v1.8.0 injected parameter (commit "Update test callbacks for the v1.8.0 injected parameter"). v1.8.0 added a trailing injected boolean to all listener callbacks. The arity-clipping in pynput._util._wrap protects callbacks passed directly via Listener(on_X=...), but tests/__init__.py:assert_event forwards args through a *args wrapper that bypasses _wrap, so any callback going through it must accept the new arity. Eleven callbacks across mouse_controller_tests.py and keyboard_controller_tests.py were stale; fixed mechanically by adding injected to their signatures. Confirmed locally on Windows: test_left/right/up/down previously raised TypeError: lambda takes 2 positional arguments but 3 were given and now run cleanly.

What's intentionally out of scope

  • Tests that synthesize input via Controller.press/click/scroll/type/move. macOS GitHub runners can't grant Accessibility permission, so they can never run there cleanly; shared Windows runners would also flake. These remain manual, per Collaborate on testing #660.
  • All listener tests — they call self.notify('Move mouse...') and self.confirm(...) and require a human at the keyboard.
  • pylint. The existing pylintrc produces a -3.37/10 rating against current master out of the box; making it green requires either many code edits or many # pylint: disable= annotations, both larger scope decisions than belong in this PR.
  • A project-level pyproject.toml/ruff.toml. The CLI flags inline in the workflow are deliberately minimal; if you'd prefer a config file I'm happy to add one in a follow-up.

Verification

  • ruff format --check lib/pynput tests is green locally with the options above.
  • The 8 headless-safe tests pass locally on Windows 11 / Python 3.13 in 8 seconds.
  • The 4 previously-broken mouse_controller_tests move tests (test_left/right/up/down) now run without TypeError — outside the CI subset, but the lambda fix unblocks anyone running the suite manually.

mxschmitt added 3 commits May 13, 2026 14:52
The ruff formatting commit (eeb7eab) only reformatted lib/pynput/. This
applies the same formatter (ruff format with quote-style=preserve and
line-length=80) to tests/ so the entire repo can be checked with a
single ruff format --check invocation.

The u-prefix on the __author__ string in lib/pynput/_info.py is a Python
2 leftover with no semantic effect under Python 3 and was the only diff
left in lib/ with the same formatter settings.
v1.8.0 added a trailing 'injected' boolean to all listener callbacks.
The arity-clipping in pynput._util._wrap protects callbacks passed
directly via Listener(on_X=...), but tests/__init__.py:assert_event
forwards args through a *args wrapper that bypasses _wrap, so callbacks
in those paths must accept the new arity.

Affects 11 callbacks across two files:
- mouse_controller_tests.py: on_click (2), on_move (4), on_scroll (2),
  and a nested def on_click in test_click (1)
- keyboard_controller_tests.py: on_press (1), on_release (1)

Confirmed locally on Windows: test_left/right/up/down previously raised
'TypeError: lambda takes 2 positional arguments but 3 were given' and
now run cleanly.
Adds .github/workflows/ci.yml with two jobs:

- lint: ruff format --check on lib/pynput and tests/ with the same
  options that produce the formatting in commit eeb7eab
  (quote-style=preserve, line-length=80). The flags are inline so
  the PR doesn't need to add a project-level ruff config file.

- test: matrixed across {ubuntu, macos, windows} x Python {3.10..3.13}.
  Runs an explicit eight-test subset — keyboard_hotkey parser/state
  machine tests, mouse_controller and keyboard_controller enum
  integrity tests, and the InvalidKey validation tests. These are
  headless-safe across all three OSes: they don't synthesize OS
  input via Controller methods, and don't poll for human input via
  self.confirm() or assert_stop().

  Tests outside this subset fall into two buckets that aren't
  feasible on shared CI runners: those that synthesize input
  (macOS Accessibility permission can't be granted on hosted
  runners, shared Windows runners would flake) and listener tests
  that wait for human input. They remain manual, per moses-palmer#660.

Linux runs under xvfb-run -a so Display() can open, and pip-installs
evdev + python-xlib.
@mxschmitt
mxschmitt force-pushed the ci/add-github-actions branch from ef81f5b to 28b4a43 Compare May 13, 2026 22:21
@mxschmitt

Copy link
Copy Markdown
Author

cc @moses-palmer some initial CI pipeline to run the existing tests for changes on the master branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant