From bc91d17cd6103f37be3d705302132df328737698 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Tue, 4 Feb 2025 17:50:34 +0000 Subject: [PATCH 01/17] 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 02/17] 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 03/17] =?UTF-8?q?Bump=20version:=201.8.0=20=E2=86=92=201.9?= =?UTF-8?q?.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/changelog.rst | 10 +++++----- docs/conf.py | 2 +- pyproject.toml | 4 ++-- src/evdev/eventio.py | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 5dfeaab..f66cfff 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,18 +1,18 @@ Changelog --------- -1.9.0 (Unreleased) +1.9.0 (Feb 08, 2025) ================== -- Fix ``CPATH/C_INCLUDE_PATH`` being ignored during build. +- Fix for ``CPATH/C_INCLUDE_PATH`` being ignored during build. -- Slightly faster reading of events. +- Slightly faster reading of events in ``device.read()`` and ``device.read_one()``. -- Fix build on FreeBSD. +- Fix FreeBSD support. - Drop deprecated ``InputDevice.fn`` (use ``InputDevice.path`` instead). -- More type hints. +- Improve type hint coverage and add a ``py.typed`` file to the sdist. 1.8.0 (Jan 25, 2025) diff --git a/docs/conf.py b/docs/conf.py index 53b5206..b938fa0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -65,7 +65,7 @@ # built documents. # # The full version, including alpha/beta/rc tags. -release = "1.8.0" +release = "1.9.0" # The short X.Y version. version = release diff --git a/pyproject.toml b/pyproject.toml index 7854d91..346dedd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "evdev" -version = "1.8.0" +version = "1.9.0" description = "Bindings to the Linux input handling subsystem" keywords = ["evdev", "input", "uinput"] readme = "README.md" @@ -36,7 +36,7 @@ line-length = 120 ignore = ["E265", "E241", "F403", "F401", "E401", "E731"] [tool.bumpversion] -current_version = "1.8.0" +current_version = "1.9.0" commit = true tag = true allow_dirty = true diff --git a/src/evdev/eventio.py b/src/evdev/eventio.py index 27bba9d..bdb91a4 100644 --- a/src/evdev/eventio.py +++ b/src/evdev/eventio.py @@ -2,7 +2,7 @@ import functools import os import select -from typing import Iterator +from typing import Iterator, Union from . import _input, _uinput, ecodes from .events import InputEvent @@ -46,7 +46,7 @@ def read_loop(self) -> Iterator[InputEvent]: for event in self.read(): yield event - def read_one(self) -> InputEvent: + def read_one(self) -> Union[InputEvent, None]: """ Read and return a single input event as an instance of :class:`InputEvent `. From 6523e3f7d77ff5fd15bc2a02033527449b117e72 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Thu, 20 Feb 2025 07:48:34 +0000 Subject: [PATCH 04/17] 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 05/17] 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 06/17] 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 07/17] =?UTF-8?q?Bump=20version:=201.9.0=20=E2=86=92=201.9?= =?UTF-8?q?.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/changelog.rst | 11 ++++++++++- docs/conf.py | 2 +- pyproject.toml | 4 ++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index f66cfff..4dcf62f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,8 +1,17 @@ Changelog --------- + +1.9.1 (Feb 22, 2025) +==================== + +- Fix fox missing ``UI_FF`` constants in generated ``ecodes.py``. + +- More type annotations. + + 1.9.0 (Feb 08, 2025) -================== +==================== - Fix for ``CPATH/C_INCLUDE_PATH`` being ignored during build. diff --git a/docs/conf.py b/docs/conf.py index b938fa0..86b3d06 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -65,7 +65,7 @@ # built documents. # # The full version, including alpha/beta/rc tags. -release = "1.9.0" +release = "1.9.1" # The short X.Y version. version = release diff --git a/pyproject.toml b/pyproject.toml index 346dedd..d248ab2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "evdev" -version = "1.9.0" +version = "1.9.1" description = "Bindings to the Linux input handling subsystem" keywords = ["evdev", "input", "uinput"] readme = "README.md" @@ -36,7 +36,7 @@ line-length = 120 ignore = ["E265", "E241", "F403", "F401", "E401", "E731"] [tool.bumpversion] -current_version = "1.9.0" +current_version = "1.9.1" commit = true tag = true allow_dirty = true From 5f9fd2cd11daa9a54452dadbf00aaf284d3d6063 Mon Sep 17 00:00:00 2001 From: bastian-wattro <106541220+bastian-wattro@users.noreply.github.com> Date: Fri, 28 Feb 2025 08:41:31 +0100 Subject: [PATCH 08/17] 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 09/17] 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 10/17] 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 11/17] 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 12/17] =?UTF-8?q?Bump=20version:=201.9.1=20=E2=86=92=201.9?= =?UTF-8?q?.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/changelog.rst | 11 +++++++++++ docs/conf.py | 2 +- pyproject.toml | 4 ++-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 4dcf62f..49f5911 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,6 +2,17 @@ Changelog --------- +1.9.2 (May 01, 2025) +==================== + +- Add the "--reproducible" build option which removes the build date and used headers from the + generated ``ecodes.c``. Example usage:: + + python -m build --config-setting=--build-option='build_ecodes --reproducible' -n + +- Use ``Generic`` to set precise type for ``InputDevice.path``. + + 1.9.1 (Feb 22, 2025) ==================== diff --git a/docs/conf.py b/docs/conf.py index 86b3d06..758f878 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -65,7 +65,7 @@ # built documents. # # The full version, including alpha/beta/rc tags. -release = "1.9.1" +release = "1.9.2" # The short X.Y version. version = release diff --git a/pyproject.toml b/pyproject.toml index d248ab2..e6a6ac7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "evdev" -version = "1.9.1" +version = "1.9.2" description = "Bindings to the Linux input handling subsystem" keywords = ["evdev", "input", "uinput"] readme = "README.md" @@ -36,7 +36,7 @@ line-length = 120 ignore = ["E265", "E241", "F403", "F401", "E401", "E731"] [tool.bumpversion] -current_version = "1.9.1" +current_version = "1.9.2" commit = true tag = true allow_dirty = true From 5227b1672cbf074287088860c855c24bb96fe6b1 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Thu, 5 Feb 2026 00:09:01 +0100 Subject: [PATCH 13/17] Fix memory leaks --- src/evdev/input.c | 49 ++++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/src/evdev/input.c b/src/evdev/input.c index 4ad0408..894db22 100644 --- a/src/evdev/input.c +++ b/src/evdev/input.c @@ -63,12 +63,12 @@ device_read(PyObject *self, PyObject *args) return NULL; } - PyObject* sec = PyLong_FromLong(event.input_event_sec); - PyObject* usec = PyLong_FromLong(event.input_event_usec); - PyObject* val = PyLong_FromLong(event.value); - PyObject* type = PyLong_FromLong(event.type); - PyObject* code = PyLong_FromLong(event.code); - PyObject* py_input_event = PyTuple_Pack(5, sec, usec, type, code, val); + PyObject *py_input_event = PyTuple_New(5); + PyTuple_SET_ITEM(py_input_event, 0, PyLong_FromLong(event.input_event_sec)); + PyTuple_SET_ITEM(py_input_event, 1, PyLong_FromLong(event.input_event_usec)); + PyTuple_SET_ITEM(py_input_event, 2, PyLong_FromLong(event.type)); + PyTuple_SET_ITEM(py_input_event, 3, PyLong_FromLong(event.code)); + PyTuple_SET_ITEM(py_input_event, 4, PyLong_FromLong(event.value)); return py_input_event; } @@ -81,14 +81,6 @@ device_read_many(PyObject *self, PyObject *args) // get device file descriptor (O_RDONLY|O_NONBLOCK) int fd = (int)PyLong_AsLong(PyTuple_GET_ITEM(args, 0)); - PyObject* py_input_event = NULL; - PyObject* events = NULL; - PyObject* sec = NULL; - PyObject* usec = NULL; - PyObject* val = NULL; - PyObject* type = NULL; - PyObject* code = NULL; - struct input_event event[64]; size_t event_size = sizeof(struct input_event); @@ -101,15 +93,15 @@ device_read_many(PyObject *self, PyObject *args) // Construct a tuple of event tuples. Each tuple is the arguments to InputEvent. size_t num_events = nread / event_size; - events = PyTuple_New(num_events); - for (size_t i = 0 ; i < num_events; i++) { - sec = PyLong_FromLong(event[i].input_event_sec); - usec = PyLong_FromLong(event[i].input_event_usec); - val = PyLong_FromLong(event[i].value); - type = PyLong_FromLong(event[i].type); - code = PyLong_FromLong(event[i].code); - py_input_event = PyTuple_Pack(5, sec, usec, type, code, val); + PyObject* events = PyTuple_New(num_events); + for (size_t i = 0 ; i < num_events; i++) { + PyObject *py_input_event = PyTuple_New(5); + PyTuple_SET_ITEM(py_input_event, 0, PyLong_FromLong(event[i].input_event_sec)); + PyTuple_SET_ITEM(py_input_event, 1, PyLong_FromLong(event[i].input_event_usec)); + PyTuple_SET_ITEM(py_input_event, 2, PyLong_FromLong(event[i].type)); + PyTuple_SET_ITEM(py_input_event, 3, PyLong_FromLong(event[i].code)); + PyTuple_SET_ITEM(py_input_event, 4, PyLong_FromLong(event[i].value)); PyTuple_SET_ITEM(events, i, py_input_event); } @@ -200,6 +192,11 @@ ioctl_capabilities(PyObject *self, PyObject *args) return capabilities; on_err: + Py_XDECREF(capabilities); + Py_XDECREF(eventcodes); + Py_XDECREF(capability); + Py_XDECREF(py_absinfo); + Py_XDECREF(absitem); PyErr_SetFromErrno(PyExc_OSError); return NULL; } @@ -408,7 +405,9 @@ ioctl_EVIOCG_bits(PyObject *self, PyObject *args) PyObject* res = PyList_New(0); for (int i=0; i<=max; i++) { if (test_bit(bytes, i)) { - PyList_Append(res, Py_BuildValue("i", i)); + PyObject *val = PyLong_FromLong(i); + PyList_Append(res, val); + Py_DECREF(val); } } @@ -523,7 +522,9 @@ ioctl_EVIOCGPROP(PyObject *self, PyObject *args) PyObject* res = PyList_New(0); for (int i=0; i Date: Thu, 5 Feb 2026 22:16:48 +0100 Subject: [PATCH 14/17] CI fixes --- .github/workflows/install.yaml | 6 +++--- .github/workflows/lint.yml | 6 +++--- .github/workflows/test.yml | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/install.yaml b/.github/workflows/install.yaml index 87502ad..f07c035 100644 --- a/.github/workflows/install.yaml +++ b/.github/workflows/install.yaml @@ -11,15 +11,15 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] include: - os: ubuntu-latest python-version: "3.8" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e293976..20d254b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,12 +11,12 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python-version: ["3.12"] + python-version: ["3.14"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b9cd26d..073d524 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,12 +11,12 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python-version: ["3.12"] + python-version: ["3.14"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} From fae2cf9d1d4a3e2700e148399f312b44b6208a56 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Thu, 5 Feb 2026 22:24:12 +0100 Subject: [PATCH 15/17] Use an SPDX license --- pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e6a6ac7..665a9b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ version = "1.9.2" description = "Bindings to the Linux input handling subsystem" keywords = ["evdev", "input", "uinput"] readme = "README.md" -license = {file = "LICENSE"} +license = "BSD-3-Clause" requires-python = ">=3.8" authors = [ { name="Georgi Valkov", email="georgi.t.valkov@gmail.com" }, @@ -22,7 +22,6 @@ classifiers = [ "Operating System :: POSIX :: Linux", "Intended Audience :: Developers", "Topic :: Software Development :: Libraries", - "License :: OSI Approved :: BSD License", "Programming Language :: Python :: Implementation :: CPython", ] From faf7bc93c6a97edc317c4cc6a8d81ab94e5ba77f Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Thu, 5 Feb 2026 22:36:13 +0100 Subject: [PATCH 16/17] Drop support for Python 3.8 and raise setuptools version to 77.0 --- .github/workflows/install.yaml | 4 ++-- docs/changelog.rst | 7 +++++++ pyproject.toml | 4 ++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/install.yaml b/.github/workflows/install.yaml index f07c035..e879179 100644 --- a/.github/workflows/install.yaml +++ b/.github/workflows/install.yaml @@ -11,10 +11,10 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] include: - os: ubuntu-latest - python-version: "3.8" + python-version: "3.9" steps: - uses: actions/checkout@v6 diff --git a/docs/changelog.rst b/docs/changelog.rst index 49f5911..bcf1636 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,13 @@ Changelog --------- +1.9.3 (Feb 05, 2025) +==================== + +- Fix several memory leaks in ``input.c``. + +- Raise the minimum supported Python version to 3.9 and the setuptools version to 77.0. + 1.9.2 (May 01, 2025) ==================== diff --git a/pyproject.toml b/pyproject.toml index 665a9b7..159460c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=61.0"] +requires = ["setuptools>=77.0"] build-backend = "setuptools.build_meta" [project] @@ -9,7 +9,7 @@ description = "Bindings to the Linux input handling subsystem" keywords = ["evdev", "input", "uinput"] readme = "README.md" license = "BSD-3-Clause" -requires-python = ">=3.8" +requires-python = ">=3.9" authors = [ { name="Georgi Valkov", email="georgi.t.valkov@gmail.com" }, ] From a47b5b5a6f79bde6823095d1105501856338aed7 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Thu, 5 Feb 2026 22:46:48 +0100 Subject: [PATCH 17/17] =?UTF-8?q?Bump=20version:=201.9.2=20=E2=86=92=201.9?= =?UTF-8?q?.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/conf.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 758f878..0be06b3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -65,7 +65,7 @@ # built documents. # # The full version, including alpha/beta/rc tags. -release = "1.9.2" +release = "1.9.3" # The short X.Y version. version = release diff --git a/pyproject.toml b/pyproject.toml index 159460c..d0b4f7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "evdev" -version = "1.9.2" +version = "1.9.3" description = "Bindings to the Linux input handling subsystem" keywords = ["evdev", "input", "uinput"] readme = "README.md" @@ -35,7 +35,7 @@ line-length = 120 ignore = ["E265", "E241", "F403", "F401", "E401", "E731"] [tool.bumpversion] -current_version = "1.9.2" +current_version = "1.9.3" commit = true tag = true allow_dirty = true