From 1a51829e7a89525db35c24bae6ef064346462e1c Mon Sep 17 00:00:00 2001 From: Andrew Ammerlaan Date: Wed, 8 Jun 2022 21:44:20 +0200 Subject: [PATCH 01/87] evdev/eventio_async.py: python3.11 compatibility Signed-off-by: Andrew Ammerlaan --- evdev/eventio_async.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/evdev/eventio_async.py b/evdev/eventio_async.py index 77542a4..68225c3 100644 --- a/evdev/eventio_async.py +++ b/evdev/eventio_async.py @@ -85,8 +85,7 @@ def __next__(self): def __aiter__(self): return self - @asyncio.coroutine - def __anext__(self): + async def __anext__(self): future = asyncio.Future() try: # Read from the previous batch of events. From d4e96ac1b30f3fce82b2fac7eb7bf6a58362e57e Mon Sep 17 00:00:00 2001 From: Dick Marinus Date: Fri, 15 Jul 2022 20:41:35 +0200 Subject: [PATCH 02/87] add keycode for all events --- evdev/events.py | 46 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/evdev/events.py b/evdev/events.py index 14bb0ce..c63244d 100644 --- a/evdev/events.py +++ b/evdev/events.py @@ -130,15 +130,23 @@ def __repr__(s): class RelEvent(object): '''A relative axis event (e.g moving the mouse 5 units to the left).''' - __slots__ = 'event' + __slots__ = 'event', 'keycode' + + def __init__(self, event, allow_unknown=False): + try: + self.keycode = REL[event.code] + except KeyError: + if allow_unknown: + self.keycode = '0x{:02X}'.format(event.code) + else: + raise - def __init__(self, event): #: Reference to an :class:`InputEvent` instance. self.event = event def __str__(self): - msg = 'relative axis event at {:f}, {} ' - return msg.format(self.event.timestamp(), REL[self.event.code]) + msg = 'relative axis event at {:f}, {} {} ' + return msg.format(self.event.timestamp(), self.keycode, self.event.value) def __repr__(s): return '{}({!r})'.format(s.__class__.__name__, s.event) @@ -147,15 +155,23 @@ def __repr__(s): class AbsEvent(object): '''An absolute axis event (e.g the coordinates of a tap on a touchscreen).''' - __slots__ = 'event' + __slots__ = 'event', 'keycode' + + def __init__(self, event, allow_unknown=False): + try: + self.keycode = ABS[event.code] + except KeyError: + if allow_unknown: + self.keycode = '0x{:02X}'.format(event.code) + else: + raise - def __init__(self, event): #: Reference to an :class:`InputEvent` instance. self.event = event def __str__(self): - msg = 'absolute axis event at {:f}, {} ' - return msg.format(self.event.timestamp(), ABS[self.event.code]) + msg = 'absolute axis event at {:f}, {} {} ' + return msg.format(self.event.timestamp(), self.keycode, self.event.value) def __repr__(s): return '{}({!r})'.format(s.__class__.__name__, s.event) @@ -169,15 +185,23 @@ class SynEvent(object): the multitouch protocol. ''' - __slots__ = 'event' + __slots__ = 'event', 'keycode' + + def __init__(self, event, allow_known): + try: + self.keycode = SYN[event.code] + except KeyError: + if allow_unknown: + self.keycode = '0x{:02X}'.format(event.code) + else: + raise - def __init__(self, event): #: Reference to an :class:`InputEvent` instance. self.event = event def __str__(self): msg = 'synchronization event at {:f}, {} ' - return msg.format(self.event.timestamp(), SYN[self.event.code]) + return msg.format(self.event.timestamp(), self.keycode) def __repr__(s): return '{}({!r})'.format(s.__class__.__name__, s.event) From f24f7f48843876c149ebda3f18842ec5db21d5d2 Mon Sep 17 00:00:00 2001 From: Dick Marinus Date: Sun, 17 Jul 2022 21:27:35 +0200 Subject: [PATCH 03/87] fix typo, allow_known -> allow_unknown --- evdev/events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evdev/events.py b/evdev/events.py index c63244d..fa122f7 100644 --- a/evdev/events.py +++ b/evdev/events.py @@ -187,7 +187,7 @@ class SynEvent(object): __slots__ = 'event', 'keycode' - def __init__(self, event, allow_known): + def __init__(self, event, allow_unknown): try: self.keycode = SYN[event.code] except KeyError: From 227a01c78b4fd0ddc074110549fd381864301d41 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 17 Jul 2022 22:15:09 +0200 Subject: [PATCH 04/87] More asyncio compatibility fixes There is no need to mark __anext__ as async, since it already returns an awaitable object, in the form of asyncio.Future. --- evdev/eventio_async.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evdev/eventio_async.py b/evdev/eventio_async.py index 68225c3..5969423 100644 --- a/evdev/eventio_async.py +++ b/evdev/eventio_async.py @@ -85,7 +85,7 @@ def __next__(self): def __aiter__(self): return self - async def __anext__(self): + def __anext__(self): future = asyncio.Future() try: # Read from the previous batch of events. From b5f69fba79d9b828e4b769c1188a81edde6433bb Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 17 Jul 2022 22:26:27 +0200 Subject: [PATCH 05/87] Remove Python 2.x compatibility leftovers --- docs/tutorial.rst | 2 +- evdev/device.py | 5 ----- evdev/eventio.py | 2 +- evdev/eventio_async.py | 6 +----- evdev/events.py | 10 +++++----- evdev/uinput.py | 5 +---- 6 files changed, 9 insertions(+), 21 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index d35299c..200c637 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -290,7 +290,7 @@ Associating classes with event types >>> from evdev import categorize, event_factory, ecodes - >>> class SynEvent(object): + >>> class SynEvent: ... def __init__(self, event): ... ... diff --git a/evdev/device.py b/evdev/device.py index 23254a3..dbfad5d 100644 --- a/evdev/device.py +++ b/evdev/device.py @@ -282,11 +282,6 @@ def __eq__(self, other): return isinstance(other, self.__class__) and self.info == other.info \ and self.path == other.path - def __ne__(self, other): - # Python 2 compatibility. Python 3 automatically negates the value of - # __eq__, in case __ne__ is not defined. - return not self == other - def __str__(self): msg = 'device {}, name "{}", phys "{}"' return msg.format(self.path, self.name, self.phys) diff --git a/evdev/eventio.py b/evdev/eventio.py index 22c37e2..8d84f55 100644 --- a/evdev/eventio.py +++ b/evdev/eventio.py @@ -13,7 +13,7 @@ class EvdevError(Exception): pass -class EventIO(object): +class EventIO: ''' Base class for reading and writing input events. diff --git a/evdev/eventio_async.py b/evdev/eventio_async.py index 5969423..2d3468e 100644 --- a/evdev/eventio_async.py +++ b/evdev/eventio_async.py @@ -60,7 +60,7 @@ def close(self): pass -class ReadIterator(object): +class ReadIterator: def __init__(self, device): self.current_batch = iter(()) self.device = device @@ -69,10 +69,6 @@ def __init__(self, device): def __iter__(self): return self - # Python 2.x compatibility. - def next(self): - return self.__next__() - def __next__(self): try: # Read from the previous batch of events. diff --git a/evdev/events.py b/evdev/events.py index 14bb0ce..9d8758d 100644 --- a/evdev/events.py +++ b/evdev/events.py @@ -42,7 +42,7 @@ from evdev.ecodes import keys, KEY, SYN, REL, ABS, EV_KEY, EV_REL, EV_ABS, EV_SYN -class InputEvent(object): +class InputEvent: '''A generic input event.''' __slots__ = 'sec', 'usec', 'type', 'code', 'value' @@ -77,7 +77,7 @@ def __repr__(s): s.sec, s.usec, s.type, s.code, s.value) -class KeyEvent(object): +class KeyEvent: '''An event generated by a keyboard, button or other key-like devices.''' key_up = 0x0 @@ -127,7 +127,7 @@ def __repr__(s): return '{}({!r})'.format(s.__class__.__name__, s.event) -class RelEvent(object): +class RelEvent: '''A relative axis event (e.g moving the mouse 5 units to the left).''' __slots__ = 'event' @@ -144,7 +144,7 @@ def __repr__(s): return '{}({!r})'.format(s.__class__.__name__, s.event) -class AbsEvent(object): +class AbsEvent: '''An absolute axis event (e.g the coordinates of a tap on a touchscreen).''' __slots__ = 'event' @@ -161,7 +161,7 @@ def __repr__(s): return '{}({!r})'.format(s.__class__.__name__, s.event) -class SynEvent(object): +class SynEvent: ''' A synchronization event. Synchronization events are used as markers to separate event. Used as markers to separate diff --git a/evdev/uinput.py b/evdev/uinput.py index 0dc5f89..4f6148a 100644 --- a/evdev/uinput.py +++ b/evdev/uinput.py @@ -34,7 +34,7 @@ class UInput(EventIO): ) @classmethod - def from_device(cls, *devices, **kwargs): + def from_device(cls, *devices, filtered_types=(ecodes.EV_SYN, ecodes.EV_FF), **kwargs): ''' Create an UInput device with the capabilities of one or more input devices. @@ -51,9 +51,6 @@ def from_device(cls, *devices, **kwargs): Keyword arguments to UInput constructor (i.e. name, vendor etc.). ''' - # TODO: Move back to the argument list once Python 2 support is dropped. - filtered_types = kwargs.pop('filtered_types', (ecodes.EV_SYN, ecodes.EV_FF)) - device_instances = [] for dev in devices: if not isinstance(dev, device.InputDevice): From 03aa7f5877ad545a7a4281d52f4385a41e47cf7e Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 17 Jul 2022 22:27:33 +0200 Subject: [PATCH 06/87] Simplify asyncio example --- docs/tutorial.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 200c637..7416181 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -123,16 +123,15 @@ Reading events (using :mod:`asyncio`) :: >>> import asyncio - >>> from evdev import InputDevice, categorize, ecodes + >>> from evdev import InputDevice >>> dev = InputDevice('/dev/input/event1') - >>> async def helper(dev): + >>> async def main(dev): ... async for ev in dev.async_read_loop(): ... print(repr(ev)) - >>> loop = asyncio.get_event_loop() - >>> loop.run_until_complete(helper(dev)) + >>> asyncio.run(main(dev)) InputEvent(1527363738, 348740, 4, 4, 458792) InputEvent(1527363738, 348740, 1, 28, 0) InputEvent(1527363738, 348740, 0, 0, 0) From beeecf4000d2554f90470e5e1357b03eb48d1bd4 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 17 Jul 2022 22:50:09 +0200 Subject: [PATCH 07/87] Fix English --- evdev/events.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/evdev/events.py b/evdev/events.py index 1c50d47..4e359bc 100644 --- a/evdev/events.py +++ b/evdev/events.py @@ -88,7 +88,7 @@ class KeyEvent: def __init__(self, event, allow_unknown=False): ''' - The ``allow_unknown`` argument determines what to do in the event of a event code + The ``allow_unknown`` argument determines what to do in the event of an event code for which a key code cannot be found. If ``False`` a ``KeyError`` will be raised. If ``True`` the keycode will be set to the hex value of the event code. ''' @@ -179,10 +179,8 @@ def __repr__(s): class SynEvent: ''' - A synchronization event. Synchronization events are used as - markers to separate event. Used as markers to separate - events. Events may be separated in time or in space, such as with - the multitouch protocol. + A synchronization event. Used as markers to separate events. Events may be + separated in time or in space, such as with the multitouch protocol. ''' __slots__ = 'event', 'keycode' From 28b1739182fdf0d09ff952af3abbf73485ecb15c Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 17 Jul 2022 22:50:36 +0200 Subject: [PATCH 08/87] allow_unknown needs default value or categorize() breaks --- evdev/events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evdev/events.py b/evdev/events.py index 4e359bc..18b7f20 100644 --- a/evdev/events.py +++ b/evdev/events.py @@ -185,7 +185,7 @@ class SynEvent: __slots__ = 'event', 'keycode' - def __init__(self, event, allow_unknown): + def __init__(self, event, allow_unknown=True): try: self.keycode = SYN[event.code] except KeyError: From 0c39ecfe7210ef2a391d41663342f23e4f9f8971 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 17 Jul 2022 23:06:08 +0200 Subject: [PATCH 09/87] Revert "add keycode for all events" This reverts commit d4e96ac1b30f3fce82b2fac7eb7bf6a58362e57e. --- evdev/events.py | 46 +++++++++++----------------------------------- 1 file changed, 11 insertions(+), 35 deletions(-) diff --git a/evdev/events.py b/evdev/events.py index 18b7f20..37cf17e 100644 --- a/evdev/events.py +++ b/evdev/events.py @@ -130,23 +130,15 @@ def __repr__(s): class RelEvent: '''A relative axis event (e.g moving the mouse 5 units to the left).''' - __slots__ = 'event', 'keycode' - - def __init__(self, event, allow_unknown=False): - try: - self.keycode = REL[event.code] - except KeyError: - if allow_unknown: - self.keycode = '0x{:02X}'.format(event.code) - else: - raise + __slots__ = 'event' + def __init__(self, event): #: Reference to an :class:`InputEvent` instance. self.event = event def __str__(self): - msg = 'relative axis event at {:f}, {} {} ' - return msg.format(self.event.timestamp(), self.keycode, self.event.value) + msg = 'relative axis event at {:f}, {} ' + return msg.format(self.event.timestamp(), REL[self.event.code]) def __repr__(s): return '{}({!r})'.format(s.__class__.__name__, s.event) @@ -155,23 +147,15 @@ def __repr__(s): class AbsEvent: '''An absolute axis event (e.g the coordinates of a tap on a touchscreen).''' - __slots__ = 'event', 'keycode' - - def __init__(self, event, allow_unknown=False): - try: - self.keycode = ABS[event.code] - except KeyError: - if allow_unknown: - self.keycode = '0x{:02X}'.format(event.code) - else: - raise + __slots__ = 'event' + def __init__(self, event): #: Reference to an :class:`InputEvent` instance. self.event = event def __str__(self): - msg = 'absolute axis event at {:f}, {} {} ' - return msg.format(self.event.timestamp(), self.keycode, self.event.value) + msg = 'absolute axis event at {:f}, {} ' + return msg.format(self.event.timestamp(), ABS[self.event.code]) def __repr__(s): return '{}({!r})'.format(s.__class__.__name__, s.event) @@ -183,23 +167,15 @@ class SynEvent: separated in time or in space, such as with the multitouch protocol. ''' - __slots__ = 'event', 'keycode' - - def __init__(self, event, allow_unknown=True): - try: - self.keycode = SYN[event.code] - except KeyError: - if allow_unknown: - self.keycode = '0x{:02X}'.format(event.code) - else: - raise + __slots__ = 'event' + def __init__(self, event): #: Reference to an :class:`InputEvent` instance. self.event = event def __str__(self): msg = 'synchronization event at {:f}, {} ' - return msg.format(self.event.timestamp(), self.keycode) + return msg.format(self.event.timestamp(), SYN[self.event.code]) def __repr__(s): return '{}({!r})'.format(s.__class__.__name__, s.event) From 4050f506b016addd759085f2ae6f07b0065a2947 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 17 Jul 2022 23:16:02 +0200 Subject: [PATCH 10/87] Remove trailing whitespace --- evdev/events.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/evdev/events.py b/evdev/events.py index 37cf17e..af42386 100644 --- a/evdev/events.py +++ b/evdev/events.py @@ -137,7 +137,7 @@ def __init__(self, event): self.event = event def __str__(self): - msg = 'relative axis event at {:f}, {} ' + msg = 'relative axis event at {:f}, {}' return msg.format(self.event.timestamp(), REL[self.event.code]) def __repr__(s): @@ -154,7 +154,7 @@ def __init__(self, event): self.event = event def __str__(self): - msg = 'absolute axis event at {:f}, {} ' + msg = 'absolute axis event at {:f}, {}' return msg.format(self.event.timestamp(), ABS[self.event.code]) def __repr__(s): @@ -174,7 +174,7 @@ def __init__(self, event): self.event = event def __str__(self): - msg = 'synchronization event at {:f}, {} ' + msg = 'synchronization event at {:f}, {}' return msg.format(self.event.timestamp(), SYN[self.event.code]) def __repr__(s): From 3cd7fbf222dbf3b4ca277d5138eabdb1819faa66 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 17 Jul 2022 23:22:58 +0200 Subject: [PATCH 11/87] Bump version: 1.5.0 -> 1.6.0 --- docs/changelog.rst | 6 ++++++ docs/conf.py | 2 +- setup.cfg | 2 +- setup.py | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index ed85d06..e7b3af4 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,12 @@ Changelog --------- +1.6.0 (Jul 17, 2022) +================== + +- Fix Python 3.11 compatibility (`#174 `_) + + 1.5.0 (Mar 24, 2022) ================== diff --git a/docs/conf.py b/docs/conf.py index 2d16b6c..8dc3ae9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -61,7 +61,7 @@ # built documents. # # The full version, including alpha/beta/rc tags. -release = '1.5.0' +release = '1.6.0' # The short X.Y version. version = release diff --git a/setup.cfg b/setup.cfg index 1f1ff70..9e2dc85 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.5.0 +current_version = 1.6.0 message = Bump version: {current_version} -> {new_version} commit = True tag = True diff --git a/setup.py b/setup.py index e8c1174..847485e 100755 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ #----------------------------------------------------------------------------- kw = { 'name': 'evdev', - 'version': '1.5.0', + 'version': '1.6.0', 'description': 'Bindings to the Linux input handling subsystem', 'long_description': open(pjoin(here, 'README.rst')).read(), From 66aa0905b8ebe257e4518767b043d9f942449eba Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Fri, 20 Jan 2023 15:26:11 +0100 Subject: [PATCH 12/87] Fix escaping of genecodes command-line --- setup.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/setup.py b/setup.py index 847485e..bdda634 100755 --- a/setup.py +++ b/setup.py @@ -1,11 +1,10 @@ #!/usr/bin/env python -# encoding: utf-8 import os import sys import textwrap +from pathlib import Path -from os.path import abspath, dirname, join as pjoin #----------------------------------------------------------------------------- try: @@ -17,7 +16,8 @@ #----------------------------------------------------------------------------- -here = abspath(dirname(__file__)) +curdir = Path(__file__).resolve().parent +ecodes_path = curdir / 'evdev/ecodes.c' #----------------------------------------------------------------------------- classifiers = [ @@ -47,7 +47,7 @@ 'version': '1.6.0', 'description': 'Bindings to the Linux input handling subsystem', - 'long_description': open(pjoin(here, 'README.rst')).read(), + 'long_description': (curdir / 'README.rst').read_text(), 'author': 'Georgi Valkov', 'author_email': 'georgi.t.valkov@gmail.com', @@ -99,11 +99,12 @@ def create_ecodes(headers=None): sys.stderr.write(textwrap.dedent(msg)) sys.exit(1) - from subprocess import check_call + from subprocess import run - print('writing ecodes.c (using %s)' % ' '.join(headers)) - cmd = '%s genecodes.py %s > ecodes.c' % (sys.executable, ' '.join(headers)) - check_call(cmd, cwd="%s/evdev" % here, shell=True) + print('writing %s (using %s)' % (ecodes_path, ' '.join(headers))) + with ecodes_path.open('w') as fh: + cmd = [sys.executable, 'evdev/genecodes.py', *headers] + run(cmd, check=True, stdout=fh) #----------------------------------------------------------------------------- @@ -127,11 +128,9 @@ def run(self): class build_ext(_build_ext.build_ext): def has_ecodes(self): - ecodes_path = os.path.join(here, 'evdev/ecodes.c') - res = os.path.exists(ecodes_path) - if res: + if ecodes_path.exists(): print('ecodes.c already exists ... skipping build_ecodes') - return not res + return not ecodes_path.exists() def run(self): for cmd_name in self.get_sub_commands(): From 4c1e2b7f78603adf429ffb0a7130c9bf5aab3b2a Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Fri, 20 Jan 2023 15:27:34 +0100 Subject: [PATCH 13/87] Drop distutils compatibility imports --- setup.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index bdda634..9517fe8 100755 --- a/setup.py +++ b/setup.py @@ -7,12 +7,8 @@ #----------------------------------------------------------------------------- -try: - from setuptools import setup, Extension, Command - from setuptools.command import build_ext as _build_ext -except ImportError: - from distutils.core import setup, Extension, Command - from distutils.command import build_ext as _build_ext +from setuptools import setup, Extension, Command +from setuptools.command import build_ext as _build_ext #----------------------------------------------------------------------------- From a51eb87db756580e25b45b5cdcdf3c511d9222e1 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Fri, 20 Jan 2023 15:38:10 +0100 Subject: [PATCH 14/87] Drop Python 2 remnants from genecodes.py --- LICENSE | 2 +- evdev/genecodes.py | 26 ++------------------------ setup.py | 1 + 3 files changed, 4 insertions(+), 25 deletions(-) diff --git a/LICENSE b/LICENSE index ce3a1f7..5600871 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2012-2016 Georgi Valkov. All rights reserved. +Copyright (c) 2012-2023 Georgi Valkov. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are diff --git a/evdev/genecodes.py b/evdev/genecodes.py index 04c10e1..15e4ded 100644 --- a/evdev/genecodes.py +++ b/evdev/genecodes.py @@ -47,7 +47,6 @@ { NULL, NULL, 0, NULL} }; -#if PY_MAJOR_VERSION >= 3 static struct PyModuleDef moduledef = { PyModuleDef_HEAD_INIT, MODULE_NAME, @@ -59,38 +58,17 @@ NULL, /* m_clear */ NULL, /* m_free */ }; -#endif -static PyObject * -moduleinit(void) +PyMODINIT_FUNC +PyInit__ecodes(void) { - -#if PY_MAJOR_VERSION >= 3 PyObject* m = PyModule_Create(&moduledef); -#else - PyObject* m = Py_InitModule3(MODULE_NAME, MethodTable, MODULE_HELP); -#endif - if (m == NULL) return NULL; %s return m; } - -#if PY_MAJOR_VERSION >= 3 -PyMODINIT_FUNC -PyInit__ecodes(void) -{ - return moduleinit(); -} -#else -PyMODINIT_FUNC -init_ecodes(void) -{ - moduleinit(); -} -#endif ''' def parse_header(header): diff --git a/setup.py b/setup.py index 9517fe8..ff01e4d 100755 --- a/setup.py +++ b/setup.py @@ -24,6 +24,7 @@ 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Operating System :: POSIX :: Linux', 'Intended Audience :: Developers', 'Topic :: Software Development :: Libraries', From 1eda7386a9efc13b8fdd50ea420f9e531e4999d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C5=A0u=C4=87ur?= Date: Fri, 20 Jan 2023 16:05:34 +0100 Subject: [PATCH 15/87] Put unused variable to use (#184) Variable effect_type was declared but never used, in the ff-event injection example. --- docs/tutorial.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 7416181..cdaa886 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -445,7 +445,7 @@ Injecting an FF-event into first FF-capable device found ecodes.FF_RUMBLE, -1, 0, ff.Trigger(0, 0), ff.Replay(duration_ms, 0), - ff.EffectType(ff_rumble_effect=rumble) + effect_type ) repeat_count = 1 From 2dd6ce6364bb67eedb209f6aa0bace0c18a3a40a Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Fri, 20 Jan 2023 16:15:19 +0100 Subject: [PATCH 16/87] Bump version: 1.6.0 -> 1.6.1 --- docs/changelog.rst | 6 ++++++ docs/conf.py | 2 +- setup.cfg | 2 +- setup.py | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index e7b3af4..f656eb1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,12 @@ Changelog --------- +1.6.1 (Jan 20, 2023) +================== + +- Fix generation of ``ecodes.c`` when the path to ````sys.executable`` contains spaces. + + 1.6.0 (Jul 17, 2022) ================== diff --git a/docs/conf.py b/docs/conf.py index 8dc3ae9..96dc4d6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -61,7 +61,7 @@ # built documents. # # The full version, including alpha/beta/rc tags. -release = '1.6.0' +release = '1.6.1' # The short X.Y version. version = release diff --git a/setup.cfg b/setup.cfg index 9e2dc85..47ca660 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.6.0 +current_version = 1.6.1 message = Bump version: {current_version} -> {new_version} commit = True tag = True diff --git a/setup.py b/setup.py index ff01e4d..73ba1f5 100755 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ #----------------------------------------------------------------------------- kw = { 'name': 'evdev', - 'version': '1.6.0', + 'version': '1.6.1', 'description': 'Bindings to the Linux input handling subsystem', 'long_description': (curdir / 'README.rst').read_text(), From bedcfbf5d4c3efed91ee2c66511c52ad3612a147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 27 Aug 2023 15:27:29 +0200 Subject: [PATCH 17/87] respect CPATH/C_INCLUDE_PATH Flags are hard to pass when python-evdev is installed as part of another python library. Instead evdev should check standard environment variables that also a C compiler would use to locate a header file. See: https://gcc.gnu.org/onlinedocs/cpp/Environment-Variables.html Update setup.py Co-authored-by: Tobi --- setup.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 73ba1f5..a9f45ef 100755 --- a/setup.py +++ b/setup.py @@ -64,11 +64,14 @@ #----------------------------------------------------------------------------- def create_ecodes(headers=None): if not headers: - headers = [ - '/usr/include/linux/input.h', - '/usr/include/linux/input-event-codes.h', - '/usr/include/linux/uinput.h', - ] + include_paths = set() + if os.environ.get("CPATH", "").strip() != "": + include_paths.update(os.environ["CPATH"].split(":")) + if os.environ.get("C_INCLUDE_PATH", "").strip() != "": + include_paths.update(os.environ["C_INCLUDE_PATH"].split(":")) + include_paths.add("/usr/include") + files = ["linux/input.h", "linux/input-event-codes.h", "linux/uinput.h"] + headers = [os.path.join(path, file) for path in include_paths for file in files] headers = [header for header in headers if os.path.isfile(header)] if not headers: From ae51cb1dca1daff3d459ca1b0c8cb6eedc5129e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 3 Dec 2023 13:21:28 +0100 Subject: [PATCH 18/87] genecodes: also include uinput.h --- evdev/genecodes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/evdev/genecodes.py b/evdev/genecodes.py index 15e4ded..5b70154 100644 --- a/evdev/genecodes.py +++ b/evdev/genecodes.py @@ -13,6 +13,7 @@ headers = [ '/usr/include/linux/input.h', '/usr/include/linux/input-event-codes.h', + '/usr/include/linux/uinput.h', ] if sys.argv[1:]: From b4db32ebbcca4f7d6a7e9bac137e22b0edbd4fae Mon Sep 17 00:00:00 2001 From: quaxalber <64684396+quaxalber@users.noreply.github.com> Date: Fri, 8 Dec 2023 18:15:23 +0100 Subject: [PATCH 19/87] Print uniq instead of phys, if available --- evdev/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evdev/device.py b/evdev/device.py index dbfad5d..6ee6168 100644 --- a/evdev/device.py +++ b/evdev/device.py @@ -284,7 +284,7 @@ def __eq__(self, other): def __str__(self): msg = 'device {}, name "{}", phys "{}"' - return msg.format(self.path, self.name, self.phys) + return msg.format(self.path, self.name, self.uniq if self.uniq else self.phys) def __repr__(self): msg = (self.__class__.__name__, self.path) From e7b9dfc65579b2b889943823207c04d304b8477e Mon Sep 17 00:00:00 2001 From: Benjamin T Date: Sun, 10 Dec 2023 13:21:56 +0100 Subject: [PATCH 20/87] Print uniq additionally to phys, if available --- evdev/device.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/evdev/device.py b/evdev/device.py index 6ee6168..808b9fd 100644 --- a/evdev/device.py +++ b/evdev/device.py @@ -283,8 +283,9 @@ def __eq__(self, other): and self.path == other.path def __str__(self): - msg = 'device {}, name "{}", phys "{}"' - return msg.format(self.path, self.name, self.uniq if self.uniq else self.phys) + msg = 'device {}, name "{}", phys "{}"{}' + uniq = ', uniq "{}"'.format(self.uniq) if self.uniq else '' + return msg.format(self.path, self.name, self.phys, uniq) def __repr__(self): msg = (self.__class__.__name__, self.path) From c688c9e63c535f3a9e0fae4930315ef432d09384 Mon Sep 17 00:00:00 2001 From: John Salamon Date: Fri, 15 Dec 2023 01:04:06 +1030 Subject: [PATCH 21/87] Correct ff_effects_max of uinputs (#198) --- docs/tutorial.rst | 52 +++++++++++++++++++++++++++++++++++++++++++++++ evdev/uinput.c | 14 +++++++------ evdev/uinput.py | 11 ++++++++-- 3 files changed, 69 insertions(+), 8 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index cdaa886..286a493 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -453,3 +453,55 @@ Injecting an FF-event into first FF-capable device found dev.write(ecodes.EV_FF, effect_id, repeat_count) time.sleep(duration_ms) dev.erase_effect(effect_id) + +Forwarding force-feedback from uinput to a real device +====================================================== + +:: + + import evdev + from evdev import ecodes as e + + # Find first EV_FF capable event device (that we have permissions to use). + for name in evdev.list_devices(): + dev = evdev.InputDevice(name) + if e.EV_FF in dev.capabilities(): + break + # To ensure forwarding works correctly it is important that `max_effects` + # of the uinput device is <= `dev.ff_effects_count`. + # `from_device()` will do this automatically, but in some situations you may + # want to set the `max_effects` parameter manually, such as when using `Uinput()`. + # `filtered_types` is specified as by default EV_FF events are filtered + uinput = evdev.UInput.from_device(dev, filtered_types=[e.EV_SYN]) + + # Keeps track of which effects have been uploaded to the device + effects = set() + + for event in uinput.read_loop(): + + # Handle the special uinput events + if event.type == e.EV_UINPUT: + + if event.code == e.UI_FF_UPLOAD: + upload = uinput.begin_upload(event.value) + + # Checks if this is a new effect + if upload.effect.id not in effects: + effects.add(upload.effect.id) + # Setting id to 1 indicates that a new effect must be allocated + upload.effect.id = -1 + + dev.upload_effect(upload.effect) + upload.retval = 0 + uinput.end_upload(upload) + + elif event.code == e.UI_FF_ERASE: + erase = uinput.begin_erase(event.value) + erase.retval = 0 + dev.erase_effect(erase.effect_id) + effects.remove(erase.effect_id) + uinput.end_erase(erase) + + # Forward writes to actual rumble device. + elif event.type == e.EV_FF: + dev.write(event.type, event.code, event.value) diff --git a/evdev/uinput.c b/evdev/uinput.c index 20318ce..6848bc2 100644 --- a/evdev/uinput.c +++ b/evdev/uinput.c @@ -108,14 +108,15 @@ static PyObject * uinput_setup(PyObject *self, PyObject *args) { int fd, len, i; uint16_t vendor, product, version, bustype; + uint32_t max_effects; PyObject *absinfo = NULL, *item = NULL; struct uinput_abs_setup abs_setup; const char* name; - int ret = PyArg_ParseTuple(args, "isHHHHO", &fd, &name, &vendor, - &product, &version, &bustype, &absinfo); + int ret = PyArg_ParseTuple(args, "isHHHHOI", &fd, &name, &vendor, + &product, &version, &bustype, &absinfo, &max_effects); if (!ret) return NULL; // Setup absinfo: @@ -147,7 +148,7 @@ uinput_setup(PyObject *self, PyObject *args) { usetup.id.product = product; usetup.id.version = version; usetup.id.bustype = bustype; - usetup.ff_effects_max = FF_MAX_EFFECTS; + usetup.ff_effects_max = max_effects; if(ioctl(fd, UI_DEV_SETUP, &usetup) < 0) goto on_err; @@ -166,14 +167,15 @@ static PyObject * uinput_setup(PyObject *self, PyObject *args) { int fd, len, i, abscode; uint16_t vendor, product, version, bustype; + uint32_t max_effects; PyObject *absinfo = NULL, *item = NULL; struct uinput_user_dev uidev; const char* name; - int ret = PyArg_ParseTuple(args, "isHHHHO", &fd, &name, &vendor, - &product, &version, &bustype, &absinfo); + int ret = PyArg_ParseTuple(args, "isHHHHOI", &fd, &name, &vendor, + &product, &version, &bustype, &absinfo, &max_effects); if (!ret) return NULL; memset(&uidev, 0, sizeof(uidev)); @@ -182,7 +184,7 @@ uinput_setup(PyObject *self, PyObject *args) { uidev.id.product = product; uidev.id.version = version; uidev.id.bustype = bustype; - uidev.ff_effects_max = FF_MAX_EFFECTS; + uidev.ff_effects_max = max_effects; len = PyList_Size(absinfo); for (i=0; i Date: Tue, 9 Jan 2024 21:04:32 +0000 Subject: [PATCH 22/87] New method for finding the device node corresponding to a uinput device (#206) * Add binding for UI_GET_SYSNAME ioctl * New modern Linux-specific method to find device The old method of scanning the filesystem to find an event device with a matching name was prone to race conditions, required a time.sleep delay, unnecessarily opened many event devices, and had other fixable issues. The new UI_GET_SYSNAME-based method is immune to race conditions and does not require time.sleep(), while also not suffering from the aforementioned fixable issues. * Move get_sysname() to the same block as _find_device_linux() Co-authored-by: Tobi <28510156+sezanzeb@users.noreply.github.com> * Sort event devices in _find_device_fallback() * Wait 0.1s if /dev/input/event* does not exist Unlike what a previous commit claimed, the new method is only guaranteed to find the device name instantaneously, but if /dev is managed by udev, the corresponding /dev/input/input* file might not exist immediately. On modern Linux systems, those files are managed by devtmpfs so they do show up immediately and it would be a waste of time to sleep, but we do want a fallback time.sleep() call for those operating systems which do not use devtmpfs. It is assumed that on any linux system, /sys is managed by sysfs; it conceptually doesn't really make sense to let that directory be managed by any other filesystem because the files in /sys are basically syscalls. As such, we assume that /sys always updates instantaneously, and if for whatever reason it doesn't, _find_device_fallback shall be used instead. * Add a comment Co-authored-by: Tobi <28510156+sezanzeb@users.noreply.github.com> * Replace IOError with OSError Since Python 3.3 (PEP 3151), IOError and OSError are aliases of each other, and OSError is the prefered name. Rename all mentions of IOError in the C bindings with OSError. * Replace IOError with OSError in documentation * Explain why the sorting is necessary Co-authored-by: Tobi <28510156+sezanzeb@users.noreply.github.com> --------- Co-authored-by: Tobi <28510156+sezanzeb@users.noreply.github.com> --- evdev/device.py | 4 +-- evdev/input.c | 18 ++++++------ evdev/uinput.c | 41 ++++++++++++++++++++------- evdev/uinput.py | 74 +++++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 114 insertions(+), 23 deletions(-) diff --git a/evdev/device.py b/evdev/device.py index dbfad5d..27c84a6 100644 --- a/evdev/device.py +++ b/evdev/device.py @@ -309,7 +309,7 @@ def grab(self): Warning ------- - Grabbing an already grabbed device will raise an ``IOError``. + Grabbing an already grabbed device will raise an ``OSError``. ''' _input.ioctl_EVIOCGRAB(self.fd, 1) @@ -321,7 +321,7 @@ def ungrab(self): Warning ------- Releasing an already released device will raise an - ``IOError('Invalid argument')``. + ``OSError('Invalid argument')``. ''' _input.ioctl_EVIOCGRAB(self.fd, 0) diff --git a/evdev/input.c b/evdev/input.c index b1497a4..0256745 100644 --- a/evdev/input.c +++ b/evdev/input.c @@ -61,7 +61,7 @@ device_read(PyObject *self, PyObject *args) return Py_None; } - PyErr_SetFromErrno(PyExc_IOError); + PyErr_SetFromErrno(PyExc_OSError); return NULL; } @@ -101,7 +101,7 @@ device_read_many(PyObject *self, PyObject *args) ssize_t nread = read(fd, event, event_size*64); if (nread < 0) { - PyErr_SetFromErrno(PyExc_IOError); + PyErr_SetFromErrno(PyExc_OSError); Py_DECREF(event_list); return NULL; } @@ -208,7 +208,7 @@ ioctl_capabilities(PyObject *self, PyObject *args) return capabilities; on_err: - PyErr_SetFromErrno(PyExc_IOError); + PyErr_SetFromErrno(PyExc_OSError); return NULL; } @@ -243,7 +243,7 @@ ioctl_devinfo(PyObject *self, PyObject *args) name, phys, uniq); on_err: - PyErr_SetFromErrno(PyExc_IOError); + PyErr_SetFromErrno(PyExc_OSError); return NULL; } @@ -261,7 +261,7 @@ ioctl_EVIOCGABS(PyObject *self, PyObject *args) memset(&absinfo, 0, sizeof(absinfo)); ret = ioctl(fd, EVIOCGABS(ev_code), &absinfo); if (ret == -1) { - PyErr_SetFromErrno(PyExc_IOError); + PyErr_SetFromErrno(PyExc_OSError); return NULL; } @@ -296,7 +296,7 @@ ioctl_EVIOCSABS(PyObject *self, PyObject *args) ret = ioctl(fd, EVIOCSABS(ev_code), &absinfo); if (ret == -1) { - PyErr_SetFromErrno(PyExc_IOError); + PyErr_SetFromErrno(PyExc_OSError); return NULL; } @@ -362,7 +362,7 @@ ioctl_EVIOCGRAB(PyObject *self, PyObject *args) ret = ioctl(fd, EVIOCGRAB, (intptr_t)flag); if (ret != 0) { - PyErr_SetFromErrno(PyExc_IOError); + PyErr_SetFromErrno(PyExc_OSError); return NULL; } @@ -485,7 +485,7 @@ upload_effect(PyObject *self, PyObject *args) ret = ioctl(fd, EVIOCSFF, &effect); if (ret != 0) { - PyErr_SetFromErrno(PyExc_IOError); + PyErr_SetFromErrno(PyExc_OSError); return NULL; } @@ -504,7 +504,7 @@ erase_effect(PyObject *self, PyObject *args) long ff_id = PyLong_AsLong(ff_id_obj); ret = ioctl(fd, EVIOCRMFF, ff_id); if (ret != 0) { - PyErr_SetFromErrno(PyExc_IOError); + PyErr_SetFromErrno(PyExc_OSError); return NULL; } diff --git a/evdev/uinput.c b/evdev/uinput.c index 6848bc2..2e2d8e5 100644 --- a/evdev/uinput.c +++ b/evdev/uinput.c @@ -49,7 +49,7 @@ uinput_open(PyObject *self, PyObject *args) int fd = open(devnode, O_RDWR | O_NONBLOCK); if (fd < 0) { - PyErr_SetString(PyExc_IOError, "could not open uinput device in write mode"); + PyErr_SetString(PyExc_OSError, "could not open uinput device in write mode"); return NULL; } @@ -73,7 +73,7 @@ uinput_set_phys(PyObject *self, PyObject *args) on_err: _uinput_close(fd); - PyErr_SetFromErrno(PyExc_IOError); + PyErr_SetFromErrno(PyExc_OSError); return NULL; } @@ -93,10 +93,28 @@ uinput_set_prop(PyObject *self, PyObject *args) on_err: _uinput_close(fd); - PyErr_SetFromErrno(PyExc_IOError); + PyErr_SetFromErrno(PyExc_OSError); return NULL; } +static PyObject * +uinput_get_sysname(PyObject *self, PyObject *args) +{ + int fd; + char sysname[64]; + + int ret = PyArg_ParseTuple(args, "i", &fd); + if (!ret) return NULL; + + if (ioctl(fd, UI_GET_SYSNAME(sizeof(sysname)), &sysname) < 0) + goto on_err; + + return Py_BuildValue("s", &sysname); + + on_err: + PyErr_SetFromErrno(PyExc_OSError); + return NULL; +} // Different kernel versions have different device setup methods. You can read // more about it here: @@ -157,7 +175,7 @@ uinput_setup(PyObject *self, PyObject *args) { on_err: _uinput_close(fd); - PyErr_SetFromErrno(PyExc_IOError); + PyErr_SetFromErrno(PyExc_OSError); return NULL; } @@ -206,7 +224,7 @@ uinput_setup(PyObject *self, PyObject *args) { on_err: _uinput_close(fd); - PyErr_SetFromErrno(PyExc_IOError); + PyErr_SetFromErrno(PyExc_OSError); return NULL; } #endif @@ -227,7 +245,7 @@ uinput_create(PyObject *self, PyObject *args) on_err: _uinput_close(fd); - PyErr_SetFromErrno(PyExc_IOError); + PyErr_SetFromErrno(PyExc_OSError); return NULL; } @@ -241,7 +259,7 @@ uinput_close(PyObject *self, PyObject *args) if (!ret) return NULL; if (_uinput_close(fd) < 0) { - PyErr_SetFromErrno(PyExc_IOError); + PyErr_SetFromErrno(PyExc_OSError); return NULL; } @@ -269,8 +287,8 @@ uinput_write(PyObject *self, PyObject *args) if (write(fd, &event, sizeof(event)) != sizeof(event)) { // @todo: elaborate - // PyErr_SetString(PyExc_IOError, "error writing event to uinput device"); - PyErr_SetFromErrno(PyExc_IOError); + // PyErr_SetString(PyExc_OSError, "error writing event to uinput device"); + PyErr_SetFromErrno(PyExc_OSError); return NULL; } @@ -312,7 +330,7 @@ uinput_enable_event(PyObject *self, PyObject *args) on_err: _uinput_close(fd); - PyErr_SetFromErrno(PyExc_IOError); + PyErr_SetFromErrno(PyExc_OSError); return NULL; } @@ -361,6 +379,9 @@ static PyMethodDef MethodTable[] = { { "set_phys", uinput_set_phys, METH_VARARGS, "Set physical path"}, + { "get_sysname", uinput_get_sysname, METH_VARARGS, + "Obtain the sysname of the uinput device."}, + { "set_prop", uinput_set_prop, METH_VARARGS, "Set device input property"}, diff --git a/evdev/uinput.py b/evdev/uinput.py index 5b11bd7..2c1424b 100644 --- a/evdev/uinput.py +++ b/evdev/uinput.py @@ -1,6 +1,8 @@ # encoding: utf-8 import os +import platform +import re import stat import time from collections import defaultdict @@ -159,7 +161,7 @@ def __init__(self, #: An :class:`InputDevice ` instance #: for the fake input device. ``None`` if the device cannot be #: opened for reading and writing. - self.device = self._find_device() + self.device = self._find_device(self.fd) def _prepare_events(self, events): '''Prepare events for passing to _uinput.enable and _uinput.setup''' @@ -281,11 +283,79 @@ def _verify(self): msg = 'uinput device name must not be longer than {} characters' raise UInputError(msg.format(_uinput.maxnamelen)) - def _find_device(self): + def _find_device(self, fd): + ''' + Tries to find the device node. Will delegate this task to one of + several platform-specific functions. + ''' + if platform.system() == 'Linux': + try: + sysname = _uinput.get_sysname(fd) + return self._find_device_linux(sysname) + except OSError: + # UI_GET_SYSNAME returned an error code. We're likely dealing with + # an old kernel. Guess the device based on the filesystem. + pass + + # If we're not running or Linux or the above method fails for any reason, + # use the generic fallback method. + return self._find_device_fallback() + + def _find_device_linux(self, sysname): + ''' + Tries to find the device node when running on Linux. + ''' + + syspath = f'/sys/devices/virtual/input/{sysname}' + + # The sysfs entry for event devices should contain exactly one folder + # whose name matches the format "event[0-9]+". It is then assumed that + # the device node in /dev/input uses the same name. + regex = re.compile('event[0-9]+') + for entry in os.listdir(syspath): + if regex.fullmatch(entry): + device_path = f'/dev/input/{entry}' + break + else: # no break + raise FileNotFoundError() + + # It is possible that there is some delay before /dev/input/event* shows + # up on old systems that do not use devtmpfs, so if the device cannot be + # found, wait for a short amount and then try again once. + try: + return device.InputDevice(device_path) + except FileNotFoundError: + time.sleep(0.1) + return device.InputDevice(device_path) + + def _find_device_fallback(self): + ''' + Tries to find the device node when UI_GET_SYSNAME is not available or + we're running on a system sufficiently exotic that we do not know how + to interpret its return value. + ''' #:bug: the device node might not be immediately available time.sleep(0.1) + # There could also be another device with the same name already present, + # make sure to select the newest one. + # Strictly speaking, we cannot be certain that everything returned by list_devices() + # ends at event[0-9]+: it might return something like "/dev/input/events_all". Find + # the devices that have the expected structure and extract their device number. + path_number_pairs = [] + regex = re.compile('/dev/input/event([0-9]+)') for path in util.list_devices('/dev/input/'): + regex_match = regex.fullmatch(path) + if not regex_match: + continue + device_number = int(regex_match[1]) + path_number_pairs.append((path, device_number)) + + # The modification date of the devnode is not reliable unfortunately, so we + # are sorting by the number in the name + path_number_pairs.sort(key=lambda pair: pair[1], reverse=True) + + for (path, _) in path_number_pairs: d = device.InputDevice(path) if d.name == self.name: return d From 693a2978af98793be634940cd53472c39bd6deaa Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 28 Jan 2024 23:48:20 +0100 Subject: [PATCH 23/87] Always include "uniq" address in InputDevice.__str__() --- evdev/device.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/evdev/device.py b/evdev/device.py index b92bfcc..83a2cc8 100644 --- a/evdev/device.py +++ b/evdev/device.py @@ -141,7 +141,7 @@ def __init__(self, dev): #: The physical topology of the device. self.phys = info_res[5] - #: The unique address of the device. + #: The unique identifier of the device. self.uniq = info_res[6] #: The evdev protocol version. @@ -283,9 +283,8 @@ def __eq__(self, other): and self.path == other.path def __str__(self): - msg = 'device {}, name "{}", phys "{}"{}' - uniq = ', uniq "{}"'.format(self.uniq) if self.uniq else '' - return msg.format(self.path, self.name, self.phys, uniq) + msg = 'device {}, name "{}", phys "{}", uniq "{}"' + return msg.format(self.path, self.name, self.phys, self.uniq or "") def __repr__(self): msg = (self.__class__.__name__, self.path) From e00c457dda090417c7258ced4d157447ca9ab771 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Mon, 29 Jan 2024 00:10:25 +0100 Subject: [PATCH 24/87] ruff format --- docs/conf.py | 163 ++++++++++++++++++++------------------- evdev/__init__.py | 4 +- evdev/device.py | 121 ++++++++++++++--------------- evdev/ecodes.py | 14 ++-- evdev/eventio.py | 41 +++++----- evdev/eventio_async.py | 18 +++-- evdev/events.py | 71 ++++++++--------- evdev/evtest.py | 81 ++++++++++--------- evdev/ff.py | 134 ++++++++++++++++---------------- evdev/genecodes.py | 35 +++++---- evdev/uinput.py | 132 ++++++++++++++++--------------- evdev/util.py | 34 ++++---- examples/udev-example.py | 18 ++--- pyproject.toml | 2 + setup.py | 113 +++++++++++++-------------- tests/test_ecodes.py | 12 ++- tests/test_events.py | 4 +- tests/test_uinput.py | 55 +++++++------ tests/test_util.py | 18 ++--- 19 files changed, 542 insertions(+), 528 deletions(-) create mode 100644 pyproject.toml diff --git a/docs/conf.py b/docs/conf.py index 96dc4d6..d1dc3fd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -4,7 +4,7 @@ import sphinx_rtd_theme # Check if readthedocs is building us -on_rtd = os.environ.get('READTHEDOCS', None) == 'True' +on_rtd = os.environ.get("READTHEDOCS", None) == "True" # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -12,93 +12,97 @@ # Trick autodoc into running without having built the extension modules. if on_rtd: - with open('../evdev/_ecodes.py', 'w') as fh: - fh.write(''' + with open("../evdev/_ecodes.py", "w") as fh: + fh.write( + """ KEY = ABS = REL = SW = MSC = LED = REP = SND = SYN = FF = FF_STATUS = BTN_A = KEY_A = 1 EV_KEY = EV_ABS = EV_REL = EV_SW = EV_MSC = EV_LED = EV_REP = 1 EV_SND = EV_SYN = EV_FF = EV_FF_STATUS = FF_STATUS = 1 -KEY_MAX, KEY_CNT = 1, 2''') +KEY_MAX, KEY_CNT = 1, 2""" + ) - with open('../evdev/_input.py', 'w'): pass - with open('../evdev/_uinput.py', 'w'): pass + with open("../evdev/_input.py", "w"): + pass + with open("../evdev/_uinput.py", "w"): + pass # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.viewcode', - 'sphinx.ext.intersphinx', - 'sphinx.ext.napoleon', - 'sphinx_copybutton', + "sphinx.ext.autodoc", + "sphinx.ext.viewcode", + "sphinx.ext.intersphinx", + "sphinx.ext.napoleon", + "sphinx_copybutton", ] -autodoc_member_order = 'bysource' +autodoc_member_order = "bysource" # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'python-evdev' -copyright = u'2012-2022, Georgi Valkov' +project = "python-evdev" +copyright = "2012-2024, Georgi Valkov and contributors" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The full version, including alpha/beta/rc tags. -release = '1.6.1' +release = "1.6.1" # The short X.Y version. version = release # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -#pygments_style = 'sphinx' +# pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- @@ -108,7 +112,8 @@ if not on_rtd: import sphinx_rtd_theme - html_theme = 'sphinx_rtd_theme' + + html_theme = "sphinx_rtd_theme" html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # Theme options are theme-specific and customize the look and feel of a theme @@ -120,122 +125,116 @@ # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -html_title = 'Python-evdev' +html_title = "Python-evdev" # A shorter title for the navigation bar. Default is the same as html_title. -html_short_title = 'evdev' +html_short_title = "evdev" # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = '_static/evdev-logo-small.png' +# html_logo = '_static/evdev-logo-small.png' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'python-evdev-doc' +htmlhelp_basename = "python-evdev-doc" # -- Options for LaTeX output -------------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'python-evdev.tex', u'evdev documentation', - u'Georgi Valkov', 'manual'), + ("index", "python-evdev.tex", "evdev documentation", "Georgi Valkov", "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'python-evdev', u'python-evdev Documentation', - [u'Georgi Valkov'], 1) -] +man_pages = [("index", "python-evdev", "python-evdev Documentation", ["Georgi Valkov"], 1)] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ @@ -244,23 +243,29 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'python-evdev', u'python-evdev Documentation', - u'Georgi Valkov', 'evdev', 'Bindings for the linux input handling subsystem.', - 'Miscellaneous'), + ( + "index", + "python-evdev", + "python-evdev Documentation", + "Georgi Valkov", + "evdev", + "Bindings for the linux input handling subsystem.", + "Miscellaneous", + ), ] -intersphinx_mapping = {'python': ('http://docs.python.org/3', None)} +intersphinx_mapping = {"python": ("http://docs.python.org/3", None)} # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # Copybutton config -#copybutton_prompt_text = r">>> " -#copybutton_prompt_is_regexp = True -#copybutton_only_copy_prompt_lines = True +# copybutton_prompt_text = r">>> " +# copybutton_prompt_is_regexp = True +# copybutton_only_copy_prompt_lines = True diff --git a/evdev/__init__.py b/evdev/__init__.py index 797623b..36b330c 100644 --- a/evdev/__init__.py +++ b/evdev/__init__.py @@ -1,6 +1,6 @@ -#-------------------------------------------------------------------------- +# -------------------------------------------------------------------------- # Gather everything into a single, convenient namespace. -#-------------------------------------------------------------------------- +# -------------------------------------------------------------------------- from evdev.device import DeviceInfo, InputDevice, AbsInfo, EvdevError from evdev.events import InputEvent, KeyEvent, RelEvent, SynEvent, AbsEvent, event_factory diff --git a/evdev/device.py b/evdev/device.py index 83a2cc8..0d2d1d9 100644 --- a/evdev/device.py +++ b/evdev/device.py @@ -14,19 +14,16 @@ from evdev.eventio import EventIO, EvdevError -#-------------------------------------------------------------------------- -_AbsInfo = collections.namedtuple( - 'AbsInfo', ['value', 'min', 'max', 'fuzz', 'flat', 'resolution']) +# -------------------------------------------------------------------------- +_AbsInfo = collections.namedtuple("AbsInfo", ["value", "min", "max", "fuzz", "flat", "resolution"]) -_KbdInfo = collections.namedtuple( - 'KbdInfo', ['repeat', 'delay']) +_KbdInfo = collections.namedtuple("KbdInfo", ["repeat", "delay"]) -_DeviceInfo = collections.namedtuple( - 'DeviceInfo', ['bustype', 'vendor', 'product', 'version']) +_DeviceInfo = collections.namedtuple("DeviceInfo", ["bustype", "vendor", "product", "version"]) class AbsInfo(_AbsInfo): - '''Absolute axis information. + """Absolute axis information. A ``namedtuple`` used for storing absolute axis information - corresponds to the ``input_absinfo`` struct: @@ -62,14 +59,14 @@ class AbsInfo(_AbsInfo): The input core does not clamp reported values to the ``[minimum, maximum]`` limits, such task is left to userspace. - ''' + """ def __str__(self): - return 'val {}, min {}, max {}, fuzz {}, flat {}, res {}'.format(*self) + return "val {}, min {}, max {}, fuzz {}, flat {}, res {}".format(*self) class KbdInfo(_KbdInfo): - '''Keyboard repeat rate. + """Keyboard repeat rate. Attributes ---------- @@ -79,45 +76,44 @@ class KbdInfo(_KbdInfo): delay Amount of time that a key must be depressed before it will start to repeat (in milliseconds). - ''' + """ def __str__(self): - return 'repeat {}, delay {}'.format(*self) + return "repeat {}, delay {}".format(*self) class DeviceInfo(_DeviceInfo): - ''' + """ Attributes ---------- bustype vendor product version - ''' + """ def __str__(self): - msg = 'bus: {:04x}, vendor {:04x}, product {:04x}, version {:04x}' + msg = "bus: {:04x}, vendor {:04x}, product {:04x}, version {:04x}" return msg.format(*self) class InputDevice(EventIO): - ''' + """ A linux input device from which input events can be read. - ''' + """ - __slots__ = ('path', 'fd', 'info', 'name', 'phys', 'uniq', '_rawcapabilities', - 'version', 'ff_effects_count') + __slots__ = ("path", "fd", "info", "name", "phys", "uniq", "_rawcapabilities", "version", "ff_effects_count") def __init__(self, dev): - ''' + """ Arguments --------- dev : str|bytes|PathLike Path to input device - ''' + """ #: Path to input device. - self.path = dev if not hasattr(dev, '__fspath__') else dev.__fspath__() + self.path = dev if not hasattr(dev, "__fspath__") else dev.__fspath__() # Certain operations are possible only when the device is opened in # read-write mode. @@ -154,7 +150,7 @@ def __init__(self, dev): self.ff_effects_count = _input.ioctl_EVIOCGEFFECTS(self.fd) def __del__(self): - if hasattr(self, 'fd') and self.fd is not None: + if hasattr(self, "fd") and self.fd is not None: try: self.close() except (OSError, ImportError, AttributeError): @@ -179,7 +175,7 @@ def _capabilities(self, absinfo=True): return res def capabilities(self, verbose=False, absinfo=True): - ''' + """ Return the event types that this device supports as a mapping of supported event types to lists of handled event codes. @@ -216,7 +212,7 @@ def capabilities(self, verbose=False, absinfo=True): { ('EV_ABS', 3): [ (('ABS_X', 0), AbsInfo(min=0, max=255, fuzz=0, flat=0)), (('ABS_Y', 1), AbsInfo(min=0, max=255, fuzz=0, flat=0)) ]} - ''' + """ if verbose: return dict(util.resolve_ecodes_dict(self._capabilities(absinfo))) @@ -224,7 +220,7 @@ def capabilities(self, verbose=False, absinfo=True): return self._capabilities(absinfo) def input_props(self, verbose=False): - ''' + """ Get device properties and quirks. Example @@ -237,7 +233,7 @@ def input_props(self, verbose=False): [('INPUT_PROP_POINTER', 0), ('INPUT_PROP_POINTING_STICK', 5)] - ''' + """ props = _input.ioctl_EVIOCGPROP(self.fd) if verbose: return util.resolve_ecodes(ecodes.INPUT_PROP, props) @@ -245,7 +241,7 @@ def input_props(self, verbose=False): return props def leds(self, verbose=False): - ''' + """ Return currently set LED keys. Example @@ -258,7 +254,7 @@ def leds(self, verbose=False): [('LED_NUML', 0), ('LED_CAPSL', 1), ('LED_MISC', 8), ('LED_MAIL', 9)] - ''' + """ leds = _input.ioctl_EVIOCG_bits(self.fd, ecodes.EV_LED) if verbose: return util.resolve_ecodes(ecodes.LED, leds) @@ -266,21 +262,20 @@ def leds(self, verbose=False): return leds def set_led(self, led_num, value): - ''' + """ Set the state of the selected LED. Example ------- >>> device.set_led(ecodes.LED_NUML, 1) - ''' + """ self.write(ecodes.EV_LED, led_num, value) def __eq__(self, other): - ''' + """ Two devices are equal if their :data:`info` attributes are equal. - ''' - return isinstance(other, self.__class__) and self.info == other.info \ - and self.path == other.path + """ + return isinstance(other, self.__class__) and self.info == other.info and self.path == other.path def __str__(self): msg = 'device {}, name "{}", phys "{}", uniq "{}"' @@ -288,7 +283,7 @@ def __str__(self): def __repr__(self): msg = (self.__class__.__name__, self.path) - return '{}({!r})'.format(*msg) + return "{}({!r})".format(*msg) def __fspath__(self): return self.path @@ -302,7 +297,7 @@ def close(self): self.fd = -1 def grab(self): - ''' + """ Grab input device using ``EVIOCGRAB`` - other applications will be unable to receive events until the device is released. Only one process can hold a ``EVIOCGRAB`` on a device. @@ -310,55 +305,55 @@ def grab(self): Warning ------- Grabbing an already grabbed device will raise an ``OSError``. - ''' + """ _input.ioctl_EVIOCGRAB(self.fd, 1) def ungrab(self): - ''' + """ Release device if it has been already grabbed (uses `EVIOCGRAB`). Warning ------- Releasing an already released device will raise an ``OSError('Invalid argument')``. - ''' + """ _input.ioctl_EVIOCGRAB(self.fd, 0) @contextlib.contextmanager def grab_context(self): - ''' + """ A context manager for the duration of which only the current process will be able to receive events from the device. - ''' + """ self.grab() yield self.ungrab() def upload_effect(self, effect): - ''' + """ Upload a force feedback effect to a force feedback device. - ''' + """ data = memoryview(effect).tobytes() ff_id = _input.upload_effect(self.fd, data) return ff_id def erase_effect(self, ff_id): - ''' + """ Erase a force effect from a force feedback device. This also stops the effect. - ''' + """ _input.erase_effect(self.fd, ff_id) @property def repeat(self): - ''' + """ Get or set the keyboard repeat rate (in characters per minute) and delay (in milliseconds). - ''' + """ return KbdInfo(*_input.ioctl_EVIOCGREP(self.fd)) @@ -367,7 +362,7 @@ def repeat(self, value): return _input.ioctl_EVIOCSREP(self.fd, *value) def active_keys(self, verbose=False): - ''' + """ Return currently active keys. Example @@ -382,7 +377,7 @@ def active_keys(self, verbose=False): [('KEY_ESC', 1), ('KEY_LEFTSHIFT', 42)] - ''' + """ active_keys = _input.ioctl_EVIOCG_bits(self.fd, ecodes.EV_KEY) if verbose: return util.resolve_ecodes(ecodes.KEY, active_keys) @@ -391,12 +386,12 @@ def active_keys(self, verbose=False): @property def fn(self): - msg = 'Please use {0}.path instead of {0}.fn'.format(self.__class__.__name__) + msg = "Please use {0}.path instead of {0}.fn".format(self.__class__.__name__) warnings.warn(msg, DeprecationWarning, stacklevel=2) return self.path def absinfo(self, axis_num): - ''' + """ Return current :class:`AbsInfo` for input device axis Arguments @@ -408,11 +403,11 @@ def absinfo(self, axis_num): ------- >>> device.absinfo(ecodes.ABS_X) AbsInfo(value=1501, min=-32768, max=32767, fuzz=0, flat=128, resolution=0) - ''' + """ return AbsInfo(*_input.ioctl_EVIOCGABS(self.fd, axis_num)) def set_absinfo(self, axis_num, value=None, min=None, max=None, fuzz=None, flat=None, resolution=None): - ''' + """ Update :class:`AbsInfo` values. Only specified values will be overwritten. Arguments @@ -427,13 +422,15 @@ def set_absinfo(self, axis_num, value=None, min=None, max=None, fuzz=None, flat= You can also unpack AbsInfo tuple that will overwrite all values >>> device.set_absinfo(ecodes.ABS_Y, *AbsInfo(0, -2000, 2000, 0, 15, 0)) - ''' + """ cur_absinfo = self.absinfo(axis_num) - new_absinfo = AbsInfo(value if value is not None else cur_absinfo.value, - min if min is not None else cur_absinfo.min, - max if max is not None else cur_absinfo.max, - fuzz if fuzz is not None else cur_absinfo.fuzz, - flat if flat is not None else cur_absinfo.flat, - resolution if resolution is not None else cur_absinfo.resolution) + new_absinfo = AbsInfo( + value if value is not None else cur_absinfo.value, + min if min is not None else cur_absinfo.min, + max if max is not None else cur_absinfo.max, + fuzz if fuzz is not None else cur_absinfo.fuzz, + flat if flat is not None else cur_absinfo.flat, + resolution if resolution is not None else cur_absinfo.resolution, + ) _input.ioctl_EVIOCSABS(self.fd, axis_num, new_absinfo) diff --git a/evdev/ecodes.py b/evdev/ecodes.py index e3e4ffc..3562368 100644 --- a/evdev/ecodes.py +++ b/evdev/ecodes.py @@ -1,6 +1,4 @@ -# encoding: utf-8 - -''' +""" This modules exposes the integer constants defined in ``linux/input.h`` and ``linux/input-event-codes.h``. @@ -38,7 +36,7 @@ >>> evdev.ecodes.FF[81] 'FF_PERIODIC' -''' +""" from inspect import getmembers from evdev import _ecodes @@ -47,8 +45,8 @@ #: Mapping of names to values. ecodes = {} -prefixes = 'KEY ABS REL SW MSC LED BTN REP SND ID EV BUS SYN FF_STATUS FF INPUT_PROP' -prev_prefix = '' +prefixes = "KEY ABS REL SW MSC LED BTN REP SND ID EV BUS SYN FF_STATUS FF INPUT_PROP" +prev_prefix = "" g = globals() # eg. code: 'REL_Z', val: 2 @@ -87,13 +85,13 @@ _ecodes.EV_KEY: keys, _ecodes.EV_ABS: ABS, _ecodes.EV_REL: REL, - _ecodes.EV_SW: SW, + _ecodes.EV_SW: SW, _ecodes.EV_MSC: MSC, _ecodes.EV_LED: LED, _ecodes.EV_REP: REP, _ecodes.EV_SND: SND, _ecodes.EV_SYN: SYN, - _ecodes.EV_FF: FF, + _ecodes.EV_FF: FF, _ecodes.EV_FF_STATUS: FF_STATUS, } diff --git a/evdev/eventio.py b/evdev/eventio.py index 8d84f55..1b0e5cc 100644 --- a/evdev/eventio.py +++ b/evdev/eventio.py @@ -1,5 +1,3 @@ -# encoding: utf-8 - import os import fcntl import select @@ -8,13 +6,14 @@ from evdev import _input, _uinput, ecodes, util from evdev.events import InputEvent -#-------------------------------------------------------------------------- + +# -------------------------------------------------------------------------- class EvdevError(Exception): pass class EventIO: - ''' + """ Base class for reading and writing input events. This class is used by :class:`InputDevice` and :class:`UInput`. @@ -26,20 +25,20 @@ class EventIO: - On, :class:`UInput` it used for writing user-generated events (e.g. key presses, mouse movements) and reading feedback events (e.g. leds, beeps). - ''' + """ def fileno(self): - ''' + """ Return the file descriptor to the open event device. This makes it possible to pass instances directly to :func:`select.select()` and :class:`asyncore.file_dispatcher`. - ''' + """ return self.fd def read_loop(self): - ''' + """ Enter an endless :func:`select.select()` loop that yields input events. - ''' + """ while True: r, w, x = select.select([self.fd], [], []) @@ -47,12 +46,12 @@ def read_loop(self): yield event def read_one(self): - ''' + """ Read and return a single input event as an instance of :class:`InputEvent `. Return ``None`` if there are no pending input events. - ''' + """ # event -> (sec, usec, type, code, val) event = _input.device_read(self.fd) @@ -61,11 +60,11 @@ def read_one(self): return InputEvent(*event) def read(self): - ''' + """ Read multiple input events from device. Return a generator object that yields :class:`InputEvent ` instances. Raises `BlockingIOError` if there are no available events at the moment. - ''' + """ # events -> [(sec, usec, type, code, val), ...] events = _input.device_read_many(self.fd) @@ -74,10 +73,11 @@ def read(self): yield InputEvent(*event) def need_write(func): - ''' + """ Decorator that raises :class:`EvdevError` if there is no write access to the input device. - ''' + """ + @functools.wraps(func) def wrapper(*args): fd = args[0].fd @@ -85,10 +85,11 @@ def wrapper(*args): return func(*args) msg = 'no write access to device "%s"' % args[0].path raise EvdevError(msg) + return wrapper def write_event(self, event): - ''' + """ Inject an input event into the input subsystem. Events are queued until a synchronization event is received. @@ -103,16 +104,16 @@ def write_event(self, event): ------- >>> ev = InputEvent(1334414993, 274296, ecodes.EV_KEY, ecodes.KEY_A, 1) >>> ui.write_event(ev) - ''' + """ - if hasattr(event, 'event'): + if hasattr(event, "event"): event = event.event self.write(event.type, event.code, event.value) @need_write def write(self, etype, code, value): - ''' + """ Inject an input event into the input subsystem. Events are queued until a synchronization event is received. @@ -131,7 +132,7 @@ def write(self, etype, code, value): --------- >>> ui.write(e.EV_KEY, e.KEY_A, 1) # key A - down >>> ui.write(e.EV_KEY, e.KEY_A, 0) # key A - up - ''' + """ _uinput.write(self.fd, etype, code, value) diff --git a/evdev/eventio_async.py b/evdev/eventio_async.py index 2d3468e..e89765e 100644 --- a/evdev/eventio_async.py +++ b/evdev/eventio_async.py @@ -1,9 +1,8 @@ -# encoding: utf-8 - import asyncio import select from evdev import eventio + # needed for compatibility from evdev.eventio import EvdevError @@ -15,6 +14,7 @@ def _do_when_readable(self, callback): def ready(): loop.remove_reader(self.fileno()) callback() + loop.add_reader(self.fileno(), ready) def _set_result(self, future, cb): @@ -24,30 +24,30 @@ def _set_result(self, future, cb): future.set_exception(error) def async_read_one(self): - ''' + """ Asyncio coroutine to read and return a single input event as an instance of :class:`InputEvent `. - ''' + """ future = asyncio.Future() self._do_when_readable(lambda: self._set_result(future, self.read_one)) return future def async_read(self): - ''' + """ Asyncio coroutine to read multiple input events from device. Return a generator object that yields :class:`InputEvent ` instances. - ''' + """ future = asyncio.Future() self._do_when_readable(lambda: self._set_result(future, self.read)) return future def async_read_loop(self): - ''' + """ Return an iterator that yields input events. This iterator is compatible with the ``async for`` syntax. - ''' + """ return ReadIterator(self) def close(self): @@ -87,11 +87,13 @@ def __anext__(self): # Read from the previous batch of events. future.set_result(next(self.current_batch)) except StopIteration: + def next_batch_ready(batch): try: self.current_batch = batch.result() future.set_result(next(self.current_batch)) except Exception as e: future.set_exception(e) + self.device.async_read().add_done_callback(next_batch_ready) return future diff --git a/evdev/events.py b/evdev/events.py index af42386..104b563 100644 --- a/evdev/events.py +++ b/evdev/events.py @@ -1,6 +1,4 @@ -# encoding: utf-8 - -''' +""" This module provides the :class:`InputEvent` class, which closely resembles the ``input_event`` struct defined in ``linux/input.h``: @@ -34,7 +32,7 @@ key event at 1337197425.477835, 28 (KEY_ENTER), up >>> print(repr(key_event)) KeyEvent(InputEvent(1337197425L, 477835L, 1, 28, 0L)) -''' +""" # event type descriptions have been taken mot-a-mot from: # http://www.kernel.org/doc/Documentation/input/event-codes.txt @@ -43,9 +41,9 @@ class InputEvent: - '''A generic input event.''' + """A generic input event.""" - __slots__ = 'sec', 'usec', 'type', 'code', 'value' + __slots__ = "sec", "usec", "type", "code", "value" def __init__(self, sec, usec, type, code, value): #: Time in seconds since epoch at which event occurred. @@ -64,34 +62,33 @@ def __init__(self, sec, usec, type, code, value): self.value = value def timestamp(self): - '''Return event timestamp as a float.''' + """Return event timestamp as a float.""" return self.sec + (self.usec / 1000000.0) def __str__(s): - msg = 'event at {:f}, code {:02d}, type {:02d}, val {:02d}' + msg = "event at {:f}, code {:02d}, type {:02d}, val {:02d}" return msg.format(s.timestamp(), s.code, s.type, s.value) def __repr__(s): - msg = '{}({!r}, {!r}, {!r}, {!r}, {!r})' - return msg.format(s.__class__.__name__, - s.sec, s.usec, s.type, s.code, s.value) + msg = "{}({!r}, {!r}, {!r}, {!r}, {!r})" + return msg.format(s.__class__.__name__, s.sec, s.usec, s.type, s.code, s.value) class KeyEvent: - '''An event generated by a keyboard, button or other key-like devices.''' + """An event generated by a keyboard, button or other key-like devices.""" - key_up = 0x0 + key_up = 0x0 key_down = 0x1 key_hold = 0x2 - __slots__ = 'scancode', 'keycode', 'keystate', 'event' + __slots__ = "scancode", "keycode", "keystate", "event" def __init__(self, event, allow_unknown=False): - ''' + """ The ``allow_unknown`` argument determines what to do in the event of an event code for which a key code cannot be found. If ``False`` a ``KeyError`` will be raised. If ``True`` the keycode will be set to the hex value of the event code. - ''' + """ self.scancode = event.code @@ -106,7 +103,7 @@ def __init__(self, event, allow_unknown=False): self.keycode = keys[event.code] except KeyError: if allow_unknown: - self.keycode = '0x{:02X}'.format(event.code) + self.keycode = "0x{:02X}".format(event.code) else: raise @@ -115,70 +112,69 @@ def __init__(self, event, allow_unknown=False): def __str__(self): try: - ks = ('up', 'down', 'hold')[self.keystate] + ks = ("up", "down", "hold")[self.keystate] except IndexError: - ks = 'unknown' + ks = "unknown" - msg = 'key event at {:f}, {} ({}), {}' - return msg.format(self.event.timestamp(), - self.scancode, self.keycode, ks) + msg = "key event at {:f}, {} ({}), {}" + return msg.format(self.event.timestamp(), self.scancode, self.keycode, ks) def __repr__(s): - return '{}({!r})'.format(s.__class__.__name__, s.event) + return "{}({!r})".format(s.__class__.__name__, s.event) class RelEvent: - '''A relative axis event (e.g moving the mouse 5 units to the left).''' + """A relative axis event (e.g moving the mouse 5 units to the left).""" - __slots__ = 'event' + __slots__ = "event" def __init__(self, event): #: Reference to an :class:`InputEvent` instance. self.event = event def __str__(self): - msg = 'relative axis event at {:f}, {}' + msg = "relative axis event at {:f}, {}" return msg.format(self.event.timestamp(), REL[self.event.code]) def __repr__(s): - return '{}({!r})'.format(s.__class__.__name__, s.event) + return "{}({!r})".format(s.__class__.__name__, s.event) class AbsEvent: - '''An absolute axis event (e.g the coordinates of a tap on a touchscreen).''' + """An absolute axis event (e.g the coordinates of a tap on a touchscreen).""" - __slots__ = 'event' + __slots__ = "event" def __init__(self, event): #: Reference to an :class:`InputEvent` instance. self.event = event def __str__(self): - msg = 'absolute axis event at {:f}, {}' + msg = "absolute axis event at {:f}, {}" return msg.format(self.event.timestamp(), ABS[self.event.code]) def __repr__(s): - return '{}({!r})'.format(s.__class__.__name__, s.event) + return "{}({!r})".format(s.__class__.__name__, s.event) class SynEvent: - ''' + """ A synchronization event. Used as markers to separate events. Events may be separated in time or in space, such as with the multitouch protocol. - ''' + """ - __slots__ = 'event' + __slots__ = "event" def __init__(self, event): #: Reference to an :class:`InputEvent` instance. self.event = event def __str__(self): - msg = 'synchronization event at {:f}, {}' + msg = "synchronization event at {:f}, {}" return msg.format(self.event.timestamp(), SYN[self.event.code]) def __repr__(s): - return '{}({!r})'.format(s.__class__.__name__, s.event) + return "{}({!r})".format(s.__class__.__name__, s.event) #: A mapping of event types to :class:`InputEvent` sub-classes. Used @@ -191,5 +187,4 @@ def __repr__(s): } -__all__ = ('InputEvent', 'KeyEvent', 'RelEvent', 'SynEvent', - 'AbsEvent', 'event_factory') +__all__ = ("InputEvent", "KeyEvent", "RelEvent", "SynEvent", "AbsEvent", "event_factory") diff --git a/evdev/evtest.py b/evdev/evtest.py index da6c683..063ca83 100644 --- a/evdev/evtest.py +++ b/evdev/evtest.py @@ -1,6 +1,4 @@ -# encoding: utf-8 - -''' +""" Usage: evtest [options] [, ...] Input device enumerator and event monitor. @@ -16,8 +14,7 @@ Examples: evtest /dev/input/event0 /dev/input/event1 -''' - +""" from __future__ import print_function @@ -38,9 +35,9 @@ def parseopt(): parser = optparse.OptionParser(add_help_option=False) - parser.add_option('-h', '--help', action='store_true') - parser.add_option('-g', '--grab', action='store_true') - parser.add_option('-c', '--capabilities', action='store_true') + parser.add_option("-h", "--help", action="store_true") + parser.add_option("-g", "--grab", action="store_true") + parser.add_option("-c", "--capabilities", action="store_true") return parser.parse_args() @@ -69,7 +66,7 @@ def main(): toggle_tty_echo(sys.stdin, enable=False) atexit.register(toggle_tty_echo, sys.stdin, enable=True) - print('Listening for events (press ctrl-c to exit) ...') + print("Listening for events (press ctrl-c to exit) ...") fd_to_device = {dev.fd: dev for dev in devices} while True: r, w, e = select.select(fd_to_device, [], []) @@ -79,31 +76,31 @@ def main(): print_event(event) -def select_devices(device_dir='/dev/input'): - ''' +def select_devices(device_dir="/dev/input"): + """ Select one or more devices from a list of accessible input devices. - ''' + """ def devicenum(device_path): - digits = re.findall(r'\d+$', device_path) + digits = re.findall(r"\d+$", device_path) return [int(i) for i in digits] devices = sorted(list_devices(device_dir), key=devicenum) devices = [InputDevice(path) for path in devices] if not devices: - msg = 'error: no input devices found (do you have rw permission on %s/*?)' + msg = "error: no input devices found (do you have rw permission on %s/*?)" print(msg % device_dir, file=sys.stderr) sys.exit(1) - dev_format = '{0:<3} {1.path:<20} {1.name:<35} {1.phys:<35} {1.uniq:<4}' + dev_format = "{0:<3} {1.path:<20} {1.name:<35} {1.phys:<35} {1.uniq:<4}" dev_lines = [dev_format.format(num, dev) for num, dev in enumerate(devices)] - print('ID {:<20} {:<35} {:<35} {}'.format('Device', 'Name', 'Phys', 'Uniq')) - print('-' * len(max(dev_lines, key=len))) - print('\n'.join(dev_lines)) + print("ID {:<20} {:<35} {:<35} {}".format("Device", "Name", "Phys", "Uniq")) + print("-" * len(max(dev_lines, key=len))) + print("\n".join(dev_lines)) print() - choices = input('Select devices [0-%s]: ' % (len(dev_lines) - 1)) + choices = input("Select devices [0-%s]: " % (len(dev_lines) - 1)) try: choices = choices.split() @@ -112,7 +109,7 @@ def devicenum(device_path): choices = None if not choices: - msg = 'error: invalid input - please enter one or more numbers separated by spaces' + msg = "error: invalid input - please enter one or more numbers separated by spaces" print(msg, file=sys.stderr) sys.exit(1) @@ -123,52 +120,52 @@ def print_capabilities(device): capabilities = device.capabilities(verbose=True) input_props = device.input_props(verbose=True) - print('Device name: {.name}'.format(device)) - print('Device info: {.info}'.format(device)) - print('Repeat settings: {}\n'.format(device.repeat)) + print("Device name: {.name}".format(device)) + print("Device info: {.info}".format(device)) + print("Repeat settings: {}\n".format(device.repeat)) - if ('EV_LED', ecodes.EV_LED) in capabilities: - leds = ','.join(i[0] for i in device.leds(True)) - print('Active LEDs: %s' % leds) + if ("EV_LED", ecodes.EV_LED) in capabilities: + leds = ",".join(i[0] for i in device.leds(True)) + print("Active LEDs: %s" % leds) - active_keys = ','.join(k[0] for k in device.active_keys(True)) - print('Active keys: %s\n' % active_keys) + active_keys = ",".join(k[0] for k in device.active_keys(True)) + print("Active keys: %s\n" % active_keys) if input_props: - print('Input properties:') + print("Input properties:") for type, code in input_props: - print(' %s %s' % (type, code)) + print(" %s %s" % (type, code)) print() - print('Device capabilities:') + print("Device capabilities:") for type, codes in capabilities.items(): - print(' Type {} {}:'.format(*type)) + print(" Type {} {}:".format(*type)) for code in codes: # code <- ('BTN_RIGHT', 273) or (['BTN_LEFT', 'BTN_MOUSE'], 272) if isinstance(code[1], AbsInfo): - print(' Code {:<4} {}:'.format(*code[0])) - print(' {}'.format(code[1])) + print(" Code {:<4} {}:".format(*code[0])) + print(" {}".format(code[1])) else: # Multiple names may resolve to one value. - s = ', '.join(code[0]) if isinstance(code[0], list) else code[0] - print(' Code {:<4} {}'.format(s, code[1])) - print('') + s = ", ".join(code[0]) if isinstance(code[0], list) else code[0] + print(" Code {:<4} {}".format(s, code[1])) + print("") def print_event(e): if e.type == ecodes.EV_SYN: if e.code == ecodes.SYN_MT_REPORT: - msg = 'time {:<16} +++++++++ {} ++++++++' + msg = "time {:<16} +++++++++ {} ++++++++" else: - msg = 'time {:<16} --------- {} --------' + msg = "time {:<16} --------- {} --------" print(msg.format(e.timestamp(), ecodes.SYN[e.code])) else: if e.type in ecodes.bytype: codename = ecodes.bytype[e.type][e.code] else: - codename = '?' + codename = "?" - evfmt = 'time {:<16} type {} ({}), code {:<4} ({}), value {}' + evfmt = "time {:<16} type {} ({}), code {:<4} ({}), value {}" print(evfmt.format(e.timestamp(), e.type, ecodes.EV[e.type], e.code, codename, e.value)) @@ -181,7 +178,7 @@ def toggle_tty_echo(fh, enable=True): termios.tcsetattr(fh.fileno(), termios.TCSANOW, flags) -if __name__ == '__main__': +if __name__ == "__main__": try: ret = main() except (KeyboardInterrupt, EOFError): diff --git a/evdev/ff.py b/evdev/ff.py index 0008906..edb5ff2 100644 --- a/evdev/ff.py +++ b/evdev/ff.py @@ -1,43 +1,42 @@ -# encoding: utf-8 - import ctypes from evdev import ecodes -_u8 = ctypes.c_uint8 +_u8 = ctypes.c_uint8 _u16 = ctypes.c_uint16 _u32 = ctypes.c_uint32 _s16 = ctypes.c_int16 _s32 = ctypes.c_int32 + class Replay(ctypes.Structure): - ''' + """ Defines scheduling of the force-feedback effect @length: duration of the effect @delay: delay before effect should start playing - ''' + """ _fields_ = [ - ('length', _u16), - ('delay', _u16), + ("length", _u16), + ("delay", _u16), ] class Trigger(ctypes.Structure): - ''' + """ Defines what triggers the force-feedback effect @button: number of the button triggering the effect @interval: controls how soon the effect can be re-triggered - ''' + """ _fields_ = [ - ('button', _u16), - ('interval', _u16), + ("button", _u16), + ("interval", _u16), ] class Envelope(ctypes.Structure): - ''' + """ Generic force-feedback effect envelope @attack_length: duration of the attack (ms) @attack_level: level at the beginning of the attack @@ -48,46 +47,46 @@ class Envelope(ctypes.Structure): envelope force-feedback core will convert to positive/negative value based on polarity of the default level of the effect. Valid range for the attack and fade levels is 0x0000 - 0x7fff - ''' + """ _fields_ = [ - ('attack_length', _u16), - ('attack_level', _u16), - ('fade_length', _u16), - ('fade_level', _u16), + ("attack_length", _u16), + ("attack_level", _u16), + ("fade_length", _u16), + ("fade_level", _u16), ] class Constant(ctypes.Structure): - ''' + """ Defines parameters of a constant force-feedback effect @level: strength of the effect; may be negative @envelope: envelope data - ''' + """ _fields_ = [ - ('level', _s16), - ('ff_envelope', Envelope), + ("level", _s16), + ("ff_envelope", Envelope), ] class Ramp(ctypes.Structure): - ''' + """ Defines parameters of a ramp force-feedback effect @start_level: beginning strength of the effect; may be negative @end_level: final strength of the effect; may be negative @envelope: envelope data - ''' + """ _fields_ = [ - ('start_level', _s16), - ('end_level', _s16), - ('ff_envelope', Envelope), + ("start_level", _s16), + ("end_level", _s16), + ("ff_envelope", Envelope), ] class Condition(ctypes.Structure): - ''' + """ Defines a spring or friction force-feedback effect @right_saturation: maximum level when joystick moved all way to the right @left_saturation: same for the left side @@ -95,20 +94,20 @@ class Condition(ctypes.Structure): @left_coeff: same for the left side @deadband: size of the dead zone, where no force is produced @center: position of the dead zone - ''' + """ _fields_ = [ - ('right_saturation', _u16), - ('left_saturation', _u16), - ('right_coeff', _s16), - ('left_coeff', _s16), - ('deadband', _u16), - ('center', _s16), + ("right_saturation", _u16), + ("left_saturation", _u16), + ("right_coeff", _s16), + ("left_coeff", _s16), + ("deadband", _u16), + ("center", _s16), ] class Periodic(ctypes.Structure): - ''' + """ Defines parameters of a periodic force-feedback effect @waveform: kind of the effect (wave) @period: period of the wave (ms) @@ -118,71 +117,74 @@ class Periodic(ctypes.Structure): @envelope: envelope data @custom_len: number of samples (FF_CUSTOM only) @custom_data: buffer of samples (FF_CUSTOM only) - ''' + """ _fields_ = [ - ('waveform', _u16), - ('period', _u16), - ('magnitude', _s16), - ('offset', _s16), - ('phase', _u16), - ('envelope', Envelope), - ('custom_len', _u32), - ('custom_data', ctypes.POINTER(_s16)), + ("waveform", _u16), + ("period", _u16), + ("magnitude", _s16), + ("offset", _s16), + ("phase", _u16), + ("envelope", Envelope), + ("custom_len", _u32), + ("custom_data", ctypes.POINTER(_s16)), ] class Rumble(ctypes.Structure): - ''' + """ Defines parameters of a periodic force-feedback effect @strong_magnitude: magnitude of the heavy motor @weak_magnitude: magnitude of the light one Some rumble pads have two motors of different weight. Strong_magnitude represents the magnitude of the vibration generated by the heavy one. - ''' + """ _fields_ = [ - ('strong_magnitude', _u16), - ('weak_magnitude', _u16), + ("strong_magnitude", _u16), + ("weak_magnitude", _u16), ] class EffectType(ctypes.Union): _fields_ = [ - ('ff_constant_effect', Constant), - ('ff_ramp_effect', Ramp), - ('ff_periodic_effect', Periodic), - ('ff_condition_effect', Condition * 2), # one for each axis - ('ff_rumble_effect', Rumble), + ("ff_constant_effect", Constant), + ("ff_ramp_effect", Ramp), + ("ff_periodic_effect", Periodic), + ("ff_condition_effect", Condition * 2), # one for each axis + ("ff_rumble_effect", Rumble), ] class Effect(ctypes.Structure): _fields_ = [ - ('type', _u16), - ('id', _s16), - ('direction', _u16), - ('ff_trigger', Trigger), - ('ff_replay', Replay), - ('u', EffectType) + ("type", _u16), + ("id", _s16), + ("direction", _u16), + ("ff_trigger", Trigger), + ("ff_replay", Replay), + ("u", EffectType), ] + class UInputUpload(ctypes.Structure): _fields_ = [ - ('request_id', _u32), - ('retval', _s32), - ('effect', Effect), - ('old', Effect), + ("request_id", _u32), + ("retval", _s32), + ("effect", Effect), + ("old", Effect), ] + class UInputErase(ctypes.Structure): _fields_ = [ - ('request_id', _u32), - ('retval', _s32), - ('effect_id', _u32), + ("request_id", _u32), + ("retval", _s32), + ("effect_id", _u32), ] + # ff_types = { # ecodes.FF_CONSTANT, # ecodes.FF_PERIODIC, diff --git a/evdev/genecodes.py b/evdev/genecodes.py index 5b70154..f27104e 100644 --- a/evdev/genecodes.py +++ b/evdev/genecodes.py @@ -1,35 +1,34 @@ -# -*- coding: utf-8; -*- - -''' +""" Generate a Python extension module with the constants defined in linux/input.h. -''' +""" from __future__ import print_function import os, sys, re -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- # The default header file locations to try. headers = [ - '/usr/include/linux/input.h', - '/usr/include/linux/input-event-codes.h', - '/usr/include/linux/uinput.h', + "/usr/include/linux/input.h", + "/usr/include/linux/input-event-codes.h", + "/usr/include/linux/uinput.h", ] if sys.argv[1:]: headers = sys.argv[1:] -#----------------------------------------------------------------------------- -macro_regex = r'#define +((?:KEY|ABS|REL|SW|MSC|LED|BTN|REP|SND|ID|EV|BUS|SYN|FF|UI_FF|INPUT_PROP)_\w+)' +# ----------------------------------------------------------------------------- +macro_regex = r"#define +((?:KEY|ABS|REL|SW|MSC|LED|BTN|REP|SND|ID|EV|BUS|SYN|FF|UI_FF|INPUT_PROP)_\w+)" macro_regex = re.compile(macro_regex) -uname = list(os.uname()); del uname[1] -uname = ' '.join(uname) +uname = list(os.uname()) +del uname[1] +uname = " ".join(uname) -#----------------------------------------------------------------------------- -template = r''' +# ----------------------------------------------------------------------------- +template = r""" #include #ifdef __FreeBSD__ #include @@ -70,13 +69,15 @@ return m; } -''' +""" + def parse_header(header): for line in open(header): macro = macro_regex.search(line) if macro: - yield ' PyModule_AddIntMacro(m, %s);' % macro.group(1) + yield " PyModule_AddIntMacro(m, %s);" % macro.group(1) + all_macros = [] for header in headers: @@ -87,7 +88,7 @@ def parse_header(header): all_macros += parse_header(header) if not all_macros: - print('no input macros found in: %s' % ' '.join(headers), file=sys.stderr) + print("no input macros found in: %s" % " ".join(headers), file=sys.stderr) sys.exit(1) diff --git a/evdev/uinput.py b/evdev/uinput.py index 2c1424b..c09b4f1 100644 --- a/evdev/uinput.py +++ b/evdev/uinput.py @@ -1,5 +1,3 @@ -# encoding: utf-8 - import os import platform import re @@ -19,25 +17,31 @@ from evdev.eventio import EventIO - class UInputError(Exception): pass class UInput(EventIO): - ''' + """ A userland input device and that can inject input events into the linux input subsystem. - ''' + """ __slots__ = ( - 'name', 'vendor', 'product', 'version', 'bustype', - 'events', 'devnode', 'fd', 'device', + "name", + "vendor", + "product", + "version", + "bustype", + "events", + "devnode", + "fd", + "device", ) @classmethod def from_device(cls, *devices, filtered_types=(ecodes.EV_SYN, ecodes.EV_FF), **kwargs): - ''' + """ Create an UInput device with the capabilities of one or more input devices. @@ -51,7 +55,7 @@ def from_device(cls, *devices, filtered_types=(ecodes.EV_SYN, ecodes.EV_FF), **k **kwargs Keyword arguments to UInput constructor (i.e. name, vendor etc.). - ''' + """ device_instances = [] for dev in devices: @@ -61,8 +65,8 @@ def from_device(cls, *devices, filtered_types=(ecodes.EV_SYN, ecodes.EV_FF), **k all_capabilities = defaultdict(set) - if 'max_effects' not in kwargs: - kwargs['max_effects'] = min([dev.ff_effects_count for dev in device_instances]) + if "max_effects" not in kwargs: + kwargs["max_effects"] = min([dev.ff_effects_count for dev in device_instances]) # Merge the capabilities of all devices into one dictionary. for dev in device_instances: @@ -75,13 +79,20 @@ def from_device(cls, *devices, filtered_types=(ecodes.EV_SYN, ecodes.EV_FF), **k return cls(events=all_capabilities, **kwargs) - def __init__(self, - events=None, - name='py-evdev-uinput', - vendor=0x1, product=0x1, version=0x1, bustype=0x3, - devnode='/dev/uinput', phys='py-evdev-uinput', input_props=None, - max_effects=ecodes.FF_MAX_EFFECTS): - ''' + def __init__( + self, + events=None, + name="py-evdev-uinput", + vendor=0x1, + product=0x1, + version=0x1, + bustype=0x3, + devnode="/dev/uinput", + phys="py-evdev-uinput", + input_props=None, + max_effects=ecodes.FF_MAX_EFFECTS, + ): + """ Arguments --------- events : dict @@ -117,15 +128,15 @@ def __init__(self, ---- If you do not specify any events, the uinput device will be able to inject only ``KEY_*`` and ``BTN_*`` event codes. - ''' + """ - self.name = name #: Uinput device name. - self.vendor = vendor #: Device vendor identifier. - self.product = product #: Device product identifier. - self.version = version #: Device version identifier. - self.bustype = bustype #: Device bustype - e.g. ``BUS_USB``. - self.phys = phys #: Uinput device physical path. - self.devnode = devnode #: Uinput device node - e.g. ``/dev/uinput/``. + self.name = name #: Uinput device name. + self.vendor = vendor #: Device vendor identifier. + self.product = product #: Device product identifier. + self.version = version #: Device version identifier. + self.bustype = bustype #: Device bustype - e.g. ``BUS_USB``. + self.phys = phys #: Uinput device physical path. + self.devnode = devnode #: Uinput device node - e.g. ``/dev/uinput/``. if not events: events = {ecodes.EV_KEY: ecodes.keys.keys()} @@ -164,7 +175,7 @@ def __init__(self, self.device = self._find_device(self.fd) def _prepare_events(self, events): - '''Prepare events for passing to _uinput.enable and _uinput.setup''' + """Prepare events for passing to _uinput.enable and _uinput.setup""" absinfo, prepared_events = [], [] for etype, codes in events.items(): for code in codes: @@ -185,23 +196,21 @@ def __enter__(self): return self def __exit__(self, type, value, tb): - if hasattr(self, 'fd'): + if hasattr(self, "fd"): self.close() def __repr__(self): # TODO: - v = (repr(getattr(self, i)) for i in - ('name', 'bustype', 'vendor', 'product', 'version', 'phys')) - return '{}({})'.format(self.__class__.__name__, ', '.join(v)) + v = (repr(getattr(self, i)) for i in ("name", "bustype", "vendor", "product", "version", "phys")) + return "{}({})".format(self.__class__.__name__, ", ".join(v)) def __str__(self): - msg = ('name "{}", bus "{}", vendor "{:04x}", product "{:04x}", version "{:04x}", phys "{}"\n' - 'event types: {}') + msg = 'name "{}", bus "{}", vendor "{:04x}", product "{:04x}", version "{:04x}", phys "{}"\n' "event types: {}" evtypes = [i[0] for i in self.capabilities(True).keys()] - msg = msg.format(self.name, ecodes.BUS[self.bustype], - self.vendor, self.product, - self.version, self.phys, ' '.join(evtypes)) + msg = msg.format( + self.name, ecodes.BUS[self.bustype], self.vendor, self.product, self.version, self.phys, " ".join(evtypes) + ) return msg @@ -216,18 +225,18 @@ def close(self): self.fd = -1 def syn(self): - ''' + """ Inject a ``SYN_REPORT`` event into the input subsystem. Events queued by :func:`write()` will be fired. If possible, events will be merged into an 'atomic' event. - ''' + """ _uinput.write(self.fd, ecodes.EV_SYN, ecodes.SYN_REPORT, 0) def capabilities(self, verbose=False, absinfo=True): - '''See :func:`capabilities `.''' + """See :func:`capabilities `.""" if self.device is None: - raise UInputError('input device not opened - cannot read capabilities') + raise UInputError("input device not opened - cannot read capabilities") return self.device.capabilities(verbose, absinfo) @@ -237,14 +246,14 @@ def begin_upload(self, effect_id): ret = self.dll._uinput_begin_upload(self.fd, ctypes.byref(upload)) if ret: - raise UInputError('Failed to begin uinput upload: ' + os.strerror(ret)) + raise UInputError("Failed to begin uinput upload: " + os.strerror(ret)) return upload def end_upload(self, upload): ret = self.dll._uinput_end_upload(self.fd, ctypes.byref(upload)) if ret: - raise UInputError('Failed to end uinput upload: ' + os.strerror(ret)) + raise UInputError("Failed to end uinput upload: " + os.strerror(ret)) def begin_erase(self, effect_id): erase = ff.UInputErase() @@ -252,27 +261,26 @@ def begin_erase(self, effect_id): ret = self.dll._uinput_begin_erase(self.fd, ctypes.byref(erase)) if ret: - raise UInputError('Failed to begin uinput erase: ' + os.strerror(ret)) + raise UInputError("Failed to begin uinput erase: " + os.strerror(ret)) return erase def end_erase(self, erase): ret = self.dll._uinput_end_erase(self.fd, ctypes.byref(erase)) if ret: - raise UInputError('Failed to end uinput erase: ' + os.strerror(ret)) + raise UInputError("Failed to end uinput erase: " + os.strerror(ret)) def _verify(self): - ''' + """ Verify that an uinput device exists and is readable and writable by the current process. - ''' + """ try: m = os.stat(self.devnode)[stat.ST_MODE] if not stat.S_ISCHR(m): raise except (IndexError, OSError): - msg = '"{}" does not exist or is not a character device file '\ - '- verify that the uinput module is loaded' + msg = '"{}" does not exist or is not a character device file ' "- verify that the uinput module is loaded" raise UInputError(msg.format(self.devnode)) if not os.access(self.devnode, os.W_OK): @@ -280,15 +288,15 @@ def _verify(self): raise UInputError(msg.format(self.devnode)) if len(self.name) > _uinput.maxnamelen: - msg = 'uinput device name must not be longer than {} characters' + msg = "uinput device name must not be longer than {} characters" raise UInputError(msg.format(_uinput.maxnamelen)) def _find_device(self, fd): - ''' + """ Tries to find the device node. Will delegate this task to one of several platform-specific functions. - ''' - if platform.system() == 'Linux': + """ + if platform.system() == "Linux": try: sysname = _uinput.get_sysname(fd) return self._find_device_linux(sysname) @@ -302,19 +310,19 @@ def _find_device(self, fd): return self._find_device_fallback() def _find_device_linux(self, sysname): - ''' + """ Tries to find the device node when running on Linux. - ''' + """ - syspath = f'/sys/devices/virtual/input/{sysname}' + syspath = f"/sys/devices/virtual/input/{sysname}" # The sysfs entry for event devices should contain exactly one folder # whose name matches the format "event[0-9]+". It is then assumed that # the device node in /dev/input uses the same name. - regex = re.compile('event[0-9]+') + regex = re.compile("event[0-9]+") for entry in os.listdir(syspath): if regex.fullmatch(entry): - device_path = f'/dev/input/{entry}' + device_path = f"/dev/input/{entry}" break else: # no break raise FileNotFoundError() @@ -329,11 +337,11 @@ def _find_device_linux(self, sysname): return device.InputDevice(device_path) def _find_device_fallback(self): - ''' + """ Tries to find the device node when UI_GET_SYSNAME is not available or we're running on a system sufficiently exotic that we do not know how to interpret its return value. - ''' + """ #:bug: the device node might not be immediately available time.sleep(0.1) @@ -343,8 +351,8 @@ def _find_device_fallback(self): # ends at event[0-9]+: it might return something like "/dev/input/events_all". Find # the devices that have the expected structure and extract their device number. path_number_pairs = [] - regex = re.compile('/dev/input/event([0-9]+)') - for path in util.list_devices('/dev/input/'): + regex = re.compile("/dev/input/event([0-9]+)") + for path in util.list_devices("/dev/input/"): regex_match = regex.fullmatch(path) if not regex_match: continue @@ -355,7 +363,7 @@ def _find_device_fallback(self): # are sorting by the number in the name path_number_pairs.sort(key=lambda pair: pair[1], reverse=True) - for (path, _) in path_number_pairs: + for path, _ in path_number_pairs: d = device.InputDevice(path) if d.name == self.name: return d diff --git a/evdev/util.py b/evdev/util.py index e8009f7..7209f4b 100644 --- a/evdev/util.py +++ b/evdev/util.py @@ -1,5 +1,3 @@ -# encoding: utf-8 - import re import os import stat @@ -10,17 +8,17 @@ from evdev.events import event_factory -def list_devices(input_device_dir='/dev/input'): - '''List readable character devices in ``input_device_dir``.''' +def list_devices(input_device_dir="/dev/input"): + """List readable character devices in ``input_device_dir``.""" - fns = glob.glob('{}/event*'.format(input_device_dir)) + fns = glob.glob("{}/event*".format(input_device_dir)) fns = list(filter(is_device, fns)) return fns def is_device(fn): - '''Check if ``fn`` is a readable and writable character device.''' + """Check if ``fn`` is a readable and writable character device.""" if not os.path.exists(fn): return False @@ -36,13 +34,13 @@ def is_device(fn): def categorize(event): - ''' + """ Categorize an event according to its type. The :data:`event_factory ` dictionary maps event types to sub-classes of :class:`InputEvent `. If the event cannot be categorized, it - is returned unmodified.''' + is returned unmodified.""" if event.type in event_factory: return event_factory[event.type](event) @@ -50,8 +48,8 @@ def categorize(event): return event -def resolve_ecodes_dict(typecodemap, unknown='?'): - ''' +def resolve_ecodes_dict(typecodemap, unknown="?"): + """ Resolve event codes and types to their verbose names. :param typecodemap: mapping of event types to lists of event codes. @@ -70,7 +68,7 @@ def resolve_ecodes_dict(typecodemap, unknown='?'): >>> resolve_ecodes_dict({ 3: [(0, AbsInfo(...))] }) { ('EV_ABS', 3L): [(('ABS_X', 0L), AbsInfo(...))] } - ''' + """ for etype, codes in typecodemap.items(): type_name = ecodes.EV[etype] @@ -79,21 +77,21 @@ def resolve_ecodes_dict(typecodemap, unknown='?'): if etype == ecodes.EV_KEY: ecode_dict = ecodes.keys else: - ecode_dict = getattr(ecodes, type_name.split('_')[-1]) + ecode_dict = getattr(ecodes, type_name.split("_")[-1]) resolved = resolve_ecodes(ecode_dict, codes, unknown) yield (type_name, etype), resolved -def resolve_ecodes(ecode_dict, ecode_list, unknown='?'): - ''' +def resolve_ecodes(ecode_dict, ecode_list, unknown="?"): + """ Resolve event codes and types to their verbose names. Example ------- >>> resolve_ecodes(ecodes.BTN, [272, 273, 274]) [(['BTN_LEFT', 'BTN_MOUSE'], 272), ('BTN_RIGHT', 273), ('BTN_MIDDLE', 274)] - ''' + """ res = [] for ecode in ecode_list: # elements with AbsInfo(), eg { 3 : [(0, AbsInfo(...)), (1, AbsInfo(...))] } @@ -115,7 +113,7 @@ def resolve_ecodes(ecode_dict, ecode_list, unknown='?'): def find_ecodes_by_regex(regex): - ''' + """ Find ecodes matching a regex and return a mapping of event type to event codes. regex can be a pattern string or a compiled regular expression object. @@ -130,7 +128,7 @@ def find_ecodes_by_regex(regex): ('EV_KEY', 1): [('KEY_BREAK', 411)], ('EV_ABS', 3): [('ABS_BRAKE', 10)] } - ''' + """ regex = re.compile(regex) # re.compile is idempotent result = collections.defaultdict(list) @@ -146,4 +144,4 @@ def find_ecodes_by_regex(regex): return dict(result) -__all__ = ('list_devices', 'is_device', 'categorize', 'resolve_ecodes', 'resolve_ecodes_dict', 'find_ecodes_by_regex') +__all__ = ("list_devices", "is_device", "categorize", "resolve_ecodes", "resolve_ecodes_dict", "find_ecodes_by_regex") diff --git a/examples/udev-example.py b/examples/udev-example.py index 12a617c..8e827f6 100755 --- a/examples/udev-example.py +++ b/examples/udev-example.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 -''' +""" This is an example of using pyudev[1] alongside evdev. [1]: https://pyudev.readthedocs.org/ -''' +""" import functools import pyudev @@ -13,7 +13,7 @@ context = pyudev.Context() monitor = pyudev.Monitor.from_netlink(context) -monitor.filter_by(subsystem='input') +monitor.filter_by(subsystem="input") monitor.start() fds = {monitor.fileno(): monitor} @@ -32,16 +32,16 @@ break # find the device we're interested in and add it to fds - for name in (i['NAME'] for i in udev.ancestors if 'NAME' in i): + for name in (i["NAME"] for i in udev.ancestors if "NAME" in i): # I used a virtual input device for this test - you # should adapt this to your needs - if u'py-evdev-uinput' in name: - if udev.action == u'add': - print('Device added: %s' % udev) + if "py-evdev-uinput" in name: + if udev.action == "add": + print("Device added: %s" % udev) fds[dev.fd] = InputDevice(udev.device_node) break - if udev.action == u'remove': - print('Device removed: %s' % udev) + if udev.action == "remove": + print("Device removed: %s" % udev) def helper(): global fds diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6dbd43f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[tool.ruff] +line-length = 120 diff --git a/setup.py b/setup.py index a9f45ef..30130c6 100755 --- a/setup.py +++ b/setup.py @@ -6,62 +6,59 @@ from pathlib import Path -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- from setuptools import setup, Extension, Command from setuptools.command import build_ext as _build_ext -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- curdir = Path(__file__).resolve().parent -ecodes_path = curdir / 'evdev/ecodes.c' +ecodes_path = curdir / "evdev/ecodes.c" -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- classifiers = [ - 'Development Status :: 5 - Production/Stable', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Operating System :: POSIX :: Linux', - 'Intended Audience :: Developers', - 'Topic :: Software Development :: Libraries', - 'License :: OSI Approved :: BSD License', - 'Programming Language :: Python :: Implementation :: CPython', + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Operating System :: POSIX :: Linux", + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python :: Implementation :: CPython", ] -#----------------------------------------------------------------------------- -cflags = ['-std=c99', '-Wno-error=declaration-after-statement'] -input_c = Extension('evdev._input', sources=['evdev/input.c'], extra_compile_args=cflags) -uinput_c = Extension('evdev._uinput', sources=['evdev/uinput.c'], extra_compile_args=cflags) -ecodes_c = Extension('evdev._ecodes', sources=['evdev/ecodes.c'], extra_compile_args=cflags) +# ----------------------------------------------------------------------------- +cflags = ["-std=c99", "-Wno-error=declaration-after-statement"] +input_c = Extension("evdev._input", sources=["evdev/input.c"], extra_compile_args=cflags) +uinput_c = Extension("evdev._uinput", sources=["evdev/uinput.c"], extra_compile_args=cflags) +ecodes_c = Extension("evdev._ecodes", sources=["evdev/ecodes.c"], extra_compile_args=cflags) -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- kw = { - 'name': 'evdev', - 'version': '1.6.1', - - 'description': 'Bindings to the Linux input handling subsystem', - 'long_description': (curdir / 'README.rst').read_text(), - - 'author': 'Georgi Valkov', - 'author_email': 'georgi.t.valkov@gmail.com', - 'license': 'Revised BSD License', - 'keywords': 'evdev input uinput', - 'url': 'https://github.com/gvalkov/python-evdev', - 'classifiers': classifiers, - - 'packages': ['evdev'], - 'ext_modules': [input_c, uinput_c, ecodes_c], - 'include_package_data': False, - 'zip_safe': True, - 'cmdclass': {}, + "name": "evdev", + "version": "1.6.1", + "description": "Bindings to the Linux input handling subsystem", + "long_description": (curdir / "README.rst").read_text(), + "author": "Georgi Valkov", + "author_email": "georgi.t.valkov@gmail.com", + "license": "Revised BSD License", + "keywords": "evdev input uinput", + "url": "https://github.com/gvalkov/python-evdev", + "classifiers": classifiers, + "packages": ["evdev"], + "ext_modules": [input_c, uinput_c, ecodes_c], + "include_package_data": False, + "zip_safe": True, + "cmdclass": {}, } -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def create_ecodes(headers=None): if not headers: include_paths = set() @@ -75,7 +72,7 @@ def create_ecodes(headers=None): headers = [header for header in headers if os.path.isfile(header)] if not headers: - msg = '''\ + msg = """\ The 'linux/input.h' and 'linux/input-event-codes.h' include files are missing. You will have to install the kernel header files in order to continue: @@ -94,25 +91,25 @@ def create_ecodes(headers=None): build_ecodes --evdev-headers path/input.h:path/input-event-codes.h \\ build_ext --include-dirs path/ \\ install - ''' + """ sys.stderr.write(textwrap.dedent(msg)) sys.exit(1) from subprocess import run - print('writing %s (using %s)' % (ecodes_path, ' '.join(headers))) - with ecodes_path.open('w') as fh: - cmd = [sys.executable, 'evdev/genecodes.py', *headers] + print("writing %s (using %s)" % (ecodes_path, " ".join(headers))) + with ecodes_path.open("w") as fh: + cmd = [sys.executable, "evdev/genecodes.py", *headers] run(cmd, check=True, stdout=fh) -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- class build_ecodes(Command): - description = 'generate ecodes.c' + description = "generate ecodes.c" user_options = [ - ('evdev-headers=', None, 'colon-separated paths to input subsystem headers'), + ("evdev-headers=", None, "colon-separated paths to input subsystem headers"), ] def initialize_options(self): @@ -120,7 +117,7 @@ def initialize_options(self): def finalize_options(self): if self.evdev_headers: - self.evdev_headers = self.evdev_headers.split(':') + self.evdev_headers = self.evdev_headers.split(":") def run(self): create_ecodes(self.evdev_headers) @@ -129,7 +126,7 @@ def run(self): class build_ext(_build_ext.build_ext): def has_ecodes(self): if ecodes_path.exists(): - print('ecodes.c already exists ... skipping build_ecodes') + print("ecodes.c already exists ... skipping build_ecodes") return not ecodes_path.exists() def run(self): @@ -137,14 +134,14 @@ def run(self): self.run_command(cmd_name) _build_ext.build_ext.run(self) - sub_commands = [('build_ecodes', has_ecodes)] + _build_ext.build_ext.sub_commands + sub_commands = [("build_ecodes", has_ecodes)] + _build_ext.build_ext.sub_commands -#----------------------------------------------------------------------------- -kw['cmdclass']['build_ext'] = build_ext -kw['cmdclass']['build_ecodes'] = build_ecodes +# ----------------------------------------------------------------------------- +kw["cmdclass"]["build_ext"] = build_ext +kw["cmdclass"]["build_ecodes"] = build_ecodes -#----------------------------------------------------------------------------- -if __name__ == '__main__': +# ----------------------------------------------------------------------------- +if __name__ == "__main__": setup(**kw) diff --git a/tests/test_ecodes.py b/tests/test_ecodes.py index b2f10c4..8f4cbbb 100644 --- a/tests/test_ecodes.py +++ b/tests/test_ecodes.py @@ -3,12 +3,14 @@ from evdev import ecodes -prefixes = 'KEY ABS REL SW MSC LED BTN REP SND ID EV BUS SYN FF_STATUS FF' +prefixes = "KEY ABS REL SW MSC LED BTN REP SND ID EV BUS SYN FF_STATUS FF" + def to_tuples(l): t = lambda x: tuple(x) if isinstance(x, list) else x return map(t, l) + def test_equality(): keys = [] for i in prefixes.split(): @@ -16,10 +18,12 @@ def test_equality(): assert set(keys) == set(ecodes.ecodes.values()) + def test_access(): - assert ecodes.KEY_A == ecodes.ecodes['KEY_A'] == ecodes.KEY_A - assert ecodes.KEY[ecodes.ecodes['KEY_A']] == 'KEY_A' - assert ecodes.REL[0] == 'REL_X' + assert ecodes.KEY_A == ecodes.ecodes["KEY_A"] == ecodes.KEY_A + assert ecodes.KEY[ecodes.ecodes["KEY_A"]] == "KEY_A" + assert ecodes.REL[0] == "REL_X" + def test_overlap(): vals_ff = set(to_tuples(ecodes.FF.values())) diff --git a/tests/test_events.py b/tests/test_events.py index d0717f2..f0f456c 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -16,6 +16,7 @@ def test_categorize(): e = events.InputEvent(1036996631, 984417, ecodes.EV_MSC, 0, 0) assert e == util.categorize(e) + def test_keyevent(): e = events.InputEvent(1036996631, 984417, ecodes.EV_KEY, ecodes.KEY_A, 2) k = events.KeyEvent(e) @@ -23,5 +24,4 @@ def test_keyevent(): assert k.keystate == events.KeyEvent.key_hold assert k.event == e assert k.scancode == ecodes.KEY_A - assert k.keycode == 'KEY_A' # :todo: - + assert k.keycode == "KEY_A" # :todo: diff --git a/tests/test_uinput.py b/tests/test_uinput.py index 21d7b4e..2bf3dc1 100644 --- a/tests/test_uinput.py +++ b/tests/test_uinput.py @@ -6,63 +6,69 @@ from evdev import uinput, ecodes, events, device, util -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- uinput_options = { - 'name' : 'test-py-evdev-uinput', - 'bustype' : ecodes.BUS_USB, - 'vendor' : 0x1100, - 'product' : 0x2200, - 'version' : 0x3300, + "name": "test-py-evdev-uinput", + "bustype": ecodes.BUS_USB, + "vendor": 0x1100, + "product": 0x2200, + "version": 0x3300, } + @fixture def c(): return uinput_options.copy() + def device_exists(bustype, vendor, product, version): - match = 'I: Bus=%04hx Vendor=%04hx Product=%04hx Version=%04hx' + match = "I: Bus=%04hx Vendor=%04hx Product=%04hx Version=%04hx" match = match % (bustype, vendor, product, version) - for line in open('/proc/bus/input/devices'): + for line in open("/proc/bus/input/devices"): if line.strip() == match: return True return False -#----------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------- def test_open(c): ui = uinput.UInput(**c) - args = (c['bustype'], c['vendor'], c['product'], c['version']) + args = (c["bustype"], c["vendor"], c["product"], c["version"]) assert device_exists(*args) ui.close() assert not device_exists(*args) + def test_open_context(c): - args = (c['bustype'], c['vendor'], c['product'], c['version']) + args = (c["bustype"], c["vendor"], c["product"], c["version"]) with uinput.UInput(**c): assert device_exists(*args) assert not device_exists(*args) + def test_maxnamelen(c): with raises(uinput.UInputError): - c['name'] = 'a' * 150 + c["name"] = "a" * 150 uinput.UInput(**c) + def test_enable_events(c): e = ecodes - c['events'] = {e.EV_KEY : [e.KEY_A, e.KEY_B, e.KEY_C]} + c["events"] = {e.EV_KEY: [e.KEY_A, e.KEY_B, e.KEY_C]} with uinput.UInput(**c) as ui: cap = ui.capabilities() assert e.EV_KEY in cap - assert sorted(cap[e.EV_KEY]) == sorted(c['events'][e.EV_KEY]) + assert sorted(cap[e.EV_KEY]) == sorted(c["events"][e.EV_KEY]) + def test_abs_values(c): e = ecodes - c['events'] = { + c["events"] = { e.EV_KEY: [e.KEY_A, e.KEY_B], - e.EV_ABS: [(e.ABS_X, (0, 255, 0, 0)), - (e.ABS_Y, device.AbsInfo(0, 255, 5, 10, 0, 0))], + e.EV_ABS: [(e.ABS_X, (0, 255, 0, 0)), (e.ABS_Y, device.AbsInfo(0, 255, 5, 10, 0, 0))], } with uinput.UInput(**c) as ui: @@ -75,11 +81,12 @@ def test_abs_values(c): c = ui.capabilities(verbose=True) abs = device.AbsInfo(value=0, min=0, max=255, fuzz=0, flat=0, resolution=0) - assert c[('EV_ABS', 3)][0] == (('ABS_X', 0), abs) + assert c[("EV_ABS", 3)][0] == (("ABS_X", 0), abs) c = ui.capabilities(verbose=False, absinfo=False) assert c[e.EV_ABS] == list((0, 1)) + def test_write(c): with uinput.UInput(**c) as ui: d = ui.device @@ -89,12 +96,12 @@ def test_write(c): r, w, x = select([d], [d], []) if w and not wrote: - ui.write(ecodes.EV_KEY, ecodes.KEY_P, 1) # KEY_P down - ui.write(ecodes.EV_KEY, ecodes.KEY_P, 1) # KEY_P down - ui.write(ecodes.EV_KEY, ecodes.KEY_P, 0) # KEY_P up - ui.write(ecodes.EV_KEY, ecodes.KEY_A, 1) # KEY_A down - ui.write(ecodes.EV_KEY, ecodes.KEY_A, 2) # KEY_A hold - ui.write(ecodes.EV_KEY, ecodes.KEY_A, 0) # KEY_P up + ui.write(ecodes.EV_KEY, ecodes.KEY_P, 1) # KEY_P down + ui.write(ecodes.EV_KEY, ecodes.KEY_P, 1) # KEY_P down + ui.write(ecodes.EV_KEY, ecodes.KEY_P, 0) # KEY_P up + ui.write(ecodes.EV_KEY, ecodes.KEY_A, 1) # KEY_A down + ui.write(ecodes.EV_KEY, ecodes.KEY_A, 2) # KEY_A hold + ui.write(ecodes.EV_KEY, ecodes.KEY_A, 0) # KEY_P up ui.syn() wrote = True diff --git a/tests/test_util.py b/tests/test_util.py index 11e338b..5a979df 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -2,20 +2,20 @@ def test_match_ecodes_a(): - res = util.find_ecodes_by_regex('KEY_ZOOM.*') + res = util.find_ecodes_by_regex("KEY_ZOOM.*") assert res == {1: [372, 418, 419, 420]} assert dict(util.resolve_ecodes_dict(res)) == { - ('EV_KEY', 1): [ - (['KEY_FULL_SCREEN', 'KEY_ZOOM'], 372), - ('KEY_ZOOMIN', 418), - ('KEY_ZOOMOUT', 419), - ('KEY_ZOOMRESET', 420) + ("EV_KEY", 1): [ + (["KEY_FULL_SCREEN", "KEY_ZOOM"], 372), + ("KEY_ZOOMIN", 418), + ("KEY_ZOOMOUT", 419), + ("KEY_ZOOMRESET", 420), ] } - res = util.find_ecodes_by_regex(r'(ABS|KEY)_BR(AKE|EAK)') + res = util.find_ecodes_by_regex(r"(ABS|KEY)_BR(AKE|EAK)") assert res == {1: [411], 3: [10]} assert dict(util.resolve_ecodes_dict(res)) == { - ('EV_KEY', 1): [('KEY_BREAK', 411)], - ('EV_ABS', 3): [('ABS_BRAKE', 10)] + ("EV_KEY", 1): [("KEY_BREAK", 411)], + ("EV_ABS", 3): [("ABS_BRAKE", 10)], } From 8bcb935cdc9a191fa89a0d5136e14acd47b8feed Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Mon, 29 Jan 2024 23:01:43 +0100 Subject: [PATCH 25/87] Remove packaging/ --- packaging/python-evdev.spec | 78 ------------------------------------- 1 file changed, 78 deletions(-) delete mode 100644 packaging/python-evdev.spec diff --git a/packaging/python-evdev.spec b/packaging/python-evdev.spec deleted file mode 100644 index a3cfb14..0000000 --- a/packaging/python-evdev.spec +++ /dev/null @@ -1,78 +0,0 @@ -Name: python-evdev -Version: 0.6.1 -Release: 1%{?dist} -Summary: Python bindings for the Linux input handling subsystem - -License: BSD -URL: https://python-evdev.readthedocs.io -Source0: https://github.com/gvalkov/%{name}/archive/v%{version}.tar.gz#/%{name}-%{version}.tar.gz - -BuildRequires: kernel-headers -BuildRequires: python2-devel -BuildRequires: python3-devel -BuildRequires: python2-setuptools -BuildRequires: python3-setuptools - - -%global _description \ -This package provides python bindings to the generic input event interface in \ -Linux. The evdev interface serves the purpose of passing events generated in \ -the kernel directly to userspace through character devices that are typically \ -located in /dev/input/. \ - \ -This package also comes with bindings to uinput, the userspace input subsystem. \ -Uinput allows userspace programs to create and handle input devices that can \ -inject events directly into the input subsystem. \ - \ -In other words, python-evdev allows you to read and write input events on Linux. \ -An event can be a key or button press, a mouse movement or a tap on a \ -touchscreen. - - -%description %{_description} - - -%package -n python2-evdev -Summary: %{summary} -%{?python_provide:%python_provide python2-evdev} -%description -n python2-evdev %{_description} - - -%package -n python3-evdev -Summary: %{summary} -%{?python_provide:%python_provide python3-evdev} -%description -n python3-evdev %{_description} - - -#------------------------------------------------------------------------------ -%prep -%autosetup - -#------------------------------------------------------------------------------ -%build -%py2_build -%py3_build - -#------------------------------------------------------------------------------ -%install -%py2_install -%py3_install - -#------------------------------------------------------------------------------ -%files -n python2-evdev -%license LICENSE -%doc README.rst -%{python2_sitearch}/evdev/ -%{python2_sitearch}/evdev-%{version}-py%{python2_version}.egg-info/ - -%files -n python3-evdev -%license LICENSE -%doc README.rst -%{python3_sitearch}/evdev/ -%{python3_sitearch}/evdev-%{version}-py%{python3_version}.egg-info/ - - -#------------------------------------------------------------------------------ -%changelog -* Sun Jun 05 2016 Georgi Valkov - 0.6.1-1 -- Initial RPM Release From c1b8cac930e33517eba286dc33b27d7cf9a3ad1a Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Mon, 29 Jan 2024 23:49:33 +0100 Subject: [PATCH 26/87] Move project metatada to pyproject.toml and simplify setup.py --- pyproject.toml | 34 +++++++++++++++++++++++ setup.py | 75 ++++++++++---------------------------------------- 2 files changed, 49 insertions(+), 60 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6dbd43f..911b8e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,36 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "evdev" +version = "1.6.1" +description = "Bindings to the Linux input handling subsystem" +keywords = ["evdev", "input", "uinput"] +readme = "README.rst" +license = {file = "LICENSE"} +requires-python = ">=3.6" +authors = [ + { name="Georgi Valkov", email="georgi.t.valkov@gmail.com" }, +] +maintainers = [ + { name="Tobi", email="proxima@sezanzeb.de" }, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python :: 3", + "Operating System :: POSIX :: Linux", + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python :: Implementation :: CPython", +] + +[project.urls] +"Homepage" = "https://github.com/gvalkov/python-evdev" + +[tool.setuptools] +packages = ["evdev"] + [tool.ruff] line-length = 120 diff --git a/setup.py b/setup.py index 30130c6..913cc6f 100755 --- a/setup.py +++ b/setup.py @@ -1,64 +1,16 @@ -#!/usr/bin/env python - import os import sys import textwrap from pathlib import Path - -# ----------------------------------------------------------------------------- from setuptools import setup, Extension, Command from setuptools.command import build_ext as _build_ext -# ----------------------------------------------------------------------------- curdir = Path(__file__).resolve().parent ecodes_path = curdir / "evdev/ecodes.c" -# ----------------------------------------------------------------------------- -classifiers = [ - "Development Status :: 5 - Production/Stable", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Operating System :: POSIX :: Linux", - "Intended Audience :: Developers", - "Topic :: Software Development :: Libraries", - "License :: OSI Approved :: BSD License", - "Programming Language :: Python :: Implementation :: CPython", -] - -# ----------------------------------------------------------------------------- -cflags = ["-std=c99", "-Wno-error=declaration-after-statement"] -input_c = Extension("evdev._input", sources=["evdev/input.c"], extra_compile_args=cflags) -uinput_c = Extension("evdev._uinput", sources=["evdev/uinput.c"], extra_compile_args=cflags) -ecodes_c = Extension("evdev._ecodes", sources=["evdev/ecodes.c"], extra_compile_args=cflags) - -# ----------------------------------------------------------------------------- -kw = { - "name": "evdev", - "version": "1.6.1", - "description": "Bindings to the Linux input handling subsystem", - "long_description": (curdir / "README.rst").read_text(), - "author": "Georgi Valkov", - "author_email": "georgi.t.valkov@gmail.com", - "license": "Revised BSD License", - "keywords": "evdev input uinput", - "url": "https://github.com/gvalkov/python-evdev", - "classifiers": classifiers, - "packages": ["evdev"], - "ext_modules": [input_c, uinput_c, ecodes_c], - "include_package_data": False, - "zip_safe": True, - "cmdclass": {}, -} - - -# ----------------------------------------------------------------------------- + def create_ecodes(headers=None): if not headers: include_paths = set() @@ -77,7 +29,7 @@ def create_ecodes(headers=None): are missing. You will have to install the kernel header files in order to continue: - yum install kernel-headers-$(uname -r) + dnf install kernel-headers-$(uname -r) apt-get install linux-headers-$(uname -r) emerge sys-kernel/linux-headers pacman -S kernel-headers @@ -89,7 +41,7 @@ def create_ecodes(headers=None): python setup.py \\ build \\ build_ecodes --evdev-headers path/input.h:path/input-event-codes.h \\ - build_ext --include-dirs path/ \\ + build_ext --include-dirs path/ \\ install """ @@ -104,7 +56,6 @@ def create_ecodes(headers=None): run(cmd, check=True, stdout=fh) -# ----------------------------------------------------------------------------- class build_ecodes(Command): description = "generate ecodes.c" @@ -137,11 +88,15 @@ def run(self): sub_commands = [("build_ecodes", has_ecodes)] + _build_ext.build_ext.sub_commands -# ----------------------------------------------------------------------------- -kw["cmdclass"]["build_ext"] = build_ext -kw["cmdclass"]["build_ecodes"] = build_ecodes - - -# ----------------------------------------------------------------------------- -if __name__ == "__main__": - setup(**kw) +cflags = ["-std=c99", "-Wno-error=declaration-after-statement"] +setup( + ext_modules=[ + Extension("evdev._input", sources=["evdev/input.c"], extra_compile_args=cflags), + Extension("evdev._uinput", sources=["evdev/uinput.c"], extra_compile_args=cflags), + Extension("evdev._ecodes", sources=["evdev/ecodes.c"], extra_compile_args=cflags), + ], + cmdclass={ + "build_ext": build_ext, + "build_ecodes": build_ecodes, + }, +) From 8c8014f78ceea2585a9092aedea5c4f528ec7ee8 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Mon, 29 Jan 2024 23:54:37 +0100 Subject: [PATCH 27/87] Cleaner CPATH and C_INC_PATH handling --- setup.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 913cc6f..a776130 100755 --- a/setup.py +++ b/setup.py @@ -14,10 +14,14 @@ def create_ecodes(headers=None): if not headers: include_paths = set() - if os.environ.get("CPATH", "").strip() != "": - include_paths.update(os.environ["CPATH"].split(":")) - if os.environ.get("C_INCLUDE_PATH", "").strip() != "": - include_paths.update(os.environ["C_INCLUDE_PATH"].split(":")) + cpath = os.environ.get("CPATH", "").strip() + c_inc_path = os.environ.get("C_INCLUDE_PATH", "").strip() + + if cpath: + include_paths.update(cpath.split(":")) + if c_inc_path: + include_paths.update(c_inc_path.split(":")) + include_paths.add("/usr/include") files = ["linux/input.h", "linux/input-event-codes.h", "linux/uinput.h"] headers = [os.path.join(path, file) for path in include_paths for file in files] From 67f4902dc5d1ace4365cb6ed5839989528929b77 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 18 Feb 2024 14:44:14 +0100 Subject: [PATCH 28/87] Test install --- .github/workflows/install.yaml | 29 +++++++++++++++++++++++++++++ requirements-dev.txt | 1 + 2 files changed, 30 insertions(+) create mode 100644 .github/workflows/install.yaml diff --git a/.github/workflows/install.yaml b/.github/workflows/install.yaml new file mode 100644 index 0000000..7d965b2 --- /dev/null +++ b/.github/workflows/install.yaml @@ -0,0 +1,29 @@ +name: Test install + +on: + - push + - pull_request + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + include: + - os: ubuntu-latest + python-version: "3.7" + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install python-evdev + run: | + python -m pip install -v . + (cd /tmp && python -c "import evdev.ecodes; print(evdev.ecodes)") diff --git a/requirements-dev.txt b/requirements-dev.txt index c7eb2f2..d8aecc7 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,3 +4,4 @@ sphinx-copybutton ~= 0.5.0 bump2version sphinx_rtd_theme twine +ruff From 8ee96b0cbf369dee5772bd8a6b32c4c30eb20210 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 18 Feb 2024 17:01:42 +0100 Subject: [PATCH 29/87] Minor ruff lint fixes --- docs/conf.py | 3 ++- evdev/device.py | 4 ++-- pyproject.toml | 3 +++ requirements-dev.txt | 11 +++++------ tests/test_ecodes.py | 6 +++--- 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index d1dc3fd..875ff1d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- -import sys, os +import os +import sys import sphinx_rtd_theme # Check if readthedocs is building us diff --git a/evdev/device.py b/evdev/device.py index 0d2d1d9..cde168e 100644 --- a/evdev/device.py +++ b/evdev/device.py @@ -159,8 +159,8 @@ def __del__(self): def _capabilities(self, absinfo=True): res = {} - for etype, ecodes in self._rawcapabilities.items(): - for code in ecodes: + for etype, _ecodes in self._rawcapabilities.items(): + for code in _ecodes: l = res.setdefault(etype, []) if isinstance(code, tuple): if absinfo: diff --git a/pyproject.toml b/pyproject.toml index 911b8e8..e1115d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,3 +34,6 @@ packages = ["evdev"] [tool.ruff] line-length = 120 + +[tool.ruff.lint] +ignore = ["E265", "E241", "F403", "F401", "E401", "E731"] \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt index d8aecc7..ef9693e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,7 +1,6 @@ -pytest ~= 7.1.0 -Sphinx ~= 4.4.0 -sphinx-copybutton ~= 0.5.0 -bump2version -sphinx_rtd_theme -twine +pytest +Sphinx +sphinx-copybutton ~= 0.5.2 +sphinx-rtd-theme ruff +bump-my-version ~= 0.17.4 \ No newline at end of file diff --git a/tests/test_ecodes.py b/tests/test_ecodes.py index 8f4cbbb..c810b4f 100644 --- a/tests/test_ecodes.py +++ b/tests/test_ecodes.py @@ -6,9 +6,9 @@ prefixes = "KEY ABS REL SW MSC LED BTN REP SND ID EV BUS SYN FF_STATUS FF" -def to_tuples(l): +def to_tuples(val): t = lambda x: tuple(x) if isinstance(x, list) else x - return map(t, l) + return map(t, val) def test_equality(): @@ -28,4 +28,4 @@ def test_access(): def test_overlap(): vals_ff = set(to_tuples(ecodes.FF.values())) vals_ff_status = set(to_tuples(ecodes.FF_STATUS.values())) - assert bool(vals_ff & vals_ff_status) == False + assert bool(vals_ff & vals_ff_status) is False From c6090df4ac715728bb910931c46590385687e662 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 18 Feb 2024 17:11:57 +0100 Subject: [PATCH 30/87] Move to bump-my-version from bump2version --- pyproject.toml | 14 +++++++++++++- setup.cfg | 13 ------------- 2 files changed, 13 insertions(+), 14 deletions(-) delete mode 100644 setup.cfg diff --git a/pyproject.toml b/pyproject.toml index e1115d4..f5f4a98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,4 +36,16 @@ packages = ["evdev"] line-length = 120 [tool.ruff.lint] -ignore = ["E265", "E241", "F403", "F401", "E401", "E731"] \ No newline at end of file +ignore = ["E265", "E241", "F403", "F401", "E401", "E731"] + +[tool.bumpversion] +current_version = "1.6.1" +commit = true +tag = true +allow_dirty = true + +[[tool.bumpversion.files]] +filename = "pyproject.toml" + +[[tool.bumpversion.files]] +filename = "docs/conf.py" \ No newline at end of file diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 47ca660..0000000 --- a/setup.cfg +++ /dev/null @@ -1,13 +0,0 @@ -[bumpversion] -current_version = 1.6.1 -message = Bump version: {current_version} -> {new_version} -commit = True -tag = True - -[flake8] -ignore = W191,E302,E265,E241,F403,E401 -max-line-length = 110 - -[bumpversion:file:setup.py] - -[bumpversion:file:docs/conf.py] From 7249d93186f5db47e52ce103b51219db697fc639 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 18 Feb 2024 19:42:23 +0100 Subject: [PATCH 31/87] Add twine and build --- requirements-dev.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index ef9693e..cee79d4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,4 +3,6 @@ Sphinx sphinx-copybutton ~= 0.5.2 sphinx-rtd-theme ruff -bump-my-version ~= 0.17.4 \ No newline at end of file +bump-my-version ~= 0.17.4 +build +twine From 452fc2dca407d1afdf96185318dfd093ecfa3140 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 18 Feb 2024 19:48:34 +0100 Subject: [PATCH 32/87] Simplify --- MANIFEST.in | 3 --- 1 file changed, 3 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 7066730..435d617 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,3 @@ -include README.rst -include LICENSE - # The _ecodes extension module source file needs to be generated against the # evdev headers of the running kernel. Refer to the 'build_ecodes' distutils # command in setup.py. From 0593dd422b2a62db9f390d49b618d12f069c9e1b Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 18 Feb 2024 19:57:59 +0100 Subject: [PATCH 33/87] Add .readthedocs.yaml --- .readthedocs.yaml | 15 +++++++++++++++ README.rst | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..fe05af4 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,15 @@ +# https://docs.readthedocs.io/en/stable/config-file/v2.html +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.12" + +sphinx: + configuration: docs/conf.py + +python: + install: + - requirements: requirements-dev.txt + - "." \ No newline at end of file diff --git a/README.rst b/README.rst index 207bc39..9337cf2 100644 --- a/README.rst +++ b/README.rst @@ -11,7 +11,7 @@ subsystem. *Uinput* allows userspace programs to create and handle input devices that can inject events directly into the input subsystem. -Documentation: +Documentation (stable): http://python-evdev.readthedocs.io/en/latest/ Development: From b613af4452d23d20bfd825595e552cada867767a Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 18 Feb 2024 21:44:59 +0100 Subject: [PATCH 34/87] Documentation fixes --- docs/changelog.rst | 22 +++++++++++++++++----- docs/conf.py | 7 ++----- docs/install.rst | 18 +++++++++--------- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index f656eb1..3c625f6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,20 +1,32 @@ Changelog --------- +1.7.0 (Feb 18, 2024) +==================== + +- Respect the ``CPATH/C_INCLUDE_PATH`` environment variables during install. + +- Add the uniq address to the string representation of ``InputDevice``. + +- Improved method for finding the device node corresponding to a uinput device (`#206 https://github.com/gvalkov/python-evdev/pull/206`_). + +- Repository TLC (reformatted with ruff, fixed linting warnings, moved packaging metadata to ``pyproject.toml`` etc.). + + 1.6.1 (Jan 20, 2023) -================== +==================== -- Fix generation of ``ecodes.c`` when the path to ````sys.executable`` contains spaces. +- Fix generation of ``ecodes.c`` when the path to ``sys.executable`` contains spaces. 1.6.0 (Jul 17, 2022) -================== +==================== -- Fix Python 3.11 compatibility (`#174 `_) +- Fix Python 3.11 compatibility (`#174 `_). 1.5.0 (Mar 24, 2022) -================== +==================== - Fix documentation (`#163 `_, `#160 `_). diff --git a/docs/conf.py b/docs/conf.py index 875ff1d..96c84f8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -40,6 +40,7 @@ "sphinx.ext.viewcode", "sphinx.ext.intersphinx", "sphinx.ext.napoleon", + "sphinx_rtd_theme", "sphinx_copybutton", ] @@ -111,11 +112,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -if not on_rtd: - import sphinx_rtd_theme - - html_theme = "sphinx_rtd_theme" - html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the diff --git a/docs/install.rst b/docs/install.rst index 6055f80..9906969 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -16,7 +16,7 @@ Python-evdev has been packaged for the following GNU/Linux distributions: - + - Consult the documentation of your OS package manager for installation instructions. From 81ac5bbd7dfc75117bbdaf79d15d1a3726f9702e Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Thu, 9 May 2024 00:03:01 +0200 Subject: [PATCH 44/87] Documentation fix --- docs/install.rst | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/install.rst b/docs/install.rst index 304e617..da86e24 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -18,7 +18,7 @@ From source The latest stable version of *python-evdev* can be installed from pypi_, provided that you have a compiler, pip_ and the Python and Linux development headers installed on your system. Installing these is distribution specific and -typically falls in one of the following: +typically falls into one of the following: On a Debian compatible OS: @@ -40,12 +40,21 @@ On Arch Linux and derivatives: $ pacman -S core/linux-api-headers python-pip gcc -Once all dependencies are available, you may install *python-evdev* using pip_: +Once all OS dependencies are available, you may install *python-evdev* using +pip_, preferably in a [virtualenv]_: .. code-block:: bash - $ sudo pip install evdev # available globally - $ pip install --user evdev # available to the current user + # Install globally (not recommended). + $ sudo python3 -m pip install evdev + + # Install for the current user. + $ python3 -m pip install --user evdev + + # Install in a virtual environment. + $ python3 -m venv abc + $ source abc/bin/activate + $ python3 -m pip install evdev Specifying header locations @@ -73,3 +82,4 @@ colon-separated paths. For example: .. _pip: http://pip.readthedocs.org/en/latest/installing.html .. _example: https://github.com/gvalkov/python-evdev/tree/master/examples .. _`async/await`: https://docs.python.org/3/library/asyncio-task.html +.. _virtualenv: https://docs.python.org/3/library/venv.html From 396bf0c71024b6f280bb44b473895384fb7de874 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Thu, 9 May 2024 00:44:49 +0200 Subject: [PATCH 45/87] Binary wheels --- .gitignore | 9 +++++---- docs/changelog.rst | 7 +++++++ docs/install.rst | 23 +++++++++++++++++++---- requirements-dev.txt | 1 + scripts/build-binary.sh | 15 +++++++++++++++ scripts/cibw-before.sh | 6 ++++++ setup.py | 4 ++++ 7 files changed, 57 insertions(+), 8 deletions(-) create mode 100755 scripts/build-binary.sh create mode 100755 scripts/cibw-before.sh diff --git a/.gitignore b/.gitignore index 8fc000b..329a06d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,20 +5,21 @@ develop-eggs/ dist/ build/ +wheelhouse/ dropin.cache pip-log.txt .installed.cfg .coverage tags TAGS -evdev/*.so -evdev/ecodes.c -evdev/iprops.c -docs/_build .#* __pycache__ .pytest_cache +evdev/*.so +evdev/ecodes.c +evdev/iprops.c +docs/_build evdev/_ecodes.py evdev/_input.py evdev/_uinput.py diff --git a/docs/changelog.rst b/docs/changelog.rst index b5b2251..c14026a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,13 @@ Changelog --------- +1.8.0 (Unreleased) +================== + +- Binary wheels are now provided by the `evdev-binary http://pypi.python.org/pypi/evdev-binary`_ package. + The package is compiled on manylinux_2_28 against kernel 4.18. + + 1.7.1 (May 8, 2024) ==================== diff --git a/docs/install.rst b/docs/install.rst index da86e24..f93e0b8 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -58,13 +58,13 @@ pip_, preferably in a [virtualenv]_: Specifying header locations -=========================== +--------------------------- By default, the setup script will look for the ``input.h`` and ``input-event-codes.h`` [#f1]_ header files ``/usr/include/linux``. You may use the ``--evdev-headers`` option to the ``build_ext`` setuptools -command to the location of these header files. It accepts one or more +command to the location of these header files. It accepts one or more colon-separated paths. For example: .. code-block:: bash @@ -74,12 +74,27 @@ colon-separated paths. For example: --include-dirs buildroot/ \ install # or any other command (e.g. develop, bdist, bdist_wheel) -.. [#f1] ``input-event-codes.h`` is found only in more recent kernel versions. +From a binary package +===================== +You may choose to install a precompiled version of *python-evdev* from pypi. The +`evdev-binary`_ package provides binary wheels that have been compiled on EL8 +against the 4.18.0 kernel headers. + +.. code-block:: bash + + $ python3 -m pip install evdev-binary + +While the evdev interface is stable, the precompiled version may not be fully +compatible or expose all the features of your running kernel. For best results, +it is recommended to use an OS package or to install from source. + + +.. [#f1] ``input-event-codes.h`` is found only in recent kernel versions. .. _pypi: http://pypi.python.org/pypi/evdev +.. _evdev-binary: http://pypi.python.org/pypi/evdev-binary .. _github: https://github.com/gvalkov/python-evdev .. _pip: http://pip.readthedocs.org/en/latest/installing.html .. _example: https://github.com/gvalkov/python-evdev/tree/master/examples -.. _`async/await`: https://docs.python.org/3/library/asyncio-task.html .. _virtualenv: https://docs.python.org/3/library/venv.html diff --git a/requirements-dev.txt b/requirements-dev.txt index cee79d4..96366e6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,3 +6,4 @@ ruff bump-my-version ~= 0.17.4 build twine +cibuildwheel diff --git a/scripts/build-binary.sh b/scripts/build-binary.sh new file mode 100755 index 0000000..bbdae6c --- /dev/null +++ b/scripts/build-binary.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +set -o allexport +set -o nounset + +CIBW_MANYLINUX_X86_64_IMAGE="manylinux_2_28" +CIBW_MANYLINUX_I686_IMAGE="manylinux_2_28" +CIBW_CONTAINER_ENGINE="podman" +CIBW_SKIP="cp36-*" +CIBW_ARCHS_LINUX="auto64" +CIBW_BEFORE_ALL_LINUX=./scripts/cibw-before.sh +CIBW_TEST_COMMAND="python -c 'import evdev; print(evdev)'" +CIBW_ENVIRONMENT="PACKAGE_NAME=evdev-binary" + +exec cibuildwheel \ No newline at end of file diff --git a/scripts/cibw-before.sh b/scripts/cibw-before.sh new file mode 100755 index 0000000..25220d4 --- /dev/null +++ b/scripts/cibw-before.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + + +if [ -n "$PACKAGE_NAME" ]; then + sed -i -re 's,^(name = ")evdev("),\1'${PACKAGE_NAME}'\2,' pyproject.toml +fi \ No newline at end of file diff --git a/setup.py b/setup.py index a776130..6781527 100755 --- a/setup.py +++ b/setup.py @@ -47,6 +47,10 @@ def create_ecodes(headers=None): build_ecodes --evdev-headers path/input.h:path/input-event-codes.h \\ build_ext --include-dirs path/ \\ install + + If you prefer to avoid building this package from source, then please consider + installing the `evdev-binary` package instead. Keep in mind that it may not be + fully compatible with, or support all the features of your current kernel. """ sys.stderr.write(textwrap.dedent(msg)) From 596dc52ba6b1bf1584d74f82351343b28af16080 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Thu, 9 May 2024 00:46:56 +0200 Subject: [PATCH 46/87] Fix example --- docs/tutorial.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 286a493..04ae42f 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -451,9 +451,10 @@ Injecting an FF-event into first FF-capable device found repeat_count = 1 effect_id = dev.upload_effect(effect) dev.write(ecodes.EV_FF, effect_id, repeat_count) - time.sleep(duration_ms) + time.sleep(duration_ms / 1000) dev.erase_effect(effect_id) + Forwarding force-feedback from uinput to a real device ====================================================== From 0d496bf8a5bce2d5c60147609cb79df1386dbf23 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Thu, 9 May 2024 01:20:33 +0200 Subject: [PATCH 47/87] Drop from __future__ import print_function --- evdev/evtest.py | 1 - evdev/genecodes.py | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/evdev/evtest.py b/evdev/evtest.py index 063ca83..b61f093 100644 --- a/evdev/evtest.py +++ b/evdev/evtest.py @@ -16,7 +16,6 @@ evtest /dev/input/event0 /dev/input/event1 """ -from __future__ import print_function import re import sys diff --git a/evdev/genecodes.py b/evdev/genecodes.py index f27104e..ce9939e 100644 --- a/evdev/genecodes.py +++ b/evdev/genecodes.py @@ -2,7 +2,6 @@ Generate a Python extension module with the constants defined in linux/input.h. """ -from __future__ import print_function import os, sys, re @@ -22,9 +21,9 @@ macro_regex = r"#define +((?:KEY|ABS|REL|SW|MSC|LED|BTN|REP|SND|ID|EV|BUS|SYN|FF|UI_FF|INPUT_PROP)_\w+)" macro_regex = re.compile(macro_regex) +# Uname without hostname. uname = list(os.uname()) -del uname[1] -uname = " ".join(uname) +uname = " ".join((uname[0], *uname[2:])) # ----------------------------------------------------------------------------- From 21dc595f32c88f02cb47015546d4c3af0d725d38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Paku=C5=82a?= Date: Tue, 12 Nov 2024 16:33:29 +0100 Subject: [PATCH 48/87] Move `syn()` convenience method from `InputDevice` to `EventIO` (#224) Move `syn()` method from `UInput` to `EventIO`, makes it possible to use it with `InputDevice` as well --- evdev/eventio.py | 9 +++++++++ evdev/uinput.py | 9 --------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/evdev/eventio.py b/evdev/eventio.py index 1b0e5cc..415e2e8 100644 --- a/evdev/eventio.py +++ b/evdev/eventio.py @@ -136,5 +136,14 @@ def write(self, etype, code, value): _uinput.write(self.fd, etype, code, value) + def syn(self): + """ + Inject a ``SYN_REPORT`` event into the input subsystem. Events + queued by :func:`write()` will be fired. If possible, events + will be merged into an 'atomic' event. + """ + + self.write(ecodes.EV_SYN, ecodes.SYN_REPORT, 0) + def close(self): pass diff --git a/evdev/uinput.py b/evdev/uinput.py index 476a84a..c4225d8 100644 --- a/evdev/uinput.py +++ b/evdev/uinput.py @@ -227,15 +227,6 @@ def close(self): _uinput.close(self.fd) self.fd = -1 - def syn(self): - """ - Inject a ``SYN_REPORT`` event into the input subsystem. Events - queued by :func:`write()` will be fired. If possible, events - will be merged into an 'atomic' event. - """ - - _uinput.write(self.fd, ecodes.EV_SYN, ecodes.SYN_REPORT, 0) - def capabilities(self, verbose=False, absinfo=True): """See :func:`capabilities `.""" if self.device is None: From d182b7fbd145a245214a3a1949c0f85d38b18cf5 Mon Sep 17 00:00:00 2001 From: dani-hs Date: Sun, 19 Jan 2025 05:17:33 +0100 Subject: [PATCH 49/87] Fix swapped delay and repeat (#227) --- evdev/device.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/evdev/device.py b/evdev/device.py index cde168e..758f899 100644 --- a/evdev/device.py +++ b/evdev/device.py @@ -17,7 +17,7 @@ # -------------------------------------------------------------------------- _AbsInfo = collections.namedtuple("AbsInfo", ["value", "min", "max", "fuzz", "flat", "resolution"]) -_KbdInfo = collections.namedtuple("KbdInfo", ["repeat", "delay"]) +_KbdInfo = collections.namedtuple("KbdInfo", ["delay", "repeat"]) _DeviceInfo = collections.namedtuple("DeviceInfo", ["bustype", "vendor", "product", "version"]) @@ -70,16 +70,16 @@ class KbdInfo(_KbdInfo): Attributes ---------- - repeat - Keyboard repeat rate in characters per second. - delay Amount of time that a key must be depressed before it will start to repeat (in milliseconds). + + repeat + Keyboard repeat rate in characters per second. """ def __str__(self): - return "repeat {}, delay {}".format(*self) + return "delay {}, repeat {}".format(*self) class DeviceInfo(_DeviceInfo): From b1a5bd1cdf2dd8294c18ae97f7c1902b639c99b3 Mon Sep 17 00:00:00 2001 From: Tobi <28510156+sezanzeb@users.noreply.github.com> Date: Sun, 19 Jan 2025 10:37:30 +0100 Subject: [PATCH 50/87] Add pylint -E and pytest to ci (#228) * Remove EOL python 3.7 from the ci * Add pylint -E, fix some pylint errors * Add pytest step * Fix test_abs_values * Turned RuntimeError into the desired UInputError if the device is not a character device, caused by a re-raise outside an except block * Add test for S_ISCHR False --- .github/workflows/install.yaml | 4 ++-- .github/workflows/lint.yml | 27 +++++++++++++++++++++++++++ .github/workflows/test.yml | 29 +++++++++++++++++++++++++++++ evdev/ecodes.py | 1 + evdev/eventio.py | 2 ++ evdev/events.py | 24 ++++++++++++------------ evdev/uinput.py | 2 +- tests/test_uinput.py | 20 ++++++++++++++------ 8 files changed, 88 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/install.yaml b/.github/workflows/install.yaml index 7d965b2..f959de2 100644 --- a/.github/workflows/install.yaml +++ b/.github/workflows/install.yaml @@ -11,10 +11,10 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] include: - os: ubuntu-latest - python-version: "3.7" + python-version: "3.8" steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..d499462 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,27 @@ +name: Lint + +on: + - push + - pull_request + +jobs: + pylint: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + python-version: ["3.12"] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Check for pylint errors + run: | + python -m pip install pylint setuptools + python setup.py build + python -m pylint --disable=no-member --verbose -E build/lib*/evdev diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..3ee56d3 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,29 @@ +name: Test + +on: + - push + - pull_request + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + python-version: ["3.12"] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Run pytest tests + # pip install -e . builds _ecodes and such into the evdev directory + # sudo required to write to uinputs + run: | + sudo python -m pip install pytest setuptools + sudo python -m pip install -e . + sudo python -m pytest tests diff --git a/evdev/ecodes.py b/evdev/ecodes.py index 3562368..759cfe7 100644 --- a/evdev/ecodes.py +++ b/evdev/ecodes.py @@ -1,3 +1,4 @@ +# pylint: disable=undefined-variable """ This modules exposes the integer constants defined in ``linux/input.h`` and ``linux/input-event-codes.h``. diff --git a/evdev/eventio.py b/evdev/eventio.py index 415e2e8..3335500 100644 --- a/evdev/eventio.py +++ b/evdev/eventio.py @@ -72,6 +72,7 @@ def read(self): for event in events: yield InputEvent(*event) + # pylint: disable=no-self-argument def need_write(func): """ Decorator that raises :class:`EvdevError` if there is no write access to the @@ -82,6 +83,7 @@ def need_write(func): def wrapper(*args): fd = args[0].fd if fcntl.fcntl(fd, fcntl.F_GETFL) & os.O_RDWR: + # pylint: disable=not-callable return func(*args) msg = 'no write access to device "%s"' % args[0].path raise EvdevError(msg) diff --git a/evdev/events.py b/evdev/events.py index 104b563..97f570d 100644 --- a/evdev/events.py +++ b/evdev/events.py @@ -65,13 +65,13 @@ def timestamp(self): """Return event timestamp as a float.""" return self.sec + (self.usec / 1000000.0) - def __str__(s): + def __str__(self): msg = "event at {:f}, code {:02d}, type {:02d}, val {:02d}" - return msg.format(s.timestamp(), s.code, s.type, s.value) + return msg.format(self.timestamp(), self.code, self.type, self.value) - def __repr__(s): + def __repr__(self): msg = "{}({!r}, {!r}, {!r}, {!r}, {!r})" - return msg.format(s.__class__.__name__, s.sec, s.usec, s.type, s.code, s.value) + return msg.format(self.__class__.__name__, self.sec, self.usec, self.type, self.code, self.value) class KeyEvent: @@ -119,8 +119,8 @@ def __str__(self): msg = "key event at {:f}, {} ({}), {}" return msg.format(self.event.timestamp(), self.scancode, self.keycode, ks) - def __repr__(s): - return "{}({!r})".format(s.__class__.__name__, s.event) + def __repr__(self): + return "{}({!r})".format(self.__class__.__name__, self.event) class RelEvent: @@ -136,8 +136,8 @@ def __str__(self): msg = "relative axis event at {:f}, {}" return msg.format(self.event.timestamp(), REL[self.event.code]) - def __repr__(s): - return "{}({!r})".format(s.__class__.__name__, s.event) + def __repr__(self): + return "{}({!r})".format(self.__class__.__name__, self.event) class AbsEvent: @@ -153,8 +153,8 @@ def __str__(self): msg = "absolute axis event at {:f}, {}" return msg.format(self.event.timestamp(), ABS[self.event.code]) - def __repr__(s): - return "{}({!r})".format(s.__class__.__name__, s.event) + def __repr__(self): + return "{}({!r})".format(self.__class__.__name__, self.event) class SynEvent: @@ -173,8 +173,8 @@ def __str__(self): msg = "synchronization event at {:f}, {}" return msg.format(self.event.timestamp(), SYN[self.event.code]) - def __repr__(s): - return "{}({!r})".format(s.__class__.__name__, s.event) + def __repr__(self): + return "{}({!r})".format(self.__class__.__name__, self.event) #: A mapping of event types to :class:`InputEvent` sub-classes. Used diff --git a/evdev/uinput.py b/evdev/uinput.py index c4225d8..756f83c 100644 --- a/evdev/uinput.py +++ b/evdev/uinput.py @@ -272,7 +272,7 @@ def _verify(self): try: m = os.stat(self.devnode)[stat.ST_MODE] if not stat.S_ISCHR(m): - raise + raise OSError except (IndexError, OSError): msg = '"{}" does not exist or is not a character device file ' "- verify that the uinput module is loaded" raise UInputError(msg.format(self.devnode)) diff --git a/tests/test_uinput.py b/tests/test_uinput.py index 2bf3dc1..dcd09e0 100644 --- a/tests/test_uinput.py +++ b/tests/test_uinput.py @@ -1,10 +1,12 @@ # encoding: utf-8 - +import stat from select import select -from pytest import raises, fixture +from unittest.mock import patch -from evdev import uinput, ecodes, events, device, util +import pytest +from pytest import raises, fixture +from evdev import uinput, ecodes, device, UInputError # ----------------------------------------------------------------------------- uinput_options = { @@ -66,12 +68,12 @@ def test_enable_events(c): def test_abs_values(c): e = ecodes - c["events"] = { + c = { e.EV_KEY: [e.KEY_A, e.KEY_B], - e.EV_ABS: [(e.ABS_X, (0, 255, 0, 0)), (e.ABS_Y, device.AbsInfo(0, 255, 5, 10, 0, 0))], + e.EV_ABS: [(e.ABS_X, (0, 0, 255, 0, 0)), (e.ABS_Y, device.AbsInfo(0, 0, 255, 5, 10, 0))], } - with uinput.UInput(**c) as ui: + with uinput.UInput(events=c) as ui: c = ui.capabilities() abs = device.AbsInfo(value=0, min=0, max=255, fuzz=0, flat=0, resolution=0) assert c[e.EV_ABS][0] == (0, abs) @@ -114,3 +116,9 @@ def test_write(c): assert evs[3].code == ecodes.KEY_A and evs[3].value == 2 assert evs[4].code == ecodes.KEY_A and evs[4].value == 0 break + + +@patch.object(stat, 'S_ISCHR', return_value=False) +def test_not_a_character_device(c): + with pytest.raises(UInputError): + uinput.UInput(**c) From 047bf13da1dc5acc3573f2d66c7d378011e73ec8 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Mon, 20 Jan 2025 21:32:02 +0100 Subject: [PATCH 51/87] Use relative imports and sort imports --- evdev/__init__.py | 11 +++++------ evdev/device.py | 11 +++++------ evdev/ecodes.py | 2 +- evdev/eventio.py | 8 ++++---- evdev/eventio_async.py | 4 ++-- evdev/events.py | 2 +- evdev/evtest.py | 13 ++++--------- evdev/ff.py | 2 +- evdev/uinput.py | 8 +++----- evdev/util.py | 10 +++++----- 10 files changed, 31 insertions(+), 40 deletions(-) diff --git a/evdev/__init__.py b/evdev/__init__.py index 36b330c..6aa6ef2 100644 --- a/evdev/__init__.py +++ b/evdev/__init__.py @@ -2,9 +2,8 @@ # Gather everything into a single, convenient namespace. # -------------------------------------------------------------------------- -from evdev.device import DeviceInfo, InputDevice, AbsInfo, EvdevError -from evdev.events import InputEvent, KeyEvent, RelEvent, SynEvent, AbsEvent, event_factory -from evdev.uinput import UInput, UInputError -from evdev.util import list_devices, categorize, resolve_ecodes, resolve_ecodes_dict -from evdev import ecodes -from evdev import ff +from . import ecodes, ff +from .device import AbsInfo, DeviceInfo, EvdevError, InputDevice +from .events import AbsEvent, InputEvent, KeyEvent, RelEvent, SynEvent, event_factory +from .uinput import UInput, UInputError +from .util import categorize, list_devices, resolve_ecodes, resolve_ecodes_dict diff --git a/evdev/device.py b/evdev/device.py index 758f899..7675a2d 100644 --- a/evdev/device.py +++ b/evdev/device.py @@ -1,17 +1,16 @@ # encoding: utf-8 +import collections +import contextlib import os import warnings -import contextlib -import collections -from evdev import _input, ecodes, util -from evdev.events import InputEvent +from . import _input, ecodes, util try: - from evdev.eventio_async import EventIO, EvdevError + from .eventio_async import EvdevError, EventIO except ImportError: - from evdev.eventio import EventIO, EvdevError + from .eventio import EvdevError, EventIO # -------------------------------------------------------------------------- diff --git a/evdev/ecodes.py b/evdev/ecodes.py index 759cfe7..3a6c3d0 100644 --- a/evdev/ecodes.py +++ b/evdev/ecodes.py @@ -40,8 +40,8 @@ """ from inspect import getmembers -from evdev import _ecodes +from . import _ecodes #: Mapping of names to values. ecodes = {} diff --git a/evdev/eventio.py b/evdev/eventio.py index 3335500..5478f02 100644 --- a/evdev/eventio.py +++ b/evdev/eventio.py @@ -1,10 +1,10 @@ -import os import fcntl -import select import functools +import os +import select -from evdev import _input, _uinput, ecodes, util -from evdev.events import InputEvent +from . import _input, _uinput, ecodes +from .events import InputEvent # -------------------------------------------------------------------------- diff --git a/evdev/eventio_async.py b/evdev/eventio_async.py index e89765e..fb8bcd2 100644 --- a/evdev/eventio_async.py +++ b/evdev/eventio_async.py @@ -1,10 +1,10 @@ import asyncio import select -from evdev import eventio +from . import eventio # needed for compatibility -from evdev.eventio import EvdevError +from .eventio import EvdevError class EventIO(eventio.EventIO): diff --git a/evdev/events.py b/evdev/events.py index 97f570d..9a85436 100644 --- a/evdev/events.py +++ b/evdev/events.py @@ -37,7 +37,7 @@ # event type descriptions have been taken mot-a-mot from: # http://www.kernel.org/doc/Documentation/input/event-codes.txt -from evdev.ecodes import keys, KEY, SYN, REL, ABS, EV_KEY, EV_REL, EV_ABS, EV_SYN +from .ecodes import ABS, EV_ABS, EV_KEY, EV_REL, EV_SYN, KEY, REL, SYN, keys class InputEvent: diff --git a/evdev/evtest.py b/evdev/evtest.py index b61f093..26e62ad 100644 --- a/evdev/evtest.py +++ b/evdev/evtest.py @@ -17,19 +17,14 @@ """ +import atexit +import optparse import re -import sys import select -import atexit +import sys import termios -import optparse - -try: - input = raw_input -except NameError: - pass -from evdev import ecodes, list_devices, AbsInfo, InputDevice +from . import AbsInfo, InputDevice, ecodes, list_devices def parseopt(): diff --git a/evdev/ff.py b/evdev/ff.py index edb5ff2..260c362 100644 --- a/evdev/ff.py +++ b/evdev/ff.py @@ -1,6 +1,6 @@ import ctypes -from evdev import ecodes +from . import ecodes _u8 = ctypes.c_uint8 _u16 = ctypes.c_uint16 diff --git a/evdev/uinput.py b/evdev/uinput.py index 756f83c..61de946 100644 --- a/evdev/uinput.py +++ b/evdev/uinput.py @@ -1,3 +1,4 @@ +import ctypes import os import platform import re @@ -5,11 +6,8 @@ import time from collections import defaultdict -from evdev import _uinput -from evdev import ecodes, util, device -from evdev.events import InputEvent -import evdev.ff as ff -import ctypes +from . import _uinput, device, ecodes, ff, util +from .events import InputEvent try: from evdev.eventio_async import EventIO diff --git a/evdev/util.py b/evdev/util.py index 7209f4b..59991f6 100644 --- a/evdev/util.py +++ b/evdev/util.py @@ -1,11 +1,11 @@ -import re +import collections +import glob import os +import re import stat -import glob -import collections -from evdev import ecodes -from evdev.events import event_factory +from . import ecodes +from .events import event_factory def list_devices(input_device_dir="/dev/input"): From 1818e9df54f84f6f1ed2bae87f8fe1e0c40d7682 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Mon, 20 Jan 2025 22:51:24 +0100 Subject: [PATCH 52/87] Bump required python version to 3.8 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9ba60ff..37260f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ description = "Bindings to the Linux input handling subsystem" keywords = ["evdev", "input", "uinput"] readme = "README.md" license = {file = "LICENSE"} -requires-python = ">=3.6" +requires-python = ">=3.8" authors = [ { name="Georgi Valkov", email="georgi.t.valkov@gmail.com" }, ] From 4a0efdd252cdd35a909ab46e89f7c76ca1cf4714 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Mon, 20 Jan 2025 22:48:47 +0100 Subject: [PATCH 53/87] Generate typing stubs for evdev.ecodes --- .gitignore | 2 +- MANIFEST.in | 1 + evdev/genecodes.py | 85 ++++++++++++++++++++++++++++++++++------------ pyproject.toml | 3 ++ setup.py | 21 ++++++++---- 5 files changed, 83 insertions(+), 29 deletions(-) diff --git a/.gitignore b/.gitignore index 329a06d..6548086 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,7 @@ __pycache__ evdev/*.so evdev/ecodes.c -evdev/iprops.c +evdev/ecodes.pyi docs/_build evdev/_ecodes.py evdev/_input.py diff --git a/MANIFEST.in b/MANIFEST.in index 435d617..1b5a7b6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,3 +2,4 @@ # evdev headers of the running kernel. Refer to the 'build_ecodes' distutils # command in setup.py. exclude evdev/ecodes.c +include evdev/ecodes.pyi diff --git a/evdev/genecodes.py b/evdev/genecodes.py index ce9939e..68b7dea 100644 --- a/evdev/genecodes.py +++ b/evdev/genecodes.py @@ -2,8 +2,10 @@ Generate a Python extension module with the constants defined in linux/input.h. """ -import os, sys, re - +import getopt +import os +import re +import sys # ----------------------------------------------------------------------------- # The default header file locations to try. @@ -13,8 +15,10 @@ "/usr/include/linux/uinput.h", ] -if sys.argv[1:]: - headers = sys.argv[1:] +opts, args = getopt.getopt(sys.argv[1:], "", ["ecodes", "stubs"]) +if not opts: + print("usage: genecodes.py [--ecodes|--stubs] ") + exit(2) # ----------------------------------------------------------------------------- @@ -27,7 +31,7 @@ # ----------------------------------------------------------------------------- -template = r""" +template_ecodes = r""" #include #ifdef __FreeBSD__ #include @@ -37,7 +41,8 @@ #endif /* Automatically generated by evdev.genecodes */ -/* Generated on %s */ +/* Generated on %s */ +/* Generated from %s */ #define MODULE_NAME "_ecodes" #define MODULE_HELP "linux/input.h macros" @@ -71,25 +76,63 @@ """ -def parse_header(header): - for line in open(header): - macro = macro_regex.search(line) - if macro: - yield " PyModule_AddIntMacro(m, %s);" % macro.group(1) +template_stubs = r""" +# Automatically generated by evdev.genecodes +# Generated on %s +# Generated from %s + +# pylint: skip-file + +ecodes: dict[str, int] +keys: dict[int, str|list[str]] +bytype: dict[int, dict[int, str|list[str]]] + +KEY: dict[int, str|list[str]] +ABS: dict[int, str|list[str]] +REL: dict[int, str|list[str]] +SW: dict[int, str|list[str]] +MSC: dict[int, str|list[str]] +LED: dict[int, str|list[str]] +BTN: dict[int, str|list[str]] +REP: dict[int, str|list[str]] +SND: dict[int, str|list[str]] +ID: dict[int, str|list[str]] +EV: dict[int, str|list[str]] +BUS: dict[int, str|list[str]] +SYN: dict[int, str|list[str]] +FF_STATUS: dict[int, str|list[str]] +FF_INPUT_PROP: dict[int, str|list[str]] + +%s +""" -all_macros = [] -for header in headers: - try: - fh = open(header) - except (IOError, OSError): - continue - all_macros += parse_header(header) +def parse_headers(headers=headers): + for header in headers: + try: + fh = open(header) + except (IOError, OSError): + continue + for line in fh: + macro = macro_regex.search(line) + if macro: + yield macro.group(1) + + +all_macros = list(parse_headers()) if not all_macros: print("no input macros found in: %s" % " ".join(headers), file=sys.stderr) sys.exit(1) - -macros = os.linesep.join(all_macros) -print(template % (uname, macros)) +# pylint: disable=possibly-used-before-assignment, used-before-assignment +if ("--ecodes", "") in opts: + body = (" PyModule_AddIntMacro(m, %s);" % macro for macro in all_macros) + template = template_ecodes +elif ("--stubs", "") in opts: + body = ("%s: int" % macro for macro in all_macros) + template = template_stubs + +body = os.linesep.join(body) +text = template % (uname, headers, body) +print(text.strip()) diff --git a/pyproject.toml b/pyproject.toml index 37260f1..3175a51 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,9 @@ classifiers = [ [tool.setuptools] packages = ["evdev"] +[tool.setuptools.data-files] +"data" = ["evdev/*.pyi"] + [tool.ruff] line-length = 120 diff --git a/setup.py b/setup.py index 6781527..0990554 100755 --- a/setup.py +++ b/setup.py @@ -8,7 +8,8 @@ curdir = Path(__file__).resolve().parent -ecodes_path = curdir / "evdev/ecodes.c" +ecodes_c_path = curdir / "evdev/ecodes.c" +ecodes_pyi_path = curdir / "evdev/ecodes.pyi" def create_ecodes(headers=None): @@ -58,9 +59,14 @@ def create_ecodes(headers=None): from subprocess import run - print("writing %s (using %s)" % (ecodes_path, " ".join(headers))) - with ecodes_path.open("w") as fh: - cmd = [sys.executable, "evdev/genecodes.py", *headers] + print("writing %s (using %s)" % (ecodes_c_path, " ".join(headers))) + with ecodes_c_path.open("w") as fh: + cmd = [sys.executable, "evdev/genecodes.py", "--ecodes", *headers] + run(cmd, check=True, stdout=fh) + + print("writing %s (using %s)" % (ecodes_pyi_path, " ".join(headers))) + with ecodes_pyi_path.open("w") as fh: + cmd = [sys.executable, "evdev/genecodes.py", "--stubs", *headers] run(cmd, check=True, stdout=fh) @@ -84,9 +90,10 @@ def run(self): class build_ext(_build_ext.build_ext): def has_ecodes(self): - if ecodes_path.exists(): - print("ecodes.c already exists ... skipping build_ecodes") - return not ecodes_path.exists() + if ecodes_c_path.exists() and ecodes_pyi_path.exists(): + print("ecodes.c and ecodes.pyi already exist ... skipping build_ecodes") + return False + return True def run(self): for cmd_name in self.get_sub_commands(): From abd286e3d8880d99556f37f6e349ce6cecaaf267 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Mon, 20 Jan 2025 23:45:41 +0100 Subject: [PATCH 54/87] Pylint fixes --- .github/workflows/install.yaml | 2 +- .github/workflows/lint.yml | 2 +- evdev/events.py | 1 + pyproject.toml | 9 +++++++++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/install.yaml b/.github/workflows/install.yaml index f959de2..87502ad 100644 --- a/.github/workflows/install.yaml +++ b/.github/workflows/install.yaml @@ -11,7 +11,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] include: - os: ubuntu-latest python-version: "3.8" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d499462..e293976 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -24,4 +24,4 @@ jobs: run: | python -m pip install pylint setuptools python setup.py build - python -m pylint --disable=no-member --verbose -E build/lib*/evdev + python -m pylint --verbose -E build/lib*/evdev diff --git a/evdev/events.py b/evdev/events.py index 9a85436..a4f817d 100644 --- a/evdev/events.py +++ b/evdev/events.py @@ -37,6 +37,7 @@ # event type descriptions have been taken mot-a-mot from: # http://www.kernel.org/doc/Documentation/input/event-codes.txt +# pylint: disable=no-name-in-module from .ecodes import ABS, EV_ABS, EV_KEY, EV_REL, EV_SYN, KEY, REL, SYN, keys diff --git a/pyproject.toml b/pyproject.toml index 3175a51..5f56454 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,3 +52,12 @@ filename = "pyproject.toml" [[tool.bumpversion.files]] filename = "docs/conf.py" + +[tool.pylint.'MESSAGES CONTROL'] +disable = """ + no-member, +""" + +[tool.pylint.typecheck] +generated-members = ["evdev.ecodes.*"] +ignored-modules= ["evdev._*"] From 83f9360948534efd4bec82d5db213b49afd9cf15 Mon Sep 17 00:00:00 2001 From: Tobi <28510156+sezanzeb@users.noreply.github.com> Date: Tue, 21 Jan 2025 12:11:15 +0100 Subject: [PATCH 55/87] Small character device verification cleanup (#229) --- evdev/uinput.py | 8 +++----- tests/test_uinput.py | 17 +++++++++++++++-- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/evdev/uinput.py b/evdev/uinput.py index 61de946..9567374 100644 --- a/evdev/uinput.py +++ b/evdev/uinput.py @@ -266,13 +266,11 @@ def _verify(self): Verify that an uinput device exists and is readable and writable by the current process. """ - try: m = os.stat(self.devnode)[stat.ST_MODE] - if not stat.S_ISCHR(m): - raise OSError - except (IndexError, OSError): - msg = '"{}" does not exist or is not a character device file ' "- verify that the uinput module is loaded" + assert stat.S_ISCHR(m) + except (IndexError, OSError, AssertionError): + msg = '"{}" does not exist or is not a character device file - verify that the uinput module is loaded' raise UInputError(msg.format(self.devnode)) if not os.access(self.devnode, os.W_OK): diff --git a/tests/test_uinput.py b/tests/test_uinput.py index dcd09e0..666361f 100644 --- a/tests/test_uinput.py +++ b/tests/test_uinput.py @@ -1,4 +1,5 @@ # encoding: utf-8 +import os import stat from select import select from unittest.mock import patch @@ -119,6 +120,18 @@ def test_write(c): @patch.object(stat, 'S_ISCHR', return_value=False) -def test_not_a_character_device(c): - with pytest.raises(UInputError): +def test_not_a_character_device(ischr_mock, c): + with pytest.raises(UInputError, match='not a character device file'): + uinput.UInput(**c) + +@patch.object(stat, 'S_ISCHR', return_value=True) +@patch.object(os, 'stat', side_effect=OSError()) +def test_not_a_character_device_2(stat_mock, ischr_mock, c): + with pytest.raises(UInputError, match='not a character device file'): + uinput.UInput(**c) + +@patch.object(stat, 'S_ISCHR', return_value=True) +@patch.object(os, 'stat', return_value=[]) +def test_not_a_character_device_3(stat_mock, ischr_mock, c): + with pytest.raises(UInputError, match='not a character device file'): uinput.UInput(**c) From 4ca0a8b41915650e10fbdd4114519c9183406940 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Wed, 22 Jan 2025 00:43:21 +0100 Subject: [PATCH 56/87] Generate ecodes.py at build time - The existing ecodes.py is renamed to ecodes_runtime.py. - An ecodes.py is generated at build time (in build_ext) with the genecodes_py.py script, after the extension modules are built. The script essentially does a repr() on vars(ecodes_runtime) and adds type annotations. - If something goes wrong in the process of generating ecodes.py, ecodes_runtime.py is copied to ecodes.py. - Stop generating ecodes.pyi as the generated ecodes.py is fully annotated. --- .gitignore | 1 + MANIFEST.in | 2 +- docs/changelog.rst | 11 +++ evdev/ecodes.py | 105 +------------------------ evdev/ecodes_runtime.py | 102 ++++++++++++++++++++++++ evdev/{genecodes.py => genecodes_c.py} | 0 evdev/genecodes_py.py | 53 +++++++++++++ pyproject.toml | 3 - setup.py | 30 ++++--- 9 files changed, 190 insertions(+), 117 deletions(-) create mode 100644 evdev/ecodes_runtime.py rename evdev/{genecodes.py => genecodes_c.py} (100%) create mode 100644 evdev/genecodes_py.py diff --git a/.gitignore b/.gitignore index 6548086..3e244aa 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ TAGS .#* __pycache__ .pytest_cache +.ruff_cache evdev/*.so evdev/ecodes.c diff --git a/MANIFEST.in b/MANIFEST.in index 1b5a7b6..bcbbd6c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,4 +2,4 @@ # evdev headers of the running kernel. Refer to the 'build_ecodes' distutils # command in setup.py. exclude evdev/ecodes.c -include evdev/ecodes.pyi +include evdev/ecodes.py diff --git a/docs/changelog.rst b/docs/changelog.rst index c14026a..81ff3d4 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,6 +7,17 @@ Changelog - Binary wheels are now provided by the `evdev-binary http://pypi.python.org/pypi/evdev-binary`_ package. The package is compiled on manylinux_2_28 against kernel 4.18. +- The ``evdev.ecodes`` module is now generated at install time and contains only constants. This allows type + checking and introspection of the ``evdev.ecodes`` module, without having to execute it first. The old + module is available as ``evdev.ecodes_runtime``. In case generation of the static ``ecodes.py`` fails, the + install process falls back to using ``ecodes_runtime.py`` as ``ecodes.py``. + +- Minimum Python version raised to Python 3.8. + +- Fix keyboard delay and repeat being swapped. + +- Move `syn()` convenience method from `InputDevice` to `EventIO`. + 1.7.1 (May 8, 2024) ==================== diff --git a/evdev/ecodes.py b/evdev/ecodes.py index 3a6c3d0..a19dcba 100644 --- a/evdev/ecodes.py +++ b/evdev/ecodes.py @@ -1,102 +1,5 @@ -# pylint: disable=undefined-variable -""" -This modules exposes the integer constants defined in ``linux/input.h`` and -``linux/input-event-codes.h``. +# When installed, this module is replaced by an ecodes.py generated at +# build time by genecodes_py.py (see build_ext in setup.py). -Exposed constants:: - - KEY, ABS, REL, SW, MSC, LED, BTN, REP, SND, ID, EV, - BUS, SYN, FF, FF_STATUS, INPUT_PROP - -This module also provides reverse and forward mappings of the names and values -of the above mentioned constants:: - - >>> evdev.ecodes.KEY_A - 30 - - >>> evdev.ecodes.ecodes['KEY_A'] - 30 - - >>> evdev.ecodes.KEY[30] - 'KEY_A' - - >>> evdev.ecodes.REL[0] - 'REL_X' - - >>> evdev.ecodes.EV[evdev.ecodes.EV_KEY] - 'EV_KEY' - - >>> evdev.ecodes.bytype[evdev.ecodes.EV_REL][0] - 'REL_X' - -Keep in mind that values in reverse mappings may point to one or more event -codes. For example:: - - >>> evdev.ecodes.FF[80] - ['FF_EFFECT_MIN', 'FF_RUMBLE'] - - >>> evdev.ecodes.FF[81] - 'FF_PERIODIC' -""" - -from inspect import getmembers - -from . import _ecodes - -#: Mapping of names to values. -ecodes = {} - -prefixes = "KEY ABS REL SW MSC LED BTN REP SND ID EV BUS SYN FF_STATUS FF INPUT_PROP" -prev_prefix = "" -g = globals() - -# eg. code: 'REL_Z', val: 2 -for code, val in getmembers(_ecodes): - for prefix in prefixes.split(): # eg. 'REL' - if code.startswith(prefix): - ecodes[code] = val - # FF_STATUS codes should not appear in the FF reverse mapping - if not code.startswith(prev_prefix): - d = g.setdefault(prefix, {}) - # codes that share the same value will be added to a list. eg: - # >>> ecodes.FF_STATUS - # {0: 'FF_STATUS_STOPPED', 1: ['FF_STATUS_MAX', 'FF_STATUS_PLAYING']} - if val in d: - if isinstance(d[val], list): - d[val].append(code) - else: - d[val] = [d[val], code] - else: - d[val] = code - - prev_prefix = prefix - -#: Keys are a combination of all BTN and KEY codes. -keys = {} -keys.update(BTN) -keys.update(KEY) - -# make keys safe to use for the default list of uinput device -# capabilities -del keys[_ecodes.KEY_MAX] -del keys[_ecodes.KEY_CNT] - -#: Mapping of event types to other value/name mappings. -bytype = { - _ecodes.EV_KEY: keys, - _ecodes.EV_ABS: ABS, - _ecodes.EV_REL: REL, - _ecodes.EV_SW: SW, - _ecodes.EV_MSC: MSC, - _ecodes.EV_LED: LED, - _ecodes.EV_REP: REP, - _ecodes.EV_SND: SND, - _ecodes.EV_SYN: SYN, - _ecodes.EV_FF: FF, - _ecodes.EV_FF_STATUS: FF_STATUS, -} - -from evdev._ecodes import * - -# cheaper than whitelisting in an __all__ -del code, val, prefix, getmembers, g, d, prefixes, prev_prefix +# This stub exists to make development of evdev itself more convenient. +from . ecodes_runtime import * diff --git a/evdev/ecodes_runtime.py b/evdev/ecodes_runtime.py new file mode 100644 index 0000000..3a6c3d0 --- /dev/null +++ b/evdev/ecodes_runtime.py @@ -0,0 +1,102 @@ +# pylint: disable=undefined-variable +""" +This modules exposes the integer constants defined in ``linux/input.h`` and +``linux/input-event-codes.h``. + +Exposed constants:: + + KEY, ABS, REL, SW, MSC, LED, BTN, REP, SND, ID, EV, + BUS, SYN, FF, FF_STATUS, INPUT_PROP + +This module also provides reverse and forward mappings of the names and values +of the above mentioned constants:: + + >>> evdev.ecodes.KEY_A + 30 + + >>> evdev.ecodes.ecodes['KEY_A'] + 30 + + >>> evdev.ecodes.KEY[30] + 'KEY_A' + + >>> evdev.ecodes.REL[0] + 'REL_X' + + >>> evdev.ecodes.EV[evdev.ecodes.EV_KEY] + 'EV_KEY' + + >>> evdev.ecodes.bytype[evdev.ecodes.EV_REL][0] + 'REL_X' + +Keep in mind that values in reverse mappings may point to one or more event +codes. For example:: + + >>> evdev.ecodes.FF[80] + ['FF_EFFECT_MIN', 'FF_RUMBLE'] + + >>> evdev.ecodes.FF[81] + 'FF_PERIODIC' +""" + +from inspect import getmembers + +from . import _ecodes + +#: Mapping of names to values. +ecodes = {} + +prefixes = "KEY ABS REL SW MSC LED BTN REP SND ID EV BUS SYN FF_STATUS FF INPUT_PROP" +prev_prefix = "" +g = globals() + +# eg. code: 'REL_Z', val: 2 +for code, val in getmembers(_ecodes): + for prefix in prefixes.split(): # eg. 'REL' + if code.startswith(prefix): + ecodes[code] = val + # FF_STATUS codes should not appear in the FF reverse mapping + if not code.startswith(prev_prefix): + d = g.setdefault(prefix, {}) + # codes that share the same value will be added to a list. eg: + # >>> ecodes.FF_STATUS + # {0: 'FF_STATUS_STOPPED', 1: ['FF_STATUS_MAX', 'FF_STATUS_PLAYING']} + if val in d: + if isinstance(d[val], list): + d[val].append(code) + else: + d[val] = [d[val], code] + else: + d[val] = code + + prev_prefix = prefix + +#: Keys are a combination of all BTN and KEY codes. +keys = {} +keys.update(BTN) +keys.update(KEY) + +# make keys safe to use for the default list of uinput device +# capabilities +del keys[_ecodes.KEY_MAX] +del keys[_ecodes.KEY_CNT] + +#: Mapping of event types to other value/name mappings. +bytype = { + _ecodes.EV_KEY: keys, + _ecodes.EV_ABS: ABS, + _ecodes.EV_REL: REL, + _ecodes.EV_SW: SW, + _ecodes.EV_MSC: MSC, + _ecodes.EV_LED: LED, + _ecodes.EV_REP: REP, + _ecodes.EV_SND: SND, + _ecodes.EV_SYN: SYN, + _ecodes.EV_FF: FF, + _ecodes.EV_FF_STATUS: FF_STATUS, +} + +from evdev._ecodes import * + +# cheaper than whitelisting in an __all__ +del code, val, prefix, getmembers, g, d, prefixes, prev_prefix diff --git a/evdev/genecodes.py b/evdev/genecodes_c.py similarity index 100% rename from evdev/genecodes.py rename to evdev/genecodes_c.py diff --git a/evdev/genecodes_py.py b/evdev/genecodes_py.py new file mode 100644 index 0000000..bd97553 --- /dev/null +++ b/evdev/genecodes_py.py @@ -0,0 +1,53 @@ +import sys +from unittest import mock +from pprint import PrettyPrinter + +sys.modules["evdev.ecodes"] = mock.Mock() +from evdev import ecodes_runtime as ecodes + +pprint = PrettyPrinter(indent=2, sort_dicts=True, width=120).pprint + + +print("# Automatically generated by evdev.genecodes_py") +print() +print('"""') +print(ecodes.__doc__.strip()) +print('"""') + +print() +print("from typing import Final, Dict, List, Union") +print() + +for name, value in ecodes.ecodes.items(): + print(f"{name}: Final[int] = {value}") +print() + +entries = [ + ("ecodes", "Dict[str, int]", "#: Mapping of names to values."), + ("bytype", "Dict[int, Dict[int, Union[str, List[str]]]]", "#: Mapping of event types to other value/name mappings."), + ("keys", "Dict[int, Union[str, List[str]]]", "#: Keys are a combination of all BTN and KEY codes."), + ("KEY", "Dict[int, Union[str, List[str]]]", None), + ("ABS", "Dict[int, Union[str, List[str]]]", None), + ("REL", "Dict[int, Union[str, List[str]]]", None), + ("SW", "Dict[int, Union[str, List[str]]]", None), + ("MSC", "Dict[int, Union[str, List[str]]]", None), + ("LED", "Dict[int, Union[str, List[str]]]", None), + ("BTN", "Dict[int, Union[str, List[str]]]", None), + ("REP", "Dict[int, Union[str, List[str]]]", None), + ("SND", "Dict[int, Union[str, List[str]]]", None), + ("ID", "Dict[int, Union[str, List[str]]]", None), + ("EV", "Dict[int, Union[str, List[str]]]", None), + ("BUS", "Dict[int, Union[str, List[str]]]", None), + ("SYN", "Dict[int, Union[str, List[str]]]", None), + ("FF", "Dict[int, Union[str, List[str]]]", None), + ("FF_STATUS", "Dict[int, Union[str, List[str]]]", None), + ("INPUT_PROP", "Dict[int, Union[str, List[str]]]", None) +] + +for key, annotation, doc in entries: + if doc: + print(doc) + + print(f"{key}: {annotation} = ", end="") + pprint(getattr(ecodes, key)) + print() diff --git a/pyproject.toml b/pyproject.toml index 5f56454..e7ea393 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,9 +32,6 @@ classifiers = [ [tool.setuptools] packages = ["evdev"] -[tool.setuptools.data-files] -"data" = ["evdev/*.pyi"] - [tool.ruff] line-length = 120 diff --git a/setup.py b/setup.py index 0990554..e6e0c1d 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,9 @@ import os import sys +import shutil import textwrap from pathlib import Path +from subprocess import run from setuptools import setup, Extension, Command from setuptools.command import build_ext as _build_ext @@ -9,7 +11,6 @@ curdir = Path(__file__).resolve().parent ecodes_c_path = curdir / "evdev/ecodes.c" -ecodes_pyi_path = curdir / "evdev/ecodes.pyi" def create_ecodes(headers=None): @@ -49,7 +50,7 @@ def create_ecodes(headers=None): build_ext --include-dirs path/ \\ install - If you prefer to avoid building this package from source, then please consider + If you want to avoid building this package from source, then please consider installing the `evdev-binary` package instead. Keep in mind that it may not be fully compatible with, or support all the features of your current kernel. """ @@ -57,16 +58,9 @@ def create_ecodes(headers=None): sys.stderr.write(textwrap.dedent(msg)) sys.exit(1) - from subprocess import run - print("writing %s (using %s)" % (ecodes_c_path, " ".join(headers))) with ecodes_c_path.open("w") as fh: - cmd = [sys.executable, "evdev/genecodes.py", "--ecodes", *headers] - run(cmd, check=True, stdout=fh) - - print("writing %s (using %s)" % (ecodes_pyi_path, " ".join(headers))) - with ecodes_pyi_path.open("w") as fh: - cmd = [sys.executable, "evdev/genecodes.py", "--stubs", *headers] + cmd = [sys.executable, "evdev/genecodes_c.py", "--ecodes", *headers] run(cmd, check=True, stdout=fh) @@ -90,15 +84,27 @@ def run(self): class build_ext(_build_ext.build_ext): def has_ecodes(self): - if ecodes_c_path.exists() and ecodes_pyi_path.exists(): - print("ecodes.c and ecodes.pyi already exist ... skipping build_ecodes") + if ecodes_c_path.exists(): + print("ecodes.c already exists ... skipping build_ecodes") return False return True + def generate_ecodes_py(self): + ecodes_py = Path(self.build_lib) / "evdev/ecodes.py" + print(f"writing {ecodes_py}") + with ecodes_py.open("w") as fh: + cmd = [sys.executable, "-B", "evdev/genecodes_py.py"] + res = run(cmd, env={"PYTHONPATH": self.build_lib}, stdout=fh) + + if res.returncode != 0: + print(f"failed to generate static {ecodes_py} - will use ecodes_runtime.py") + shutil.copy("evdev/ecodes_runtime.py", ecodes_py) + def run(self): for cmd_name in self.get_sub_commands(): self.run_command(cmd_name) _build_ext.build_ext.run(self) + self.generate_ecodes_py() sub_commands = [("build_ecodes", has_ecodes)] + _build_ext.build_ext.sub_commands From dfd45df12abcb6b55f3f19ec1fb100821284a089 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sat, 25 Jan 2025 17:36:10 +0100 Subject: [PATCH 57/87] ecodes mappings that point to more than one value are now tuples --- docs/changelog.rst | 7 ++++++- evdev/ecodes_runtime.py | 17 +++++++++++++---- evdev/genecodes_py.py | 38 +++++++++++++++++++------------------- 3 files changed, 38 insertions(+), 24 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 81ff3d4..15ba749 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,7 +1,7 @@ Changelog --------- -1.8.0 (Unreleased) +1.8.0 (Jan 25, 2025) ================== - Binary wheels are now provided by the `evdev-binary http://pypi.python.org/pypi/evdev-binary`_ package. @@ -12,6 +12,11 @@ Changelog module is available as ``evdev.ecodes_runtime``. In case generation of the static ``ecodes.py`` fails, the install process falls back to using ``ecodes_runtime.py`` as ``ecodes.py``. +- Reverse mappings in ``evdev.ecodes`` that point to more than one value are now tuples and not lists. For example:: + + >>> ecodes.KEY[153] + 153: ('KEY_DIRECTION', 'KEY_ROTATE_DISPLAY'), + - Minimum Python version raised to Python 3.8. - Fix keyboard delay and repeat being swapped. diff --git a/evdev/ecodes_runtime.py b/evdev/ecodes_runtime.py index 3a6c3d0..d6c8b2a 100644 --- a/evdev/ecodes_runtime.py +++ b/evdev/ecodes_runtime.py @@ -33,7 +33,7 @@ codes. For example:: >>> evdev.ecodes.FF[80] - ['FF_EFFECT_MIN', 'FF_RUMBLE'] + ('FF_EFFECT_MIN', 'FF_RUMBLE') >>> evdev.ecodes.FF[81] 'FF_PERIODIC' @@ -46,13 +46,13 @@ #: Mapping of names to values. ecodes = {} -prefixes = "KEY ABS REL SW MSC LED BTN REP SND ID EV BUS SYN FF_STATUS FF INPUT_PROP" +prefixes = "KEY ABS REL SW MSC LED BTN REP SND ID EV BUS SYN FF_STATUS FF INPUT_PROP".split() prev_prefix = "" g = globals() # eg. code: 'REL_Z', val: 2 for code, val in getmembers(_ecodes): - for prefix in prefixes.split(): # eg. 'REL' + for prefix in prefixes: # eg. 'REL' if code.startswith(prefix): ecodes[code] = val # FF_STATUS codes should not appear in the FF reverse mapping @@ -71,6 +71,15 @@ prev_prefix = prefix + +# Convert lists to tuples. +k, v = None, None +for prefix in prefixes: + for k, v in g[prefix].items(): + if isinstance(v, list): + g[prefix][k] = tuple(v) + + #: Keys are a combination of all BTN and KEY codes. keys = {} keys.update(BTN) @@ -99,4 +108,4 @@ from evdev._ecodes import * # cheaper than whitelisting in an __all__ -del code, val, prefix, getmembers, g, d, prefixes, prev_prefix +del code, val, prefix, getmembers, g, d, k, v, prefixes, prev_prefix diff --git a/evdev/genecodes_py.py b/evdev/genecodes_py.py index bd97553..1afbc34 100644 --- a/evdev/genecodes_py.py +++ b/evdev/genecodes_py.py @@ -15,7 +15,7 @@ print('"""') print() -print("from typing import Final, Dict, List, Union") +print("from typing import Final, Dict, Tuple, Union") print() for name, value in ecodes.ecodes.items(): @@ -24,24 +24,24 @@ entries = [ ("ecodes", "Dict[str, int]", "#: Mapping of names to values."), - ("bytype", "Dict[int, Dict[int, Union[str, List[str]]]]", "#: Mapping of event types to other value/name mappings."), - ("keys", "Dict[int, Union[str, List[str]]]", "#: Keys are a combination of all BTN and KEY codes."), - ("KEY", "Dict[int, Union[str, List[str]]]", None), - ("ABS", "Dict[int, Union[str, List[str]]]", None), - ("REL", "Dict[int, Union[str, List[str]]]", None), - ("SW", "Dict[int, Union[str, List[str]]]", None), - ("MSC", "Dict[int, Union[str, List[str]]]", None), - ("LED", "Dict[int, Union[str, List[str]]]", None), - ("BTN", "Dict[int, Union[str, List[str]]]", None), - ("REP", "Dict[int, Union[str, List[str]]]", None), - ("SND", "Dict[int, Union[str, List[str]]]", None), - ("ID", "Dict[int, Union[str, List[str]]]", None), - ("EV", "Dict[int, Union[str, List[str]]]", None), - ("BUS", "Dict[int, Union[str, List[str]]]", None), - ("SYN", "Dict[int, Union[str, List[str]]]", None), - ("FF", "Dict[int, Union[str, List[str]]]", None), - ("FF_STATUS", "Dict[int, Union[str, List[str]]]", None), - ("INPUT_PROP", "Dict[int, Union[str, List[str]]]", None) + ("bytype", "Dict[int, Dict[int, Union[str, Tuple[str]]]]", "#: Mapping of event types to other value/name mappings."), + ("keys", "Dict[int, Union[str, Tuple[str]]]", "#: Keys are a combination of all BTN and KEY codes."), + ("KEY", "Dict[int, Union[str, Tuple[str]]]", None), + ("ABS", "Dict[int, Union[str, Tuple[str]]]", None), + ("REL", "Dict[int, Union[str, Tuple[str]]]", None), + ("SW", "Dict[int, Union[str, Tuple[str]]]", None), + ("MSC", "Dict[int, Union[str, Tuple[str]]]", None), + ("LED", "Dict[int, Union[str, Tuple[str]]]", None), + ("BTN", "Dict[int, Union[str, Tuple[str]]]", None), + ("REP", "Dict[int, Union[str, Tuple[str]]]", None), + ("SND", "Dict[int, Union[str, Tuple[str]]]", None), + ("ID", "Dict[int, Union[str, Tuple[str]]]", None), + ("EV", "Dict[int, Union[str, Tuple[str]]]", None), + ("BUS", "Dict[int, Union[str, Tuple[str]]]", None), + ("SYN", "Dict[int, Union[str, Tuple[str]]]", None), + ("FF", "Dict[int, Union[str, Tuple[str]]]", None), + ("FF_STATUS", "Dict[int, Union[str, Tuple[str]]]", None), + ("INPUT_PROP", "Dict[int, Union[str, Tuple[str]]]", None) ] for key, annotation, doc in entries: From 2e3b843f37f79a9404da4541bbf9cf96dc5a4d57 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sat, 25 Jan 2025 17:37:33 +0100 Subject: [PATCH 58/87] =?UTF-8?q?Bump=20version:=201.7.1=20=E2=86=92=201.8?= =?UTF-8?q?.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LICENSE | 2 +- docs/conf.py | 2 +- pyproject.toml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/LICENSE b/LICENSE index 5600871..8482b07 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2012-2023 Georgi Valkov. All rights reserved. +Copyright (c) 2012-2025 Georgi Valkov. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are diff --git a/docs/conf.py b/docs/conf.py index bf03b42..7af99b9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -67,7 +67,7 @@ # built documents. # # The full version, including alpha/beta/rc tags. -release = "1.7.1" +release = "1.8.0" # The short X.Y version. version = release diff --git a/pyproject.toml b/pyproject.toml index e7ea393..e5e5d00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "evdev" -version = "1.7.1" +version = "1.8.0" description = "Bindings to the Linux input handling subsystem" keywords = ["evdev", "input", "uinput"] readme = "README.md" @@ -39,7 +39,7 @@ line-length = 120 ignore = ["E265", "E241", "F403", "F401", "E401", "E731"] [tool.bumpversion] -current_version = "1.7.1" +current_version = "1.8.0" commit = true tag = true allow_dirty = true From 27eb2ff11bb6b41fa0cfcff4f80d6c26d4b65742 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sat, 25 Jan 2025 18:04:39 +0100 Subject: [PATCH 59/87] Fix tests --- tests/test_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_util.py b/tests/test_util.py index 5a979df..7112927 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -6,7 +6,7 @@ def test_match_ecodes_a(): assert res == {1: [372, 418, 419, 420]} assert dict(util.resolve_ecodes_dict(res)) == { ("EV_KEY", 1): [ - (["KEY_FULL_SCREEN", "KEY_ZOOM"], 372), + (("KEY_FULL_SCREEN", "KEY_ZOOM"), 372), ("KEY_ZOOMIN", 418), ("KEY_ZOOMOUT", 419), ("KEY_ZOOMRESET", 420), From 4b8fa71d3c0123916138d19729e13caee03ab47f Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sat, 25 Jan 2025 21:23:11 +0100 Subject: [PATCH 60/87] Fix docs --- docs/changelog.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 15ba749..92f4e23 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,7 +4,7 @@ Changelog 1.8.0 (Jan 25, 2025) ================== -- Binary wheels are now provided by the `evdev-binary http://pypi.python.org/pypi/evdev-binary`_ package. +- Binary wheels are now provided by the `evdev-binary `_ package. The package is compiled on manylinux_2_28 against kernel 4.18. - The ``evdev.ecodes`` module is now generated at install time and contains only constants. This allows type @@ -12,16 +12,16 @@ Changelog module is available as ``evdev.ecodes_runtime``. In case generation of the static ``ecodes.py`` fails, the install process falls back to using ``ecodes_runtime.py`` as ``ecodes.py``. -- Reverse mappings in ``evdev.ecodes`` that point to more than one value are now tuples and not lists. For example:: +- Reverse mappings in ``evdev.ecodes`` that point to more than one value are now tuples instead of lists. For example:: >>> ecodes.KEY[153] - 153: ('KEY_DIRECTION', 'KEY_ROTATE_DISPLAY'), + ('KEY_DIRECTION', 'KEY_ROTATE_DISPLAY') -- Minimum Python version raised to Python 3.8. +- Raise the minimum supported Python version to 3.8. -- Fix keyboard delay and repeat being swapped. +- Fix keyboard delay and repeat being swapped (#227). -- Move `syn()` convenience method from `InputDevice` to `EventIO`. +- Move the ``syn()`` convenience method from ``InputDevice`` to ``EventIO`` (#224). 1.7.1 (May 8, 2024) @@ -41,7 +41,7 @@ Changelog - Add the uniq address to the string representation of ``InputDevice``. -- Improved method for finding the device node corresponding to a uinput device (`#206 https://github.com/gvalkov/python-evdev/pull/206`_). +- Improved method for finding the device node corresponding to a uinput device (`#206 `_). - Repository TLC (reformatted with ruff, fixed linting warnings, moved packaging metadata to ``pyproject.toml`` etc.). From 3ff9816e08be95b331ea9dadc18fc17f1e04e272 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 2 Feb 2025 14:19:12 +0100 Subject: [PATCH 61/87] Fix ecodes.c generation Header files passed to genecodes_c.py were ignored after commit 4a0efdd. --- evdev/genecodes_c.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/evdev/genecodes_c.py b/evdev/genecodes_c.py index 68b7dea..dd3ee91 100644 --- a/evdev/genecodes_c.py +++ b/evdev/genecodes_c.py @@ -20,6 +20,9 @@ print("usage: genecodes.py [--ecodes|--stubs] ") exit(2) +if args: + headers = args + # ----------------------------------------------------------------------------- macro_regex = r"#define +((?:KEY|ABS|REL|SW|MSC|LED|BTN|REP|SND|ID|EV|BUS|SYN|FF|UI_FF|INPUT_PROP)_\w+)" From 61beda72e7b101e270f914d5f1d633730e60d083 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 2 Feb 2025 17:25:16 +0100 Subject: [PATCH 62/87] Move from flat-layout to src-layout --- .gitignore | 12 ++++++------ MANIFEST.in | 4 ++-- docs/conf.py | 10 ++++------ pyproject.toml | 3 --- setup.py | 14 +++++++------- {evdev => src/evdev}/__init__.py | 0 {evdev => src/evdev}/device.py | 0 {evdev => src/evdev}/ecodes.py | 0 {evdev => src/evdev}/ecodes_runtime.py | 0 {evdev => src/evdev}/eventio.py | 0 {evdev => src/evdev}/eventio_async.py | 0 {evdev => src/evdev}/events.py | 0 {evdev => src/evdev}/evtest.py | 0 {evdev => src/evdev}/ff.py | 0 {evdev => src/evdev}/genecodes_c.py | 0 {evdev => src/evdev}/genecodes_py.py | 0 {evdev => src/evdev}/input.c | 0 {evdev => src/evdev}/uinput.c | 0 {evdev => src/evdev}/uinput.py | 0 {evdev => src/evdev}/util.py | 0 20 files changed, 19 insertions(+), 24 deletions(-) rename {evdev => src/evdev}/__init__.py (100%) rename {evdev => src/evdev}/device.py (100%) rename {evdev => src/evdev}/ecodes.py (100%) rename {evdev => src/evdev}/ecodes_runtime.py (100%) rename {evdev => src/evdev}/eventio.py (100%) rename {evdev => src/evdev}/eventio_async.py (100%) rename {evdev => src/evdev}/events.py (100%) rename {evdev => src/evdev}/evtest.py (100%) rename {evdev => src/evdev}/ff.py (100%) rename {evdev => src/evdev}/genecodes_c.py (100%) rename {evdev => src/evdev}/genecodes_py.py (100%) rename {evdev => src/evdev}/input.c (100%) rename {evdev => src/evdev}/uinput.c (100%) rename {evdev => src/evdev}/uinput.py (100%) rename {evdev => src/evdev}/util.py (100%) diff --git a/.gitignore b/.gitignore index 3e244aa..557f265 100644 --- a/.gitignore +++ b/.gitignore @@ -17,10 +17,10 @@ __pycache__ .pytest_cache .ruff_cache -evdev/*.so -evdev/ecodes.c -evdev/ecodes.pyi +src/evdev/*.so +src/evdev/ecodes.c +src/evdev/ecodes.pyi docs/_build -evdev/_ecodes.py -evdev/_input.py -evdev/_uinput.py +src/evdev/_ecodes.py +src/evdev/_input.py +src/evdev/_uinput.py diff --git a/MANIFEST.in b/MANIFEST.in index bcbbd6c..be2be3d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,5 @@ # The _ecodes extension module source file needs to be generated against the # evdev headers of the running kernel. Refer to the 'build_ecodes' distutils # command in setup.py. -exclude evdev/ecodes.c -include evdev/ecodes.py +exclude src/evdev/ecodes.c +include src/evdev/ecodes.py diff --git a/docs/conf.py b/docs/conf.py index 7af99b9..53b5206 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import os import sys import sphinx_rtd_theme @@ -13,7 +11,7 @@ # Trick autodoc into running without having built the extension modules. if on_rtd: - with open("../evdev/_ecodes.py", "w") as fh: + with open("../src/evdev/_ecodes.py", "w") as fh: fh.write( """ KEY = ABS = REL = SW = MSC = LED = REP = SND = SYN = FF = FF_STATUS = BTN_A = KEY_A = 1 @@ -22,9 +20,9 @@ KEY_MAX, KEY_CNT = 1, 2""" ) - with open("../evdev/_input.py", "w"): + with open("../src/evdev/_input.py", "w"): pass - with open("../evdev/_uinput.py", "w"): + with open("../src/evdev/_uinput.py", "w"): pass @@ -60,7 +58,7 @@ # General information about the project. project = "python-evdev" -copyright = "2012-2024, Georgi Valkov and contributors" +copyright = "2012-2025, Georgi Valkov and contributors" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/pyproject.toml b/pyproject.toml index e5e5d00..7854d91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,9 +29,6 @@ classifiers = [ [project.urls] "Homepage" = "https://github.com/gvalkov/python-evdev" -[tool.setuptools] -packages = ["evdev"] - [tool.ruff] line-length = 120 diff --git a/setup.py b/setup.py index e6e0c1d..c5ab4a0 100755 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ curdir = Path(__file__).resolve().parent -ecodes_c_path = curdir / "evdev/ecodes.c" +ecodes_c_path = curdir / "src/evdev/ecodes.c" def create_ecodes(headers=None): @@ -60,7 +60,7 @@ def create_ecodes(headers=None): print("writing %s (using %s)" % (ecodes_c_path, " ".join(headers))) with ecodes_c_path.open("w") as fh: - cmd = [sys.executable, "evdev/genecodes_c.py", "--ecodes", *headers] + cmd = [sys.executable, "src/evdev/genecodes_c.py", "--ecodes", *headers] run(cmd, check=True, stdout=fh) @@ -93,12 +93,12 @@ def generate_ecodes_py(self): ecodes_py = Path(self.build_lib) / "evdev/ecodes.py" print(f"writing {ecodes_py}") with ecodes_py.open("w") as fh: - cmd = [sys.executable, "-B", "evdev/genecodes_py.py"] + cmd = [sys.executable, "-B", "src/evdev/genecodes_py.py"] res = run(cmd, env={"PYTHONPATH": self.build_lib}, stdout=fh) if res.returncode != 0: print(f"failed to generate static {ecodes_py} - will use ecodes_runtime.py") - shutil.copy("evdev/ecodes_runtime.py", ecodes_py) + shutil.copy("src/evdev/ecodes_runtime.py", ecodes_py) def run(self): for cmd_name in self.get_sub_commands(): @@ -112,9 +112,9 @@ def run(self): cflags = ["-std=c99", "-Wno-error=declaration-after-statement"] setup( ext_modules=[ - Extension("evdev._input", sources=["evdev/input.c"], extra_compile_args=cflags), - Extension("evdev._uinput", sources=["evdev/uinput.c"], extra_compile_args=cflags), - Extension("evdev._ecodes", sources=["evdev/ecodes.c"], extra_compile_args=cflags), + Extension("evdev._input", sources=["src/evdev/input.c"], extra_compile_args=cflags), + Extension("evdev._uinput", sources=["src/evdev/uinput.c"], extra_compile_args=cflags), + Extension("evdev._ecodes", sources=["src/evdev/ecodes.c"], extra_compile_args=cflags), ], cmdclass={ "build_ext": build_ext, diff --git a/evdev/__init__.py b/src/evdev/__init__.py similarity index 100% rename from evdev/__init__.py rename to src/evdev/__init__.py diff --git a/evdev/device.py b/src/evdev/device.py similarity index 100% rename from evdev/device.py rename to src/evdev/device.py diff --git a/evdev/ecodes.py b/src/evdev/ecodes.py similarity index 100% rename from evdev/ecodes.py rename to src/evdev/ecodes.py diff --git a/evdev/ecodes_runtime.py b/src/evdev/ecodes_runtime.py similarity index 100% rename from evdev/ecodes_runtime.py rename to src/evdev/ecodes_runtime.py diff --git a/evdev/eventio.py b/src/evdev/eventio.py similarity index 100% rename from evdev/eventio.py rename to src/evdev/eventio.py diff --git a/evdev/eventio_async.py b/src/evdev/eventio_async.py similarity index 100% rename from evdev/eventio_async.py rename to src/evdev/eventio_async.py diff --git a/evdev/events.py b/src/evdev/events.py similarity index 100% rename from evdev/events.py rename to src/evdev/events.py diff --git a/evdev/evtest.py b/src/evdev/evtest.py similarity index 100% rename from evdev/evtest.py rename to src/evdev/evtest.py diff --git a/evdev/ff.py b/src/evdev/ff.py similarity index 100% rename from evdev/ff.py rename to src/evdev/ff.py diff --git a/evdev/genecodes_c.py b/src/evdev/genecodes_c.py similarity index 100% rename from evdev/genecodes_c.py rename to src/evdev/genecodes_c.py diff --git a/evdev/genecodes_py.py b/src/evdev/genecodes_py.py similarity index 100% rename from evdev/genecodes_py.py rename to src/evdev/genecodes_py.py diff --git a/evdev/input.c b/src/evdev/input.c similarity index 100% rename from evdev/input.c rename to src/evdev/input.c diff --git a/evdev/uinput.c b/src/evdev/uinput.c similarity index 100% rename from evdev/uinput.c rename to src/evdev/uinput.c diff --git a/evdev/uinput.py b/src/evdev/uinput.py similarity index 100% rename from evdev/uinput.py rename to src/evdev/uinput.py diff --git a/evdev/util.py b/src/evdev/util.py similarity index 100% rename from evdev/util.py rename to src/evdev/util.py From 64c6555101b5172d4194c7f92728c1638c613200 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 2 Feb 2025 18:12:47 +0100 Subject: [PATCH 63/87] Optimize reading of events - Construct tuple directly instead of using Py_BuildValue. - Return a tuple of tuples instead of a list of tuples. - Read argument (fd) directly instead of using PyArg_ParseTuple. --- src/evdev/input.c | 43 +++++++++++++++++-------------------------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/src/evdev/input.c b/src/evdev/input.c index 0256745..55e6808 100644 --- a/src/evdev/input.c +++ b/src/evdev/input.c @@ -46,12 +46,10 @@ int test_bit(const char* bitmask, int bit) { static PyObject * device_read(PyObject *self, PyObject *args) { - int fd; struct input_event event; // get device file descriptor (O_RDONLY|O_NONBLOCK) - if (PyArg_ParseTuple(args, "i", &fd) < 0) - return NULL; + int fd = (int)PyLong_AsLong(PyTuple_GET_ITEM(args, 0)); int n = read(fd, &event, sizeof(event)); @@ -68,12 +66,9 @@ device_read(PyObject *self, PyObject *args) PyObject* sec = PyLong_FromLong(event.input_event_sec); PyObject* usec = PyLong_FromLong(event.input_event_usec); PyObject* val = PyLong_FromLong(event.value); - PyObject* py_input_event = NULL; - - py_input_event = Py_BuildValue("OOhhO", sec, usec, event.type, event.code, val); - Py_DECREF(sec); - Py_DECREF(usec); - Py_DECREF(val); + PyObject* type = PyLong_FromLong(event.type); + PyObject* code = PyLong_FromLong(event.code); + PyObject* py_input_event = PyTuple_Pack(5, sec, usec, type, code, val); return py_input_event; } @@ -83,17 +78,16 @@ device_read(PyObject *self, PyObject *args) static PyObject * device_read_many(PyObject *self, PyObject *args) { - int fd; - // get device file descriptor (O_RDONLY|O_NONBLOCK) - int ret = PyArg_ParseTuple(args, "i", &fd); - if (!ret) return NULL; + int fd = (int)PyLong_AsLong(PyTuple_GET_ITEM(args, 0)); - PyObject* event_list = PyList_New(0); PyObject* py_input_event = NULL; + PyObject* events = NULL; PyObject* sec = NULL; PyObject* usec = NULL; PyObject* val = NULL; + PyObject* type = NULL; + PyObject* code = NULL; struct input_event event[64]; @@ -102,26 +96,24 @@ device_read_many(PyObject *self, PyObject *args) if (nread < 0) { PyErr_SetFromErrno(PyExc_OSError); - Py_DECREF(event_list); return NULL; } - // Construct a list of event tuples, which we'll make sense of in Python - for (unsigned i = 0 ; i < nread/event_size ; i++) { + // Construct a tuple of event tuples. Each tuple is the arguments to InputEvent. + size_t num_events = nread / event_size; + events = PyTuple_New(num_events); + for (size_t i = 0 ; i < num_events; i++) { sec = PyLong_FromLong(event[i].input_event_sec); usec = PyLong_FromLong(event[i].input_event_usec); val = PyLong_FromLong(event[i].value); + type = PyLong_FromLong(event[i].type); + code = PyLong_FromLong(event[i].code); - py_input_event = Py_BuildValue("OOhhO", sec, usec, event[i].type, event[i].code, val); - PyList_Append(event_list, py_input_event); - - Py_DECREF(py_input_event); - Py_DECREF(sec); - Py_DECREF(usec); - Py_DECREF(val); + py_input_event = PyTuple_Pack(5, sec, usec, type, code, val); + PyTuple_SET_ITEM(events, i, py_input_event); } - return event_list; + return events; } @@ -539,7 +531,6 @@ ioctl_EVIOCGPROP(PyObject *self, PyObject *args) } - static PyMethodDef MethodTable[] = { { "ioctl_devinfo", ioctl_devinfo, METH_VARARGS, "fetch input device info" }, { "ioctl_capabilities", ioctl_capabilities, METH_VARARGS, "fetch input device capabilities" }, From 0487652d1cb8f32c0f4a3830753b66cc12bab4c5 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 2 Feb 2025 18:25:22 +0100 Subject: [PATCH 64/87] Drop Python 2 support from input.c and uinput.c --- src/evdev/input.c | 24 ++---------------------- src/evdev/uinput.c | 25 ++----------------------- 2 files changed, 4 insertions(+), 45 deletions(-) diff --git a/src/evdev/input.c b/src/evdev/input.c index 55e6808..cfce67c 100644 --- a/src/evdev/input.c +++ b/src/evdev/input.c @@ -552,14 +552,10 @@ static PyMethodDef MethodTable[] = { }; -#define MODULE_NAME "_input" -#define MODULE_HELP "Python bindings to certain linux input subsystem functions" - -#if PY_MAJOR_VERSION >= 3 static struct PyModuleDef moduledef = { PyModuleDef_HEAD_INIT, - MODULE_NAME, - MODULE_HELP, + "_input", + "Python bindings to certain linux input subsystem functions", -1, /* m_size */ MethodTable, /* m_methods */ NULL, /* m_reload */ @@ -581,19 +577,3 @@ PyInit__input(void) { return moduleinit(); } - -#else -static PyObject * -moduleinit(void) -{ - PyObject* m = Py_InitModule3(MODULE_NAME, MethodTable, MODULE_HELP); - if (m == NULL) return NULL; - return m; -} - -PyMODINIT_FUNC -init_input(void) -{ - moduleinit(); -} -#endif diff --git a/src/evdev/uinput.c b/src/evdev/uinput.c index 3494705..8d2c096 100644 --- a/src/evdev/uinput.c +++ b/src/evdev/uinput.c @@ -356,8 +356,6 @@ int _uinput_end_erase(int fd, struct uinput_ff_erase *upload) return ioctl(fd, UI_END_FF_ERASE, upload); } -#define MODULE_NAME "_uinput" -#define MODULE_HELP "Python bindings for parts of linux/uinput.c" static PyMethodDef MethodTable[] = { { "open", uinput_open, METH_VARARGS, @@ -390,11 +388,10 @@ static PyMethodDef MethodTable[] = { { NULL, NULL, 0, NULL} }; -#if PY_MAJOR_VERSION >= 3 static struct PyModuleDef moduledef = { PyModuleDef_HEAD_INIT, - MODULE_NAME, - MODULE_HELP, + "_uinput", + "Python bindings for parts of linux/uinput.c", -1, /* m_size */ MethodTable, /* m_methods */ NULL, /* m_reload */ @@ -418,21 +415,3 @@ PyInit__uinput(void) { return moduleinit(); } - -#else -static PyObject * -moduleinit(void) -{ - PyObject* m = Py_InitModule3(MODULE_NAME, MethodTable, MODULE_HELP); - if (m == NULL) return NULL; - - PyModule_AddIntConstant(m, "maxnamelen", UINPUT_MAX_NAME_SIZE); - return m; -} - -PyMODINIT_FUNC -init_uinput(void) -{ - moduleinit(); -} -#endif From 3e6fd3e24218d868d8c8d1c08b475574b062d08f Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 2 Feb 2025 18:44:40 +0100 Subject: [PATCH 65/87] Update changelog and requirements --- .gitignore | 2 ++ docs/changelog.rst | 10 +++++++++- requirements-dev.txt | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 557f265..70ac303 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,8 @@ TAGS __pycache__ .pytest_cache .ruff_cache +.venv +uv.lock src/evdev/*.so src/evdev/ecodes.c diff --git a/docs/changelog.rst b/docs/changelog.rst index 92f4e23..8320eae 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,9 +1,17 @@ Changelog --------- -1.8.0 (Jan 25, 2025) +1.9.0 (Unreleased) ================== +- Fix ``CPATH/C_INCLUDE_PATH`` being ignored during build. + +- Slightly faster reading of events. + + +1.8.0 (Jan 25, 2025) +==================== + - Binary wheels are now provided by the `evdev-binary `_ package. The package is compiled on manylinux_2_28 against kernel 4.18. diff --git a/requirements-dev.txt b/requirements-dev.txt index 96366e6..725ad7f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,3 +7,4 @@ bump-my-version ~= 0.17.4 build twine cibuildwheel +setuptools From 78650f8f50f6a51fe98af9af54d42a121f602e5e Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 2 Feb 2025 20:49:43 +0100 Subject: [PATCH 66/87] FreeBSD related fixes --- docs/changelog.rst | 2 ++ setup.py | 7 ++++++- src/evdev/genecodes_c.py | 3 ++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 8320eae..20e7293 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,6 +8,8 @@ Changelog - Slightly faster reading of events. +- Fix build on FreeBSD. + 1.8.0 (Jan 25, 2025) ==================== diff --git a/setup.py b/setup.py index c5ab4a0..6b721d7 100755 --- a/setup.py +++ b/setup.py @@ -2,6 +2,7 @@ import sys import shutil import textwrap +import platform from pathlib import Path from subprocess import run @@ -25,7 +26,11 @@ def create_ecodes(headers=None): include_paths.update(c_inc_path.split(":")) include_paths.add("/usr/include") - files = ["linux/input.h", "linux/input-event-codes.h", "linux/uinput.h"] + if platform.system().lower() == "freebsd": + files = ["dev/evdev/input.h", "dev/evdev/input-event-codes.h", "dev/evdev/uinput.h"] + else: + files = ["linux/input.h", "linux/input-event-codes.h", "linux/uinput.h"] + headers = [os.path.join(path, file) for path in include_paths for file in files] headers = [header for header in headers if os.path.isfile(header)] diff --git a/src/evdev/genecodes_c.py b/src/evdev/genecodes_c.py index dd3ee91..5c2d946 100644 --- a/src/evdev/genecodes_c.py +++ b/src/evdev/genecodes_c.py @@ -25,7 +25,7 @@ # ----------------------------------------------------------------------------- -macro_regex = r"#define +((?:KEY|ABS|REL|SW|MSC|LED|BTN|REP|SND|ID|EV|BUS|SYN|FF|UI_FF|INPUT_PROP)_\w+)" +macro_regex = r"#define\s+((?:KEY|ABS|REL|SW|MSC|LED|BTN|REP|SND|ID|EV|BUS|SYN|FF|UI_FF|INPUT_PROP)_\w+)" macro_regex = re.compile(macro_regex) # Uname without hostname. @@ -38,6 +38,7 @@ #include #ifdef __FreeBSD__ #include +#include #else #include #include From 5478d94359801adb73d642c412cecd2042677230 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 2 Feb 2025 22:31:20 +0100 Subject: [PATCH 67/87] Give SYN_DROPPED special treatment in evtest and fix alignment --- src/evdev/eventio.py | 2 +- src/evdev/evtest.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/evdev/eventio.py b/src/evdev/eventio.py index 5478f02..5735d18 100644 --- a/src/evdev/eventio.py +++ b/src/evdev/eventio.py @@ -66,7 +66,7 @@ def read(self): `BlockingIOError` if there are no available events at the moment. """ - # events -> [(sec, usec, type, code, val), ...] + # events -> ((sec, usec, type, code, val), ...) events = _input.device_read_many(self.fd) for event in events: diff --git a/src/evdev/evtest.py b/src/evdev/evtest.py index 26e62ad..b0244a9 100644 --- a/src/evdev/evtest.py +++ b/src/evdev/evtest.py @@ -149,9 +149,11 @@ def print_capabilities(device): def print_event(e): if e.type == ecodes.EV_SYN: if e.code == ecodes.SYN_MT_REPORT: - msg = "time {:<16} +++++++++ {} ++++++++" + msg = "time {:<17} +++++++++++++ {} +++++++++++++" + elif e.code == ecodes.SYN_DROPPED: + msg = "time {:<17} !!!!!!!!!!!!! {} !!!!!!!!!!!!!" else: - msg = "time {:<16} --------- {} --------" + msg = "time {:<17} ------------- {} -------------" print(msg.format(e.timestamp(), ecodes.SYN[e.code])) else: if e.type in ecodes.bytype: @@ -159,7 +161,7 @@ def print_event(e): else: codename = "?" - evfmt = "time {:<16} type {} ({}), code {:<4} ({}), value {}" + evfmt = "time {:<17} type {} ({}), code {:<4} ({}), value {}" print(evfmt.format(e.timestamp(), e.type, ecodes.EV[e.type], e.code, codename, e.value)) From 1f083add5e5c377351db976c28fd477507dd7286 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 2 Feb 2025 22:33:17 +0100 Subject: [PATCH 68/87] Drop deprecated InputDevice.fn --- docs/changelog.rst | 2 ++ src/evdev/device.py | 9 --------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 20e7293..6ad25ed 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,8 @@ Changelog - Fix build on FreeBSD. +- Drop deprecated ``InputDevice.fn`` (use ``InputDevice.path`` instead). + 1.8.0 (Jan 25, 2025) ==================== diff --git a/src/evdev/device.py b/src/evdev/device.py index 7675a2d..fdd8363 100644 --- a/src/evdev/device.py +++ b/src/evdev/device.py @@ -1,9 +1,6 @@ -# encoding: utf-8 - import collections import contextlib import os -import warnings from . import _input, ecodes, util @@ -383,12 +380,6 @@ def active_keys(self, verbose=False): return active_keys - @property - def fn(self): - msg = "Please use {0}.path instead of {0}.fn".format(self.__class__.__name__) - warnings.warn(msg, DeprecationWarning, stacklevel=2) - return self.path - def absinfo(self, axis_num): """ Return current :class:`AbsInfo` for input device axis From e71716192675fba399cca083e87ac3db64327ca6 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 2 Feb 2025 23:49:30 +0100 Subject: [PATCH 69/87] Use REP_ constants in input.c --- src/evdev/input.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/evdev/input.c b/src/evdev/input.c index cfce67c..4ad0408 100644 --- a/src/evdev/input.c +++ b/src/evdev/input.c @@ -301,7 +301,7 @@ static PyObject * ioctl_EVIOCGREP(PyObject *self, PyObject *args) { int fd, ret; - unsigned int rep[2] = {0}; + unsigned int rep[REP_CNT] = {0}; ret = PyArg_ParseTuple(args, "i", &fd); if (!ret) return NULL; @@ -309,7 +309,7 @@ ioctl_EVIOCGREP(PyObject *self, PyObject *args) if (ret == -1) return NULL; - return Py_BuildValue("(ii)", rep[0], rep[1]); + return Py_BuildValue("(ii)", rep[REP_DELAY], rep[REP_PERIOD]); } @@ -317,7 +317,7 @@ static PyObject * ioctl_EVIOCSREP(PyObject *self, PyObject *args) { int fd, ret; - unsigned int rep[2] = {0}; + unsigned int rep[REP_CNT] = {0}; ret = PyArg_ParseTuple(args, "iii", &fd, &rep[0], &rep[1]); if (!ret) return NULL; From 59dc614a0b3a7046d611aa3ef5f5ee04b4050580 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 2 Feb 2025 23:51:22 +0100 Subject: [PATCH 70/87] More type hints --- docs/changelog.rst | 2 ++ src/evdev/device.py | 74 ++++++++++++++++++++++++-------------------- src/evdev/eventio.py | 9 +++--- src/evdev/uinput.py | 65 +++++++++++++++++++++----------------- src/evdev/util.py | 9 +++--- 5 files changed, 87 insertions(+), 72 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6ad25ed..5dfeaab 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -12,6 +12,8 @@ Changelog - Drop deprecated ``InputDevice.fn`` (use ``InputDevice.path`` instead). +- More type hints. + 1.8.0 (Jan 25, 2025) ==================== diff --git a/src/evdev/device.py b/src/evdev/device.py index fdd8363..73b0acb 100644 --- a/src/evdev/device.py +++ b/src/evdev/device.py @@ -1,6 +1,6 @@ -import collections import contextlib import os +from typing import NamedTuple, Tuple, Union from . import _input, ecodes, util @@ -10,18 +10,10 @@ from .eventio import EvdevError, EventIO -# -------------------------------------------------------------------------- -_AbsInfo = collections.namedtuple("AbsInfo", ["value", "min", "max", "fuzz", "flat", "resolution"]) - -_KbdInfo = collections.namedtuple("KbdInfo", ["delay", "repeat"]) - -_DeviceInfo = collections.namedtuple("DeviceInfo", ["bustype", "vendor", "product", "version"]) - - -class AbsInfo(_AbsInfo): +class AbsInfo(NamedTuple): """Absolute axis information. - A ``namedtuple`` used for storing absolute axis information - + A ``namedtuple`` with absolute axis information - corresponds to the ``input_absinfo`` struct: Attributes @@ -57,11 +49,18 @@ class AbsInfo(_AbsInfo): """ + value: int + min: int + max: int + fuzz: int + flat: int + resolution: int + def __str__(self): - return "val {}, min {}, max {}, fuzz {}, flat {}, res {}".format(*self) + return "value {}, min {}, max {}, fuzz {}, flat {}, res {}".format(*self) # pylint: disable=not-an-iterable -class KbdInfo(_KbdInfo): +class KbdInfo(NamedTuple): """Keyboard repeat rate. Attributes @@ -74,11 +73,14 @@ class KbdInfo(_KbdInfo): Keyboard repeat rate in characters per second. """ + delay: int + repeat: int + def __str__(self): - return "delay {}, repeat {}".format(*self) + return "delay {}, repeat {}".format(self.delay, self.repeat) -class DeviceInfo(_DeviceInfo): +class DeviceInfo(NamedTuple): """ Attributes ---------- @@ -88,9 +90,14 @@ class DeviceInfo(_DeviceInfo): version """ + bustype: int + vendor: int + product: int + version: int + def __str__(self): msg = "bus: {:04x}, vendor {:04x}, product {:04x}, version {:04x}" - return msg.format(*self) + return msg.format(*self) # pylint: disable=not-an-iterable class InputDevice(EventIO): @@ -100,7 +107,7 @@ class InputDevice(EventIO): __slots__ = ("path", "fd", "info", "name", "phys", "uniq", "_rawcapabilities", "version", "ff_effects_count") - def __init__(self, dev): + def __init__(self, dev: Union[str, bytes, os.PathLike]): """ Arguments --------- @@ -111,15 +118,14 @@ def __init__(self, dev): #: Path to input device. self.path = dev if not hasattr(dev, "__fspath__") else dev.__fspath__() - # Certain operations are possible only when the device is opened in - # read-write mode. + # Certain operations are possible only when the device is opened in read-write mode. try: fd = os.open(dev, os.O_RDWR | os.O_NONBLOCK) except OSError: fd = os.open(dev, os.O_RDONLY | os.O_NONBLOCK) #: A non-blocking file descriptor to the device file. - self.fd = fd + self.fd: int = fd # Returns (bustype, vendor, product, version, name, phys, capabilities). info_res = _input.ioctl_devinfo(self.fd) @@ -128,16 +134,16 @@ def __init__(self, dev): self.info = DeviceInfo(*info_res[:4]) #: The name of the event device. - self.name = info_res[4] + self.name: str = info_res[4] #: The physical topology of the device. - self.phys = info_res[5] + self.phys: str = info_res[5] #: The unique identifier of the device. - self.uniq = info_res[6] + self.uniq: str = info_res[6] #: The evdev protocol version. - self.version = _input.ioctl_EVIOCGVERSION(self.fd) + self.version: int = _input.ioctl_EVIOCGVERSION(self.fd) #: The raw dictionary of device capabilities - see `:func:capabilities()`. self._rawcapabilities = _input.ioctl_capabilities(self.fd) @@ -152,7 +158,7 @@ def __del__(self): except (OSError, ImportError, AttributeError): pass - def _capabilities(self, absinfo=True): + def _capabilities(self, absinfo: bool = True): res = {} for etype, _ecodes in self._rawcapabilities.items(): @@ -170,7 +176,7 @@ def _capabilities(self, absinfo=True): return res - def capabilities(self, verbose=False, absinfo=True): + def capabilities(self, verbose: bool = False, absinfo: bool = True): """ Return the event types that this device supports as a mapping of supported event types to lists of handled event codes. @@ -215,7 +221,7 @@ def capabilities(self, verbose=False, absinfo=True): else: return self._capabilities(absinfo) - def input_props(self, verbose=False): + def input_props(self, verbose: bool = False): """ Get device properties and quirks. @@ -236,7 +242,7 @@ def input_props(self, verbose=False): return props - def leds(self, verbose=False): + def leds(self, verbose: bool = False): """ Return currently set LED keys. @@ -257,7 +263,7 @@ def leds(self, verbose=False): return leds - def set_led(self, led_num, value): + def set_led(self, led_num: int, value: int): """ Set the state of the selected LED. @@ -327,7 +333,7 @@ def grab_context(self): yield self.ungrab() - def upload_effect(self, effect): + def upload_effect(self, effect: "ff.Effect"): """ Upload a force feedback effect to a force feedback device. """ @@ -354,10 +360,10 @@ def repeat(self): return KbdInfo(*_input.ioctl_EVIOCGREP(self.fd)) @repeat.setter - def repeat(self, value): + def repeat(self, value: Tuple[int, int]): return _input.ioctl_EVIOCSREP(self.fd, *value) - def active_keys(self, verbose=False): + def active_keys(self, verbose: bool = False): """ Return currently active keys. @@ -380,7 +386,7 @@ def active_keys(self, verbose=False): return active_keys - def absinfo(self, axis_num): + def absinfo(self, axis_num: int): """ Return current :class:`AbsInfo` for input device axis @@ -396,7 +402,7 @@ def absinfo(self, axis_num): """ return AbsInfo(*_input.ioctl_EVIOCGABS(self.fd, axis_num)) - def set_absinfo(self, axis_num, value=None, min=None, max=None, fuzz=None, flat=None, resolution=None): + def set_absinfo(self, axis_num: int, value=None, min=None, max=None, fuzz=None, flat=None, resolution=None): """ Update :class:`AbsInfo` values. Only specified values will be overwritten. diff --git a/src/evdev/eventio.py b/src/evdev/eventio.py index 5735d18..27bba9d 100644 --- a/src/evdev/eventio.py +++ b/src/evdev/eventio.py @@ -2,6 +2,7 @@ import functools import os import select +from typing import Iterator from . import _input, _uinput, ecodes from .events import InputEvent @@ -35,7 +36,7 @@ def fileno(self): """ return self.fd - def read_loop(self): + def read_loop(self) -> Iterator[InputEvent]: """ Enter an endless :func:`select.select()` loop that yields input events. """ @@ -45,7 +46,7 @@ def read_loop(self): for event in self.read(): yield event - def read_one(self): + def read_one(self) -> InputEvent: """ Read and return a single input event as an instance of :class:`InputEvent `. @@ -59,7 +60,7 @@ def read_one(self): if event: return InputEvent(*event) - def read(self): + def read(self) -> Iterator[InputEvent]: """ Read multiple input events from device. Return a generator object that yields :class:`InputEvent ` instances. Raises @@ -114,7 +115,7 @@ def write_event(self, event): self.write(event.type, event.code, event.value) @need_write - def write(self, etype, code, value): + def write(self, etype: int, code: int, value: int): """ Inject an input event into the input subsystem. Events are queued until a synchronization event is received. diff --git a/src/evdev/uinput.py b/src/evdev/uinput.py index 9567374..2c69c2b 100644 --- a/src/evdev/uinput.py +++ b/src/evdev/uinput.py @@ -5,8 +5,10 @@ import stat import time from collections import defaultdict +from typing import Union, Tuple, Dict, Sequence, Optional -from . import _uinput, device, ecodes, ff, util +from . import _uinput, ecodes, ff, util +from .device import InputDevice, AbsInfo from .events import InputEvent try: @@ -38,7 +40,12 @@ class UInput(EventIO): ) @classmethod - def from_device(cls, *devices, filtered_types=(ecodes.EV_SYN, ecodes.EV_FF), **kwargs): + def from_device( + cls, + *devices: Union[InputDevice, Union[str, bytes, os.PathLike]], + filtered_types: Tuple[int] = (ecodes.EV_SYN, ecodes.EV_FF), + **kwargs, + ): """ Create an UInput device with the capabilities of one or more input devices. @@ -57,8 +64,8 @@ def from_device(cls, *devices, filtered_types=(ecodes.EV_SYN, ecodes.EV_FF), **k device_instances = [] for dev in devices: - if not isinstance(dev, device.InputDevice): - dev = device.InputDevice(str(dev)) + if not isinstance(dev, InputDevice): + dev = InputDevice(str(dev)) device_instances.append(dev) all_capabilities = defaultdict(set) @@ -79,14 +86,14 @@ def from_device(cls, *devices, filtered_types=(ecodes.EV_SYN, ecodes.EV_FF), **k def __init__( self, - events=None, - name="py-evdev-uinput", - vendor=0x1, - product=0x1, - version=0x1, - bustype=0x3, - devnode="/dev/uinput", - phys="py-evdev-uinput", + events: Optional[Dict[int, Sequence[int]]] = None, + name: str = "py-evdev-uinput", + vendor: int = 0x1, + product: int = 0x1, + version: int = 0x1, + bustype: int = 0x3, + devnode: str = "/dev/uinput", + phys: str = "py-evdev-uinput", input_props=None, # CentOS 7 has sufficiently old headers that FF_MAX_EFFECTS is not defined there, # which causes the whole module to fail loading. Fallback on a hardcoded value of @@ -131,13 +138,13 @@ def __init__( to inject only ``KEY_*`` and ``BTN_*`` event codes. """ - self.name = name #: Uinput device name. - self.vendor = vendor #: Device vendor identifier. - self.product = product #: Device product identifier. - self.version = version #: Device version identifier. - self.bustype = bustype #: Device bustype - e.g. ``BUS_USB``. - self.phys = phys #: Uinput device physical path. - self.devnode = devnode #: Uinput device node - e.g. ``/dev/uinput/``. + self.name: str = name #: Uinput device name. + self.vendor: int = vendor #: Device vendor identifier. + self.product: int = product #: Device product identifier. + self.version: int = version #: Device version identifier. + self.bustype: int = bustype #: Device bustype - e.g. ``BUS_USB``. + self.phys: str = phys #: Uinput device physical path. + self.devnode: str = devnode #: Uinput device node - e.g. ``/dev/uinput/``. if not events: events = {ecodes.EV_KEY: ecodes.keys.keys()} @@ -173,7 +180,7 @@ def __init__( #: An :class:`InputDevice ` instance #: for the fake input device. ``None`` if the device cannot be #: opened for reading and writing. - self.device = self._find_device(self.fd) + self.device: InputDevice = self._find_device(self.fd) def _prepare_events(self, events): """Prepare events for passing to _uinput.enable and _uinput.setup""" @@ -181,7 +188,7 @@ def _prepare_events(self, events): for etype, codes in events.items(): for code in codes: # Handle max, min, fuzz, flat. - if isinstance(code, (tuple, list, device.AbsInfo)): + if isinstance(code, (tuple, list, AbsInfo)): # Flatten (ABS_Y, (0, 255, 0, 0, 0, 0)) to (ABS_Y, 0, 255, 0, 0, 0, 0). f = [code[0]] f.extend(code[1]) @@ -206,7 +213,7 @@ def __repr__(self): return "{}({})".format(self.__class__.__name__, ", ".join(v)) def __str__(self): - msg = 'name "{}", bus "{}", vendor "{:04x}", product "{:04x}", version "{:04x}", phys "{}"\n' "event types: {}" + msg = 'name "{}", bus "{}", vendor "{:04x}", product "{:04x}", version "{:04x}", phys "{}"\nevent types: {}' evtypes = [i[0] for i in self.capabilities(True).keys()] msg = msg.format( @@ -225,7 +232,7 @@ def close(self): _uinput.close(self.fd) self.fd = -1 - def capabilities(self, verbose=False, absinfo=True): + def capabilities(self, verbose: bool = False, absinfo: bool = True): """See :func:`capabilities `.""" if self.device is None: raise UInputError("input device not opened - cannot read capabilities") @@ -281,7 +288,7 @@ def _verify(self): msg = "uinput device name must not be longer than {} characters" raise UInputError(msg.format(_uinput.maxnamelen)) - def _find_device(self, fd): + def _find_device(self, fd: int) -> InputDevice: """ Tries to find the device node. Will delegate this task to one of several platform-specific functions. @@ -299,7 +306,7 @@ def _find_device(self, fd): # use the generic fallback method. return self._find_device_fallback() - def _find_device_linux(self, sysname): + def _find_device_linux(self, sysname: str) -> InputDevice: """ Tries to find the device node when running on Linux. """ @@ -327,15 +334,15 @@ def _find_device_linux(self, sysname): # device to show up or the permissions to be set. for attempt in range(19): try: - return device.InputDevice(device_path) + return InputDevice(device_path) except (FileNotFoundError, PermissionError): time.sleep(0.1) # Last attempt. If this fails, whatever exception the last attempt raises # shall be the exception that this function raises. - return device.InputDevice(device_path) + return InputDevice(device_path) - def _find_device_fallback(self): + def _find_device_fallback(self) -> Union[InputDevice, None]: """ Tries to find the device node when UI_GET_SYSNAME is not available or we're running on a system sufficiently exotic that we do not know how @@ -363,6 +370,6 @@ def _find_device_fallback(self): path_number_pairs.sort(key=lambda pair: pair[1], reverse=True) for path, _ in path_number_pairs: - d = device.InputDevice(path) + d = InputDevice(path) if d.name == self.name: return d diff --git a/src/evdev/util.py b/src/evdev/util.py index 59991f6..dd7cba6 100644 --- a/src/evdev/util.py +++ b/src/evdev/util.py @@ -3,21 +3,20 @@ import os import re import stat +from typing import Union, List from . import ecodes from .events import event_factory -def list_devices(input_device_dir="/dev/input"): +def list_devices(input_device_dir="/dev/input") -> List[str]: """List readable character devices in ``input_device_dir``.""" fns = glob.glob("{}/event*".format(input_device_dir)) - fns = list(filter(is_device, fns)) + return list(filter(is_device, fns)) - return fns - -def is_device(fn): +def is_device(fn: Union[str, bytes, os.PathLike]) -> bool: """Check if ``fn`` is a readable and writable character device.""" if not os.path.exists(fn): From bc91d17cd6103f37be3d705302132df328737698 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Tue, 4 Feb 2025 17:50:34 +0000 Subject: [PATCH 71/87] Expose type annotations (#233) * Expose type annotations --- src/evdev/py.typed | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/evdev/py.typed diff --git a/src/evdev/py.typed b/src/evdev/py.typed new file mode 100644 index 0000000..e69de29 From 2c623eb5b3b6442ae9cc6a7113d776f23e041cba Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sat, 8 Feb 2025 11:40:49 +0100 Subject: [PATCH 72/87] More type hints --- src/evdev/ecodes.py | 6 +++--- src/evdev/events.py | 37 +++++++++++++++++++------------------ src/evdev/evtest.py | 1 - src/evdev/util.py | 2 +- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/evdev/ecodes.py b/src/evdev/ecodes.py index a19dcba..fd4afc4 100644 --- a/src/evdev/ecodes.py +++ b/src/evdev/ecodes.py @@ -1,5 +1,5 @@ -# When installed, this module is replaced by an ecodes.py generated at +# When installed, this module is replaced by an ecodes.py generated at # build time by genecodes_py.py (see build_ext in setup.py). -# This stub exists to make development of evdev itself more convenient. -from . ecodes_runtime import * +# This stub exists to make development of evdev itself more convenient. +from .ecodes_runtime import * diff --git a/src/evdev/events.py b/src/evdev/events.py index a4f817d..922bfe6 100644 --- a/src/evdev/events.py +++ b/src/evdev/events.py @@ -38,6 +38,7 @@ # http://www.kernel.org/doc/Documentation/input/event-codes.txt # pylint: disable=no-name-in-module +from typing import Final from .ecodes import ABS, EV_ABS, EV_KEY, EV_REL, EV_SYN, KEY, REL, SYN, keys @@ -48,21 +49,21 @@ class InputEvent: def __init__(self, sec, usec, type, code, value): #: Time in seconds since epoch at which event occurred. - self.sec = sec + self.sec: int = sec #: Microsecond portion of the timestamp. - self.usec = usec + self.usec: int = usec #: Event type - one of ``ecodes.EV_*``. - self.type = type + self.type: int = type #: Event code related to the event type. - self.code = code + self.code: int = code #: Event value related to the event type. - self.value = value + self.value: int = value - def timestamp(self): + def timestamp(self) -> float: """Return event timestamp as a float.""" return self.sec + (self.usec / 1000000.0) @@ -78,20 +79,20 @@ def __repr__(self): class KeyEvent: """An event generated by a keyboard, button or other key-like devices.""" - key_up = 0x0 - key_down = 0x1 - key_hold = 0x2 + key_up: Final[int] = 0x0 + key_down: Final[int] = 0x1 + key_hold: Final[int] = 0x2 __slots__ = "scancode", "keycode", "keystate", "event" - def __init__(self, event, allow_unknown=False): + def __init__(self, event: InputEvent, allow_unknown: bool = False): """ The ``allow_unknown`` argument determines what to do in the event of an event code for which a key code cannot be found. If ``False`` a ``KeyError`` will be raised. If ``True`` the keycode will be set to the hex value of the event code. """ - self.scancode = event.code + self.scancode: int = event.code if event.value == 0: self.keystate = KeyEvent.key_up @@ -109,7 +110,7 @@ def __init__(self, event, allow_unknown=False): raise #: Reference to an :class:`InputEvent` instance. - self.event = event + self.event: InputEvent = event def __str__(self): try: @@ -129,9 +130,9 @@ class RelEvent: __slots__ = "event" - def __init__(self, event): + def __init__(self, event: InputEvent): #: Reference to an :class:`InputEvent` instance. - self.event = event + self.event: InputEvent = event def __str__(self): msg = "relative axis event at {:f}, {}" @@ -146,9 +147,9 @@ class AbsEvent: __slots__ = "event" - def __init__(self, event): + def __init__(self, event: InputEvent): #: Reference to an :class:`InputEvent` instance. - self.event = event + self.event: InputEvent = event def __str__(self): msg = "absolute axis event at {:f}, {}" @@ -166,9 +167,9 @@ class SynEvent: __slots__ = "event" - def __init__(self, event): + def __init__(self, event: InputEvent): #: Reference to an :class:`InputEvent` instance. - self.event = event + self.event: InputEvent = event def __str__(self): msg = "synchronization event at {:f}, {}" diff --git a/src/evdev/evtest.py b/src/evdev/evtest.py index b0244a9..6ea3bb5 100644 --- a/src/evdev/evtest.py +++ b/src/evdev/evtest.py @@ -16,7 +16,6 @@ evtest /dev/input/event0 /dev/input/event1 """ - import atexit import optparse import re diff --git a/src/evdev/util.py b/src/evdev/util.py index dd7cba6..b84ef09 100644 --- a/src/evdev/util.py +++ b/src/evdev/util.py @@ -9,7 +9,7 @@ from .events import event_factory -def list_devices(input_device_dir="/dev/input") -> List[str]: +def list_devices(input_device_dir: Union[str, bytes, os.PathLike] = "/dev/input") -> List[str]: """List readable character devices in ``input_device_dir``.""" fns = glob.glob("{}/event*".format(input_device_dir)) From 7cb02b9c644fbcce69b09375492e39382a1f450c Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sat, 8 Feb 2025 13:36:06 +0100 Subject: [PATCH 73/87] =?UTF-8?q?Bump=20version:=201.8.0=20=E2=86=92=201.9?= =?UTF-8?q?.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/changelog.rst | 10 +++++----- docs/conf.py | 2 +- pyproject.toml | 4 ++-- src/evdev/eventio.py | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 5dfeaab..f66cfff 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,18 +1,18 @@ Changelog --------- -1.9.0 (Unreleased) +1.9.0 (Feb 08, 2025) ================== -- Fix ``CPATH/C_INCLUDE_PATH`` being ignored during build. +- Fix for ``CPATH/C_INCLUDE_PATH`` being ignored during build. -- Slightly faster reading of events. +- Slightly faster reading of events in ``device.read()`` and ``device.read_one()``. -- Fix build on FreeBSD. +- Fix FreeBSD support. - Drop deprecated ``InputDevice.fn`` (use ``InputDevice.path`` instead). -- More type hints. +- Improve type hint coverage and add a ``py.typed`` file to the sdist. 1.8.0 (Jan 25, 2025) diff --git a/docs/conf.py b/docs/conf.py index 53b5206..b938fa0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -65,7 +65,7 @@ # built documents. # # The full version, including alpha/beta/rc tags. -release = "1.8.0" +release = "1.9.0" # The short X.Y version. version = release diff --git a/pyproject.toml b/pyproject.toml index 7854d91..346dedd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "evdev" -version = "1.8.0" +version = "1.9.0" description = "Bindings to the Linux input handling subsystem" keywords = ["evdev", "input", "uinput"] readme = "README.md" @@ -36,7 +36,7 @@ line-length = 120 ignore = ["E265", "E241", "F403", "F401", "E401", "E731"] [tool.bumpversion] -current_version = "1.8.0" +current_version = "1.9.0" commit = true tag = true allow_dirty = true diff --git a/src/evdev/eventio.py b/src/evdev/eventio.py index 27bba9d..bdb91a4 100644 --- a/src/evdev/eventio.py +++ b/src/evdev/eventio.py @@ -2,7 +2,7 @@ import functools import os import select -from typing import Iterator +from typing import Iterator, Union from . import _input, _uinput, ecodes from .events import InputEvent @@ -46,7 +46,7 @@ def read_loop(self) -> Iterator[InputEvent]: for event in self.read(): yield event - def read_one(self) -> InputEvent: + def read_one(self) -> Union[InputEvent, None]: """ Read and return a single input event as an instance of :class:`InputEvent `. From 6523e3f7d77ff5fd15bc2a02033527449b117e72 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Thu, 20 Feb 2025 07:48:34 +0000 Subject: [PATCH 74/87] Explicit export (#236) --- src/evdev/__init__.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/evdev/__init__.py b/src/evdev/__init__.py index 6aa6ef2..5d056f0 100644 --- a/src/evdev/__init__.py +++ b/src/evdev/__init__.py @@ -2,8 +2,25 @@ # Gather everything into a single, convenient namespace. # -------------------------------------------------------------------------- -from . import ecodes, ff -from .device import AbsInfo, DeviceInfo, EvdevError, InputDevice -from .events import AbsEvent, InputEvent, KeyEvent, RelEvent, SynEvent, event_factory -from .uinput import UInput, UInputError -from .util import categorize, list_devices, resolve_ecodes, resolve_ecodes_dict +from . import ecodes as ecodes, ff as ff +from .device import ( + AbsInfo as AbsInfo, + DeviceInfo as DeviceInfo, + EvdevError as EvdevError, + InputDevice as InputDevice, +) +from .events import ( + AbsEvent as AbsEvent, + InputEvent as InputEvent, + KeyEvent as KeyEvent, + RelEvent as RelEvent, + SynEvent as SynEvent, + event_factory as event_factory, +) +from .uinput import UInput as UInput, UInputError as UInputError +from .util import ( + categorize as categorize, + list_devices as list_devices, + resolve_ecodes as resolve_ecodes, + resolve_ecodes_dict as resolve_ecodes_dict, +) From 7916a7beb16f13cb0827e712aa3e889d38ea67e2 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Thu, 20 Feb 2025 20:56:45 +0000 Subject: [PATCH 75/87] Fill in some type annotations (#237) --- src/evdev/device.py | 32 ++++++++------ src/evdev/eventio_async.py | 87 ++++++++++++++++++++------------------ src/evdev/util.py | 4 +- 3 files changed, 68 insertions(+), 55 deletions(-) diff --git a/src/evdev/device.py b/src/evdev/device.py index 73b0acb..878a937 100644 --- a/src/evdev/device.py +++ b/src/evdev/device.py @@ -1,6 +1,6 @@ import contextlib import os -from typing import NamedTuple, Tuple, Union +from typing import Dict, Iterator, List, Literal, NamedTuple, Tuple, Union, overload from . import _input, ecodes, util @@ -95,7 +95,7 @@ class DeviceInfo(NamedTuple): product: int version: int - def __str__(self): + def __str__(self) -> str: msg = "bus: {:04x}, vendor {:04x}, product {:04x}, version {:04x}" return msg.format(*self) # pylint: disable=not-an-iterable @@ -151,7 +151,7 @@ def __init__(self, dev: Union[str, bytes, os.PathLike]): #: The number of force feedback effects the device can keep in its memory. self.ff_effects_count = _input.ioctl_EVIOCGEFFECTS(self.fd) - def __del__(self): + def __del__(self) -> None: if hasattr(self, "fd") and self.fd is not None: try: self.close() @@ -176,7 +176,13 @@ def _capabilities(self, absinfo: bool = True): return res - def capabilities(self, verbose: bool = False, absinfo: bool = True): + @overload + def capabilities(self, verbose: Literal[False] = ..., absinfo: bool = ...) -> Dict[int, List[int]]: + ... + @overload + def capabilities(self, verbose: Literal[True], absinfo: bool = ...) -> Dict[Tuple[str, int], List[Tuple[str, int]]]: + ... + def capabilities(self, verbose: bool = False, absinfo: bool = True) -> Union[Dict[int, List[int]], Dict[Tuple[str, int], List[Tuple[str, int]]]]: """ Return the event types that this device supports as a mapping of supported event types to lists of handled event codes. @@ -263,7 +269,7 @@ def leds(self, verbose: bool = False): return leds - def set_led(self, led_num: int, value: int): + def set_led(self, led_num: int, value: int) -> None: """ Set the state of the selected LED. @@ -279,18 +285,18 @@ def __eq__(self, other): """ return isinstance(other, self.__class__) and self.info == other.info and self.path == other.path - def __str__(self): + def __str__(self) -> str: msg = 'device {}, name "{}", phys "{}", uniq "{}"' return msg.format(self.path, self.name, self.phys, self.uniq or "") - def __repr__(self): + def __repr__(self) -> str: msg = (self.__class__.__name__, self.path) return "{}({!r})".format(*msg) def __fspath__(self): return self.path - def close(self): + def close(self) -> None: if self.fd > -1: try: super().close() @@ -298,7 +304,7 @@ def close(self): finally: self.fd = -1 - def grab(self): + def grab(self) -> None: """ Grab input device using ``EVIOCGRAB`` - other applications will be unable to receive events until the device is released. Only @@ -311,7 +317,7 @@ def grab(self): _input.ioctl_EVIOCGRAB(self.fd, 1) - def ungrab(self): + def ungrab(self) -> None: """ Release device if it has been already grabbed (uses `EVIOCGRAB`). @@ -324,7 +330,7 @@ def ungrab(self): _input.ioctl_EVIOCGRAB(self.fd, 0) @contextlib.contextmanager - def grab_context(self): + def grab_context(self) -> Iterator[None]: """ A context manager for the duration of which only the current process will be able to receive events from the device. @@ -342,7 +348,7 @@ def upload_effect(self, effect: "ff.Effect"): ff_id = _input.upload_effect(self.fd, data) return ff_id - def erase_effect(self, ff_id): + def erase_effect(self, ff_id) -> None: """ Erase a force effect from a force feedback device. This also stops the effect. @@ -402,7 +408,7 @@ def absinfo(self, axis_num: int): """ return AbsInfo(*_input.ioctl_EVIOCGABS(self.fd, axis_num)) - def set_absinfo(self, axis_num: int, value=None, min=None, max=None, fuzz=None, flat=None, resolution=None): + def set_absinfo(self, axis_num: int, value=None, min=None, max=None, fuzz=None, flat=None, resolution=None) -> None: """ Update :class:`AbsInfo` values. Only specified values will be overwritten. diff --git a/src/evdev/eventio_async.py b/src/evdev/eventio_async.py index fb8bcd2..4af1aab 100644 --- a/src/evdev/eventio_async.py +++ b/src/evdev/eventio_async.py @@ -1,11 +1,57 @@ import asyncio import select +import sys from . import eventio +from .events import InputEvent # needed for compatibility from .eventio import EvdevError +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing import Any as Self + + +class ReadIterator: + def __init__(self, device): + self.current_batch = iter(()) + self.device = device + + # Standard iterator protocol. + def __iter__(self) -> Self: + return self + + def __next__(self) -> InputEvent: + try: + # Read from the previous batch of events. + return next(self.current_batch) + except StopIteration: + r, w, x = select.select([self.device.fd], [], []) + self.current_batch = self.device.read() + return next(self.current_batch) + + def __aiter__(self) -> Self: + return self + + def __anext__(self) -> "asyncio.Future[InputEvent]": + future = asyncio.Future() + try: + # Read from the previous batch of events. + future.set_result(next(self.current_batch)) + except StopIteration: + + def next_batch_ready(batch): + try: + self.current_batch = batch.result() + future.set_result(next(self.current_batch)) + except Exception as e: + future.set_exception(e) + + self.device.async_read().add_done_callback(next_batch_ready) + return future + class EventIO(eventio.EventIO): def _do_when_readable(self, callback): @@ -42,7 +88,7 @@ def async_read(self): self._do_when_readable(lambda: self._set_result(future, self.read)) return future - def async_read_loop(self): + def async_read_loop(self) -> ReadIterator: """ Return an iterator that yields input events. This iterator is compatible with the ``async for`` syntax. @@ -58,42 +104,3 @@ def close(self): # no event loop present, so there is nothing to # remove the reader from. Ignore pass - - -class ReadIterator: - def __init__(self, device): - self.current_batch = iter(()) - self.device = device - - # Standard iterator protocol. - def __iter__(self): - return self - - def __next__(self): - try: - # Read from the previous batch of events. - return next(self.current_batch) - except StopIteration: - r, w, x = select.select([self.device.fd], [], []) - self.current_batch = self.device.read() - return next(self.current_batch) - - def __aiter__(self): - return self - - def __anext__(self): - future = asyncio.Future() - try: - # Read from the previous batch of events. - future.set_result(next(self.current_batch)) - except StopIteration: - - def next_batch_ready(batch): - try: - self.current_batch = batch.result() - future.set_result(next(self.current_batch)) - except Exception as e: - future.set_exception(e) - - self.device.async_read().add_done_callback(next_batch_ready) - return future diff --git a/src/evdev/util.py b/src/evdev/util.py index b84ef09..f873655 100644 --- a/src/evdev/util.py +++ b/src/evdev/util.py @@ -6,7 +6,7 @@ from typing import Union, List from . import ecodes -from .events import event_factory +from .events import InputEvent, event_factory def list_devices(input_device_dir: Union[str, bytes, os.PathLike] = "/dev/input") -> List[str]: @@ -32,7 +32,7 @@ def is_device(fn: Union[str, bytes, os.PathLike]) -> bool: return True -def categorize(event): +def categorize(event: InputEvent) -> InputEvent: """ Categorize an event according to its type. From a98b68f9ac7ac32dbd175f6090e2458ec612f75c Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Fri, 21 Feb 2025 18:24:49 +0100 Subject: [PATCH 76/87] Fix for UI_FF constants missing from generated ecodes.py --- .github/workflows/test.yml | 1 - src/evdev/__init__.py | 17 +++++++++++++++-- src/evdev/ecodes_runtime.py | 2 +- src/evdev/genecodes_py.py | 3 ++- tests/test_ecodes.py | 16 +++++++++++++--- 5 files changed, 31 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3ee56d3..b9cd26d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,7 +21,6 @@ jobs: python-version: ${{ matrix.python-version }} - name: Run pytest tests - # pip install -e . builds _ecodes and such into the evdev directory # sudo required to write to uinputs run: | sudo python -m pip install pytest setuptools diff --git a/src/evdev/__init__.py b/src/evdev/__init__.py index 5d056f0..bae0fec 100644 --- a/src/evdev/__init__.py +++ b/src/evdev/__init__.py @@ -2,13 +2,21 @@ # Gather everything into a single, convenient namespace. # -------------------------------------------------------------------------- -from . import ecodes as ecodes, ff as ff +# The superfluous "import name as name" syntax is here to satisfy mypy's attrs-defined rule. +# Alternatively all exported objects can be listed in __all__. + +from . import ( + ecodes as ecodes, + ff as ff, +) + from .device import ( AbsInfo as AbsInfo, DeviceInfo as DeviceInfo, EvdevError as EvdevError, InputDevice as InputDevice, ) + from .events import ( AbsEvent as AbsEvent, InputEvent as InputEvent, @@ -17,7 +25,12 @@ SynEvent as SynEvent, event_factory as event_factory, ) -from .uinput import UInput as UInput, UInputError as UInputError + +from .uinput import ( + UInput as UInput, + UInputError as UInputError, +) + from .util import ( categorize as categorize, list_devices as list_devices, diff --git a/src/evdev/ecodes_runtime.py b/src/evdev/ecodes_runtime.py index d6c8b2a..47f3b23 100644 --- a/src/evdev/ecodes_runtime.py +++ b/src/evdev/ecodes_runtime.py @@ -46,7 +46,7 @@ #: Mapping of names to values. ecodes = {} -prefixes = "KEY ABS REL SW MSC LED BTN REP SND ID EV BUS SYN FF_STATUS FF INPUT_PROP".split() +prefixes = "KEY ABS REL SW MSC LED BTN REP SND ID EV BUS SYN FF_STATUS FF INPUT_PROP UI_FF".split() prev_prefix = "" g = globals() diff --git a/src/evdev/genecodes_py.py b/src/evdev/genecodes_py.py index 1afbc34..f00020c 100644 --- a/src/evdev/genecodes_py.py +++ b/src/evdev/genecodes_py.py @@ -40,6 +40,7 @@ ("BUS", "Dict[int, Union[str, Tuple[str]]]", None), ("SYN", "Dict[int, Union[str, Tuple[str]]]", None), ("FF", "Dict[int, Union[str, Tuple[str]]]", None), + ("UI_FF", "Dict[int, Union[str, Tuple[str]]]", None), ("FF_STATUS", "Dict[int, Union[str, Tuple[str]]]", None), ("INPUT_PROP", "Dict[int, Union[str, Tuple[str]]]", None) ] @@ -50,4 +51,4 @@ print(f"{key}: {annotation} = ", end="") pprint(getattr(ecodes, key)) - print() + print() \ No newline at end of file diff --git a/tests/test_ecodes.py b/tests/test_ecodes.py index c810b4f..5c3e38d 100644 --- a/tests/test_ecodes.py +++ b/tests/test_ecodes.py @@ -1,9 +1,8 @@ -# encoding: utf-8 - from evdev import ecodes +from evdev import ecodes_runtime -prefixes = "KEY ABS REL SW MSC LED BTN REP SND ID EV BUS SYN FF_STATUS FF" +prefixes = "KEY ABS REL SW MSC LED BTN REP SND ID EV BUS SYN FF_STATUS FF UI_FF" def to_tuples(val): @@ -29,3 +28,14 @@ def test_overlap(): vals_ff = set(to_tuples(ecodes.FF.values())) vals_ff_status = set(to_tuples(ecodes.FF_STATUS.values())) assert bool(vals_ff & vals_ff_status) is False + + +def test_generated(): + e_run = vars(ecodes_runtime) + e_gen = vars(ecodes) + + def keys(v): + res = {k for k in v.keys() if not k.startswith("_") and not k[1].islower()} + return res + + assert keys(e_run) == keys(e_gen) \ No newline at end of file From 82d09f631a16329c6d3ca2a2fd3789fac3a01c4b Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sat, 22 Feb 2025 12:05:08 +0100 Subject: [PATCH 77/87] =?UTF-8?q?Bump=20version:=201.9.0=20=E2=86=92=201.9?= =?UTF-8?q?.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/changelog.rst | 11 ++++++++++- docs/conf.py | 2 +- pyproject.toml | 4 ++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index f66cfff..4dcf62f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,8 +1,17 @@ Changelog --------- + +1.9.1 (Feb 22, 2025) +==================== + +- Fix fox missing ``UI_FF`` constants in generated ``ecodes.py``. + +- More type annotations. + + 1.9.0 (Feb 08, 2025) -================== +==================== - Fix for ``CPATH/C_INCLUDE_PATH`` being ignored during build. diff --git a/docs/conf.py b/docs/conf.py index b938fa0..86b3d06 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -65,7 +65,7 @@ # built documents. # # The full version, including alpha/beta/rc tags. -release = "1.9.0" +release = "1.9.1" # The short X.Y version. version = release diff --git a/pyproject.toml b/pyproject.toml index 346dedd..d248ab2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "evdev" -version = "1.9.0" +version = "1.9.1" description = "Bindings to the Linux input handling subsystem" keywords = ["evdev", "input", "uinput"] readme = "README.md" @@ -36,7 +36,7 @@ line-length = 120 ignore = ["E265", "E241", "F403", "F401", "E401", "E731"] [tool.bumpversion] -current_version = "1.9.0" +current_version = "1.9.1" commit = true tag = true allow_dirty = true From 5f9fd2cd11daa9a54452dadbf00aaf284d3d6063 Mon Sep 17 00:00:00 2001 From: bastian-wattro <106541220+bastian-wattro@users.noreply.github.com> Date: Fri, 28 Feb 2025 08:41:31 +0100 Subject: [PATCH 78/87] fix utils.categorize return type (#240) --- src/evdev/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/evdev/util.py b/src/evdev/util.py index f873655..db89a22 100644 --- a/src/evdev/util.py +++ b/src/evdev/util.py @@ -6,7 +6,7 @@ from typing import Union, List from . import ecodes -from .events import InputEvent, event_factory +from .events import InputEvent, event_factory, KeyEvent, RelEvent, AbsEvent, SynEvent def list_devices(input_device_dir: Union[str, bytes, os.PathLike] = "/dev/input") -> List[str]: @@ -32,7 +32,7 @@ def is_device(fn: Union[str, bytes, os.PathLike]) -> bool: return True -def categorize(event: InputEvent) -> InputEvent: +def categorize(event: InputEvent) -> Union[InputEvent, KeyEvent, RelEvent, AbsEvent, SynEvent]: """ Categorize an event according to its type. From 6b4e8ef0ee505d9c3d46b1787eac339d8bd0b934 Mon Sep 17 00:00:00 2001 From: Yoann Congal Date: Thu, 1 May 2025 19:42:17 +0200 Subject: [PATCH 79/87] Add a reproducibility option for building ecodes.c (#242) ecodes.c currently contains the kernel info of the build machine and the full path of the input*.h headers: This is not reproducible as output can change even is headers content do not. Downstream distributions might package ecodes.c and get non-reproducible output. To fix this: introduce a --reproducible option in the build: - in setup.py build_ecodes command - in underlying genecodes_c.py Note: These options are disabled by default so no change is expected in current builds. Signed-off-by: Yoann Congal --- setup.py | 13 ++++++++++--- src/evdev/genecodes_c.py | 17 +++++++++++------ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/setup.py b/setup.py index 6b721d7..3371199 100755 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ ecodes_c_path = curdir / "src/evdev/ecodes.c" -def create_ecodes(headers=None): +def create_ecodes(headers=None, reproducibility=False): if not headers: include_paths = set() cpath = os.environ.get("CPATH", "").strip() @@ -65,7 +65,10 @@ def create_ecodes(headers=None): print("writing %s (using %s)" % (ecodes_c_path, " ".join(headers))) with ecodes_c_path.open("w") as fh: - cmd = [sys.executable, "src/evdev/genecodes_c.py", "--ecodes", *headers] + cmd = [sys.executable, "src/evdev/genecodes_c.py"] + if reproducibility: + cmd.append("--reproducibility") + cmd.extend(["--ecodes", *headers]) run(cmd, check=True, stdout=fh) @@ -74,17 +77,21 @@ class build_ecodes(Command): user_options = [ ("evdev-headers=", None, "colon-separated paths to input subsystem headers"), + ("reproducibility", None, "hide host details (host/paths) to create a reproducible output"), ] def initialize_options(self): self.evdev_headers = None + self.reproducibility = False def finalize_options(self): if self.evdev_headers: self.evdev_headers = self.evdev_headers.split(":") + if self.reproducibility is None: + self.reproducibility = False def run(self): - create_ecodes(self.evdev_headers) + create_ecodes(self.evdev_headers, reproducibility=self.reproducibility) class build_ext(_build_ext.build_ext): diff --git a/src/evdev/genecodes_c.py b/src/evdev/genecodes_c.py index 5c2d946..24cad27 100644 --- a/src/evdev/genecodes_c.py +++ b/src/evdev/genecodes_c.py @@ -15,22 +15,27 @@ "/usr/include/linux/uinput.h", ] -opts, args = getopt.getopt(sys.argv[1:], "", ["ecodes", "stubs"]) +opts, args = getopt.getopt(sys.argv[1:], "", ["ecodes", "stubs", "reproducibility"]) if not opts: - print("usage: genecodes.py [--ecodes|--stubs] ") + print("usage: genecodes.py [--ecodes|--stubs] [--reproducibility] ") exit(2) if args: headers = args +reproducibility = ("--reproducibility", "") in opts + # ----------------------------------------------------------------------------- macro_regex = r"#define\s+((?:KEY|ABS|REL|SW|MSC|LED|BTN|REP|SND|ID|EV|BUS|SYN|FF|UI_FF|INPUT_PROP)_\w+)" macro_regex = re.compile(macro_regex) -# Uname without hostname. -uname = list(os.uname()) -uname = " ".join((uname[0], *uname[2:])) +if reproducibility: + uname = "hidden for reproducibility" +else: + # Uname without hostname. + uname = list(os.uname()) + uname = " ".join((uname[0], *uname[2:])) # ----------------------------------------------------------------------------- @@ -138,5 +143,5 @@ def parse_headers(headers=headers): template = template_stubs body = os.linesep.join(body) -text = template % (uname, headers, body) +text = template % (uname, headers if not reproducibility else ["hidden for reproducibility"], body) print(text.strip()) From 3bc969bf59e842c9e2f9569f39434f77b911224f Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Thu, 1 May 2025 22:14:44 +0300 Subject: [PATCH 80/87] s/reproducibility/reproducible --- setup.py | 16 ++++++++-------- src/evdev/genecodes_c.py | 10 +++++----- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/setup.py b/setup.py index 3371199..1f6eaac 100755 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ ecodes_c_path = curdir / "src/evdev/ecodes.c" -def create_ecodes(headers=None, reproducibility=False): +def create_ecodes(headers=None, reproducible=False): if not headers: include_paths = set() cpath = os.environ.get("CPATH", "").strip() @@ -66,8 +66,8 @@ def create_ecodes(headers=None, reproducibility=False): print("writing %s (using %s)" % (ecodes_c_path, " ".join(headers))) with ecodes_c_path.open("w") as fh: cmd = [sys.executable, "src/evdev/genecodes_c.py"] - if reproducibility: - cmd.append("--reproducibility") + if reproducible: + cmd.append("--reproducible") cmd.extend(["--ecodes", *headers]) run(cmd, check=True, stdout=fh) @@ -77,21 +77,21 @@ class build_ecodes(Command): user_options = [ ("evdev-headers=", None, "colon-separated paths to input subsystem headers"), - ("reproducibility", None, "hide host details (host/paths) to create a reproducible output"), + ("reproducible", None, "hide host details (host/paths) to create a reproducible output"), ] def initialize_options(self): self.evdev_headers = None - self.reproducibility = False + self.reproducible = False def finalize_options(self): if self.evdev_headers: self.evdev_headers = self.evdev_headers.split(":") - if self.reproducibility is None: - self.reproducibility = False + if self.reproducible is None: + self.reproducible = False def run(self): - create_ecodes(self.evdev_headers, reproducibility=self.reproducibility) + create_ecodes(self.evdev_headers, reproducible=self.reproducible) class build_ext(_build_ext.build_ext): diff --git a/src/evdev/genecodes_c.py b/src/evdev/genecodes_c.py index 24cad27..15a6693 100644 --- a/src/evdev/genecodes_c.py +++ b/src/evdev/genecodes_c.py @@ -15,22 +15,22 @@ "/usr/include/linux/uinput.h", ] -opts, args = getopt.getopt(sys.argv[1:], "", ["ecodes", "stubs", "reproducibility"]) +opts, args = getopt.getopt(sys.argv[1:], "", ["ecodes", "stubs", "reproducible"]) if not opts: - print("usage: genecodes.py [--ecodes|--stubs] [--reproducibility] ") + print("usage: genecodes.py [--ecodes|--stubs] [--reproducible] ") exit(2) if args: headers = args -reproducibility = ("--reproducibility", "") in opts +reproducible = ("--reproducible", "") in opts # ----------------------------------------------------------------------------- macro_regex = r"#define\s+((?:KEY|ABS|REL|SW|MSC|LED|BTN|REP|SND|ID|EV|BUS|SYN|FF|UI_FF|INPUT_PROP)_\w+)" macro_regex = re.compile(macro_regex) -if reproducibility: +if reproducible: uname = "hidden for reproducibility" else: # Uname without hostname. @@ -143,5 +143,5 @@ def parse_headers(headers=headers): template = template_stubs body = os.linesep.join(body) -text = template % (uname, headers if not reproducibility else ["hidden for reproducibility"], body) +text = template % (uname, headers if not reproducible else ["hidden for reproducibility"], body) print(text.strip()) From 8f45223a11d0b48b8485e059d63896e65657dea8 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Thu, 1 May 2025 19:31:49 +0100 Subject: [PATCH 81/87] Use Generic to set precise type for InputDevice.path (#241) * Use Generic to set precise type for InputDevice.path * Update src/evdev/device.py --- src/evdev/device.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/evdev/device.py b/src/evdev/device.py index 878a937..a7f9b92 100644 --- a/src/evdev/device.py +++ b/src/evdev/device.py @@ -1,6 +1,6 @@ import contextlib import os -from typing import Dict, Iterator, List, Literal, NamedTuple, Tuple, Union, overload +from typing import Dict, Generic, Iterator, List, Literal, NamedTuple, Tuple, TypeVar, Union, overload from . import _input, ecodes, util @@ -9,6 +9,8 @@ except ImportError: from .eventio import EvdevError, EventIO +_AnyStr = TypeVar("_AnyStr", str, bytes) + class AbsInfo(NamedTuple): """Absolute axis information. @@ -100,14 +102,14 @@ def __str__(self) -> str: return msg.format(*self) # pylint: disable=not-an-iterable -class InputDevice(EventIO): +class InputDevice(EventIO, Generic[_AnyStr]): """ A linux input device from which input events can be read. """ __slots__ = ("path", "fd", "info", "name", "phys", "uniq", "_rawcapabilities", "version", "ff_effects_count") - def __init__(self, dev: Union[str, bytes, os.PathLike]): + def __init__(self, dev: Union[_AnyStr, "os.PathLike[_AnyStr]"]): """ Arguments --------- @@ -116,7 +118,7 @@ def __init__(self, dev: Union[str, bytes, os.PathLike]): """ #: Path to input device. - self.path = dev if not hasattr(dev, "__fspath__") else dev.__fspath__() + self.path: _AnyStr = dev if not hasattr(dev, "__fspath__") else dev.__fspath__() # Certain operations are possible only when the device is opened in read-write mode. try: From a5d8cf0749f15d44feb76bbed27b30a75b3c7c1f Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Thu, 1 May 2025 22:15:19 +0300 Subject: [PATCH 82/87] =?UTF-8?q?Bump=20version:=201.9.1=20=E2=86=92=201.9?= =?UTF-8?q?.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/changelog.rst | 11 +++++++++++ docs/conf.py | 2 +- pyproject.toml | 4 ++-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 4dcf62f..49f5911 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,6 +2,17 @@ Changelog --------- +1.9.2 (May 01, 2025) +==================== + +- Add the "--reproducible" build option which removes the build date and used headers from the + generated ``ecodes.c``. Example usage:: + + python -m build --config-setting=--build-option='build_ecodes --reproducible' -n + +- Use ``Generic`` to set precise type for ``InputDevice.path``. + + 1.9.1 (Feb 22, 2025) ==================== diff --git a/docs/conf.py b/docs/conf.py index 86b3d06..758f878 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -65,7 +65,7 @@ # built documents. # # The full version, including alpha/beta/rc tags. -release = "1.9.1" +release = "1.9.2" # The short X.Y version. version = release diff --git a/pyproject.toml b/pyproject.toml index d248ab2..e6a6ac7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "evdev" -version = "1.9.1" +version = "1.9.2" description = "Bindings to the Linux input handling subsystem" keywords = ["evdev", "input", "uinput"] readme = "README.md" @@ -36,7 +36,7 @@ line-length = 120 ignore = ["E265", "E241", "F403", "F401", "E401", "E731"] [tool.bumpversion] -current_version = "1.9.1" +current_version = "1.9.2" commit = true tag = true allow_dirty = true From 5227b1672cbf074287088860c855c24bb96fe6b1 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Thu, 5 Feb 2026 00:09:01 +0100 Subject: [PATCH 83/87] Fix memory leaks --- src/evdev/input.c | 49 ++++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/src/evdev/input.c b/src/evdev/input.c index 4ad0408..894db22 100644 --- a/src/evdev/input.c +++ b/src/evdev/input.c @@ -63,12 +63,12 @@ device_read(PyObject *self, PyObject *args) return NULL; } - PyObject* sec = PyLong_FromLong(event.input_event_sec); - PyObject* usec = PyLong_FromLong(event.input_event_usec); - PyObject* val = PyLong_FromLong(event.value); - PyObject* type = PyLong_FromLong(event.type); - PyObject* code = PyLong_FromLong(event.code); - PyObject* py_input_event = PyTuple_Pack(5, sec, usec, type, code, val); + PyObject *py_input_event = PyTuple_New(5); + PyTuple_SET_ITEM(py_input_event, 0, PyLong_FromLong(event.input_event_sec)); + PyTuple_SET_ITEM(py_input_event, 1, PyLong_FromLong(event.input_event_usec)); + PyTuple_SET_ITEM(py_input_event, 2, PyLong_FromLong(event.type)); + PyTuple_SET_ITEM(py_input_event, 3, PyLong_FromLong(event.code)); + PyTuple_SET_ITEM(py_input_event, 4, PyLong_FromLong(event.value)); return py_input_event; } @@ -81,14 +81,6 @@ device_read_many(PyObject *self, PyObject *args) // get device file descriptor (O_RDONLY|O_NONBLOCK) int fd = (int)PyLong_AsLong(PyTuple_GET_ITEM(args, 0)); - PyObject* py_input_event = NULL; - PyObject* events = NULL; - PyObject* sec = NULL; - PyObject* usec = NULL; - PyObject* val = NULL; - PyObject* type = NULL; - PyObject* code = NULL; - struct input_event event[64]; size_t event_size = sizeof(struct input_event); @@ -101,15 +93,15 @@ device_read_many(PyObject *self, PyObject *args) // Construct a tuple of event tuples. Each tuple is the arguments to InputEvent. size_t num_events = nread / event_size; - events = PyTuple_New(num_events); - for (size_t i = 0 ; i < num_events; i++) { - sec = PyLong_FromLong(event[i].input_event_sec); - usec = PyLong_FromLong(event[i].input_event_usec); - val = PyLong_FromLong(event[i].value); - type = PyLong_FromLong(event[i].type); - code = PyLong_FromLong(event[i].code); - py_input_event = PyTuple_Pack(5, sec, usec, type, code, val); + PyObject* events = PyTuple_New(num_events); + for (size_t i = 0 ; i < num_events; i++) { + PyObject *py_input_event = PyTuple_New(5); + PyTuple_SET_ITEM(py_input_event, 0, PyLong_FromLong(event[i].input_event_sec)); + PyTuple_SET_ITEM(py_input_event, 1, PyLong_FromLong(event[i].input_event_usec)); + PyTuple_SET_ITEM(py_input_event, 2, PyLong_FromLong(event[i].type)); + PyTuple_SET_ITEM(py_input_event, 3, PyLong_FromLong(event[i].code)); + PyTuple_SET_ITEM(py_input_event, 4, PyLong_FromLong(event[i].value)); PyTuple_SET_ITEM(events, i, py_input_event); } @@ -200,6 +192,11 @@ ioctl_capabilities(PyObject *self, PyObject *args) return capabilities; on_err: + Py_XDECREF(capabilities); + Py_XDECREF(eventcodes); + Py_XDECREF(capability); + Py_XDECREF(py_absinfo); + Py_XDECREF(absitem); PyErr_SetFromErrno(PyExc_OSError); return NULL; } @@ -408,7 +405,9 @@ ioctl_EVIOCG_bits(PyObject *self, PyObject *args) PyObject* res = PyList_New(0); for (int i=0; i<=max; i++) { if (test_bit(bytes, i)) { - PyList_Append(res, Py_BuildValue("i", i)); + PyObject *val = PyLong_FromLong(i); + PyList_Append(res, val); + Py_DECREF(val); } } @@ -523,7 +522,9 @@ ioctl_EVIOCGPROP(PyObject *self, PyObject *args) PyObject* res = PyList_New(0); for (int i=0; i Date: Thu, 5 Feb 2026 22:16:48 +0100 Subject: [PATCH 84/87] CI fixes --- .github/workflows/install.yaml | 6 +++--- .github/workflows/lint.yml | 6 +++--- .github/workflows/test.yml | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/install.yaml b/.github/workflows/install.yaml index 87502ad..f07c035 100644 --- a/.github/workflows/install.yaml +++ b/.github/workflows/install.yaml @@ -11,15 +11,15 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] include: - os: ubuntu-latest python-version: "3.8" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e293976..20d254b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,12 +11,12 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python-version: ["3.12"] + python-version: ["3.14"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b9cd26d..073d524 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,12 +11,12 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python-version: ["3.12"] + python-version: ["3.14"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} From fae2cf9d1d4a3e2700e148399f312b44b6208a56 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Thu, 5 Feb 2026 22:24:12 +0100 Subject: [PATCH 85/87] Use an SPDX license --- pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e6a6ac7..665a9b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ version = "1.9.2" description = "Bindings to the Linux input handling subsystem" keywords = ["evdev", "input", "uinput"] readme = "README.md" -license = {file = "LICENSE"} +license = "BSD-3-Clause" requires-python = ">=3.8" authors = [ { name="Georgi Valkov", email="georgi.t.valkov@gmail.com" }, @@ -22,7 +22,6 @@ classifiers = [ "Operating System :: POSIX :: Linux", "Intended Audience :: Developers", "Topic :: Software Development :: Libraries", - "License :: OSI Approved :: BSD License", "Programming Language :: Python :: Implementation :: CPython", ] From faf7bc93c6a97edc317c4cc6a8d81ab94e5ba77f Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Thu, 5 Feb 2026 22:36:13 +0100 Subject: [PATCH 86/87] Drop support for Python 3.8 and raise setuptools version to 77.0 --- .github/workflows/install.yaml | 4 ++-- docs/changelog.rst | 7 +++++++ pyproject.toml | 4 ++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/install.yaml b/.github/workflows/install.yaml index f07c035..e879179 100644 --- a/.github/workflows/install.yaml +++ b/.github/workflows/install.yaml @@ -11,10 +11,10 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] include: - os: ubuntu-latest - python-version: "3.8" + python-version: "3.9" steps: - uses: actions/checkout@v6 diff --git a/docs/changelog.rst b/docs/changelog.rst index 49f5911..bcf1636 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,13 @@ Changelog --------- +1.9.3 (Feb 05, 2025) +==================== + +- Fix several memory leaks in ``input.c``. + +- Raise the minimum supported Python version to 3.9 and the setuptools version to 77.0. + 1.9.2 (May 01, 2025) ==================== diff --git a/pyproject.toml b/pyproject.toml index 665a9b7..159460c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=61.0"] +requires = ["setuptools>=77.0"] build-backend = "setuptools.build_meta" [project] @@ -9,7 +9,7 @@ description = "Bindings to the Linux input handling subsystem" keywords = ["evdev", "input", "uinput"] readme = "README.md" license = "BSD-3-Clause" -requires-python = ">=3.8" +requires-python = ">=3.9" authors = [ { name="Georgi Valkov", email="georgi.t.valkov@gmail.com" }, ] From a47b5b5a6f79bde6823095d1105501856338aed7 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Thu, 5 Feb 2026 22:46:48 +0100 Subject: [PATCH 87/87] =?UTF-8?q?Bump=20version:=201.9.2=20=E2=86=92=201.9?= =?UTF-8?q?.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/conf.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 758f878..0be06b3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -65,7 +65,7 @@ # built documents. # # The full version, including alpha/beta/rc tags. -release = "1.9.2" +release = "1.9.3" # The short X.Y version. version = release diff --git a/pyproject.toml b/pyproject.toml index 159460c..d0b4f7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "evdev" -version = "1.9.2" +version = "1.9.3" description = "Bindings to the Linux input handling subsystem" keywords = ["evdev", "input", "uinput"] readme = "README.md" @@ -35,7 +35,7 @@ line-length = 120 ignore = ["E265", "E241", "F403", "F401", "E401", "E731"] [tool.bumpversion] -current_version = "1.9.2" +current_version = "1.9.3" commit = true tag = true allow_dirty = true