Skip to content

Commit c67e500

Browse files
committed
evdevremapkeys: fill in all the missing type information
This was long overdue. The opaque config dictionaries makes it hard to reason about what devices, remappings, and modifier_groups are, and so we really need explicit types to keep things straight. I also added reformat-on-save settings for VS Code
1 parent 1f3aae6 commit c67e500

File tree

2 files changed

+72
-37
lines changed

2 files changed

+72
-37
lines changed

evdevremapkeys/evdevremapkeys.py

Lines changed: 68 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -23,48 +23,80 @@
2323

2424
import argparse
2525
import asyncio
26+
import functools
27+
import signal
2628
from asyncio.events import AbstractEventLoop
2729
from collections.abc import Iterable
28-
import functools
2930
from pathlib import Path
30-
import signal
31-
from typing import Optional, Sequence, cast
32-
31+
from typing import Any, Collection, Optional, Sequence, TypedDict, cast
3332

3433
import evdev
35-
from evdev import KeyEvent, ecodes, InputDevice, UInput
3634
import pyudev
37-
from xdg import BaseDirectory
3835
import yaml
36+
from evdev import InputDevice, InputEvent, KeyEvent, UInput, ecodes
37+
from xdg import BaseDirectory
3938

4039
DEFAULT_RATE = 0.1 # seconds
41-
repeat_tasks = {}
42-
remapped_tasks = {}
43-
registered_devices = {}
40+
repeat_tasks: dict[int, asyncio.Task] = {}
41+
remapped_tasks: dict[int, int] = {}
42+
registered_devices: dict[str, dict[str, Any]] = {}
43+
44+
45+
class Remapping(TypedDict):
46+
code: int
47+
value: list[int]
48+
repeat: bool
49+
count: int
50+
modifier_group: str
51+
52+
53+
type Remappings = dict[int, list[Remapping]]
54+
type ModifierGroups = dict[str, Remappings]
55+
56+
57+
class Device(TypedDict):
58+
input_name: str
59+
input_fn: str
60+
output_name: str
61+
remappings: Remappings
62+
modifier_groups: ModifierGroups
63+
64+
65+
class Config(TypedDict):
66+
devices: list[Device]
67+
68+
69+
class ActiveGroup(TypedDict):
70+
name: str
71+
code: int
4472

4573

4674
async def handle_events(
47-
input: InputDevice, output: UInput, remappings, modifier_groups
75+
input: InputDevice,
76+
output: UInput,
77+
remappings: Remappings,
78+
modifier_groups: ModifierGroups,
4879
):
49-
active_group = {}
80+
active_group: Optional[ActiveGroup] = None
5081
try:
5182
async for event in input.async_read_loop():
83+
event = cast(InputEvent, event)
5284
if not active_group:
5385
active_mappings = remappings
5486
else:
5587
active_mappings = modifier_groups[active_group["name"]]
5688

57-
if event.code == active_group.get("code") or (
89+
if (active_group and event.code == active_group.get("code")) or (
5890
event.code in active_mappings
59-
and "modifier_group" in active_mappings.get(event.code)[0]
91+
and "modifier_group" in active_mappings[event.code][0]
6092
):
6193
if event.value == 1:
62-
active_group["name"] = active_mappings[event.code][0][
63-
"modifier_group"
64-
]
65-
active_group["code"] = event.code
94+
active_group = {
95+
"name": active_mappings[event.code][0]["modifier_group"],
96+
"code": event.code,
97+
}
6698
elif event.value == 0:
67-
active_group = {}
99+
active_group = None
68100
else:
69101
if event.code in active_mappings:
70102
remap_event(output, event, active_mappings[event.code])
@@ -80,7 +112,9 @@ async def handle_events(
80112
input.close()
81113

82114

83-
async def repeat_event(event, rate, count, values, output):
115+
async def repeat_event(
116+
event: InputEvent, rate: float, count: int, values: list[int], output: UInput
117+
):
84118
if count == 0:
85119
count = -1
86120
while count != 0:
@@ -92,7 +126,7 @@ async def repeat_event(event, rate, count, values, output):
92126
await asyncio.sleep(rate)
93127

94128

95-
def remap_event(output, event, event_remapping):
129+
def remap_event(output: UInput, event: InputEvent, event_remapping: list[Remapping]):
96130
for remapping in event_remapping:
97131
original_code = event.code
98132
event.code = remapping["code"]
@@ -176,7 +210,7 @@ def remap_event(output, event, event_remapping):
176210
# 'mod1': { -- is the same as 'remappings' --}
177211
# }
178212
# }]
179-
def load_config(config_override):
213+
def load_config(config_override: str):
180214
conf_path = None
181215
if config_override is None:
182216
for dir in BaseDirectory.load_config_paths("evdevremapkeys"):
@@ -191,11 +225,11 @@ def load_config(config_override):
191225
raise NameError("Cannot open %s" % config_override)
192226

193227
with open(conf_path.as_posix(), "r") as fd:
194-
config = yaml.safe_load(fd)
228+
config: dict[str, Any] = yaml.safe_load(fd)
195229
return parse_config(config)
196230

197231

198-
def parse_config(config):
232+
def parse_config(config: dict[str, Any]) -> Config:
199233
for device in config["devices"]:
200234
device["remappings"] = normalize_config(device["remappings"])
201235
device["remappings"] = resolve_ecodes(device["remappings"])
@@ -208,7 +242,7 @@ def parse_config(config):
208242
device["modifier_groups"][group]
209243
)
210244

211-
return config
245+
return cast(Config, config)
212246

213247

214248
# Converts general config schema
@@ -229,7 +263,7 @@ def parse_config(config):
229263
# {'code': 'KEY_Y', 'value': [1,0]]}
230264
# ]
231265
# }}
232-
def normalize_config(remappings):
266+
def normalize_config(remappings: dict[str, Any]):
233267
norm = {}
234268
for key, mappings in remappings.items():
235269
new_mappings = []
@@ -243,14 +277,14 @@ def normalize_config(remappings):
243277
return norm
244278

245279

246-
def normalize_value(mapping):
280+
def normalize_value(mapping: dict[str, Any]):
247281
value = mapping.get("value")
248282
if value is None or type(value) is list:
249283
return
250284
mapping["value"] = [mapping["value"]]
251285

252286

253-
def resolve_ecodes(by_name):
287+
def resolve_ecodes(by_name: dict[str, Any]):
254288
def resolve_mapping(mapping):
255289
if "code" in mapping:
256290
code = mapping["code"]
@@ -270,7 +304,7 @@ def resolve_mapping(mapping):
270304
}
271305

