diff --git a/.github/workflows/install.yaml b/.github/workflows/install.yaml index 87502ad..e879179 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.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@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 }} 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/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 e6a6ac7..d0b4f7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,15 +1,15 @@ [build-system] -requires = ["setuptools>=61.0"] +requires = ["setuptools>=77.0"] 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" -license = {file = "LICENSE"} -requires-python = ">=3.8" +license = "BSD-3-Clause" +requires-python = ">=3.9" 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", ] @@ -36,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 diff --git a/src/evdev/eventio_async.py b/src/evdev/eventio_async.py index 4af1aab..89a2ede 100644 --- a/src/evdev/eventio_async.py +++ b/src/evdev/eventio_async.py @@ -36,7 +36,7 @@ def __aiter__(self) -> Self: return self def __anext__(self) -> "asyncio.Future[InputEvent]": - future = asyncio.Future() + future = asyncio.get_running_loop().create_future() try: # Read from the previous batch of events. future.set_result(next(self.current_batch)) @@ -54,8 +54,15 @@ def next_batch_ready(batch): class EventIO(eventio.EventIO): + # The event loop a reader was last registered on, or None if no async read + # has been awaited yet. Set in _do_when_readable, used by close(). + _loop: "asyncio.AbstractEventLoop | None" = None + def _do_when_readable(self, callback): - loop = asyncio.get_event_loop() + # Remember the loop the reader is registered on so that close() can + # remove it later, even when called without a running event loop. + loop = asyncio.get_running_loop() + self._loop = loop def ready(): loop.remove_reader(self.fileno()) @@ -74,7 +81,7 @@ def async_read_one(self): Asyncio coroutine to read and return a single input event as an instance of :class:`InputEvent `. """ - future = asyncio.Future() + future = asyncio.get_running_loop().create_future() self._do_when_readable(lambda: self._set_result(future, self.read_one)) return future @@ -84,7 +91,7 @@ def async_read(self): a generator object that yields :class:`InputEvent ` instances. """ - future = asyncio.Future() + future = asyncio.get_running_loop().create_future() self._do_when_readable(lambda: self._set_result(future, self.read)) return future @@ -97,10 +104,11 @@ def async_read_loop(self) -> ReadIterator: return ReadIterator(self) def close(self): - try: - loop = asyncio.get_event_loop() - loop.remove_reader(self.fileno()) - except RuntimeError: - # no event loop present, so there is nothing to - # remove the reader from. Ignore - pass + # A reader is only registered once an async read has been awaited, in + # which case _do_when_readable recorded the loop it was added to. + loop = self._loop + if loop is None or loop.is_closed(): + # No reader was ever registered, or its loop is already gone, so + # there is nothing to remove the reader from. + return + loop.remove_reader(self.fileno()) 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