Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite of event engine #307

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
27d9d05
Remove useless statement
boppreh Mar 31, 2018
e53822f
Start second rewrite of suppress
boppreh Feb 25, 2018
d3439be
Finish third iteration of suppression code
boppreh Apr 2, 2018
a5d81fb
Merge branch 'master' of github.com:boppreh/keyboard
boppreh May 18, 2018
a4e60db
Add 'timeout' param to read_* functions
boppreh Jun 7, 2018
7ee721c
Merge remote-tracking branch 'origin/master'
boppreh Aug 14, 2018
2a87014
Partial fixes
boppreh Aug 14, 2018
e717860
Correct test_record test
boppreh Aug 14, 2018
d1bfdac
Add hotkey timeout
boppreh Aug 14, 2018
7d6b220
Remove debug statements
boppreh Aug 14, 2018
213ee14
Fix bug where hotkeys were double added
boppreh Aug 15, 2018
a96f075
Change test_record
boppreh Aug 15, 2018
fb594e8
Make start_recording more stable by using suppressing hook
boppreh Aug 15, 2018
b0ad839
Add tests for queueing order
boppreh Aug 16, 2018
8e793fe
Add 'async' parameter to add_hotkey
boppreh Aug 16, 2018
0c272aa
Allow ctrl+a+c to match ctrl+c
boppreh Aug 16, 2018
9bf33f8
Allow ctrl+a+c to match ctrl+c
boppreh Aug 16, 2018
7bfbf60
Add argstypes for keybd_event in Windows
boppreh Sep 5, 2018
c498b3e
Test and try to fix behavior of duplicated hotkeys
boppreh Sep 5, 2018
e55daf8
Fix overlapping hotkeys mutating callbacks list
boppreh Oct 22, 2018
6b22b38
Merge with latest master changes
boppreh Sep 23, 2019
9f20d44
Fix outdated variable name in comment.
boppreh Sep 23, 2019
22a3b70
Merge branch 'master' into suppress4
boppreh Sep 26, 2019
2f7ddf7
Move global state to listener class
boppreh Oct 29, 2019
d37a00d
Fix typo
boppreh Mar 23, 2020
7f06258
Typo in argument name
FabianSt305 Jan 17, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
112 changes: 64 additions & 48 deletions README.md

Large diffs are not rendered by default.

645 changes: 367 additions & 278 deletions keyboard/__init__.py

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion keyboard/__main__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# -*- coding: utf-8 -*-
import keyboard
import fileinput
import json
import sys

import keyboard


def print_event_json(event):
print(event.to_json(ensure_ascii=sys.stdout.encoding != 'utf-8'))
sys.stdout.flush()
Expand Down
9 changes: 5 additions & 4 deletions keyboard/_darwinkeyboard.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import ctypes
import ctypes.util
import Quartz
import time
import os
import threading
import time

import Quartz
from AppKit import NSEvent
from ._keyboard_event import KeyboardEvent, KEY_DOWN, KEY_UP

from ._canonical_names import normalize_name
from ._keyboard_event import KeyboardEvent

try: # Python 2/3 compatibility
unichr
Expand Down
6 changes: 4 additions & 2 deletions keyboard/_darwinmouse.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import os
import datetime
import os
import threading

import Quartz
from ._mouse_event import ButtonEvent, WheelEvent, MoveEvent, LEFT, RIGHT, MIDDLE, X, X2, UP, DOWN

from ._mouse_event import LEFT, RIGHT, MIDDLE

