Skip to content

feat: test the Windows backend and release 1.1.0 - #6

Open
WigoWigo10 wants to merge 2 commits into
masterfrom
feat/windows-backend-tests
Open

feat: test the Windows backend and release 1.1.0#6
WigoWigo10 wants to merge 2 commits into
masterfrom
feat/windows-backend-tests

Conversation

@WigoWigo10

Copy link
Copy Markdown
Owner

Why 1.1.0 and not 1.0.0

directkeys 1.0.0 is already on PyPI, uploaded on 2025-10-11, before any of
the fork's fixes existed. What is published there does not work:
force_reset_keyboard() and get_stuck_keys() resolve to empty stubs, the
AltGr abstraction cannot be toggled, and holding shift builds a 32772-element
tuple on every keystroke. PyPI does not allow re-uploading a version, so the
fixes have to ship under a new one.

Why it shipped broken

Nothing tested it. The existing suite replaces directkeys._os_keyboard with a
fake, so none of its 161 tests reach _winkeyboard — which is precisely the
code that makes this fork distinctive. Every bug fixed since passed through
that suite unnoticed.

What this adds

tests/test_winkeyboard.py drives process_key with synthetic hook events, so
it needs no physical keyboard and no message pump, and is skipped off Windows.
20 tests covering:

  • the AltGr merge, its disabled mode, and the lone-Right-Alt flush
  • is_keypad and the scan_code or -vk fallback
  • KeyboardEvent.flags reaching the event and its JSON
  • the backend helper wiring and rebuild_name_tables reachability

process_key moves from a closure inside prepare_intercept to module level,
taking the callback as its first argument, so it can be called directly. No
behaviour change — prepare_intercept passes its own callback through.

How the tests were validated

Passing against correct code proves nothing, so each of the eight bugs was
reintroduced into the source and the matching test re-run. All eight are
caught.

Two mutants survived the first pass. That turned out to be a flaw in the
mutation harness rather than in the tests: is_keypad = ... in keypad_keys
appears in both get_event_names and process_key, and the harness was
patching the wrong occurrence. Retargeted, the result is 8 of 8.

Also

  • CHANGES.md restructured: the 1.0.0 entry now describes what that release
    actually contained, instead of crediting it with work that only exists now.
  • Distribution built and twine checked as 1.1.0.

Still open

  • KeyboardEvent.flags is masked to the LLKHF_EXTENDED bit, so it does not
    yet expose the raw hook flags its name promises.
  • _darwinmouse.py raises NameError on macOS, inherited from upstream.

🤖 Generated with Claude Code

WigoWigo10 and others added 2 commits August 1, 2026 19:11
directkeys 1.0.0 is already on PyPI, published before any of the fork's fixes
landed, so the features it advertises do not work there: the stuck key helpers
resolve to empty stubs, the AltGr abstraction cannot be toggled, and holding
shift builds a 32772-element tuple on every keystroke. PyPI does not allow
re-uploading a version, so the fixes ship as 1.1.0.

The reason all of that shipped broken is that nothing tested it. The existing
suite replaces `directkeys._os_keyboard` with a fake, so none of its 161 tests
reach `_winkeyboard` -- exactly the code that makes this fork distinctive.

tests/test_winkeyboard.py drives `process_key` with synthetic hook events, so
it needs no keyboard and no message pump, and is skipped off Windows. It covers
the AltGr merge and its disabled mode, the keypad and scan-code fields, the
event flags, and the backend helper wiring.

To make that possible, `process_key` moves from a closure inside
`prepare_intercept` to module level, taking the callback as its first argument.
No behaviour change; `prepare_intercept` passes its own callback through.

The tests were validated by mutation testing rather than by passing: each of
the eight bugs was reintroduced into the source, and each was caught by its
test. Two mutants initially survived, which turned out to be a flaw in the
mutation harness -- `is_keypad = ... in keypad_keys` appears in both
`get_event_names` and `process_key`, and the wrong occurrence was being
patched. With the right one targeted, 8 of 8 are detected.

CHANGES.md is restructured: the 1.0.0 entry now describes what that release
actually contained, rather than crediting it with work that only exists now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI caught two things the local run had missed.

**test_record**

The test was flaky, and the earlier "widen the sleep" mitigation was not a fix:
it failed again on ubuntu-latest/3.13. Investigating properly turned up two
distinct races, one of them in the library rather than the test.

The first is a registration race, and is fixable: `record` registers the
recording hook and then the suppressing hotkey, so events fed before that
finishes are lost. Waiting for the hotkey covers both. The barrier has to wait
for the handler and not merely for the key, because `blocking_hotkeys` is a
defaultdict and `container[scan_codes].append(handler)` publishes an empty list
under the key before appending to it.

The second is in the library. Handlers -- and therefore the recorder -- are
invoked by the listener thread draining `_listener.queue`, while the hotkey
callback that ends the recording runs synchronously inside `direct_callback`,
*before* the event is put on that queue. So `record` can return and unhook the
recorder before the terminating key is ever delivered to it. No barrier in the
test can close that window.

The assertion is therefore relaxed to what the library actually guarantees: the
keys pressed before the terminator are recorded, and `record` returns. Asserting
that `space` is included was asserting something that merely happened to work
most of the time. Fixing it properly means changing when `direct_callback`
queues the event relative to the hotkey callbacks, which has suppression
implications and does not belong in this change.

80 consecutive runs pass, against 3 failures in 60 with the previous barrier.

**Ruff pin**

`astral-sh/ruff-action@v3` installs its own latest Ruff, whose formatter
disagreed with the pinned 0.16.1 in .pre-commit-config.yaml, failing CI on
tests/test_winkeyboard.py. The action is now pinned to the same version.

That file also reached the previous commit unformatted, because
`pre-commit run --all-files` only visits files git already tracks and it was
still untracked at the time. It is formatted here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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