272306

273-
def find_input(device):
307+
def find_input(device: Device):
274308
name = device.get("input_name", None)
275309
phys = device.get("input_phys", None)
276310
fn = device.get("input_fn", None)
@@ -295,7 +329,7 @@ def find_input(device):
295329
return None
296330

297331

298-
def register_device(device, loop: AbstractEventLoop):
332+
def register_device(device: Device, loop: AbstractEventLoop):
299333
for value in registered_devices.values():
300334
if device == value["device"]:
301335
return value["task"]
@@ -312,11 +346,11 @@ def register_device(device, loop: AbstractEventLoop):
312346
remappings = device["remappings"]
313347
extended = set(caps[ecodes.EV_KEY])
314348

315-
modifier_groups = []
349+
modifier_groups: ModifierGroups = {}
316350
if "modifier_groups" in device:
317351
modifier_groups = device["modifier_groups"]
318352

319-
def flatmap(lst):
353+
def flatmap(lst: Collection[Collection[Any]]):
320354
return [l2 for l1 in lst for l2 in l1]
321355

322356
for remapping in flatmap(remappings.values()):
@@ -353,7 +387,7 @@ async def shutdown(loop: AbstractEventLoop):
353387
loop.stop()
354388

355389

356-
def handle_udev_event(monitor, config, loop):
390+
def handle_udev_event(monitor: pyudev.Monitor, config: Config, loop: AbstractEventLoop):
357391
count = 0
358392
while True:
359393
device = monitor.poll(0)
@@ -370,7 +404,7 @@ def create_shutdown_task(loop: AbstractEventLoop):
370404
return loop.create_task(shutdown(loop))
371405

372406

373-
def run_loop(args):
407+
def run_loop(args: argparse.Namespace):
374408
context = pyudev.Context()
375409
monitor = pyudev.Monitor.from_netlink(context)
376410
monitor.filter_by("input")
@@ -409,7 +443,7 @@ def list_devices():
409443
yield [device.path, device.phys, device.name]
410444

411445

412-
def read_events(req_device):
446+
def read_events(req_device: str):
413447
found: Optional[InputDevice] = None
414448
for device in list_devices():
415449
# Look in all 3 identifiers + event number

tests/load_config_test.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,11 @@
2424
import os
2525
import sys
2626

27-
from evdev import ecodes
28-
from evdevremapkeys.evdevremapkeys import parse_config
2927
import pytest
3028
import yaml
29+
from evdev import ecodes
30+
31+
from evdevremapkeys.evdevremapkeys import parse_config
3132

3233
spec_dir = os.path.dirname(os.path.realpath(__file__))
3334
sys.path.append("{}/..".format(spec_dir))
@@ -134,7 +135,7 @@ def test_mod_group_resolves_single_value(sample_config):
134135

135136
def test_mod_group_accepts_multiple_values(sample_config):
136137
mapping = modified_remapping(sample_config, ecodes.KEY_D)
137-
[{"code": 33, "value": [1, 3]}] == mapping
138+
assert [{"code": 33, "value": [1, 3]}] == mapping
138139

139140

140141
def test_mod_group_accepts_other_parameters(sample_config):

0 commit comments

Comments
 (0)