Skip to content

Commit 065f797

Browse files
mikahanninenclaude
andauthored
fix(deps): replace pynput-robocorp-fork with upstream pynput (#1328)
* fix(deps): replace pynput-robocorp-fork with upstream pynput The fork was introduced in 2020 to avoid the evdev C extension on Linux. Upstream pynput 1.8.1 no longer requires evdev, making the fork redundant. The fork also pins pyobjc<10.0 which blocks Python 3.13 installs on macOS (pyobjc 9.2 has no cp313 wheel and fails to build from source). All APIs are identical between pynput_robocorp and pynput. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix keyboard handling * Add tests for keyboard and desktop --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 74d554d commit 065f797

8 files changed

Lines changed: 1226 additions & 760 deletions

File tree

packages/main/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ dependencies = [
4040
"pywin32>=302,<=311; sys_platform == 'win32'",
4141
"comtypes>=1.1.11; sys_platform == 'win32'",
4242
"robotframework-pythonlibcore>=4.2.0",
43-
"pynput-robocorp-fork>=5.0.0",
43+
"pynput>=1.8.1",
4444
"python-xlib>=0.17; sys_platform == 'linux'",
4545
"psutil>=5.7.0; sys_platform == 'win32'",
4646
"pyperclip>=1.8.0",

packages/main/src/RPA/Desktop/keywords/keyboard.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
from RPA.Desktop.keywords import LibraryContext, keyword
44

55

6-
def to_key(key: str, escaped=False) -> Any:
6+
def to_key(key: Any) -> Any:
77
"""Convert key string to correct enum value."""
88
# pylint: disable=C0415
9-
from pynput_robocorp.keyboard import Key, KeyCode
9+
from pynput.keyboard import Key, KeyCode
1010

1111
if isinstance(key, (Key, KeyCode)):
1212
return key
@@ -22,7 +22,7 @@ def to_key(key: str, escaped=False) -> Any:
2222
# Check for individual character
2323
if len(value) == 1:
2424
try:
25-
return KeyCode.from_char(value, escaped=escaped)
25+
return KeyCode.from_char(value)
2626
except ValueError:
2727
pass
2828

@@ -36,7 +36,7 @@ def __init__(self, ctx):
3636
super().__init__(ctx)
3737
try:
3838
# pylint: disable=C0415
39-
from pynput_robocorp.keyboard import Controller
39+
from pynput.keyboard import Controller
4040

4141
self._keyboard = Controller()
4242
self._error = None
@@ -89,7 +89,7 @@ def press_keys(self, *keys: str) -> None:
8989
if self._error:
9090
raise self._error
9191

92-
keys = [to_key(key, escaped=True) for key in keys]
92+
keys = [to_key(key) for key in keys]
9393
self.logger.info("Pressing keys: %s", ", ".join(str(key) for key in keys))
9494

9595
with self.buffer():

packages/main/src/RPA/Desktop/keywords/mouse.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def to_action(value):
3232
def to_button(value):
3333
"""Convert value to Button enum."""
3434
# pylint: disable=C0415
35-
from pynput_robocorp.mouse import Button
35+
from pynput.mouse import Button
3636

3737
if isinstance(value, Button):
3838
return value
@@ -61,7 +61,7 @@ def __init__(self, ctx):
6161
super().__init__(ctx)
6262
try:
6363
# pylint: disable=C0415
64-
from pynput_robocorp.mouse import Controller
64+
from pynput.mouse import Controller
6565

6666
self._mouse = Controller()
6767
self._error = None
@@ -84,7 +84,7 @@ def _click(
8484
) -> None:
8585
"""Perform defined mouse action, and optionally move to given point first."""
8686
# pylint: disable=C0415
87-
from pynput_robocorp.mouse import Button
87+
from pynput.mouse import Button
8888

8989
action = to_action(action)
9090

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
"""Tests for RPA.Desktop keyboard and mouse keyword utilities."""
2+
from unittest.mock import MagicMock, patch
3+
4+
import pytest
5+
6+
7+
# --- to_key ---
8+
9+
10+
def test_to_key_special_keys():
11+
pytest.importorskip("pynput")
12+
from pynput.keyboard import Key
13+
14+
from RPA.Desktop.keywords.keyboard import to_key
15+
16+
assert to_key("ctrl") == Key.ctrl
17+
assert to_key("shift") == Key.shift
18+
assert to_key("enter") == Key.enter
19+
assert to_key("alt") == Key.alt
20+
assert to_key("tab") == Key.tab
21+
assert to_key("delete") == Key.delete
22+
assert to_key("f4") == Key.f4
23+
assert to_key("space") == Key.space
24+
25+
26+
def test_to_key_single_character():
27+
pytest.importorskip("pynput")
28+
from pynput.keyboard import KeyCode
29+
30+
from RPA.Desktop.keywords.keyboard import to_key
31+
32+
assert isinstance(to_key("a"), KeyCode)
33+
assert isinstance(to_key("z"), KeyCode)
34+
assert isinstance(to_key("1"), KeyCode)
35+
36+
37+
def test_to_key_case_insensitive():
38+
pytest.importorskip("pynput")
39+
from pynput.keyboard import Key
40+
41+
from RPA.Desktop.keywords.keyboard import to_key
42+
43+
assert to_key("CTRL") == Key.ctrl
44+
assert to_key("Enter") == Key.enter
45+
assert to_key("SHIFT") == Key.shift
46+
47+
48+
def test_to_key_passthrough():
49+
pytest.importorskip("pynput")
50+
from pynput.keyboard import Key, KeyCode
51+
52+
from RPA.Desktop.keywords.keyboard import to_key
53+
54+
assert to_key(Key.ctrl) is Key.ctrl
55+
kc = KeyCode.from_char("a")
56+
assert to_key(kc) is kc
57+
58+
59+
def test_to_key_invalid_raises():
60+
pytest.importorskip("pynput")
61+
from RPA.Desktop.keywords.keyboard import to_key
62+
63+
with pytest.raises(ValueError, match="Invalid key"):
64+
to_key("notakey")
65+
66+
with pytest.raises(ValueError, match="Invalid key"):
67+
to_key("ab")
68+
69+
70+
# --- to_action ---
71+
72+
73+
def test_to_action_valid():
74+
from RPA.Desktop.keywords.mouse import Action, to_action
75+
76+
assert to_action("click") == Action.click
77+
assert to_action("double_click") == Action.double_click
78+
assert to_action("right_click") == Action.right_click
79+
assert to_action("triple_click") == Action.triple_click
80+
assert to_action("double click") == Action.double_click
81+
82+
83+
def test_to_action_passthrough():
84+
from RPA.Desktop.keywords.mouse import Action, to_action
85+
86+
assert to_action(Action.click) is Action.click
87+
88+
89+
def test_to_action_invalid_raises():
90+
from RPA.Desktop.keywords.mouse import to_action
91+
92+
with pytest.raises(ValueError, match="Unknown mouse action"):
93+
to_action("spin_click")
94+
95+
96+
# --- to_button ---
97+
98+
99+
def test_to_button_valid():
100+
pytest.importorskip("pynput")
101+
from pynput.mouse import Button
102+
103+
from RPA.Desktop.keywords.mouse import to_button
104+
105+
assert to_button("left") == Button.left
106+
assert to_button("right") == Button.right
107+
assert to_button("middle") == Button.middle
108+
109+
110+
def test_to_button_passthrough():
111+
pytest.importorskip("pynput")
112+
from pynput.mouse import Button
113+
114+
from RPA.Desktop.keywords.mouse import to_button
115+
116+
assert to_button(Button.left) is Button.left
117+
118+
119+
def test_to_button_invalid_raises():
120+
pytest.importorskip("pynput")
121+
from RPA.Desktop.keywords.mouse import to_button
122+
123+
with pytest.raises(ValueError, match="Unknown mouse button"):
124+
to_button("side")
125+
126+
127+
# --- KeyboardKeywords ---
128+
129+
130+
def _make_keyboard_keywords():
131+
"""Create KeyboardKeywords with a mocked pynput Controller and ctx."""
132+
mock_controller = MagicMock()
133+
ctx = MagicMock()
134+
135+
with patch("pynput.keyboard.Controller", return_value=mock_controller):
136+
from RPA.Desktop.keywords.keyboard import KeyboardKeywords
137+
138+
kb = KeyboardKeywords(ctx)
139+
140+
return kb, mock_controller
141+
142+
143+
def test_press_keys_special():
144+
pytest.importorskip("pynput")
145+
from pynput.keyboard import Key
146+
147+
kb, ctrl = _make_keyboard_keywords()
148+
kb.press_keys("enter")
149+
150+
ctrl.press.assert_called_once_with(Key.enter)
151+
ctrl.release.assert_called_once_with(Key.enter)
152+
153+
154+
def test_press_keys_character():
155+
pytest.importorskip("pynput")
156+
from pynput.keyboard import KeyCode
157+
158+
kb, ctrl = _make_keyboard_keywords()
159+
kb.press_keys("a")
160+
161+
pressed = ctrl.press.call_args[0][0]
162+
assert isinstance(pressed, KeyCode)
163+
164+
165+
def test_press_keys_combination_order():
166+
"""ctrl+a: ctrl pressed first, released last."""
167+
pytest.importorskip("pynput")
168+
from pynput.keyboard import Key, KeyCode
169+
170+
kb, ctrl = _make_keyboard_keywords()
171+
kb.press_keys("ctrl", "a")
172+
173+
press_calls = [c[0][0] for c in ctrl.press.call_args_list]
174+
release_calls = [c[0][0] for c in ctrl.release.call_args_list]
175+
176+
assert press_calls[0] == Key.ctrl
177+
assert isinstance(press_calls[1], KeyCode)
178+
assert isinstance(release_calls[0], KeyCode)
179+
assert release_calls[1] == Key.ctrl
180+
181+
182+
def test_type_text():
183+
pytest.importorskip("pynput")
184+
kb, ctrl = _make_keyboard_keywords()
185+
kb.type_text("hello")
186+
187+
ctrl.type.assert_called_once_with("hello")

0 commit comments

Comments
 (0)