feat: test the Windows backend and release 1.1.0 - #6
Open
WigoWigo10 wants to merge 2 commits into
Open
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why 1.1.0 and not 1.0.0
directkeys1.0.0 is already on PyPI, uploaded on 2025-10-11, before any ofthe fork's fixes existed. What is published there does not work:
force_reset_keyboard()andget_stuck_keys()resolve to empty stubs, theAltGr 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_keyboardwith afake, so none of its 161 tests reach
_winkeyboard— which is precisely thecode that makes this fork distinctive. Every bug fixed since passed through
that suite unnoticed.
What this adds
tests/test_winkeyboard.pydrivesprocess_keywith synthetic hook events, soit needs no physical keyboard and no message pump, and is skipped off Windows.
20 tests covering:
is_keypadand thescan_code or -vkfallbackKeyboardEvent.flagsreaching the event and its JSONrebuild_name_tablesreachabilityprocess_keymoves from a closure insideprepare_interceptto module level,taking the callback as its first argument, so it can be called directly. No
behaviour change —
prepare_interceptpasses 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_keysappears in both
get_event_namesandprocess_key, and the harness waspatching the wrong occurrence. Retargeted, the result is 8 of 8.
Also
CHANGES.mdrestructured: the 1.0.0 entry now describes what that releaseactually contained, instead of crediting it with work that only exists now.
twine checked as 1.1.0.Still open
KeyboardEvent.flagsis masked to theLLKHF_EXTENDEDbit, so it does notyet expose the raw hook flags its name promises.
_darwinmouse.pyraisesNameErroron macOS, inherited from upstream.🤖 Generated with Claude Code