From 92d73fab4eee8f95866a6eb9be249d6f04b099ce Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sat, 28 Jul 2018 11:55:30 +0200 Subject: [PATCH 001/147] Add link to packages.debian.org --- docs/install.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/install.rst b/docs/install.rst index d1abea7..6055f80 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -10,7 +10,10 @@ Python-evdev has been packaged for the following GNU/Linux distributions: - + + + + @@ -24,7 +27,7 @@ Python-evdev has been packaged for the following GNU/Linux distributions: --!> -Consult the relevant documentation of your OS package manager for installation instructions. +Consult the documentation of your OS package manager for installation instructions. From source From 92670c1a12d8cc8df39afb0ede7be9d0db1bf68a Mon Sep 17 00:00:00 2001 From: brandonmoak Date: Sun, 5 Aug 2018 21:33:17 -0700 Subject: [PATCH 002/147] fixing typo in ff Condition, left_coeff This was preventing any force feedback effect that needs the left coefficient from working --- evdev/ff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evdev/ff.py b/evdev/ff.py index e4598d7..dd68c3f 100644 --- a/evdev/ff.py +++ b/evdev/ff.py @@ -100,7 +100,7 @@ class Condition(ctypes.Structure): ('right_saturation', _u16), ('left_saturation', _u16), ('right_coeff', _s16), - ('left_foeff', _s16), + ('left_coeff', _s16), ('deadband', _u16), ('center', _s16), ] From 34fe42c0b5bf015b4b8315833b19d32d0b4de440 Mon Sep 17 00:00:00 2001 From: Andrey Smirnov Date: Thu, 12 Jul 2018 21:32:34 -0700 Subject: [PATCH 003/147] Split "create" and "setup" parts for uinput devices creation In order to be able to enable EV_FF in uinput device, ff_max_effects fields needs to be set to a non-zero value. However the ordering of execution in uinput.py does not allow for that because indvidual events are enabled before _uinput.create() is invoked. In order to accomodate EV_FF, split _uinput.create() into two functions: - _uinput.setup(), which does the majority of device configuration. - _uinput.create(), which only creates the device without changing any of its configuration On top of that, change the code in uinput.py to make the call to setup() before individual events are enabled to enable adding ff_max_effects configuration in follow-up commits. --- evdev/uinput.c | 21 ++++++++++++++++++++- evdev/uinput.py | 4 +++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/evdev/uinput.c b/evdev/uinput.c index 791d3a9..0d2d282 100644 --- a/evdev/uinput.c +++ b/evdev/uinput.c @@ -69,7 +69,7 @@ uinput_set_phys(PyObject *self, PyObject *args) static PyObject * -uinput_create(PyObject *self, PyObject *args) { +uinput_setup(PyObject *self, PyObject *args) { int fd, len, i, abscode; uint16_t vendor, product, version, bustype; @@ -113,6 +113,22 @@ uinput_create(PyObject *self, PyObject *args) { /* goto on_err; */ /* } */ + Py_RETURN_NONE; + + on_err: + _uinput_close(fd); + PyErr_SetFromErrno(PyExc_IOError); + return NULL; +} + +static PyObject * +uinput_create(PyObject *self, PyObject *args) +{ + int fd; + + int ret = PyArg_ParseTuple(args, "i", &fd); + if (!ret) return NULL; + if (ioctl(fd, UI_DEV_CREATE) < 0) goto on_err; @@ -214,6 +230,9 @@ static PyMethodDef MethodTable[] = { { "open", uinput_open, METH_VARARGS, "Open uinput device node."}, + { "setup", uinput_setup, METH_VARARGS, + "Set an uinput device up."}, + { "create", uinput_create, METH_VARARGS, "Create an uinput device."}, diff --git a/evdev/uinput.py b/evdev/uinput.py index 4f910e2..b7c9fa5 100644 --- a/evdev/uinput.py +++ b/evdev/uinput.py @@ -131,6 +131,8 @@ def __init__(self, # Set phys name _uinput.set_phys(self.fd, phys) + _uinput.setup(self.fd, name, vendor, product, version, bustype, absinfo) + # Set device capabilities. for etype, codes in events.items(): for code in codes: @@ -149,7 +151,7 @@ def __init__(self, _uinput.enable(self.fd, etype, code) # Create the uinput device. - _uinput.create(self.fd, name, vendor, product, version, bustype, absinfo) + _uinput.create(self.fd) #: An :class:`InputDevice ` instance #: for the fake input device. ``None`` if the device cannot be From 71dcbe098ea0f5f8e5227cd35124fd6377af2817 Mon Sep 17 00:00:00 2001 From: Andrey Smirnov Date: Thu, 12 Jul 2018 21:32:44 -0700 Subject: [PATCH 004/147] Set ff_effects_max to FF_EFFECTS_MAX in new uinput devices Set ff_effects_max to FF_EFFECTS_MAX in new uinput devices, so it would be possible to create devices that emit various EV_FF events. --- evdev/uinput.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/evdev/uinput.c b/evdev/uinput.c index 0d2d282..9ebff9d 100644 --- a/evdev/uinput.c +++ b/evdev/uinput.c @@ -89,6 +89,8 @@ uinput_setup(PyObject *self, PyObject *args) { uidev.id.version = version; uidev.id.bustype = bustype; + uidev.ff_effects_max = FF_MAX_EFFECTS; + len = PyList_Size(absinfo); for (i=0; i (ABS_X, 0, 255, 0, 0, 0, 0) From aa3b157eb71d4552c0f4294f0289b7b8e4b7be55 Mon Sep 17 00:00:00 2001 From: Andrey Smirnov Date: Thu, 12 Jul 2018 21:32:56 -0700 Subject: [PATCH 005/147] Search for constants in as well A couple of important uinput constants are located in , so we need to scan that file in order to get them properly exposed. --- evdev/genecodes.py | 3 ++- setup.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/evdev/genecodes.py b/evdev/genecodes.py index 981a66d..eb6481a 100644 --- a/evdev/genecodes.py +++ b/evdev/genecodes.py @@ -20,7 +20,7 @@ #----------------------------------------------------------------------------- -macro_regex = r'#define +((?:KEY|ABS|REL|SW|MSC|LED|BTN|REP|SND|ID|EV|BUS|SYN|FF)_\w+)' +macro_regex = r'#define +((?:KEY|ABS|REL|SW|MSC|LED|BTN|REP|SND|ID|EV|BUS|SYN|FF|UI_FF)_\w+)' macro_regex = re.compile(macro_regex) uname = list(os.uname()); del uname[1] @@ -34,6 +34,7 @@ #include #else #include +#include #endif /* Automatically generated by evdev.genecodes */ diff --git a/setup.py b/setup.py index 3924880..cfa370a 100755 --- a/setup.py +++ b/setup.py @@ -70,6 +70,7 @@ def create_ecodes(headers=None): headers = [ '/usr/include/linux/input.h', '/usr/include/linux/input-event-codes.h', + '/usr/include/linux/uinput.h', ] headers = [header for header in headers if os.path.isfile(header)] From c9904b375a00d93b043104641141215cf1844070 Mon Sep 17 00:00:00 2001 From: Andrey Smirnov Date: Fri, 13 Jul 2018 10:58:35 -0700 Subject: [PATCH 006/147] Add support for handling FF effects uploads Add code and type definitions needed to handle forced-feedback effect uploads done by the client of uinput device. The pluming added is a necessary minimum to implement algorithms listed here: https://elixir.bootlin.com/linux/v4.17/source/include/uapi/linux/uinput.h#L173 --- evdev/ff.py | 16 ++++++++++++++++ evdev/uinput.c | 19 +++++++++++++++++++ evdev/uinput.py | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+) diff --git a/evdev/ff.py b/evdev/ff.py index e4598d7..cb64af9 100644 --- a/evdev/ff.py +++ b/evdev/ff.py @@ -8,6 +8,7 @@ _u16 = ctypes.c_uint16 _u32 = ctypes.c_uint32 _s16 = ctypes.c_int16 +_s32 = ctypes.c_int32 class Replay(ctypes.Structure): ''' @@ -167,6 +168,21 @@ class Effect(ctypes.Structure): ('u', EffectType) ] +class UInputUpload(ctypes.Structure): + _fields_ = [ + ('request_id', _u32), + ('retval', _s32), + ('effect', Effect), + ('old', Effect), + ] + +class UInputErase(ctypes.Structure): + _fields_ = [ + ('request_id', _u32), + ('retval', _s32), + ('effect_id', _u32), + ] + # ff_types = { # ecodes.FF_CONSTANT, # ecodes.FF_PERIODIC, diff --git a/evdev/uinput.c b/evdev/uinput.c index 9ebff9d..2580543 100644 --- a/evdev/uinput.c +++ b/evdev/uinput.c @@ -224,6 +224,25 @@ uinput_enable_event(PyObject *self, PyObject *args) return NULL; } +int _uinput_begin_upload(int fd, struct uinput_ff_upload *upload) +{ + return ioctl(fd, UI_BEGIN_FF_UPLOAD, upload); +} + +int _uinput_end_upload(int fd, struct uinput_ff_upload *upload) +{ + return ioctl(fd, UI_END_FF_UPLOAD, upload); +} + +int _uinput_begin_erase(int fd, struct uinput_ff_erase *upload) +{ + return ioctl(fd, UI_BEGIN_FF_ERASE, upload); +} + +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" diff --git a/evdev/uinput.py b/evdev/uinput.py index b7c9fa5..ee28aa9 100644 --- a/evdev/uinput.py +++ b/evdev/uinput.py @@ -8,6 +8,8 @@ from evdev import _uinput from evdev import ecodes, util, device from evdev.events import InputEvent +import evdev.ff as ff +import ctypes try: from evdev.eventio_async import EventIO @@ -153,6 +155,10 @@ def __init__(self, # Create the uinput device. _uinput.create(self.fd) + self.dll = ctypes.CDLL(_uinput.__file__) + self.dll._uinput_begin_upload.restype = ctypes.c_int + self.dll._uinput_end_upload.restype = ctypes.c_int + #: An :class:`InputDevice ` instance #: for the fake input device. ``None`` if the device cannot be #: opened for reading and writing. @@ -208,6 +214,35 @@ def capabilities(self, verbose=False, absinfo=True): return self.device.capabilities(verbose, absinfo) + def begin_upload(self, effect_id): + upload = ff.UInputUpload() + upload.effect_id = effect_id + + if self.dll._uinput_begin_upload(self.fd, ctypes.byref(upload)): + raise UInputError('Failed to begin uinput upload: ' + + os.strerror()) + + return upload + + def end_upload(self, upload): + if self.dll._uinput_end_upload(self.fd, ctypes.byref(upload)): + raise UInputError('Failed to end uinput upload: ' + + os.strerror()) + + def begin_erase(self, effect_id): + erase = ff.UInputErase() + erase.effect_id = effect_id + + if self.dll._uinput_begin_erase(self.fd, ctypes.byref(erase)): + raise UInputError('Failed to begin uinput erase: ' + + os.strerror()) + return erase + + def end_erase(self, erase): + if self.dll._uinput_end_erase(self.fd, ctypes.byref(erase)): + raise UInputError('Failed to end uinput erase: ' + + os.strerror()) + def _verify(self): ''' Verify that an uinput device exists and is readable and writable From 1fb9e4cf75b689a52ee920ede5dc51af82d90521 Mon Sep 17 00:00:00 2001 From: Andrey Smirnov Date: Thu, 9 Aug 2018 16:02:40 -0700 Subject: [PATCH 007/147] Add ruble effect tracing to print_ff_effects() --- evdev/input.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/evdev/input.c b/evdev/input.c index 86c54e9..b3e6e08 100644 --- a/evdev/input.c +++ b/evdev/input.c @@ -381,6 +381,11 @@ void print_ff_effect(struct ff_effect* effect) { effect->u.constant.envelope.attack_level, effect->u.constant.envelope.fade_length, effect->u.constant.envelope.fade_level); + + case FF_RUMBLE: + fprintf(stderr, " rumble: (%d, %d)\n", + effect->u.rumble.strong_magnitude, + effect->u.rumble.weak_magnitude); break; } } From 3c6a12eee054e20453a00368ddf50b37ef0b9946 Mon Sep 17 00:00:00 2001 From: Andrey Smirnov Date: Thu, 9 Aug 2018 16:03:28 -0700 Subject: [PATCH 008/147] Add examples of working with FF-effects --- docs/tutorial.rst | 90 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 14337c5..9a5129e 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -362,3 +362,93 @@ Create ``uinput`` device with capabilities of another device .. _`async/await`: https://docs.python.org/3/library/asyncio-task.html + +Create ``uinput`` device capable of recieving FF-effects +======================================================== + +:: + import asyncio + from evdev import UInput, categorize, ecodes as e + + cap = { + e.EV_FF : [ e.FF_RUMBLE ], + e.EV_KEY : [e.KEY_A, e.KEY_B], + } + + try: + ui = UInput(cap, name='test-controller', version=0x3) + except UInputError: + print('Please make sure you have appropriate permissions to access /dev/uinput and the "uinput" kernel module is loaded') + raise + + async def print_events(device): + async for event in device.async_read_loop(): + print(categorize(event)) + # + # Wait for EV_UINPUT event that will signal us that effect + # upload/erase operation is in progress + # + if event.type != e.EV_UINPUT: + pass + + if event.code == e.UI_FF_UPLOAD: + upload = device.begin_upload(event.value) + upload.retval = 0 + + print('[upload] effect_id: {}, type: {}' + .format(upload.effect_id, upload.effect.type)) + device.end_upload(upload) + + elif event.code == e.UI_FF_ERASE: + erase = device.begin_erase(event.value) + print('[erase] effect_id {}'.format(erase.effect_id)) + + erase.retval = 0 + device.end_erase(erase) + + asyncio.ensure_future(print_events(ui)) + loop = asyncio.get_event_loop() + loop.run_forever() + + +Injecting FF-events into first FF-capable device found +====================================================== + +:: + from evdev import ecodes as e + from glob import glob + + dev = None + + # + # Find first EV_FF capable event device (that we have permissions to + # use) + # + for name in glob('/dev/input/event*'): + try: + dev = InputDevice(name) + + if e.EV_FF in dev.capabilities(): + break + except PermissionError as e: + print(e) + continue + + if dev: + duration_ms = 1000 + + rumble = ff.Rumble(strong_magnitude=0x0000, + weak_magnitude=0xffff) + effect_type = ff.EffectType(ff_rumble_effect=rumble) + effect = ff.Effect(e.FF_RUMBLE, -1, 0, + ff.Trigger(0, 0), + ff.Replay(duration_ms, 0), + effect_type) + + repeat_count = 1 + effect_id = dev.upload_effect(effect) + dev.write(e.EV_FF, effect_id, repeat_count) + dev.erase_effect(effect_id) + else: + print('No FF capable device was found!') + From df81554f4f166662da1ad53da4ea686383bb76a4 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Tue, 21 Aug 2018 20:22:41 +0200 Subject: [PATCH 009/147] Simplify effect upload examples --- docs/tutorial.rst | 94 +++++++++++++++++++---------------------------- 1 file changed, 38 insertions(+), 56 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 9a5129e..d0a4279 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -367,41 +367,36 @@ Create ``uinput`` device capable of recieving FF-effects ======================================================== :: + import asyncio - from evdev import UInput, categorize, ecodes as e + from evdev import UInput, categorize, ecodes cap = { - e.EV_FF : [ e.FF_RUMBLE ], - e.EV_KEY : [e.KEY_A, e.KEY_B], + ecodes.EV_FF: [ecodes.FF_RUMBLE ], + ecodes.EV_KEY: [ecodes.KEY_A, ecodes.KEY_B] } - try: - ui = UInput(cap, name='test-controller', version=0x3) - except UInputError: - print('Please make sure you have appropriate permissions to access /dev/uinput and the "uinput" kernel module is loaded') - raise + ui = UInput(cap, name='test-controller', version=0x3) async def print_events(device): async for event in device.async_read_loop(): print(categorize(event)) - # - # Wait for EV_UINPUT event that will signal us that effect - # upload/erase operation is in progress - # - if event.type != e.EV_UINPUT: + + # Wait for an EV_UINPUT event that will signal us that an + # effect upload/erase operation is in progress. + if event.type != ecodes.EV_UINPUT: pass - if event.code == e.UI_FF_UPLOAD: + if event.code == ecodes.UI_FF_UPLOAD: upload = device.begin_upload(event.value) upload.retval = 0 - print('[upload] effect_id: {}, type: {}' - .format(upload.effect_id, upload.effect.type)) + print(f'[upload] effect_id: {upload.effect_id}, type: {upload.effect.type}') device.end_upload(upload) - elif event.code == e.UI_FF_ERASE: + elif event.code == ecodes.UI_FF_ERASE: erase = device.begin_erase(event.value) - print('[erase] effect_id {}'.format(erase.effect_id)) + print(f'[erase] effect_id {erase.effect_id}') erase.retval = 0 device.end_erase(erase) @@ -411,44 +406,31 @@ Create ``uinput`` device capable of recieving FF-effects loop.run_forever() -Injecting FF-events into first FF-capable device found -====================================================== +Injecting an FF-event into first FF-capable device found +======================================================== :: - from evdev import ecodes as e - from glob import glob - - dev = None - - # - # Find first EV_FF capable event device (that we have permissions to - # use) - # - for name in glob('/dev/input/event*'): - try: - dev = InputDevice(name) - - if e.EV_FF in dev.capabilities(): - break - except PermissionError as e: - print(e) - continue - - if dev: - duration_ms = 1000 - - rumble = ff.Rumble(strong_magnitude=0x0000, - weak_magnitude=0xffff) - effect_type = ff.EffectType(ff_rumble_effect=rumble) - effect = ff.Effect(e.FF_RUMBLE, -1, 0, - ff.Trigger(0, 0), - ff.Replay(duration_ms, 0), - effect_type) - - repeat_count = 1 - effect_id = dev.upload_effect(effect) - dev.write(e.EV_FF, effect_id, repeat_count) - dev.erase_effect(effect_id) - else: - print('No FF capable device was found!') + from evdev import ecodes, InputDevice, ff + + # Find first EV_FF capable event device (that we have permissions to use). + for name in evdev.list_devices(): + dev = InputDevice(name) + if ecodes.EV_FF in dev.capabilities(): + break + + rumble = ff.Rumble(strong_magnitude=0x0000, weak_magnitude=0xffff) + effect_type = ff.EffectType(ff_rumble_effect=rumble) + duration_ms = 1000 + + effect = ff.Effect( + ecodes.FF_RUMBLE, -1, 0, + ff.Trigger(0, 0), + ff.Replay(duration_ms, 0), + ff.EffectType(ff_rumble_effect=rumble) + ) + + repeat_count = 1 + effect_id = dev.upload_effect(effect) + dev.write(e.EV_FF, effect_id, repeat_count) + dev.erase_effect(effect_id) From 78a4727772a82f147b2b4049e5ace0ecd775e889 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Mon, 27 Aug 2018 21:11:05 +0200 Subject: [PATCH 010/147] Update changelog --- docs/changelog.rst | 9 +++++++++ evdev/uinput.c | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 1afbc18..03c0a43 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,14 @@ Changelog --------- +1.1.0 (Aug 27, 2018) +==================== + +- Add support for handling force-feedback effect uploads (many thanks to `@ndreys`). + +- Fix typo preventing ff effects that need left coefficients from working. + + 1.0.0 (Jun 02, 2018) ==================== @@ -335,6 +343,7 @@ Changelog .. _`@isia`: https://github.com/isia .. _`@forsenonlhaimaisentito`: https://github.com/forsenonlhaimaisentito .. _`@paulo-raca`: https://github.com/paulo-raca +.. _`@ndreys`: https://github.com/ndreys .. _issue21121: http://bugs.python.org/issue21121 .. _`#63`: https://github.com/gvalkov/python-evdev/issues/63 diff --git a/evdev/uinput.c b/evdev/uinput.c index 2580543..011d868 100644 --- a/evdev/uinput.c +++ b/evdev/uinput.c @@ -268,7 +268,7 @@ static PyMethodDef MethodTable[] = { { "set_phys", uinput_set_phys, METH_VARARGS, "Set physical path"}, - + { NULL, NULL, 0, NULL} }; From ae19a3a9d0f9637f0cb0166426a009ca42514488 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Mon, 27 Aug 2018 21:11:25 +0200 Subject: [PATCH 011/147] Bump version: 1.0.0 -> 1.1.0 --- docs/conf.py | 2 +- setup.cfg | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 01de55f..35f7f4f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -60,7 +60,7 @@ # built documents. # # The full version, including alpha/beta/rc tags. -release = '1.0.0' +release = '1.1.0' # The short X.Y version. version = release diff --git a/setup.cfg b/setup.cfg index 5b380df..e0fe229 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.0.0 +current_version = 1.1.0 message = Bump version: {current_version} -> {new_version} commit = True tag = True diff --git a/setup.py b/setup.py index cfa370a..3f28181 100755 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ #----------------------------------------------------------------------------- kw = { 'name': 'evdev', - 'version': '1.0.0', + 'version': '1.1.0', 'description': 'Bindings to the Linux input handling subsystem', 'long_description': open(pjoin(here, 'README.rst')).read(), From 686cc5094100206c6d7fdbd7ef9de2c9a6906ad8 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sat, 1 Sep 2018 11:39:38 +0200 Subject: [PATCH 012/147] Check if FF_MAX_EFFECTS is defined --- evdev/uinput.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/evdev/uinput.c b/evdev/uinput.c index 011d868..528d75f 100644 --- a/evdev/uinput.c +++ b/evdev/uinput.c @@ -16,6 +16,12 @@ #include #endif + +// Workaround for installing on kernels newer than 4.4. +#ifndef FF_MAX_EFFECTS +#define FF_MAX_EFFECTS FF_GAIN; +#endif + int _uinput_close(int fd) { if (ioctl(fd, UI_DEV_DESTROY) < 0) { @@ -88,7 +94,6 @@ 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; len = PyList_Size(absinfo); From 481b697ba1df154454e186274faa05ad92ba321e Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sat, 1 Sep 2018 19:12:27 +0200 Subject: [PATCH 013/147] Fix invalid example --- docs/tutorial.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index d0a4279..e229244 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -325,7 +325,7 @@ Specifying ``uinput`` device options ... (e.ABS_X, AbsInfo(value=0, min=0, max=255, ... fuzz=0, flat=0, resolution=0)), ... (e.ABS_Y, AbsInfo(0, 0, 255, 0, 0, 0)), - ... (e.ABS_MT_POSITION_X, (0, 255, 128, 0)) ] + ... (e.ABS_MT_POSITION_X, (0, 128, 255, 0)) ] ... } >>> ui = UInput(cap, name='example-device', version=0x3) From a017827a5102d39d639d23affdc8e7b159e6e231 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sat, 1 Sep 2018 18:35:02 +0200 Subject: [PATCH 014/147] Fix handling of absinfo capabilities --- evdev/uinput.py | 55 ++++++++++++++++++++++++------------------------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/evdev/uinput.py b/evdev/uinput.py index ee28aa9..eafdd4f 100644 --- a/evdev/uinput.py +++ b/evdev/uinput.py @@ -121,21 +121,37 @@ def __init__(self, if not events: events = {ecodes.EV_KEY: ecodes.keys.keys()} - # The min, max, fuzz and flat values for the absolute axis for - # a given code. - absinfo = [] - self._verify() #: Write-only, non-blocking file descriptor to the uinput device node. self.fd = _uinput.open(devnode) + # Prepare the list of events for passing to _uinput.enable and _uinput.setup. + absinfo, prepared_events = self._prepare_events(events) + # Set phys name _uinput.set_phys(self.fd, phys) + for etype, code in prepared_events: + _uinput.enable(self.fd, etype, code) + _uinput.setup(self.fd, name, vendor, product, version, bustype, absinfo) - # Set device capabilities. + # Create the uinput device. + _uinput.create(self.fd) + + self.dll = ctypes.CDLL(_uinput.__file__) + self.dll._uinput_begin_upload.restype = ctypes.c_int + self.dll._uinput_end_upload.restype = ctypes.c_int + + #: 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() + + def _prepare_events(self, events): + '''Prepare events for passing to _uinput.enable and _uinput.setup''' + absinfo, prepared_events = [], [] for etype, codes in events.items(): for code in codes: # Handle max, min, fuzz, flat. @@ -148,21 +164,8 @@ def __init__(self, f.extend([0] * (6 - len(code[1]))) absinfo.append(f) code = code[0] - - # TODO: remove a lot of unnecessary packing/unpacking - _uinput.enable(self.fd, etype, code) - - # Create the uinput device. - _uinput.create(self.fd) - - self.dll = ctypes.CDLL(_uinput.__file__) - self.dll._uinput_begin_upload.restype = ctypes.c_int - self.dll._uinput_end_upload.restype = ctypes.c_int - - #: 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() + prepared_events.append((etype, code)) + return absinfo, prepared_events def __enter__(self): return self @@ -219,29 +222,25 @@ def begin_upload(self, effect_id): upload.effect_id = effect_id if self.dll._uinput_begin_upload(self.fd, ctypes.byref(upload)): - raise UInputError('Failed to begin uinput upload: ' + - os.strerror()) + raise UInputError('Failed to begin uinput upload: ' + os.strerror()) return upload def end_upload(self, upload): if self.dll._uinput_end_upload(self.fd, ctypes.byref(upload)): - raise UInputError('Failed to end uinput upload: ' + - os.strerror()) + raise UInputError('Failed to end uinput upload: ' + os.strerror()) def begin_erase(self, effect_id): erase = ff.UInputErase() erase.effect_id = effect_id if self.dll._uinput_begin_erase(self.fd, ctypes.byref(erase)): - raise UInputError('Failed to begin uinput erase: ' + - os.strerror()) + raise UInputError('Failed to begin uinput erase: ' + os.strerror()) return erase def end_erase(self, erase): if self.dll._uinput_end_erase(self.fd, ctypes.byref(erase)): - raise UInputError('Failed to end uinput erase: ' + - os.strerror()) + raise UInputError('Failed to end uinput erase: ' + os.strerror()) def _verify(self): ''' From 34a7535e8e160c78bf923adc48a1f1f6bcdcaae8 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sat, 1 Sep 2018 19:37:44 +0200 Subject: [PATCH 015/147] Bump version: 1.1.0 -> 1.1.2 --- docs/changelog.rst | 8 ++++++++ docs/conf.py | 2 +- setup.cfg | 2 +- setup.py | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 03c0a43..74ffe0e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,14 @@ Changelog --------- +1.1.2 (Sep 1, 2018) +==================== + +- Fix installation on kernels <= 4.4. + +- Fix uinput creation ignoring absinfo settings. + + 1.1.0 (Aug 27, 2018) ==================== diff --git a/docs/conf.py b/docs/conf.py index 35f7f4f..a0b9c03 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -60,7 +60,7 @@ # built documents. # # The full version, including alpha/beta/rc tags. -release = '1.1.0' +release = '1.1.2' # The short X.Y version. version = release diff --git a/setup.cfg b/setup.cfg index e0fe229..79e6c99 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.1.0 +current_version = 1.1.2 message = Bump version: {current_version} -> {new_version} commit = True tag = True diff --git a/setup.py b/setup.py index 3f28181..c3e9e60 100755 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ #----------------------------------------------------------------------------- kw = { 'name': 'evdev', - 'version': '1.1.0', + 'version': '1.1.2', 'description': 'Bindings to the Linux input handling subsystem', 'long_description': open(pjoin(here, 'README.rst')).read(), From 9c027ce3e5564375a20cfcb4a8d95df2565af793 Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Fri, 5 Oct 2018 08:32:13 +1000 Subject: [PATCH 016/147] input: fix unintended fallthrough --- evdev/input.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evdev/input.c b/evdev/input.c index b3e6e08..b37d3b7 100644 --- a/evdev/input.c +++ b/evdev/input.c @@ -381,7 +381,7 @@ void print_ff_effect(struct ff_effect* effect) { effect->u.constant.envelope.attack_level, effect->u.constant.envelope.fade_length, effect->u.constant.envelope.fade_level); - + break; case FF_RUMBLE: fprintf(stderr, " rumble: (%d, %d)\n", effect->u.rumble.strong_magnitude, From 061417d37f48ca6d77d6a5aacc4faa94be31cf62 Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Fri, 5 Oct 2018 08:41:07 +1000 Subject: [PATCH 017/147] input: fix ioctl return value handling() If any of those ioctls fail, the returned data is undefined. --- evdev/input.c | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/evdev/input.c b/evdev/input.c index b37d3b7..67b9348 100644 --- a/evdev/input.c +++ b/evdev/input.c @@ -250,7 +250,10 @@ ioctl_EVIOCGREP(PyObject *self, PyObject *args) ret = PyArg_ParseTuple(args, "i", &fd); if (!ret) return NULL; - ioctl(fd, EVIOCGREP, &rep); + ret = ioctl(fd, EVIOCGREP, &rep); + if (ret == -1) + return NULL; + return Py_BuildValue("(ii)", rep[0], rep[1]); } @@ -265,6 +268,9 @@ ioctl_EVIOCSREP(PyObject *self, PyObject *args) if (!ret) return NULL; ret = ioctl(fd, EVIOCSREP, &rep); + if (ret == -1) + return NULL; + return Py_BuildValue("i", ret); } @@ -277,6 +283,9 @@ ioctl_EVIOCGVERSION(PyObject *self, PyObject *args) if (!ret) return NULL; ret = ioctl(fd, EVIOCGVERSION, &res); + if (ret == -1) + return NULL; + return Py_BuildValue("i", res); } @@ -338,6 +347,9 @@ ioctl_EVIOCG_bits(PyObject *self, PyObject *args) break; } + if (ret == -1) + return NULL; + PyObject* res = PyList_New(0); for (int i=0; i Date: Fri, 5 Oct 2018 08:30:03 +1000 Subject: [PATCH 018/147] uinput: avoid possible unterminated string. This is a false positive because the kernel does the right thing and takes UINPUT_MAX_NAME_SIZE into account. But this shuts up coverity and other static analyzers so I think we can live with a 79 character restriction instead of 80. Also change to use sizeof(), not that it really makes a difference but it allows for the array size to be changed while leaving the constant as-is, impossible as that is. --- evdev/uinput.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evdev/uinput.c b/evdev/uinput.c index 528d75f..96a4fec 100644 --- a/evdev/uinput.c +++ b/evdev/uinput.c @@ -89,7 +89,7 @@ uinput_setup(PyObject *self, PyObject *args) { if (!ret) return NULL; memset(&uidev, 0, sizeof(uidev)); - strncpy(uidev.name, name, UINPUT_MAX_NAME_SIZE); + strncpy(uidev.name, name, sizeof(uidev.name) - 1); uidev.id.vendor = vendor; uidev.id.product = product; uidev.id.version = version; From 316ae76020e4e578620ec3ff0c0da9522c28d916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20V=C3=A1radi?= Date: Mon, 7 Jan 2019 11:44:39 +0100 Subject: [PATCH 019/147] Use H to parse uinput_setup arguments --- evdev/uinput.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evdev/uinput.c b/evdev/uinput.c index 96a4fec..d7e3406 100644 --- a/evdev/uinput.c +++ b/evdev/uinput.c @@ -84,7 +84,7 @@ uinput_setup(PyObject *self, PyObject *args) { struct uinput_user_dev uidev; const char* name; - int ret = PyArg_ParseTuple(args, "ishhhhO", &fd, &name, &vendor, + int ret = PyArg_ParseTuple(args, "isHHHHO", &fd, &name, &vendor, &product, &version, &bustype, &absinfo); if (!ret) return NULL; From 52576087e647d958cf64cd46479a1854d297c3e7 Mon Sep 17 00:00:00 2001 From: Linus Date: Sun, 31 Mar 2019 04:39:00 +0200 Subject: [PATCH 020/147] Add UInput support for the resolution parameter in AbsInfo Creating a UInput device always ignored the resolution parameter since the c implementation was old for current linux kernels. --- evdev/uinput.c | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/evdev/uinput.c b/evdev/uinput.c index d7e3406..cc23d7f 100644 --- a/evdev/uinput.c +++ b/evdev/uinput.c @@ -74,6 +74,68 @@ uinput_set_phys(PyObject *self, PyObject *args) } +#if defined(UI_DEV_SETUP) && defined(UI_ABS_SETUP) +// New variant is not supported in old linux kernels and FreeBSD +static PyObject * +uinput_setup(PyObject *self, PyObject *args) { + int fd, len, i; + uint16_t vendor, product, version, bustype; + + 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); + if (!ret) return NULL; + + // Setup absinfo: + len = PyList_Size(absinfo); + for (i=0; i (ABS_X, 0, 255, 0, 0, 0, 0) + item = PyList_GetItem(absinfo, i); + + memset(&abs_setup, 0, sizeof(abs_setup)); // Clear struct + abs_setup.code = PyLong_AsLong(PyList_GetItem(item, 0)); + abs_setup.absinfo.value = PyLong_AsLong(PyList_GetItem(item, 1)); + abs_setup.absinfo.minimum = PyLong_AsLong(PyList_GetItem(item, 2)); + abs_setup.absinfo.maximum = PyLong_AsLong(PyList_GetItem(item, 3)); + abs_setup.absinfo.fuzz = PyLong_AsLong(PyList_GetItem(item, 4)); + abs_setup.absinfo.flat = PyLong_AsLong(PyList_GetItem(item, 5)); + abs_setup.absinfo.resolution = PyLong_AsLong(PyList_GetItem(item, 6)); + + if(ioctl(fd, UI_ABS_SETUP, &abs_setup) < 0) + goto on_err; + } + + + // Setup evdev: + struct uinput_setup usetup; + + memset(&usetup, 0, sizeof(usetup)); + strncpy(usetup.name, name, sizeof(usetup.name) - 1); + usetup.id.vendor = vendor; + usetup.id.product = product; + usetup.id.version = version; + usetup.id.bustype = bustype; + usetup.ff_effects_max = FF_MAX_EFFECTS; + ioctl(fd, UI_DEV_SETUP, &usetup); + + if(ioctl(fd, UI_DEV_SETUP, &usetup) < 0) + goto on_err; + + + Py_RETURN_NONE; + + on_err: + _uinput_close(fd); + PyErr_SetFromErrno(PyExc_IOError); + return NULL; +} +#else +// Old variant (fallback): static PyObject * uinput_setup(PyObject *self, PyObject *args) { int fd, len, i, abscode; @@ -127,6 +189,8 @@ uinput_setup(PyObject *self, PyObject *args) { PyErr_SetFromErrno(PyExc_IOError); return NULL; } +#endif + static PyObject * uinput_create(PyObject *self, PyObject *args) From 2c5d62dccaf56b659a1cadb7f335c83173da1e7c Mon Sep 17 00:00:00 2001 From: Linus Date: Tue, 2 Apr 2019 19:01:24 +0200 Subject: [PATCH 021/147] Removed uncecessary duplicated ioctl-call --- evdev/uinput.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/evdev/uinput.c b/evdev/uinput.c index cc23d7f..5b5b74c 100644 --- a/evdev/uinput.c +++ b/evdev/uinput.c @@ -93,7 +93,7 @@ uinput_setup(PyObject *self, PyObject *args) { // Setup absinfo: len = PyList_Size(absinfo); for (i=0; i (ABS_X, 0, 255, 0, 0, 0, 0) item = PyList_GetItem(absinfo, i); @@ -113,7 +113,7 @@ uinput_setup(PyObject *self, PyObject *args) { // Setup evdev: struct uinput_setup usetup; - + memset(&usetup, 0, sizeof(usetup)); strncpy(usetup.name, name, sizeof(usetup.name) - 1); usetup.id.vendor = vendor; @@ -121,7 +121,6 @@ uinput_setup(PyObject *self, PyObject *args) { usetup.id.version = version; usetup.id.bustype = bustype; usetup.ff_effects_max = FF_MAX_EFFECTS; - ioctl(fd, UI_DEV_SETUP, &usetup); if(ioctl(fd, UI_DEV_SETUP, &usetup) < 0) goto on_err; From 6799dd8d101fe925d9472c6e3fa15c9eebf2e371 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 7 Apr 2019 17:28:03 +0200 Subject: [PATCH 022/147] Add comments and remove extra newlines --- evdev/uinput.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/evdev/uinput.c b/evdev/uinput.c index 5b5b74c..2f1bee1 100644 --- a/evdev/uinput.c +++ b/evdev/uinput.c @@ -74,8 +74,12 @@ uinput_set_phys(PyObject *self, PyObject *args) } +// Different kernel versions have different device setup methods. You can read +// more about it here: +// https://github.com/torvalds/linux/commit/052876f8e5aec887d22c4d06e54aa5531ffcec75 + +// Setup function for kernel >= v4.5 #if defined(UI_DEV_SETUP) && defined(UI_ABS_SETUP) -// New variant is not supported in old linux kernels and FreeBSD static PyObject * uinput_setup(PyObject *self, PyObject *args) { int fd, len, i; @@ -110,7 +114,6 @@ uinput_setup(PyObject *self, PyObject *args) { goto on_err; } - // Setup evdev: struct uinput_setup usetup; @@ -125,7 +128,6 @@ uinput_setup(PyObject *self, PyObject *args) { if(ioctl(fd, UI_DEV_SETUP, &usetup) < 0) goto on_err; - Py_RETURN_NONE; on_err: @@ -133,8 +135,9 @@ uinput_setup(PyObject *self, PyObject *args) { PyErr_SetFromErrno(PyExc_IOError); return NULL; } + +// Fallback setup function (Linux <= 4.5 and FreeBSD). #else -// Old variant (fallback): static PyObject * uinput_setup(PyObject *self, PyObject *args) { int fd, len, i, abscode; From 1c7773c750c57834ad96a2505666981594fd1c5c Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 7 Apr 2019 17:28:16 +0200 Subject: [PATCH 023/147] Remove dead code --- evdev/uinput.c | 8 -------- 1 file changed, 8 deletions(-) diff --git a/evdev/uinput.c b/evdev/uinput.c index 2f1bee1..192568d 100644 --- a/evdev/uinput.c +++ b/evdev/uinput.c @@ -176,14 +176,6 @@ uinput_setup(PyObject *self, PyObject *args) { if (write(fd, &uidev, sizeof(uidev)) != sizeof(uidev)) goto on_err; - /* if (ioctl(fd, UI_SET_EVBIT, EV_KEY) < 0) */ - /* goto on_err; */ - /* int i; */ - /* for (i=0; i Date: Sun, 7 Apr 2019 17:40:25 +0200 Subject: [PATCH 024/147] Bump version: 1.1.2 -> 1.2.0 --- docs/changelog.rst | 16 +++++++++++++++- docs/conf.py | 2 +- setup.cfg | 2 +- setup.py | 2 +- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 74ffe0e..0af74a5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,17 @@ Changelog --------- +1.2.0 (Apr 7, 2019) +==================== + +- Add UInput support for the resolution parameter in AbsInfo. This brings + support for the new method of uinput device setup, which was `introduced in + Linux 4.5`_ (thanks to `@LinusCDE`_). + +- Vendor and product identifiers can be greater or equal to `0x8000` (thanks + `@ivaradi`_). + + 1.1.2 (Sep 1, 2018) ==================== @@ -12,7 +23,7 @@ Changelog 1.1.0 (Aug 27, 2018) ==================== -- Add support for handling force-feedback effect uploads (many thanks to `@ndreys`). +- Add support for handling force-feedback effect uploads (many thanks to `@ndreys`_). - Fix typo preventing ff effects that need left coefficients from working. @@ -352,7 +363,10 @@ Changelog .. _`@forsenonlhaimaisentito`: https://github.com/forsenonlhaimaisentito .. _`@paulo-raca`: https://github.com/paulo-raca .. _`@ndreys`: https://github.com/ndreys +.. _`@LinusCDE`: https://github.com/gvalkov/python-evdev/pulls/LinusCDE +.. _`ivaradi`: https://github.com/gvalkov/python-evdev/pull/104 +.. _`introduced in Linux 4.5`: https://github.com/torvalds/linux/commit/052876f8e5aec887d22c4d06e54aa5531ffcec75 .. _issue21121: http://bugs.python.org/issue21121 .. _`#63`: https://github.com/gvalkov/python-evdev/issues/63 .. _`#63`: https://github.com/gvalkov/python-evdev/issues/67 diff --git a/docs/conf.py b/docs/conf.py index a0b9c03..f7e4a82 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -60,7 +60,7 @@ # built documents. # # The full version, including alpha/beta/rc tags. -release = '1.1.2' +release = '1.2.0' # The short X.Y version. version = release diff --git a/setup.cfg b/setup.cfg index 79e6c99..d1ce534 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.1.2 +current_version = 1.2.0 message = Bump version: {current_version} -> {new_version} commit = True tag = True diff --git a/setup.py b/setup.py index c3e9e60..086bdc8 100755 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ #----------------------------------------------------------------------------- kw = { 'name': 'evdev', - 'version': '1.1.2', + 'version': '1.2.0', 'description': 'Bindings to the Linux input handling subsystem', 'long_description': open(pjoin(here, 'README.rst')).read(), From 2e1c7f952077a733066095b0f08fc51e279d0f10 Mon Sep 17 00:00:00 2001 From: Stephen Kitt Date: Thu, 11 Jul 2019 17:58:05 +0200 Subject: [PATCH 025/147] Fix a couple of typos These were flagged by Lintian, Debian's code-quality tool. Signed-off-by: Stephen Kitt --- docs/changelog.rst | 2 +- docs/tutorial.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0af74a5..265de54 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -134,7 +134,7 @@ Changelog 0.6.1 (Jun 04, 2016) ==================== -- Dissable tty echoing while evtest is running. +- Disable tty echoing while evtest is running. - Allow evtest to listen to more than one devices. - The setup.py script now allows the location of the input header files to be diff --git a/docs/tutorial.rst b/docs/tutorial.rst index e229244..92a8780 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -363,7 +363,7 @@ Create ``uinput`` device with capabilities of another device .. _`async/await`: https://docs.python.org/3/library/asyncio-task.html -Create ``uinput`` device capable of recieving FF-effects +Create ``uinput`` device capable of receiving FF-effects ======================================================== :: From bcafe723e489e00441bbd0c86d252df63c422e2c Mon Sep 17 00:00:00 2001 From: Khem Raj Date: Sat, 30 Nov 2019 11:21:20 -0800 Subject: [PATCH 026/147] Fix build on 32bit arches with 64bit time_t time element is deprecated on new input_event structure in kernel's input.h [1] [1] https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit?id=152194fe9c3f --- evdev/input.c | 13 +++++++++---- evdev/uinput.c | 9 ++++++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/evdev/input.c b/evdev/input.c index 67b9348..432db92 100644 --- a/evdev/input.c +++ b/evdev/input.c @@ -24,6 +24,11 @@ #include #endif +#ifndef input_event_sec +#define input_event_sec time.tv_sec +#define input_event_usec time.tv_usec +#endif + #define MAX_NAME_SIZE 256 extern char* EV_NAME[EV_CNT]; @@ -60,8 +65,8 @@ device_read(PyObject *self, PyObject *args) return NULL; } - PyObject* sec = PyLong_FromLong(event.time.tv_sec); - PyObject* usec = PyLong_FromLong(event.time.tv_usec); + 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; @@ -102,8 +107,8 @@ device_read_many(PyObject *self, PyObject *args) // Construct a list of event tuples, which we'll make sense of in Python for (unsigned i = 0 ; i < nread/event_size ; i++) { - sec = PyLong_FromLong(event[i].time.tv_sec); - usec = PyLong_FromLong(event[i].time.tv_usec); + sec = PyLong_FromLong(event[i].input_event_sec); + usec = PyLong_FromLong(event[i].input_event_usec); val = PyLong_FromLong(event[i].value); py_input_event = Py_BuildValue("OOhhO", sec, usec, event[i].type, event[i].code, val); diff --git a/evdev/uinput.c b/evdev/uinput.c index 192568d..56fe86c 100644 --- a/evdev/uinput.c +++ b/evdev/uinput.c @@ -16,6 +16,10 @@ #include #endif +#ifndef input_event_sec +#define input_event_sec time.tv_sec +#define input_event_usec time.tv_usec +#endif // Workaround for installing on kernels newer than 4.4. #ifndef FF_MAX_EFFECTS @@ -232,8 +236,11 @@ uinput_write(PyObject *self, PyObject *args) if (!ret) return NULL; struct input_event event; + struct timeval tval; memset(&event, 0, sizeof(event)); - gettimeofday(&event.time, 0); + gettimeofday(&tval, 0); + event.input_event_usec = tval.tv_usec; + event.input_event_sec = tval.tv_sec; event.type = type; event.code = code; event.value = value; From a4cf8b5b6223aef1851fb8d5cf49af614c1c8c2f Mon Sep 17 00:00:00 2001 From: Quoing Date: Sat, 21 Dec 2019 13:30:10 +0100 Subject: [PATCH 027/147] Functionality to set device properties eg. INPUT_PROP_DIRECT or INPUT_PROP_POINTER --- .gitignore | 1 + evdev/device.py | 5 ++ evdev/geniprops.py | 116 +++++++++++++++++++++++++++++++++++++++++++++ evdev/input.c | 28 +++++++++++ evdev/uinput.c | 23 +++++++++ evdev/uinput.py | 6 ++- setup.py | 73 +++++++++++++++++++++++++++- 7 files changed, 249 insertions(+), 3 deletions(-) create mode 100644 evdev/geniprops.py diff --git a/.gitignore b/.gitignore index 7180328..8fc000b 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ tags TAGS evdev/*.so evdev/ecodes.c +evdev/iprops.c docs/_build .#* __pycache__ diff --git a/evdev/device.py b/evdev/device.py index de0da82..6c8feec 100644 --- a/evdev/device.py +++ b/evdev/device.py @@ -223,6 +223,11 @@ def capabilities(self, verbose=False, absinfo=True): else: return self._capabilities(absinfo) + def props(self): + props = _input.ioctl_EVIOCGPROP(self.fd) + + return props + def leds(self, verbose=False): ''' Return currently set LED keys. diff --git a/evdev/geniprops.py b/evdev/geniprops.py new file mode 100644 index 0000000..6a60469 --- /dev/null +++ b/evdev/geniprops.py @@ -0,0 +1,116 @@ +# -*- 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', +] + +if sys.argv[1:]: + headers = sys.argv[1:] + + +#----------------------------------------------------------------------------- +macro_regex = r'#define +((?:INPUT_PROP)_\w+).*' +macro_regex = re.compile(macro_regex) + +uname = list(os.uname()); del uname[1] +uname = ' '.join(uname) + + +#----------------------------------------------------------------------------- +template = r''' +#include +#ifdef __FreeBSD__ +#include +#else +#include +#include +#endif + +/* Automatically generated by evdev.geniprops */ +/* Generated on %s */ + +#define MODULE_NAME "iprops" +#define MODULE_HELP "linux/input.h macros" + +static PyMethodDef MethodTable[] = { + { NULL, NULL, 0, NULL} +}; + +#if PY_MAJOR_VERSION >= 3 +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + MODULE_NAME, + MODULE_HELP, + -1, /* m_size */ + MethodTable, /* m_methods */ + NULL, /* m_reload */ + NULL, /* m_traverse */ + NULL, /* m_clear */ + NULL, /* m_free */ +}; +#endif + +static PyObject * +moduleinit(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__iprops(void) +{ + return moduleinit(); +} +#else +PyMODINIT_FUNC +init_iprops(void) +{ + moduleinit(); +} +#endif +''' + +def parse_header(header): + for line in open(header): + macro = macro_regex.search(line) + if macro: + yield ' PyModule_AddIntMacro(m, %s);' % macro.group(1) + +all_macros = [] +for header in headers: + try: + fh = open(header) + except (IOError, OSError): + continue + all_macros += parse_header(header) + +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)) diff --git a/evdev/input.c b/evdev/input.c index 432db92..9fd7ec1 100644 --- a/evdev/input.c +++ b/evdev/input.c @@ -454,6 +454,33 @@ erase_effect(PyObject *self, PyObject *args) return Py_None; } +static PyObject * +ioctl_EVIOCGPROP(PyObject *self, PyObject *args) +{ + int fd, ret; + + ret = PyArg_ParseTuple(args, "i", &fd); + if (!ret) return NULL; + + char bytes[(INPUT_PROP_MAX+7)/8]; + memset(bytes, 0, sizeof bytes); + + ret = ioctl(fd, EVIOCGPROP(sizeof(bytes)), &bytes); + + if (ret == -1) + return NULL; + + PyObject* res = PyList_New(0); + for (int i=0; i Date: Sat, 11 Jan 2020 23:19:15 +0100 Subject: [PATCH 028/147] Rework input props functionality - Merged geniprops into genecodes and reverted the changes to setup.py. - Renamed InputDevice.props to InputDevice.input_props. - Renamed UInput(props=) to UInput(input_props=). --- docs/changelog.rst | 9 ++++ evdev/device.py | 18 ++++++- evdev/ecodes.py | 7 +-- evdev/genecodes.py | 2 +- evdev/geniprops.py | 116 --------------------------------------------- evdev/input.c | 2 +- evdev/uinput.c | 2 +- evdev/uinput.py | 14 ++++-- setup.py | 73 +--------------------------- 9 files changed, 44 insertions(+), 199 deletions(-) delete mode 100644 evdev/geniprops.py diff --git a/docs/changelog.rst b/docs/changelog.rst index 265de54..dc359ed 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,15 @@ Changelog --------- +Master +==================== + +- Fix build on 32bit arches with 64bit time_t + +- Add functionality to query device properties. See ``InputDevice.input_props`` + and the ``input_props`` argument to ``Uinput``. + + 1.2.0 (Apr 7, 2019) ==================== diff --git a/evdev/device.py b/evdev/device.py index 6c8feec..e0893c4 100644 --- a/evdev/device.py +++ b/evdev/device.py @@ -223,8 +223,24 @@ def capabilities(self, verbose=False, absinfo=True): else: return self._capabilities(absinfo) - def props(self): + def input_props(self, verbose=False): + ''' + Get device properties and quirks. + + Example + ------- + >>> device.input_props() + [0, 5] + + If ``verbose`` is ``True``, input properties are resolved to their + names. Unknown codes are resolved to ``'?'``:: + + [('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) return props diff --git a/evdev/ecodes.py b/evdev/ecodes.py index f0b5961..e3e4ffc 100644 --- a/evdev/ecodes.py +++ b/evdev/ecodes.py @@ -7,7 +7,7 @@ Exposed constants:: KEY, ABS, REL, SW, MSC, LED, BTN, REP, SND, ID, EV, - BUS, SYN, FF, FF_STATUS + 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:: @@ -47,7 +47,7 @@ #: Mapping of names to values. 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 INPUT_PROP' prev_prefix = '' g = globals() @@ -94,7 +94,8 @@ _ecodes.EV_SND: SND, _ecodes.EV_SYN: SYN, _ecodes.EV_FF: FF, - _ecodes.EV_FF_STATUS: FF_STATUS, } + _ecodes.EV_FF_STATUS: FF_STATUS, +} from evdev._ecodes import * diff --git a/evdev/genecodes.py b/evdev/genecodes.py index eb6481a..04c10e1 100644 --- a/evdev/genecodes.py +++ b/evdev/genecodes.py @@ -20,7 +20,7 @@ #----------------------------------------------------------------------------- -macro_regex = r'#define +((?:KEY|ABS|REL|SW|MSC|LED|BTN|REP|SND|ID|EV|BUS|SYN|FF|UI_FF)_\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] diff --git a/evdev/geniprops.py b/evdev/geniprops.py deleted file mode 100644 index 6a60469..0000000 --- a/evdev/geniprops.py +++ /dev/null @@ -1,116 +0,0 @@ -# -*- 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', -] - -if sys.argv[1:]: - headers = sys.argv[1:] - - -#----------------------------------------------------------------------------- -macro_regex = r'#define +((?:INPUT_PROP)_\w+).*' -macro_regex = re.compile(macro_regex) - -uname = list(os.uname()); del uname[1] -uname = ' '.join(uname) - - -#----------------------------------------------------------------------------- -template = r''' -#include -#ifdef __FreeBSD__ -#include -#else -#include -#include -#endif - -/* Automatically generated by evdev.geniprops */ -/* Generated on %s */ - -#define MODULE_NAME "iprops" -#define MODULE_HELP "linux/input.h macros" - -static PyMethodDef MethodTable[] = { - { NULL, NULL, 0, NULL} -}; - -#if PY_MAJOR_VERSION >= 3 -static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - MODULE_NAME, - MODULE_HELP, - -1, /* m_size */ - MethodTable, /* m_methods */ - NULL, /* m_reload */ - NULL, /* m_traverse */ - NULL, /* m_clear */ - NULL, /* m_free */ -}; -#endif - -static PyObject * -moduleinit(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__iprops(void) -{ - return moduleinit(); -} -#else -PyMODINIT_FUNC -init_iprops(void) -{ - moduleinit(); -} -#endif -''' - -def parse_header(header): - for line in open(header): - macro = macro_regex.search(line) - if macro: - yield ' PyModule_AddIntMacro(m, %s);' % macro.group(1) - -all_macros = [] -for header in headers: - try: - fh = open(header) - except (IOError, OSError): - continue - all_macros += parse_header(header) - -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)) diff --git a/evdev/input.c b/evdev/input.c index 9fd7ec1..7a7c0c2 100644 --- a/evdev/input.c +++ b/evdev/input.c @@ -491,11 +491,11 @@ static PyMethodDef MethodTable[] = { { "ioctl_EVIOCGRAB", ioctl_EVIOCGRAB, METH_VARARGS}, { "ioctl_EVIOCGEFFECTS", ioctl_EVIOCGEFFECTS, METH_VARARGS, "fetch the number of effects the device can keep in its memory." }, { "ioctl_EVIOCG_bits", ioctl_EVIOCG_bits, METH_VARARGS, "get state of KEY|LED|SND|SW"}, + { "ioctl_EVIOCGPROP", ioctl_EVIOCGPROP, METH_VARARGS, "get device properties"}, { "device_read", device_read, METH_VARARGS, "read an input event from a device" }, { "device_read_many", device_read_many, METH_VARARGS, "read all available input events from a device" }, { "upload_effect", upload_effect, METH_VARARGS, "" }, { "erase_effect", erase_effect, METH_VARARGS, "" }, - { "ioctl_EVIOCGPROP", ioctl_EVIOCGPROP, METH_VARARGS, "get device properties"}, { NULL, NULL, 0, NULL} }; diff --git a/evdev/uinput.c b/evdev/uinput.c index 297ef2d..20318ce 100644 --- a/evdev/uinput.c +++ b/evdev/uinput.c @@ -360,7 +360,7 @@ static PyMethodDef MethodTable[] = { "Set physical path"}, { "set_prop", uinput_set_prop, METH_VARARGS, - "Set device property"}, + "Set device input property"}, { NULL, NULL, 0, NULL} }; diff --git a/evdev/uinput.py b/evdev/uinput.py index a3c9b27..8b9a597 100644 --- a/evdev/uinput.py +++ b/evdev/uinput.py @@ -77,7 +77,7 @@ def __init__(self, events=None, name='py-evdev-uinput', vendor=0x1, product=0x1, version=0x1, bustype=0x3, - devnode='/dev/uinput', phys='py-evdev-uinput', props=[]): + devnode='/dev/uinput', phys='py-evdev-uinput', input_props=None): ''' Arguments --------- @@ -96,13 +96,16 @@ def __init__(self, Product identifier. version - version identifier. + Version identifier. bustype - bustype identifier. + Bustype identifier. phys - physical path. + Physical path. + + input_props + Input properties and quirks. Note ---- @@ -133,7 +136,8 @@ def __init__(self, _uinput.set_phys(self.fd, phys) # Set properties - for prop in props: + input_props = input_props or [] + for prop in input_props: _uinput.set_prop(self.fd, prop) for etype, code in prepared_events: diff --git a/setup.py b/setup.py index b2a3f71..086bdc8 100755 --- a/setup.py +++ b/setup.py @@ -40,7 +40,6 @@ 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) -iprops_c = Extension('evdev.iprops', sources=['evdev/iprops.c'], extra_compile_args=cflags) #----------------------------------------------------------------------------- kw = { @@ -58,7 +57,7 @@ 'classifiers': classifiers, 'packages': ['evdev'], - 'ext_modules': [input_c, uinput_c, ecodes_c, iprops_c], + 'ext_modules': [input_c, uinput_c, ecodes_c], 'include_package_data': False, 'zip_safe': True, 'cmdclass': {}, @@ -107,48 +106,6 @@ def create_ecodes(headers=None): check_call(cmd, cwd="%s/evdev" % here, shell=True) -#----------------------------------------------------------------------------- -def create_iprops(headers=None): - if not headers: - headers = [ - '/usr/include/linux/input.h', - '/usr/include/linux/input-event-codes.h', - '/usr/include/linux/uinput.h', - ] - - headers = [header for header in headers if os.path.isfile(header)] - if not headers: - 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: - - yum install kernel-headers-$(uname -r) - apt-get install linux-headers-$(uname -r) - emerge sys-kernel/linux-headers - pacman -S kernel-headers - - In case they are installed in a non-standard location, you may use - the '--evdev-headers' option to specify one or more colon-separated - paths. For example: - - python setup.py \\ - build \\ - build_iprops --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 check_call - - print('writing iprops.c (using %s)' % ' '.join(headers)) - cmd = '%s geniprops.py %s > iprops.c' % (sys.executable, ' '.join(headers)) - check_call(cmd, cwd="%s/evdev" % here, shell=True) - - #----------------------------------------------------------------------------- class build_ecodes(Command): description = 'generate ecodes.c' @@ -168,24 +125,6 @@ def run(self): create_ecodes(self.evdev_headers) -class build_iprops(Command): - description = 'generate iprops.c' - - user_options = [ - ('evdev-headers=', None, 'colon-separated paths to input subsystem headers'), - ] - - def initialize_options(self): - self.evdev_headers = None - - def finalize_options(self): - if self.evdev_headers: - self.evdev_headers = self.evdev_headers.split(':') - - def run(self): - create_iprops(self.evdev_headers) - - class build_ext(_build_ext.build_ext): def has_ecodes(self): ecodes_path = os.path.join(here, 'evdev/ecodes.c') @@ -194,25 +133,17 @@ def has_ecodes(self): print('ecodes.c already exists ... skipping build_ecodes') return not res - def has_iprops(self): - iprops_path = os.path.join(here, 'evdev/iprops.c') - res = os.path.exists(iprops_path) - if res: - print('iprops.c already exists ... skipping build_iprops') - return not res - def run(self): for cmd_name in self.get_sub_commands(): self.run_command(cmd_name) _build_ext.build_ext.run(self) - sub_commands = [('build_ecodes', has_ecodes), ('build_iprops', has_iprops)] + _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_iprops'] = build_iprops #----------------------------------------------------------------------------- From 05fd3fa3cae85e6f4ee5471437d692de2bc24d7a Mon Sep 17 00:00:00 2001 From: TheMadScientist1234 <40321768+TheMadScientist1234@users.noreply.github.com> Date: Mon, 24 Jun 2019 14:52:18 -0400 Subject: [PATCH 029/147] Handle unknown keys Add to the `keys` dictionary when an unknown key is pressed. --- evdev/events.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/evdev/events.py b/evdev/events.py index 6c75588..36eab8b 100644 --- a/evdev/events.py +++ b/evdev/events.py @@ -94,7 +94,9 @@ def __init__(self, event): elif event.value == 1: self.keystate = KeyEvent.key_down - self.keycode = keys[event.code] # :todo: + if not (event.code in keys): + keys[event.code] = ''.join('{:02X}'.format(event.code)) + self.keycode = keys[event.code] self.scancode = event.code #: Reference to an :class:`InputEvent` instance. From 8d549b97d4d818f9f58c023712a110d8f1a0440c Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sat, 11 Jan 2020 23:46:50 +0100 Subject: [PATCH 030/147] Add the allow_unknown argument to KeyEvent --- docs/changelog.rst | 5 +++++ evdev/events.py | 21 ++++++++++++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index dc359ed..8278d3e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -9,6 +9,11 @@ Master - Add functionality to query device properties. See ``InputDevice.input_props`` and the ``input_props`` argument to ``Uinput``. +- ``KeyEvent`` received an ``allow_unknown`` constructor argument, which + determines what will happen when an event code cannot be mapped to a keycode. + The default and behavior so far has been to raise ``KeyError``. If set to + ``True``, the keycode will be set to the event code formatted as a hex number. + 1.2.0 (Apr 7, 2019) ==================== diff --git a/evdev/events.py b/evdev/events.py index 36eab8b..14bb0ce 100644 --- a/evdev/events.py +++ b/evdev/events.py @@ -86,7 +86,15 @@ class KeyEvent(object): __slots__ = 'scancode', 'keycode', 'keystate', 'event' - def __init__(self, event): + def __init__(self, event, allow_unknown=False): + ''' + The ``allow_unknown`` argument determines what to do in the event of a 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 + if event.value == 0: self.keystate = KeyEvent.key_up elif event.value == 2: @@ -94,10 +102,13 @@ def __init__(self, event): elif event.value == 1: self.keystate = KeyEvent.key_down - if not (event.code in keys): - keys[event.code] = ''.join('{:02X}'.format(event.code)) - self.keycode = keys[event.code] - self.scancode = event.code + try: + self.keycode = keys[event.code] + except KeyError: + if allow_unknown: + self.keycode = '0x{:02X}'.format(event.code) + else: + raise #: Reference to an :class:`InputEvent` instance. self.event = event From 05e36c2488e00e86dc1610a5edae22ce7fb6fb8d Mon Sep 17 00:00:00 2001 From: Arti Zirk Date: Tue, 9 Apr 2019 16:26:22 +0300 Subject: [PATCH 031/147] Expose get/set absinfo ioctl commands Changing AbsInfo data in kernel is useful for setting min/max calibration for other programs and flatness/fuzz settings are useful for kernel side filtering of input events --- evdev/device.py | 50 +++++++++++++++++++++++++++++++++++++++++ evdev/input.c | 59 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) diff --git a/evdev/device.py b/evdev/device.py index e0893c4..14afaa6 100644 --- a/evdev/device.py +++ b/evdev/device.py @@ -398,3 +398,53 @@ 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 get_absinfo(self, axis_num): + """ + Return current :class:`AbsInfo` for input device axis + + Arguments + --------- + axis_num : int + EV_ABS keycode (example :attr:`ecodes.ABS_X`) + + Example + ------- + + >>> device.get_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): + """ + Set AbsInfo values for input device. + + Only values set will be overwritten. + + See :class:`AbsInfo` for more info about the arguments + + Arguments + --------- + axis_num : int + EV_ABS keycode (example :attr:`ecodes.ABS_X`) + + Example + ------- + >>> device.set_absinfo(ecodes.ABS_X, min=-2000, max=2000) + + 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.get_absinfo(axis_num) + new_absinfo = AbsInfo(value if value else cur_absinfo.value, + min if min else cur_absinfo.min, + max if max else cur_absinfo.max, + fuzz if fuzz else cur_absinfo.fuzz, + flat if flat else cur_absinfo.flat, + resolution if resolution else cur_absinfo.resolution) + _input.ioctl_EVIOCSABS(self.fd, axis_num, new_absinfo) diff --git a/evdev/input.c b/evdev/input.c index 7a7c0c2..b09102b 100644 --- a/evdev/input.c +++ b/evdev/input.c @@ -247,6 +247,63 @@ ioctl_devinfo(PyObject *self, PyObject *args) } +static PyObject * +ioctl_EVIOCGABS(PyObject *self, PyObject *args) +{ + int fd, ev_code; + struct input_absinfo absinfo; + PyObject* py_absinfo = NULL; + + int ret = PyArg_ParseTuple(args, "ii", &fd, &ev_code); + if (!ret) return NULL; + + memset(&absinfo, 0, sizeof(absinfo)); + ret = ioctl(fd, EVIOCGABS(ev_code), &absinfo); + if (ret == -1) { + PyErr_SetFromErrno(PyExc_IOError); + return NULL; + } + + py_absinfo = Py_BuildValue("(iiiiii)", + absinfo.value, + absinfo.minimum, + absinfo.maximum, + absinfo.fuzz, + absinfo.flat, + absinfo.resolution); + return py_absinfo; +} + + +static PyObject * +ioctl_EVIOCSABS(PyObject *self, PyObject *args) +{ + int fd, ev_code; + struct input_absinfo absinfo; + + int ret = PyArg_ParseTuple(args, + "ii(iiiiii)", + &fd, + &ev_code, + &absinfo.value, + &absinfo.minimum, + &absinfo.maximum, + &absinfo.fuzz, + &absinfo.flat, + &absinfo.resolution); + if (!ret) return NULL; + + ret = ioctl(fd, EVIOCSABS(ev_code), &absinfo); + if (ret == -1) { + PyErr_SetFromErrno(PyExc_IOError); + return NULL; + } + + Py_INCREF(Py_None); + return Py_None; +} + + static PyObject * ioctl_EVIOCGREP(PyObject *self, PyObject *args) { @@ -485,6 +542,8 @@ 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" }, + { "ioctl_EVIOCGABS", ioctl_EVIOCGABS, METH_VARARGS, "get input device absinfo"}, + { "ioctl_EVIOCSABS", ioctl_EVIOCSABS, METH_VARARGS, "set input device absinfo"}, { "ioctl_EVIOCGREP", ioctl_EVIOCGREP, METH_VARARGS}, { "ioctl_EVIOCSREP", ioctl_EVIOCSREP, METH_VARARGS}, { "ioctl_EVIOCGVERSION", ioctl_EVIOCGVERSION, METH_VARARGS}, From 2fa3d246e1116f3301ee935dbecdc81ffc8a709d Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sat, 11 Jan 2020 23:59:03 +0100 Subject: [PATCH 032/147] Minor changes to docstrings --- docs/changelog.rst | 2 ++ evdev/device.py | 17 +++++------------ 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 8278d3e..8df0b48 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -14,6 +14,8 @@ Master The default and behavior so far has been to raise ``KeyError``. If set to ``True``, the keycode will be set to the event code formatted as a hex number. +- Add ``InputDevice.set_absinfo()`` and ``InputDevice.get_absinfo()``. + 1.2.0 (Apr 7, 2019) ==================== diff --git a/evdev/device.py b/evdev/device.py index 14afaa6..1c17c6d 100644 --- a/evdev/device.py +++ b/evdev/device.py @@ -400,7 +400,7 @@ def fn(self): return self.path def get_absinfo(self, axis_num): - """ + ''' Return current :class:`AbsInfo` for input device axis Arguments @@ -410,20 +410,14 @@ def get_absinfo(self, axis_num): Example ------- - >>> device.get_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): - """ - Set AbsInfo values for input device. - - Only values set will be overwritten. - - See :class:`AbsInfo` for more info about the arguments + ''' + Update :class:`AbsInfo` values. Only specified values will be overwritten. Arguments --------- @@ -437,9 +431,8 @@ 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.get_absinfo(axis_num) new_absinfo = AbsInfo(value if value else cur_absinfo.value, min if min else cur_absinfo.min, From c115d5b7745000d706a6d434888258a1a1ac20be Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 12 Jan 2020 00:20:13 +0100 Subject: [PATCH 033/147] Call loop.remove_reader() when InputDevice is closed --- docs/changelog.rst | 3 +++ evdev/device.py | 1 + evdev/eventio.py | 3 +++ evdev/eventio_async.py | 4 ++++ 4 files changed, 11 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 8df0b48..6c10d3c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -16,6 +16,9 @@ Master - Add ``InputDevice.set_absinfo()`` and ``InputDevice.get_absinfo()``. +- Instruct the asyncio event loop to stop monitoring the fd of the input device + when the device is closed. + 1.2.0 (Apr 7, 2019) ==================== diff --git a/evdev/device.py b/evdev/device.py index 1c17c6d..84bd73e 100644 --- a/evdev/device.py +++ b/evdev/device.py @@ -301,6 +301,7 @@ def __fspath__(self): def close(self): if self.fd > -1: try: + super().close() os.close(self.fd) finally: self.fd = -1 diff --git a/evdev/eventio.py b/evdev/eventio.py index fafdd93..22c37e2 100644 --- a/evdev/eventio.py +++ b/evdev/eventio.py @@ -134,3 +134,6 @@ def write(self, etype, code, value): ''' _uinput.write(self.fd, etype, code, value) + + def close(self): + pass diff --git a/evdev/eventio_async.py b/evdev/eventio_async.py index e899f58..d3b8ac3 100644 --- a/evdev/eventio_async.py +++ b/evdev/eventio_async.py @@ -50,6 +50,10 @@ def async_read_loop(self): ''' return ReadIterator(self) + def close(self): + loop = asyncio.get_event_loop() + loop.remove_reader(self.fileno()) + class ReadIterator(object): def __init__(self, device): From 1cc547b3cf583dfd704737a23678d072e57cf66c Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 12 Jan 2020 00:33:23 +0100 Subject: [PATCH 034/147] Rename get_absinfo() to absinfo() for consistency with the other poorly named methods --- docs/changelog.rst | 2 +- evdev/device.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6c10d3c..c3254e7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -14,7 +14,7 @@ Master The default and behavior so far has been to raise ``KeyError``. If set to ``True``, the keycode will be set to the event code formatted as a hex number. -- Add ``InputDevice.set_absinfo()`` and ``InputDevice.get_absinfo()``. +- Add ``InputDevice.set_absinfo()`` and ``InputDevice.absinfo()``. - Instruct the asyncio event loop to stop monitoring the fd of the input device when the device is closed. diff --git a/evdev/device.py b/evdev/device.py index 84bd73e..94fa037 100644 --- a/evdev/device.py +++ b/evdev/device.py @@ -400,7 +400,7 @@ def fn(self): warnings.warn(msg, DeprecationWarning, stacklevel=2) return self.path - def get_absinfo(self, axis_num): + def absinfo(self, axis_num): ''' Return current :class:`AbsInfo` for input device axis @@ -411,7 +411,7 @@ def get_absinfo(self, axis_num): Example ------- - >>> device.get_absinfo(ecodes.ABS_X) + >>> 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)) @@ -434,7 +434,7 @@ def set_absinfo(self, axis_num, value=None, min=None, max=None, fuzz=None, flat= >>> device.set_absinfo(ecodes.ABS_Y, *AbsInfo(0, -2000, 2000, 0, 15, 0)) ''' - cur_absinfo = self.get_absinfo(axis_num) + cur_absinfo = self.absinfo(axis_num) new_absinfo = AbsInfo(value if value else cur_absinfo.value, min if min else cur_absinfo.min, max if max else cur_absinfo.max, From 646ac0c3cdab3d8f5be14c5b24ffdfae654d09a9 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 12 Jan 2020 00:37:28 +0100 Subject: [PATCH 035/147] Add input properties to output of evtest --- evdev/evtest.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/evdev/evtest.py b/evdev/evtest.py index 982744a..49e2c35 100644 --- a/evdev/evtest.py +++ b/evdev/evtest.py @@ -121,6 +121,7 @@ def devicenum(device_path): 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)) @@ -133,6 +134,12 @@ def print_capabilities(device): 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:') + for type, code in input_props: + print(' %s %s' % (type, code)) + print() + print('Device capabilities:') for type, codes in capabilities.items(): print(' Type {} {}:'.format(*type)) From 43c8791d33ca1cc1ed787c7a7e29a2fcf9652a08 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 12 Jan 2020 00:41:29 +0100 Subject: [PATCH 036/147] Bump version: 1.2.0 -> 1.3.0 --- docs/changelog.rst | 2 +- docs/conf.py | 2 +- setup.cfg | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index c3254e7..e9cef1b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,7 +1,7 @@ Changelog --------- -Master +1.3.0 (Jan 12, 2020) ==================== - Fix build on 32bit arches with 64bit time_t diff --git a/docs/conf.py b/docs/conf.py index f7e4a82..3b8bde3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -60,7 +60,7 @@ # built documents. # # The full version, including alpha/beta/rc tags. -release = '1.2.0' +release = '1.3.0' # The short X.Y version. version = release diff --git a/setup.cfg b/setup.cfg index d1ce534..fa36a17 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.2.0 +current_version = 1.3.0 message = Bump version: {current_version} -> {new_version} commit = True tag = True diff --git a/setup.py b/setup.py index 086bdc8..9f5d8ef 100755 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ #----------------------------------------------------------------------------- kw = { 'name': 'evdev', - 'version': '1.2.0', + 'version': '1.3.0', 'description': 'Bindings to the Linux input handling subsystem', 'long_description': open(pjoin(here, 'README.rst')).read(), From 0cdf86adf9576ba343c97a31bd0368c889e8d494 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sun, 12 Jan 2020 00:45:01 +0100 Subject: [PATCH 037/147] Fix links in documentation --- docs/changelog.rst | 4 ++-- requirements-dev.txt | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index e9cef1b..8266989 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -383,9 +383,9 @@ Changelog .. _`@paulo-raca`: https://github.com/paulo-raca .. _`@ndreys`: https://github.com/ndreys .. _`@LinusCDE`: https://github.com/gvalkov/python-evdev/pulls/LinusCDE -.. _`ivaradi`: https://github.com/gvalkov/python-evdev/pull/104 +.. _`@ivaradi`: https://github.com/gvalkov/python-evdev/pull/104 .. _`introduced in Linux 4.5`: https://github.com/torvalds/linux/commit/052876f8e5aec887d22c4d06e54aa5531ffcec75 .. _issue21121: http://bugs.python.org/issue21121 .. _`#63`: https://github.com/gvalkov/python-evdev/issues/63 -.. _`#63`: https://github.com/gvalkov/python-evdev/issues/67 +.. _`#67`: https://github.com/gvalkov/python-evdev/issues/67 diff --git a/requirements-dev.txt b/requirements-dev.txt index 0561107..41728fd 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,3 +2,4 @@ pytest>=3.4.0 Sphinx>=1.7.0 bump2version sphinx_rtd_theme +twine From fb3f9e8559f3df2da84f317164ab18cdd26a0d34 Mon Sep 17 00:00:00 2001 From: Olivier Dormond Date: Sat, 30 May 2020 13:31:14 +0200 Subject: [PATCH 038/147] Fix set_absinfo to allow setting parameters to zero --- docs/changelog.rst | 6 ++++++ evdev/device.py | 12 ++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 8266989..07abba6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,12 @@ Changelog --------- +Master +==================== + +- Fix ``InputDevice.set_absinfo`` to allow setting parameters to zero. + + 1.3.0 (Jan 12, 2020) ==================== diff --git a/evdev/device.py b/evdev/device.py index 94fa037..6949183 100644 --- a/evdev/device.py +++ b/evdev/device.py @@ -435,10 +435,10 @@ def set_absinfo(self, axis_num, value=None, min=None, max=None, fuzz=None, flat= ''' cur_absinfo = self.absinfo(axis_num) - new_absinfo = AbsInfo(value if value else cur_absinfo.value, - min if min else cur_absinfo.min, - max if max else cur_absinfo.max, - fuzz if fuzz else cur_absinfo.fuzz, - flat if flat else cur_absinfo.flat, - resolution if resolution 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) From 1db1c053d1cf6e4b4be8e31d9066a9b0c42f1c75 Mon Sep 17 00:00:00 2001 From: Frederik Aalund Date: Wed, 12 Aug 2020 16:55:39 +0200 Subject: [PATCH 039/147] Fix off-by-one in ioctl_EVIOCG_bits This bug caused values at the end of the list to not be reported back. Example: On my system, SW_PEN_INSERTED (0x0f) is the last switch constant. That is, SW_MAX=SW_PEN_INSERTED. Other tools (python-libevdev) correctly reported this switch as 'set' but python-evdev did not. This commit fixes this issue. --- evdev/input.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evdev/input.c b/evdev/input.c index b09102b..0a7b0d0 100644 --- a/evdev/input.c +++ b/evdev/input.c @@ -413,7 +413,7 @@ ioctl_EVIOCG_bits(PyObject *self, PyObject *args) return NULL; PyObject* res = PyList_New(0); - for (int i=0; i Date: Wed, 30 Sep 2020 01:10:27 -0700 Subject: [PATCH 040/147] Add missing return codes to os.strerror() calls. --- evdev/uinput.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/evdev/uinput.py b/evdev/uinput.py index 8b9a597..0dc5f89 100644 --- a/evdev/uinput.py +++ b/evdev/uinput.py @@ -229,26 +229,30 @@ def begin_upload(self, effect_id): upload = ff.UInputUpload() upload.effect_id = effect_id - if self.dll._uinput_begin_upload(self.fd, ctypes.byref(upload)): - raise UInputError('Failed to begin uinput upload: ' + os.strerror()) + ret = self.dll._uinput_begin_upload(self.fd, ctypes.byref(upload)) + if ret: + raise UInputError('Failed to begin uinput upload: ' + os.strerror(ret)) return upload def end_upload(self, upload): - if self.dll._uinput_end_upload(self.fd, ctypes.byref(upload)): - raise UInputError('Failed to end uinput upload: ' + os.strerror()) + ret = self.dll._uinput_end_upload(self.fd, ctypes.byref(upload)) + if ret: + raise UInputError('Failed to end uinput upload: ' + os.strerror(ret)) def begin_erase(self, effect_id): erase = ff.UInputErase() erase.effect_id = effect_id - if self.dll._uinput_begin_erase(self.fd, ctypes.byref(erase)): - raise UInputError('Failed to begin uinput erase: ' + os.strerror()) + ret = self.dll._uinput_begin_erase(self.fd, ctypes.byref(erase)) + if ret: + raise UInputError('Failed to begin uinput erase: ' + os.strerror(ret)) return erase def end_erase(self, erase): - if self.dll._uinput_end_erase(self.fd, ctypes.byref(erase)): - raise UInputError('Failed to end uinput erase: ' + os.strerror()) + ret = self.dll._uinput_end_erase(self.fd, ctypes.byref(erase)) + if ret: + raise UInputError('Failed to end uinput erase: ' + os.strerror(ret)) def _verify(self): ''' From 454f50f3118adbfd497a06c5c7657aed7e5a52d1 Mon Sep 17 00:00:00 2001 From: Taylor Alexander Date: Thu, 1 Oct 2020 01:25:03 -0700 Subject: [PATCH 041/147] Fix a typo in force feedback example. --- docs/tutorial.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 92a8780..332908b 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -391,7 +391,7 @@ Create ``uinput`` device capable of receiving FF-effects upload = device.begin_upload(event.value) upload.retval = 0 - print(f'[upload] effect_id: {upload.effect_id}, type: {upload.effect.type}') + print(f'[upload] effect_id: {upload.effect.id}, type: {upload.effect.type}') device.end_upload(upload) elif event.code == ecodes.UI_FF_ERASE: From 148e6e04dcbf4de6087923f5ae2706ec7ae61ae7 Mon Sep 17 00:00:00 2001 From: Taylor Alexander Date: Thu, 1 Oct 2020 02:44:33 -0700 Subject: [PATCH 042/147] One more bug fix in the example. --- docs/tutorial.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 332908b..fc0e3e0 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -385,7 +385,7 @@ Create ``uinput`` device capable of receiving FF-effects # Wait for an EV_UINPUT event that will signal us that an # effect upload/erase operation is in progress. if event.type != ecodes.EV_UINPUT: - pass + continue if event.code == ecodes.UI_FF_UPLOAD: upload = device.begin_upload(event.value) From 0622179729588c66b9346eecfa7af8cec30f6ba5 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Fri, 2 Oct 2020 00:29:48 +0200 Subject: [PATCH 043/147] Add the util.find_ecodes_by_regex helper function --- docs/tutorial.rst | 14 ++++++++++++++ evdev/util.py | 34 +++++++++++++++++++++++++++++++++- tests/test_util.py | 21 +++++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 tests/test_util.py diff --git a/docs/tutorial.rst b/docs/tutorial.rst index fc0e3e0..604d289 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -253,6 +253,20 @@ Accessing evdev constants ... ['KEY_COFFEE', 'KEY_SCREENLOCK'] +Searching event codes by regex +============================== + +:: + + >>> from evdev import util + + >>> res = util.find_ecodes_by_regex(r'(ABS|KEY)_BR(AKE|EAK)') + >>> res + ... {1: [411], 3: [10]} + >>> util.resolve_ecodes_dict(res) + ... {('EV_KEY', 1): [('KEY_BREAK', 411)], ('EV_ABS', 3): [('ABS_BRAKE', 10)]} + + Getting exclusive access to a device ==================================== diff --git a/evdev/util.py b/evdev/util.py index b6652d7..185f475 100644 --- a/evdev/util.py +++ b/evdev/util.py @@ -1,8 +1,10 @@ # encoding: utf-8 +import re import os import stat import glob +import collections from evdev import ecodes from evdev.events import event_factory @@ -112,4 +114,34 @@ def resolve_ecodes(ecode_dict, ecode_list, unknown='?'): return res -__all__ = ('list_devices', 'is_device', 'categorize', 'resolve_ecodes', 'resolve_ecodes_dict') +def find_ecodes_by_regex(regex): + ''' + Find ecodes matching a regex and return a mapping of event type to event codes. + + Example + ------- + >>> find_ecodes_by_regex(r'(ABS|KEY)_BR(AKE|EAK)') + {1: [411], 3: [10]} + >>> res = find_ecodes_by_regex(r'(ABS|KEY)_BR(AKE|EAK)') + >>> resolve_ecodes_dict(res) + { + ('EV_KEY', 1): [('KEY_BREAK', 411)], + ('EV_ABS', 3): [('ABS_BRAKE', 10)] + } + ''' + + regex = regex if isinstance(regex, re.Pattern) else re.compile(regex) + result = collections.defaultdict(list) + + for type_code, codes in ecodes.bytype.items(): + for code, names in codes.items(): + names = (names,) if isinstance(names, str) else names + for name in names: + if regex.match(name): + result[type_code].append(code) + break + + return dict(result) + + +__all__ = ('list_devices', 'is_device', 'categorize', 'resolve_ecodes', 'resolve_ecodes_dict', 'find_ecodes_by_regex') diff --git a/tests/test_util.py b/tests/test_util.py new file mode 100644 index 0000000..11e338b --- /dev/null +++ b/tests/test_util.py @@ -0,0 +1,21 @@ +from evdev import util + + +def test_match_ecodes_a(): + 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) + ] + } + + 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)] + } From e0492245a3fed73cc586564e91ecd12421241274 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Fri, 2 Oct 2020 00:55:14 +0200 Subject: [PATCH 044/147] Fix util.resolve_ecodes() example --- evdev/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evdev/util.py b/evdev/util.py index 185f475..818be69 100644 --- a/evdev/util.py +++ b/evdev/util.py @@ -91,8 +91,8 @@ def resolve_ecodes(ecode_dict, ecode_list, unknown='?'): Example ------- - >>> resolve_ecodes([272, 273, 274]) - [('BTN_MOUSE', 272), ('BTN_RIGHT', 273), ('BTN_MIDDLE', 274)] + >>> resolve_ecodes(ecodes.BTN, [272, 273, 274]) + [(['BTN_LEFT', 'BTN_MOUSE'], 272), ('BTN_RIGHT', 273), ('BTN_MIDDLE', 274)] ''' res = [] for ecode in ecode_list: From 39ba89ce5b40e05ab1304882c4cd14df6222b5da Mon Sep 17 00:00:00 2001 From: jbleon95 Date: Thu, 29 Oct 2020 10:15:47 -0400 Subject: [PATCH 045/147] deref event_list if returning IOError in device_ready_many --- evdev/input.c | 1 + 1 file changed, 1 insertion(+) diff --git a/evdev/input.c b/evdev/input.c index 0a7b0d0..b1497a4 100644 --- a/evdev/input.c +++ b/evdev/input.c @@ -102,6 +102,7 @@ device_read_many(PyObject *self, PyObject *args) if (nread < 0) { PyErr_SetFromErrno(PyExc_IOError); + Py_DECREF(event_list); return NULL; } From f95869ceaf442354bfdda0298907ca1e9f191ddc Mon Sep 17 00:00:00 2001 From: sezanzeb Date: Mon, 16 Nov 2020 12:42:26 +0100 Subject: [PATCH 046/147] threaded non-blocking close --- evdev/device.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/evdev/device.py b/evdev/device.py index 6949183..9c66e62 100644 --- a/evdev/device.py +++ b/evdev/device.py @@ -4,6 +4,7 @@ import warnings import contextlib import collections +import threading from evdev import _input, ecodes, util from evdev.events import InputEvent @@ -302,7 +303,7 @@ def close(self): if self.fd > -1: try: super().close() - os.close(self.fd) + threading.Thread(target=os.close, args=(self.fd,)).start() finally: self.fd = -1 From 3e06e5b61f979a644754b15beafa51b081685878 Mon Sep 17 00:00:00 2001 From: sezanzeb Date: Mon, 16 Nov 2020 13:18:36 +0100 Subject: [PATCH 047/147] add a comment explaining why --- evdev/device.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/evdev/device.py b/evdev/device.py index 9c66e62..ba5af0e 100644 --- a/evdev/device.py +++ b/evdev/device.py @@ -303,6 +303,8 @@ def close(self): if self.fd > -1: try: super().close() + # avoid blocking at the end of functions when the destructor + # is called. threading.Thread(target=os.close, args=(self.fd,)).start() finally: self.fd = -1 From deba5670496fba6555a5cfd155ab8dbf7a88ffbd Mon Sep 17 00:00:00 2001 From: sezanzeb Date: Sun, 6 Dec 2020 17:03:33 +0100 Subject: [PATCH 048/147] Update eventio_async.py fixes https://github.com/lutris/lutris/issues/3250 _do_when_readable is only used for e.g. async_read, but InputDevice may not use that and use read instead. So _do_when_readable is never called and no event loop is needed. --- evdev/eventio_async.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/evdev/eventio_async.py b/evdev/eventio_async.py index d3b8ac3..d7c6c86 100644 --- a/evdev/eventio_async.py +++ b/evdev/eventio_async.py @@ -51,8 +51,13 @@ def async_read_loop(self): return ReadIterator(self) def close(self): - loop = asyncio.get_event_loop() - loop.remove_reader(self.fileno()) + try: + loop = asyncio.get_event_loop() + loop.remove_reader(self.fileno()) + except RuntimeError: + # no event loop present, so there is nothing to + # remove the header from. ignore + pass class ReadIterator(object): From d62b36fd533702f242b781c729a4b05b34fd44e2 Mon Sep 17 00:00:00 2001 From: sezanzeb Date: Mon, 21 Dec 2020 18:10:32 +0100 Subject: [PATCH 049/147] typo --- 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 d7c6c86..77542a4 100644 --- a/evdev/eventio_async.py +++ b/evdev/eventio_async.py @@ -56,7 +56,7 @@ def close(self): loop.remove_reader(self.fileno()) except RuntimeError: # no event loop present, so there is nothing to - # remove the header from. ignore + # remove the reader from. Ignore pass From 62884d675ac781dd09df18978028a32cf6591397 Mon Sep 17 00:00:00 2001 From: Paul Mundt Date: Fri, 8 Jan 2021 02:14:17 +0100 Subject: [PATCH 050/147] ff: Fix up attack_length key typo in Envelope struct The Envelope struct defined an 'attach_length' key instead of the expected 'attack_length'. While the documentation was correct, any passed in 'attack_length' value was being silently dropped. This was discovered when configuring a periodic sinusoidal effect and observing differences in the waveform in comparison to the C API. Signed-off-by: Paul Mundt --- evdev/ff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evdev/ff.py b/evdev/ff.py index 182be4a..0008906 100644 --- a/evdev/ff.py +++ b/evdev/ff.py @@ -51,7 +51,7 @@ class Envelope(ctypes.Structure): ''' _fields_ = [ - ('attach_length', _u16), + ('attack_length', _u16), ('attack_level', _u16), ('fade_length', _u16), ('fade_level', _u16), From 642a3150ec50c027024cda02a6749f7a1a3b8a92 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sat, 16 Jan 2021 14:30:17 +0100 Subject: [PATCH 051/147] PyUserInput is deprecated in favor of pynput --- docs/scope.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/scope.rst b/docs/scope.rst index a67a096..66a887d 100644 --- a/docs/scope.rst +++ b/docs/scope.rst @@ -18,13 +18,13 @@ injection support. - python-uinput_ - uinput-mapper_ -- PyUserInput_ (cross-platform, works on the display server level) +- pynput_ - pygame_ (cross-platform) .. _python-uinput: https://github.com/tuomasjjrasanen/python-uinput .. _uinput-mapper: https://github.com/MerlijnWajer/uinput-mapper -.. _PyUserInput: https://github.com/PyUserInput/PyUserInput +.. _pynput: https://github.com/moses-palmer/pynput .. _pygame: http://www.pygame.org/ .. _`#7`: https://github.com/gvalkov/python-evdev/issues/7 From 5adc2abf1ec8a02c05c80cb1b3e34ba1d4237803 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Sat, 16 Jan 2021 14:50:51 +0100 Subject: [PATCH 052/147] Bump version: 1.3.0 -> 1.4.0 --- docs/changelog.rst | 21 ++++++++++++++++++++- docs/conf.py | 2 +- setup.cfg | 3 +-- setup.py | 2 +- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 07abba6..042db50 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,11 +1,30 @@ Changelog --------- -Master + +1.4.0 (Jan 16, 2021) ==================== - Fix ``InputDevice.set_absinfo`` to allow setting parameters to zero. +- Fix off-by-one in ``ioctl_EVIOCG_bits``, which causes value at the end of the + list to not be reported back (`#131 `_). + +- Fix ``set_absinfo`` to allow setting parameters to zero (`#128 `_). + +- Fix leak when returning ``BlockingIOError`` from a read (`#143 `_). + +- Fix "There is no current event loop in thread" error for non asyncio code + (`#146 `_). + +- Prevent ``InputDevice`` destructor from blocking (`#145 `_). + +- Add missing return codes to ``os.strerror()`` calls and fix force feedback + example in docs (`#138 `_). + +- Add the ``util.find_ecodes_by_regex()`` helper function. + + 1.3.0 (Jan 12, 2020) ==================== diff --git a/docs/conf.py b/docs/conf.py index 3b8bde3..803993c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -60,7 +60,7 @@ # built documents. # # The full version, including alpha/beta/rc tags. -release = '1.3.0' +release = '1.4.0' # The short X.Y version. version = release diff --git a/setup.cfg b/setup.cfg index fa36a17..ff3bb7b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.3.0 +current_version = 1.4.0 message = Bump version: {current_version} -> {new_version} commit = True tag = True @@ -11,4 +11,3 @@ max-line-length = 110 [bumpversion:file:setup.py] [bumpversion:file:docs/conf.py] - diff --git a/setup.py b/setup.py index 9f5d8ef..e8ab8f1 100755 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ #----------------------------------------------------------------------------- kw = { 'name': 'evdev', - 'version': '1.3.0', + 'version': '1.4.0', 'description': 'Bindings to the Linux input handling subsystem', 'long_description': open(pjoin(here, 'README.rst')).read(), From a1382dcd9a50b1aae997c2ce62c7312746b1d806 Mon Sep 17 00:00:00 2001 From: Ben Greiner Date: Fri, 9 Apr 2021 22:14:41 +0200 Subject: [PATCH 053/147] re.Pattern is not a guaranteed class name --- evdev/util.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/evdev/util.py b/evdev/util.py index 818be69..e8009f7 100644 --- a/evdev/util.py +++ b/evdev/util.py @@ -118,6 +118,8 @@ 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. + Example ------- >>> find_ecodes_by_regex(r'(ABS|KEY)_BR(AKE|EAK)') @@ -130,7 +132,7 @@ def find_ecodes_by_regex(regex): } ''' - regex = regex if isinstance(regex, re.Pattern) else re.compile(regex) + regex = re.compile(regex) # re.compile is idempotent result = collections.defaultdict(list) for type_code, codes in ecodes.bytype.items(): From ddf3c78144db5659ef68c94286134196397f7379 Mon Sep 17 00:00:00 2001 From: Luiz Antonio Lazoti Date: Fri, 30 Apr 2021 15:23:15 -0300 Subject: [PATCH 054/147] fix --> ImportError: sys.meta_path is None Fix this error. Exception ignored in: Traceback (most recent call last): File "/usr/local/lib/python3.6/dist-packages/evdev/device.py", line 160, in __del__ File "/usr/local/lib/python3.6/dist-packages/evdev/device.py", line 305, in close File "/usr/local/lib/python3.6/dist-packages/evdev/eventio_async.py", line 55, in close File "/usr/lib/python3.6/asyncio/events.py", line 694, in get_event_loop File "/usr/lib/python3.6/asyncio/events.py", line 669, in get_event_loop_policy File "/usr/lib/python3.6/asyncio/events.py", line 662, in _init_event_loop_policy ImportError: sys.meta_path is None, Python is likely shutting down --- evdev/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evdev/device.py b/evdev/device.py index ba5af0e..d9f2b91 100644 --- a/evdev/device.py +++ b/evdev/device.py @@ -158,7 +158,7 @@ def __del__(self): if hasattr(self, 'fd') and self.fd is not None: try: self.close() - except OSError: + except (OSError, ImportError): pass def _capabilities(self, absinfo=True): From 426057d68f78bd9a22c2121ac4f9dd29b2360792 Mon Sep 17 00:00:00 2001 From: Luiz Antonio Lazoti Date: Fri, 30 Apr 2021 15:33:34 -0300 Subject: [PATCH 055/147] fix ImportError, AttributeError FIX Exception ignored in: Traceback (most recent call last): File "/usr/local/lib/python3.6/dist-packages/evdev/device.py", line 160, in __del__ File "/usr/local/lib/python3.6/dist-packages/evdev/device.py", line 305, in close File "/usr/local/lib/python3.6/dist-packages/evdev/eventio_async.py", line 55, in close File "/usr/lib/python3.6/asyncio/events.py", line 694, in get_event_loop File "/usr/lib/python3.6/asyncio/events.py", line 669, in get_event_loop_policy File "/usr/lib/python3.6/asyncio/events.py", line 662, in _init_event_loop_policy ImportError: sys.meta_path is None, Python is likely shutting down --- evdev/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evdev/device.py b/evdev/device.py index d9f2b91..02150be 100644 --- a/evdev/device.py +++ b/evdev/device.py @@ -158,7 +158,7 @@ def __del__(self): if hasattr(self, 'fd') and self.fd is not None: try: self.close() - except (OSError, ImportError): + except (OSError, ImportError, AttributeError): pass def _capabilities(self, absinfo=True): From 17c68cac99b811a3811d29f19dc8ac8c0746c983 Mon Sep 17 00:00:00 2001 From: Luc Ducazu <6098321+lducazu@users.noreply.github.com> Date: Tue, 18 May 2021 19:02:55 +0200 Subject: [PATCH 056/147] Re-enable TTY echo at exit. --- evdev/evtest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evdev/evtest.py b/evdev/evtest.py index 49e2c35..da6c683 100644 --- a/evdev/evtest.py +++ b/evdev/evtest.py @@ -67,7 +67,7 @@ def main(): # Disable tty echoing if stdin is a tty. if sys.stdin.isatty(): toggle_tty_echo(sys.stdin, enable=False) - atexit.register(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) ...') fd_to_device = {dev.fd: dev for dev in devices} From 3033ddfe6db429872a389067a21cd4524722147f Mon Sep 17 00:00:00 2001 From: Mario Mey Date: Wed, 16 Jun 2021 17:04:24 -0300 Subject: [PATCH 057/147] Importing modules was wrong in `selector` --- docs/tutorial.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 604d289..b594516 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -175,14 +175,14 @@ This can also be achieved using the :mod:`selectors` module in Python 3.4: from evdev import InputDevice from selectors import DefaultSelector, EVENT_READ - selector = selectors.DefaultSelector() + selector = DefaultSelector() - mouse = evdev.InputDevice('/dev/input/event1') - keybd = evdev.InputDevice('/dev/input/event2') + mouse = InputDevice('/dev/input/event1') + keybd = InputDevice('/dev/input/event2') # This works because InputDevice has a `fileno()` method. - selector.register(mouse, selectors.EVENT_READ) - selector.register(keybd, selectors.EVENT_READ) + selector.register(mouse, EVENT_READ) + selector.register(keybd, EVENT_READ) while True: for key, mask in selector.select(): From 44cc232db4686b63c2b96863e3ae3e1ef6c2feab Mon Sep 17 00:00:00 2001 From: Martin Dinov <46628515+martinthedinov@users.noreply.github.com> Date: Wed, 23 Jun 2021 01:09:14 +0100 Subject: [PATCH 058/147] Update tutorial.rst The "Injecting an FF-event into first FF-capable device found" example was slightly broken, with a missing import and a typo on what is now line 450 (previously 449) - should be ecodes.EV_FF and not e.EV_FF. Finally, added a duration_ms of time.sleep(), as otherwise the effect immediately gets erased and doesn't seem to do anything. --- docs/tutorial.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 604d289..4528730 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -425,10 +425,11 @@ Injecting an FF-event into first FF-capable device found :: - from evdev import ecodes, InputDevice, ff + from evdev import ecodes, InputDevice, ff, list_devices + import time # Find first EV_FF capable event device (that we have permissions to use). - for name in evdev.list_devices(): + for name in list_devices(): dev = InputDevice(name) if ecodes.EV_FF in dev.capabilities(): break @@ -446,5 +447,6 @@ Injecting an FF-event into first FF-capable device found repeat_count = 1 effect_id = dev.upload_effect(effect) - dev.write(e.EV_FF, effect_id, repeat_count) + dev.write(ecodes.EV_FF, effect_id, repeat_count) + time.sleep(duration_ms) dev.erase_effect(effect_id) From 893c8aed129bc5fb29e8b52dee838855913786c9 Mon Sep 17 00:00:00 2001 From: Daniel Quinn Date: Thu, 2 Sep 2021 21:03:32 +0000 Subject: [PATCH 059/147] Fix imports in sample The imports were for classes, while the example was referencing the package name --- docs/tutorial.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 604d289..b594516 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -175,14 +175,14 @@ This can also be achieved using the :mod:`selectors` module in Python 3.4: from evdev import InputDevice from selectors import DefaultSelector, EVENT_READ - selector = selectors.DefaultSelector() + selector = DefaultSelector() - mouse = evdev.InputDevice('/dev/input/event1') - keybd = evdev.InputDevice('/dev/input/event2') + mouse = InputDevice('/dev/input/event1') + keybd = InputDevice('/dev/input/event2') # This works because InputDevice has a `fileno()` method. - selector.register(mouse, selectors.EVENT_READ) - selector.register(keybd, selectors.EVENT_READ) + selector.register(mouse, EVENT_READ) + selector.register(keybd, EVENT_READ) while True: for key, mask in selector.select(): From 7ee9e37eb070b4bb3dbf3f11a2d306a5033c7cc0 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Thu, 24 Mar 2022 01:00:27 +0100 Subject: [PATCH 060/147] Add sphinx-copybutton --- docs/conf.py | 16 +++++++++++----- requirements-dev.txt | 5 +++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 803993c..5d8d0b1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -31,10 +31,11 @@ # 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.ext.autodoc', + 'sphinx.ext.viewcode', + 'sphinx.ext.intersphinx', + 'sphinx.ext.napoleon', + 'sphinx_copybutton', ] autodoc_member_order = 'bysource' @@ -53,7 +54,7 @@ # General information about the project. project = u'python-evdev' -copyright = u'2012-2016, Georgi Valkov' +copyright = u'2012-2022, Georgi Valkov' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -258,3 +259,8 @@ # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' + +# Copybutton config +#copybutton_prompt_text = r">>> " +#copybutton_prompt_is_regexp = True +#copybutton_only_copy_prompt_lines = True diff --git a/requirements-dev.txt b/requirements-dev.txt index 41728fd..c7eb2f2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,6 @@ -pytest>=3.4.0 -Sphinx>=1.7.0 +pytest ~= 7.1.0 +Sphinx ~= 4.4.0 +sphinx-copybutton ~= 0.5.0 bump2version sphinx_rtd_theme twine From d3b712285c50af9a758f6502a69c30b44c7166d7 Mon Sep 17 00:00:00 2001 From: Tobi Date: Thu, 24 Mar 2022 01:10:32 +0100 Subject: [PATCH 061/147] threaded_close (#165) * threaded_close * default to false * revert --- evdev/device.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/evdev/device.py b/evdev/device.py index 02150be..23254a3 100644 --- a/evdev/device.py +++ b/evdev/device.py @@ -4,7 +4,6 @@ import warnings import contextlib import collections -import threading from evdev import _input, ecodes, util from evdev.events import InputEvent @@ -303,9 +302,7 @@ def close(self): if self.fd > -1: try: super().close() - # avoid blocking at the end of functions when the destructor - # is called. - threading.Thread(target=os.close, args=(self.fd,)).start() + os.close(self.fd) finally: self.fd = -1 From 92c009a5001ebbfa94b535d8cff8c6ed9c51c35b Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Thu, 24 Mar 2022 01:29:19 +0100 Subject: [PATCH 062/147] Bump version: 1.4.0 -> 1.5.0 --- docs/changelog.rst | 16 ++++++++++++++++ docs/conf.py | 2 +- setup.cfg | 2 +- setup.py | 10 +++++----- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 042db50..ed85d06 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,22 @@ Changelog --------- +1.5.0 (Mar 24, 2022) +================== + +- Fix documentation (`#163 `_, `#160 `_). + +- Re-enable TTY echo at evtest exit (`#155 `_). + +- Fix ``ImportError: sys.meta_path is None, Python is likely shutting down`` (`#154 `_). + +- Closing the input device file descriptor in ``InputDevice.close()`` now + happens in the main thread, instead of in a new thread (reverts `#146 + `_). + +- Fix ``util.find_ecodes_by_regex`` not working across all supported Python versions (`#152 `_). + + 1.4.0 (Jan 16, 2021) ==================== diff --git a/docs/conf.py b/docs/conf.py index 5d8d0b1..2d16b6c 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.4.0' +release = '1.5.0' # The short X.Y version. version = release diff --git a/setup.cfg b/setup.cfg index ff3bb7b..1f1ff70 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.4.0 +current_version = 1.5.0 message = Bump version: {current_version} -> {new_version} commit = True tag = True diff --git a/setup.py b/setup.py index e8ab8f1..e8c1174 100755 --- a/setup.py +++ b/setup.py @@ -22,12 +22,12 @@ #----------------------------------------------------------------------------- classifiers = [ 'Development Status :: 5 - Production/Stable', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', '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', 'Operating System :: POSIX :: Linux', 'Intended Audience :: Developers', 'Topic :: Software Development :: Libraries', @@ -44,7 +44,7 @@ #----------------------------------------------------------------------------- kw = { 'name': 'evdev', - 'version': '1.4.0', + 'version': '1.5.0', 'description': 'Bindings to the Linux input handling subsystem', 'long_description': open(pjoin(here, 'README.rst')).read(), From 1bf0999a6587aacc6877cfcda0dcd8f6fa4c6180 Mon Sep 17 00:00:00 2001 From: Dave Riedstra Date: Sat, 30 Apr 2022 23:38:34 +0100 Subject: [PATCH 063/147] Note that UInput capabilities can't include EV_SYN per https://github.com/gvalkov/python-evdev/issues/60#issuecomment-238791290 --- docs/tutorial.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 37d0182..d4f3c0b 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -380,6 +380,8 @@ Create ``uinput`` device with capabilities of another device Create ``uinput`` device capable of receiving FF-effects ======================================================== +Note that ``ecodes.EV_SYN`` cannot be in the ``cap`` dictionary or the device will not be created. + :: import asyncio From 6554c1bc576e8de14ff2a0a180880110b5c6183a Mon Sep 17 00:00:00 2001 From: Tobi Date: Mon, 2 May 2022 19:24:39 +0200 Subject: [PATCH 064/147] Update tutorial.rst --- docs/tutorial.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index d4f3c0b..ef60282 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -329,6 +329,8 @@ Injecting events (using a context manager) Specifying ``uinput`` device options ==================================== +Note that ``ecodes.EV_SYN`` cannot be in the ``cap`` dictionary or the device will not be created. + :: >>> from evdev import UInput, AbsInfo, ecodes as e @@ -380,8 +382,6 @@ Create ``uinput`` device with capabilities of another device Create ``uinput`` device capable of receiving FF-effects ======================================================== -Note that ``ecodes.EV_SYN`` cannot be in the ``cap`` dictionary or the device will not be created. - :: import asyncio From 4c6bee7c912a70cf7309e77d1ff6ccc6c2fc8211 Mon Sep 17 00:00:00 2001 From: Tobi Date: Mon, 2 May 2022 19:28:30 +0200 Subject: [PATCH 065/147] Update tutorial.rst --- docs/tutorial.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index ef60282..d35299c 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -329,7 +329,9 @@ Injecting events (using a context manager) Specifying ``uinput`` device options ==================================== -Note that ``ecodes.EV_SYN`` cannot be in the ``cap`` dictionary or the device will not be created. +.. note:: + + ``ecodes.EV_SYN`` cannot be in the ``cap`` dictionary or the device will not be created. :: From 1a51829e7a89525db35c24bae6ef064346462e1c Mon Sep 17 00:00:00 2001 From: Andrew Ammerlaan Date: Wed, 8 Jun 2022 21:44:20 +0200 Subject: [PATCH 066/147] 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 067/147] 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 068/147] 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 069/147] 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 070/147] 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 071/147] 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 072/147] 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 073/147] 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 074/147] 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 075/147] 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 076/147] 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 077/147] 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 078/147] 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 079/147] 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 080/147] 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 081/147] 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 082/147] 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 083/147] 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 084/147] 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 085/147] 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 086/147] 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 087/147] 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 088/147] 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 089/147] 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 090/147] 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 091/147] 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 092/147] 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 093/147] 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 094/147] 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 095/147] 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 096/147] 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 097/147] 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 098/147] 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 099/147] 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 109/147] 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 110/147] 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 111/147] 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 112/147] 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 113/147] 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 114/147] 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 115/147] 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 116/147] 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 117/147] 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 118/147] 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 119/147] 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 120/147] 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 121/147] 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 122/147] 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 123/147] =?UTF-8?q?Bump=20version:=201.7.1=20=E2=86=92=201?= =?UTF-8?q?.8.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 124/147] 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 125/147] 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 126/147] 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 127/147] 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 128/147] 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 129/147] 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 130/147] 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 131/147] 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 132/147] 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 133/147] 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 134/147] 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 135/147] 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 136/147] 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 137/147] 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 138/147] =?UTF-8?q?Bump=20version:=201.8.0=20=E2=86=92=201?= =?UTF-8?q?.9.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 139/147] 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 140/147] 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 141/147] 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 142/147] =?UTF-8?q?Bump=20version:=201.9.0=20=E2=86=92=201?= =?UTF-8?q?.9.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 143/147] 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 144/147] 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 145/147] 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 146/147] 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 147/147] =?UTF-8?q?Bump=20version:=201.9.1=20=E2=86=92=201?= =?UTF-8?q?.9.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