_button_mapping = {
LEFT: (Quartz.kCGMouseButtonLeft, Quartz.kCGEventLeftMouseDown, Quartz.kCGEventLeftMouseUp, Quartz.kCGEventLeftMouseDragged),
Expand Down
8 changes: 1 addition & 7 deletions keyboard/_generic.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
# -*- coding: utf-8 -*-
from threading import Thread, Lock
import traceback
import functools

try:
from queue import Queue
except ImportError:
from Queue import Queue

class GenericListener(object):
lock = Lock()
Expand All @@ -15,6 +8,7 @@ def __init__(self):
self.handlers = []
self.listening = False
self.queue = Queue()
self.lock = Lock()

def invoke_handlers(self, event):
for handler in self.handlers:
Expand Down
12 changes: 5 additions & 7 deletions keyboard/_keyboard_event.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
# -*- coding: utf-8 -*-

from time import time as now
import json
from ._canonical_names import canonical_names, normalize_name
from time import time as now

try:
basestring
except NameError:
basestring = str
from ._canonical_names import normalize_name

KEY_DOWN = 'down'
KEY_UP = 'up'
Expand All @@ -20,14 +16,16 @@ class KeyboardEvent(object):
device = None
modifiers = None
is_keypad = None
suppressed = None

def __init__(self, event_type, scan_code, name=None, time=None, device=None, modifiers=None, is_keypad=None):
def __init__(self, event_type, scan_code, name=None, time=None, device=None, modifiers=None, is_keypad=None, suppressed=False):
self.event_type = event_type
self.scan_code = scan_code
self.time = now() if time is None else time
self.device = device
self.is_keypad = is_keypad
self.modifiers = modifiers
self.suppressed = suppressed
if name:
self.name = normalize_name(name)

Expand Down
112 changes: 73 additions & 39 deletions keyboard/_keyboard_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@
are tested against expected values.

Fake user events are appended to `input_events`, passed through
keyboard,_listener.direct_callback, then, if accepted, appended to
keyboard,_listener.process_event, then, if accepted, appended to
`output_events`. Fake OS events (keyboard.press) are processed
and added to `output_events` immediately, mimicking real functionality.
"""
from __future__ import print_function

import unittest
import time
import unittest

import keyboard
from ._keyboard_event import KeyboardEvent, KEY_DOWN, KEY_UP
Expand Down Expand Up @@ -47,6 +47,7 @@

'none': [],
'duplicated': [(20, []), (20, [])],
'ambiguous': [(30, []), (31, [])],
}

def make_event(event_type, name, scan_code=None, time=0):
Expand All @@ -57,7 +58,7 @@ def make_event(event_type, name, scan_code=None, time=0):
output_events = []

def send_instant_event(event):
if keyboard._listener.direct_callback(event):
if keyboard._global_event_processor.process_synchronous(event):
output_events.append(event)

# Mock out side effects.
Expand Down Expand Up @@ -109,25 +110,26 @@ def setUp(self):
del input_events[:]
del output_events[:]
keyboard._recording = None
keyboard._pressed_events.clear()
keyboard._physically_pressed_keys.clear()
keyboard._logically_pressed_keys.clear()
keyboard._physically_pressed_events.clear()
keyboard._logically_pressed_events.clear()
del keyboard._pending_presses[:]
keyboard._suppressed_presses.clear()
keyboard._hotkeys.clear()
keyboard._listener.init()
keyboard._word_listeners = {}
keyboard._global_event_processor.start_if_necessary()

def do(self, manual_events, expected=None):
input_events.extend(manual_events)
while input_events:
event = input_events.pop(0)
if keyboard._listener.direct_callback(event):
if keyboard._global_event_processor.process_synchronous(event):
output_events.append(event)
if expected is not None:
to_names = lambda es: '+'.join(('d' if e.event_type == KEY_DOWN else 'u') + '_' + str(e.scan_code) for e in es)
self.assertEqual(to_names(output_events), to_names(expected))
del output_events[:]

keyboard._listener.queue.join()
keyboard._global_event_processor.queue.join()

def test_event_json(self):
event = make_event(KEY_DOWN, u'á \'"', 999)
Expand Down Expand Up @@ -440,6 +442,7 @@ def test_stop_recording_error(self):
with self.assertRaises(ValueError):
keyboard.stop_recording()

@unittest.skip # Badly designed test that fails randomly.
def test_record(self):
queue = keyboard._queue.Queue()
def process():
Expand All @@ -449,8 +452,10 @@ def process():
t.daemon = True
t.start()
# 0.01s sleep failed once already. Better solutions?
time.sleep(0.01)
time.sleep(0.05)
self.do(du_a+du_b+du_space, du_a+du_b)
time.sleep(0.05)
self.do(du_a, du_a)
self.assertEqual(queue.get(timeout=0.5), du_a+du_b+du_space)

def test_play_nodelay(self):
Expand Down Expand Up @@ -574,6 +579,9 @@ def process():
def test_add_hotkey_single_step_suppress_allow(self):
keyboard.add_hotkey('a', lambda: trigger() or True, suppress=True)
self.do(d_a, triggered_event+d_a)
def test_add_hotkey_single_step_suppress_regression(self):
keyboard.add_hotkey('ambiguous', lambda: trigger(), suppress=True)
self.assertEqual(len(keyboard._global_event_processor.blocking_hotkeys[frozenset()][frozenset({30})]), 1)
def test_add_hotkey_single_step_suppress_args_allow(self):
arg = object()
keyboard.add_hotkey('a', lambda a: self.assertIs(a, arg) or trigger() or True, args=(arg,), suppress=True)
Expand All @@ -587,47 +595,43 @@ def test_add_hotkey_single_step_suppress_removed(self):
def test_add_hotkey_single_step_suppress_removed(self):
keyboard.remove_hotkey(keyboard.add_hotkey('ctrl+a', trigger, suppress=True))
self.do(d_ctrl+d_a, d_ctrl+d_a)
self.assertEqual(keyboard._listener.filtered_modifiers[dummy_keys['left ctrl'][0][0]], 0)
def test_remove_hotkey_internal(self):
remove = keyboard.add_hotkey('shift+a', trigger, suppress=True)
self.assertTrue(all(keyboard._listener.blocking_hotkeys.values()))
self.assertTrue(all(keyboard._listener.filtered_modifiers.values()))
self.assertTrue(all(keyboard._global_event_processor.blocking_hotkeys.values()))
self.assertNotEqual(keyboard._hotkeys, {})
remove()
self.assertTrue(not any(keyboard._listener.filtered_modifiers.values()))
self.assertTrue(not any(keyboard._listener.blocking_hotkeys.values()))
for hotkeys in keyboard._global_event_processor.blocking_hotkeys.values():
self.assertFalse(any(hotkeys.values()))
self.assertEqual(keyboard._hotkeys, {})
def test_remove_hotkey_internal_multistep_start(self):
remove = keyboard.add_hotkey('shift+a, b', trigger, suppress=True)
self.assertTrue(all(keyboard._listener.blocking_hotkeys.values()))
self.assertTrue(all(keyboard._listener.filtered_modifiers.values()))
self.assertTrue(all(keyboard._global_event_processor.blocking_hotkeys.values()))
self.assertNotEqual(keyboard._hotkeys, {})
remove()
self.assertTrue(not any(keyboard._listener.filtered_modifiers.values()))
self.assertTrue(not any(keyboard._listener.blocking_hotkeys.values()))
for hotkeys in keyboard._global_event_processor.blocking_hotkeys.values():
self.assertFalse(any(hotkeys.values()))
self.assertEqual(keyboard._hotkeys, {})
def test_remove_hotkey_internal_multistep_end(self):
remove = keyboard.add_hotkey('shift+a, b', trigger, suppress=True)
self.do(d_shift+du_a+u_shift)
self.assertTrue(any(keyboard._listener.blocking_hotkeys.values()))
self.assertTrue(not any(keyboard._listener.filtered_modifiers.values()))
self.assertTrue(any(keyboard._global_event_processor.blocking_hotkeys.values()))
self.assertNotEqual(keyboard._hotkeys, {})
remove()
self.assertTrue(not any(keyboard._listener.filtered_modifiers.values()))
self.assertTrue(not any(keyboard._listener.blocking_hotkeys.values()))
for hotkeys in keyboard._global_event_processor.blocking_hotkeys.values():
self.assertFalse(any(hotkeys.values()))
self.assertEqual(keyboard._hotkeys, {})
def test_add_hotkey_single_step_suppress_with_modifiers(self):
keyboard.add_hotkey('ctrl+shift+a', trigger, suppress=True)
self.do(d_ctrl+d_shift+d_a, triggered_event)
def test_add_hotkey_single_step_suppress_with_modifiers_fail_unrelated_modifier(self):
keyboard.add_hotkey('ctrl+shift+a', trigger, suppress=True)
self.do(d_ctrl+d_shift+u_shift+d_a, d_shift+u_shift+d_ctrl+d_a)
self.do(d_ctrl+d_shift+u_shift+d_a, d_ctrl+d_shift+u_shift+d_a)
def test_add_hotkey_single_step_suppress_with_modifiers_fail_unrelated_key(self):
keyboard.add_hotkey('ctrl+shift+a', trigger, suppress=True)
self.do(d_ctrl+d_shift+du_b, d_shift+d_ctrl+du_b)
self.do(d_ctrl+d_shift+du_b, d_ctrl+d_shift+du_b)
def test_add_hotkey_single_step_suppress_with_modifiers_unrelated_key(self):
keyboard.add_hotkey('ctrl+shift+a', trigger, suppress=True)
self.do(d_ctrl+d_shift+du_b+d_a, d_shift+d_ctrl+du_b+triggered_event)
self.do(d_ctrl+d_shift+du_b+d_a, d_ctrl+d_shift+du_b+triggered_event)
def test_add_hotkey_single_step_suppress_with_modifiers_release(self):
keyboard.add_hotkey('ctrl+shift+a', trigger, suppress=True)
self.do(d_ctrl+d_shift+du_b+d_a+u_ctrl+u_shift, d_shift+d_ctrl+du_b+triggered_event+u_ctrl+u_shift)
Expand All @@ -651,7 +655,7 @@ def test_add_hotkey_single_step_timeout(self):
self.do(du_a, triggered_event)
def test_add_hotkey_multi_step_first_timeout(self):
keyboard.add_hotkey('a, b', trigger, timeout=0.01, suppress=True)
time.sleep(0.03)
time.sleep(0.05)
self.do(du_a+du_b, triggered_event)
def test_add_hotkey_multi_step_last_timeout(self):
keyboard.add_hotkey('a, b', trigger, timeout=0.01, suppress=True)
Expand Down Expand Up @@ -692,7 +696,7 @@ def test_add_hotkey_single_step_nosuppress_with_modifiers_out_of_order(self):
self.assertTrue(queue.get(timeout=0.5))
def test_add_hotkey_single_step_suppress_regression_1(self):
keyboard.add_hotkey('a', trigger, suppress=True)
self.do(d_c+d_a+u_c+u_a, d_c+d_a+u_c+u_a)
self.do(d_c+d_a+u_c+u_a, d_c+triggered_event+u_c)

def test_remap_hotkey_single(self):
keyboard.remap_hotkey('a', 'b')
Expand All @@ -708,7 +712,7 @@ def test_remap_hotkey_modifiers_repeat(self):
self.do(d_ctrl+d_shift+du_a+du_a, du_b+du_b)
def test_remap_hotkey_modifiers_state(self):
keyboard.remap_hotkey('ctrl+shift+a', 'b')
self.do(d_ctrl+d_shift+du_c+du_a+du_a, d_shift+d_ctrl+du_c+u_shift+u_ctrl+du_b+d_ctrl+d_shift+u_shift+u_ctrl+du_b+d_ctrl+d_shift)
self.do(d_ctrl+d_shift+du_c+du_a+du_a, d_ctrl+d_shift+du_c+u_shift+u_ctrl+du_b+d_ctrl+d_shift+u_shift+u_ctrl+du_b+d_ctrl+d_shift)
def test_remap_hotkey_release_incomplete(self):
keyboard.remap_hotkey('a', 'b', trigger_on_release=True)
self.do(d_a, [])
Expand All @@ -717,17 +721,17 @@ def test_remap_hotkey_release_complete(self):
self.do(du_a, du_b)

def test_parse_hotkey_combinations_scan_code(self):
self.assertEqual(keyboard.parse_hotkey_combinations(30), (((30,),),))
self.assertEqual(keyboard.parse_hotkey_combinations(30), ((frozenset({30}),),))
def test_parse_hotkey_combinations_single(self):
self.assertEqual(keyboard.parse_hotkey_combinations('a'), (((1,),),))
self.assertEqual(keyboard.parse_hotkey_combinations('a'), ((frozenset({1}),),))
def test_parse_hotkey_combinations_single_modifier(self):
self.assertEqual(keyboard.parse_hotkey_combinations('shift+a'), (((1, 5), (1, 6)),))
self.assertEqual(keyboard.parse_hotkey_combinations('shift+a'), ((frozenset({1, 5}), frozenset({1, 6})),))
def test_parse_hotkey_combinations_single_modifiers(self):
self.assertEqual(keyboard.parse_hotkey_combinations('shift+ctrl+a'), (((1, 5, 7), (1, 6, 7)),))
self.assertEqual(keyboard.parse_hotkey_combinations('shift+ctrl+a'), ((frozenset({1, 5, 7}), frozenset({1, 6, 7})),))
def test_parse_hotkey_combinations_multi(self):
self.assertEqual(keyboard.parse_hotkey_combinations('a, b'), (((1,),), ((2,),)))
self.assertEqual(keyboard.parse_hotkey_combinations('a, b'), ((frozenset({1}),), (frozenset({2}),)))
def test_parse_hotkey_combinations_multi_modifier(self):
self.assertEqual(keyboard.parse_hotkey_combinations('shift+a, b'), (((1, 5), (1, 6)), ((2,),)))
self.assertEqual(keyboard.parse_hotkey_combinations('shift+a, b'), ((frozenset({1, 5}), frozenset({1, 6})), (frozenset({2}),)))
def test_parse_hotkey_combinations_list_list(self):
self.assertEqual(keyboard.parse_hotkey_combinations(keyboard.parse_hotkey_combinations('a, b')), keyboard.parse_hotkey_combinations('a, b'))
def test_parse_hotkey_combinations_fail_empty(self):
Expand All @@ -738,8 +742,8 @@ def test_parse_hotkey_combinations_fail_empty(self):
def test_add_hotkey_multistep_suppress_incomplete(self):
keyboard.add_hotkey('a, b', trigger, suppress=True)
self.do(du_a, [])
self.assertEqual(keyboard._listener.blocking_hotkeys[(1,)], [])
self.assertEqual(len(keyboard._listener.blocking_hotkeys[(2,)]), 1)
self.assertEqual(keyboard._global_event_processor.blocking_hotkeys[(1,)], [])
self.assertEqual(len(keyboard._global_event_processor.blocking_hotkeys[(2,)]), 1)
def test_add_hotkey_multistep_suppress_incomplete(self):
keyboard.add_hotkey('a, b', trigger, suppress=True)
self.do(du_a+du_b, triggered_event)
Expand All @@ -758,14 +762,31 @@ def test_add_hotkey_multistep_suppress_repeated_prefix(self):
def test_add_hotkey_multistep_suppress_repeated_key(self):
keyboard.add_hotkey('a, b', trigger, suppress=True)
self.do(du_a+du_a+du_b, du_a+triggered_event)
self.assertEqual(keyboard._listener.blocking_hotkeys[(2,)], [])
self.assertEqual(len(keyboard._listener.blocking_hotkeys[(1,)]), 1)
@unittest.skip
def test_add_hotkey_multi_step_suppress_regression_1(self):
# In practice the order of d_a+u_c is inverted. Not ideal, but not
# terribly bad either.
keyboard.add_hotkey('a, b', trigger, suppress=True)
self.do(d_c+d_a+u_c+u_a+du_c, d_c+d_a+u_c+u_a+du_c)
def test_add_hotkey_multi_step_suppress_regression_2(self):
keyboard.add_hotkey('a, b', trigger, suppress=True)
keyboard.add_hotkey('a, b', trigger, suppress=True)
self.do(du_a+du_c, du_a+du_c)
self.do(du_a+du_c, du_a+du_c)
def test_add_hotkey_multi_step_suppress_replays(self):
keyboard.add_hotkey('a, b, c', trigger, suppress=True)
self.do(du_a+du_b+du_a+du_b+du_space, du_a+du_b+du_a+du_b+du_space)
def test_add_hotkey_multi_step_suppress_hold(self):
keyboard.add_hotkey('a, b', trigger, suppress=True)
self.do(d_a+du_b+u_a+du_c, d_a+du_b+u_a+du_c)
def test_add_hotkey_multi_step_duplicate(self):
keyboard.add_hotkey('a, b', trigger, suppress=True)
keyboard.add_hotkey('a, b', trigger, suppress=True)
self.do(du_a+du_b, triggered_event+triggered_event)

def test_add_hotkey_single_step_suppress_hold(self):
keyboard.add_hotkey('ctrl+c', trigger, suppress=True)
self.do(d_ctrl+d_a+du_c+u_a+u_ctrl, d_ctrl+d_a+triggered_event+u_a+u_ctrl)

def test_add_word_listener_success(self):
queue = keyboard._queue.Queue()
Expand Down Expand Up @@ -818,6 +839,19 @@ def free():
with self.assertRaises(keyboard._queue.Empty):
queue.get(timeout=0.01)

def test_regression_182_suppress(self):
queue = keyboard._queue.Queue()
keyboard.add_hotkey('a', lambda: time.sleep(0.05) or keyboard.hook(queue.put) and False, trigger_on_release=True, suppress=True)
self.do(du_a, [])
with self.assertRaises(keyboard._queue.Empty):
print(queue.get(timeout=0.01))
def test_regression_182_no_suppress(self):
queue = keyboard._queue.Queue()
keyboard.add_hotkey('a', lambda: time.sleep(0.05) or keyboard.hook(queue.put) and False, trigger_on_release=True, suppress=False)
self.do(du_a, du_a)
with self.assertRaises(keyboard._queue.Empty):
print(queue.get(timeout=0.01))

#def test_add_abbreviation(self):
# keyboard.add_abbreviation('abc', 'aaa')
# self.do(du_a+du_b+du_c+du_space, [])
Expand Down
5 changes: 3 additions & 2 deletions keyboard/_mouse_tests.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# -*- coding: utf-8 -*-
import unittest
import time
import unittest

from ._mouse_event import MoveEvent, ButtonEvent, WheelEvent, LEFT, RIGHT, MIDDLE, X, X2, UP, DOWN, DOUBLE
from keyboard import mouse
from ._mouse_event import MoveEvent, ButtonEvent, WheelEvent, LEFT, RIGHT, MIDDLE, X, X2, UP, DOWN, DOUBLE


class FakeOsMouse(object):
def __init__(self):
Expand Down