linux-gpio.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* [libgpiod v2][PATCH v3 0/4] bindings: implement python bindings for libgpiod v2
@ 2022-10-07 14:55 Bartosz Golaszewski
  2022-10-07 14:55 ` [libgpiod v2][PATCH v3 1/4] bindings: python: remove old version Bartosz Golaszewski
                   ` (4 more replies)
  0 siblings, 5 replies; 30+ messages in thread
From: Bartosz Golaszewski @ 2022-10-07 14:55 UTC (permalink / raw)
  To: Kent Gibson, Linus Walleij, Andy Shevchenko, Viresh Kumar
  Cc: linux-gpio, Bartosz Golaszewski

This is the third iteration of python bindings for libgpiod but it really has
very little in common with the previous version.

This time the code has been split into high-level python and low-level
C layers with the latter only doing the bare minimum.

The data model is mostly based on the C++ one with the main difference
being utilizing dynamic typing and keyword arguments in place of the
builder pattern. That allows us to reduce the number of methods and
objects.

Because python doesn't have RAII, unlike C++, we provide a module-level
request_lines() helper as gpiod.Chip(path).request_lines(...) one-liner
could lead to the chip left dangling even after the last reference is
dropped.

Because python forces us to dynamically allocate objects all the time even
for fundamental types (which are also immutable) there's no point in trying
to replicate the edge-event buffer. Instead LineRequest.read_edge_event()
just returns a list of events.

Bartosz Golaszewski (4):
  bindings: python: remove old version
  bindings: python: add examples
  bindings: python: add tests
  bindings: python: implement python bindings for libgpiod v2

 bindings/python/.gitignore                   |    8 +
 bindings/python/Makefile.am                  |   26 +-
 bindings/python/examples/Makefile.am         |   16 +-
 bindings/python/examples/gpiodetect.py       |   15 +-
 bindings/python/examples/gpiofind.py         |   14 +-
 bindings/python/examples/gpioget.py          |   34 +-
 bindings/python/examples/gpioinfo.py         |   41 +-
 bindings/python/examples/gpiomon.py          |   52 +-
 bindings/python/examples/gpioset.py          |   46 +-
 bindings/python/gpiod/Makefile.am            |   17 +
 bindings/python/gpiod/__init__.py            |   53 +
 bindings/python/gpiod/chip.py                |  308 ++
 bindings/python/gpiod/chip_info.py           |   21 +
 bindings/python/gpiod/edge_event.py          |   46 +
 bindings/python/gpiod/exception.py           |   20 +
 bindings/python/gpiod/ext/Makefile.am        |   11 +
 bindings/python/gpiod/ext/chip.c             |  335 +++
 bindings/python/gpiod/ext/common.c           |   92 +
 bindings/python/gpiod/ext/internal.h         |   20 +
 bindings/python/gpiod/ext/line-config.c      |  133 +
 bindings/python/gpiod/ext/line-settings.c    |  130 +
 bindings/python/gpiod/ext/module.c           |  193 ++
 bindings/python/gpiod/ext/request.c          |  402 +++
 bindings/python/gpiod/info_event.py          |   33 +
 bindings/python/gpiod/internal.py            |   19 +
 bindings/python/gpiod/line.py                |   56 +
 bindings/python/gpiod/line_info.py           |   73 +
 bindings/python/gpiod/line_request.py        |  258 ++
 bindings/python/gpiod/line_settings.py       |   62 +
 bindings/python/gpiodmodule.c                | 2662 ------------------
 bindings/python/setup.py                     |   47 +
 bindings/python/tests/Makefile.am            |   26 +-
 bindings/python/tests/__init__.py            |   17 +
 bindings/python/tests/__main__.py            |   16 +
 bindings/python/tests/gpiod_py_test.py       |  832 ------
 bindings/python/tests/gpiomockupmodule.c     |  309 --
 bindings/python/tests/gpiosim/Makefile.am    |    7 +
 bindings/python/tests/gpiosim/__init__.py    |    4 +
 bindings/python/tests/gpiosim/chip.py        |   66 +
 bindings/python/tests/gpiosim/ext.c          |  345 +++
 bindings/python/tests/helpers.py             |   16 +
 bindings/python/tests/tests_chip.py          |  231 ++
 bindings/python/tests/tests_chip_info.py     |   52 +
 bindings/python/tests/tests_edge_event.py    |  219 ++
 bindings/python/tests/tests_info_event.py    |  189 ++
 bindings/python/tests/tests_line_info.py     |  101 +
 bindings/python/tests/tests_line_request.py  |  449 +++
 bindings/python/tests/tests_line_settings.py |   79 +
 bindings/python/tests/tests_module.py        |   59 +
 configure.ac                                 |    3 +
 50 files changed, 4338 insertions(+), 3925 deletions(-)
 create mode 100644 bindings/python/.gitignore
 create mode 100644 bindings/python/gpiod/Makefile.am
 create mode 100644 bindings/python/gpiod/__init__.py
 create mode 100644 bindings/python/gpiod/chip.py
 create mode 100644 bindings/python/gpiod/chip_info.py
 create mode 100644 bindings/python/gpiod/edge_event.py
 create mode 100644 bindings/python/gpiod/exception.py
 create mode 100644 bindings/python/gpiod/ext/Makefile.am
 create mode 100644 bindings/python/gpiod/ext/chip.c
 create mode 100644 bindings/python/gpiod/ext/common.c
 create mode 100644 bindings/python/gpiod/ext/internal.h
 create mode 100644 bindings/python/gpiod/ext/line-config.c
 create mode 100644 bindings/python/gpiod/ext/line-settings.c
 create mode 100644 bindings/python/gpiod/ext/module.c
 create mode 100644 bindings/python/gpiod/ext/request.c
 create mode 100644 bindings/python/gpiod/info_event.py
 create mode 100644 bindings/python/gpiod/internal.py
 create mode 100644 bindings/python/gpiod/line.py
 create mode 100644 bindings/python/gpiod/line_info.py
 create mode 100644 bindings/python/gpiod/line_request.py
 create mode 100644 bindings/python/gpiod/line_settings.py
 delete mode 100644 bindings/python/gpiodmodule.c
 create mode 100644 bindings/python/setup.py
 create mode 100644 bindings/python/tests/__init__.py
 create mode 100644 bindings/python/tests/__main__.py
 delete mode 100755 bindings/python/tests/gpiod_py_test.py
 delete mode 100644 bindings/python/tests/gpiomockupmodule.c
 create mode 100644 bindings/python/tests/gpiosim/Makefile.am
 create mode 100644 bindings/python/tests/gpiosim/__init__.py
 create mode 100644 bindings/python/tests/gpiosim/chip.py
 create mode 100644 bindings/python/tests/gpiosim/ext.c
 create mode 100644 bindings/python/tests/helpers.py
 create mode 100644 bindings/python/tests/tests_chip.py
 create mode 100644 bindings/python/tests/tests_chip_info.py
 create mode 100644 bindings/python/tests/tests_edge_event.py
 create mode 100644 bindings/python/tests/tests_info_event.py
 create mode 100644 bindings/python/tests/tests_line_info.py
 create mode 100644 bindings/python/tests/tests_line_request.py
 create mode 100644 bindings/python/tests/tests_line_settings.py
 create mode 100644 bindings/python/tests/tests_module.py

-- 
2.34.1


^ permalink raw reply	[flat|nested] 30+ messages in thread

* [libgpiod v2][PATCH v3 1/4] bindings: python: remove old version
  2022-10-07 14:55 [libgpiod v2][PATCH v3 0/4] bindings: implement python bindings for libgpiod v2 Bartosz Golaszewski
@ 2022-10-07 14:55 ` Bartosz Golaszewski
  2022-10-07 14:55 ` [libgpiod v2][PATCH v3 2/4] bindings: python: add examples Bartosz Golaszewski
                   ` (3 subsequent siblings)
  4 siblings, 0 replies; 30+ messages in thread
From: Bartosz Golaszewski @ 2022-10-07 14:55 UTC (permalink / raw)
  To: Kent Gibson, Linus Walleij, Andy Shevchenko, Viresh Kumar
  Cc: linux-gpio, Bartosz Golaszewski

This removes v1 python bindings for easier review of v2.

Signed-off-by: Bartosz Golaszewski <brgl@bgdev.pl>
---
 bindings/python/Makefile.am              |   25 -
 bindings/python/examples/Makefile.am     |   10 -
 bindings/python/examples/gpiodetect.py   |   16 -
 bindings/python/examples/gpiofind.py     |   20 -
 bindings/python/examples/gpioget.py      |   25 -
 bindings/python/examples/gpioinfo.py     |   28 -
 bindings/python/examples/gpiomon.py      |   42 -
 bindings/python/examples/gpioset.py      |   25 -
 bindings/python/gpiodmodule.c            | 2662 ----------------------
 bindings/python/tests/Makefile.am        |   13 -
 bindings/python/tests/gpiod_py_test.py   |  832 -------
 bindings/python/tests/gpiomockupmodule.c |  309 ---
 12 files changed, 4007 deletions(-)
 delete mode 100644 bindings/python/Makefile.am
 delete mode 100644 bindings/python/examples/Makefile.am
 delete mode 100755 bindings/python/examples/gpiodetect.py
 delete mode 100755 bindings/python/examples/gpiofind.py
 delete mode 100755 bindings/python/examples/gpioget.py
 delete mode 100755 bindings/python/examples/gpioinfo.py
 delete mode 100755 bindings/python/examples/gpiomon.py
 delete mode 100755 bindings/python/examples/gpioset.py
 delete mode 100644 bindings/python/gpiodmodule.c
 delete mode 100644 bindings/python/tests/Makefile.am
 delete mode 100755 bindings/python/tests/gpiod_py_test.py
 delete mode 100644 bindings/python/tests/gpiomockupmodule.c

diff --git a/bindings/python/Makefile.am b/bindings/python/Makefile.am
deleted file mode 100644
index 4405d8f..0000000
--- a/bindings/python/Makefile.am
+++ /dev/null
@@ -1,25 +0,0 @@
-# SPDX-License-Identifier: GPL-2.0-or-later
-# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
-
-pyexec_LTLIBRARIES = gpiod.la
-
-gpiod_la_SOURCES = gpiodmodule.c
-
-gpiod_la_CFLAGS = -I$(top_srcdir)/include/
-gpiod_la_CFLAGS += -Wall -Wextra -g -std=gnu89 $(PYTHON_CPPFLAGS)
-gpiod_la_LDFLAGS = -module -avoid-version
-gpiod_la_LIBADD = $(top_builddir)/lib/libgpiod.la $(PYTHON_LIBS)
-
-SUBDIRS = .
-
-if WITH_TESTS
-
-SUBDIRS += tests
-
-endif
-
-if WITH_EXAMPLES
-
-SUBDIRS += examples
-
-endif
diff --git a/bindings/python/examples/Makefile.am b/bindings/python/examples/Makefile.am
deleted file mode 100644
index 4169469..0000000
--- a/bindings/python/examples/Makefile.am
+++ /dev/null
@@ -1,10 +0,0 @@
-# SPDX-License-Identifier: GPL-2.0-or-later
-# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
-
-EXTRA_DIST =				\
-		gpiodetect.py		\
-		gpiofind.py		\
-		gpioget.py		\
-		gpioinfo.py		\
-		gpiomon.py		\
-		gpioset.py
diff --git a/bindings/python/examples/gpiodetect.py b/bindings/python/examples/gpiodetect.py
deleted file mode 100755
index da6ee9a..0000000
--- a/bindings/python/examples/gpiodetect.py
+++ /dev/null
@@ -1,16 +0,0 @@
-#!/usr/bin/env python3
-# SPDX-License-Identifier: GPL-2.0-or-later
-# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
-
-'''Reimplementation of the gpiodetect tool in Python.'''
-
-import gpiod
-import os
-
-if __name__ == '__main__':
-    for entry in os.scandir('/dev/'):
-        if gpiod.is_gpiochip_device(entry.path):
-            with gpiod.Chip(entry.path) as chip:
-                print('{} [{}] ({} lines)'.format(chip.name(),
-                                                  chip.label(),
-                                                  chip.num_lines()))
diff --git a/bindings/python/examples/gpiofind.py b/bindings/python/examples/gpiofind.py
deleted file mode 100755
index a9ec734..0000000
--- a/bindings/python/examples/gpiofind.py
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/usr/bin/env python3
-# SPDX-License-Identifier: GPL-2.0-or-later
-# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
-
-'''Reimplementation of the gpiofind tool in Python.'''
-
-import gpiod
-import os
-import sys
-
-if __name__ == '__main__':
-    for entry in os.scandir('/dev/'):
-        if gpiod.is_gpiochip_device(entry.path):
-            with gpiod.Chip(entry.path) as chip:
-                offset = chip.find_line(sys.argv[1], unique=True)
-                if offset is not None:
-                     print('{} {}'.format(line.owner().name(), offset))
-                     sys.exit(0)
-
-    sys.exit(1)
diff --git a/bindings/python/examples/gpioget.py b/bindings/python/examples/gpioget.py
deleted file mode 100755
index 26a2ced..0000000
--- a/bindings/python/examples/gpioget.py
+++ /dev/null
@@ -1,25 +0,0 @@
-#!/usr/bin/env python3
-# SPDX-License-Identifier: GPL-2.0-or-later
-# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
-
-'''Simplified reimplementation of the gpioget tool in Python.'''
-
-import gpiod
-import sys
-
-if __name__ == '__main__':
-    if len(sys.argv) < 3:
-        raise TypeError('usage: gpioget.py <gpiochip> <offset1> <offset2> ...')
-
-    with gpiod.Chip(sys.argv[1]) as chip:
-        offsets = []
-        for off in sys.argv[2:]:
-            offsets.append(int(off))
-
-        lines = chip.get_lines(offsets)
-        lines.request(consumer=sys.argv[0], type=gpiod.LINE_REQ_DIR_IN)
-        vals = lines.get_values()
-
-        for val in vals:
-            print(val, end=' ')
-        print()
diff --git a/bindings/python/examples/gpioinfo.py b/bindings/python/examples/gpioinfo.py
deleted file mode 100755
index 84188f1..0000000
--- a/bindings/python/examples/gpioinfo.py
+++ /dev/null
@@ -1,28 +0,0 @@
-#!/usr/bin/env python3
-# SPDX-License-Identifier: GPL-2.0-or-later
-# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
-
-'''Simplified reimplementation of the gpioinfo tool in Python.'''
-
-import gpiod
-import os
-
-if __name__ == '__main__':
-    for entry in os.scandir('/dev/'):
-        if gpiod.is_gpiochip_device(entry.path):
-            with gpiod.Chip(entry.path) as chip:
-                print('{} - {} lines:'.format(chip.name(), chip.num_lines()))
-
-                for line in gpiod.LineIter(chip):
-                    offset = line.offset()
-                    name = line.name()
-                    consumer = line.consumer()
-                    direction = line.direction()
-                    active_low = line.is_active_low()
-
-                    print('\tline {:>3}: {:>18} {:>12} {:>8} {:>10}'.format(
-                          offset,
-                          'unnamed' if name is None else name,
-                          'unused' if consumer is None else consumer,
-                          'input' if direction == gpiod.Line.DIRECTION_INPUT else 'output',
-                          'active-low' if active_low else 'active-high'))
diff --git a/bindings/python/examples/gpiomon.py b/bindings/python/examples/gpiomon.py
deleted file mode 100755
index b29f3ce..0000000
--- a/bindings/python/examples/gpiomon.py
+++ /dev/null
@@ -1,42 +0,0 @@
-#!/usr/bin/env python3
-# SPDX-License-Identifier: GPL-2.0-or-later
-# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
-
-'''Simplified reimplementation of the gpiomon tool in Python.'''
-
-import gpiod
-import sys
-
-if __name__ == '__main__':
-    def print_event(event):
-        if event.type == gpiod.LineEvent.RISING_EDGE:
-            evstr = ' RISING EDGE'
-        elif event.type == gpiod.LineEvent.FALLING_EDGE:
-            evstr = 'FALLING EDGE'
-        else:
-            raise TypeError('Invalid event type')
-
-        print('event: {} offset: {} timestamp: [{}.{}]'.format(evstr,
-                                                               event.source.offset(),
-                                                               event.sec, event.nsec))
-
-    if len(sys.argv) < 3:
-        raise TypeError('usage: gpiomon.py <gpiochip> <offset1> <offset2> ...')
-
-    with gpiod.Chip(sys.argv[1]) as chip:
-        offsets = []
-        for off in sys.argv[2:]:
-            offsets.append(int(off))
-
-        lines = chip.get_lines(offsets)
-        lines.request(consumer=sys.argv[0], type=gpiod.LINE_REQ_EV_BOTH_EDGES)
-
-        try:
-            while True:
-                ev_lines = lines.event_wait(sec=1)
-                if ev_lines:
-                    for line in ev_lines:
-                        event = line.event_read()
-                        print_event(event)
-        except KeyboardInterrupt:
-            sys.exit(130)
diff --git a/bindings/python/examples/gpioset.py b/bindings/python/examples/gpioset.py
deleted file mode 100755
index 63e08dc..0000000
--- a/bindings/python/examples/gpioset.py
+++ /dev/null
@@ -1,25 +0,0 @@
-#!/usr/bin/env python3
-# SPDX-License-Identifier: GPL-2.0-or-later
-# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
-
-'''Simplified reimplementation of the gpioset tool in Python.'''
-
-import gpiod
-import sys
-
-if __name__ == '__main__':
-    if len(sys.argv) < 3:
-        raise TypeError('usage: gpioset.py <gpiochip> <offset1>=<value1> ...')
-
-    with gpiod.Chip(sys.argv[1]) as chip:
-        offsets = []
-        values = []
-        for arg in sys.argv[2:]:
-            arg = arg.split('=')
-            offsets.append(int(arg[0]))
-            values.append(int(arg[1]))
-
-        lines = chip.get_lines(offsets)
-        lines.request(consumer=sys.argv[0], type=gpiod.LINE_REQ_DIR_OUT)
-        lines.set_values(values)
-        input()
diff --git a/bindings/python/gpiodmodule.c b/bindings/python/gpiodmodule.c
deleted file mode 100644
index ed039e4..0000000
--- a/bindings/python/gpiodmodule.c
+++ /dev/null
@@ -1,2662 +0,0 @@
-// SPDX-License-Identifier: LGPL-2.1-or-later
-// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
-
-#include <Python.h>
-#include <gpiod.h>
-
-#define LINE_REQUEST_MAX_LINES 64
-
-typedef struct {
-	PyObject_HEAD;
-	struct gpiod_chip *chip;
-} gpiod_ChipObject;
-
-typedef struct {
-	PyObject_HEAD;
-	struct gpiod_line *line;
-	gpiod_ChipObject *owner;
-} gpiod_LineObject;
-
-typedef struct {
-	PyObject_HEAD;
-	struct gpiod_line_event event;
-	gpiod_LineObject *source;
-} gpiod_LineEventObject;
-
-typedef struct {
-	PyObject_HEAD;
-	PyObject **lines;
-	Py_ssize_t num_lines;
-	Py_ssize_t iter_idx;
-} gpiod_LineBulkObject;
-
-typedef struct {
-	PyObject_HEAD;
-	unsigned int offset;
-	gpiod_ChipObject *owner;
-} gpiod_LineIterObject;
-
-static gpiod_LineBulkObject *gpiod_LineToLineBulk(gpiod_LineObject *line);
-static gpiod_LineObject *gpiod_MakeLineObject(gpiod_ChipObject *owner,
-					      struct gpiod_line *line);
-
-enum {
-	gpiod_LINE_REQ_DIR_AS_IS = 1,
-	gpiod_LINE_REQ_DIR_IN,
-	gpiod_LINE_REQ_DIR_OUT,
-	gpiod_LINE_REQ_EV_FALLING_EDGE,
-	gpiod_LINE_REQ_EV_RISING_EDGE,
-	gpiod_LINE_REQ_EV_BOTH_EDGES,
-};
-
-enum {
-	gpiod_LINE_REQ_FLAG_OPEN_DRAIN		= GPIOD_BIT(0),
-	gpiod_LINE_REQ_FLAG_OPEN_SOURCE		= GPIOD_BIT(1),
-	gpiod_LINE_REQ_FLAG_ACTIVE_LOW		= GPIOD_BIT(2),
-	gpiod_LINE_REQ_FLAG_BIAS_DISABLED	= GPIOD_BIT(3),
-	gpiod_LINE_REQ_FLAG_BIAS_PULL_DOWN	= GPIOD_BIT(4),
-	gpiod_LINE_REQ_FLAG_BIAS_PULL_UP	= GPIOD_BIT(5),
-};
-
-enum {
-	gpiod_DIRECTION_INPUT = 1,
-	gpiod_DIRECTION_OUTPUT,
-};
-
-enum {
-	gpiod_DRIVE_PUSH_PULL,
-	gpiod_DRIVE_OPEN_DRAIN,
-	gpiod_DRIVE_OPEN_SOURCE,
-};
-
-enum {
-	gpiod_BIAS_UNKNOWN = 1,
-	gpiod_BIAS_DISABLED,
-	gpiod_BIAS_PULL_UP,
-	gpiod_BIAS_PULL_DOWN,
-};
-
-enum {
-	gpiod_RISING_EDGE = 1,
-	gpiod_FALLING_EDGE,
-};
-
-static bool gpiod_ChipIsClosed(gpiod_ChipObject *chip)
-{
-	if (!chip->chip) {
-		PyErr_SetString(PyExc_ValueError,
-				"I/O operation on closed file");
-		return true;
-	}
-
-	return false;
-}
-
-static PyObject *gpiod_CallMethodPyArgs(PyObject *obj, const char *method,
-					PyObject *args, PyObject *kwds)
-{
-	PyObject *callable, *ret;
-
-	callable = PyObject_GetAttrString((PyObject *)obj, method);
-	if (!callable)
-		return NULL;
-
-	ret = PyObject_Call(callable, args, kwds);
-	Py_DECREF(callable);
-
-	return ret;
-}
-
-static int gpiod_LineEvent_init(PyObject *Py_UNUSED(ignored0),
-				PyObject *Py_UNUSED(ignored1),
-				PyObject *Py_UNUSED(ignored2))
-{
-	PyErr_SetString(PyExc_NotImplementedError,
-			"Only gpiod.Line can create new LineEvent objects.");
-	return -1;
-}
-
-static void gpiod_LineEvent_dealloc(gpiod_LineEventObject *self)
-{
-	if (self->source)
-		Py_DECREF(self->source);
-
-	PyObject_Del(self);
-}
-
-PyDoc_STRVAR(gpiod_LineEvent_get_type_doc,
-"Event type of this line event (integer).");
-
-PyObject *gpiod_LineEvent_get_type(gpiod_LineEventObject *self,
-				   PyObject *Py_UNUSED(ignored))
-{
-	int rv;
-
-	if (self->event.event_type == GPIOD_LINE_EVENT_RISING_EDGE)
-		rv = gpiod_RISING_EDGE;
-	else
-		rv = gpiod_FALLING_EDGE;
-
-	return Py_BuildValue("I", rv);
-}
-
-PyDoc_STRVAR(gpiod_LineEvent_get_sec_doc,
-"Seconds value of the line event timestamp (integer).");
-
-PyObject *gpiod_LineEvent_get_sec(gpiod_LineEventObject *self,
-				  PyObject *Py_UNUSED(ignored))
-{
-	return Py_BuildValue("I", self->event.ts.tv_sec);
-}
-
-PyDoc_STRVAR(gpiod_LineEvent_get_nsec_doc,
-"Nanoseconds value of the line event timestamp (integer).");
-
-PyObject *gpiod_LineEvent_get_nsec(gpiod_LineEventObject *self,
-				   PyObject *Py_UNUSED(ignored))
-{
-	return Py_BuildValue("I", self->event.ts.tv_nsec);
-}
-
-PyDoc_STRVAR(gpiod_LineEvent_get_source_doc,
-"Line object representing the GPIO line on which this event\n"
-"occurred (gpiod.Line object).");
-
-gpiod_LineObject *gpiod_LineEvent_get_source(gpiod_LineEventObject *self,
-					     PyObject *Py_UNUSED(ignored))
-{
-	Py_INCREF(self->source);
-	return self->source;
-}
-
-static PyGetSetDef gpiod_LineEvent_getset[] = {
-	{
-		.name = "type",
-		.get = (getter)gpiod_LineEvent_get_type,
-		.doc = gpiod_LineEvent_get_type_doc,
-	},
-	{
-		.name = "sec",
-		.get = (getter)gpiod_LineEvent_get_sec,
-		.doc = gpiod_LineEvent_get_sec_doc,
-	},
-	{
-		.name = "nsec",
-		.get = (getter)gpiod_LineEvent_get_nsec,
-		.doc = gpiod_LineEvent_get_nsec_doc,
-	},
-	{
-		.name = "source",
-		.get = (getter)gpiod_LineEvent_get_source,
-		.doc = gpiod_LineEvent_get_source_doc,
-	},
-	{ }
-};
-
-static PyObject *gpiod_LineEvent_repr(gpiod_LineEventObject *self)
-{
-	PyObject *line_repr, *ret;
-	const char *edge;
-
-	if (self->event.event_type == GPIOD_LINE_EVENT_RISING_EDGE)
-		edge = "RISING EDGE";
-	else
-		edge = "FALLING EDGE";
-
-	line_repr = PyObject_CallMethod((PyObject *)self->source,
-					"__repr__", "");
-
-	ret = PyUnicode_FromFormat("'%s (%ld.%ld) source(%S)'",
-				   edge, self->event.ts.tv_sec,
-				   self->event.ts.tv_nsec, line_repr);
-	Py_DECREF(line_repr);
-
-	return ret;
-}
-
-PyDoc_STRVAR(gpiod_LineEventType_doc,
-"Represents a single GPIO line event. This object is immutable and can only\n"
-"be created by an instance of gpiod.Line.");
-
-static PyTypeObject gpiod_LineEventType = {
-	PyVarObject_HEAD_INIT(NULL, 0)
-	.tp_name = "gpiod.LineEvent",
-	.tp_basicsize = sizeof(gpiod_LineEventObject),
-	.tp_flags = Py_TPFLAGS_DEFAULT,
-	.tp_doc = gpiod_LineEventType_doc,
-	.tp_new = PyType_GenericNew,
-	.tp_init = (initproc)gpiod_LineEvent_init,
-	.tp_dealloc = (destructor)gpiod_LineEvent_dealloc,
-	.tp_getset = gpiod_LineEvent_getset,
-	.tp_repr = (reprfunc)gpiod_LineEvent_repr,
-};
-
-static int gpiod_Line_init(PyObject *Py_UNUSED(ignored0),
-			   PyObject *Py_UNUSED(ignored1),
-			   PyObject *Py_UNUSED(ignored2))
-{
-	PyErr_SetString(PyExc_NotImplementedError,
-			"Only gpiod.Chip can create new Line objects.");
-	return -1;
-}
-
-static void gpiod_Line_dealloc(gpiod_LineObject *self)
-{
-	if (self->owner)
-		Py_DECREF(self->owner);
-
-	PyObject_Del(self);
-}
-
-PyDoc_STRVAR(gpiod_Line_owner_doc,
-"owner() -> Chip object owning the line\n"
-"\n"
-"Get the GPIO chip owning this line.");
-
-static PyObject *gpiod_Line_owner(gpiod_LineObject *self,
-				  PyObject *Py_UNUSED(ignored))
-{
-	Py_INCREF(self->owner);
-	return (PyObject *)self->owner;
-}
-
-PyDoc_STRVAR(gpiod_Line_offset_doc,
-"offset() -> integer\n"
-"\n"
-"Get the offset of the GPIO line.");
-
-static PyObject *gpiod_Line_offset(gpiod_LineObject *self,
-				   PyObject *Py_UNUSED(ignored))
-{
-	if (gpiod_ChipIsClosed(self->owner))
-		return NULL;
-
-	return Py_BuildValue("I", gpiod_line_offset(self->line));
-}
-
-PyDoc_STRVAR(gpiod_Line_name_doc,
-"name() -> string\n"
-"\n"
-"Get the name of the GPIO line.");
-
-static PyObject *gpiod_Line_name(gpiod_LineObject *self,
-				 PyObject *Py_UNUSED(ignored))
-{
-	const char *name;
-
-	if (gpiod_ChipIsClosed(self->owner))
-		return NULL;
-
-	name = gpiod_line_name(self->line);
-	if (name)
-		return PyUnicode_FromFormat("%s", name);
-
-	Py_RETURN_NONE;
-}
-
-PyDoc_STRVAR(gpiod_Line_consumer_doc,
-"consumer() -> string\n"
-"\n"
-"Get the consumer string of the GPIO line.");
-
-static PyObject *gpiod_Line_consumer(gpiod_LineObject *self,
-				     PyObject *Py_UNUSED(ignored))
-{
-	const char *consumer;
-
-	if (gpiod_ChipIsClosed(self->owner))
-		return NULL;
-
-	consumer = gpiod_line_consumer(self->line);
-	if (consumer)
-		return PyUnicode_FromFormat("%s", consumer);
-
-	Py_RETURN_NONE;
-}
-
-PyDoc_STRVAR(gpiod_Line_direction_doc,
-"direction() -> integer\n"
-"\n"
-"Get the direction setting of this GPIO line.");
-
-static PyObject *gpiod_Line_direction(gpiod_LineObject *self,
-				      PyObject *Py_UNUSED(ignored))
-{
-	PyObject *ret;
-	int dir;
-
-	if (gpiod_ChipIsClosed(self->owner))
-		return NULL;
-
-	dir = gpiod_line_direction(self->line);
-
-	if (dir == GPIOD_LINE_DIRECTION_INPUT)
-		ret = Py_BuildValue("I", gpiod_DIRECTION_INPUT);
-	else
-		ret = Py_BuildValue("I", gpiod_DIRECTION_OUTPUT);
-
-	return ret;
-}
-
-PyDoc_STRVAR(gpiod_Line_is_active_low_doc,
-"is_active_low() -> boolean\n"
-"\n"
-"Check if this line's signal is inverted");
-
-static PyObject *gpiod_Line_is_active_low(gpiod_LineObject *self,
-					  PyObject *Py_UNUSED(ignored))
-{
-	if (gpiod_ChipIsClosed(self->owner))
-		return NULL;
-
-	if (gpiod_line_is_active_low(self->line))
-		Py_RETURN_TRUE;
-	Py_RETURN_FALSE;
-}
-
-PyDoc_STRVAR(gpiod_Line_bias_doc,
-"bias() -> integer\n"
-"\n"
-"Get the bias setting of this GPIO line.");
-
-static PyObject *gpiod_Line_bias(gpiod_LineObject *self,
-				 PyObject *Py_UNUSED(ignored))
-{
-	int bias;
-
-	if (gpiod_ChipIsClosed(self->owner))
-		return NULL;
-
-	bias = gpiod_line_bias(self->line);
-
-	switch (bias) {
-	case GPIOD_LINE_BIAS_PULL_UP:
-		return Py_BuildValue("I", gpiod_BIAS_PULL_UP);
-	case GPIOD_LINE_BIAS_PULL_DOWN:
-		return Py_BuildValue("I", gpiod_BIAS_PULL_DOWN);
-	case GPIOD_LINE_BIAS_DISABLED:
-		return Py_BuildValue("I", gpiod_BIAS_DISABLED);
-	case GPIOD_LINE_BIAS_UNKNOWN:
-	default:
-		return Py_BuildValue("I", gpiod_BIAS_UNKNOWN);
-	}
-}
-
-PyDoc_STRVAR(gpiod_Line_is_used_doc,
-"is_used() -> boolean\n"
-"\n"
-"Check if this line is used by the kernel or other user space process.");
-
-static PyObject *gpiod_Line_is_used(gpiod_LineObject *self,
-				    PyObject *Py_UNUSED(ignored))
-{
-	if (gpiod_ChipIsClosed(self->owner))
-		return NULL;
-
-	if (gpiod_line_is_used(self->line))
-		Py_RETURN_TRUE;
-
-	Py_RETURN_FALSE;
-}
-
-PyDoc_STRVAR(gpiod_Line_drive_doc,
-"drive() -> integer\n"
-"\n"
-"Get the current drive setting of this GPIO line.");
-
-static PyObject *gpiod_Line_drive(gpiod_LineObject *self,
-				  PyObject *Py_UNUSED(ignored))
-{
-	int drive;
-
-	if (gpiod_ChipIsClosed(self->owner))
-		return NULL;
-
-	drive = gpiod_line_drive(self->line);
-
-	switch (drive) {
-	case GPIOD_LINE_DRIVE_OPEN_DRAIN:
-		return Py_BuildValue("I", gpiod_DRIVE_OPEN_DRAIN);
-	case GPIOD_LINE_DRIVE_OPEN_SOURCE:
-		return Py_BuildValue("I", gpiod_DRIVE_OPEN_SOURCE);
-	case GPIOD_LINE_DRIVE_PUSH_PULL:
-	default:
-		return Py_BuildValue("I", gpiod_DRIVE_PUSH_PULL);
-	}
-}
-
-PyDoc_STRVAR(gpiod_Line_request_doc,
-"request(consumer[, type[, flags[, default_val]]]) -> None\n"
-"\n"
-"Request this GPIO line.\n"
-"\n"
-"  consumer\n"
-"    Name of the consumer.\n"
-"  type\n"
-"    Type of the request.\n"
-"  flags\n"
-"    Other configuration flags.\n"
-"  default_val\n"
-"    Default value of this line."
-"\n"
-"Note: default_vals argument (sequence of default values passed down to\n"
-"LineBulk.request()) is still supported for backward compatibility but is\n"
-"now deprecated when requesting single lines.");
-
-static PyObject *gpiod_Line_request(gpiod_LineObject *self,
-				    PyObject *args, PyObject *kwds)
-{
-	PyObject *ret, *def_val, *def_vals;
-	gpiod_LineBulkObject *bulk_obj;
-	int rv;
-
-	if (kwds && PyDict_Size(kwds) > 0) {
-		def_val = PyDict_GetItemString(kwds, "default_val");
-		def_vals = PyDict_GetItemString(kwds, "default_vals");
-	} else {
-		def_val = def_vals = NULL;
-	}
-
-	if (def_val && def_vals) {
-		PyErr_SetString(PyExc_TypeError,
-				"Cannot pass both default_val and default_vals arguments at the same time");
-		return NULL;
-	}
-
-	if (def_val) {
-		/*
-		 * If default_val was passed as a single value, we wrap it
-		 * in a tuple and add it to the kwds dictionary to be passed
-		 * down to LineBulk.request(). We also remove the 'default_val'
-		 * entry from kwds.
-		 *
-		 * I'm not sure if it's allowed to modify the kwds dictionary
-		 * but it doesn't seem to cause any problems. If it does then
-		 * we can simply copy the dictionary before calling
-		 * LineBulk.request().
-		 */
-		rv = PyDict_DelItemString(kwds, "default_val");
-		if (rv)
-			return NULL;
-
-		def_vals = Py_BuildValue("(O)", def_val);
-		if (!def_vals)
-			return NULL;
-
-		rv = PyDict_SetItemString(kwds, "default_vals", def_vals);
-		if (rv) {
-			Py_DECREF(def_vals);
-			return NULL;
-		}
-	}
-
-	bulk_obj = gpiod_LineToLineBulk(self);
-	if (!bulk_obj)
-		return NULL;
-
-	ret = gpiod_CallMethodPyArgs((PyObject *)bulk_obj,
-				     "request", args, kwds);
-	Py_DECREF(bulk_obj);
-
-	return ret;
-}
-
-PyDoc_STRVAR(gpiod_Line_get_value_doc,
-"get_value() -> integer\n"
-"\n"
-"Read the current value of this GPIO line.");
-
-static PyObject *gpiod_Line_get_value(gpiod_LineObject *self,
-				      PyObject *Py_UNUSED(ignored))
-{
-	gpiod_LineBulkObject *bulk_obj;
-	PyObject *vals, *ret;
-
-	bulk_obj = gpiod_LineToLineBulk(self);
-	if (!bulk_obj)
-		return NULL;
-
-	vals = PyObject_CallMethod((PyObject *)bulk_obj, "get_values", "");
-	Py_DECREF(bulk_obj);
-	if (!vals)
-		return NULL;
-
-	ret = PyList_GetItem(vals, 0);
-	Py_INCREF(ret);
-	Py_DECREF(vals);
-
-	return ret;
-}
-
-PyDoc_STRVAR(gpiod_Line_set_value_doc,
-"set_value(value) -> None\n"
-"\n"
-"Set the value of this GPIO line.\n"
-"\n"
-"  value\n"
-"    New value (integer)");
-
-static PyObject *gpiod_Line_set_value(gpiod_LineObject *self, PyObject *args)
-{
-	gpiod_LineBulkObject *bulk_obj;
-	PyObject *val, *vals, *ret;
-	int rv;
-
-	rv = PyArg_ParseTuple(args, "O", &val);
-	if (!rv)
-		return NULL;
-
-	bulk_obj = gpiod_LineToLineBulk(self);
-	if (!bulk_obj)
-		return NULL;
-
-	vals = Py_BuildValue("(O)", val);
-	if (!vals) {
-		Py_DECREF(bulk_obj);
-		return NULL;
-	}
-
-	ret = PyObject_CallMethod((PyObject *)bulk_obj,
-				  "set_values", "(O)", vals);
-	Py_DECREF(bulk_obj);
-	Py_DECREF(vals);
-
-	return ret;
-}
-
-PyDoc_STRVAR(gpiod_Line_set_config_doc,
-"set_config(direction,flags,value) -> None\n"
-"\n"
-"Set the configuration of this GPIO line.\n"
-"\n"
-"  direction\n"
-"    New direction (integer)\n"
-"  flags\n"
-"    New flags (integer)\n"
-"  value\n"
-"    New value (integer)");
-
-static PyObject *gpiod_Line_set_config(gpiod_LineObject *self, PyObject *args)
-{
-	gpiod_LineBulkObject *bulk_obj;
-	PyObject *dirn, *flags, *val, *vals, *ret;
-	int rv;
-
-	val = NULL;
-	rv = PyArg_ParseTuple(args, "OO|O", &dirn, &flags, &val);
-	if (!rv)
-		return NULL;
-
-	bulk_obj = gpiod_LineToLineBulk(self);
-	if (!bulk_obj)
-		return NULL;
-
-	if (val) {
-		vals = Py_BuildValue("(O)", val);
-		if (!vals) {
-			Py_DECREF(bulk_obj);
-			return NULL;
-		}
-		ret = PyObject_CallMethod((PyObject *)bulk_obj,
-				"set_config", "OO(O)", dirn, flags, vals);
-		Py_DECREF(vals);
-	} else {
-		ret = PyObject_CallMethod((PyObject *)bulk_obj,
-				"set_config", "OO", dirn, flags);
-	}
-
-	Py_DECREF(bulk_obj);
-
-	return ret;
-}
-
-PyDoc_STRVAR(gpiod_Line_set_flags_doc,
-"set_flags(flags) -> None\n"
-"\n"
-"Set the flags of this GPIO line.\n"
-"\n"
-"  flags\n"
-"    New flags (integer)");
-
-static PyObject *gpiod_Line_set_flags(gpiod_LineObject *self, PyObject *args)
-{
-	gpiod_LineBulkObject *bulk_obj;
-	PyObject *ret;
-
-	bulk_obj = gpiod_LineToLineBulk(self);
-	if (!bulk_obj)
-		return NULL;
-
-	ret = PyObject_CallMethod((PyObject *)bulk_obj,
-				  "set_flags", "O", args);
-	Py_DECREF(bulk_obj);
-
-	return ret;
-}
-
-PyDoc_STRVAR(gpiod_Line_set_direction_input_doc,
-"set_direction_input() -> None\n"
-"\n"
-"Set the direction of this GPIO line to input.\n");
-
-static PyObject *gpiod_Line_set_direction_input(gpiod_LineObject *self,
-						PyObject *Py_UNUSED(ignored))
-{
-	gpiod_LineBulkObject *bulk_obj;
-	PyObject *ret;
-
-	bulk_obj = gpiod_LineToLineBulk(self);
-	if (!bulk_obj)
-		return NULL;
-
-	ret = PyObject_CallMethod((PyObject *)bulk_obj,
-				  "set_direction_input", "");
-	Py_DECREF(bulk_obj);
-
-	return ret;
-}
-
-PyDoc_STRVAR(gpiod_Line_set_direction_output_doc,
-"set_direction_output(value) -> None\n"
-"\n"
-"Set the direction of this GPIO line to output.\n"
-"\n"
-"  value\n"
-"    New value (integer)");
-
-static PyObject *gpiod_Line_set_direction_output(gpiod_LineObject *self,
-						 PyObject *args)
-{
-	gpiod_LineBulkObject *bulk_obj;
-	PyObject *val, *vals, *ret;
-	int rv;
-	const char *fmt;
-
-	val = NULL;
-	rv = PyArg_ParseTuple(args, "|O", &val);
-	if (!rv)
-		return NULL;
-
-	if (val) {
-		fmt = "(O)";
-		vals = Py_BuildValue(fmt, val);
-	} else {
-		vals = Py_BuildValue("()");
-		fmt = "O"; /* pass empty args to bulk */
-	}
-	if (!vals)
-		return NULL;
-
-	bulk_obj = gpiod_LineToLineBulk(self);
-	if (!bulk_obj)
-		return NULL;
-
-	ret = PyObject_CallMethod((PyObject *)bulk_obj,
-				  "set_direction_output", fmt, vals);
-
-	Py_DECREF(bulk_obj);
-	Py_DECREF(vals);
-
-	return ret;
-}
-
-PyDoc_STRVAR(gpiod_Line_release_doc,
-"release() -> None\n"
-"\n"
-"Release this GPIO line.");
-
-static PyObject *gpiod_Line_release(gpiod_LineObject *self,
-				    PyObject *Py_UNUSED(ignored))
-{
-	gpiod_LineBulkObject *bulk_obj;
-	PyObject *ret;
-
-	bulk_obj = gpiod_LineToLineBulk(self);
-	if (!bulk_obj)
-		return NULL;
-
-	ret = PyObject_CallMethod((PyObject *)bulk_obj, "release", "");
-	Py_DECREF(bulk_obj);
-
-	return ret;
-}
-
-PyDoc_STRVAR(gpiod_Line_event_wait_doc,
-"event_wait([sec[ ,nsec]]) -> boolean\n"
-"\n"
-"Wait for a line event to occur on this GPIO line.\n"
-"\n"
-"  sec\n"
-"    Number of seconds to wait before timeout.\n"
-"  nsec\n"
-"    Number of nanoseconds to wait before timeout.\n"
-"\n"
-"Returns True if an event occurred on this line before timeout. False\n"
-"otherwise.");
-
-static PyObject *gpiod_Line_event_wait(gpiod_LineObject *self,
-				       PyObject *args, PyObject *kwds)
-{
-	gpiod_LineBulkObject *bulk_obj;
-	PyObject *events;
-
-	bulk_obj = gpiod_LineToLineBulk(self);
-	if (!bulk_obj)
-		return NULL;
-
-	events = gpiod_CallMethodPyArgs((PyObject *)bulk_obj,
-					"event_wait", args, kwds);
-	Py_DECREF(bulk_obj);
-	if (!events)
-		return NULL;
-
-	if (events == Py_None) {
-		Py_DECREF(Py_None);
-		Py_RETURN_FALSE;
-	}
-
-	Py_DECREF(events);
-	Py_RETURN_TRUE;
-}
-
-PyDoc_STRVAR(gpiod_Line_event_read_doc,
-"event_read() -> gpiod.LineEvent object\n"
-"\n"
-"Read a single line event from this GPIO line object.");
-
-static gpiod_LineEventObject *gpiod_Line_event_read(gpiod_LineObject *self,
-						    PyObject *Py_UNUSED(ignored))
-{
-	gpiod_LineEventObject *ret;
-	int rv;
-
-	if (gpiod_ChipIsClosed(self->owner))
-		return NULL;
-
-	ret = PyObject_New(gpiod_LineEventObject, &gpiod_LineEventType);
-	if (!ret)
-		return NULL;
-
-	ret->source = NULL;
-
-	Py_BEGIN_ALLOW_THREADS;
-	rv = gpiod_line_event_read(self->line, &ret->event);
-	Py_END_ALLOW_THREADS;
-	if (rv) {
-		Py_DECREF(ret);
-		return (gpiod_LineEventObject *)PyErr_SetFromErrno(
-							PyExc_OSError);
-	}
-
-	Py_INCREF(self);
-	ret->source = self;
-
-	return ret;
-}
-
-PyDoc_STRVAR(gpiod_Line_event_read_multiple_doc,
-"event_read_multiple() -> list of gpiod.LineEvent object\n"
-"\n"
-"Read multiple line events from this GPIO line object.");
-
-static PyObject *gpiod_Line_event_read_multiple(gpiod_LineObject *self,
-						PyObject *Py_UNUSED(ignored))
-{
-	struct gpiod_line_event evbuf[16];
-	gpiod_LineEventObject *event;
-	int rv, num_events, i;
-	PyObject *events;
-
-	if (gpiod_ChipIsClosed(self->owner))
-		return NULL;
-
-	memset(evbuf, 0, sizeof(evbuf));
-	Py_BEGIN_ALLOW_THREADS;
-	num_events = gpiod_line_event_read_multiple(self->line, evbuf,
-					sizeof(evbuf) / sizeof(*evbuf));
-	Py_END_ALLOW_THREADS;
-	if (num_events < 0)
-		return PyErr_SetFromErrno(PyExc_OSError);
-
-	events = PyList_New(num_events);
-	if (!events)
-		return NULL;
-
-	for (i = 0; i < num_events; i++) {
-		event = PyObject_New(gpiod_LineEventObject,
-				     &gpiod_LineEventType);
-		if (!event) {
-			Py_DECREF(events);
-			return NULL;
-		}
-
-		memcpy(&event->event, &evbuf[i], sizeof(event->event));
-		Py_INCREF(self);
-		event->source = self;
-
-		rv = PyList_SetItem(events, i, (PyObject *)event);
-		if (rv < 0) {
-			Py_DECREF(events);
-			Py_DECREF(event);
-			return NULL;
-		}
-	}
-
-	return events;
-}
-
-PyDoc_STRVAR(gpiod_Line_event_get_fd_doc,
-"event_get_fd() -> integer\n"
-"\n"
-"Get the event file descriptor number associated with this line.");
-
-static PyObject *gpiod_Line_event_get_fd(gpiod_LineObject *self,
-					 PyObject *Py_UNUSED(ignored))
-{
-	int fd;
-
-	if (gpiod_ChipIsClosed(self->owner))
-		return NULL;
-
-	fd = gpiod_line_event_get_fd(self->line);
-	if (fd < 0) {
-		PyErr_SetFromErrno(PyExc_OSError);
-		return NULL;
-	}
-
-	return PyLong_FromLong(fd);
-}
-
-static PyObject *gpiod_Line_repr(gpiod_LineObject *self)
-{
-	PyObject *chip_name, *ret;
-	const char *line_name;
-
-	if (gpiod_ChipIsClosed(self->owner))
-		return NULL;
-
-	chip_name = PyObject_CallMethod((PyObject *)self->owner, "name", "");
-	if (!chip_name)
-		return NULL;
-
-	line_name = gpiod_line_name(self->line);
-
-	ret = PyUnicode_FromFormat("'%S:%u /%s/'", chip_name,
-				   gpiod_line_offset(self->line),
-				   line_name ?: "unnamed");
-	Py_DECREF(chip_name);
-	return ret;
-}
-
-static PyMethodDef gpiod_Line_methods[] = {
-	{
-		.ml_name = "owner",
-		.ml_meth = (PyCFunction)gpiod_Line_owner,
-		.ml_flags = METH_NOARGS,
-		.ml_doc = gpiod_Line_owner_doc,
-	},
-	{
-		.ml_name = "offset",
-		.ml_meth = (PyCFunction)gpiod_Line_offset,
-		.ml_flags = METH_NOARGS,
-		.ml_doc = gpiod_Line_offset_doc,
-	},
-	{
-		.ml_name = "name",
-		.ml_meth = (PyCFunction)gpiod_Line_name,
-		.ml_flags = METH_NOARGS,
-		.ml_doc = gpiod_Line_name_doc,
-	},
-	{
-		.ml_name = "consumer",
-		.ml_meth = (PyCFunction)gpiod_Line_consumer,
-		.ml_flags = METH_NOARGS,
-		.ml_doc = gpiod_Line_consumer_doc,
-	},
-	{
-		.ml_name = "direction",
-		.ml_meth = (PyCFunction)gpiod_Line_direction,
-		.ml_flags = METH_NOARGS,
-		.ml_doc = gpiod_Line_direction_doc,
-	},
-	{
-		.ml_name = "is_active_low",
-		.ml_meth = (PyCFunction)gpiod_Line_is_active_low,
-		.ml_flags = METH_NOARGS,
-		.ml_doc = gpiod_Line_is_active_low_doc,
-	},
-	{
-		.ml_name = "bias",
-		.ml_meth = (PyCFunction)gpiod_Line_bias,
-		.ml_flags = METH_NOARGS,
-		.ml_doc = gpiod_Line_bias_doc,
-	},
-	{
-		.ml_name = "is_used",
-		.ml_meth = (PyCFunction)gpiod_Line_is_used,
-		.ml_flags = METH_NOARGS,
-		.ml_doc = gpiod_Line_is_used_doc,
-	},
-	{
-		.ml_name = "drive",
-		.ml_meth = (PyCFunction)gpiod_Line_drive,
-		.ml_flags = METH_NOARGS,
-		.ml_doc = gpiod_Line_drive_doc,
-	},
-	{
-		.ml_name = "request",
-		.ml_meth = (PyCFunction)(void (*)(void))gpiod_Line_request,
-		.ml_flags = METH_VARARGS | METH_KEYWORDS,
-		.ml_doc = gpiod_Line_request_doc,
-	},
-	{
-		.ml_name = "get_value",
-		.ml_meth = (PyCFunction)gpiod_Line_get_value,
-		.ml_flags = METH_NOARGS,
-		.ml_doc = gpiod_Line_get_value_doc,
-	},
-	{
-		.ml_name = "set_value",
-		.ml_meth = (PyCFunction)gpiod_Line_set_value,
-		.ml_flags = METH_VARARGS,
-		.ml_doc = gpiod_Line_set_value_doc,
-	},
-	{
-		.ml_name = "set_config",
-		.ml_meth = (PyCFunction)gpiod_Line_set_config,
-		.ml_flags = METH_VARARGS,
-		.ml_doc = gpiod_Line_set_config_doc,
-	},
-	{
-		.ml_name = "set_flags",
-		.ml_meth = (PyCFunction)gpiod_Line_set_flags,
-		.ml_flags = METH_VARARGS,
-		.ml_doc = gpiod_Line_set_flags_doc,
-	},
-	{
-		.ml_name = "set_direction_input",
-		.ml_meth = (PyCFunction)gpiod_Line_set_direction_input,
-		.ml_flags = METH_NOARGS,
-		.ml_doc = gpiod_Line_set_direction_input_doc,
-	},
-	{
-		.ml_name = "set_direction_output",
-		.ml_meth = (PyCFunction)gpiod_Line_set_direction_output,
-		.ml_flags = METH_VARARGS,
-		.ml_doc = gpiod_Line_set_direction_output_doc,
-	},
-	{
-		.ml_name = "release",
-		.ml_meth = (PyCFunction)gpiod_Line_release,
-		.ml_flags = METH_NOARGS,
-		.ml_doc = gpiod_Line_release_doc,
-	},
-	{
-		.ml_name = "event_wait",
-		.ml_meth = (PyCFunction)(void (*)(void))gpiod_Line_event_wait,
-		.ml_flags = METH_VARARGS | METH_KEYWORDS,
-		.ml_doc = gpiod_Line_event_wait_doc,
-	},
-	{
-		.ml_name = "event_read",
-		.ml_meth = (PyCFunction)gpiod_Line_event_read,
-		.ml_flags = METH_NOARGS,
-		.ml_doc = gpiod_Line_event_read_doc,
-	},
-	{
-		.ml_name = "event_read_multiple",
-		.ml_meth = (PyCFunction)gpiod_Line_event_read_multiple,
-		.ml_flags = METH_NOARGS,
-		.ml_doc = gpiod_Line_event_read_multiple_doc,
-	},
-	{
-		.ml_name = "event_get_fd",
-		.ml_meth = (PyCFunction)gpiod_Line_event_get_fd,
-		.ml_flags = METH_NOARGS,
-		.ml_doc = gpiod_Line_event_get_fd_doc,
-	},
-	{ }
-};
-
-PyDoc_STRVAR(gpiod_LineType_doc,
-"Represents a GPIO line.\n"
-"\n"
-"The lifetime of this object is managed by the chip that owns it. Once\n"
-"the corresponding gpiod.Chip is closed, a gpiod.Line object must not be\n"
-"used.\n"
-"\n"
-"Line objects can only be created by the owning chip.");
-
-static PyTypeObject gpiod_LineType = {
-	PyVarObject_HEAD_INIT(NULL, 0)
-	.tp_name = "gpiod.Line",
-	.tp_basicsize = sizeof(gpiod_LineObject),
-	.tp_flags = Py_TPFLAGS_DEFAULT,
-	.tp_doc = gpiod_LineType_doc,
-	.tp_new = PyType_GenericNew,
-	.tp_init = (initproc)gpiod_Line_init,
-	.tp_dealloc = (destructor)gpiod_Line_dealloc,
-	.tp_repr = (reprfunc)gpiod_Line_repr,
-	.tp_methods = gpiod_Line_methods,
-};
-
-static bool gpiod_LineBulkOwnerIsClosed(gpiod_LineBulkObject *self)
-{
-	gpiod_LineObject *line = (gpiod_LineObject *)self->lines[0];
-
-	return gpiod_ChipIsClosed(line->owner);
-}
-
-static int gpiod_LineBulk_init(gpiod_LineBulkObject *self,
-			       PyObject *args, PyObject *Py_UNUSED(ignored))
-{
-	PyObject *lines, *iter, *next;
-	Py_ssize_t i;
-	int rv;
-
-	rv = PyArg_ParseTuple(args, "O", &lines);
-	if (!rv)
-		return -1;
-
-	self->num_lines = PyObject_Size(lines);
-	if (self->num_lines < 1) {
-		PyErr_SetString(PyExc_TypeError,
-				"Argument must be a non-empty sequence");
-		return -1;
-	}
-	if (self->num_lines > LINE_REQUEST_MAX_LINES) {
-		PyErr_SetString(PyExc_TypeError,
-				"Too many objects in the sequence");
-		return -1;
-	}
-
-	self->lines = PyMem_Calloc(self->num_lines, sizeof(PyObject *));
-	if (!self->lines) {
-		PyErr_SetString(PyExc_MemoryError, "Out of memory");
-		return -1;
-	}
-
-	iter = PyObject_GetIter(lines);
-	if (!iter) {
-		PyMem_Free(self->lines);
-		return -1;
-	}
-
-	for (i = 0;;) {
-		next = PyIter_Next(iter);
-		if (!next) {
-			Py_DECREF(iter);
-			break;
-		}
-
-		if (next->ob_type != &gpiod_LineType) {
-			PyErr_SetString(PyExc_TypeError,
-					"Argument must be a sequence of GPIO lines");
-			Py_DECREF(next);
-			Py_DECREF(iter);
-			goto errout;
-		}
-
-		self->lines[i++] = next;
-	}
-
-	self->iter_idx = -1;
-
-	return 0;
-
-errout:
-
-	if (i > 0) {
-		for (--i; i >= 0; i--)
-			Py_DECREF(self->lines[i]);
-	}
-	PyMem_Free(self->lines);
-	self->lines = NULL;
-
-	return -1;
-}
-
-static void gpiod_LineBulk_dealloc(gpiod_LineBulkObject *self)
-{
-	Py_ssize_t i;
-
-	if (!self->lines)
-		return;
-
-	for (i = 0; i < self->num_lines; i++)
-		Py_DECREF(self->lines[i]);
-
-	PyMem_Free(self->lines);
-	PyObject_Del(self);
-}
-
-static PyObject *gpiod_LineBulk_iternext(gpiod_LineBulkObject *self)
-{
-	if (self->iter_idx < 0) {
-		self->iter_idx = 0; /* First element */
-	} else if (self->iter_idx >= self->num_lines) {
-		self->iter_idx = -1;
-		return NULL; /* Last element */
-	}
-
-	Py_INCREF(self->lines[self->iter_idx]);
-	return self->lines[self->iter_idx++];
-}
-
-PyDoc_STRVAR(gpiod_LineBulk_to_list_doc,
-"to_list() -> list of gpiod.Line objects\n"
-"\n"
-"Convert this LineBulk to a list");
-
-static PyObject *gpiod_LineBulk_to_list(gpiod_LineBulkObject *self,
-					PyObject *Py_UNUSED(ignored))
-{
-	PyObject *list;
-	Py_ssize_t i;
-	int rv;
-
-	list = PyList_New(self->num_lines);
-	if (!list)
-		return NULL;
-
-	for (i = 0; i < self->num_lines; i++) {
-		Py_INCREF(self->lines[i]);
-		rv = PyList_SetItem(list, i, self->lines[i]);
-		if (rv < 0) {
-			Py_DECREF(list);
-			return NULL;
-		}
-	}
-
-	return list;
-}
-
-static struct gpiod_line_bulk *
-gpiod_LineBulkObjToCLineBulk(gpiod_LineBulkObject *bulk_obj)
-{
-	struct gpiod_line_bulk *bulk;
-	gpiod_LineObject *line_obj;
-	Py_ssize_t i;
-
-	bulk = gpiod_line_bulk_new(bulk_obj->num_lines);
-	if (!bulk) {
-		PyErr_SetFromErrno(PyExc_OSError);
-		return NULL;
-	}
-
-	for (i = 0; i < bulk_obj->num_lines; i++) {
-		line_obj = (gpiod_LineObject *)bulk_obj->lines[i];
-		gpiod_line_bulk_add_line(bulk, line_obj->line);
-	}
-
-	return bulk;
-}
-
-static void gpiod_MakeRequestConfig(struct gpiod_line_request_config *conf,
-				    const char *consumer,
-				    int request_type, int flags)
-{
-	memset(conf, 0, sizeof(*conf));
-
-	conf->consumer = consumer;
-
-	switch (request_type) {
-	case gpiod_LINE_REQ_DIR_IN:
-		conf->request_type = GPIOD_LINE_REQUEST_DIRECTION_INPUT;
-		break;
-	case gpiod_LINE_REQ_DIR_OUT:
-		conf->request_type = GPIOD_LINE_REQUEST_DIRECTION_OUTPUT;
-		break;
-	case gpiod_LINE_REQ_EV_FALLING_EDGE:
-		conf->request_type = GPIOD_LINE_REQUEST_EVENT_FALLING_EDGE;
-		break;
-	case gpiod_LINE_REQ_EV_RISING_EDGE:
-		conf->request_type = GPIOD_LINE_REQUEST_EVENT_RISING_EDGE;
-		break;
-	case gpiod_LINE_REQ_EV_BOTH_EDGES:
-		conf->request_type = GPIOD_LINE_REQUEST_EVENT_BOTH_EDGES;
-		break;
-	case gpiod_LINE_REQ_DIR_AS_IS:
-	default:
-		conf->request_type = GPIOD_LINE_REQUEST_DIRECTION_AS_IS;
-		break;
-	}
-
-	if (flags & gpiod_LINE_REQ_FLAG_OPEN_DRAIN)
-		conf->flags |= GPIOD_LINE_REQUEST_FLAG_OPEN_DRAIN;
-	if (flags & gpiod_LINE_REQ_FLAG_OPEN_SOURCE)
-		conf->flags |= GPIOD_LINE_REQUEST_FLAG_OPEN_SOURCE;
-	if (flags & gpiod_LINE_REQ_FLAG_ACTIVE_LOW)
-		conf->flags |= GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW;
-	if (flags & gpiod_LINE_REQ_FLAG_BIAS_DISABLED)
-		conf->flags |= GPIOD_LINE_REQUEST_FLAG_BIAS_DISABLED;
-	if (flags & gpiod_LINE_REQ_FLAG_BIAS_PULL_DOWN)
-		conf->flags |= GPIOD_LINE_REQUEST_FLAG_BIAS_PULL_DOWN;
-	if (flags & gpiod_LINE_REQ_FLAG_BIAS_PULL_UP)
-		conf->flags |= GPIOD_LINE_REQUEST_FLAG_BIAS_PULL_UP;
-}
-
-PyDoc_STRVAR(gpiod_LineBulk_request_doc,
-"request(consumer[, type[, flags[, default_vals]]]) -> None\n"
-"\n"
-"Request all lines held by this LineBulk object.\n"
-"\n"
-"  consumer\n"
-"    Name of the consumer.\n"
-"  type\n"
-"    Type of the request.\n"
-"  flags\n"
-"    Other configuration flags.\n"
-"  default_vals\n"
-"    List of default values.\n");
-
-static PyObject *gpiod_LineBulk_request(gpiod_LineBulkObject *self,
-					PyObject *args, PyObject *kwds)
-{
-	static char *kwlist[] = { "consumer",
-				  "type",
-				  "flags",
-				  "default_vals",
-				  NULL };
-
-	int rv, type = gpiod_LINE_REQ_DIR_AS_IS, flags = 0,
-	    vals[LINE_REQUEST_MAX_LINES], val;
-	PyObject *def_vals_obj = NULL, *iter, *next;
-	struct gpiod_line_request_config conf;
-	const int *default_vals = NULL;
-	struct gpiod_line_bulk *bulk;
-	Py_ssize_t num_def_vals;
-	char *consumer = NULL;
-	Py_ssize_t i;
-
-	if (gpiod_LineBulkOwnerIsClosed(self))
-		return NULL;
-
-	rv = PyArg_ParseTupleAndKeywords(args, kwds, "s|iiO", kwlist,
-					 &consumer, &type,
-					 &flags, &def_vals_obj);
-	if (!rv)
-		return NULL;
-
-	bulk = gpiod_LineBulkObjToCLineBulk(self);
-	if (!bulk)
-		return NULL;
-
-	gpiod_MakeRequestConfig(&conf, consumer, type, flags);
-
-	if (def_vals_obj) {
-		memset(vals, 0, sizeof(vals));
-
-		num_def_vals = PyObject_Size(def_vals_obj);
-		if (num_def_vals != self->num_lines) {
-			PyErr_SetString(PyExc_TypeError,
-					"Number of default values is not the same as the number of lines");
-			return NULL;
-		}
-
-		iter = PyObject_GetIter(def_vals_obj);
-		if (!iter)
-			return NULL;
-
-		for (i = 0;; i++) {
-			next = PyIter_Next(iter);
-			if (!next) {
-				Py_DECREF(iter);
-				break;
-			}
-
-			val = PyLong_AsUnsignedLong(next);
-			Py_DECREF(next);
-			if (PyErr_Occurred()) {
-				Py_DECREF(iter);
-				return NULL;
-			}
-
-			vals[i] = !!val;
-		}
-		default_vals = vals;
-	}
-
-	bulk = gpiod_LineBulkObjToCLineBulk(self);
-	if (!bulk)
-		return NULL;
-
-	Py_BEGIN_ALLOW_THREADS;
-	rv = gpiod_line_request_bulk(bulk, &conf, default_vals);
-	gpiod_line_bulk_free(bulk);
-	Py_END_ALLOW_THREADS;
-	if (rv)
-		return PyErr_SetFromErrno(PyExc_OSError);
-
-	Py_RETURN_NONE;
-}
-
-PyDoc_STRVAR(gpiod_LineBulk_get_values_doc,
-"get_values() -> list of integers\n"
-"\n"
-"Read the values of all the lines held by this LineBulk object. The index\n"
-"of each value in the returned list corresponds to the index of the line\n"
-"in this gpiod.LineBulk object.");
-
-static PyObject *gpiod_LineBulk_get_values(gpiod_LineBulkObject *self,
-					   PyObject *Py_UNUSED(ignored))
-{
-	int rv, vals[LINE_REQUEST_MAX_LINES];
-	struct gpiod_line_bulk *bulk;
-	PyObject *val_list, *val;
-	Py_ssize_t i;
-
-	if (gpiod_LineBulkOwnerIsClosed(self))
-		return NULL;
-
-	bulk = gpiod_LineBulkObjToCLineBulk(self);
-	if (!bulk)
-		return NULL;
-
-	memset(vals, 0, sizeof(vals));
-	Py_BEGIN_ALLOW_THREADS;
-	rv = gpiod_line_get_value_bulk(bulk, vals);
-	gpiod_line_bulk_free(bulk);
-	Py_END_ALLOW_THREADS;
-	if (rv)
-		return PyErr_SetFromErrno(PyExc_OSError);
-
-	val_list = PyList_New(self->num_lines);
-	if (!val_list)
-		return NULL;
-
-	for (i = 0; i < self->num_lines; i++) {
-		val = Py_BuildValue("i", vals[i]);
-		if (!val) {
-			Py_DECREF(val_list);
-			return NULL;
-		}
-
-		rv = PyList_SetItem(val_list, i, val);
-		if (rv < 0) {
-			Py_DECREF(val);
-			Py_DECREF(val_list);
-			return NULL;
-		}
-	}
-
-	return val_list;
-}
-
-static int gpiod_TupleToIntArray(PyObject *src, int *dst, Py_ssize_t nv)
-{
-	Py_ssize_t num_vals, i;
-	PyObject *iter, *next;
-	int val;
-
-	num_vals = PyObject_Size(src);
-	if (num_vals != nv) {
-		PyErr_SetString(PyExc_TypeError,
-				"Number of values must correspond to the number of lines");
-		return -1;
-	}
-
-	iter = PyObject_GetIter(src);
-	if (!iter)
-		return -1;
-
-	for (i = 0;; i++) {
-		next = PyIter_Next(iter);
-		if (!next) {
-			Py_DECREF(iter);
-			break;
-		}
-
-		val = PyLong_AsLong(next);
-		Py_DECREF(next);
-		if (PyErr_Occurred()) {
-			Py_DECREF(iter);
-			return -1;
-		}
-		dst[i] = (int)val;
-	}
-
-	return 0;
-}
-
-PyDoc_STRVAR(gpiod_LineBulk_set_values_doc,
-"set_values(values) -> None\n"
-"\n"
-"Set the values of all the lines held by this LineBulk object.\n"
-"\n"
-"  values\n"
-"    List of values (integers) to set.\n"
-"\n"
-"The number of values in the list passed as argument must be the same as\n"
-"the number of lines held by this gpiod.LineBulk object. The index of each\n"
-"value corresponds to the index of each line in the object.\n");
-
-static PyObject *gpiod_LineBulk_set_values(gpiod_LineBulkObject *self,
-					   PyObject *args)
-{
-	int rv, vals[LINE_REQUEST_MAX_LINES];
-	struct gpiod_line_bulk *bulk;
-	PyObject *val_list;
-
-	if (gpiod_LineBulkOwnerIsClosed(self))
-		return NULL;
-
-	memset(vals, 0, sizeof(vals));
-
-	rv = PyArg_ParseTuple(args, "O", &val_list);
-	if (!rv)
-		return NULL;
-
-	rv = gpiod_TupleToIntArray(val_list, vals, self->num_lines);
-	if (rv)
-		return NULL;
-
-	bulk = gpiod_LineBulkObjToCLineBulk(self);
-	if (!bulk)
-		return NULL;
-
-	Py_BEGIN_ALLOW_THREADS;
-	rv = gpiod_line_set_value_bulk(bulk, vals);
-	gpiod_line_bulk_free(bulk);
-	Py_END_ALLOW_THREADS;
-	if (rv)
-		return PyErr_SetFromErrno(PyExc_OSError);
-
-	Py_RETURN_NONE;
-}
-
-PyDoc_STRVAR(gpiod_LineBulk_set_config_doc,
-"set_config(direction,flags,values) -> None\n"
-"\n"
-"Set the configuration of all the lines held by this LineBulk object.\n"
-"\n"
-"  direction\n"
-"    New direction (integer)\n"
-"  flags\n"
-"    New flags (integer)\n"
-"  values\n"
-"    List of values (integers) to set when direction is output.\n"
-"\n"
-"The number of values in the list passed as argument must be the same as\n"
-"the number of lines held by this gpiod.LineBulk object. The index of each\n"
-"value corresponds to the index of each line in the object.\n");
-
-static PyObject *gpiod_LineBulk_set_config(gpiod_LineBulkObject *self,
-					   PyObject *args)
-{
-	int rv, vals[LINE_REQUEST_MAX_LINES];
-	struct gpiod_line_bulk *bulk;
-	PyObject *val_list;
-	const int *valp;
-	int dirn, flags;
-
-	if (gpiod_LineBulkOwnerIsClosed(self))
-		return NULL;
-
-	val_list = NULL;
-	rv = PyArg_ParseTuple(args, "ii|(O)", &dirn, &flags, &val_list);
-	if (!rv)
-		return NULL;
-
-	if (val_list == NULL) {
-		valp = NULL;
-	} else {
-		memset(vals, 0, sizeof(vals));
-		rv = gpiod_TupleToIntArray(val_list, vals, self->num_lines);
-		if (rv)
-			return NULL;
-		valp = vals;
-	}
-
-	bulk = gpiod_LineBulkObjToCLineBulk(self);
-	if (!bulk)
-		return NULL;
-
-	Py_BEGIN_ALLOW_THREADS;
-	rv = gpiod_line_set_config_bulk(bulk, dirn, flags, valp);
-	Py_END_ALLOW_THREADS;
-	if (rv)
-		return PyErr_SetFromErrno(PyExc_OSError);
-
-	Py_RETURN_NONE;
-}
-
-PyDoc_STRVAR(gpiod_LineBulk_set_flags_doc,
-"set_flags(flags) -> None\n"
-"\n"
-"Set the flags of all the lines held by this LineBulk object.\n"
-"\n"
-"  flags\n"
-"    New flags (integer)");
-
-static PyObject *gpiod_LineBulk_set_flags(gpiod_LineBulkObject *self,
-					  PyObject *args)
-{
-	struct gpiod_line_bulk *bulk;
-	int rv, flags;
-
-	if (gpiod_LineBulkOwnerIsClosed(self))
-		return NULL;
-
-	rv = PyArg_ParseTuple(args, "i", &flags);
-	if (!rv)
-		return NULL;
-
-	bulk = gpiod_LineBulkObjToCLineBulk(self);
-	if (!bulk)
-		return NULL;
-
-	Py_BEGIN_ALLOW_THREADS;
-	rv = gpiod_line_set_flags_bulk(bulk, flags);
-	gpiod_line_bulk_free(bulk);
-	Py_END_ALLOW_THREADS;
-	if (rv)
-		return PyErr_SetFromErrno(PyExc_OSError);
-
-	Py_RETURN_NONE;
-}
-
-PyDoc_STRVAR(gpiod_LineBulk_set_direction_input_doc,
-"set_direction_input() -> None\n"
-"\n"
-"Set the direction of all the lines held by this LineBulk object to input.\n");
-
-static PyObject *gpiod_LineBulk_set_direction_input(gpiod_LineBulkObject *self,
-						PyObject *Py_UNUSED(ignored))
-{
-	struct gpiod_line_bulk *bulk;
-	int rv;
-
-	if (gpiod_LineBulkOwnerIsClosed(self))
-		return NULL;
-
-	bulk = gpiod_LineBulkObjToCLineBulk(self);
-	if (!bulk)
-		return NULL;
-
-	Py_BEGIN_ALLOW_THREADS;
-	rv = gpiod_line_set_direction_input_bulk(bulk);
-	gpiod_line_bulk_free(bulk);
-	Py_END_ALLOW_THREADS;
-	if (rv)
-		return PyErr_SetFromErrno(PyExc_OSError);
-
-	Py_RETURN_NONE;
-}
-
-PyDoc_STRVAR(gpiod_LineBulk_set_direction_output_doc,
-"set_direction_output(value) -> None\n"
-"\n"
-"Set the direction of all the lines held by this LineBulk object to output.\n"
-"\n"
-"  values\n"
-"    List of values (integers) to set when direction is output.\n"
-"\n"
-"The number of values in the list passed as argument must be the same as\n"
-"the number of lines held by this gpiod.LineBulk object. The index of each\n"
-"value corresponds to the index of each line in the object.\n");
-
-static PyObject *gpiod_LineBulk_set_direction_output(
-				gpiod_LineBulkObject *self,
-				PyObject *args)
-{
-	int rv, vals[LINE_REQUEST_MAX_LINES];
-	struct gpiod_line_bulk *bulk;
-	PyObject *val_list;
-	const int *valp;
-
-	if (gpiod_LineBulkOwnerIsClosed(self))
-		return NULL;
-
-	val_list = NULL;
-	rv = PyArg_ParseTuple(args, "|O", &val_list);
-	if (!rv)
-		return NULL;
-
-	if (val_list == NULL)
-		valp = NULL;
-	else {
-		memset(vals, 0, sizeof(vals));
-		rv = gpiod_TupleToIntArray(val_list, vals, self->num_lines);
-		if (rv)
-			return NULL;
-		valp = vals;
-	}
-
-	bulk = gpiod_LineBulkObjToCLineBulk(self);
-	if (!bulk)
-		return NULL;
-
-	Py_BEGIN_ALLOW_THREADS;
-	rv = gpiod_line_set_direction_output_bulk(bulk, valp);
-	gpiod_line_bulk_free(bulk);
-	Py_END_ALLOW_THREADS;
-	if (rv)
-		return PyErr_SetFromErrno(PyExc_OSError);
-
-	Py_RETURN_NONE;
-}
-
-PyDoc_STRVAR(gpiod_LineBulk_release_doc,
-"release() -> None\n"
-"\n"
-"Release all lines held by this LineBulk object.");
-
-static PyObject *gpiod_LineBulk_release(gpiod_LineBulkObject *self,
-					PyObject *Py_UNUSED(ignored))
-{
-	struct gpiod_line_bulk *bulk;
-
-	if (gpiod_LineBulkOwnerIsClosed(self))
-		return NULL;
-
-	bulk = gpiod_LineBulkObjToCLineBulk(self);
-	if (!bulk)
-		return NULL;
-
-	gpiod_line_release_bulk(bulk);
-	gpiod_line_bulk_free(bulk);
-
-	Py_RETURN_NONE;
-}
-
-PyDoc_STRVAR(gpiod_LineBulk_event_wait_doc,
-"event_wait([sec[ ,nsec]]) -> gpiod.LineBulk object or None\n"
-"\n"
-"Poll the lines held by this LineBulk Object for line events.\n"
-"\n"
-"  sec\n"
-"    Number of seconds to wait before timeout.\n"
-"  nsec\n"
-"    Number of nanoseconds to wait before timeout.\n"
-"\n"
-"Returns a gpiod.LineBulk object containing references to lines on which\n"
-"events occurred or None if we reached the timeout without any event\n"
-"occurring.");
-
-static PyObject *gpiod_LineBulk_event_wait(gpiod_LineBulkObject *self,
-					   PyObject *args, PyObject *kwds)
-{
-	static char *kwlist[] = { "sec", "nsec", NULL };
-
-	struct gpiod_line_bulk *bulk, *ev_bulk;
-	gpiod_LineObject *line_obj;
-	gpiod_ChipObject *owner;
-	long sec = 0, nsec = 0;
-	struct timespec ts;
-	PyObject *ret;
-	unsigned int idx;
-	int rv;
-
-	if (gpiod_LineBulkOwnerIsClosed(self))
-		return NULL;
-
-	rv = PyArg_ParseTupleAndKeywords(args, kwds,
-					 "|ll", kwlist, &sec, &nsec);
-	if (!rv)
-		return NULL;
-
-	ts.tv_sec = sec;
-	ts.tv_nsec = nsec;
-
-	bulk = gpiod_LineBulkObjToCLineBulk(self);
-	if (!bulk)
-		return NULL;
-
-	ev_bulk = gpiod_line_bulk_new(self->num_lines);
-	if (!ev_bulk) {
-		gpiod_line_bulk_free(bulk);
-		return NULL;
-	}
-
-	Py_BEGIN_ALLOW_THREADS;
-	rv = gpiod_line_event_wait_bulk(bulk, &ts, ev_bulk);
-	gpiod_line_bulk_free(bulk);
-	Py_END_ALLOW_THREADS;
-	if (rv < 0) {
-		gpiod_line_bulk_free(ev_bulk);
-		return PyErr_SetFromErrno(PyExc_OSError);
-	} else if (rv == 0) {
-		gpiod_line_bulk_free(ev_bulk);
-		Py_RETURN_NONE;
-	}
-
-	ret = PyList_New(gpiod_line_bulk_num_lines(ev_bulk));
-	if (!ret) {
-		gpiod_line_bulk_free(ev_bulk);
-		return NULL;
-	}
-
-	owner = ((gpiod_LineObject *)(self->lines[0]))->owner;
-
-	for (idx = 0; idx < gpiod_line_bulk_num_lines(ev_bulk); idx++) {
-		line_obj = gpiod_MakeLineObject(owner, gpiod_line_bulk_get_line(ev_bulk, idx));
-		if (!line_obj) {
-			gpiod_line_bulk_free(ev_bulk);
-			Py_DECREF(ret);
-			return NULL;
-		}
-
-		rv = PyList_SetItem(ret, idx, (PyObject *)line_obj);
-		if (rv < 0) {
-			gpiod_line_bulk_free(ev_bulk);
-			Py_DECREF(ret);
-			return NULL;
-		}
-	}
-
-	gpiod_line_bulk_free(ev_bulk);
-
-	return ret;
-}
-
-static PyObject *gpiod_LineBulk_repr(gpiod_LineBulkObject *self)
-{
-	PyObject *list, *list_repr, *chip_name, *ret;
-	gpiod_LineObject *line;
-
-	if (gpiod_LineBulkOwnerIsClosed(self))
-		return NULL;
-
-	list = gpiod_LineBulk_to_list(self, NULL);
-	if (!list)
-		return NULL;
-
-	list_repr = PyObject_Repr(list);
-	Py_DECREF(list);
-	if (!list_repr)
-		return NULL;
-
-	line = (gpiod_LineObject *)self->lines[0];
-	chip_name = PyObject_CallMethod((PyObject *)line->owner, "name", "");
-	if (!chip_name) {
-		Py_DECREF(list_repr);
-		return NULL;
-	}
-
-	ret = PyUnicode_FromFormat("%U%U", chip_name, list_repr);
-	Py_DECREF(chip_name);
-	Py_DECREF(list_repr);
-	return ret;
-}
-
-static PyMethodDef gpiod_LineBulk_methods[] = {
-	{
-		.ml_name = "to_list",
-		.ml_meth = (PyCFunction)gpiod_LineBulk_to_list,
-		.ml_doc = gpiod_LineBulk_to_list_doc,
-		.ml_flags = METH_NOARGS,
-	},
-	{
-		.ml_name = "request",
-		.ml_meth = (PyCFunction)(void (*)(void))gpiod_LineBulk_request,
-		.ml_doc = gpiod_LineBulk_request_doc,
-		.ml_flags = METH_VARARGS | METH_KEYWORDS,
-	},
-	{
-		.ml_name = "get_values",
-		.ml_meth = (PyCFunction)gpiod_LineBulk_get_values,
-		.ml_doc = gpiod_LineBulk_get_values_doc,
-		.ml_flags = METH_NOARGS,
-	},
-	{
-		.ml_name = "set_values",
-		.ml_meth = (PyCFunction)gpiod_LineBulk_set_values,
-		.ml_doc = gpiod_LineBulk_set_values_doc,
-		.ml_flags = METH_VARARGS,
-	},
-	{
-		.ml_name = "set_config",
-		.ml_meth = (PyCFunction)gpiod_LineBulk_set_config,
-		.ml_flags = METH_VARARGS,
-		.ml_doc = gpiod_LineBulk_set_config_doc,
-	},
-	{
-		.ml_name = "set_flags",
-		.ml_meth = (PyCFunction)gpiod_LineBulk_set_flags,
-		.ml_flags = METH_VARARGS,
-		.ml_doc = gpiod_LineBulk_set_flags_doc,
-	},
-	{
-		.ml_name = "set_direction_input",
-		.ml_meth = (PyCFunction)gpiod_LineBulk_set_direction_input,
-		.ml_flags = METH_NOARGS,
-		.ml_doc = gpiod_LineBulk_set_direction_input_doc,
-	},
-	{
-		.ml_name = "set_direction_output",
-		.ml_meth = (PyCFunction)gpiod_LineBulk_set_direction_output,
-		.ml_flags = METH_VARARGS,
-		.ml_doc = gpiod_LineBulk_set_direction_output_doc,
-	},
-	{
-		.ml_name = "release",
-		.ml_meth = (PyCFunction)gpiod_LineBulk_release,
-		.ml_doc = gpiod_LineBulk_release_doc,
-		.ml_flags = METH_NOARGS,
-	},
-	{
-		.ml_name = "event_wait",
-		.ml_meth = (PyCFunction)(void (*)(void))gpiod_LineBulk_event_wait,
-		.ml_doc = gpiod_LineBulk_event_wait_doc,
-		.ml_flags = METH_VARARGS | METH_KEYWORDS,
-	},
-	{ }
-};
-
-PyDoc_STRVAR(gpiod_LineBulkType_doc,
-"Represents a set of GPIO lines.\n"
-"\n"
-"Objects of this type are immutable. The constructor takes as argument\n"
-"a sequence of gpiod.Line objects. It doesn't accept objects of any other\n"
-"type.");
-
-static PyTypeObject gpiod_LineBulkType = {
-	PyVarObject_HEAD_INIT(NULL, 0)
-	.tp_name = "gpiod.LineBulk",
-	.tp_basicsize = sizeof(gpiod_LineBulkObject),
-	.tp_flags = Py_TPFLAGS_DEFAULT,
-	.tp_doc = gpiod_LineBulkType_doc,
-	.tp_new = PyType_GenericNew,
-	.tp_init = (initproc)gpiod_LineBulk_init,
-	.tp_dealloc = (destructor)gpiod_LineBulk_dealloc,
-	.tp_iter = PyObject_SelfIter,
-	.tp_iternext = (iternextfunc)gpiod_LineBulk_iternext,
-	.tp_repr = (reprfunc)gpiod_LineBulk_repr,
-	.tp_methods = gpiod_LineBulk_methods,
-};
-
-static gpiod_LineBulkObject *gpiod_LineToLineBulk(gpiod_LineObject *line)
-{
-	gpiod_LineBulkObject *ret;
-	PyObject *args;
-
-	args = Py_BuildValue("((O))", line);
-	if (!args)
-		return NULL;
-
-	ret = (gpiod_LineBulkObject *)PyObject_CallObject(
-					(PyObject *)&gpiod_LineBulkType,
-					args);
-	Py_DECREF(args);
-
-	return ret;
-}
-
-static int gpiod_Chip_init(gpiod_ChipObject *self,
-			   PyObject *args, PyObject *Py_UNUSED(ignored))
-{
-	char *path;
-	int rv;
-
-	rv = PyArg_ParseTuple(args, "s", &path);
-	if (!rv)
-		return -1;
-
-	Py_BEGIN_ALLOW_THREADS;
-	self->chip = gpiod_chip_open(path);
-	Py_END_ALLOW_THREADS;
-	if (!self->chip) {
-		PyErr_SetFromErrno(PyExc_OSError);
-		return -1;
-	}
-
-	return 0;
-}
-
-static void gpiod_Chip_dealloc(gpiod_ChipObject *self)
-{
-	if (self->chip)
-		gpiod_chip_unref(self->chip);
-
-	PyObject_Del(self);
-}
-
-static PyObject *gpiod_Chip_repr(gpiod_ChipObject *self)
-{
-	if (gpiod_ChipIsClosed(self))
-		return NULL;
-
-	return PyUnicode_FromFormat("'%s /%s/ %u lines'",
-				    gpiod_chip_get_name(self->chip),
-				    gpiod_chip_get_label(self->chip),
-				    gpiod_chip_get_num_lines(self->chip));
-}
-
-PyDoc_STRVAR(gpiod_Chip_close_doc,
-"close() -> None\n"
-"\n"
-"Close the associated gpiochip descriptor. The chip object must no longer\n"
-"be used after this method is called.\n");
-
-static PyObject *gpiod_Chip_close(gpiod_ChipObject *self,
-				  PyObject *Py_UNUSED(ignored))
-{
-	if (gpiod_ChipIsClosed(self))
-		return NULL;
-
-	gpiod_chip_unref(self->chip);
-	self->chip = NULL;
-
-	Py_RETURN_NONE;
-}
-
-PyDoc_STRVAR(gpiod_Chip_enter_doc,
-"Controlled execution enter callback.");
-
-static PyObject *gpiod_Chip_enter(gpiod_ChipObject *chip,
-				  PyObject *Py_UNUSED(ignored))
-{
-	Py_INCREF(chip);
-	return (PyObject *)chip;
-}
-
-PyDoc_STRVAR(gpiod_Chip_exit_doc,
-"Controlled execution exit callback.");
-
-static PyObject *gpiod_Chip_exit(gpiod_ChipObject *chip,
-				 PyObject *Py_UNUSED(ignored))
-{
-	return PyObject_CallMethod((PyObject *)chip, "close", "");
-}
-
-PyDoc_STRVAR(gpiod_Chip_name_doc,
-"name() -> string\n"
-"\n"
-"Get the name of the GPIO chip");
-
-static PyObject *gpiod_Chip_name(gpiod_ChipObject *self,
-				 PyObject *Py_UNUSED(ignored))
-{
-	if (gpiod_ChipIsClosed(self))
-		return NULL;
-
-	return PyUnicode_FromFormat("%s", gpiod_chip_get_name(self->chip));
-}
-
-PyDoc_STRVAR(gpiod_Chip_label_doc,
-"label() -> string\n"
-"\n"
-"Get the label of the GPIO chip");
-
-static PyObject *gpiod_Chip_label(gpiod_ChipObject *self,
-				  PyObject *Py_UNUSED(ignored))
-{
-	if (gpiod_ChipIsClosed(self))
-		return NULL;
-
-	return PyUnicode_FromFormat("%s", gpiod_chip_get_label(self->chip));
-}
-
-PyDoc_STRVAR(gpiod_Chip_num_lines_doc,
-"num_lines() -> integer\n"
-"\n"
-"Get the number of lines exposed by this GPIO chip.");
-
-static PyObject *gpiod_Chip_num_lines(gpiod_ChipObject *self,
-				      PyObject *Py_UNUSED(ignored))
-{
-	if (gpiod_ChipIsClosed(self))
-		return NULL;
-
-	return Py_BuildValue("I", gpiod_chip_get_num_lines(self->chip));
-}
-
-static gpiod_LineObject *
-gpiod_MakeLineObject(gpiod_ChipObject *owner, struct gpiod_line *line)
-{
-	gpiod_LineObject *obj;
-
-	obj = PyObject_New(gpiod_LineObject, &gpiod_LineType);
-	if (!obj)
-		return NULL;
-
-	obj->line = line;
-	Py_INCREF(owner);
-	obj->owner = owner;
-
-	return obj;
-}
-
-PyDoc_STRVAR(gpiod_Chip_get_line_doc,
-"get_line(offset) -> gpiod.Line object\n"
-"\n"
-"Get the GPIO line at given offset.\n"
-"\n"
-"  offset\n"
-"    Line offset (integer)");
-
-static gpiod_LineObject *
-gpiod_Chip_get_line(gpiod_ChipObject *self, PyObject *args)
-{
-	struct gpiod_line *line;
-	unsigned int offset;
-	int rv;
-
-	if (gpiod_ChipIsClosed(self))
-		return NULL;
-
-	rv = PyArg_ParseTuple(args, "I", &offset);
-	if (!rv)
-		return NULL;
-
-	Py_BEGIN_ALLOW_THREADS;
-	line = gpiod_chip_get_line(self->chip, offset);
-	Py_END_ALLOW_THREADS;
-	if (!line)
-		return (gpiod_LineObject *)PyErr_SetFromErrno(PyExc_OSError);
-
-	return gpiod_MakeLineObject(self, line);
-}
-
-static gpiod_LineBulkObject *gpiod_ListToLineBulk(PyObject *lines)
-{
-	gpiod_LineBulkObject *bulk;
-	PyObject *arg;
-
-	arg = PyTuple_Pack(1, lines);
-	if (!arg)
-		return NULL;
-
-	bulk = (gpiod_LineBulkObject *)PyObject_CallObject(
-					(PyObject *)&gpiod_LineBulkType,
-					arg);
-	Py_DECREF(arg);
-
-	return bulk;
-}
-
-static gpiod_LineBulkObject *
-gpiod_LineBulkObjectFromBulk(gpiod_ChipObject *chip, struct gpiod_line_bulk *bulk)
-{
-	gpiod_LineBulkObject *bulk_obj;
-	gpiod_LineObject *line_obj;
-	struct gpiod_line *line;
-	unsigned int idx;
-	PyObject *list;
-	int rv;
-
-	list = PyList_New(gpiod_line_bulk_num_lines(bulk));
-	if (!list)
-		return NULL;
-
-	for (idx = 0; idx < gpiod_line_bulk_num_lines(bulk); idx++) {
-		line = gpiod_line_bulk_get_line(bulk, idx);
-		line_obj = gpiod_MakeLineObject(chip, line);
-		if (!line_obj) {
-			Py_DECREF(list);
-			return NULL;
-		}
-
-		rv = PyList_SetItem(list, idx, (PyObject *)line_obj);
-		if (rv < 0) {
-			Py_DECREF(line_obj);
-			Py_DECREF(list);
-			return NULL;
-		}
-	}
-
-	bulk_obj = gpiod_ListToLineBulk(list);
-	Py_DECREF(list);
-	if (!bulk_obj)
-		return NULL;
-
-	return bulk_obj;
-}
-
-PyDoc_STRVAR(gpiod_Chip_find_line_doc,
-"find_line(name) -> integer or None\n"
-"\n"
-"Find the offset of the line with given name among lines exposed by this\n"
-"GPIO chip.\n"
-"\n"
-"  name\n"
-"    Line name (string)\n"
-"\n"
-"Returns the offset of the line with given name or None if it is not\n"
-"associated with this chip.");
-
-static PyObject *gpiod_Chip_find_line(gpiod_ChipObject *self, PyObject *args)
-{
-	const char *name;
-	int rv, offset;
-
-	if (gpiod_ChipIsClosed(self))
-		return NULL;
-
-	rv = PyArg_ParseTuple(args, "s", &name);
-	if (!rv)
-		return NULL;
-
-	Py_BEGIN_ALLOW_THREADS;
-	offset = gpiod_chip_find_line(self->chip, name);
-	Py_END_ALLOW_THREADS;
-	if (offset < 0) {
-		if (errno == ENOENT)
-			Py_RETURN_NONE;
-
-		return PyErr_SetFromErrno(PyExc_OSError);
-	}
-
-	return Py_BuildValue("i", offset);
-}
-
-PyDoc_STRVAR(gpiod_Chip_get_lines_doc,
-"get_lines(offsets) -> gpiod.LineBulk object\n"
-"\n"
-"Get a set of GPIO lines by their offsets.\n"
-"\n"
-"  offsets\n"
-"    List of lines offsets.");
-
-static gpiod_LineBulkObject *
-gpiod_Chip_get_lines(gpiod_ChipObject *self, PyObject *args)
-{
-	PyObject *offsets, *iter, *next, *lines, *arg;
-	gpiod_LineBulkObject *bulk;
-	Py_ssize_t num_offsets, i;
-	gpiod_LineObject *line;
-	int rv;
-
-	rv = PyArg_ParseTuple(args, "O", &offsets);
-	if (!rv)
-		return NULL;
-
-	num_offsets = PyObject_Size(offsets);
-	if (num_offsets < 1) {
-		PyErr_SetString(PyExc_TypeError,
-				"Argument must be a non-empty sequence of offsets");
-		return NULL;
-	}
-
-	lines = PyList_New(num_offsets);
-	if (!lines)
-		return NULL;
-
-	iter = PyObject_GetIter(offsets);
-	if (!iter) {
-		Py_DECREF(lines);
-		return NULL;
-	}
-
-	for (i = 0;;) {
-		next = PyIter_Next(iter);
-		if (!next) {
-			Py_DECREF(iter);
-			break;
-		}
-
-		arg = PyTuple_Pack(1, next);
-		Py_DECREF(next);
-		if (!arg) {
-			Py_DECREF(iter);
-			Py_DECREF(lines);
-			return NULL;
-		}
-
-		line = gpiod_Chip_get_line(self, arg);
-		Py_DECREF(arg);
-		if (!line) {
-			Py_DECREF(iter);
-			Py_DECREF(lines);
-			return NULL;
-		}
-
-		rv = PyList_SetItem(lines, i++, (PyObject *)line);
-		if (rv < 0) {
-			Py_DECREF(line);
-			Py_DECREF(iter);
-			Py_DECREF(lines);
-			return NULL;
-		}
-	}
-
-	bulk = gpiod_ListToLineBulk(lines);
-	Py_DECREF(lines);
-	if (!bulk)
-		return NULL;
-
-	return bulk;
-}
-
-PyDoc_STRVAR(gpiod_Chip_get_all_lines_doc,
-"get_all_lines() -> gpiod.LineBulk object\n"
-"\n"
-"Get all lines exposed by this Chip.");
-
-static gpiod_LineBulkObject *
-gpiod_Chip_get_all_lines(gpiod_ChipObject *self, PyObject *Py_UNUSED(ignored))
-{
-	gpiod_LineBulkObject *bulk_obj;
-	struct gpiod_line_bulk *bulk;
-
-	if (gpiod_ChipIsClosed(self))
-		return NULL;
-
-	bulk = gpiod_chip_get_all_lines(self->chip);
-	if (!bulk)
-		return (gpiod_LineBulkObject *)PyErr_SetFromErrno(
-							PyExc_OSError);
-
-	bulk_obj = gpiod_LineBulkObjectFromBulk(self, bulk);
-	gpiod_line_bulk_free(bulk);
-	return bulk_obj;
-}
-
-static PyMethodDef gpiod_Chip_methods[] = {
-	{
-		.ml_name = "close",
-		.ml_meth = (PyCFunction)gpiod_Chip_close,
-		.ml_flags = METH_NOARGS,
-		.ml_doc = gpiod_Chip_close_doc,
-	},
-	{
-		.ml_name = "__enter__",
-		.ml_meth = (PyCFunction)gpiod_Chip_enter,
-		.ml_flags = METH_NOARGS,
-		.ml_doc = gpiod_Chip_enter_doc,
-	},
-	{
-		.ml_name = "__exit__",
-		.ml_meth = (PyCFunction)gpiod_Chip_exit,
-		.ml_flags = METH_VARARGS,
-		.ml_doc = gpiod_Chip_exit_doc,
-	},
-	{
-		.ml_name = "name",
-		.ml_meth = (PyCFunction)gpiod_Chip_name,
-		.ml_flags = METH_NOARGS,
-		.ml_doc = gpiod_Chip_name_doc,
-	},
-	{
-		.ml_name = "label",
-		.ml_meth = (PyCFunction)gpiod_Chip_label,
-		.ml_flags = METH_NOARGS,
-		.ml_doc = gpiod_Chip_label_doc,
-	},
-	{
-		.ml_name = "num_lines",
-		.ml_meth = (PyCFunction)gpiod_Chip_num_lines,
-		.ml_flags = METH_NOARGS,
-		.ml_doc = gpiod_Chip_num_lines_doc,
-	},
-	{
-		.ml_name = "get_line",
-		.ml_meth = (PyCFunction)gpiod_Chip_get_line,
-		.ml_flags = METH_VARARGS,
-		.ml_doc = gpiod_Chip_get_line_doc,
-	},
-	{
-		.ml_name = "find_line",
-		.ml_meth = (PyCFunction)gpiod_Chip_find_line,
-		.ml_flags = METH_VARARGS,
-		.ml_doc = gpiod_Chip_find_line_doc,
-	},
-	{
-		.ml_name = "get_lines",
-		.ml_meth = (PyCFunction)gpiod_Chip_get_lines,
-		.ml_flags = METH_VARARGS,
-		.ml_doc = gpiod_Chip_get_lines_doc,
-	},
-	{
-		.ml_name = "get_all_lines",
-		.ml_meth = (PyCFunction)gpiod_Chip_get_all_lines,
-		.ml_flags = METH_NOARGS,
-		.ml_doc = gpiod_Chip_get_all_lines_doc,
-	},
-	{ }
-};
-
-PyDoc_STRVAR(gpiod_ChipType_doc,
-"Represents a GPIO chip.\n"
-"\n"
-"Chip object manages all resources associated with the GPIO chip\n"
-"it represents.\n"
-"\n"
-"The gpiochip device file is opened during the object's construction.\n"
-"The Chip object's constructor takes a description string as argument the\n"
-"meaning of which depends on the second, optional parameter which defines\n"
-"the way the description string should be interpreted. The available\n"
-"options are: OPEN_BY_NAME, OPEN_BY_NUMBER, OPEN_BY_PATH and OPEN_LOOKUP.\n"
-"The last option means that libgpiod should open the chip based on the best\n"
-"guess what the path is. This is also the default if the second argument is\n"
-"missing.\n"
-"\n"
-"Callers must close the chip by calling the close() method when it's no\n"
-"longer used.\n"
-"\n"
-"Example:\n"
-"\n"
-"    chip = gpiod.Chip('gpiochip0', gpiod.Chip.OPEN_BY_NAME)\n"
-"    do_something(chip)\n"
-"    chip.close()\n"
-"\n"
-"The gpiod.Chip class also supports controlled execution ('with' statement).\n"
-"\n"
-"Example:\n"
-"\n"
-"    with gpiod.Chip('0', gpiod.Chip.OPEN_BY_NUMBER) as chip:\n"
-"        do_something(chip)");
-
-static PyTypeObject gpiod_ChipType = {
-	PyVarObject_HEAD_INIT(NULL, 0)
-	.tp_name = "gpiod.Chip",
-	.tp_basicsize = sizeof(gpiod_ChipObject),
-	.tp_flags = Py_TPFLAGS_DEFAULT,
-	.tp_doc = gpiod_ChipType_doc,
-	.tp_new = PyType_GenericNew,
-	.tp_init = (initproc)gpiod_Chip_init,
-	.tp_dealloc = (destructor)gpiod_Chip_dealloc,
-	.tp_repr = (reprfunc)gpiod_Chip_repr,
-	.tp_methods = gpiod_Chip_methods,
-};
-
-static int gpiod_LineIter_init(gpiod_LineIterObject *self,
-			       PyObject *args, PyObject *Py_UNUSED(ignored))
-{
-	gpiod_ChipObject *chip_obj;
-	int rv;
-
-	rv = PyArg_ParseTuple(args, "O!", &gpiod_ChipType,
-			      (PyObject *)&chip_obj);
-	if (!rv)
-		return -1;
-
-	if (gpiod_ChipIsClosed(chip_obj))
-		return -1;
-
-	self->offset = 0;
-	self->owner = chip_obj;
-	Py_INCREF(chip_obj);
-
-	return 0;
-}
-
-static void gpiod_LineIter_dealloc(gpiod_LineIterObject *self)
-{
-	PyObject_Del(self);
-}
-
-static gpiod_LineObject *gpiod_LineIter_next(gpiod_LineIterObject *self)
-{
-	struct gpiod_line *line;
-
-	if (self->offset == gpiod_chip_get_num_lines(self->owner->chip))
-		return NULL; /* Last element. */
-
-	line = gpiod_chip_get_line(self->owner->chip, self->offset++);
-	if (!line)
-		return (gpiod_LineObject *)PyErr_SetFromErrno(PyExc_OSError);
-
-	return gpiod_MakeLineObject(self->owner, line);
-}
-
-PyDoc_STRVAR(gpiod_LineIterType_doc,
-"Allows to iterate over all lines exposed by a GPIO chip.\n"
-"\n"
-"New line iterator is created by passing a reference to an open gpiod.Chip\n"
-"object to the constructor of gpiod.LineIter.\n"
-"\n"
-"Caller doesn't need to handle the resource management for lines as their\n"
-"lifetime is managed by the owning chip.\n"
-"\n"
-"Example:\n"
-"\n"
-"    chip = gpiod.Chip('gpiochip0')\n"
-"    for line in gpiod.LineIter(chip):\n"
-"        do_stuff_with_line(line)");
-
-static PyTypeObject gpiod_LineIterType = {
-	PyVarObject_HEAD_INIT(NULL, 0)
-	.tp_name = "gpiod.LineIter",
-	.tp_basicsize = sizeof(gpiod_LineIterObject),
-	.tp_flags = Py_TPFLAGS_DEFAULT,
-	.tp_doc = gpiod_LineIterType_doc,
-	.tp_new = PyType_GenericNew,
-	.tp_init = (initproc)gpiod_LineIter_init,
-	.tp_dealloc = (destructor)gpiod_LineIter_dealloc,
-	.tp_iter = PyObject_SelfIter,
-	.tp_iternext = (iternextfunc)gpiod_LineIter_next,
-};
-
-typedef struct {
-	const char *name;
-	PyTypeObject *typeobj;
-} gpiod_PyType;
-
-static gpiod_PyType gpiod_PyType_list[] = {
-	{ .name = "Chip",	.typeobj = &gpiod_ChipType,		},
-	{ .name = "Line",	.typeobj = &gpiod_LineType,		},
-	{ .name = "LineEvent",	.typeobj = &gpiod_LineEventType,	},
-	{ .name = "LineBulk",	.typeobj = &gpiod_LineBulkType,		},
-	{ .name = "LineIter",	.typeobj = &gpiod_LineIterType,		},
-	{ }
-};
-
-typedef struct {
-	PyTypeObject *typeobj;
-	const char *name;
-	long int val;
-} gpiod_ConstDescr;
-
-static gpiod_ConstDescr gpiod_ConstList[] = {
-	{
-		.typeobj = &gpiod_LineType,
-		.name = "DIRECTION_INPUT",
-		.val = gpiod_DIRECTION_INPUT,
-	},
-	{
-		.typeobj = &gpiod_LineType,
-		.name = "DIRECTION_OUTPUT",
-		.val = gpiod_DIRECTION_OUTPUT,
-	},
-	{
-		.typeobj = &gpiod_LineType,
-		.name = "DRIVE_PUSH_PULL",
-		.val = gpiod_DRIVE_PUSH_PULL,
-	},
-	{
-		.typeobj = &gpiod_LineType,
-		.name = "DRIVE_OPEN_DRAIN",
-		.val = gpiod_DRIVE_OPEN_DRAIN,
-	},
-	{
-		.typeobj = &gpiod_LineType,
-		.name = "DRIVE_OPEN_SOURCE",
-		.val = gpiod_DRIVE_OPEN_SOURCE,
-	},
-	{
-		.typeobj = &gpiod_LineType,
-		.name = "BIAS_UNKNOWN",
-		.val = gpiod_BIAS_UNKNOWN,
-	},
-	{
-		.typeobj = &gpiod_LineType,
-		.name = "BIAS_DISABLED",
-		.val = gpiod_BIAS_DISABLED,
-	},
-	{
-		.typeobj = &gpiod_LineType,
-		.name = "BIAS_PULL_UP",
-		.val = gpiod_BIAS_PULL_UP,
-	},
-	{
-		.typeobj = &gpiod_LineType,
-		.name = "BIAS_PULL_DOWN",
-		.val = gpiod_BIAS_PULL_DOWN,
-	},
-	{
-		.typeobj = &gpiod_LineEventType,
-		.name = "RISING_EDGE",
-		.val = gpiod_RISING_EDGE,
-	},
-	{
-		.typeobj = &gpiod_LineEventType,
-		.name = "FALLING_EDGE",
-		.val = gpiod_FALLING_EDGE,
-	},
-	{ }
-};
-
-PyDoc_STRVAR(gpiod_Module_is_gpiochip_device_doc,
-"is_gpiochip_device(path) -> boolean\n"
-"\n"
-"Check if the file pointed to by path is a GPIO chip character device.\n"
-"Returns true if so, False otherwise.\n"
-"\n"
-"  path\n"
-"    Path to the file that should be checked.\n");
-
-static PyObject *
-gpiod_Module_is_gpiochip_device(PyObject *Py_UNUSED(self), PyObject *args)
-{
-	const char *path;
-	int ret;
-
-	ret = PyArg_ParseTuple(args, "s", &path);
-	if (!ret)
-		return NULL;
-
-	if (gpiod_is_gpiochip_device(path))
-		Py_RETURN_TRUE;
-
-	Py_RETURN_FALSE;
-}
-
-static PyMethodDef gpiod_module_methods[] = {
-	{
-		.ml_name = "is_gpiochip_device",
-		.ml_meth = (PyCFunction)gpiod_Module_is_gpiochip_device,
-		.ml_flags = METH_VARARGS,
-		.ml_doc = gpiod_Module_is_gpiochip_device_doc,
-	},
-	{ }
-};
-
-PyDoc_STRVAR(gpiod_Module_doc,
-"Python bindings for libgpiod.\n\
-\n\
-This module wraps the native C API of libgpiod in a set of python classes.");
-
-static PyModuleDef gpiod_Module = {
-	PyModuleDef_HEAD_INIT,
-	.m_name = "gpiod",
-	.m_doc = gpiod_Module_doc,
-	.m_size = -1,
-	.m_methods = gpiod_module_methods,
-};
-
-typedef struct {
-	const char *name;
-	long int value;
-} gpiod_ModuleConst;
-
-static gpiod_ModuleConst gpiod_ModuleConsts[] = {
-	{
-		.name = "LINE_REQ_DIR_AS_IS",
-		.value = gpiod_LINE_REQ_DIR_AS_IS,
-	},
-	{
-		.name = "LINE_REQ_DIR_IN",
-		.value = gpiod_LINE_REQ_DIR_IN,
-	},
-	{
-		.name = "LINE_REQ_DIR_OUT",
-		.value = gpiod_LINE_REQ_DIR_OUT,
-	},
-	{
-		.name = "LINE_REQ_EV_FALLING_EDGE",
-		.value = gpiod_LINE_REQ_EV_FALLING_EDGE,
-	},
-	{
-		.name = "LINE_REQ_EV_RISING_EDGE",
-		.value = gpiod_LINE_REQ_EV_RISING_EDGE,
-	},
-	{
-		.name = "LINE_REQ_EV_BOTH_EDGES",
-		.value = gpiod_LINE_REQ_EV_BOTH_EDGES,
-	},
-	{
-		.name = "LINE_REQ_FLAG_OPEN_DRAIN",
-		.value = gpiod_LINE_REQ_FLAG_OPEN_DRAIN,
-	},
-	{
-		.name = "LINE_REQ_FLAG_OPEN_SOURCE",
-		.value = gpiod_LINE_REQ_FLAG_OPEN_SOURCE,
-	},
-	{
-		.name = "LINE_REQ_FLAG_ACTIVE_LOW",
-		.value = gpiod_LINE_REQ_FLAG_ACTIVE_LOW,
-	},
-	{
-		.name = "LINE_REQ_FLAG_BIAS_DISABLED",
-		.value = gpiod_LINE_REQ_FLAG_BIAS_DISABLED,
-	},
-	{
-		.name = "LINE_REQ_FLAG_BIAS_PULL_DOWN",
-		.value = gpiod_LINE_REQ_FLAG_BIAS_PULL_DOWN,
-	},
-	{
-		.name = "LINE_REQ_FLAG_BIAS_PULL_UP",
-		.value = gpiod_LINE_REQ_FLAG_BIAS_PULL_UP,
-	},
-	{ }
-};
-
-PyMODINIT_FUNC PyInit_gpiod(void)
-{
-	gpiod_ConstDescr *const_descr;
-	gpiod_ModuleConst *mod_const;
-	PyObject *module, *val;
-	gpiod_PyType *type;
-	unsigned int i;
-	int rv;
-
-	module = PyModule_Create(&gpiod_Module);
-	if (!module)
-		return NULL;
-
-	for (i = 0; gpiod_PyType_list[i].typeobj; i++) {
-		type = &gpiod_PyType_list[i];
-
-		rv = PyType_Ready(type->typeobj);
-		if (rv)
-			return NULL;
-
-		Py_INCREF(type->typeobj);
-		rv = PyModule_AddObject(module, type->name,
-					(PyObject *)type->typeobj);
-		if (rv < 0)
-			return NULL;
-	}
-
-	for (i = 0; gpiod_ConstList[i].name; i++) {
-		const_descr = &gpiod_ConstList[i];
-
-		val = PyLong_FromLong(const_descr->val);
-		if (!val)
-			return NULL;
-
-		rv = PyDict_SetItemString(const_descr->typeobj->tp_dict,
-					  const_descr->name, val);
-		Py_DECREF(val);
-		if (rv)
-			return NULL;
-	}
-
-	for (i = 0; gpiod_ModuleConsts[i].name; i++) {
-		mod_const = &gpiod_ModuleConsts[i];
-
-		rv = PyModule_AddIntConstant(module,
-					     mod_const->name, mod_const->value);
-		if (rv < 0)
-			return NULL;
-	}
-
-	rv = PyModule_AddStringConstant(module, "__version__",
-					gpiod_version_string());
-	if (rv < 0)
-		return NULL;
-
-	return module;
-}
diff --git a/bindings/python/tests/Makefile.am b/bindings/python/tests/Makefile.am
deleted file mode 100644
index 972b669..0000000
--- a/bindings/python/tests/Makefile.am
+++ /dev/null
@@ -1,13 +0,0 @@
-# SPDX-License-Identifier: GPL-2.0-or-later
-# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
-
-dist_bin_SCRIPTS = gpiod_py_test.py
-
-pyexec_LTLIBRARIES = gpiomockup.la
-
-gpiomockup_la_SOURCES = gpiomockupmodule.c
-gpiomockup_la_CFLAGS = -I$(top_srcdir)/tests/mockup/
-gpiomockup_la_CFLAGS += -Wall -Wextra -g -std=gnu89 $(PYTHON_CPPFLAGS)
-gpiomockup_la_LDFLAGS = -module -avoid-version
-gpiomockup_la_LIBADD = $(top_builddir)/tests/mockup/libgpiomockup.la
-gpiomockup_la_LIBADD += $(PYTHON_LIBS)
diff --git a/bindings/python/tests/gpiod_py_test.py b/bindings/python/tests/gpiod_py_test.py
deleted file mode 100755
index f93c72c..0000000
--- a/bindings/python/tests/gpiod_py_test.py
+++ /dev/null
@@ -1,832 +0,0 @@
-#!/usr/bin/env python3
-# SPDX-License-Identifier: GPL-2.0-or-later
-# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
-
-import errno
-import gpiod
-import gpiomockup
-import os
-import select
-import time
-import threading
-import unittest
-
-from packaging import version
-
-mockup = None
-default_consumer = 'gpiod-py-test'
-
-class MockupTestCase(unittest.TestCase):
-
-    chip_sizes = None
-    flags = 0
-
-    def setUp(self):
-        mockup.probe(self.chip_sizes, flags=self.flags)
-
-    def tearDown(self):
-        mockup.remove()
-
-class EventThread(threading.Thread):
-
-    def __init__(self, chip_idx, line_offset, period_ms):
-        threading.Thread.__init__(self)
-        self.chip_idx = chip_idx
-        self.line_offset = line_offset
-        self.period_ms = period_ms
-        self.lock = threading.Lock()
-        self.cond = threading.Condition(self.lock)
-        self.should_stop = False
-
-    def run(self):
-        i = 0;
-        while True:
-            with self.lock:
-                if self.should_stop:
-                    break;
-
-                if not self.cond.wait(float(self.period_ms) / 1000):
-                    mockup.chip_set_pull(self.chip_idx,
-                                         self.line_offset, i % 2)
-                    i += 1
-
-    def stop(self):
-        with self.lock:
-            self.should_stop = True
-            self.cond.notify_all()
-
-    def __enter__(self):
-        self.start()
-        return self
-
-    def __exit__(self, exc_type, exc_value, traceback):
-        self.stop()
-        self.join()
-
-def check_kernel(major, minor, release):
-    current = os.uname().release.split('-')[0]
-    required = '{}.{}.{}'.format(major, minor, release)
-    if version.parse(current) < version.parse(required):
-        raise NotImplementedError(
-                'linux kernel version must be at least {} - got {}'.format(required, current))
-
-#
-# Chip test cases
-#
-
-class IsGpioDevice(MockupTestCase):
-
-    chip_sizes = ( 8, )
-
-    def test_is_gpiochip_device_good(self):
-        self.assertTrue(gpiod.is_gpiochip_device(mockup.chip_path(0)))
-
-    def test_is_gpiochip_device_bad(self):
-        self.assertFalse(gpiod.is_gpiochip_device('/dev/null'))
-
-    def test_is_gpiochip_device_nonexistent(self):
-        self.assertFalse(gpiod.is_gpiochip_device('/dev/nonexistent_device'))
-
-class ChipOpen(MockupTestCase):
-
-    chip_sizes = ( 8, 8, 8 )
-
-    def test_open_good(self):
-        with gpiod.Chip(mockup.chip_path(1)) as chip:
-            self.assertEqual(chip.name(), mockup.chip_name(1))
-
-    def test_nonexistent_chip(self):
-        with self.assertRaises(FileNotFoundError):
-            chip = gpiod.Chip('nonexistent-chip')
-
-    def test_open_chip_no_arguments(self):
-        with self.assertRaises(TypeError):
-            chip = gpiod.Chip()
-
-class ChipClose(MockupTestCase):
-
-    chip_sizes = ( 8, )
-
-    def test_use_chip_after_close(self):
-        chip = gpiod.Chip(mockup.chip_path(0))
-        self.assertEqual(chip.name(), mockup.chip_name(0))
-        chip.close()
-        with self.assertRaises(ValueError):
-            chip.name()
-
-class ChipInfo(MockupTestCase):
-
-    chip_sizes = ( 16, )
-
-    def test_chip_get_info(self):
-        chip = gpiod.Chip(mockup.chip_path(0))
-        self.assertEqual(chip.name(), mockup.chip_name(0))
-        self.assertEqual(chip.label(), 'gpio-mockup-A')
-        self.assertEqual(chip.num_lines(), 16)
-
-class ChipGetLines(MockupTestCase):
-
-    chip_sizes = ( 8, 8, 4 )
-    flags = gpiomockup.Mockup.FLAG_NAMED_LINES
-
-    def test_get_single_line_by_offset(self):
-        with gpiod.Chip(mockup.chip_path(1)) as chip:
-            line = chip.get_line(4)
-            self.assertEqual(line.name(), 'gpio-mockup-B-4')
-
-    def test_find_single_line_by_name(self):
-        with gpiod.Chip(mockup.chip_path(1)) as chip:
-            offset = chip.find_line('gpio-mockup-B-4')
-            self.assertIsNotNone(offset)
-            self.assertEqual(offset, 4)
-
-    def test_get_single_line_invalid_offset(self):
-        with gpiod.Chip(mockup.chip_path(1)) as chip:
-            with self.assertRaises(OSError) as err_ctx:
-                line = chip.get_line(11)
-
-            self.assertEqual(err_ctx.exception.errno, errno.EINVAL)
-
-    def test_find_single_line_nonexistent(self):
-        with gpiod.Chip(mockup.chip_path(1)) as chip:
-            offset = chip.find_line('nonexistent-line')
-            self.assertIsNone(offset)
-
-    def test_get_multiple_lines_by_offsets_in_tuple(self):
-        with gpiod.Chip(mockup.chip_path(1)) as chip:
-            lines = chip.get_lines(( 1, 3, 6, 7 )).to_list()
-            self.assertEqual(len(lines), 4)
-            self.assertEqual(lines[0].name(), 'gpio-mockup-B-1')
-            self.assertEqual(lines[1].name(), 'gpio-mockup-B-3')
-            self.assertEqual(lines[2].name(), 'gpio-mockup-B-6')
-            self.assertEqual(lines[3].name(), 'gpio-mockup-B-7')
-
-    def test_get_multiple_lines_by_offsets_in_list(self):
-        with gpiod.Chip(mockup.chip_path(1)) as chip:
-            lines = chip.get_lines([ 1, 3, 6, 7 ]).to_list()
-            self.assertEqual(len(lines), 4)
-            self.assertEqual(lines[0].name(), 'gpio-mockup-B-1')
-            self.assertEqual(lines[1].name(), 'gpio-mockup-B-3')
-            self.assertEqual(lines[2].name(), 'gpio-mockup-B-6')
-            self.assertEqual(lines[3].name(), 'gpio-mockup-B-7')
-
-    def test_get_multiple_lines_invalid_offset(self):
-        with gpiod.Chip(mockup.chip_path(1)) as chip:
-            with self.assertRaises(OSError) as err_ctx:
-                line = chip.get_lines(( 1, 3, 11, 7 ))
-
-            self.assertEqual(err_ctx.exception.errno, errno.EINVAL)
-
-    def test_get_all_lines(self):
-        with gpiod.Chip(mockup.chip_path(2)) as chip:
-            lines = chip.get_all_lines().to_list()
-            self.assertEqual(len(lines), 4)
-            self.assertEqual(lines[0].name(), 'gpio-mockup-C-0')
-            self.assertEqual(lines[1].name(), 'gpio-mockup-C-1')
-            self.assertEqual(lines[2].name(), 'gpio-mockup-C-2')
-            self.assertEqual(lines[3].name(), 'gpio-mockup-C-3')
-
-#
-# Line test cases
-#
-
-class LineInfo(MockupTestCase):
-
-    chip_sizes = ( 8, )
-    flags = gpiomockup.Mockup.FLAG_NAMED_LINES
-
-    def test_unexported_line(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            line = chip.get_line(4)
-            self.assertEqual(line.offset(), 4)
-            self.assertEqual(line.name(), 'gpio-mockup-A-4')
-            self.assertEqual(line.direction(), gpiod.Line.DIRECTION_INPUT)
-            self.assertFalse(line.is_active_low())
-            self.assertEqual(line.consumer(), None)
-            self.assertFalse(line.is_used())
-
-    def test_exported_line(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            line = chip.get_line(4)
-            line.request(consumer=default_consumer,
-                         type=gpiod.LINE_REQ_DIR_OUT,
-                         flags=gpiod.LINE_REQ_FLAG_ACTIVE_LOW)
-            self.assertEqual(line.offset(), 4)
-            self.assertEqual(line.name(), 'gpio-mockup-A-4')
-            self.assertEqual(line.direction(), gpiod.Line.DIRECTION_OUTPUT)
-            self.assertTrue(line.is_active_low())
-            self.assertEqual(line.consumer(), default_consumer)
-            self.assertTrue(line.is_used())
-
-    def test_exported_line_with_flags(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            line = chip.get_line(4)
-            flags = (gpiod.LINE_REQ_FLAG_ACTIVE_LOW |
-                     gpiod.LINE_REQ_FLAG_OPEN_DRAIN)
-            line.request(consumer=default_consumer,
-                         type=gpiod.LINE_REQ_DIR_OUT,
-                         flags=flags)
-            self.assertEqual(line.offset(), 4)
-            self.assertEqual(line.name(), 'gpio-mockup-A-4')
-            self.assertEqual(line.direction(), gpiod.Line.DIRECTION_OUTPUT)
-            self.assertTrue(line.is_active_low())
-            self.assertEqual(line.consumer(), default_consumer)
-            self.assertTrue(line.is_used())
-            self.assertEqual(line.drive(), gpiod.Line.DRIVE_OPEN_DRAIN)
-            self.assertEqual(line.bias(), gpiod.Line.BIAS_UNKNOWN)
-
-    def test_exported_open_drain_line(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            line = chip.get_line(4)
-            flags = gpiod.LINE_REQ_FLAG_OPEN_DRAIN
-            line.request(consumer=default_consumer,
-                         type=gpiod.LINE_REQ_DIR_OUT,
-                         flags=flags)
-            self.assertEqual(line.offset(), 4)
-            self.assertEqual(line.name(), 'gpio-mockup-A-4')
-            self.assertEqual(line.direction(), gpiod.Line.DIRECTION_OUTPUT)
-            self.assertFalse(line.is_active_low())
-            self.assertEqual(line.consumer(), default_consumer)
-            self.assertTrue(line.is_used())
-            self.assertEqual(line.drive(), gpiod.Line.DRIVE_OPEN_DRAIN)
-            self.assertEqual(line.bias(), gpiod.Line.BIAS_UNKNOWN)
-
-    def test_exported_open_source_line(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            line = chip.get_line(4)
-            flags = gpiod.LINE_REQ_FLAG_OPEN_SOURCE
-            line.request(consumer=default_consumer,
-                         type=gpiod.LINE_REQ_DIR_OUT,
-                         flags=flags)
-            self.assertEqual(line.offset(), 4)
-            self.assertEqual(line.name(), 'gpio-mockup-A-4')
-            self.assertEqual(line.direction(), gpiod.Line.DIRECTION_OUTPUT)
-            self.assertFalse(line.is_active_low())
-            self.assertEqual(line.consumer(), default_consumer)
-            self.assertTrue(line.is_used())
-            self.assertEqual(line.drive(), gpiod.Line.DRIVE_OPEN_SOURCE)
-            self.assertEqual(line.bias(), gpiod.Line.BIAS_UNKNOWN)
-
-    def test_exported_bias_disable_line(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            line = chip.get_line(4)
-            flags = gpiod.LINE_REQ_FLAG_BIAS_DISABLED
-            line.request(consumer=default_consumer,
-                         type=gpiod.LINE_REQ_DIR_OUT,
-                         flags=flags)
-            self.assertEqual(line.offset(), 4)
-            self.assertEqual(line.name(), 'gpio-mockup-A-4')
-            self.assertEqual(line.direction(), gpiod.Line.DIRECTION_OUTPUT)
-            self.assertFalse(line.is_active_low())
-            self.assertEqual(line.consumer(), default_consumer)
-            self.assertTrue(line.is_used())
-            self.assertEqual(line.drive(), gpiod.Line.DRIVE_PUSH_PULL)
-            self.assertEqual(line.bias(), gpiod.Line.BIAS_DISABLED)
-
-    def test_exported_bias_pull_down_line(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            line = chip.get_line(4)
-            flags = gpiod.LINE_REQ_FLAG_BIAS_PULL_DOWN
-            line.request(consumer=default_consumer,
-                         type=gpiod.LINE_REQ_DIR_OUT,
-                         flags=flags)
-            self.assertEqual(line.offset(), 4)
-            self.assertEqual(line.name(), 'gpio-mockup-A-4')
-            self.assertEqual(line.direction(), gpiod.Line.DIRECTION_OUTPUT)
-            self.assertFalse(line.is_active_low())
-            self.assertEqual(line.consumer(), default_consumer)
-            self.assertTrue(line.is_used())
-            self.assertEqual(line.drive(), gpiod.Line.DRIVE_PUSH_PULL)
-            self.assertEqual(line.bias(), gpiod.Line.BIAS_PULL_DOWN)
-
-    def test_exported_bias_pull_up_line(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            line = chip.get_line(4)
-            flags = gpiod.LINE_REQ_FLAG_BIAS_PULL_UP
-            line.request(consumer=default_consumer,
-                         type=gpiod.LINE_REQ_DIR_OUT,
-                         flags=flags)
-            self.assertEqual(line.offset(), 4)
-            self.assertEqual(line.name(), 'gpio-mockup-A-4')
-            self.assertEqual(line.direction(), gpiod.Line.DIRECTION_OUTPUT)
-            self.assertFalse(line.is_active_low())
-            self.assertEqual(line.consumer(), default_consumer)
-            self.assertTrue(line.is_used())
-            self.assertEqual(line.drive(), gpiod.Line.DRIVE_PUSH_PULL)
-            self.assertEqual(line.bias(), gpiod.Line.BIAS_PULL_UP)
-
-class LineValues(MockupTestCase):
-
-    chip_sizes = ( 8, )
-
-    def test_get_value_single_line(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            line = chip.get_line(3)
-            line.request(consumer=default_consumer,
-                         type=gpiod.LINE_REQ_DIR_IN)
-            self.assertEqual(line.get_value(), 0)
-            mockup.chip_set_pull(0, 3, 1)
-            self.assertEqual(line.get_value(), 1)
-
-    def test_set_value_single_line(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            line = chip.get_line(3)
-            line.request(consumer=default_consumer,
-                         type=gpiod.LINE_REQ_DIR_OUT)
-            line.set_value(1)
-            self.assertEqual(mockup.chip_get_value(0, 3), 1)
-            line.set_value(0)
-            self.assertEqual(mockup.chip_get_value(0, 3), 0)
-
-    def test_set_value_with_default_value_argument(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            line = chip.get_line(3)
-            line.request(consumer=default_consumer,
-                         type=gpiod.LINE_REQ_DIR_OUT,
-                         default_val=1)
-            self.assertEqual(mockup.chip_get_value(0, 3), 1)
-
-    def test_get_value_multiple_lines(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            lines = chip.get_lines(( 0, 3, 4, 6 ))
-            lines.request(consumer=default_consumer,
-                          type=gpiod.LINE_REQ_DIR_IN)
-            self.assertEqual(lines.get_values(), [ 0, 0, 0, 0 ])
-            mockup.chip_set_pull(0, 0, 1)
-            mockup.chip_set_pull(0, 4, 1)
-            mockup.chip_set_pull(0, 6, 1)
-            self.assertEqual(lines.get_values(), [ 1, 0, 1, 1 ])
-
-    def test_set_value_multiple_lines(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            lines = chip.get_lines(( 0, 3, 4, 6 ))
-            lines.request(consumer=default_consumer,
-                          type=gpiod.LINE_REQ_DIR_OUT)
-            lines.set_values(( 1, 0, 1, 1 ))
-            self.assertEqual(mockup.chip_get_value(0, 0), 1)
-            self.assertEqual(mockup.chip_get_value(0, 3), 0)
-            self.assertEqual(mockup.chip_get_value(0, 4), 1)
-            self.assertEqual(mockup.chip_get_value(0, 6), 1)
-            lines.set_values(( 0, 0, 1, 0 ))
-            self.assertEqual(mockup.chip_get_value(0, 0), 0)
-            self.assertEqual(mockup.chip_get_value(0, 3), 0)
-            self.assertEqual(mockup.chip_get_value(0, 4), 1)
-            self.assertEqual(mockup.chip_get_value(0, 6), 0)
-
-    def test_set_multiple_values_with_default_vals_argument(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            lines = chip.get_lines(( 0, 3, 4, 6 ))
-            lines.request(consumer=default_consumer,
-                         type=gpiod.LINE_REQ_DIR_OUT,
-                         default_vals=( 1, 0, 1, 1 ))
-            self.assertEqual(mockup.chip_get_value(0, 0), 1)
-            self.assertEqual(mockup.chip_get_value(0, 3), 0)
-            self.assertEqual(mockup.chip_get_value(0, 4), 1)
-            self.assertEqual(mockup.chip_get_value(0, 6), 1)
-
-    def test_get_value_active_low(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            line = chip.get_line(3)
-            line.request(consumer=default_consumer,
-                         type=gpiod.LINE_REQ_DIR_IN,
-                         flags=gpiod.LINE_REQ_FLAG_ACTIVE_LOW)
-            self.assertEqual(line.get_value(), 1)
-            mockup.chip_set_pull(0, 3, 1)
-            self.assertEqual(line.get_value(), 0)
-
-    def test_set_value_active_low(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            line = chip.get_line(3)
-            line.request(consumer=default_consumer,
-                         type=gpiod.LINE_REQ_DIR_OUT,
-                         flags=gpiod.LINE_REQ_FLAG_ACTIVE_LOW)
-            line.set_value(1)
-            self.assertEqual(mockup.chip_get_value(0, 3), 0)
-            line.set_value(0)
-            self.assertEqual(mockup.chip_get_value(0, 3), 1)
-
-class LineConfig(MockupTestCase):
-
-    chip_sizes = ( 8, )
-
-    def test_set_config_direction(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            line = chip.get_line(3)
-            line.request(consumer=default_consumer,
-                         type=gpiod.LINE_REQ_DIR_IN)
-            self.assertEqual(line.direction(), gpiod.Line.DIRECTION_INPUT)
-            line.set_config(gpiod.LINE_REQ_DIR_IN, 0, 0)
-            self.assertEqual(line.direction(), gpiod.Line.DIRECTION_INPUT)
-            line.set_config(gpiod.LINE_REQ_DIR_OUT,0,0)
-            self.assertEqual(line.direction(), gpiod.Line.DIRECTION_OUTPUT)
-
-    def test_set_config_flags(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            line = chip.get_line(3)
-            line.request(consumer=default_consumer,
-                         type=gpiod.LINE_REQ_DIR_OUT)
-            line.set_config(gpiod.LINE_REQ_DIR_OUT,
-                            gpiod.LINE_REQ_FLAG_ACTIVE_LOW, 0)
-            self.assertEqual(mockup.chip_get_value(0, 3), 1)
-            line.set_config(gpiod.LINE_REQ_DIR_OUT, 0, 0)
-            self.assertEqual(mockup.chip_get_value(0, 3), 0)
-
-    def test_set_config_output_value(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            line = chip.get_line(3)
-            line.request(consumer=default_consumer,
-                         type=gpiod.LINE_REQ_DIR_IN)
-            line.set_config(gpiod.LINE_REQ_DIR_OUT,0,1)
-            self.assertEqual(mockup.chip_get_value(0, 3), 1)
-            line.set_config(gpiod.LINE_REQ_DIR_OUT,0,0)
-            self.assertEqual(mockup.chip_get_value(0, 3), 0)
-
-    def test_set_config_output_no_value(self):
-         with gpiod.Chip(mockup.chip_path(0)) as chip:
-            line = chip.get_line(3)
-            line.request(consumer=default_consumer,
-                         type=gpiod.LINE_REQ_DIR_OUT,
-                         default_val=1)
-            self.assertEqual(mockup.chip_get_value(0, 3), 1)
-            line.set_config(gpiod.LINE_REQ_DIR_OUT,0)
-            self.assertEqual(mockup.chip_get_value(0, 3), 0)
-
-    def test_set_config_bulk_output_no_values(self):
-         with gpiod.Chip(mockup.chip_path(0)) as chip:
-            lines = chip.get_lines(( 0, 3, 4, 6 ))
-            lines.request(consumer=default_consumer,
-                          type=gpiod.LINE_REQ_DIR_OUT,
-                          default_vals=(1,1,1,1))
-            self.assertEqual(mockup.chip_get_value(0, 0), 1)
-            self.assertEqual(mockup.chip_get_value(0, 3), 1)
-            self.assertEqual(mockup.chip_get_value(0, 4), 1)
-            self.assertEqual(mockup.chip_get_value(0, 6), 1)
-            lines.set_config(gpiod.LINE_REQ_DIR_OUT,0)
-            self.assertEqual(mockup.chip_get_value(0, 0), 0)
-            self.assertEqual(mockup.chip_get_value(0, 3), 0)
-            self.assertEqual(mockup.chip_get_value(0, 4), 0)
-            self.assertEqual(mockup.chip_get_value(0, 6), 0)
-
-class LineFlags(MockupTestCase):
-
-    chip_sizes = ( 8, )
-
-    def test_set_flags(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            line = chip.get_line(3)
-            line.request(consumer=default_consumer,
-                         type=gpiod.LINE_REQ_DIR_OUT,
-                         default_val=1)
-            self.assertEqual(mockup.chip_get_value(0, 3), 1)
-            line.set_flags(gpiod.LINE_REQ_FLAG_ACTIVE_LOW)
-            self.assertEqual(mockup.chip_get_value(0, 3), 0)
-            line.set_flags(0)
-            self.assertEqual(mockup.chip_get_value(0, 3), 1)
-
-    def test_set_flags_bulk(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            lines = chip.get_lines(( 0, 3, 4, 6 ))
-            lines.request(consumer=default_consumer,
-                          type=gpiod.LINE_REQ_DIR_OUT,
-                          default_vals=(1,1,1,1))
-            self.assertEqual(mockup.chip_get_value(0, 0), 1)
-            self.assertEqual(mockup.chip_get_value(0, 3), 1)
-            self.assertEqual(mockup.chip_get_value(0, 4), 1)
-            self.assertEqual(mockup.chip_get_value(0, 6), 1)
-            lines.set_flags(gpiod.LINE_REQ_FLAG_ACTIVE_LOW)
-            self.assertEqual(mockup.chip_get_value(0, 0), 0)
-            self.assertEqual(mockup.chip_get_value(0, 3), 0)
-            self.assertEqual(mockup.chip_get_value(0, 4), 0)
-            self.assertEqual(mockup.chip_get_value(0, 6), 0)
-            lines.set_flags(0)
-            self.assertEqual(mockup.chip_get_value(0, 0), 1)
-            self.assertEqual(mockup.chip_get_value(0, 3), 1)
-            self.assertEqual(mockup.chip_get_value(0, 4), 1)
-            self.assertEqual(mockup.chip_get_value(0, 6), 1)
-
-class LineDirection(MockupTestCase):
-
-    chip_sizes = ( 8, )
-
-    def test_set_direction(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            line = chip.get_line(3)
-            line.request(consumer=default_consumer,
-                         type=gpiod.LINE_REQ_DIR_OUT)
-            self.assertEqual(line.direction(), gpiod.Line.DIRECTION_OUTPUT)
-            line.set_direction_input()
-            self.assertEqual(line.direction(), gpiod.Line.DIRECTION_INPUT)
-            line.set_direction_output(0)
-            self.assertEqual(line.direction(), gpiod.Line.DIRECTION_OUTPUT)
-            self.assertEqual(mockup.chip_get_value(0, 3), 0)
-            line.set_direction_output(1)
-            self.assertEqual(line.direction(), gpiod.Line.DIRECTION_OUTPUT)
-            self.assertEqual(mockup.chip_get_value(0, 3), 1)
-            line.set_direction_output()
-            self.assertEqual(line.direction(), gpiod.Line.DIRECTION_OUTPUT)
-            self.assertEqual(mockup.chip_get_value(0, 3), 0)
-
-    def test_set_direction_bulk(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            lines = chip.get_lines(( 0, 3, 4, 6 ))
-            lines.request(consumer=default_consumer,
-                          type=gpiod.LINE_REQ_DIR_OUT)
-            self.assertEqual(lines.to_list()[0].direction(),
-                             gpiod.Line.DIRECTION_OUTPUT)
-            self.assertEqual(lines.to_list()[1].direction(),
-                             gpiod.Line.DIRECTION_OUTPUT)
-            self.assertEqual(lines.to_list()[2].direction(),
-                             gpiod.Line.DIRECTION_OUTPUT)
-            self.assertEqual(lines.to_list()[3].direction(),
-                             gpiod.Line.DIRECTION_OUTPUT)
-            lines.set_direction_input()
-            self.assertEqual(lines.to_list()[0].direction(),
-                             gpiod.Line.DIRECTION_INPUT)
-            self.assertEqual(lines.to_list()[1].direction(),
-                             gpiod.Line.DIRECTION_INPUT)
-            self.assertEqual(lines.to_list()[2].direction(),
-                             gpiod.Line.DIRECTION_INPUT)
-            self.assertEqual(lines.to_list()[3].direction(),
-                             gpiod.Line.DIRECTION_INPUT)
-            lines.set_direction_output((0,0,1,0))
-            self.assertEqual(lines.to_list()[0].direction(),
-                             gpiod.Line.DIRECTION_OUTPUT)
-            self.assertEqual(lines.to_list()[1].direction(),
-                             gpiod.Line.DIRECTION_OUTPUT)
-            self.assertEqual(lines.to_list()[2].direction(),
-                             gpiod.Line.DIRECTION_OUTPUT)
-            self.assertEqual(lines.to_list()[3].direction(),
-                             gpiod.Line.DIRECTION_OUTPUT)
-            self.assertEqual(mockup.chip_get_value(0, 0), 0)
-            self.assertEqual(mockup.chip_get_value(0, 3), 0)
-            self.assertEqual(mockup.chip_get_value(0, 4), 1)
-            self.assertEqual(mockup.chip_get_value(0, 6), 0)
-            lines.set_direction_output((1,1,1,0))
-            self.assertEqual(lines.to_list()[0].direction(),
-                             gpiod.Line.DIRECTION_OUTPUT)
-            self.assertEqual(lines.to_list()[1].direction(),
-                             gpiod.Line.DIRECTION_OUTPUT)
-            self.assertEqual(lines.to_list()[2].direction(),
-                             gpiod.Line.DIRECTION_OUTPUT)
-            self.assertEqual(lines.to_list()[3].direction(),
-                             gpiod.Line.DIRECTION_OUTPUT)
-            self.assertEqual(mockup.chip_get_value(0, 0), 1)
-            self.assertEqual(mockup.chip_get_value(0, 3), 1)
-            self.assertEqual(mockup.chip_get_value(0, 4), 1)
-            self.assertEqual(mockup.chip_get_value(0, 6), 0)
-            lines.set_direction_output()
-            self.assertEqual(lines.to_list()[0].direction(),
-                             gpiod.Line.DIRECTION_OUTPUT)
-            self.assertEqual(lines.to_list()[1].direction(),
-                             gpiod.Line.DIRECTION_OUTPUT)
-            self.assertEqual(lines.to_list()[2].direction(),
-                             gpiod.Line.DIRECTION_OUTPUT)
-            self.assertEqual(lines.to_list()[3].direction(),
-                             gpiod.Line.DIRECTION_OUTPUT)
-            self.assertEqual(mockup.chip_get_value(0, 0), 0)
-            self.assertEqual(mockup.chip_get_value(0, 3), 0)
-            self.assertEqual(mockup.chip_get_value(0, 4), 0)
-            self.assertEqual(mockup.chip_get_value(0, 6), 0)
-
-class LineRequestBehavior(MockupTestCase):
-
-    chip_sizes = ( 8, )
-
-    def test_line_request_twice_two_calls(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            line = chip.get_line(3)
-            line.request(consumer=default_consumer,
-                         type=gpiod.LINE_REQ_DIR_IN)
-            with self.assertRaises(OSError) as err_ctx:
-                line.request(consumer=default_consumer,
-                             type=gpiod.LINE_REQ_DIR_IN)
-
-            self.assertEqual(err_ctx.exception.errno, errno.EBUSY)
-
-    def test_line_request_twice_in_bulk(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            lines = chip.get_lines(( 2, 3, 6, 6 ))
-            with self.assertRaises(OSError) as err_ctx:
-                lines.request(consumer=default_consumer,
-                              type=gpiod.LINE_REQ_DIR_IN)
-
-            self.assertEqual(err_ctx.exception.errno, errno.EBUSY)
-
-    def test_use_value_unrequested(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            line = chip.get_line(3)
-            with self.assertRaises(OSError) as err_ctx:
-                line.get_value()
-
-            self.assertEqual(err_ctx.exception.errno, errno.EPERM)
-
-    def test_request_with_no_kwds(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            line = chip.get_line(2)
-            line.request(default_consumer)
-            self.assertEqual(line.direction(), gpiod.Line.DIRECTION_INPUT)
-            line.release()
-
-#
-# Iterator test cases
-#
-
-class LineIterator(MockupTestCase):
-
-    chip_sizes = ( 4, )
-
-    def test_iterate_over_lines(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            count = 0
-
-            for line in gpiod.LineIter(chip):
-                self.assertEqual(line.offset(), count)
-                count += 1
-
-            self.assertEqual(count, chip.num_lines())
-
-#
-# Event test cases
-#
-
-class EventSingleLine(MockupTestCase):
-
-    chip_sizes = ( 8, )
-
-    def test_single_line_rising_edge_event(self):
-        with EventThread(0, 4, 200):
-            with gpiod.Chip(mockup.chip_path(0)) as chip:
-                line = chip.get_line(4)
-                line.request(consumer=default_consumer,
-                             type=gpiod.LINE_REQ_EV_RISING_EDGE)
-                self.assertTrue(line.event_wait(sec=1))
-                event = line.event_read()
-                self.assertEqual(event.type, gpiod.LineEvent.RISING_EDGE)
-                self.assertEqual(event.source.offset(), 4)
-
-    def test_single_line_falling_edge_event(self):
-        with EventThread(0, 4, 200):
-            with gpiod.Chip(mockup.chip_path(0)) as chip:
-                line = chip.get_line(4)
-                line.request(consumer=default_consumer,
-                             type=gpiod.LINE_REQ_EV_FALLING_EDGE)
-                self.assertTrue(line.event_wait(sec=1))
-                event = line.event_read()
-                self.assertEqual(event.type, gpiod.LineEvent.FALLING_EDGE)
-                self.assertEqual(event.source.offset(), 4)
-
-    def test_single_line_both_edges_events(self):
-        with EventThread(0, 4, 200):
-            with gpiod.Chip(mockup.chip_path(0)) as chip:
-                line = chip.get_line(4)
-                line.request(consumer=default_consumer,
-                             type=gpiod.LINE_REQ_EV_BOTH_EDGES)
-                self.assertTrue(line.event_wait(sec=1))
-                event = line.event_read()
-                self.assertEqual(event.type, gpiod.LineEvent.RISING_EDGE)
-                self.assertEqual(event.source.offset(), 4)
-                self.assertTrue(line.event_wait(sec=1))
-                event = line.event_read()
-                self.assertEqual(event.type, gpiod.LineEvent.FALLING_EDGE)
-                self.assertEqual(event.source.offset(), 4)
-
-    def test_single_line_both_edges_events_active_low(self):
-        with EventThread(0, 4, 200):
-            with gpiod.Chip(mockup.chip_path(0)) as chip:
-                line = chip.get_line(4)
-                line.request(consumer=default_consumer,
-                             type=gpiod.LINE_REQ_EV_BOTH_EDGES,
-                             flags=gpiod.LINE_REQ_FLAG_ACTIVE_LOW)
-                self.assertTrue(line.event_wait(sec=1))
-                event = line.event_read()
-                self.assertEqual(event.type, gpiod.LineEvent.FALLING_EDGE)
-                self.assertEqual(event.source.offset(), 4)
-                self.assertTrue(line.event_wait(sec=1))
-                event = line.event_read()
-                self.assertEqual(event.type, gpiod.LineEvent.RISING_EDGE)
-                self.assertEqual(event.source.offset(), 4)
-
-    def test_single_line_read_multiple_events(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            line = chip.get_line(4)
-            line.request(consumer=default_consumer,
-                         type=gpiod.LINE_REQ_EV_BOTH_EDGES)
-            mockup.chip_set_pull(0, 4, 1)
-            time.sleep(0.01)
-            mockup.chip_set_pull(0, 4, 0)
-            time.sleep(0.01)
-            mockup.chip_set_pull(0, 4, 1)
-            time.sleep(0.01)
-            self.assertTrue(line.event_wait(sec=1))
-            events = line.event_read_multiple()
-            self.assertEqual(len(events), 3)
-            self.assertEqual(events[0].type, gpiod.LineEvent.RISING_EDGE)
-            self.assertEqual(events[1].type, gpiod.LineEvent.FALLING_EDGE)
-            self.assertEqual(events[2].type, gpiod.LineEvent.RISING_EDGE)
-            self.assertEqual(events[0].source.offset(), 4)
-            self.assertEqual(events[1].source.offset(), 4)
-            self.assertEqual(events[2].source.offset(), 4)
-
-class EventBulk(MockupTestCase):
-
-    chip_sizes = ( 8, )
-
-    def test_watch_multiple_lines_for_events(self):
-        with EventThread(0, 2, 200):
-            with gpiod.Chip(mockup.chip_path(0)) as chip:
-                lines = chip.get_lines(( 0, 1, 2, 3, 4 ))
-                lines.request(consumer=default_consumer,
-                              type=gpiod.LINE_REQ_EV_BOTH_EDGES)
-                event_lines = lines.event_wait(sec=1)
-                self.assertEqual(len(event_lines), 1)
-                line = event_lines[0]
-                event = line.event_read()
-                self.assertEqual(event.type, gpiod.LineEvent.RISING_EDGE)
-                self.assertEqual(event.source.offset(), 2)
-                event_lines = lines.event_wait(sec=1)
-                self.assertEqual(len(event_lines), 1)
-                line = event_lines[0]
-                event = line.event_read()
-                self.assertEqual(event.type, gpiod.LineEvent.FALLING_EDGE)
-                self.assertEqual(event.source.offset(), 2)
-
-class EventValues(MockupTestCase):
-
-    chip_sizes = ( 8, )
-
-    def test_request_for_events_get_value(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            line = chip.get_line(3)
-            line.request(consumer=default_consumer,
-                         type=gpiod.LINE_REQ_EV_BOTH_EDGES)
-            self.assertEqual(line.get_value(), 0)
-            mockup.chip_set_pull(0, 3, 1)
-            self.assertEqual(line.get_value(), 1)
-
-    def test_request_for_events_get_value_active_low(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            line = chip.get_line(3)
-            line.request(consumer=default_consumer,
-                         type=gpiod.LINE_REQ_EV_BOTH_EDGES,
-                         flags=gpiod.LINE_REQ_FLAG_ACTIVE_LOW)
-            self.assertEqual(line.get_value(), 1)
-            mockup.chip_set_pull(0, 3, 1)
-            self.assertEqual(line.get_value(), 0)
-
-class EventFileDescriptor(MockupTestCase):
-
-    chip_sizes = ( 8, )
-
-    def test_event_get_fd(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            line = chip.get_line(3)
-            line.request(consumer=default_consumer,
-                         type=gpiod.LINE_REQ_EV_BOTH_EDGES)
-            fd = line.event_get_fd();
-            self.assertGreaterEqual(fd, 0)
-
-    def test_event_get_fd_not_requested(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            line = chip.get_line(3)
-            with self.assertRaises(OSError) as err_ctx:
-                fd = line.event_get_fd();
-
-            self.assertEqual(err_ctx.exception.errno, errno.EPERM)
-
-    def test_event_get_fd_requested_for_values(self):
-        with gpiod.Chip(mockup.chip_path(0)) as chip:
-            line = chip.get_line(3)
-            line.request(consumer=default_consumer,
-                         type=gpiod.LINE_REQ_DIR_IN)
-            with self.assertRaises(OSError) as err_ctx:
-                fd = line.event_get_fd();
-
-            self.assertEqual(err_ctx.exception.errno, errno.EPERM)
-
-    def test_event_fd_polling(self):
-        with EventThread(0, 2, 200):
-            with gpiod.Chip(mockup.chip_path(0)) as chip:
-                lines = chip.get_lines(( 0, 1, 2, 3, 4, 5, 6 ))
-                lines.request(consumer=default_consumer,
-                              type=gpiod.LINE_REQ_EV_BOTH_EDGES)
-
-                inputs = []
-                for line in lines:
-                    inputs.append(line.event_get_fd())
-
-                readable, writable, exceptional = select.select(inputs, [],
-                                                                inputs, 1.0)
-
-                self.assertEqual(len(readable), 1)
-                event = lines.to_list()[2].event_read()
-                self.assertEqual(event.type, gpiod.LineEvent.RISING_EDGE)
-                self.assertEqual(event.source.offset(), 2)
-
-#
-# Main
-#
-
-if __name__ == '__main__':
-    check_kernel(5, 10, 0)
-    mockup = gpiomockup.Mockup()
-    unittest.main()
diff --git a/bindings/python/tests/gpiomockupmodule.c b/bindings/python/tests/gpiomockupmodule.c
deleted file mode 100644
index 761d431..0000000
--- a/bindings/python/tests/gpiomockupmodule.c
+++ /dev/null
@@ -1,309 +0,0 @@
-// SPDX-License-Identifier: LGPL-2.1-or-later
-// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
-
-#include <Python.h>
-#include <gpio-mockup.h>
-
-typedef struct {
-	PyObject_HEAD
-	struct gpio_mockup *mockup;
-} gpiomockup_MockupObject;
-
-enum {
-	gpiomockup_FLAG_NAMED_LINES = 1,
-};
-
-static int gpiomockup_Mockup_init(gpiomockup_MockupObject *self,
-				  PyObject *Py_UNUSED(ignored0),
-				  PyObject *Py_UNUSED(ignored1))
-{
-	Py_BEGIN_ALLOW_THREADS;
-	self->mockup = gpio_mockup_new();
-	Py_END_ALLOW_THREADS;
-	if (!self->mockup) {
-		PyErr_SetFromErrno(PyExc_OSError);
-		return -1;
-	}
-
-	return 0;
-}
-
-static void gpiomockup_Mockup_dealloc(gpiomockup_MockupObject *self)
-{
-	if (self->mockup) {
-		Py_BEGIN_ALLOW_THREADS;
-		gpio_mockup_unref(self->mockup);
-		Py_END_ALLOW_THREADS;
-	}
-
-	PyObject_Del(self);
-}
-
-static PyObject *gpiomockup_Mockup_probe(gpiomockup_MockupObject *self,
-					 PyObject *args, PyObject *kwds)
-{
-	static char *kwlist[] = { "chip_sizes",
-				  "flags",
-				  NULL };
-
-	PyObject *chip_sizes_obj, *iter, *next;
-	unsigned int *chip_sizes;
-	int ret, flags = 0, i;
-	Py_ssize_t num_chips;
-
-	ret = PyArg_ParseTupleAndKeywords(args, kwds, "O|i", kwlist,
-					  &chip_sizes_obj, &flags);
-	if (!ret)
-		return NULL;
-
-	num_chips = PyObject_Size(chip_sizes_obj);
-	if (num_chips < 0) {
-		return NULL;
-	} else if (num_chips == 0) {
-		PyErr_SetString(PyExc_TypeError,
-				"Number of chips must be greater thatn 0");
-		return NULL;
-	}
-
-	chip_sizes = PyMem_RawCalloc(num_chips, sizeof(unsigned int));
-	if (!chip_sizes)
-		return NULL;
-
-	iter = PyObject_GetIter(chip_sizes_obj);
-	if (!iter) {
-		PyMem_RawFree(chip_sizes);
-		return NULL;
-	}
-
-	for (i = 0;; i++) {
-		next = PyIter_Next(iter);
-		if (!next) {
-			Py_DECREF(iter);
-			break;
-		}
-
-		chip_sizes[i] = PyLong_AsUnsignedLong(next);
-		Py_DECREF(next);
-		if (PyErr_Occurred()) {
-			Py_DECREF(iter);
-			PyMem_RawFree(chip_sizes);
-			return NULL;
-		}
-	}
-
-	if (flags & gpiomockup_FLAG_NAMED_LINES)
-		flags |= GPIO_MOCKUP_FLAG_NAMED_LINES;
-
-	Py_BEGIN_ALLOW_THREADS;
-	ret = gpio_mockup_probe(self->mockup, num_chips, chip_sizes, flags);
-	Py_END_ALLOW_THREADS;
-	PyMem_RawFree(chip_sizes);
-	if (ret)
-		return NULL;
-
-	Py_RETURN_NONE;
-}
-
-static PyObject *gpiomockup_Mockup_remove(gpiomockup_MockupObject *self,
-					  PyObject *Py_UNUSED(ignored))
-{
-	int ret;
-
-	Py_BEGIN_ALLOW_THREADS;
-	ret = gpio_mockup_remove(self->mockup);
-	Py_END_ALLOW_THREADS;
-	if (ret) {
-		PyErr_SetFromErrno(PyExc_OSError);
-		return NULL;
-	}
-
-	Py_RETURN_NONE;
-}
-
-static PyObject *gpiomockup_Mockup_chip_name(gpiomockup_MockupObject *self,
-					     PyObject *args)
-{
-	unsigned int idx;
-	const char *name;
-	int ret;
-
-	ret = PyArg_ParseTuple(args, "I", &idx);
-	if (!ret)
-		return NULL;
-
-	name = gpio_mockup_chip_name(self->mockup, idx);
-	if (!name) {
-		PyErr_SetFromErrno(PyExc_OSError);
-		return NULL;
-	}
-
-	return PyUnicode_FromString(name);
-}
-
-static PyObject *gpiomockup_Mockup_chip_path(gpiomockup_MockupObject *self,
-					     PyObject *args)
-{
-	unsigned int idx;
-	const char *path;
-	int ret;
-
-	ret = PyArg_ParseTuple(args, "I", &idx);
-	if (!ret)
-		return NULL;
-
-	path = gpio_mockup_chip_path(self->mockup, idx);
-	if (!path) {
-		PyErr_SetFromErrno(PyExc_OSError);
-		return NULL;
-	}
-
-	return PyUnicode_FromString(path);
-}
-
-static PyObject *gpiomockup_Mockup_chip_num(gpiomockup_MockupObject *self,
-					     PyObject *args)
-{
-	unsigned int idx;
-	int ret, num;
-
-	ret = PyArg_ParseTuple(args, "I", &idx);
-	if (!ret)
-		return NULL;
-
-	num = gpio_mockup_chip_num(self->mockup, idx);
-	if (num < 0) {
-		PyErr_SetFromErrno(PyExc_OSError);
-		return NULL;
-	}
-
-	return PyLong_FromLong(num);
-}
-
-static PyObject *gpiomockup_Mockup_chip_get_value(gpiomockup_MockupObject *self,
-						  PyObject *args)
-{
-	unsigned int chip_idx, line_offset;
-	int ret, val;
-
-	ret = PyArg_ParseTuple(args, "II", &chip_idx, &line_offset);
-	if (!ret)
-		return NULL;
-
-	Py_BEGIN_ALLOW_THREADS;
-	val = gpio_mockup_get_value(self->mockup, chip_idx, line_offset);
-	Py_END_ALLOW_THREADS;
-	if (val < 0) {
-		PyErr_SetFromErrno(PyExc_OSError);
-		return NULL;
-	}
-
-	return PyLong_FromUnsignedLong(val);
-}
-
-static PyObject *gpiomockup_Mockup_chip_set_pull(gpiomockup_MockupObject *self,
-						 PyObject *args)
-{
-	unsigned int chip_idx, line_offset;
-	int ret, pull;
-
-	ret = PyArg_ParseTuple(args, "IIi", &chip_idx, &line_offset, &pull);
-	if (!ret)
-		return NULL;
-
-	Py_BEGIN_ALLOW_THREADS;
-	ret = gpio_mockup_set_pull(self->mockup, chip_idx, line_offset, pull);
-	Py_END_ALLOW_THREADS;
-	if (ret) {
-		PyErr_SetFromErrno(PyExc_OSError);
-		return NULL;
-	}
-
-	Py_RETURN_NONE;
-}
-
-static PyMethodDef gpiomockup_Mockup_methods[] = {
-	{
-		.ml_name = "probe",
-		.ml_meth = (PyCFunction)(void (*)(void))gpiomockup_Mockup_probe,
-		.ml_flags = METH_VARARGS | METH_KEYWORDS,
-	},
-	{
-		.ml_name = "remove",
-		.ml_meth = (PyCFunction)gpiomockup_Mockup_remove,
-		.ml_flags = METH_NOARGS,
-	},
-	{
-		.ml_name = "chip_name",
-		.ml_meth = (PyCFunction)gpiomockup_Mockup_chip_name,
-		.ml_flags = METH_VARARGS,
-	},
-	{
-		.ml_name = "chip_path",
-		.ml_meth = (PyCFunction)gpiomockup_Mockup_chip_path,
-		.ml_flags = METH_VARARGS,
-	},
-	{
-		.ml_name = "chip_num",
-		.ml_meth = (PyCFunction)gpiomockup_Mockup_chip_num,
-		.ml_flags = METH_VARARGS,
-	},
-	{
-		.ml_name = "chip_get_value",
-		.ml_meth = (PyCFunction)gpiomockup_Mockup_chip_get_value,
-		.ml_flags = METH_VARARGS,
-	},
-	{
-		.ml_name = "chip_set_pull",
-		.ml_meth = (PyCFunction)gpiomockup_Mockup_chip_set_pull,
-		.ml_flags = METH_VARARGS,
-	},
-	{ }
-};
-
-static PyTypeObject gpiomockup_MockupType = {
-	PyVarObject_HEAD_INIT(NULL, 0)
-	.tp_name = "gpiomockup.Mockup",
-	.tp_basicsize = sizeof(gpiomockup_MockupObject),
-	.tp_flags = Py_TPFLAGS_DEFAULT,
-	.tp_new = PyType_GenericNew,
-	.tp_init = (initproc)gpiomockup_Mockup_init,
-	.tp_dealloc = (destructor)gpiomockup_Mockup_dealloc,
-	.tp_methods = gpiomockup_Mockup_methods,
-};
-
-static PyModuleDef gpiomockup_Module = {
-	PyModuleDef_HEAD_INIT,
-	.m_name = "gpiomockup",
-	.m_size = -1,
-};
-
-PyMODINIT_FUNC PyInit_gpiomockup(void)
-{
-	PyObject *module, *val;
-	int ret;
-
-	module = PyModule_Create(&gpiomockup_Module);
-	if (!module)
-		return NULL;
-
-	ret = PyType_Ready(&gpiomockup_MockupType);
-	if (ret)
-		return NULL;
-	Py_INCREF(&gpiomockup_MockupType);
-
-	ret = PyModule_AddObject(module, "Mockup",
-				 (PyObject *)&gpiomockup_MockupType);
-	if (ret)
-		return NULL;
-
-	val = PyLong_FromLong(gpiomockup_FLAG_NAMED_LINES);
-	if (!val)
-		return NULL;
-
-	ret = PyDict_SetItemString(gpiomockup_MockupType.tp_dict,
-				   "FLAG_NAMED_LINES", val);
-	if (ret)
-		return NULL;
-
-	return module;
-}
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [libgpiod v2][PATCH v3 2/4] bindings: python: add examples
  2022-10-07 14:55 [libgpiod v2][PATCH v3 0/4] bindings: implement python bindings for libgpiod v2 Bartosz Golaszewski
  2022-10-07 14:55 ` [libgpiod v2][PATCH v3 1/4] bindings: python: remove old version Bartosz Golaszewski
@ 2022-10-07 14:55 ` Bartosz Golaszewski
  2022-10-13  3:09   ` Kent Gibson
  2022-10-07 14:55 ` [libgpiod v2][PATCH v3 3/4] bindings: python: add tests Bartosz Golaszewski
                   ` (2 subsequent siblings)
  4 siblings, 1 reply; 30+ messages in thread
From: Bartosz Golaszewski @ 2022-10-07 14:55 UTC (permalink / raw)
  To: Kent Gibson, Linus Walleij, Andy Shevchenko, Viresh Kumar
  Cc: linux-gpio, Bartosz Golaszewski

This adds the regular set of example programs implemented using libgpiod
python bindings.

Signed-off-by: Bartosz Golaszewski <brgl@bgdev.pl>
---
 bindings/python/examples/Makefile.am   | 10 +++++++
 bindings/python/examples/gpiodetect.py | 17 ++++++++++++
 bindings/python/examples/gpiofind.py   | 20 ++++++++++++++
 bindings/python/examples/gpioget.py    | 31 +++++++++++++++++++++
 bindings/python/examples/gpioinfo.py   | 35 ++++++++++++++++++++++++
 bindings/python/examples/gpiomon.py    | 28 +++++++++++++++++++
 bindings/python/examples/gpioset.py    | 37 ++++++++++++++++++++++++++
 7 files changed, 178 insertions(+)
 create mode 100644 bindings/python/examples/Makefile.am
 create mode 100755 bindings/python/examples/gpiodetect.py
 create mode 100755 bindings/python/examples/gpiofind.py
 create mode 100755 bindings/python/examples/gpioget.py
 create mode 100755 bindings/python/examples/gpioinfo.py
 create mode 100755 bindings/python/examples/gpiomon.py
 create mode 100755 bindings/python/examples/gpioset.py

diff --git a/bindings/python/examples/Makefile.am b/bindings/python/examples/Makefile.am
new file mode 100644
index 0000000..f42b80e
--- /dev/null
+++ b/bindings/python/examples/Makefile.am
@@ -0,0 +1,10 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+EXTRA_DIST = \
+	gpiodetect.py \
+	gpiofind.py \
+	gpioget.py \
+	gpioinfo.py \
+	gpiomon.py \
+	gpioset.py
diff --git a/bindings/python/examples/gpiodetect.py b/bindings/python/examples/gpiodetect.py
new file mode 100755
index 0000000..c32014f
--- /dev/null
+++ b/bindings/python/examples/gpiodetect.py
@@ -0,0 +1,17 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+"""Reimplementation of the gpiodetect tool in Python."""
+
+import gpiod
+import os
+
+if __name__ == "__main__":
+    for entry in os.scandir("/dev/"):
+        if gpiod.is_gpiochip_device(entry.path):
+            with gpiod.Chip(entry.path) as chip:
+                info = chip.get_info()
+                print(
+                    "{} [{}] ({} lines)".format(info.name, info.label, info.num_lines)
+                )
diff --git a/bindings/python/examples/gpiofind.py b/bindings/python/examples/gpiofind.py
new file mode 100755
index 0000000..2f30445
--- /dev/null
+++ b/bindings/python/examples/gpiofind.py
@@ -0,0 +1,20 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+"""Reimplementation of the gpiofind tool in Python."""
+
+import gpiod
+import os
+import sys
+
+if __name__ == "__main__":
+    for entry in os.scandir("/dev/"):
+        if gpiod.is_gpiochip_device(entry.path):
+            with gpiod.Chip(entry.path) as chip:
+                offset = chip.map_line(sys.argv[1])
+                if offset is not None:
+                    print("{} {}".format(chip.get_info().name, offset))
+                    sys.exit(0)
+
+    sys.exit(1)
diff --git a/bindings/python/examples/gpioget.py b/bindings/python/examples/gpioget.py
new file mode 100755
index 0000000..d441535
--- /dev/null
+++ b/bindings/python/examples/gpioget.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+"""Simplified reimplementation of the gpioget tool in Python."""
+
+import gpiod
+import sys
+
+from gpiod.line import Direction
+
+if __name__ == "__main__":
+    if len(sys.argv) < 3:
+        raise TypeError("usage: gpioget.py <gpiochip> <offset1> <offset2> ...")
+
+    path = sys.argv[1]
+    lines = []
+    for line in sys.argv[2:]:
+        lines.append(int(line) if line.isdigit() else line)
+
+    request = gpiod.request_lines(
+        path,
+        consumer="gpioget.py",
+        config={tuple(lines): gpiod.LineSettings(direction=Direction.INPUT)},
+    )
+
+    vals = request.get_values()
+
+    for val in vals:
+        print("{} ".format(val.value), end="")
+    print()
diff --git a/bindings/python/examples/gpioinfo.py b/bindings/python/examples/gpioinfo.py
new file mode 100755
index 0000000..e8c7d46
--- /dev/null
+++ b/bindings/python/examples/gpioinfo.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+"""Simplified reimplementation of the gpioinfo tool in Python."""
+
+import gpiod
+import os
+
+if __name__ == "__main__":
+    for entry in os.scandir("/dev/"):
+        if gpiod.is_gpiochip_device(entry.path):
+            with gpiod.Chip(entry.path) as chip:
+                cinfo = chip.get_info()
+                print("{} - {} lines:".format(cinfo.name, cinfo.num_lines))
+
+                for offset in range(0, cinfo.num_lines):
+                    linfo = chip.get_line_info(offset)
+                    offset = linfo.offset
+                    name = linfo.name
+                    consumer = linfo.consumer
+                    direction = linfo.direction
+                    active_low = linfo.active_low
+
+                    print(
+                        "\tline {:>3}: {:>18} {:>12} {:>8} {:>10}".format(
+                            offset,
+                            "unnamed" if name is None else name,
+                            "unused" if consumer is None else consumer,
+                            "input"
+                            if direction == gpiod.line.Direction.INPUT
+                            else "output",
+                            "active-low" if active_low else "active-high",
+                        )
+                    )
diff --git a/bindings/python/examples/gpiomon.py b/bindings/python/examples/gpiomon.py
new file mode 100755
index 0000000..e0db16f
--- /dev/null
+++ b/bindings/python/examples/gpiomon.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+"""Simplified reimplementation of the gpiomon tool in Python."""
+
+import gpiod
+import sys
+
+from gpiod.line import Edge
+
+if __name__ == "__main__":
+    if len(sys.argv) < 3:
+        raise TypeError("usage: gpiomon.py <gpiochip> <offset1> <offset2> ...")
+
+    path = sys.argv[1]
+    lines = []
+    for line in sys.argv[2:]:
+        lines.append(int(line) if line.isdigit() else line)
+
+    with gpiod.request_lines(
+        path,
+        consumer="gpiomon.py",
+        config={tuple(lines): gpiod.LineSettings(edge_detection=Edge.BOTH)},
+    ) as request:
+        while True:
+            for event in request.read_edge_event():
+                print(event)
diff --git a/bindings/python/examples/gpioset.py b/bindings/python/examples/gpioset.py
new file mode 100755
index 0000000..f0b0681
--- /dev/null
+++ b/bindings/python/examples/gpioset.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+"""Simplified reimplementation of the gpioset tool in Python."""
+
+import gpiod
+import sys
+
+from gpiod.line import Direction, Value
+
+if __name__ == "__main__":
+    if len(sys.argv) < 3:
+        raise TypeError(
+            "usage: gpioset.py <gpiochip> <offset1>=<value1> <offset2>=<value2> ..."
+        )
+
+    path = sys.argv[1]
+    values = dict()
+    lines = []
+    for arg in sys.argv[2:]:
+        arg = arg.split("=")
+        key = int(arg[0]) if arg[0].isdigit() else arg[0]
+        val = int(arg[1])
+
+        lines.append(key)
+        values[key] = Value(val)
+
+    request = gpiod.request_lines(
+        path,
+        consumer="gpioset.py",
+        config={tuple(lines): gpiod.LineSettings(direction=Direction.OUTPUT)},
+    )
+
+    vals = request.set_values(values)
+
+    input()
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [libgpiod v2][PATCH v3 3/4] bindings: python: add tests
  2022-10-07 14:55 [libgpiod v2][PATCH v3 0/4] bindings: implement python bindings for libgpiod v2 Bartosz Golaszewski
  2022-10-07 14:55 ` [libgpiod v2][PATCH v3 1/4] bindings: python: remove old version Bartosz Golaszewski
  2022-10-07 14:55 ` [libgpiod v2][PATCH v3 2/4] bindings: python: add examples Bartosz Golaszewski
@ 2022-10-07 14:55 ` Bartosz Golaszewski
  2022-10-13  3:09   ` Kent Gibson
  2022-10-07 14:55 ` [libgpiod v2][PATCH v3 4/4] bindings: python: implement python bindings for libgpiod v2 Bartosz Golaszewski
  2022-10-12 12:34 ` [libgpiod v2][PATCH v3 0/4] bindings: " Bartosz Golaszewski
  4 siblings, 1 reply; 30+ messages in thread
From: Bartosz Golaszewski @ 2022-10-07 14:55 UTC (permalink / raw)
  To: Kent Gibson, Linus Walleij, Andy Shevchenko, Viresh Kumar
  Cc: linux-gpio, Bartosz Golaszewski

This adds a test-suite for python bindings based on the gpio-sim kernel
module.

Signed-off-by: Bartosz Golaszewski <brgl@bgdev.pl>
---
 bindings/python/tests/Makefile.am            |  17 +
 bindings/python/tests/__init__.py            |  17 +
 bindings/python/tests/__main__.py            |  16 +
 bindings/python/tests/gpiosim/Makefile.am    |   7 +
 bindings/python/tests/gpiosim/__init__.py    |   4 +
 bindings/python/tests/gpiosim/chip.py        |  66 +++
 bindings/python/tests/gpiosim/ext.c          | 345 ++++++++++++++
 bindings/python/tests/helpers.py             |  16 +
 bindings/python/tests/tests_chip.py          | 231 ++++++++++
 bindings/python/tests/tests_chip_info.py     |  52 +++
 bindings/python/tests/tests_edge_event.py    | 219 +++++++++
 bindings/python/tests/tests_info_event.py    | 189 ++++++++
 bindings/python/tests/tests_line_info.py     | 101 +++++
 bindings/python/tests/tests_line_request.py  | 449 +++++++++++++++++++
 bindings/python/tests/tests_line_settings.py |  79 ++++
 bindings/python/tests/tests_module.py        |  59 +++
 16 files changed, 1867 insertions(+)
 create mode 100644 bindings/python/tests/Makefile.am
 create mode 100644 bindings/python/tests/__init__.py
 create mode 100644 bindings/python/tests/__main__.py
 create mode 100644 bindings/python/tests/gpiosim/Makefile.am
 create mode 100644 bindings/python/tests/gpiosim/__init__.py
 create mode 100644 bindings/python/tests/gpiosim/chip.py
 create mode 100644 bindings/python/tests/gpiosim/ext.c
 create mode 100644 bindings/python/tests/helpers.py
 create mode 100644 bindings/python/tests/tests_chip.py
 create mode 100644 bindings/python/tests/tests_chip_info.py
 create mode 100644 bindings/python/tests/tests_edge_event.py
 create mode 100644 bindings/python/tests/tests_info_event.py
 create mode 100644 bindings/python/tests/tests_line_info.py
 create mode 100644 bindings/python/tests/tests_line_request.py
 create mode 100644 bindings/python/tests/tests_line_settings.py
 create mode 100644 bindings/python/tests/tests_module.py

diff --git a/bindings/python/tests/Makefile.am b/bindings/python/tests/Makefile.am
new file mode 100644
index 0000000..7dcdebb
--- /dev/null
+++ b/bindings/python/tests/Makefile.am
@@ -0,0 +1,17 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+SUBDIRS = gpiosim
+
+EXTRA_DIST = \
+	helpers.py \
+	__init__.py \
+	__main__.py \
+	tests_chip_info.py \
+	tests_chip.py \
+	tests_edge_event.py \
+	tests_info_event.py \
+	tests_line_info.py \
+	tests_line_request.py \
+	tests_line_settings.py \
+	tests_module.py
diff --git a/bindings/python/tests/__init__.py b/bindings/python/tests/__init__.py
new file mode 100644
index 0000000..2bf14e6
--- /dev/null
+++ b/bindings/python/tests/__init__.py
@@ -0,0 +1,17 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+import os
+import unittest
+
+from packaging import version
+
+required_kernel_version = "5.19.0"
+current_version = os.uname().release.split("-")[0]
+
+if version.parse(current_version) < version.parse(required_kernel_version):
+    raise NotImplementedError(
+        "linux kernel version must be at least {} - got {}".format(
+            required_kernel_version, current_version
+        )
+    )
diff --git a/bindings/python/tests/__main__.py b/bindings/python/tests/__main__.py
new file mode 100644
index 0000000..b5d7f0a
--- /dev/null
+++ b/bindings/python/tests/__main__.py
@@ -0,0 +1,16 @@
+#!/usr/bin/python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+import unittest
+
+from .tests_chip import *
+from .tests_chip_info import *
+from .tests_edge_event import *
+from .tests_info_event import *
+from .tests_line_info import *
+from .tests_line_settings import *
+from .tests_module import *
+from .tests_line_request import *
+
+unittest.main()
diff --git a/bindings/python/tests/gpiosim/Makefile.am b/bindings/python/tests/gpiosim/Makefile.am
new file mode 100644
index 0000000..7004f3a
--- /dev/null
+++ b/bindings/python/tests/gpiosim/Makefile.am
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+EXTRA_DIST = \
+	chip.py \
+	ext.c \
+	__init__.py
diff --git a/bindings/python/tests/gpiosim/__init__.py b/bindings/python/tests/gpiosim/__init__.py
new file mode 100644
index 0000000..f65e413
--- /dev/null
+++ b/bindings/python/tests/gpiosim/__init__.py
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+from .chip import Chip
diff --git a/bindings/python/tests/gpiosim/chip.py b/bindings/python/tests/gpiosim/chip.py
new file mode 100644
index 0000000..65c5af1
--- /dev/null
+++ b/bindings/python/tests/gpiosim/chip.py
@@ -0,0 +1,66 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+from . import _ext
+from enum import Enum
+from typing import Optional
+
+
+class Chip:
+    """
+    Represents a simulated GPIO chip.
+    """
+
+    class Pull(Enum):
+        DOWN = _ext.PULL_DOWN
+        UP = _ext.PULL_UP
+
+    class Value(Enum):
+        INACTIVE = _ext.VALUE_INACTIVE
+        ACTIVE = _ext.VALUE_ACTIVE
+
+    class Direction(Enum):
+        INPUT = _ext.DIRECTION_INPUT
+        OUTPUT_HIGH = _ext.DIRECTION_OUTPUT_HIGH
+        OUTPUT_LOW = _ext.DIRECTION_OUTPUT_LOW
+
+    def __init__(
+        self,
+        /,
+        label: Optional[str] = None,
+        num_lines: Optional[int] = None,
+        line_names: Optional[dict[int, str]] = None,
+        hogs: Optional[dict[int, tuple[str, Direction]]] = None,
+    ):
+        self._chip = _ext.Chip()
+
+        if label:
+            self._chip.set_label(label)
+
+        if num_lines:
+            self._chip.set_num_lines(num_lines)
+
+        if line_names:
+            for off, name in line_names.items():
+                self._chip.set_line_name(off, name)
+
+        if hogs:
+            for off, (name, direction) in hogs.items():
+                self._chip.set_hog(off, name, direction.value)
+
+        self._chip.enable()
+
+    def get_value(self, offset: int) -> Value:
+        val = self._chip.get_value(offset)
+        return Chip.Value(val)
+
+    def set_pull(self, offset: int, pull: Pull) -> None:
+        self._chip.set_pull(offset, pull.value)
+
+    @property
+    def dev_path(self) -> str:
+        return self._chip.dev_path
+
+    @property
+    def name(self) -> str:
+        return self._chip.name
diff --git a/bindings/python/tests/gpiosim/ext.c b/bindings/python/tests/gpiosim/ext.c
new file mode 100644
index 0000000..6671352
--- /dev/null
+++ b/bindings/python/tests/gpiosim/ext.c
@@ -0,0 +1,345 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <gpiosim.h>
+#include <Python.h>
+
+struct module_const {
+	const char *name;
+	long val;
+};
+
+static const struct module_const module_constants[] = {
+	{
+		.name = "PULL_DOWN",
+		.val = GPIOSIM_PULL_DOWN,
+	},
+	{
+		.name = "PULL_UP",
+		.val = GPIOSIM_PULL_UP,
+	},
+	{
+		.name = "VALUE_INACTIVE",
+		.val = GPIOSIM_VALUE_INACTIVE,
+	},
+	{
+		.name = "VALUE_ACTIVE",
+		.val = GPIOSIM_VALUE_ACTIVE,
+	},
+	{
+		.name = "DIRECTION_INPUT",
+		.val = GPIOSIM_HOG_DIR_INPUT,
+	},
+	{
+		.name = "DIRECTION_OUTPUT_HIGH",
+		.val = GPIOSIM_HOG_DIR_OUTPUT_HIGH,
+	},
+	{
+		.name = "DIRECTION_OUTPUT_LOW",
+		.val = GPIOSIM_HOG_DIR_OUTPUT_LOW,
+	},
+	{ }
+};
+
+struct module_state {
+	struct gpiosim_ctx *sim_ctx;
+};
+
+static void free_module_state(void *mod)
+{
+	struct module_state *state = PyModule_GetState((PyObject *)mod);
+
+	if (state->sim_ctx)
+		gpiosim_ctx_unref(state->sim_ctx);
+}
+
+static PyModuleDef module_def = {
+	PyModuleDef_HEAD_INIT,
+	.m_name = "gpiosim._ext",
+	.m_size = sizeof(struct module_state),
+	.m_free = free_module_state,
+};
+
+typedef struct {
+	PyObject_HEAD
+	struct gpiosim_dev *dev;
+	struct gpiosim_bank *bank;
+} chip_object;
+
+static int chip_init(chip_object *self,
+		     PyObject *Py_UNUSED(ignored0),
+		     PyObject *Py_UNUSED(ignored1))
+{
+	struct module_state *state;
+	PyObject *mod;
+
+	mod = PyState_FindModule(&module_def);
+	if (!mod)
+		return -1;
+
+	state = PyModule_GetState(mod);
+
+	self->dev = gpiosim_dev_new(state->sim_ctx);
+	if (!self->dev) {
+		PyErr_SetFromErrno(PyExc_OSError);
+		return -1;
+	}
+
+	self->bank = gpiosim_bank_new(self->dev);
+	if (!self->bank) {
+		PyErr_SetFromErrno(PyExc_OSError);
+		return -1;
+	}
+
+	return 0;
+}
+
+static void chip_finalize(chip_object *self)
+{
+	if (self->bank)
+		gpiosim_bank_unref(self->bank);
+
+	if (self->dev) {
+		if (gpiosim_dev_is_live(self->dev))
+			gpiosim_dev_disable(self->dev);
+
+		gpiosim_dev_unref(self->dev);
+	}
+}
+
+static void chip_dealloc(PyObject *self)
+{
+	int ret;
+
+	ret = PyObject_CallFinalizerFromDealloc(self);
+	if (ret < 0)
+		return;
+
+	PyObject_Del(self);
+}
+
+static PyObject *chip_dev_path(chip_object *self, void *Py_UNUSED(ignored))
+{
+	return PyUnicode_FromString(gpiosim_bank_get_dev_path(self->bank));
+}
+
+static PyObject *chip_name(chip_object *self, void *Py_UNUSED(ignored))
+{
+	return PyUnicode_FromString(gpiosim_bank_get_chip_name(self->bank));
+}
+
+static PyGetSetDef chip_getset[] = {
+	{
+		.name = "dev_path",
+		.get = (getter)chip_dev_path,
+	},
+	{
+		.name = "name",
+		.get = (getter)chip_name,
+	},
+	{ }
+};
+
+static PyObject *chip_set_label(chip_object *self, PyObject *args)
+{
+	const char *label;
+	int ret;
+
+	ret = PyArg_ParseTuple(args, "s", &label);
+	if (!ret)
+		return NULL;
+
+	ret = gpiosim_bank_set_label(self->bank, label);
+	if (ret)
+		return PyErr_SetFromErrno(PyExc_OSError);
+
+	Py_RETURN_NONE;
+}
+
+static PyObject *chip_set_num_lines(chip_object *self, PyObject *args)
+{
+	unsigned int num_lines;
+	int ret;
+
+	ret = PyArg_ParseTuple(args, "I", &num_lines);
+	if (!ret)
+		return NULL;
+
+	ret = gpiosim_bank_set_num_lines(self->bank, num_lines);
+	if (ret)
+		return PyErr_SetFromErrno(PyExc_OSError);
+
+	Py_RETURN_NONE;
+}
+
+static PyObject *chip_set_line_name(chip_object *self, PyObject *args)
+{
+	unsigned int offset;
+	const char *name;
+	int ret;
+
+	ret = PyArg_ParseTuple(args, "Is", &offset, &name);
+	if (!ret)
+		return NULL;
+
+	ret = gpiosim_bank_set_line_name(self->bank, offset, name);
+	if (ret)
+		return PyErr_SetFromErrno(PyExc_OSError);
+
+	Py_RETURN_NONE;
+}
+
+static PyObject *chip_set_hog(chip_object *self, PyObject *args)
+{
+	unsigned int offset;
+	const char *name;
+	int ret, dir;
+
+	ret = PyArg_ParseTuple(args, "Isi", &offset, &name, &dir);
+	if (!ret)
+		return NULL;
+
+	ret = gpiosim_bank_hog_line(self->bank, offset, name, dir);
+	if (ret)
+		return PyErr_SetFromErrno(PyExc_OSError);
+
+	Py_RETURN_NONE;
+}
+
+static PyObject *chip_enable(chip_object *self, PyObject *Py_UNUSED(args))
+{
+	int ret;
+
+	ret = gpiosim_dev_enable(self->dev);
+	if (ret)
+		return PyErr_SetFromErrno(PyExc_OSError);
+
+	Py_RETURN_NONE;
+}
+
+static PyObject *chip_get_value(chip_object *self, PyObject *args)
+{
+	unsigned int offset;
+	int ret, val;
+
+	ret = PyArg_ParseTuple(args, "I", &offset);
+	if (!ret)
+		return NULL;
+
+	val = gpiosim_bank_get_value(self->bank, offset);
+	if (val < 0)
+		return PyErr_SetFromErrno(PyExc_OSError);
+
+	return PyLong_FromLong(val);
+}
+
+static PyObject *chip_set_pull(chip_object *self, PyObject *args)
+{
+	unsigned int offset;
+	int ret, pull;
+
+	ret = PyArg_ParseTuple(args, "II", &offset, &pull);
+	if (!ret)
+		return NULL;
+
+	ret = gpiosim_bank_set_pull(self->bank, offset, pull);
+	if (ret)
+		return PyErr_SetFromErrno(PyExc_OSError);
+
+	Py_RETURN_NONE;
+}
+
+static PyMethodDef chip_methods[] = {
+	{
+		.ml_name = "set_label",
+		.ml_meth = (PyCFunction)chip_set_label,
+		.ml_flags = METH_VARARGS,
+	},
+	{
+		.ml_name = "set_num_lines",
+		.ml_meth = (PyCFunction)chip_set_num_lines,
+		.ml_flags = METH_VARARGS,
+	},
+	{
+		.ml_name = "set_line_name",
+		.ml_meth = (PyCFunction)chip_set_line_name,
+		.ml_flags = METH_VARARGS,
+	},
+	{
+		.ml_name = "set_hog",
+		.ml_meth = (PyCFunction)chip_set_hog,
+		.ml_flags = METH_VARARGS,
+	},
+	{
+		.ml_name = "enable",
+		.ml_meth = (PyCFunction)chip_enable,
+		.ml_flags = METH_NOARGS,
+	},
+	{
+		.ml_name = "get_value",
+		.ml_meth = (PyCFunction)chip_get_value,
+		.ml_flags = METH_VARARGS,
+	},
+	{
+		.ml_name = "set_pull",
+		.ml_meth = (PyCFunction)chip_set_pull,
+		.ml_flags = METH_VARARGS,
+	},
+	{ }
+};
+
+static PyTypeObject chip_type = {
+	PyVarObject_HEAD_INIT(NULL, 0)
+	.tp_name = "gpiosim.Chip",
+	.tp_basicsize = sizeof(chip_object),
+	.tp_flags = Py_TPFLAGS_DEFAULT,
+	.tp_new = PyType_GenericNew,
+	.tp_init = (initproc)chip_init,
+	.tp_finalize = (destructor)chip_finalize,
+	.tp_dealloc = (destructor)chip_dealloc,
+	.tp_methods = chip_methods,
+	.tp_getset = chip_getset,
+};
+
+PyMODINIT_FUNC PyInit__ext(void)
+{
+	const struct module_const *modconst;
+	struct module_state *state;
+	PyObject *module;
+	int ret;
+
+	module = PyModule_Create(&module_def);
+	if (!module)
+		return NULL;
+
+	ret = PyState_AddModule(module, &module_def);
+	if (ret) {
+		Py_DECREF(module);
+		return NULL;
+	}
+
+	state = PyModule_GetState(module);
+
+	state->sim_ctx = gpiosim_ctx_new();
+	if (!state->sim_ctx) {
+		Py_DECREF(module);
+		return PyErr_SetFromErrno(PyExc_OSError);
+	}
+
+	ret = PyModule_AddType(module, &chip_type);
+	if (ret) {
+		Py_DECREF(module);
+		return NULL;
+	}
+
+	for (modconst = module_constants; modconst->name; modconst++) {
+		ret = PyModule_AddIntConstant(module,
+					      modconst->name, modconst->val);
+		if (ret) {
+			Py_DECREF(module);
+			return NULL;
+		}
+ 	}
+
+	return module;
+}
diff --git a/bindings/python/tests/helpers.py b/bindings/python/tests/helpers.py
new file mode 100644
index 0000000..f9a15e8
--- /dev/null
+++ b/bindings/python/tests/helpers.py
@@ -0,0 +1,16 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+import os
+
+
+class LinkGuard:
+    def __init__(self, src, dst):
+        self.src = src
+        self.dst = dst
+
+    def __enter__(self):
+        os.symlink(self.src, self.dst)
+
+    def __exit__(self, type, val, tb):
+        os.unlink(self.dst)
diff --git a/bindings/python/tests/tests_chip.py b/bindings/python/tests/tests_chip.py
new file mode 100644
index 0000000..0d1effe
--- /dev/null
+++ b/bindings/python/tests/tests_chip.py
@@ -0,0 +1,231 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+import errno
+import gpiod
+import os
+
+from . import gpiosim
+from .helpers import LinkGuard
+from unittest import TestCase
+
+
+class ChipConstructor(TestCase):
+    def test_open_existing_chip(self):
+        sim = gpiosim.Chip()
+
+        with gpiod.Chip(sim.dev_path):
+            pass
+
+    def test_open_existing_chip_with_keyword(self):
+        sim = gpiosim.Chip()
+
+        with gpiod.Chip(path=sim.dev_path):
+            pass
+
+    def test_open_chip_by_link(self):
+        link = "/tmp/gpiod-py-test-link.{}".format(os.getpid())
+        sim = gpiosim.Chip()
+
+        with LinkGuard(sim.dev_path, link):
+            with gpiod.Chip(link):
+                pass
+
+    def test_open_nonexistent_chip(self):
+        with self.assertRaises(OSError) as ex:
+            gpiod.Chip("/dev/nonexistent")
+
+        self.assertEqual(ex.exception.errno, errno.ENOENT)
+
+    def test_open_not_a_character_device(self):
+        with self.assertRaises(OSError) as ex:
+            gpiod.Chip("/tmp")
+
+        self.assertEqual(ex.exception.errno, errno.ENOTTY)
+
+    def test_open_not_a_gpio_device(self):
+        with self.assertRaises(OSError) as ex:
+            gpiod.Chip("/dev/null")
+
+        self.assertEqual(ex.exception.errno, errno.ENODEV)
+
+    def test_missing_path(self):
+        with self.assertRaises(TypeError):
+            gpiod.Chip()
+
+    def test_invalid_type_for_path(self):
+        with self.assertRaises(TypeError):
+            gpiod.Chip(4)
+
+
+class ChipBooleanConversion(TestCase):
+    def test_chip_bool(self):
+        sim = gpiosim.Chip()
+        chip = gpiod.Chip(sim.dev_path)
+        self.assertTrue(chip)
+        chip.close()
+        self.assertFalse(chip)
+
+
+class ChipProperties(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip()
+        self.chip = gpiod.Chip(self.sim.dev_path)
+
+    def tearDown(self):
+        self.chip.close()
+        self.sim = None
+
+    def test_get_chip_path(self):
+        self.assertEqual(self.sim.dev_path, self.chip.path)
+
+    def test_get_fd(self):
+        self.assertGreaterEqual(self.chip.fd, 0)
+
+    def test_properties_are_immutable(self):
+        with self.assertRaises(AttributeError):
+            self.chip.path = "foobar"
+
+        with self.assertRaises(AttributeError):
+            self.chip.fd = 4
+
+
+class ChipDevPathFromLink(TestCase):
+    def test_dev_path_open_by_link(self):
+        sim = gpiosim.Chip()
+        link = "/tmp/gpiod-py-test-link.{}".format(os.getpid())
+
+        with LinkGuard(sim.dev_path, link):
+            with gpiod.Chip(link) as chip:
+                self.assertEqual(chip.path, link)
+
+
+class ChipMapLine(TestCase):
+    def test_lookup_by_name_good(self):
+        sim = gpiosim.Chip(
+            num_lines=8, line_names={1: "foo", 2: "bar", 4: "baz", 5: "xyz"}
+        )
+
+        with gpiod.Chip(sim.dev_path) as chip:
+            self.assertEqual(chip.map_line("baz"), 4)
+
+    def test_lookup_by_name_good_keyword_argument(self):
+        sim = gpiosim.Chip(
+            num_lines=8, line_names={1: "foo", 2: "bar", 4: "baz", 5: "xyz"}
+        )
+
+        with gpiod.Chip(sim.dev_path) as chip:
+            self.assertEqual(chip.map_line(id="baz"), 4)
+
+    def test_lookup_bad_name(self):
+        sim = gpiosim.Chip(
+            num_lines=8, line_names={1: "foo", 2: "bar", 4: "baz", 5: "xyz"}
+        )
+
+        with gpiod.Chip(sim.dev_path) as chip:
+            with self.assertRaises(FileNotFoundError):
+                chip.map_line("nonexistent")
+
+    def test_lookup_bad_offset(self):
+        sim = gpiosim.Chip()
+
+        with gpiod.Chip(sim.dev_path) as chip:
+            with self.assertRaises(ValueError):
+                chip.map_line(4)
+
+    def test_lookup_bad_offset_as_string(self):
+        sim = gpiosim.Chip()
+
+        with gpiod.Chip(sim.dev_path) as chip:
+            with self.assertRaises(ValueError):
+                chip.map_line("4")
+
+    def test_duplicate_names(self):
+        sim = gpiosim.Chip(
+            num_lines=8, line_names={1: "foo", 2: "bar", 4: "baz", 5: "bar"}
+        )
+
+        with gpiod.Chip(sim.dev_path) as chip:
+            self.assertEqual(chip.map_line("bar"), 2)
+
+    def test_integer_offsets(self):
+        sim = gpiosim.Chip(num_lines=8, line_names={1: "foo", 2: "bar", 6: "baz"})
+
+        with gpiod.Chip(sim.dev_path) as chip:
+            self.assertEqual(chip.map_line(4), 4)
+            self.assertEqual(chip.map_line(1), 1)
+
+    def test_offsets_as_string(self):
+        sim = gpiosim.Chip(num_lines=8, line_names={1: "foo", 2: "bar", 7: "6"})
+
+        with gpiod.Chip(sim.dev_path) as chip:
+            self.assertEqual(chip.map_line("2"), 2)
+            self.assertEqual(chip.map_line("6"), 7)
+
+
+class ClosedChipCannotBeUsed(TestCase):
+    def test_close_chip_and_try_to_use_it(self):
+        sim = gpiosim.Chip(label="foobar")
+
+        chip = gpiod.Chip(sim.dev_path)
+        chip.close()
+
+        with self.assertRaises(gpiod.ChipClosedError):
+            chip.path
+
+    def test_close_chip_and_try_controlled_execution(self):
+        sim = gpiosim.Chip()
+
+        chip = gpiod.Chip(sim.dev_path)
+        chip.close()
+
+        with self.assertRaises(gpiod.ChipClosedError):
+            with chip:
+                chip.fd
+
+    def test_close_chip_twice(self):
+        sim = gpiosim.Chip(label="foobar")
+        chip = gpiod.Chip(sim.dev_path)
+        chip.close()
+
+        with self.assertRaises(gpiod.ChipClosedError):
+            chip.close()
+
+
+class StringRepresentation(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=4, label="foobar")
+        self.chip = gpiod.Chip(self.sim.dev_path)
+
+    def tearDown(self):
+        self.chip.close()
+        self.sim = None
+
+    def test_repr(self):
+        self.assertEqual(repr(self.chip), 'Chip("{}")'.format(self.sim.dev_path))
+
+    def test_str(self):
+        info = self.chip.get_info()
+        self.assertEqual(
+            str(self.chip),
+            '<Chip path="{}" fd={} info=<ChipInfo name="{}" label="foobar" num_lines=4>>'.format(
+                self.sim.dev_path, self.chip.fd, info.name
+            ),
+        )
+
+
+class StringRepresentationClosed(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=4, label="foobar")
+        self.chip = gpiod.Chip(self.sim.dev_path)
+
+    def tearDown(self):
+        self.sim = None
+
+    def test_repr_closed(self):
+        self.chip.close()
+        self.assertEqual(repr(self.chip), "<Chip CLOSED>")
+
+    def test_str_closed(self):
+        self.chip.close()
+        self.assertEqual(str(self.chip), "<Chip CLOSED>")
diff --git a/bindings/python/tests/tests_chip_info.py b/bindings/python/tests/tests_chip_info.py
new file mode 100644
index 0000000..d392ec3
--- /dev/null
+++ b/bindings/python/tests/tests_chip_info.py
@@ -0,0 +1,52 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+import gpiod
+
+from . import gpiosim
+from unittest import TestCase
+
+
+class ChipInfoProperties(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(label="foobar", num_lines=16)
+        self.chip = gpiod.Chip(self.sim.dev_path)
+        self.info = self.chip.get_info()
+
+    def tearDown(self):
+        self.info = None
+        self.chip.close()
+        self.chip = None
+        self.sim = None
+
+    def test_chip_info_name(self):
+        self.assertEqual(self.info.name, self.sim.name)
+
+    def test_chip_info_label(self):
+        self.assertEqual(self.info.label, "foobar")
+
+    def test_chip_info_num_lines(self):
+        self.assertEqual(self.info.num_lines, 16)
+
+    def test_chip_info_properties_are_immutable(self):
+        with self.assertRaises(AttributeError):
+            self.info.name = "foobar"
+
+        with self.assertRaises(AttributeError):
+            self.info.num_lines = 4
+
+        with self.assertRaises(AttributeError):
+            self.info.label = "foobar"
+
+
+class ChipInfoStringRepresentation(TestCase):
+    def test_chip_info_str(self):
+        sim = gpiosim.Chip(label="foobar", num_lines=16)
+
+        with gpiod.Chip(sim.dev_path) as chip:
+            info = chip.get_info()
+
+            self.assertEqual(
+                str(info),
+                '<ChipInfo name="{}" label="foobar" num_lines=16>'.format(sim.name),
+            )
diff --git a/bindings/python/tests/tests_edge_event.py b/bindings/python/tests/tests_edge_event.py
new file mode 100644
index 0000000..8d52fdd
--- /dev/null
+++ b/bindings/python/tests/tests_edge_event.py
@@ -0,0 +1,219 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+import gpiod
+import time
+
+from . import gpiosim
+from datetime import timedelta
+from functools import partial
+from gpiod.line import Direction, Edge
+from threading import Thread
+from unittest import TestCase
+
+EventType = gpiod.EdgeEvent.Type
+Pull = gpiosim.Chip.Pull
+
+
+class EdgeEventWaitTimeout(TestCase):
+    def test_event_wait_timeout(self):
+        sim = gpiosim.Chip()
+
+        with gpiod.request_lines(
+            sim.dev_path,
+            {0: gpiod.LineSettings(edge_detection=Edge.BOTH)},
+        ) as req:
+            self.assertEqual(req.wait_edge_event(timedelta(microseconds=10000)), False)
+
+    def test_event_wait_timeout_float(self):
+        sim = gpiosim.Chip()
+
+        with gpiod.request_lines(
+            sim.dev_path,
+            {0: gpiod.LineSettings(edge_detection=Edge.BOTH)},
+        ) as req:
+            self.assertEqual(req.wait_edge_event(0.01), False)
+
+
+class EdgeEventInvalidConfig(TestCase):
+    def test_output_mode_and_edge_detection(self):
+        sim = gpiosim.Chip()
+
+        with self.assertRaises(ValueError):
+            gpiod.request_lines(
+                sim.dev_path,
+                {
+                    0: gpiod.LineSettings(
+                        direction=Direction.OUTPUT, edge_detection=Edge.BOTH
+                    )
+                },
+            )
+
+
+class WaitingForEdgeEvents(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=8)
+        self.thread = None
+
+    def tearDown(self):
+        if self.thread:
+            self.thread.join()
+            del self.thread
+        self.sim = None
+
+    def trigger_falling_and_rising_edge(self, offset):
+        time.sleep(0.05)
+        self.sim.set_pull(offset, Pull.UP)
+        time.sleep(0.05)
+        self.sim.set_pull(offset, Pull.DOWN)
+
+    def trigger_rising_edge_events_on_two_offsets(self, offset0, offset1):
+        time.sleep(0.05)
+        self.sim.set_pull(offset0, Pull.UP)
+        time.sleep(0.05)
+        self.sim.set_pull(offset1, Pull.UP)
+
+    def test_both_edge_events(self):
+        with gpiod.request_lines(
+            self.sim.dev_path, {2: gpiod.LineSettings(edge_detection=Edge.BOTH)}
+        ) as req:
+            self.thread = Thread(
+                target=partial(self.trigger_falling_and_rising_edge, 2)
+            )
+            self.thread.start()
+
+            self.assertTrue(req.wait_edge_event(timedelta(seconds=1)))
+            events = req.read_edge_event()
+            self.assertEqual(len(events), 1)
+            event = events[0]
+            self.assertEqual(event.event_type, EventType.RISING_EDGE)
+            self.assertEqual(event.line_offset, 2)
+            ts_rising = event.timestamp_ns
+
+            self.assertTrue(req.wait_edge_event(timedelta(seconds=1)))
+            events = req.read_edge_event()
+            self.assertEqual(len(events), 1)
+            event = events[0]
+            self.assertEqual(event.event_type, EventType.FALLING_EDGE)
+            self.assertEqual(event.line_offset, 2)
+            ts_falling = event.timestamp_ns
+
+            self.assertGreater(ts_falling, ts_rising)
+
+    def test_rising_edge_event(self):
+        with gpiod.request_lines(
+            self.sim.dev_path, {6: gpiod.LineSettings(edge_detection=Edge.RISING)}
+        ) as req:
+            self.thread = Thread(
+                target=partial(self.trigger_falling_and_rising_edge, 6)
+            )
+            self.thread.start()
+
+            self.assertTrue(req.wait_edge_event(timedelta(seconds=1)))
+            events = req.read_edge_event()
+            self.assertEqual(len(events), 1)
+            event = events[0]
+            self.assertEqual(event.event_type, EventType.RISING_EDGE)
+            self.assertEqual(event.line_offset, 6)
+
+            self.assertFalse(req.wait_edge_event(timedelta(microseconds=10000)))
+
+    def test_rising_edge_event(self):
+        with gpiod.request_lines(
+            self.sim.dev_path, {6: gpiod.LineSettings(edge_detection=Edge.FALLING)}
+        ) as req:
+            self.thread = Thread(
+                target=partial(self.trigger_falling_and_rising_edge, 6)
+            )
+            self.thread.start()
+
+            self.assertTrue(req.wait_edge_event(timedelta(seconds=1)))
+            events = req.read_edge_event()
+            self.assertEqual(len(events), 1)
+            event = events[0]
+            self.assertEqual(event.event_type, EventType.FALLING_EDGE)
+            self.assertEqual(event.line_offset, 6)
+
+            self.assertFalse(req.wait_edge_event(timedelta(microseconds=10000)))
+
+    def test_sequence_numbers(self):
+        with gpiod.request_lines(
+            self.sim.dev_path, {(2, 4): gpiod.LineSettings(edge_detection=Edge.BOTH)}
+        ) as req:
+            self.thread = Thread(
+                target=partial(self.trigger_rising_edge_events_on_two_offsets, 2, 4)
+            )
+            self.thread.start()
+
+            self.assertTrue(req.wait_edge_event(timedelta(seconds=1)))
+            events = req.read_edge_event()
+            self.assertEqual(len(events), 1)
+            event = events[0]
+            self.assertEqual(event.event_type, EventType.RISING_EDGE)
+            self.assertEqual(event.line_offset, 2)
+            self.assertEqual(event.global_seqno, 1)
+            self.assertEqual(event.line_seqno, 1)
+
+            self.assertTrue(req.wait_edge_event(timedelta(seconds=1)))
+            events = req.read_edge_event()
+            self.assertEqual(len(events), 1)
+            event = events[0]
+            self.assertEqual(event.event_type, EventType.RISING_EDGE)
+            self.assertEqual(event.line_offset, 4)
+            self.assertEqual(event.global_seqno, 2)
+            self.assertEqual(event.line_seqno, 1)
+
+
+class ReadingMultipleEdgeEvents(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=8)
+        self.request = gpiod.request_lines(
+            self.sim.dev_path, {1: gpiod.LineSettings(edge_detection=Edge.BOTH)}
+        )
+        self.line_seqno = 1
+        self.global_seqno = 1
+        self.sim.set_pull(1, Pull.UP)
+        time.sleep(0.05)
+        self.sim.set_pull(1, Pull.DOWN)
+        time.sleep(0.05)
+        self.sim.set_pull(1, Pull.UP)
+        time.sleep(0.05)
+
+    def tearDown(self):
+        self.request.release()
+        del self.request
+        del self.sim
+
+    def test_read_multiple_events(self):
+        self.assertTrue(self.request.wait_edge_event(timedelta(seconds=1)))
+        events = self.request.read_edge_event()
+        self.assertEqual(len(events), 3)
+
+        for event in events:
+            self.assertEqual(event.line_offset, 1)
+            self.assertEqual(event.line_seqno, self.line_seqno)
+            self.assertEqual(event.global_seqno, self.global_seqno)
+            self.line_seqno += 1
+            self.global_seqno += 1
+
+    # TODO buffer capacity
+    # def test_read_over_buffer_capacity(self):
+    #     buf = gpiod.EdgeEventBuffer(2)
+    #     self.assertTrue(self.request.wait_edge_event(datetime.timedelta(seconds=1)))
+    #     self.assertEqual(self.request.read_edge_event(buf), 2)
+    #     self.assertEqual(len(buf), 2)
+
+
+class EdgeEventStringRepresentation(TestCase):
+    def test_edge_event_str(self):
+        sim = gpiosim.Chip()
+
+        with gpiod.request_lines(
+            path=sim.dev_path, config={0: gpiod.LineSettings(edge_detection=Edge.BOTH)}
+        ) as req:
+            sim.set_pull(0, Pull.UP)
+            event = req.read_edge_event()[0]
+            self.assertRegex(
+                str(event),
+                "<EdgeEvent type=Type\.RISING_EDGE timestamp_ns=[0-9]+ line_offset=0 global_seqno=1 line_seqno=1>",
+            )
diff --git a/bindings/python/tests/tests_info_event.py b/bindings/python/tests/tests_info_event.py
new file mode 100644
index 0000000..f3926d9
--- /dev/null
+++ b/bindings/python/tests/tests_info_event.py
@@ -0,0 +1,189 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+import datetime
+import errno
+import gpiod
+import threading
+import time
+import unittest
+
+from . import gpiosim
+from dataclasses import FrozenInstanceError
+from functools import partial
+from gpiod.line import Direction
+from unittest import TestCase
+
+EventType = gpiod.InfoEvent.Type
+
+
+class InfoEventDataclassBehavior(TestCase):
+    def test_info_event_props_are_frozen(self):
+        sim = gpiosim.Chip()
+
+        with gpiod.Chip(sim.dev_path) as chip:
+            chip.watch_line_info(0)
+            with chip.request_lines(config={0: None}) as request:
+                self.assertTrue(chip.wait_info_event(datetime.timedelta(seconds=1)))
+                event = chip.read_info_event()
+
+                with self.assertRaises(FrozenInstanceError):
+                    event.event_type = 4
+
+                with self.assertRaises(FrozenInstanceError):
+                    event.timestamp_ns = 4
+
+                with self.assertRaises(FrozenInstanceError):
+                    event.line_info = 4
+
+
+def request_reconfigure_release_line(chip, offset):
+    time.sleep(0.1)
+    with chip.request_lines(config={offset: None}) as request:
+        time.sleep(0.1)
+        request.reconfigure_lines(
+            config={offset: gpiod.LineSettings(direction=Direction.OUTPUT)}
+        )
+        time.sleep(0.1)
+
+
+class WatchingInfoEventWorks(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=8, line_names={4: "foobar"})
+        self.chip = gpiod.Chip(self.sim.dev_path)
+        self.thread = None
+
+    def tearDown(self):
+        if self.thread:
+            self.thread.join()
+            self.thread = None
+
+        self.chip.close()
+        self.chip = None
+        self.sim = None
+
+    def test_watch_line_info_returns_line_info(self):
+        info = self.chip.watch_line_info(7)
+        self.assertEqual(info.offset, 7)
+
+    def test_watch_line_info_keyword_argument(self):
+        info = self.chip.watch_line_info(line=7)
+
+    def test_watch_line_info_offset_out_of_range(self):
+        with self.assertRaises(ValueError):
+            self.chip.watch_line_info(8)
+
+    def test_watch_line_info_no_arguments(self):
+        with self.assertRaises(TypeError):
+            self.chip.watch_line_info()
+
+    def test_watch_line_info_by_line_name(self):
+        self.chip.watch_line_info("foobar")
+
+    def test_watch_line_info_invalid_argument_type(self):
+        with self.assertRaises(TypeError):
+            self.chip.watch_line_info(None)
+
+    def test_wait_for_event_timeout(self):
+        info = self.chip.watch_line_info(7)
+        self.assertFalse(
+            self.chip.wait_info_event(datetime.timedelta(microseconds=10000))
+        )
+
+    def test_request_reconfigure_release_events(self):
+        info = self.chip.watch_line_info(7)
+        self.assertEqual(info.direction, Direction.INPUT)
+
+        self.thread = threading.Thread(
+            target=partial(request_reconfigure_release_line, self.chip, 7)
+        )
+        self.thread.start()
+
+        self.assertTrue(self.chip.wait_info_event(datetime.timedelta(seconds=1)))
+        event = self.chip.read_info_event()
+        self.assertEqual(event.event_type, EventType.LINE_REQUESTED)
+        self.assertEqual(event.line_info.offset, 7)
+        self.assertEqual(event.line_info.direction, Direction.INPUT)
+        ts_req = event.timestamp_ns
+
+        # Check that we can use a float directly instead of datetime.timedelta.
+        self.assertTrue(self.chip.wait_info_event(1.0))
+        event = self.chip.read_info_event()
+        self.assertEqual(event.event_type, EventType.LINE_CONFIG_CHANGED)
+        self.assertEqual(event.line_info.offset, 7)
+        self.assertEqual(event.line_info.direction, Direction.OUTPUT)
+        ts_rec = event.timestamp_ns
+
+        self.assertTrue(self.chip.wait_info_event(datetime.timedelta(seconds=1)))
+        event = self.chip.read_info_event()
+        self.assertEqual(event.event_type, EventType.LINE_RELEASED)
+        self.assertEqual(event.line_info.offset, 7)
+        self.assertEqual(event.line_info.direction, Direction.OUTPUT)
+        ts_rel = event.timestamp_ns
+
+        # No more events.
+        self.assertFalse(
+            self.chip.wait_info_event(datetime.timedelta(microseconds=10000))
+        )
+
+        # Check timestamps are really monotonic.
+        self.assertGreater(ts_rel, ts_rec)
+        self.assertGreater(ts_rec, ts_req)
+
+
+class UnwatchingLineInfo(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=8, line_names={4: "foobar"})
+        self.chip = gpiod.Chip(self.sim.dev_path)
+
+    def tearDown(self):
+        self.chip.close()
+        self.chip = None
+        self.sim = None
+
+    def test_unwatch_line_info(self):
+        self.chip.watch_line_info(0)
+        with self.chip.request_lines(config={0: None}) as request:
+            self.assertTrue(self.chip.wait_info_event(datetime.timedelta(seconds=1)))
+            event = self.chip.read_info_event()
+            self.assertEqual(event.event_type, EventType.LINE_REQUESTED)
+            self.chip.unwatch_line_info(0)
+
+        self.assertFalse(
+            self.chip.wait_info_event(datetime.timedelta(microseconds=10000))
+        )
+
+    def test_unwatch_not_watched_line(self):
+        with self.assertRaises(OSError) as ex:
+            self.chip.unwatch_line_info(2)
+
+        self.assertEqual(ex.exception.errno, errno.EBUSY)
+
+    def test_unwatch_line_info_no_argument(self):
+        with self.assertRaises(TypeError):
+            self.chip.unwatch_line_info()
+
+    def test_unwatch_line_info_by_line_name(self):
+        self.chip.watch_line_info(4)
+        with self.chip.request_lines(config={4: None}) as request:
+            self.assertIsNotNone(self.chip.read_info_event())
+            self.chip.unwatch_line_info("foobar")
+
+        self.assertFalse(
+            self.chip.wait_info_event(datetime.timedelta(microseconds=10000))
+        )
+
+
+class InfoEventStringRepresentation(TestCase):
+    def test_info_event_str(self):
+        sim = gpiosim.Chip()
+
+        with gpiod.Chip(sim.dev_path) as chip:
+            chip.watch_line_info(0)
+            with chip.request_lines(config={0: None}) as request:
+                self.assertTrue(chip.wait_info_event(datetime.timedelta(seconds=1)))
+                event = chip.read_info_event()
+                self.assertRegex(
+                    str(event),
+                    '<InfoEvent type=Type\.LINE_REQUESTED timestamp_ns=[0-9]+ line_info=<LineInfo offset=0 name="None" used=True consumer="\?" direction=Direction\.INPUT active_low=False bias=Bias\.UNKNOWN drive=Drive\.PUSH_PULL edge_detection=Edge\.NONE event_clock=Clock\.MONOTONIC debounced=False debounce_period=0:00:00>>',
+                )
diff --git a/bindings/python/tests/tests_line_info.py b/bindings/python/tests/tests_line_info.py
new file mode 100644
index 0000000..2779e7a
--- /dev/null
+++ b/bindings/python/tests/tests_line_info.py
@@ -0,0 +1,101 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+import errno
+import gpiod
+import unittest
+
+from . import gpiosim
+from gpiod.line import Direction, Bias, Drive, Clock
+
+HogDir = gpiosim.Chip.Direction
+
+
+class GetLineInfo(unittest.TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(
+            num_lines=4,
+            line_names={0: "foobar"},
+        )
+
+        self.chip = gpiod.Chip(self.sim.dev_path)
+
+    def tearDown(self):
+        self.chip.close()
+        self.chip = None
+        self.sim = None
+
+    def test_get_line_info_by_offset(self):
+        self.chip.get_line_info(0)
+
+    def test_get_line_info_by_offset_keyword(self):
+        self.chip.get_line_info(line=0)
+
+    def test_get_line_info_by_name(self):
+        self.chip.get_line_info("foobar")
+
+    def test_get_line_info_by_name_keyword(self):
+        self.chip.get_line_info(line="foobar")
+
+    def test_get_line_info_by_offset_string(self):
+        self.chip.get_line_info("2")
+
+    def test_offset_out_of_range(self):
+        with self.assertRaises(ValueError) as ex:
+            self.chip.get_line_info(4)
+
+    def test_no_offset(self):
+        with self.assertRaises(TypeError):
+            self.chip.get_line_info()
+
+
+class LinePropertiesCanBeRead(unittest.TestCase):
+    def test_basic_properties(self):
+        sim = gpiosim.Chip(
+            num_lines=8,
+            line_names={1: "foo", 2: "bar", 4: "baz", 5: "xyz"},
+            hogs={3: ("hog3", HogDir.OUTPUT_HIGH), 4: ("hog4", HogDir.OUTPUT_LOW)},
+        )
+
+        with gpiod.Chip(sim.dev_path) as chip:
+            info4 = chip.get_line_info(4)
+            info6 = chip.get_line_info(6)
+
+            self.assertEqual(info4.offset, 4)
+            self.assertEqual(info4.name, "baz")
+            self.assertTrue(info4.used)
+            self.assertEqual(info4.consumer, "hog4")
+            self.assertEqual(info4.direction, Direction.OUTPUT)
+            self.assertFalse(info4.active_low)
+            self.assertEqual(info4.bias, Bias.UNKNOWN)
+            self.assertEqual(info4.drive, Drive.PUSH_PULL)
+            self.assertEqual(info4.event_clock, Clock.MONOTONIC)
+            self.assertFalse(info4.debounced)
+            self.assertEqual(info4.debounce_period.total_seconds(), 0.0)
+
+            self.assertEqual(info6.offset, 6)
+            self.assertEqual(info6.name, None)
+            self.assertFalse(info6.used)
+            self.assertEqual(info6.consumer, None)
+            self.assertEqual(info6.direction, Direction.INPUT)
+            self.assertFalse(info6.active_low)
+            self.assertEqual(info6.bias, Bias.UNKNOWN)
+            self.assertEqual(info6.drive, Drive.PUSH_PULL)
+            self.assertEqual(info6.event_clock, Clock.MONOTONIC)
+            self.assertFalse(info6.debounced)
+            self.assertEqual(info6.debounce_period.total_seconds(), 0.0)
+
+
+class LineInfoStringRepresentation(unittest.TestCase):
+    def test_line_info_str(self):
+        sim = gpiosim.Chip(
+            line_names={0: "foo"}, hogs={0: ("hogger", HogDir.OUTPUT_HIGH)}
+        )
+
+        with gpiod.Chip(sim.dev_path) as chip:
+            info = chip.get_line_info(0)
+
+            self.assertEqual(
+                str(info),
+                '<LineInfo offset=0 name="foo" used=True consumer="hogger" direction=Direction.OUTPUT active_low=False bias=Bias.UNKNOWN drive=Drive.PUSH_PULL edge_detection=Edge.NONE event_clock=Clock.MONOTONIC debounced=False debounce_period=0:00:00>',
+            )
diff --git a/bindings/python/tests/tests_line_request.py b/bindings/python/tests/tests_line_request.py
new file mode 100644
index 0000000..045ace6
--- /dev/null
+++ b/bindings/python/tests/tests_line_request.py
@@ -0,0 +1,449 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+import errno
+import gpiod
+
+from . import gpiosim
+from gpiod.line import Direction, Edge, Value
+from unittest import TestCase
+
+Pull = gpiosim.Chip.Pull
+SimVal = gpiosim.Chip.Value
+
+
+class ChipLineRequestsBehaveCorrectlyWithInvalidArguments(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=8)
+        self.chip = gpiod.Chip(self.sim.dev_path)
+
+    def tearDown(self):
+        self.chip.close()
+        del self.chip
+        del self.sim
+
+    def test_passing_invalid_types_as_configs(self):
+        with self.assertRaises(AttributeError):
+            self.chip.request_lines("foobar")
+
+        with self.assertRaises(AttributeError):
+            self.chip.request_lines(None, "foobar")
+
+    def test_duplicate_offsets(self):
+        with self.chip.request_lines(config={(2, 5, 1, 7, 5): None}) as req:
+            self.assertEqual(req.offsets, [2, 5, 1, 7])
+
+    def test_offset_out_of_range(self):
+        with self.assertRaises(ValueError):
+            self.chip.request_lines(config={(1, 0, 4, 8): None})
+
+    def test_line_name_not_found(self):
+        with self.assertRaises(FileNotFoundError):
+            self.chip.request_lines(config={"foo": None})
+
+    def test_request_no_arguments(self):
+        with self.assertRaises(TypeError):
+            self.chip.request_lines()
+
+
+class ModuleLineRequestsBehaveCorrectlyWithInvalidArguments(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=8)
+
+    def tearDown(self):
+        del self.sim
+
+    def test_passing_invalid_types_as_configs(self):
+        with self.assertRaises(AttributeError):
+            gpiod.request_lines(self.sim.dev_path, "foobar")
+
+        with self.assertRaises(AttributeError):
+            gpiod.request_lines(self.sim.dev_path, None, "foobar")
+
+    def test_duplicate_offsets(self):
+        with gpiod.request_lines(
+            self.sim.dev_path, config={(2, 5, 1, 7, 5): None}
+        ) as req:
+            self.assertEqual(req.offsets, [2, 5, 1, 7])
+
+    def test_offset_out_of_range(self):
+        with self.assertRaises(ValueError):
+            gpiod.request_lines(self.sim.dev_path, config={(1, 0, 4, 8): None})
+
+    def test_line_name_not_found(self):
+        with self.assertRaises(FileNotFoundError):
+            gpiod.request_lines(self.sim.dev_path, config={"foo": None})
+
+    def test_request_no_arguments(self):
+        with self.assertRaises(TypeError):
+            gpiod.request_lines()
+
+
+class ChipLineRequestWorks(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=8, line_names={5: "foo", 7: "bar"})
+        self.chip = gpiod.Chip(self.sim.dev_path)
+
+    def tearDown(self):
+        self.chip.close()
+        del self.chip
+        del self.sim
+
+    def test_request_with_positional_arguments(self):
+        with self.chip.request_lines({(0, 5, 3, 1): None}, "foobar", 32) as req:
+            self.assertEqual(req.offsets, [0, 5, 3, 1])
+            self.assertEqual(self.chip.get_line_info(0).consumer, "foobar")
+
+    def test_request_with_keyword_arguments(self):
+        with self.chip.request_lines(
+            config={(0, 5, 6): None},
+            consumer="foobar",
+            event_buffer_size=16,
+        ) as req:
+            self.assertEqual(req.offsets, [0, 5, 6])
+            self.assertEqual(self.chip.get_line_info(0).consumer, "foobar")
+
+    def test_request_single_offset_as_int(self):
+        with self.chip.request_lines(config={4: None}) as req:
+            self.assertEqual(req.offsets, [4])
+
+    def test_request_single_offset_as_tuple(self):
+        with self.chip.request_lines(config={(4): None}) as req:
+            self.assertEqual(req.offsets, [4])
+
+    def test_request_by_name(self):
+        with self.chip.request_lines(config={(1, 2, "foo", "bar"): None}) as req:
+            self.assertEqual(req.offsets, [1, 2, 5, 7])
+
+    def test_request_single_line_by_name(self):
+        with self.chip.request_lines(config={"foo": None}) as req:
+            self.assertEqual(req.offsets, [5])
+
+
+class ModuleLineRequestWorks(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=8, line_names={5: "foo", 7: "bar"})
+
+    def tearDown(self):
+        del self.sim
+
+    def test_request_with_positional_arguments(self):
+        with gpiod.request_lines(
+            self.sim.dev_path, {(0, 5, 3, 1): None}, "foobar", 32
+        ) as req:
+            self.assertEqual(req.offsets, [0, 5, 3, 1])
+            with gpiod.Chip(self.sim.dev_path) as chip:
+                self.assertEqual(chip.get_line_info(5).consumer, "foobar")
+
+    def test_request_with_keyword_arguments(self):
+        with gpiod.request_lines(
+            path=self.sim.dev_path,
+            config={(0, 5, 6): None},
+            consumer="foobar",
+            event_buffer_size=16,
+        ) as req:
+            self.assertEqual(req.offsets, [0, 5, 6])
+            with gpiod.Chip(self.sim.dev_path) as chip:
+                self.assertEqual(chip.get_line_info(5).consumer, "foobar")
+
+    def test_request_single_offset_as_int(self):
+        with gpiod.request_lines(path=self.sim.dev_path, config={4: None}) as req:
+            self.assertEqual(req.offsets, [4])
+
+    def test_request_single_offset_as_tuple(self):
+        with gpiod.request_lines(path=self.sim.dev_path, config={(4): None}) as req:
+            self.assertEqual(req.offsets, [4])
+
+    def test_request_by_name(self):
+        with gpiod.request_lines(
+            self.sim.dev_path, {(1, 2, "foo", "bar"): None}
+        ) as req:
+            self.assertEqual(req.offsets, [1, 2, 5, 7])
+
+
+class LineRequestGettingValues(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=8)
+        self.req = gpiod.request_lines(
+            self.sim.dev_path,
+            {(0, 1, 2, 3): gpiod.LineSettings(direction=Direction.INPUT)},
+        )
+
+    def tearDown(self):
+        self.req.release()
+        del self.req
+        del self.sim
+
+    def test_get_single_value(self):
+        self.sim.set_pull(1, Pull.UP)
+
+        self.assertEqual(self.req.get_values([1]), [Value.ACTIVE])
+
+    def test_get_single_value_helper(self):
+        self.sim.set_pull(1, Pull.UP)
+
+        self.assertEqual(self.req.get_value(1), Value.ACTIVE)
+
+    def test_get_values_for_subset_of_lines(self):
+        self.sim.set_pull(0, Pull.UP)
+        self.sim.set_pull(1, Pull.DOWN)
+        self.sim.set_pull(3, Pull.UP)
+
+        self.assertEqual(
+            self.req.get_values([0, 1, 3]), [Value.ACTIVE, Value.INACTIVE, Value.ACTIVE]
+        )
+
+    def test_get_all_values(self):
+        self.sim.set_pull(0, Pull.DOWN)
+        self.sim.set_pull(1, Pull.UP)
+        self.sim.set_pull(2, Pull.UP)
+        self.sim.set_pull(3, Pull.UP)
+
+        self.assertEqual(
+            self.req.get_values(),
+            [Value.INACTIVE, Value.ACTIVE, Value.ACTIVE, Value.ACTIVE],
+        )
+
+    def test_get_values_invalid_offset(self):
+        with self.assertRaises(ValueError):
+            self.req.get_values([9])
+
+    def test_get_values_invalid_argument_type(self):
+        with self.assertRaises(TypeError):
+            self.req.get_values(True)
+
+
+class LineRequestGettingValuesByName(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=4, line_names={2: "foo", 3: "bar", 1: "baz"})
+        self.req = gpiod.request_lines(
+            self.sim.dev_path,
+            {(0, "baz", "bar", "foo"): gpiod.LineSettings(direction=Direction.INPUT)},
+        )
+
+    def tearDown(self):
+        self.req.release()
+        del self.req
+        del self.sim
+
+    def test_get_values_by_name(self):
+        self.sim.set_pull(1, Pull.UP)
+        self.sim.set_pull(2, Pull.DOWN)
+        self.sim.set_pull(3, Pull.UP)
+
+        self.assertEqual(
+            self.req.get_values(["foo", "bar", 1]),
+            [Value.INACTIVE, Value.ACTIVE, Value.ACTIVE],
+        )
+
+    def test_get_values_by_bad_name(self):
+        with self.assertRaises(ValueError):
+            self.req.get_values(["xyz"])
+
+
+class LineRequestSettingValues(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=8)
+        self.req = gpiod.request_lines(
+            self.sim.dev_path,
+            {(0, 1, 2, 3): gpiod.LineSettings(direction=Direction.OUTPUT)},
+        )
+
+    def tearDown(self):
+        self.req.release()
+        del self.req
+        del self.sim
+
+    def test_set_single_value(self):
+        self.req.set_values({1: Value.ACTIVE})
+        self.assertEqual(self.sim.get_value(1), SimVal.ACTIVE)
+
+    def test_set_single_value_helper(self):
+        self.req.set_value(1, Value.ACTIVE)
+        self.assertEqual(self.sim.get_value(1), SimVal.ACTIVE)
+
+    def test_set_values_for_subset_of_lines(self):
+        self.req.set_values({0: Value.ACTIVE, 1: Value.INACTIVE, 3: Value.ACTIVE})
+
+        self.assertEqual(self.sim.get_value(0), SimVal.ACTIVE)
+        self.assertEqual(self.sim.get_value(1), SimVal.INACTIVE)
+        self.assertEqual(self.sim.get_value(3), SimVal.ACTIVE)
+
+    def test_set_values_invalid_offset(self):
+        with self.assertRaises(ValueError):
+            self.req.set_values({9: Value.ACTIVE})
+
+
+class LineRequestSettingValuesByName(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=4, line_names={2: "foo", 3: "bar", 1: "baz"})
+        self.req = gpiod.request_lines(
+            self.sim.dev_path,
+            {(0, "baz", "bar", "foo"): gpiod.LineSettings(direction=Direction.OUTPUT)},
+        )
+
+    def tearDown(self):
+        self.req.release()
+        del self.req
+        del self.sim
+
+    def test_set_values_by_name(self):
+        self.req.set_values(
+            {"foo": Value.INACTIVE, "bar": Value.ACTIVE, 1: Value.ACTIVE}
+        )
+
+        self.assertEqual(self.sim.get_value(2), SimVal.INACTIVE)
+        self.assertEqual(self.sim.get_value(1), SimVal.ACTIVE)
+        self.assertEqual(self.sim.get_value(3), SimVal.ACTIVE)
+
+    def test_set_values_by_bad_name(self):
+        with self.assertRaises(ValueError):
+            self.req.set_values({"xyz": Value.ACTIVE})
+
+
+class LineRequestPropertiesWork(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=16, line_names={0: "foo", 2: "bar", 5: "baz"})
+
+    def tearDown(self):
+        del self.sim
+
+    def test_property_fd(self):
+        with gpiod.request_lines(
+            self.sim.dev_path,
+            config={
+                0: gpiod.LineSettings(
+                    direction=Direction.INPUT, edge_detection=Edge.BOTH
+                )
+            },
+        ) as req:
+            self.assertGreaterEqual(req.fd, 0)
+
+    def test_property_num_lines(self):
+        with gpiod.request_lines(
+            self.sim.dev_path, config={(0, 2, 3, 5, 6, 8, 12): None}
+        ) as req:
+            self.assertEqual(req.num_lines, 7)
+
+    def test_property_offsets(self):
+        with gpiod.request_lines(
+            self.sim.dev_path, config={(1, 6, 12, 4): None}
+        ) as req:
+            self.assertEqual(req.offsets, [1, 6, 12, 4])
+
+    def test_property_lines(self):
+        with gpiod.request_lines(
+            self.sim.dev_path, config={("foo", 1, "bar", 4, "baz"): None}
+        ) as req:
+            self.assertEqual(req.lines, ["foo", 1, "bar", 4, "baz"])
+
+
+class LineRequestConsumerString(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=4)
+        self.chip = gpiod.Chip(self.sim.dev_path)
+
+    def tearDown(self):
+        self.chip.close()
+        del self.chip
+        del self.sim
+
+    def test_custom_consumer(self):
+        with self.chip.request_lines(
+            consumer="foobar", config={(2, 3): None}
+        ) as request:
+            info = self.chip.get_line_info(2)
+            self.assertEqual(info.consumer, "foobar")
+
+    def test_empty_consumer(self):
+        with self.chip.request_lines(consumer="", config={(2, 3): None}) as request:
+            info = self.chip.get_line_info(2)
+            self.assertEqual(info.consumer, "?")
+
+    def test_default_consumer(self):
+        with self.chip.request_lines(config={(2, 3): None}) as request:
+            info = self.chip.get_line_info(2)
+            self.assertEqual(info.consumer, "?")
+
+
+class ReconfigureRequestedLines(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=8, line_names={3: "foo", 4: "bar", 6: "baz"})
+        self.chip = gpiod.Chip(self.sim.dev_path)
+        self.req = self.chip.request_lines(
+            {(0, 2, "foo", "baz"): gpiod.LineSettings(direction=Direction.OUTPUT)}
+        )
+
+    def tearDown(self):
+        self.chip.close()
+        del self.chip
+        self.req.release()
+        del self.req
+        del self.sim
+
+    def test_reconfigure_by_offsets(self):
+        info = self.chip.get_line_info(2)
+        self.assertEqual(info.direction, Direction.OUTPUT)
+        self.req.reconfigure_lines(
+            {(0, 2, 3, 6): gpiod.LineSettings(direction=Direction.INPUT)}
+        )
+        info = self.chip.get_line_info(2)
+        self.assertEqual(info.direction, Direction.INPUT)
+
+    def test_reconfigure_by_names(self):
+        info = self.chip.get_line_info(2)
+        self.assertEqual(info.direction, Direction.OUTPUT)
+        self.req.reconfigure_lines(
+            {(0, 2, "foo", "baz"): gpiod.LineSettings(direction=Direction.INPUT)}
+        )
+        info = self.chip.get_line_info(2)
+        self.assertEqual(info.direction, Direction.INPUT)
+
+
+class ReleasedLineRequestCannotBeUsed(TestCase):
+    def test_using_released_line_request(self):
+        sim = gpiosim.Chip()
+
+        with gpiod.Chip(sim.dev_path) as chip:
+            req = chip.request_lines(config={0: None})
+            req.release()
+
+            with self.assertRaises(gpiod.RequestReleasedError):
+                req.fd
+
+
+class LineRequestSurvivesParentChip(TestCase):
+    def test_line_request_survives_parent_chip(self):
+        sim = gpiosim.Chip()
+
+        chip = gpiod.Chip(sim.dev_path)
+        try:
+            req = chip.request_lines(
+                config={0: gpiod.LineSettings(direction=Direction.INPUT)}
+            )
+        except:
+            chip.close()
+            raise
+
+        chip.close()
+        self.assertEqual(req.get_values([0]), [Value.INACTIVE])
+
+
+class LineRequestStringRepresentation(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=8)
+
+    def tearDown(self):
+        del self.sim
+
+    def test_str(self):
+        with gpiod.request_lines(self.sim.dev_path, config={(2, 6, 4, 1): None}) as req:
+            self.assertEqual(
+                str(req),
+                "<LineRequest num_lines=4 offsets=[2, 6, 4, 1] fd={}>".format(req.fd),
+            )
+
+    def test_str_released(self):
+        req = gpiod.request_lines(self.sim.dev_path, config={(2, 6, 4, 1): None})
+        req.release()
+        self.assertEqual(str(req), "<LineRequest RELEASED>")
diff --git a/bindings/python/tests/tests_line_settings.py b/bindings/python/tests/tests_line_settings.py
new file mode 100644
index 0000000..36dda6d
--- /dev/null
+++ b/bindings/python/tests/tests_line_settings.py
@@ -0,0 +1,79 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+import gpiod
+
+from . import gpiosim
+from datetime import timedelta
+from gpiod.line import Direction, Edge, Bias, Drive, Value, Clock
+from unittest import TestCase
+
+
+class LineSettingsConstructor(TestCase):
+    def test_default_values(self):
+        settings = gpiod.LineSettings()
+
+        self.assertEqual(settings.direction, Direction.AS_IS)
+        self.assertEqual(settings.edge_detection, Edge.NONE)
+        self.assertEqual(settings.bias, Bias.AS_IS)
+        self.assertEqual(settings.drive, Drive.PUSH_PULL)
+        self.assertFalse(settings.active_low)
+        self.assertEqual(settings.debounce_period.total_seconds(), 0.0)
+        self.assertEqual(settings.event_clock, Clock.MONOTONIC)
+        self.assertEqual(settings.output_value, Value.INACTIVE)
+
+    def test_keyword_arguments(self):
+        settings = gpiod.LineSettings(
+            direction=Direction.INPUT,
+            edge_detection=Edge.BOTH,
+            bias=Bias.PULL_UP,
+            event_clock=Clock.REALTIME,
+        )
+
+        self.assertEqual(settings.direction, Direction.INPUT)
+        self.assertEqual(settings.edge_detection, Edge.BOTH)
+        self.assertEqual(settings.bias, Bias.PULL_UP)
+        self.assertEqual(settings.drive, Drive.PUSH_PULL)
+        self.assertFalse(settings.active_low)
+        self.assertEqual(settings.debounce_period.total_seconds(), 0.0)
+        self.assertEqual(settings.event_clock, Clock.REALTIME)
+        self.assertEqual(settings.output_value, Value.INACTIVE)
+
+
+class LineSettingsAttributes(TestCase):
+    def test_line_settings_attributes_are_mutable(self):
+        settings = gpiod.LineSettings()
+
+        settings.direction = Direction.INPUT
+        settings.edge_detection = Edge.BOTH
+        settings.bias = Bias.DISABLED
+        settings.debounce_period = timedelta(microseconds=3000)
+        settings.event_clock = Clock.HTE
+
+        self.assertEqual(settings.direction, Direction.INPUT)
+        self.assertEqual(settings.edge_detection, Edge.BOTH)
+        self.assertEqual(settings.bias, Bias.DISABLED)
+        self.assertEqual(settings.drive, Drive.PUSH_PULL)
+        self.assertFalse(settings.active_low)
+        self.assertEqual(settings.debounce_period.total_seconds(), 0.003)
+        self.assertEqual(settings.event_clock, Clock.HTE)
+        self.assertEqual(settings.output_value, Value.INACTIVE)
+
+
+class LineSettingsStringRepresentation(TestCase):
+    def setUp(self):
+        self.settings = gpiod.LineSettings(
+            direction=Direction.OUTPUT, drive=Drive.OPEN_SOURCE, active_low=True
+        )
+
+    def test_repr(self):
+        self.assertEqual(
+            repr(self.settings),
+            "LineSettings(direction=Direction.OUTPUT, edge_detection=Edge.NONE bias=Bias.AS_IS drive=Drive.OPEN_SOURCE active_low=True debounce_period=datetime.timedelta(0) event_clock=Clock.MONOTONIC output_value=Value.INACTIVE)",
+        )
+
+    def test_str(self):
+        self.assertEqual(
+            str(self.settings),
+            "<LineSettings direction=Direction.OUTPUT edge_detection=Edge.NONE bias=Bias.AS_IS drive=Drive.OPEN_SOURCE active_low=True debounce_period=0:00:00 event_clock=Clock.MONOTONIC output_value=Value.INACTIVE>",
+        )
diff --git a/bindings/python/tests/tests_module.py b/bindings/python/tests/tests_module.py
new file mode 100644
index 0000000..4eeae76
--- /dev/null
+++ b/bindings/python/tests/tests_module.py
@@ -0,0 +1,59 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+import gpiod
+import os
+import re
+import unittest
+
+from . import gpiosim
+from .helpers import LinkGuard
+from unittest import TestCase
+
+
+class IsGPIOChip(TestCase):
+    def test_is_gpiochip_bad(self):
+        self.assertFalse(gpiod.is_gpiochip_device("/dev/null"))
+        self.assertFalse(gpiod.is_gpiochip_device("/dev/nonexistent"))
+
+    def test_is_gpiochip_invalid_argument(self):
+        with self.assertRaises(TypeError):
+            gpiod.is_gpiochip_device(4)
+
+    def test_is_gpiochip_superfluous_argument(self):
+        with self.assertRaises(TypeError):
+            gpiod.is_gpiochip_device("/dev/null", 4)
+
+    def test_is_gpiochip_missing_argument(self):
+        with self.assertRaises(TypeError):
+            gpiod.is_gpiochip_device()
+
+    def test_is_gpiochip_good(self):
+        sim = gpiosim.Chip()
+        self.assertTrue(gpiod.is_gpiochip_device(sim.dev_path))
+
+    def test_is_gpiochip_good_keyword_argument(self):
+        sim = gpiosim.Chip()
+        self.assertTrue(gpiod.is_gpiochip_device(path=sim.dev_path))
+
+    def test_is_gpiochip_link_good(self):
+        link = "/tmp/gpiod-py-test-link.{}".format(os.getpid())
+        sim = gpiosim.Chip()
+
+        with LinkGuard(sim.dev_path, link):
+            self.assertTrue(gpiod.is_gpiochip_device(link))
+
+    def test_is_gpiochip_link_bad(self):
+        link = "/tmp/gpiod-py-test-link.{}".format(os.getpid())
+
+        with LinkGuard("/dev/null", link):
+            self.assertFalse(gpiod.is_gpiochip_device(link))
+
+
+class VersionString(TestCase):
+    def test_version_string(self):
+        self.assertTrue(
+            re.match(
+                "^[0-9][1-9]?\\.[0-9][1-9]?([\\.0-9]?|\\-devel)$", gpiod.__version__
+            )
+        )
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [libgpiod v2][PATCH v3 4/4] bindings: python: implement python bindings for libgpiod v2
  2022-10-07 14:55 [libgpiod v2][PATCH v3 0/4] bindings: implement python bindings for libgpiod v2 Bartosz Golaszewski
                   ` (2 preceding siblings ...)
  2022-10-07 14:55 ` [libgpiod v2][PATCH v3 3/4] bindings: python: add tests Bartosz Golaszewski
@ 2022-10-07 14:55 ` Bartosz Golaszewski
  2022-10-07 15:26   ` Andy Shevchenko
  2022-10-13  3:10   ` Kent Gibson
  2022-10-12 12:34 ` [libgpiod v2][PATCH v3 0/4] bindings: " Bartosz Golaszewski
  4 siblings, 2 replies; 30+ messages in thread
From: Bartosz Golaszewski @ 2022-10-07 14:55 UTC (permalink / raw)
  To: Kent Gibson, Linus Walleij, Andy Shevchenko, Viresh Kumar
  Cc: linux-gpio, Bartosz Golaszewski

This adds python bindings for libgpiod v2. As opposed to v1, they are
mostly written in python with just low-level elements written in C and
interfacing with libgpiod.so.

We've also added setup.py which will allow to use pip for managing the
bindings and split them into a separate meta-openembedded recipe.

Signed-off-by: Bartosz Golaszewski <brgl@bgdev.pl>
---
 bindings/python/.gitignore                |   8 +
 bindings/python/Makefile.am               |  35 ++
 bindings/python/gpiod/Makefile.am         |  17 +
 bindings/python/gpiod/__init__.py         |  53 +++
 bindings/python/gpiod/chip.py             | 308 +++++++++++++++++
 bindings/python/gpiod/chip_info.py        |  21 ++
 bindings/python/gpiod/edge_event.py       |  46 +++
 bindings/python/gpiod/exception.py        |  20 ++
 bindings/python/gpiod/ext/Makefile.am     |  11 +
 bindings/python/gpiod/ext/chip.c          | 335 ++++++++++++++++++
 bindings/python/gpiod/ext/common.c        |  92 +++++
 bindings/python/gpiod/ext/internal.h      |  20 ++
 bindings/python/gpiod/ext/line-config.c   | 133 +++++++
 bindings/python/gpiod/ext/line-settings.c | 130 +++++++
 bindings/python/gpiod/ext/module.c        | 193 +++++++++++
 bindings/python/gpiod/ext/request.c       | 402 ++++++++++++++++++++++
 bindings/python/gpiod/info_event.py       |  33 ++
 bindings/python/gpiod/internal.py         |  19 +
 bindings/python/gpiod/line.py             |  56 +++
 bindings/python/gpiod/line_info.py        |  73 ++++
 bindings/python/gpiod/line_request.py     | 258 ++++++++++++++
 bindings/python/gpiod/line_settings.py    |  62 ++++
 bindings/python/setup.py                  |  47 +++
 configure.ac                              |   3 +
 24 files changed, 2375 insertions(+)
 create mode 100644 bindings/python/.gitignore
 create mode 100644 bindings/python/Makefile.am
 create mode 100644 bindings/python/gpiod/Makefile.am
 create mode 100644 bindings/python/gpiod/__init__.py
 create mode 100644 bindings/python/gpiod/chip.py
 create mode 100644 bindings/python/gpiod/chip_info.py
 create mode 100644 bindings/python/gpiod/edge_event.py
 create mode 100644 bindings/python/gpiod/exception.py
 create mode 100644 bindings/python/gpiod/ext/Makefile.am
 create mode 100644 bindings/python/gpiod/ext/chip.c
 create mode 100644 bindings/python/gpiod/ext/common.c
 create mode 100644 bindings/python/gpiod/ext/internal.h
 create mode 100644 bindings/python/gpiod/ext/line-config.c
 create mode 100644 bindings/python/gpiod/ext/line-settings.c
 create mode 100644 bindings/python/gpiod/ext/module.c
 create mode 100644 bindings/python/gpiod/ext/request.c
 create mode 100644 bindings/python/gpiod/info_event.py
 create mode 100644 bindings/python/gpiod/internal.py
 create mode 100644 bindings/python/gpiod/line.py
 create mode 100644 bindings/python/gpiod/line_info.py
 create mode 100644 bindings/python/gpiod/line_request.py
 create mode 100644 bindings/python/gpiod/line_settings.py
 create mode 100644 bindings/python/setup.py

diff --git a/bindings/python/.gitignore b/bindings/python/.gitignore
new file mode 100644
index 0000000..b603068
--- /dev/null
+++ b/bindings/python/.gitignore
@@ -0,0 +1,8 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+build/
+__pycache__/
+dist/
+gpiod.egg-info/
+*.so
diff --git a/bindings/python/Makefile.am b/bindings/python/Makefile.am
new file mode 100644
index 0000000..3212a8f
--- /dev/null
+++ b/bindings/python/Makefile.am
@@ -0,0 +1,35 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+EXTRA_DIST = setup.py
+
+if WITH_TESTS
+
+BUILD_TESTS = 1
+
+endif
+
+all-local:
+	GPIOD_VERSION_STRING=$(VERSION_STR) \
+	GPIOD_WITH_TESTS=$(BUILD_TESTS) \
+	$(PYTHON) setup.py build_ext --inplace \
+		--include-dirs=$(top_srcdir)/include/:$(top_srcdir)/tests/gpiosim/ \
+		--library-dirs=$(top_builddir)/lib/.libs/:$(top_srcdir)/tests/gpiosim/.libs/
+
+install-exec-local:
+	GPIOD_WITH_TESTS= \
+	$(PYTHON) setup.py install --prefix=$(prefix)
+
+SUBDIRS = gpiod
+
+if WITH_TESTS
+
+SUBDIRS += tests
+
+endif
+
+if WITH_EXAMPLES
+
+SUBDIRS += examples
+
+endif
diff --git a/bindings/python/gpiod/Makefile.am b/bindings/python/gpiod/Makefile.am
new file mode 100644
index 0000000..b0f4126
--- /dev/null
+++ b/bindings/python/gpiod/Makefile.am
@@ -0,0 +1,17 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+SUBDIRS = ext
+
+EXTRA_DIST = \
+	chip_info.py \
+	chip.py \
+	edge_event.py \
+	exception.py \
+	info_event.py \
+	__init__.py \
+	internal.py \
+	line_info.py \
+	line.py \
+	line_request.py \
+	line_settings.py 
diff --git a/bindings/python/gpiod/__init__.py b/bindings/python/gpiod/__init__.py
new file mode 100644
index 0000000..7854cfd
--- /dev/null
+++ b/bindings/python/gpiod/__init__.py
@@ -0,0 +1,53 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+"""
+Python bindings for libgpiod.
+
+This module wraps the native C API of libgpiod in a set of python classes.
+"""
+
+from . import _ext
+from . import line
+from .chip import Chip
+from .chip_info import ChipInfo
+from .edge_event import EdgeEvent
+from .exception import ChipClosedError, RequestReleasedError
+from .info_event import InfoEvent
+from .line_request import LineRequest
+from .line_settings import LineSettings
+
+__version__ = _ext.__version__
+
+
+def is_gpiochip_device(path: str) -> bool:
+    """
+    Check if the file pointed to by path is a GPIO chip character device.
+
+    Args:
+      path
+        Path to the file that should be checked.
+
+    Returns:
+      Returns true if so, False otherwise.
+    """
+    return _ext.is_gpiochip_device(path)
+
+
+def request_lines(path: str, *args, **kwargs) -> LineRequest:
+    """
+    Open a GPIO chip pointed to by 'path', request lines according to the
+    configuration arguments, close the chip and return the request object.
+
+    Args:
+      path
+        Path to the GPIO character device file.
+      *args
+      **kwargs
+        See Chip.request_lines() for configuration arguments.
+
+    Returns:
+      Returns a new LineRequest object.
+    """
+    with Chip(path) as chip:
+        return chip.request_lines(*args, **kwargs)
diff --git a/bindings/python/gpiod/chip.py b/bindings/python/gpiod/chip.py
new file mode 100644
index 0000000..7896958
--- /dev/null
+++ b/bindings/python/gpiod/chip.py
@@ -0,0 +1,308 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+from . import _ext
+from .chip_info import ChipInfo
+from .exception import ChipClosedError
+from .info_event import InfoEvent
+from .internal import poll_fd
+from .line_info import LineInfo
+from .line_settings import LineSettings, _line_settings_to_ext_line_settings
+from .line_request import LineRequest
+from datetime import timedelta
+from errno import ENOENT
+from select import select
+from typing import Union, Optional
+
+
+class Chip:
+    """
+    Represents a GPIO chip.
+
+    Chip object manages all resources associated with the GPIO chip it represents.
+
+    The gpiochip device file is opened during the object's construction. The Chip
+    object's constructor takes the path to the GPIO chip device file
+    as the only argument.
+
+    Callers must close the chip by calling the close() method when it's no longer
+    used.
+
+    Example:
+
+        chip = gpiod.Chip(\"/dev/gpiochip0\")
+        do_something(chip)
+        chip.close()
+
+    The gpiod.Chip class also supports controlled execution ('with' statement).
+
+    Example:
+
+        with gpiod.Chip(path="/dev/gpiochip0") as chip:
+            do_something(chip)
+    """
+
+    def __init__(self, path: str):
+        """
+        Open a GPIO device.
+
+        Args:
+          path:
+            Path to the GPIO character device file.
+        """
+        self._chip = _ext.Chip(path)
+
+    def __bool__(self) -> bool:
+        """
+        Boolean conversion for GPIO chips.
+
+        Returns:
+          True if the chip is open and False if it's closed.
+        """
+        return True if self._chip else False
+
+    def __enter__(self):
+        """
+        Controlled execution enter callback.
+        """
+        self._check_closed()
+        return self
+
+    def __exit__(self, exc_type, exc_value, traceback) -> None:
+        """
+        Controlled execution exit callback.
+        """
+        self.close()
+
+    def _check_closed(self) -> None:
+        if not self._chip:
+            raise ChipClosedError()
+
+    def close(self) -> None:
+        """
+        Close the associated GPIO chip descriptor. The chip object must no
+        longer be used after this method is called.
+        """
+        self._check_closed()
+        self._chip.close()
+        self._chip = None
+
+    def get_info(self) -> ChipInfo:
+        """
+        Get the information about the chip.
+
+        Returns:
+          New gpiod.ChipInfo object.
+        """
+        self._check_closed()
+        return self._chip.get_info()
+
+    def map_line(self, id: Union[str, int]) -> int:
+        """
+        Map a line's identifier to its offset within the chip.
+
+        Args:
+          id:
+            Name of the GPIO line, its offset as a string or its offset as an
+            integer.
+
+        Returns:
+          If id is an integer - it's returned as is (unless it's out of range
+          for this chip). If it's a string, the method tries to interpret it as
+          the name of the line first and tries too perform a name lookup within
+          the chip. If it fails, it tries to convert the string to an integer
+          and check if it represents a valid offset within the chip and if
+          so - returns it.
+        """
+        self._check_closed()
+
+        if not isinstance(id, int):
+            try:
+                return self._chip.map_line(id)
+            except OSError as ex:
+                if ex.errno == ENOENT:
+                    try:
+                        offset = int(id)
+                    except ValueError:
+                        raise ex
+                else:
+                    raise ex
+        else:
+            offset = id
+
+        if offset >= self.get_info().num_lines:
+            raise ValueError("line offset of out range")
+
+        return offset
+
+    def _get_line_info(self, line: Union[int, str], watch: bool) -> LineInfo:
+        self._check_closed()
+        return self._chip.get_line_info(self.map_line(line), watch)
+
+    def get_line_info(self, line: Union[int, str]) -> LineInfo:
+        """
+        Get the snapshot of information about the line at given offset.
+
+        Args:
+          line:
+            Offset or name of the GPIO line to get information for.
+
+        Returns:
+          New LineInfo object.
+        """
+        return self._get_line_info(line, watch=False)
+
+    def watch_line_info(self, line: Union[int, str]) -> LineInfo:
+        """
+        Get the snapshot of information about the line at given offset and
+        start watching it for future changes.
+
+        Args:
+          line:
+            Offset or name of the GPIO line to get information for.
+
+        Returns:
+          New gpiod.LineInfo object.
+        """
+        return self._get_line_info(line, watch=True)
+
+    def unwatch_line_info(self, line: Union[int, str]) -> None:
+        """
+        Stop watching a line for status changes.
+
+        Args:
+          line:
+            Offset or name of the line to stop watching.
+        """
+        self._check_closed()
+        return self._chip.unwatch_line_info(self.map_line(line))
+
+    def wait_info_event(
+        self, timeout: Optional[Union[timedelta, float]] = None
+    ) -> bool:
+        """
+        Wait for line status change events on any of the watched lines on the
+        chip.
+
+        Args:
+          timeout:
+            Wait time limit represented as either a datetime.timedelta object
+            or the number of seconds stored in a float.
+
+        Returns:
+          True if an info event is ready to be read from the chip, False if the
+          wait timed out without any events.
+        """
+        self._check_closed()
+
+        return poll_fd(self.fd, timeout)
+
+    def read_info_event(self) -> InfoEvent:
+        """
+        Read a single line status change event from the chip.
+
+        Returns:
+          New gpiod.InfoEvent object.
+
+        Note:
+          This function may block if there are no available events in the queue.
+        """
+        self._check_closed()
+        return self._chip.read_info_event()
+
+    def request_lines(
+        self,
+        config: dict[tuple[Union[int, str]], Optional[LineSettings]],
+        consumer: Optional[str] = None,
+        event_buffer_size: Optional[int] = None,
+    ) -> LineRequest:
+        """
+        Request a set of lines for exclusive usage.
+
+        Args:
+          config:
+            Dictionary mapping offsets or names (or tuples thereof) to
+            LineSettings. If None is passed as the value of the mapping,
+            default settings are used.
+          consumer:
+            Consumer string to use for this request.
+          event_buffer_size:
+            Size of the kernel edge event buffer to configure for this request.
+
+        Returns:
+          New LineRequest object.
+        """
+        self._check_closed()
+
+        line_cfg = _ext.LineConfig()
+
+        for lines, settings in config.items():
+            offsets = list()
+            name_map = dict()
+            offset_map = dict()
+
+            if isinstance(lines, int) or isinstance(lines, str):
+                lines = (lines,)
+
+            for line in lines:
+                offset = self.map_line(line)
+                offsets.append(offset)
+                if isinstance(line, str):
+                    name_map[line] = offset
+                    offset_map[offset] = line
+
+            if settings is None:
+                settings = LineSettings()
+
+            line_cfg.add_line_settings(
+                offsets, _line_settings_to_ext_line_settings(settings)
+            )
+
+        req_internal = self._chip.request_lines(line_cfg, consumer, event_buffer_size)
+        request = LineRequest(req_internal)
+
+        request._offsets = req_internal.offsets
+        request._name_map = name_map
+        request._offset_map = offset_map
+
+        request._lines = list()
+        for off in request.offsets:
+            request._lines.append(offset_map[off] if off in offset_map else off)
+
+        return request
+
+    def __repr__(self) -> str:
+        """
+        Return a string that can be used to re-create this chip object.
+        """
+        if not self._chip:
+            return "<Chip CLOSED>"
+
+        return 'Chip("{}")'.format(self.path)
+
+    def __str__(self) -> str:
+        """
+        Return a user-friendly, human-readable description of this chip.
+        """
+        if not self._chip:
+            return "<Chip CLOSED>"
+
+        return '<Chip path="{}" fd={} info={}>'.format(
+            self.path, self.fd, self.get_info()
+        )
+
+    @property
+    def path(self) -> str:
+        """
+        Filesystem path used to open this chip.
+        """
+        self._check_closed()
+        return self._chip.path
+
+    @property
+    def fd(self) -> int:
+        """
+        File descriptor associated with this chip.
+        """
+        self._check_closed()
+        return self._chip.fd
diff --git a/bindings/python/gpiod/chip_info.py b/bindings/python/gpiod/chip_info.py
new file mode 100644
index 0000000..a506b55
--- /dev/null
+++ b/bindings/python/gpiod/chip_info.py
@@ -0,0 +1,21 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+
+from dataclasses import dataclass
+
+
+@dataclass(frozen=True, repr=False)
+class ChipInfo:
+    """
+    Snapshot of a chip's status.
+    """
+
+    name: str
+    label: str
+    num_lines: int
+
+    def __str__(self):
+        return '<ChipInfo name="{}" label="{}" num_lines={}>'.format(
+            self.name, self.label, self.num_lines
+        )
diff --git a/bindings/python/gpiod/edge_event.py b/bindings/python/gpiod/edge_event.py
new file mode 100644
index 0000000..88f8e9b
--- /dev/null
+++ b/bindings/python/gpiod/edge_event.py
@@ -0,0 +1,46 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+from . import _ext
+from dataclasses import dataclass
+from enum import Enum
+
+
+@dataclass(frozen=True, init=False, repr=False)
+class EdgeEvent:
+    """
+    Immutable object containing data about a single edge event.
+    """
+
+    class Type(Enum):
+        RISING_EDGE = _ext.EDGE_EVENT_TYPE_RISING
+        FALLING_EDGE = _ext.EDGE_EVENT_TYPE_FALLING
+
+    event_type: Type
+    timestamp_ns: int
+    line_offset: int
+    global_seqno: int
+    line_seqno: int
+
+    def __init__(
+        self,
+        event_type: int,
+        timestamp_ns: int,
+        line_offset: int,
+        global_seqno: int,
+        line_seqno: int,
+    ):
+        object.__setattr__(self, "event_type", EdgeEvent.Type(event_type))
+        object.__setattr__(self, "timestamp_ns", timestamp_ns)
+        object.__setattr__(self, "line_offset", line_offset)
+        object.__setattr__(self, "global_seqno", global_seqno)
+        object.__setattr__(self, "line_seqno", line_seqno)
+
+    def __str__(self):
+        return "<EdgeEvent type={} timestamp_ns={} line_offset={} global_seqno={} line_seqno={}>".format(
+            self.event_type,
+            self.timestamp_ns,
+            self.line_offset,
+            self.global_seqno,
+            self.line_seqno,
+        )
diff --git a/bindings/python/gpiod/exception.py b/bindings/python/gpiod/exception.py
new file mode 100644
index 0000000..07ffaa6
--- /dev/null
+++ b/bindings/python/gpiod/exception.py
@@ -0,0 +1,20 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+
+class ChipClosedError(Exception):
+    """
+    Error raised when an already closed chip is used.
+    """
+
+    def __init__(self):
+        super().__init__("I/O operation on closed chip")
+
+
+class RequestReleasedError(Exception):
+    """
+    Error raised when a released request is used.
+    """
+
+    def __init__(self):
+        super().__init__("GPIO lines have been released")
diff --git a/bindings/python/gpiod/ext/Makefile.am b/bindings/python/gpiod/ext/Makefile.am
new file mode 100644
index 0000000..9c81b17
--- /dev/null
+++ b/bindings/python/gpiod/ext/Makefile.am
@@ -0,0 +1,11 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+EXTRA_DIST = \
+	chip.c \
+	common.c \
+	internal.h \
+	line-config.c \
+	line-settings.c \
+	module.c \
+	request.c
diff --git a/bindings/python/gpiod/ext/chip.c b/bindings/python/gpiod/ext/chip.c
new file mode 100644
index 0000000..47d5455
--- /dev/null
+++ b/bindings/python/gpiod/ext/chip.c
@@ -0,0 +1,335 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include "internal.h"
+
+typedef struct {
+	PyObject_HEAD;
+	struct gpiod_chip *chip;
+} chip_object;
+
+static int
+chip_init(chip_object *self, PyObject *args, PyObject *Py_UNUSED(ignored))
+{
+	char *path;
+	int ret;
+
+	ret = PyArg_ParseTuple(args, "s", &path);
+	if (!ret)
+		return -1;
+
+	Py_BEGIN_ALLOW_THREADS;
+	self->chip = gpiod_chip_open(path);
+	Py_END_ALLOW_THREADS;
+	if (!self->chip) {
+		Py_gpiod_SetErrFromErrno();
+		return -1;
+	}
+
+	return 0;
+}
+
+static void chip_finalize(chip_object *self)
+{
+	if (self->chip)
+		PyObject_CallMethod((PyObject *)self, "close", "");
+}
+
+static PyObject *chip_path(chip_object *self, void *Py_UNUSED(ignored))
+{
+	return PyUnicode_FromString(gpiod_chip_get_path(self->chip));
+}
+
+static PyObject *chip_fd(chip_object *self, void *Py_UNUSED(ignored))
+{
+	return PyLong_FromLong(gpiod_chip_get_fd(self->chip));
+}
+
+static PyGetSetDef chip_getset[] = {
+	{
+		.name = "path",
+		.get = (getter)chip_path,
+	},
+	{
+		.name = "fd",
+		.get = (getter)chip_fd,
+	},
+	{ }
+};
+
+static PyObject *chip_close(chip_object *self, PyObject *Py_UNUSED(ignored))
+{
+	Py_BEGIN_ALLOW_THREADS;
+	gpiod_chip_close(self->chip);
+	Py_END_ALLOW_THREADS;
+	self->chip = NULL;
+
+	Py_RETURN_NONE;
+}
+
+static PyObject *chip_get_info(chip_object *self, PyObject *Py_UNUSED(ignored))
+{
+	struct gpiod_chip_info *info;
+	PyObject *type, *ret;
+
+	type = Py_gpiod_GetGlobalType("ChipInfo");
+	if (!type)
+		return NULL;
+
+	info = gpiod_chip_get_info(self->chip);
+	if (!info)
+		return PyErr_SetFromErrno(PyExc_OSError);
+
+	 ret = PyObject_CallFunction(type, "ssI",
+				     gpiod_chip_info_get_name(info),
+				     gpiod_chip_info_get_label(info),
+				     gpiod_chip_info_get_num_lines(info));
+	 gpiod_chip_info_free(info);
+	 return ret;
+}
+
+static PyObject *make_line_info(struct gpiod_line_info *info)
+{
+	PyObject *type;
+
+	type = Py_gpiod_GetGlobalType("LineInfo");
+	if (!type)
+		return NULL;
+
+	return PyObject_CallFunction(type, "IsOsiOiiiiOi",
+				gpiod_line_info_get_offset(info),
+				gpiod_line_info_get_name(info),
+				gpiod_line_info_is_used(info) ?
+							Py_True : Py_False,
+				gpiod_line_info_get_consumer(info),
+				gpiod_line_info_get_direction(info),
+				gpiod_line_info_is_active_low(info) ?
+							Py_True : Py_False,
+				gpiod_line_info_get_bias(info),
+				gpiod_line_info_get_drive(info),
+				gpiod_line_info_get_edge_detection(info),
+				gpiod_line_info_get_event_clock(info),
+				gpiod_line_info_is_debounced(info) ?
+							Py_True : Py_False,
+				gpiod_line_info_get_debounce_period_us(info));
+}
+
+static PyObject *chip_get_line_info(chip_object *self, PyObject *args)
+{
+	struct gpiod_line_info *info;
+	unsigned int offset;
+	PyObject *info_obj;
+	int ret, watch;
+
+	ret = PyArg_ParseTuple(args, "Ip", &offset, &watch);
+	if (!ret)
+		return NULL;
+
+	Py_BEGIN_ALLOW_THREADS;
+	if (watch)
+		info = gpiod_chip_watch_line_info(self->chip, offset);
+	else
+		info = gpiod_chip_get_line_info(self->chip, offset);
+	Py_END_ALLOW_THREADS;
+	if (!info)
+		return Py_gpiod_SetErrFromErrno();
+
+	info_obj = make_line_info(info);
+	gpiod_line_info_free(info);
+	return info_obj;
+}
+
+static PyObject *
+chip_unwatch_line_info(chip_object *self, PyObject *args)
+{
+	unsigned int offset;
+	int ret;
+
+	ret = PyArg_ParseTuple(args, "I", &offset);
+	if (!ret)
+		return NULL;
+
+	Py_BEGIN_ALLOW_THREADS;
+	ret = gpiod_chip_unwatch_line_info(self->chip, offset);
+	Py_END_ALLOW_THREADS;
+	if (ret)
+		return Py_gpiod_SetErrFromErrno();
+
+	Py_RETURN_NONE;
+}
+
+static PyObject *
+chip_read_info_event(chip_object *self, PyObject *Py_UNUSED(ignored))
+{
+	PyObject *type, *info_obj, *event_obj;
+	struct gpiod_info_event *event;
+	struct gpiod_line_info *info;
+
+	type = Py_gpiod_GetGlobalType("InfoEvent");
+	if (!type)
+		return NULL;
+
+	Py_BEGIN_ALLOW_THREADS;
+	event = gpiod_chip_read_info_event(self->chip);
+	Py_END_ALLOW_THREADS;
+	if (!event)
+		return Py_gpiod_SetErrFromErrno();
+
+	info = gpiod_info_event_get_line_info(event);
+
+	info_obj = make_line_info(info);
+	if (!info_obj) {
+		gpiod_info_event_free(event);
+		return NULL;
+	}
+
+	event_obj = PyObject_CallFunction(type, "iKO",
+				gpiod_info_event_get_event_type(event),
+				gpiod_info_event_get_timestamp_ns(event),
+				info_obj);
+	Py_DECREF(info_obj);
+	gpiod_info_event_free(event);
+	return event_obj;
+}
+
+static PyObject *chip_map_line(chip_object *self, PyObject *args)
+{
+	int ret, offset;
+	char *name;
+
+	ret = PyArg_ParseTuple(args, "s", &name);
+	if (!ret)
+		return NULL;
+
+	Py_BEGIN_ALLOW_THREADS;
+	offset = gpiod_chip_get_line_offset_from_name(self->chip, name);
+	Py_END_ALLOW_THREADS;
+	if (offset < 0)
+		return Py_gpiod_SetErrFromErrno();
+
+	return PyLong_FromLong(offset);
+}
+
+static struct gpiod_request_config *
+make_request_config(PyObject *consumer_obj, PyObject *event_buffer_size_obj)
+{
+	struct gpiod_request_config *req_cfg;
+	size_t event_buffer_size;
+	const char *consumer;
+
+	req_cfg = gpiod_request_config_new();
+	if (!req_cfg) {
+		Py_gpiod_SetErrFromErrno();
+		return NULL;
+	}
+
+	if (consumer_obj != Py_None) {
+		consumer = PyUnicode_AsUTF8(consumer_obj);
+		if (!consumer) {
+			gpiod_request_config_free(req_cfg);
+			return NULL;
+		}
+
+		gpiod_request_config_set_consumer(req_cfg, consumer);
+	}
+
+	if (event_buffer_size_obj != Py_None) {
+		event_buffer_size = PyLong_AsSize_t(event_buffer_size_obj);
+		if (PyErr_Occurred()) {
+			gpiod_request_config_free(req_cfg);
+			return NULL;
+		}
+
+		gpiod_request_config_set_event_buffer_size(req_cfg,
+							   event_buffer_size);
+	}
+
+	return req_cfg;
+}
+
+static PyObject *chip_request_lines(chip_object *self, PyObject *args)
+{
+	PyObject *line_config, *consumer, *event_buffer_size, *req_obj;
+	struct gpiod_request_config *req_cfg;
+	struct gpiod_line_config *line_cfg;
+	struct gpiod_line_request *request;
+	int ret;
+
+	ret = PyArg_ParseTuple(args, "OOO",
+			       &line_config, &consumer, &event_buffer_size);
+	if (!ret)
+		return NULL;
+
+	line_cfg = Py_gpiod_LineConfigGetData(line_config);
+	if (!line_cfg)
+		return NULL;
+
+	req_cfg = make_request_config(consumer, event_buffer_size);
+	if (!req_cfg)
+		return NULL;
+
+	Py_BEGIN_ALLOW_THREADS;
+	request = gpiod_chip_request_lines(self->chip, req_cfg, line_cfg);
+	Py_END_ALLOW_THREADS;
+	gpiod_request_config_free(req_cfg);
+	if (!request)
+		return Py_gpiod_SetErrFromErrno();
+
+	req_obj = Py_gpiod_MakeRequestObject(request);
+	if (!req_obj)
+		gpiod_line_request_release(request);
+
+	return req_obj;
+}
+
+static PyMethodDef chip_methods[] = {
+	{
+		.ml_name = "close",
+		.ml_meth = (PyCFunction)chip_close,
+		.ml_flags = METH_NOARGS,
+	},
+	{
+		.ml_name = "get_info",
+		.ml_meth = (PyCFunction)chip_get_info,
+		.ml_flags = METH_NOARGS,
+	},
+	{
+		.ml_name = "get_line_info",
+		.ml_meth = (PyCFunction)chip_get_line_info,
+		.ml_flags = METH_VARARGS,
+	},
+	{
+		.ml_name = "unwatch_line_info",
+		.ml_meth = (PyCFunction)chip_unwatch_line_info,
+		.ml_flags = METH_VARARGS,
+	},
+	{
+		.ml_name = "read_info_event",
+		.ml_meth = (PyCFunction)chip_read_info_event,
+		.ml_flags = METH_NOARGS,
+	},
+	{
+		.ml_name = "map_line",
+		.ml_meth = (PyCFunction)chip_map_line,
+		.ml_flags = METH_VARARGS,
+	},
+	{
+		.ml_name = "request_lines",
+		.ml_meth = (PyCFunction)chip_request_lines,
+		.ml_flags = METH_VARARGS,
+	},
+	{ }
+};
+
+PyTypeObject chip_type = {
+	PyVarObject_HEAD_INIT(NULL, 0)
+	.tp_name = "gpiod._ext.Chip",
+	.tp_basicsize = sizeof(chip_object),
+	.tp_flags = Py_TPFLAGS_DEFAULT,
+	.tp_new = PyType_GenericNew,
+	.tp_init = (initproc)chip_init,
+	.tp_finalize = (destructor)chip_finalize,
+	.tp_dealloc = (destructor)Py_gpiod_dealloc,
+	.tp_getset = chip_getset,
+	.tp_methods = chip_methods,
+};
diff --git a/bindings/python/gpiod/ext/common.c b/bindings/python/gpiod/ext/common.c
new file mode 100644
index 0000000..7e53c02
--- /dev/null
+++ b/bindings/python/gpiod/ext/common.c
@@ -0,0 +1,92 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include "internal.h"
+
+/* Generic dealloc callback for all gpiod objects. */
+void Py_gpiod_dealloc(PyObject *self)
+{
+	int ret;
+
+	ret = PyObject_CallFinalizerFromDealloc(self);
+	if (ret < 0)
+		return;
+
+	PyObject_Del(self);
+}
+
+PyObject *_Py_gpiod_SetErrFromErrno(const char *filename)
+{
+	PyObject *exc;
+
+	if (errno == ENOMEM)
+		return PyErr_NoMemory();
+
+	switch (errno) {
+	case EINVAL:
+		exc = PyExc_ValueError;
+		break;
+	case EOPNOTSUPP:
+		exc = PyExc_NotImplementedError;
+		break;
+	case EPIPE:
+		exc = PyExc_BrokenPipeError;
+		break;
+	case ECHILD:
+		exc = PyExc_ChildProcessError;
+		break;
+	case EINTR:
+		exc = PyExc_InterruptedError;
+		break;
+	case EEXIST:
+		exc = PyExc_FileExistsError;
+		break;
+	case ENOENT:
+		exc = PyExc_FileNotFoundError;
+		break;
+	case EISDIR:
+		exc = PyExc_IsADirectoryError;
+		break;
+	case ENOTDIR:
+		exc = PyExc_NotADirectoryError;
+		break;
+	case EPERM:
+		exc = PyExc_PermissionError;
+		break;
+	case ETIMEDOUT:
+		exc = PyExc_TimeoutError;
+		break;
+	default:
+		exc = PyExc_OSError;
+		break;
+	}
+
+	return PyErr_SetFromErrnoWithFilename(exc, filename);
+}
+
+PyObject *Py_gpiod_GetGlobalType(const char *type_name)
+{
+	PyObject *globals;
+
+	globals = PyEval_GetGlobals();
+	if (!globals)
+		return NULL;
+
+	return PyDict_GetItemString(globals, type_name);
+}
+
+unsigned int Py_gpiod_PyLongAsUnsignedInt(PyObject *pylong)
+{
+	unsigned long tmp;
+
+	tmp = PyLong_AsUnsignedLong(pylong);
+	if (PyErr_Occurred())
+		return 0;
+
+	if (tmp > UINT_MAX) {
+		PyErr_SetString(PyExc_ValueError, "value exceeding UINT_MAX");
+		return 0;
+	}
+
+	return tmp;
+}
diff --git a/bindings/python/gpiod/ext/internal.h b/bindings/python/gpiod/ext/internal.h
new file mode 100644
index 0000000..ed80034
--- /dev/null
+++ b/bindings/python/gpiod/ext/internal.h
@@ -0,0 +1,20 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+#ifndef __LIBGPIOD_PYTHON_MODULE_H__
+#define __LIBGPIOD_PYTHON_MODULE_H__
+
+#include <gpiod.h>
+#include <Python.h>
+
+PyObject *_Py_gpiod_SetErrFromErrno(const char *filename);
+#define Py_gpiod_SetErrFromErrno() _Py_gpiod_SetErrFromErrno(__FILE__)
+
+PyObject *Py_gpiod_GetGlobalType(const char *type_name);
+unsigned int Py_gpiod_PyLongAsUnsignedInt(PyObject *pylong);
+void Py_gpiod_dealloc(PyObject *self);
+PyObject *Py_gpiod_MakeRequestObject(struct gpiod_line_request *request);
+struct gpiod_line_config *Py_gpiod_LineConfigGetData(PyObject *obj);
+struct gpiod_line_settings *Py_gpiod_LineSettingsGetData(PyObject *obj);
+
+#endif /* __LIBGPIOD_PYTHON_MODULE_H__ */
diff --git a/bindings/python/gpiod/ext/line-config.c b/bindings/python/gpiod/ext/line-config.c
new file mode 100644
index 0000000..173ca6b
--- /dev/null
+++ b/bindings/python/gpiod/ext/line-config.c
@@ -0,0 +1,133 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include "internal.h"
+
+typedef struct {
+	PyObject_HEAD;
+	struct gpiod_line_config *cfg;
+} line_config_object;
+
+static int line_config_init(line_config_object *self,
+		       PyObject *Py_UNUSED(args), PyObject *Py_UNUSED(ignored))
+{
+	self->cfg = gpiod_line_config_new();
+	if (!self->cfg) {
+		Py_gpiod_SetErrFromErrno();
+		return -1;
+	}
+
+	return 0;
+}
+
+static void line_config_finalize(line_config_object *self)
+{
+	if (self->cfg)
+		gpiod_line_config_free(self->cfg);
+}
+
+static unsigned int *make_offsets(PyObject *obj, Py_ssize_t len)
+{
+	unsigned int *offsets;
+	PyObject *offset;
+	Py_ssize_t i;
+
+	offsets = PyMem_Calloc(len, sizeof(unsigned int));
+	if (!offsets)
+		return (unsigned int *)PyErr_NoMemory();
+
+	for (i = 0; i < len; i++) {
+		offset = PyList_GetItem(obj, i);
+		if (!offset) {
+			PyMem_Free(offsets);
+			return NULL;
+		}
+
+		offsets[i] = Py_gpiod_PyLongAsUnsignedInt(offset);
+		if (PyErr_Occurred()) {
+			PyMem_Free(offsets);
+			return NULL;
+		}
+	}
+
+	return offsets;
+}
+
+static PyObject *
+line_config_add_line_settings(line_config_object *self, PyObject *args)
+{
+	PyObject *offsets_obj, *settings_obj;
+	struct gpiod_line_settings *settings;
+	unsigned int *offsets;
+	Py_ssize_t num_offsets;
+	int ret;
+
+	ret = PyArg_ParseTuple(args, "OO", &offsets_obj, &settings_obj);
+	if (!ret)
+		return NULL;
+
+	num_offsets = PyObject_Size(offsets_obj);
+	if (num_offsets < 0)
+		return NULL;
+
+	offsets = make_offsets(offsets_obj, num_offsets);
+	if (!offsets)
+		return NULL;
+
+	settings = Py_gpiod_LineSettingsGetData(settings_obj);
+	if (!settings) {
+		PyMem_Free(offsets);
+		return NULL;
+	}
+
+	ret = gpiod_line_config_add_line_settings(self->cfg, offsets,
+						  num_offsets, settings);
+	PyMem_Free(offsets);
+	if (ret)
+		return Py_gpiod_SetErrFromErrno();
+
+	Py_RETURN_NONE;
+}
+
+static PyMethodDef line_config_methods[] = {
+	{
+		.ml_name = "add_line_settings",
+		.ml_meth = (PyCFunction)line_config_add_line_settings,
+		.ml_flags = METH_VARARGS,
+	},
+	{ }
+};
+
+PyTypeObject line_config_type = {
+	PyVarObject_HEAD_INIT(NULL, 0)
+	.tp_name = "gpiod._ext.LineConfig",
+	.tp_basicsize = sizeof(line_config_object),
+	.tp_flags = Py_TPFLAGS_DEFAULT,
+	.tp_new = PyType_GenericNew,
+	.tp_init = (initproc)line_config_init,
+	.tp_finalize = (destructor)line_config_finalize,
+	.tp_dealloc = (destructor)Py_gpiod_dealloc,
+	.tp_methods = line_config_methods,
+};
+
+struct gpiod_line_config *Py_gpiod_LineConfigGetData(PyObject *obj)
+{
+	line_config_object *line_cfg;
+	PyObject *type;
+
+	type = PyObject_Type(obj);
+	if (!type)
+		return NULL;
+
+	if ((PyTypeObject *)type != &line_config_type) {
+		PyErr_SetString(PyExc_TypeError,
+				"not a gpiod._ext.LineConfig object");
+		Py_DECREF(type);
+		return NULL;
+	}
+	Py_DECREF(type);
+
+	line_cfg = (line_config_object *)obj;
+
+	return line_cfg->cfg;
+}
diff --git a/bindings/python/gpiod/ext/line-settings.c b/bindings/python/gpiod/ext/line-settings.c
new file mode 100644
index 0000000..bd2a66a
--- /dev/null
+++ b/bindings/python/gpiod/ext/line-settings.c
@@ -0,0 +1,130 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include "internal.h"
+
+typedef struct {
+	PyObject_HEAD;
+	struct gpiod_line_settings *settings;
+} line_settings_object;
+
+static int set_int_prop(struct gpiod_line_settings *settings, int val,
+			int (*func)(struct gpiod_line_settings *, int))
+{
+	int ret;
+
+	ret = func(settings, val);
+	if (ret) {
+		Py_gpiod_SetErrFromErrno();
+		return -1;
+	}
+
+	return 0;
+}
+
+static int
+line_settings_init(line_settings_object *self, PyObject *args, PyObject *kwargs)
+{
+	static char *kwlist[] = {
+		"direction",
+		"edge_detection",
+		"bias",
+		"drive",
+		"active_low",
+		"debounce_period",
+		"event_clock",
+		"output_value",
+		NULL
+	};
+
+	int direction, edge, bias, drive, active_low, event_clock, output_value,
+	    ret;
+	unsigned long debounce_period;
+
+	ret = PyArg_ParseTupleAndKeywords(args, kwargs, "IIIIpdII", kwlist,
+			&direction, &edge, &bias, &drive, &active_low,
+			&debounce_period, &event_clock, &output_value);
+	if (!ret)
+		return -1;
+
+	self->settings = gpiod_line_settings_new();
+	if (!self->settings) {
+		Py_gpiod_SetErrFromErrno();
+		return -1;
+	}
+
+	ret = set_int_prop(self->settings, direction,
+			   gpiod_line_settings_set_direction);
+	if (ret)
+		return -1;
+
+	ret = set_int_prop(self->settings, edge,
+			   gpiod_line_settings_set_edge_detection);
+	if (ret)
+		return -1;
+
+	ret = set_int_prop(self->settings, bias,
+			   gpiod_line_settings_set_bias);
+	if (ret)
+		return -1;
+
+	ret = set_int_prop(self->settings, drive,
+			   gpiod_line_settings_set_drive);
+	if (ret)
+		return -1;
+
+	gpiod_line_settings_set_active_low(self->settings, active_low);
+	gpiod_line_settings_set_debounce_period_us(self->settings,
+						   debounce_period);
+
+	ret = set_int_prop(self->settings, edge,
+			   gpiod_line_settings_set_edge_detection);
+	if (ret)
+		return -1;
+
+	ret = set_int_prop(self->settings, output_value,
+			   gpiod_line_settings_set_output_value);
+	if (ret)
+		return -1;
+
+	return 0;
+}
+
+static void line_settings_finalize(line_settings_object *self)
+{
+	if (self->settings)
+		gpiod_line_settings_free(self->settings);
+}
+
+PyTypeObject line_settings_type = {
+	PyVarObject_HEAD_INIT(NULL, 0)
+	.tp_name = "gpiod._ext.LineSettings",
+	.tp_basicsize = sizeof(line_settings_object),
+	.tp_flags = Py_TPFLAGS_DEFAULT,
+	.tp_new = PyType_GenericNew,
+	.tp_init = (initproc)line_settings_init,
+	.tp_finalize = (destructor)line_settings_finalize,
+	.tp_dealloc = (destructor)Py_gpiod_dealloc,
+};
+
+struct gpiod_line_settings *Py_gpiod_LineSettingsGetData(PyObject *obj)
+{
+	line_settings_object *settings;
+	PyObject *type;
+
+	type = PyObject_Type(obj);
+	if (!type)
+		return NULL;
+
+	if ((PyTypeObject *)type != &line_settings_type) {
+		PyErr_SetString(PyExc_TypeError,
+				"not a gpiod._ext.LineSettings object");
+		Py_DECREF(type);
+		return NULL;
+	}
+	Py_DECREF(type);
+
+	settings = (line_settings_object *)obj;
+
+	return settings->settings;
+}
diff --git a/bindings/python/gpiod/ext/module.c b/bindings/python/gpiod/ext/module.c
new file mode 100644
index 0000000..8725ef2
--- /dev/null
+++ b/bindings/python/gpiod/ext/module.c
@@ -0,0 +1,193 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <gpiod.h>
+#include <Python.h>
+
+struct module_const {
+	const char *name;
+	long val;
+};
+
+static const struct module_const module_constants[] = {
+	{
+		.name = "VALUE_INACTIVE",
+		.val = GPIOD_LINE_VALUE_INACTIVE,
+	},
+	{
+		.name = "VALUE_ACTIVE",
+		.val = GPIOD_LINE_VALUE_ACTIVE,
+	},
+	{
+		.name = "DIRECTION_AS_IS",
+		.val = GPIOD_LINE_DIRECTION_AS_IS,
+	},
+	{
+		.name = "DIRECTION_INPUT",
+		.val = GPIOD_LINE_DIRECTION_INPUT,
+	},
+	{
+		.name = "DIRECTION_OUTPUT",
+		.val = GPIOD_LINE_DIRECTION_OUTPUT,
+	},
+	{
+		.name = "BIAS_AS_IS",
+		.val = GPIOD_LINE_BIAS_AS_IS,
+	},
+	{
+		.name = "BIAS_UNKNOWN",
+		.val = GPIOD_LINE_BIAS_UNKNOWN,
+	},
+	{
+		.name = "BIAS_DISABLED",
+		.val = GPIOD_LINE_BIAS_DISABLED,
+	},
+	{
+		.name = "BIAS_PULL_UP",
+		.val = GPIOD_LINE_BIAS_PULL_UP,
+	},
+	{
+		.name = "BIAS_PULL_DOWN",
+		.val = GPIOD_LINE_BIAS_PULL_DOWN,
+	},
+	{
+		.name = "DRIVE_PUSH_PULL",
+		.val = GPIOD_LINE_DRIVE_PUSH_PULL,
+	},
+	{
+		.name = "DRIVE_OPEN_DRAIN",
+		.val = GPIOD_LINE_DRIVE_OPEN_DRAIN,
+	},
+	{
+		.name = "DRIVE_OPEN_SOURCE",
+		.val = GPIOD_LINE_DRIVE_OPEN_SOURCE,
+	},
+	{
+		.name = "EDGE_NONE",
+		.val = GPIOD_LINE_EDGE_NONE,
+	},
+	{
+		.name = "EDGE_FALLING",
+		.val = GPIOD_LINE_EDGE_FALLING,
+	},
+	{
+		.name = "EDGE_RISING",
+		.val = GPIOD_LINE_EDGE_RISING,
+	},
+	{
+		.name = "EDGE_BOTH",
+		.val = GPIOD_LINE_EDGE_BOTH,
+	},
+	{
+		.name = "CLOCK_MONOTONIC",
+		.val = GPIOD_LINE_EVENT_CLOCK_MONOTONIC,
+	},
+	{
+		.name = "CLOCK_REALTIME",
+		.val = GPIOD_LINE_EVENT_CLOCK_REALTIME,
+	},
+	{
+		.name = "CLOCK_HTE",
+		.val = GPIOD_LINE_EVENT_CLOCK_HTE,
+	},
+	{
+		.name = "EDGE_EVENT_TYPE_RISING",
+		.val = GPIOD_EDGE_EVENT_RISING_EDGE,
+	},
+	{
+		.name = "EDGE_EVENT_TYPE_FALLING",
+		.val = GPIOD_EDGE_EVENT_FALLING_EDGE,
+	},
+	{
+		.name = "INFO_EVENT_TYPE_LINE_REQUESTED",
+		.val = GPIOD_INFO_EVENT_LINE_REQUESTED,
+	},
+	{
+		.name = "INFO_EVENT_TYPE_LINE_RELEASED",
+		.val = GPIOD_INFO_EVENT_LINE_RELEASED,
+	},
+	{
+		.name = "INFO_EVENT_TYPE_LINE_CONFIG_CHANGED",
+		.val = GPIOD_INFO_EVENT_LINE_CONFIG_CHANGED,
+	},
+	{ }
+};
+
+static PyObject *
+module_is_gpiochip_device(PyObject *Py_UNUSED(self), PyObject *args)
+{
+	const char *path;
+	int ret;
+
+	ret =  PyArg_ParseTuple(args, "s", &path);
+	if (!ret)
+		return NULL;
+
+	return PyBool_FromLong(gpiod_is_gpiochip_device(path));
+}
+
+static PyMethodDef module_methods[] = {
+	{
+		.ml_name = "is_gpiochip_device",
+		.ml_meth = (PyCFunction)module_is_gpiochip_device,
+		.ml_flags = METH_VARARGS,
+	},
+	{ }
+};
+
+static PyModuleDef module_def = {
+	PyModuleDef_HEAD_INIT,
+	.m_name = "gpiod._ext",
+	.m_methods = module_methods,
+};
+
+extern PyTypeObject chip_type;
+extern PyTypeObject line_config_type;
+extern PyTypeObject line_settings_type;
+extern PyTypeObject request_type;
+
+static PyTypeObject *types[] = {
+	&chip_type,
+	&line_config_type,
+	&line_settings_type,
+	&request_type,
+	NULL,
+};
+
+PyMODINIT_FUNC PyInit__ext(void)
+{
+	const struct module_const *modconst;
+	PyTypeObject **type;
+	PyObject *module;
+	int ret;
+
+	module = PyModule_Create(&module_def);
+	if (!module)
+		return NULL;
+
+	ret = PyModule_AddStringConstant(module, "__version__",
+					 gpiod_version_string());
+	if (ret) {
+		Py_DECREF(module);
+		return NULL;
+	}
+
+	for (type = types; *type; type++) {
+		ret = PyModule_AddType(module, *type);
+		if (ret) {
+			Py_DECREF(module);
+			return NULL;
+		}
+	}
+
+	for (modconst = module_constants; modconst->name; modconst++) {
+		ret = PyModule_AddIntConstant(module,
+					      modconst->name, modconst->val);
+		if (ret) {
+			Py_DECREF(module);
+			return NULL;
+		}
+	}
+
+	return module;
+}
diff --git a/bindings/python/gpiod/ext/request.c b/bindings/python/gpiod/ext/request.c
new file mode 100644
index 0000000..36b5b48
--- /dev/null
+++ b/bindings/python/gpiod/ext/request.c
@@ -0,0 +1,402 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include "internal.h"
+
+typedef struct {
+	PyObject_HEAD;
+	struct gpiod_line_request *request;
+	unsigned int *offsets;
+	int *values;
+	size_t num_lines;
+} request_object;
+
+static int request_init(PyObject *Py_UNUSED(ignored0),
+			PyObject *Py_UNUSED(ignored1),
+			PyObject *Py_UNUSED(ignored2))
+{
+	PyErr_SetString(PyExc_NotImplementedError,
+			"_ext.LineRequest cannot be instantiated");
+
+	return -1;
+}
+
+static void request_finalize(request_object *self)
+{
+	if (self->request)
+		PyObject_CallMethod((PyObject *)self, "release", "");
+
+	if (self->offsets)
+		PyMem_Free(self->offsets);
+
+	if (self->values)
+		PyMem_Free(self->values);
+}
+
+static PyObject *
+request_num_lines(request_object *self, void *Py_UNUSED(ignored))
+{
+	return PyLong_FromUnsignedLong(
+			gpiod_line_request_get_num_lines(self->request));
+}
+
+static PyObject *request_offsets(request_object *self, void *Py_UNUSED(ignored))
+{
+	PyObject *lines, *line;
+	unsigned int *offsets;
+	size_t num_lines, i;
+	int ret;
+
+	num_lines = gpiod_line_request_get_num_lines(self->request);
+
+	offsets = PyMem_Calloc(num_lines, sizeof(unsigned int));
+	if (!offsets)
+		return PyErr_NoMemory();
+
+	gpiod_line_request_get_offsets(self->request, offsets);
+
+	lines = PyList_New(num_lines);
+	if (!lines) {
+		PyMem_Free(offsets);
+		return NULL;
+	}
+
+	for (i = 0; i < num_lines; i++) {
+		line = PyLong_FromUnsignedLong(offsets[i]);
+		if (!lines) {
+			Py_DECREF(lines);
+			PyMem_Free(offsets);
+			return NULL;
+		}
+
+		ret = PyList_SetItem(lines, i, line);
+		if (ret) {
+			Py_DECREF(line);
+			Py_DECREF(lines);
+			PyMem_Free(offsets);
+			return NULL;
+		}
+	}
+
+	PyMem_Free(offsets);
+	return lines;
+}
+
+static PyObject *request_fd(request_object *self, void *Py_UNUSED(ignored))
+{
+	return PyLong_FromLong(gpiod_line_request_get_fd(self->request));
+}
+
+static PyGetSetDef request_getset[] = {
+	{
+		.name = "num_lines",
+		.get = (getter)request_num_lines,
+	},
+	{
+		.name = "offsets",
+		.get = (getter)request_offsets,
+	},
+	{
+		.name = "fd",
+		.get = (getter)request_fd,
+	},
+	{ }
+};
+
+static PyObject *
+request_release(request_object *self, PyObject *Py_UNUSED(ignored))
+{
+	Py_BEGIN_ALLOW_THREADS;
+	gpiod_line_request_release(self->request);
+	Py_END_ALLOW_THREADS;
+	self->request = NULL;
+
+	Py_RETURN_NONE;
+}
+
+static void clear_buffers(request_object *self)
+{
+	memset(self->offsets, 0, self->num_lines * sizeof(unsigned int));
+	memset(self->values, 0, self->num_lines * sizeof(int));
+}
+
+static PyObject *request_get_values(request_object *self, PyObject *args)
+{
+	PyObject *offsets, *values, *val, *type, *iter, *next;
+	Py_ssize_t num_offsets;
+	unsigned int pos;
+	int ret;
+
+	ret = PyArg_ParseTuple(args, "OO", &offsets, &values);
+	if (!ret)
+		return NULL;
+
+	num_offsets = PyObject_Size(offsets);
+	if (num_offsets < 0)
+		return NULL;
+
+	type = Py_gpiod_GetGlobalType("Value");
+	if (!type)
+		return NULL;
+
+	iter = PyObject_GetIter(offsets);
+	if (!iter)
+		return NULL;
+
+	clear_buffers(self);
+
+	for (pos = 0;; pos++) {
+		next = PyIter_Next(iter);
+		if (!next) {
+			Py_DECREF(iter);
+			break;
+		}
+
+		self->offsets[pos] = Py_gpiod_PyLongAsUnsignedInt(next);
+		Py_DECREF(next);
+		if (PyErr_Occurred()) {
+			Py_DECREF(iter);
+			return NULL;
+		}
+	}
+
+	Py_BEGIN_ALLOW_THREADS;
+	ret = gpiod_line_request_get_values_subset(self->request,
+						   self->num_lines,
+						   self->offsets,
+						   self->values);
+	Py_END_ALLOW_THREADS;
+	if (ret)
+		return Py_gpiod_SetErrFromErrno();
+
+	for (pos = 0; pos < num_offsets; pos++) {
+		val = PyObject_CallFunction(type, "i", self->values[pos]);
+		if (!val)
+			return NULL;
+
+		ret = PyList_SetItem(values, pos, val);
+		if (ret) {
+			Py_DECREF(val);
+			return NULL;
+		}
+	}
+
+	Py_RETURN_NONE;
+}
+
+static PyObject *request_set_values(request_object *self, PyObject *args)
+{
+	PyObject *values, *key, *val, *val_stripped;
+	Py_ssize_t pos = 0;
+	int ret;
+
+	ret = PyArg_ParseTuple(args, "O", &values);
+	if (!ret)
+		return NULL;
+
+	clear_buffers(self);
+
+	while (PyDict_Next(values, &pos, &key, &val)) {
+		self->offsets[pos] = Py_gpiod_PyLongAsUnsignedInt(key);
+		if (PyErr_Occurred())
+			return NULL;
+
+		val_stripped = PyObject_GetAttrString(val, "value");
+		if (!val_stripped)
+			return NULL;
+
+		self->values[pos] = PyLong_AsLong(val_stripped);
+		Py_DECREF(val_stripped);
+		if (PyErr_Occurred())
+			return NULL;
+	}
+
+	Py_BEGIN_ALLOW_THREADS;
+	ret = gpiod_line_request_set_values_subset(self->request,
+						   self->num_lines,
+						   self->offsets,
+						   self->values);
+	Py_END_ALLOW_THREADS;
+	if (ret)
+		return Py_gpiod_SetErrFromErrno();
+
+	Py_RETURN_NONE;
+}
+
+static PyObject *request_reconfigure_lines(request_object *self, PyObject *args)
+{
+	struct gpiod_line_config *line_cfg;
+	PyObject *line_cfg_obj;
+	int ret;
+
+	ret = PyArg_ParseTuple(args, "O", &line_cfg_obj);
+	if (!ret)
+		return NULL;
+
+	line_cfg = Py_gpiod_LineConfigGetData(line_cfg_obj);
+	if (!line_cfg)
+		return NULL;
+
+	Py_BEGIN_ALLOW_THREADS;
+	ret = gpiod_line_request_reconfigure_lines(self->request, line_cfg);
+	Py_END_ALLOW_THREADS;
+	if (ret)
+		return Py_gpiod_SetErrFromErrno();
+
+	Py_RETURN_NONE;
+}
+
+static PyObject *request_read_edge_event(request_object *self, PyObject *args)
+{
+	PyObject *max_events_obj, *event_obj, *events, *type;
+	struct gpiod_edge_event_buffer *buffer;
+	size_t max_events, num_events, i;
+	struct gpiod_edge_event *event;
+	int ret;
+
+	ret = PyArg_ParseTuple(args, "O", &max_events_obj);
+	if (!ret)
+		return NULL;
+
+	if (max_events_obj != Py_None) {
+		max_events = PyLong_AsSize_t(max_events_obj);
+		if (PyErr_Occurred())
+			return NULL;
+	} else {
+		max_events = 64;
+	}
+
+	type = Py_gpiod_GetGlobalType("EdgeEvent");
+	if (!type)
+		return NULL;
+
+	buffer = gpiod_edge_event_buffer_new(max_events);
+	if (!buffer)
+		return Py_gpiod_SetErrFromErrno();
+
+	Py_BEGIN_ALLOW_THREADS;
+	ret = gpiod_line_request_read_edge_event(self->request,
+						 buffer, max_events);
+	Py_END_ALLOW_THREADS;
+	if (ret < 0) {
+		gpiod_edge_event_buffer_free(buffer);
+		return NULL;
+	}
+
+	num_events = ret;
+
+	events = PyList_New(num_events);
+	if (!events) {
+		gpiod_edge_event_buffer_free(buffer);
+		return NULL;
+	}
+
+	for (i = 0; i < num_events; i++) {
+		event = gpiod_edge_event_buffer_get_event(buffer, i);
+		if (!event) {
+			gpiod_edge_event_buffer_free(buffer);
+			Py_DECREF(events);
+			return NULL;
+		}
+
+		event_obj = PyObject_CallFunction(type, "iKiii",
+				gpiod_edge_event_get_event_type(event),
+				gpiod_edge_event_get_timestamp_ns(event),
+				gpiod_edge_event_get_line_offset(event),
+				gpiod_edge_event_get_global_seqno(event),
+				gpiod_edge_event_get_line_seqno(event));
+		if (!event_obj) {
+			gpiod_edge_event_buffer_free(buffer);
+			Py_DECREF(events);
+			return NULL;
+		}
+
+		ret = PyList_SetItem(events, i, event_obj);
+		if (ret) {
+			gpiod_edge_event_buffer_free(buffer);
+			Py_DECREF(event_obj);
+			Py_DECREF(events);
+			return NULL;
+		}
+	}
+
+	gpiod_edge_event_buffer_free(buffer);
+
+	return events;
+}
+
+static PyMethodDef request_methods[] = {
+	{
+		.ml_name = "release",
+		.ml_meth = (PyCFunction)request_release,
+		.ml_flags = METH_NOARGS,
+	},
+	{
+		.ml_name = "get_values",
+		.ml_meth = (PyCFunction)request_get_values,
+		.ml_flags = METH_VARARGS,
+	},
+	{
+		.ml_name = "set_values",
+		.ml_meth = (PyCFunction)request_set_values,
+		.ml_flags = METH_VARARGS,
+	},
+	{
+		.ml_name = "reconfigure_lines",
+		.ml_meth = (PyCFunction)request_reconfigure_lines,
+		.ml_flags = METH_VARARGS,
+	},
+	{
+		.ml_name = "read_edge_event",
+		.ml_meth = (PyCFunction)request_read_edge_event,
+		.ml_flags = METH_VARARGS,
+	},
+	{ }
+};
+
+PyTypeObject request_type = {
+	PyVarObject_HEAD_INIT(NULL, 0)
+	.tp_name = "gpiod._ext.Request",
+	.tp_basicsize = sizeof(request_object),
+	.tp_flags = Py_TPFLAGS_DEFAULT,
+	.tp_new = PyType_GenericNew,
+	.tp_init = (initproc)request_init,
+	.tp_finalize = (destructor)request_finalize,
+	.tp_dealloc = (destructor)Py_gpiod_dealloc,
+	.tp_getset = request_getset,
+	.tp_methods = request_methods,
+};
+
+PyObject *Py_gpiod_MakeRequestObject(struct gpiod_line_request *request)
+{
+	request_object *req_obj;
+	unsigned int *offsets;
+	size_t num_lines;
+	int *values;
+
+	num_lines = gpiod_line_request_get_num_lines(request);
+
+	req_obj = PyObject_New(request_object, &request_type);
+	if (!req_obj)
+		return NULL;
+
+	offsets = PyMem_Calloc(num_lines, sizeof(unsigned int));
+	if (!offsets) {
+		Py_DECREF(req_obj);
+		return NULL;
+	}
+
+	values = PyMem_Calloc(num_lines, sizeof(int));
+	if (!values) {
+		PyMem_Free(offsets);
+		Py_DECREF(req_obj);
+		return NULL;
+	}
+
+	req_obj->request = request;
+	req_obj->offsets = offsets;
+	req_obj->values = values;
+	req_obj->num_lines = num_lines;
+
+	return (PyObject *)req_obj;
+}
diff --git a/bindings/python/gpiod/info_event.py b/bindings/python/gpiod/info_event.py
new file mode 100644
index 0000000..78b1459
--- /dev/null
+++ b/bindings/python/gpiod/info_event.py
@@ -0,0 +1,33 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+from . import _ext
+from .line_info import LineInfo
+from dataclasses import dataclass
+from enum import Enum
+
+
+@dataclass(frozen=True, init=False, repr=False)
+class InfoEvent:
+    """
+    Immutable object containing data about a single line info event.
+    """
+
+    class Type(Enum):
+        LINE_REQUESTED = _ext.INFO_EVENT_TYPE_LINE_REQUESTED
+        LINE_RELEASED = _ext.INFO_EVENT_TYPE_LINE_RELEASED
+        LINE_CONFIG_CHANGED = _ext.INFO_EVENT_TYPE_LINE_CONFIG_CHANGED
+
+    event_type: Type
+    timestamp_ns: int
+    line_info: LineInfo
+
+    def __init__(self, event_type: int, timestamp_ns: int, line_info: LineInfo):
+        object.__setattr__(self, "event_type", InfoEvent.Type(event_type))
+        object.__setattr__(self, "timestamp_ns", timestamp_ns)
+        object.__setattr__(self, "line_info", line_info)
+
+    def __str__(self):
+        return "<InfoEvent type={} timestamp_ns={} line_info={}>".format(
+            self.event_type, self.timestamp_ns, self.line_info
+        )
diff --git a/bindings/python/gpiod/internal.py b/bindings/python/gpiod/internal.py
new file mode 100644
index 0000000..37e8b62
--- /dev/null
+++ b/bindings/python/gpiod/internal.py
@@ -0,0 +1,19 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+from datetime import timedelta
+from select import select
+from typing import Optional, Union
+
+
+def poll_fd(fd: int, timeout: Optional[Union[timedelta, float]] = None) -> bool:
+    if timeout is None:
+        timeout = 0.0
+
+    if isinstance(timeout, timedelta):
+        sec = timeout.total_seconds()
+    else:
+        sec = timeout
+
+    readable, _, _ = select([fd], [], [], sec)
+    return True if fd in readable else False
diff --git a/bindings/python/gpiod/line.py b/bindings/python/gpiod/line.py
new file mode 100644
index 0000000..c5d5ddf
--- /dev/null
+++ b/bindings/python/gpiod/line.py
@@ -0,0 +1,56 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+
+from . import _ext
+from enum import Enum
+
+
+class Value(Enum):
+    """Logical line states."""
+
+    INACTIVE = _ext.VALUE_INACTIVE
+    ACTIVE = _ext.VALUE_ACTIVE
+
+
+class Direction(Enum):
+    """Direction settings."""
+
+    AS_IS = _ext.DIRECTION_AS_IS
+    INPUT = _ext.DIRECTION_INPUT
+    OUTPUT = _ext.DIRECTION_OUTPUT
+
+
+class Bias(Enum):
+    """Internal bias settings."""
+
+    AS_IS = _ext.BIAS_AS_IS
+    UNKNOWN = _ext.BIAS_UNKNOWN
+    DISABLED = _ext.BIAS_DISABLED
+    PULL_UP = _ext.BIAS_PULL_UP
+    PULL_DOWN = _ext.BIAS_PULL_DOWN
+
+
+class Drive(Enum):
+    """Drive settings."""
+
+    PUSH_PULL = _ext.DRIVE_PUSH_PULL
+    OPEN_DRAIN = _ext.DRIVE_OPEN_DRAIN
+    OPEN_SOURCE = _ext.DRIVE_OPEN_SOURCE
+
+
+class Edge(Enum):
+    """Edge detection settings."""
+
+    NONE = _ext.EDGE_NONE
+    RISING = _ext.EDGE_RISING
+    FALLING = _ext.EDGE_FALLING
+    BOTH = _ext.EDGE_BOTH
+
+
+class Clock(Enum):
+    """Event clock settings."""
+
+    MONOTONIC = _ext.CLOCK_MONOTONIC
+    REALTIME = _ext.CLOCK_REALTIME
+    HTE = _ext.CLOCK_HTE
diff --git a/bindings/python/gpiod/line_info.py b/bindings/python/gpiod/line_info.py
new file mode 100644
index 0000000..9a6c9bf
--- /dev/null
+++ b/bindings/python/gpiod/line_info.py
@@ -0,0 +1,73 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+from . import _ext
+from dataclasses import dataclass
+from datetime import timedelta
+from gpiod.line import Direction, Bias, Drive, Edge, Clock
+
+
+@dataclass(frozen=True, init=False, repr=False)
+class LineInfo:
+    """
+    Snapshot of a line's status.
+    """
+
+    offset: int
+    name: str
+    used: bool
+    consumer: str
+    direction: Direction
+    active_low: bool
+    bias: Bias
+    drive: Drive
+    edge_detection: Edge
+    event_clock: Clock
+    debounced: bool
+    debounce_period: timedelta
+
+    def __init__(
+        self,
+        offset: int,
+        name: str,
+        used: bool,
+        consumer: str,
+        direction: int,
+        active_low: bool,
+        bias: int,
+        drive: int,
+        edge_detection: int,
+        event_clock: int,
+        debounced: bool,
+        debounce_period_us: int,
+    ):
+        object.__setattr__(self, "offset", offset)
+        object.__setattr__(self, "name", name)
+        object.__setattr__(self, "used", used)
+        object.__setattr__(self, "consumer", consumer)
+        object.__setattr__(self, "direction", Direction(direction))
+        object.__setattr__(self, "active_low", active_low)
+        object.__setattr__(self, "bias", Bias(bias))
+        object.__setattr__(self, "drive", Drive(drive))
+        object.__setattr__(self, "edge_detection", Edge(edge_detection))
+        object.__setattr__(self, "event_clock", Clock(event_clock))
+        object.__setattr__(self, "debounced", debounced)
+        object.__setattr__(
+            self, "debounce_period", timedelta(microseconds=debounce_period_us)
+        )
+
+    def __str__(self):
+        return '<LineInfo offset={} name="{}" used={} consumer="{}" direction={} active_low={} bias={} drive={} edge_detection={} event_clock={} debounced={} debounce_period={}>'.format(
+            self.offset,
+            self.name,
+            self.used,
+            self.consumer,
+            self.direction,
+            self.active_low,
+            self.bias,
+            self.drive,
+            self.edge_detection,
+            self.event_clock,
+            self.debounced,
+            self.debounce_period,
+        )
diff --git a/bindings/python/gpiod/line_request.py b/bindings/python/gpiod/line_request.py
new file mode 100644
index 0000000..a3ee392
--- /dev/null
+++ b/bindings/python/gpiod/line_request.py
@@ -0,0 +1,258 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+from . import _ext
+from .edge_event import EdgeEvent
+from .exception import RequestReleasedError
+from .internal import poll_fd
+from .line import Value
+from .line_settings import LineSettings, _line_settings_to_ext_line_settings
+from collections.abc import Iterable
+from datetime import timedelta
+from typing import Optional, Union
+
+
+class LineRequest:
+    """
+    Stores the context of a set of requested GPIO lines.
+    """
+
+    def __init__(self, req: _ext.Request):
+        """
+        DON'T USE
+
+        LineRequest objects can only be instantiated by a Chip parent. This is
+        not part of stable API.
+        """
+        self._req = req
+
+    def __bool__(self) -> bool:
+        """
+        Boolean conversion for GPIO line requests.
+
+        Returns:
+          True if the request is live and False if it's been released.
+        """
+        return True if self._req else False
+
+    def __enter__(self):
+        """
+        Controlled execution enter callback.
+        """
+        self._check_released()
+        return self
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        """
+        Controlled execution exit callback.
+        """
+        self.release()
+
+    def _check_released(self) -> None:
+        if not self._req:
+            raise RequestReleasedError()
+
+    def release(self) -> None:
+        """
+        Release this request and free all associated resources. The object must
+        not be used after a call to this method.
+        """
+        self._check_released()
+        self._req.release()
+        self._req = None
+
+    def get_value(self, line: Union[int, str]) -> Value:
+        """
+        Get a single GPIO line value.
+
+        Args:
+          line:
+            Offset or name of the line to get value for.
+
+        Returns:
+          Logical value of the line.
+        """
+        return self.get_values([line])[0]
+
+    def get_values(
+        self, lines: Optional[Iterable[Union[int, str]]] = None
+    ) -> list[Value]:
+        """
+        Get values of a set of GPIO lines.
+
+        Args:
+          lines:
+            List of names or offsets of GPIO lines to get values for. Can be
+            None in which case all requested lines will be read.
+
+        Returns:
+          List of logical line values.
+        """
+        self._check_released()
+
+        if lines is None:
+            lines = self._lines
+
+        offsets = [None] * len(lines)
+
+        for i, line in enumerate(lines):
+            if isinstance(line, str):
+                if line not in self._name_map:
+                    raise ValueError("unknown line name: {}".format(line))
+
+                offsets[i] = self._name_map[line]
+            else:
+                offsets[i] = line
+
+        buf = [None] * len(lines)
+
+        self._req.get_values(offsets, buf)
+        return buf
+
+    def set_value(self, line: Union[int, str], value: Value) -> None:
+        """
+        Set the value of a single GPIO line.
+
+        Args:
+          line:
+            Offset or name of the line to set.
+          value:
+            New value.
+        """
+        self.set_values({line: value})
+
+    def set_values(self, values: dict[Union[int, str], Value]) -> None:
+        """
+        Set the values of a subset of GPIO lines.
+
+        Args:
+          values:
+            Dictionary mapping line offsets or names to desired values.
+        """
+        self._check_released()
+
+        mapped = dict()
+        for i, line in enumerate(values):
+            if isinstance(line, str):
+                if line not in self._name_map:
+                    raise ValueError("unknown line name: {}".format(line))
+
+                mapped[self._name_map[line]] = values[line]
+            else:
+                mapped[line] = values[line]
+
+        self._req.set_values(mapped)
+
+    def reconfigure_lines(
+        self, config: dict[tuple[Union[int, str]], LineSettings]
+    ) -> None:
+        """
+        Reconfigure requested lines.
+
+        Args:
+          config
+            Dictionary mapping offsets or names (or tuples thereof) to
+            LineSettings. If None is passed as the value of the mapping,
+            default settings are used.
+        """
+        self._check_released()
+
+        line_cfg = _ext.LineConfig()
+
+        for lines, settings in config.items():
+            if isinstance(lines, int) or isinstance(lines, str):
+                lines = [lines]
+
+            offsets = [None] * len(lines)
+
+            for i, line in enumerate(lines):
+                if isinstance(line, str):
+                    if line not in self._name_map:
+                        raise ValueError("unknown line name: {}".format(line))
+
+                    offsets[i] = self._name_map[line]
+                else:
+                    offsets[i] = line
+
+            line_cfg.add_line_settings(
+                offsets, _line_settings_to_ext_line_settings(settings)
+            )
+
+        self._req.reconfigure_lines(line_cfg)
+
+    def wait_edge_event(
+        self, timeout: Optional[Union[timedelta, float]] = None
+    ) -> bool:
+        """
+        Wait for edge events on any of the requested lines.
+
+        Args:
+          timeout:
+            Wait time limit expressed as either a datetime.timedelta object
+            or the number of seconds stored in a float.
+
+        Returns:
+          True if events are ready to be read. False on timeout.
+        """
+        self._check_released()
+
+        return poll_fd(self.fd, timeout)
+
+    def read_edge_event(self, max_events: Optional[int] = None) -> list[EdgeEvent]:
+        """
+        Read a number of edge events from a line request.
+
+        Args:
+          max_events:
+            Maximum number of events to read.
+
+        Returns:
+          List of read EdgeEvent objects.
+        """
+        self._check_released()
+
+        return self._req.read_edge_event(max_events)
+
+    def __str__(self):
+        """
+        Return a user-friendly, human-readable description of this request.
+        """
+        if not self._req:
+            return "<LineRequest RELEASED>"
+
+        return "<LineRequest num_lines={} offsets={} fd={}>".format(
+            self.num_lines, self.offsets, self.fd
+        )
+
+    @property
+    def num_lines(self) -> int:
+        """
+        Number of requested lines.
+        """
+        self._check_released()
+        return len(self._offsets)
+
+    @property
+    def offsets(self) -> list[int]:
+        """
+        List of requested offsets. Lines requested by name are mapped to their
+        offsets.
+        """
+        self._check_released()
+        return self._offsets
+
+    @property
+    def lines(self) -> list[Union[int, str]]:
+        """
+        List of requested lines. Lines requested by name are listed as such.
+        """
+        self._check_released()
+        return self._lines
+
+    @property
+    def fd(self) -> int:
+        """
+        File descriptor associated with this request.
+        """
+        self._check_released()
+        return self._req.fd
diff --git a/bindings/python/gpiod/line_settings.py b/bindings/python/gpiod/line_settings.py
new file mode 100644
index 0000000..1315b0c
--- /dev/null
+++ b/bindings/python/gpiod/line_settings.py
@@ -0,0 +1,62 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+from . import _ext
+from dataclasses import dataclass
+from datetime import timedelta
+from gpiod.line import Direction, Bias, Drive, Edge, Clock, Value
+
+
+@dataclass(repr=False)
+class LineSettings:
+    """
+    Stores a set of line properties.
+    """
+
+    direction: Direction = Direction.AS_IS
+    edge_detection: Edge = Edge.NONE
+    bias: Bias = Bias.AS_IS
+    drive: Drive = Drive.PUSH_PULL
+    active_low: bool = False
+    debounce_period: timedelta = timedelta()
+    event_clock: Clock = Clock.MONOTONIC
+    output_value: Value = Value.INACTIVE
+
+    # __repr__ generated by @dataclass uses repr for enum members resulting in
+    # an unusable representation as those are of the form: <NAME: $value>
+    def __repr__(self):
+        return "LineSettings(direction={}, edge_detection={} bias={} drive={} active_low={} debounce_period={} event_clock={} output_value={})".format(
+            str(self.direction),
+            str(self.edge_detection),
+            str(self.bias),
+            str(self.drive),
+            self.active_low,
+            repr(self.debounce_period),
+            str(self.event_clock),
+            str(self.output_value),
+        )
+
+    def __str__(self):
+        return "<LineSettings direction={} edge_detection={} bias={} drive={} active_low={} debounce_period={} event_clock={} output_value={}>".format(
+            self.direction,
+            self.edge_detection,
+            self.bias,
+            self.drive,
+            self.active_low,
+            self.debounce_period,
+            self.event_clock,
+            self.output_value,
+        )
+
+
+def _line_settings_to_ext_line_settings(settings: LineSettings) -> _ext.LineSettings:
+    return _ext.LineSettings(
+        direction=settings.direction.value,
+        edge_detection=settings.edge_detection.value,
+        bias=settings.bias.value,
+        drive=settings.drive.value,
+        active_low=settings.active_low,
+        debounce_period=int(settings.debounce_period.total_seconds() * 1000000),
+        event_clock=settings.event_clock.value,
+        output_value=settings.output_value.value,
+    )
diff --git a/bindings/python/setup.py b/bindings/python/setup.py
new file mode 100644
index 0000000..ec8f99d
--- /dev/null
+++ b/bindings/python/setup.py
@@ -0,0 +1,47 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+from os import environ
+from setuptools import setup, Extension, find_packages
+
+gpiod_ext = Extension(
+    "gpiod._ext",
+    sources=[
+        "gpiod/ext/chip.c",
+        "gpiod/ext/common.c",
+        "gpiod/ext/line-config.c",
+        "gpiod/ext/line-settings.c",
+        "gpiod/ext/module.c",
+        "gpiod/ext/request.c",
+    ],
+    define_macros=[("_GNU_SOURCE", "1")],
+    libraries=["gpiod"],
+    extra_compile_args=["-Wall", "-Wextra"],
+)
+
+gpiosim_ext = Extension(
+    "tests.gpiosim._ext",
+    sources=["tests/gpiosim/ext.c"],
+    define_macros=[("_GNU_SOURCE", "1")],
+    libraries=["gpiosim"],
+    extra_compile_args=["-Wall", "-Wextra"],
+)
+
+extensions = [gpiod_ext]
+with_tests = bool(environ["GPIOD_WITH_TESTS"])
+if with_tests:
+    extensions.append(gpiosim_ext)
+
+# FIXME Find a better way to get the version
+version = None
+try:
+    version = environ["GPIOD_VERSION_STR"]
+except KeyError:
+    pass
+
+setup(
+    name="gpiod",
+    packages=find_packages(include=["gpiod"]),
+    ext_modules=extensions,
+    version=version,
+)
diff --git a/configure.ac b/configure.ac
index 6ac1d8e..048b2ac 100644
--- a/configure.ac
+++ b/configure.ac
@@ -244,8 +244,11 @@ AC_CONFIG_FILES([Makefile
 		 bindings/cxx/examples/Makefile
 		 bindings/cxx/tests/Makefile
 		 bindings/python/Makefile
+		 bindings/python/gpiod/Makefile
+		 bindings/python/gpiod/ext/Makefile
 		 bindings/python/examples/Makefile
 		 bindings/python/tests/Makefile
+		 bindings/python/tests/gpiosim/Makefile
 		 man/Makefile])
 
 AC_OUTPUT
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* Re: [libgpiod v2][PATCH v3 4/4] bindings: python: implement python bindings for libgpiod v2
  2022-10-07 14:55 ` [libgpiod v2][PATCH v3 4/4] bindings: python: implement python bindings for libgpiod v2 Bartosz Golaszewski
@ 2022-10-07 15:26   ` Andy Shevchenko
  2022-10-07 18:19     ` Bartosz Golaszewski
  2022-10-13  3:10   ` Kent Gibson
  1 sibling, 1 reply; 30+ messages in thread
From: Andy Shevchenko @ 2022-10-07 15:26 UTC (permalink / raw)
  To: Bartosz Golaszewski; +Cc: Kent Gibson, Linus Walleij, Viresh Kumar, linux-gpio

On Fri, Oct 07, 2022 at 04:55:21PM +0200, Bartosz Golaszewski wrote:
> This adds python bindings for libgpiod v2. As opposed to v1, they are
> mostly written in python with just low-level elements written in C and
> interfacing with libgpiod.so.
> 
> We've also added setup.py which will allow to use pip for managing the
> bindings and split them into a separate meta-openembedded recipe.

...

> +	line_settings.py 

Trailing white space.

...

> +__version__ = _ext.__version__
> +
> +

Single blank line is enough, no?
Same seems applicable to other files.

...

> +def request_lines(path: str, *args, **kwargs) -> LineRequest:
> +    """
> +    Open a GPIO chip pointed to by 'path', request lines according to the
> +    configuration arguments, close the chip and return the request object.

This description seems wrong. First we collect the request object, then close
the chip, right?

> +    Args:
> +      path
> +        Path to the GPIO character device file.
> +      *args
> +      **kwargs
> +        See Chip.request_lines() for configuration arguments.
> +
> +    Returns:
> +      Returns a new LineRequest object.
> +    """
> +    with Chip(path) as chip:
> +        return chip.request_lines(*args, **kwargs)

...

> +class Chip:
> +    """
> +    Represents a GPIO chip.
> +
> +    Chip object manages all resources associated with the GPIO chip it represents.
> +
> +    The gpiochip device file is opened during the object's construction. The Chip
> +    object's constructor takes the path to the GPIO chip device file
> +    as the only argument.
> +
> +    Callers must close the chip by calling the close() method when it's no longer
> +    used.
> +
> +    Example:
> +
> +        chip = gpiod.Chip(\"/dev/gpiochip0\")
> +        do_something(chip)
> +        chip.close()
> +
> +    The gpiod.Chip class also supports controlled execution ('with' statement).

Oh, this does not sound pythonic, does it? I would expect that this will be closed
automatically on object destruction.

Or user may call it explicitly by

	del chip

Of course .close() method may be good to have.

> +    Example:
> +
> +        with gpiod.Chip(path="/dev/gpiochip0") as chip:
> +            do_something(chip)
> +    """
> +
> +    def __init__(self, path: str):
> +        """
> +        Open a GPIO device.
> +
> +        Args:
> +          path:
> +            Path to the GPIO character device file.
> +        """
> +        self._chip = _ext.Chip(path)
> +
> +    def __bool__(self) -> bool:
> +        """
> +        Boolean conversion for GPIO chips.
> +
> +        Returns:
> +          True if the chip is open and False if it's closed.
> +        """
> +        return True if self._chip else False
> +
> +    def __enter__(self):
> +        """
> +        Controlled execution enter callback.
> +        """

> +        self._check_closed()

I don't understand this dance in the most of the methods. Why do we need to
check this?

> +        return self
> +
> +    def __exit__(self, exc_type, exc_value, traceback) -> None:
> +        """
> +        Controlled execution exit callback.
> +        """
> +        self.close()
> +
> +    def _check_closed(self) -> None:
> +        if not self._chip:
> +            raise ChipClosedError()
> +
> +    def close(self) -> None:
> +        """
> +        Close the associated GPIO chip descriptor. The chip object must no
> +        longer be used after this method is called.
> +        """
> +        self._check_closed()
> +        self._chip.close()
> +        self._chip = None
> +
> +    def get_info(self) -> ChipInfo:
> +        """
> +        Get the information about the chip.
> +
> +        Returns:
> +          New gpiod.ChipInfo object.
> +        """
> +        self._check_closed()
> +        return self._chip.get_info()
> +
> +    def map_line(self, id: Union[str, int]) -> int:
> +        """
> +        Map a line's identifier to its offset within the chip.
> +
> +        Args:
> +          id:
> +            Name of the GPIO line, its offset as a string or its offset as an
> +            integer.
> +
> +        Returns:
> +          If id is an integer - it's returned as is (unless it's out of range
> +          for this chip). If it's a string, the method tries to interpret it as
> +          the name of the line first and tries too perform a name lookup within
> +          the chip. If it fails, it tries to convert the string to an integer
> +          and check if it represents a valid offset within the chip and if
> +          so - returns it.
> +        """
> +        self._check_closed()

> +        if not isinstance(id, int):

Why not use positive conditional here and everywhere in the similar cases?

	if isinstance():
		...
	else
		...

> +            try:
> +                return self._chip.map_line(id)
> +            except OSError as ex:
> +                if ex.errno == ENOENT:

> +                    try:
> +                        offset = int(id)
> +                    except ValueError:
> +                        raise ex

How this can be non-exceptional? I don't see how id can change its type from
non-int (which is checked above by isinstance() call) to int?

> +                else:
> +                    raise ex

Last time I have interaction with Python and developers who write the code
in Python the custom exception class was good thing to have. Is this changed
during the times?

> +        else:
> +            offset = id
> +
> +        if offset >= self.get_info().num_lines:
> +            raise ValueError("line offset of out range")
> +
> +        return offset
> +
> +    def _get_line_info(self, line: Union[int, str], watch: bool) -> LineInfo:
> +        self._check_closed()
> +        return self._chip.get_line_info(self.map_line(line), watch)
> +
> +    def get_line_info(self, line: Union[int, str]) -> LineInfo:
> +        """
> +        Get the snapshot of information about the line at given offset.
> +
> +        Args:
> +          line:
> +            Offset or name of the GPIO line to get information for.
> +
> +        Returns:
> +          New LineInfo object.
> +        """
> +        return self._get_line_info(line, watch=False)
> +
> +    def watch_line_info(self, line: Union[int, str]) -> LineInfo:
> +        """
> +        Get the snapshot of information about the line at given offset and
> +        start watching it for future changes.
> +
> +        Args:
> +          line:
> +            Offset or name of the GPIO line to get information for.
> +
> +        Returns:
> +          New gpiod.LineInfo object.
> +        """
> +        return self._get_line_info(line, watch=True)
> +
> +    def unwatch_line_info(self, line: Union[int, str]) -> None:
> +        """
> +        Stop watching a line for status changes.
> +
> +        Args:
> +          line:
> +            Offset or name of the line to stop watching.
> +        """
> +        self._check_closed()
> +        return self._chip.unwatch_line_info(self.map_line(line))

> +    def wait_info_event(
> +        self, timeout: Optional[Union[timedelta, float]] = None
> +    ) -> bool:

pylint (btw, do you use it?) has a limit of 100 for a long time. The above can
be places on one line.

> +        """
> +        Wait for line status change events on any of the watched lines on the
> +        chip.
> +
> +        Args:
> +          timeout:
> +            Wait time limit represented as either a datetime.timedelta object
> +            or the number of seconds stored in a float.
> +
> +        Returns:
> +          True if an info event is ready to be read from the chip, False if the
> +          wait timed out without any events.
> +        """
> +        self._check_closed()
> +
> +        return poll_fd(self.fd, timeout)
> +
> +    def read_info_event(self) -> InfoEvent:
> +        """
> +        Read a single line status change event from the chip.
> +
> +        Returns:
> +          New gpiod.InfoEvent object.
> +
> +        Note:
> +          This function may block if there are no available events in the queue.
> +        """
> +        self._check_closed()
> +        return self._chip.read_info_event()
> +
> +    def request_lines(
> +        self,
> +        config: dict[tuple[Union[int, str]], Optional[LineSettings]],
> +        consumer: Optional[str] = None,
> +        event_buffer_size: Optional[int] = None,
> +    ) -> LineRequest:
> +        """
> +        Request a set of lines for exclusive usage.
> +
> +        Args:
> +          config:
> +            Dictionary mapping offsets or names (or tuples thereof) to
> +            LineSettings. If None is passed as the value of the mapping,
> +            default settings are used.
> +          consumer:
> +            Consumer string to use for this request.
> +          event_buffer_size:
> +            Size of the kernel edge event buffer to configure for this request.
> +
> +        Returns:
> +          New LineRequest object.
> +        """
> +        self._check_closed()
> +
> +        line_cfg = _ext.LineConfig()
> +
> +        for lines, settings in config.items():
> +            offsets = list()
> +            name_map = dict()
> +            offset_map = dict()

> +            if isinstance(lines, int) or isinstance(lines, str):
> +                lines = (lines,)

Instead of putting str and int objects into one bucket, handling exceptions,
etc, I would rather see two methods (one per type).

> +            for line in lines:
> +                offset = self.map_line(line)
> +                offsets.append(offset)
> +                if isinstance(line, str):
> +                    name_map[line] = offset
> +                    offset_map[offset] = line
> +
> +            if settings is None:
> +                settings = LineSettings()
> +
> +            line_cfg.add_line_settings(
> +                offsets, _line_settings_to_ext_line_settings(settings)
> +            )
> +
> +        req_internal = self._chip.request_lines(line_cfg, consumer, event_buffer_size)
> +        request = LineRequest(req_internal)
> +
> +        request._offsets = req_internal.offsets
> +        request._name_map = name_map
> +        request._offset_map = offset_map
> +
> +        request._lines = list()
> +        for off in request.offsets:
> +            request._lines.append(offset_map[off] if off in offset_map else off)
> +
> +        return request
> +
> +    def __repr__(self) -> str:
> +        """
> +        Return a string that can be used to re-create this chip object.
> +        """
> +        if not self._chip:
> +            return "<Chip CLOSED>"
> +
> +        return 'Chip("{}")'.format(self.path)
> +
> +    def __str__(self) -> str:
> +        """
> +        Return a user-friendly, human-readable description of this chip.
> +        """
> +        if not self._chip:
> +            return "<Chip CLOSED>"
> +
> +        return '<Chip path="{}" fd={} info={}>'.format(
> +            self.path, self.fd, self.get_info()
> +        )
> +
> +    @property
> +    def path(self) -> str:
> +        """
> +        Filesystem path used to open this chip.
> +        """
> +        self._check_closed()
> +        return self._chip.path
> +
> +    @property
> +    def fd(self) -> int:
> +        """
> +        File descriptor associated with this chip.
> +        """
> +        self._check_closed()
> +        return self._chip.fd

-- 
With Best Regards,
Andy Shevchenko



^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [libgpiod v2][PATCH v3 4/4] bindings: python: implement python bindings for libgpiod v2
  2022-10-07 15:26   ` Andy Shevchenko
@ 2022-10-07 18:19     ` Bartosz Golaszewski
  0 siblings, 0 replies; 30+ messages in thread
From: Bartosz Golaszewski @ 2022-10-07 18:19 UTC (permalink / raw)
  To: Andy Shevchenko; +Cc: Kent Gibson, Linus Walleij, Viresh Kumar, linux-gpio

On Fri, Oct 7, 2022 at 5:26 PM Andy Shevchenko
<andriy.shevchenko@linux.intel.com> wrote:
>
> On Fri, Oct 07, 2022 at 04:55:21PM +0200, Bartosz Golaszewski wrote:
> > This adds python bindings for libgpiod v2. As opposed to v1, they are
> > mostly written in python with just low-level elements written in C and
> > interfacing with libgpiod.so.
> >
> > We've also added setup.py which will allow to use pip for managing the
> > bindings and split them into a separate meta-openembedded recipe.
>
> ...
>
> > +     line_settings.py
>
> Trailing white space.
>
> ...
>
> > +__version__ = _ext.__version__
> > +
> > +
>
> Single blank line is enough, no?
> Same seems applicable to other files.
>

I use black (https://pypi.org/project/black/) for formatting and it
does this between classes and in some other cases. I like this
formatting and it seems it's in line with various PEPs.

> ...
>
> > +def request_lines(path: str, *args, **kwargs) -> LineRequest:
> > +    """
> > +    Open a GPIO chip pointed to by 'path', request lines according to the
> > +    configuration arguments, close the chip and return the request object.
>
> This description seems wrong. First we collect the request object, then close
> the chip, right?
>

Well, yeah, we return it after we close the chip. Not sure what you mean.

> > +    Args:
> > +      path
> > +        Path to the GPIO character device file.
> > +      *args
> > +      **kwargs
> > +        See Chip.request_lines() for configuration arguments.
> > +
> > +    Returns:
> > +      Returns a new LineRequest object.
> > +    """
> > +    with Chip(path) as chip:
> > +        return chip.request_lines(*args, **kwargs)
>
> ...
>
> > +class Chip:
> > +    """
> > +    Represents a GPIO chip.
> > +
> > +    Chip object manages all resources associated with the GPIO chip it represents.
> > +
> > +    The gpiochip device file is opened during the object's construction. The Chip
> > +    object's constructor takes the path to the GPIO chip device file
> > +    as the only argument.
> > +
> > +    Callers must close the chip by calling the close() method when it's no longer
> > +    used.
> > +
> > +    Example:
> > +
> > +        chip = gpiod.Chip(\"/dev/gpiochip0\")
> > +        do_something(chip)
> > +        chip.close()
> > +
> > +    The gpiod.Chip class also supports controlled execution ('with' statement).
>
> Oh, this does not sound pythonic, does it? I would expect that this will be closed
> automatically on object destruction.
>
> Or user may call it explicitly by
>
>         del chip
>
> Of course .close() method may be good to have.
>

Python is not like C++, it doesn't have RAII. There are no guarantees
from the language itself that an object will be destroyed the moment
the last reference is dropped. It's more lika java in that aspect.
It's true that cpython implementation works like this for most cases
but we must not rely on any given implementation. Controlled execution
is very much the pythonic way.

> > +    Example:
> > +
> > +        with gpiod.Chip(path="/dev/gpiochip0") as chip:
> > +            do_something(chip)
> > +    """
> > +
> > +    def __init__(self, path: str):
> > +        """
> > +        Open a GPIO device.
> > +
> > +        Args:
> > +          path:
> > +            Path to the GPIO character device file.
> > +        """
> > +        self._chip = _ext.Chip(path)
> > +
> > +    def __bool__(self) -> bool:
> > +        """
> > +        Boolean conversion for GPIO chips.
> > +
> > +        Returns:
> > +          True if the chip is open and False if it's closed.
> > +        """
> > +        return True if self._chip else False
> > +
> > +    def __enter__(self):
> > +        """
> > +        Controlled execution enter callback.
> > +        """
>
> > +        self._check_closed()
>
> I don't understand this dance in the most of the methods. Why do we need to
> check this?
>

Because we don't want to allow the user to use a closed chip? The
underlying C part doesn't check it so if we don't do it here, it will
segfault.

> > +        return self
> > +
> > +    def __exit__(self, exc_type, exc_value, traceback) -> None:
> > +        """
> > +        Controlled execution exit callback.
> > +        """
> > +        self.close()
> > +
> > +    def _check_closed(self) -> None:
> > +        if not self._chip:
> > +            raise ChipClosedError()
> > +
> > +    def close(self) -> None:
> > +        """
> > +        Close the associated GPIO chip descriptor. The chip object must no
> > +        longer be used after this method is called.
> > +        """
> > +        self._check_closed()
> > +        self._chip.close()
> > +        self._chip = None
> > +
> > +    def get_info(self) -> ChipInfo:
> > +        """
> > +        Get the information about the chip.
> > +
> > +        Returns:
> > +          New gpiod.ChipInfo object.
> > +        """
> > +        self._check_closed()
> > +        return self._chip.get_info()
> > +
> > +    def map_line(self, id: Union[str, int]) -> int:
> > +        """
> > +        Map a line's identifier to its offset within the chip.
> > +
> > +        Args:
> > +          id:
> > +            Name of the GPIO line, its offset as a string or its offset as an
> > +            integer.
> > +
> > +        Returns:
> > +          If id is an integer - it's returned as is (unless it's out of range
> > +          for this chip). If it's a string, the method tries to interpret it as
> > +          the name of the line first and tries too perform a name lookup within
> > +          the chip. If it fails, it tries to convert the string to an integer
> > +          and check if it represents a valid offset within the chip and if
> > +          so - returns it.
> > +        """
> > +        self._check_closed()
>
> > +        if not isinstance(id, int):
>
> Why not use positive conditional here and everywhere in the similar cases?
>
>         if isinstance():
>                 ...
>         else
>                 ...
>
> > +            try:
> > +                return self._chip.map_line(id)
> > +            except OSError as ex:
> > +                if ex.errno == ENOENT:
>
> > +                    try:
> > +                        offset = int(id)
> > +                    except ValueError:
> > +                        raise ex
>
> How this can be non-exceptional? I don't see how id can change its type from
> non-int (which is checked above by isinstance() call) to int?
>

It's explained in the pydoc for this method. :)

If the line ID is not an int, try to map the name to an offset, if it
fails, try to convert it to offset from whatever type (officially only
string but it's python) was supplied.

> > +                else:
> > +                    raise ex
>
> Last time I have interaction with Python and developers who write the code
> in Python the custom exception class was good thing to have. Is this changed
> during the times?
>

We do have custom exceptions, see gpiod/exception.py. It's just that
in this case we want to propagate whatever exception was raised by
libgpiod - OSError translates directly from errno.

> > +        else:
> > +            offset = id
> > +
> > +        if offset >= self.get_info().num_lines:
> > +            raise ValueError("line offset of out range")
> > +
> > +        return offset
> > +
> > +    def _get_line_info(self, line: Union[int, str], watch: bool) -> LineInfo:
> > +        self._check_closed()
> > +        return self._chip.get_line_info(self.map_line(line), watch)
> > +
> > +    def get_line_info(self, line: Union[int, str]) -> LineInfo:
> > +        """
> > +        Get the snapshot of information about the line at given offset.
> > +
> > +        Args:
> > +          line:
> > +            Offset or name of the GPIO line to get information for.
> > +
> > +        Returns:
> > +          New LineInfo object.
> > +        """
> > +        return self._get_line_info(line, watch=False)
> > +
> > +    def watch_line_info(self, line: Union[int, str]) -> LineInfo:
> > +        """
> > +        Get the snapshot of information about the line at given offset and
> > +        start watching it for future changes.
> > +
> > +        Args:
> > +          line:
> > +            Offset or name of the GPIO line to get information for.
> > +
> > +        Returns:
> > +          New gpiod.LineInfo object.
> > +        """
> > +        return self._get_line_info(line, watch=True)
> > +
> > +    def unwatch_line_info(self, line: Union[int, str]) -> None:
> > +        """
> > +        Stop watching a line for status changes.
> > +
> > +        Args:
> > +          line:
> > +            Offset or name of the line to stop watching.
> > +        """
> > +        self._check_closed()
> > +        return self._chip.unwatch_line_info(self.map_line(line))
>
> > +    def wait_info_event(
> > +        self, timeout: Optional[Union[timedelta, float]] = None
> > +    ) -> bool:
>
> pylint (btw, do you use it?) has a limit of 100 for a long time. The above can
> be places on one line.
>

I use black and it enforces a 88 character limit.

> > +        """
> > +        Wait for line status change events on any of the watched lines on the
> > +        chip.
> > +
> > +        Args:
> > +          timeout:
> > +            Wait time limit represented as either a datetime.timedelta object
> > +            or the number of seconds stored in a float.
> > +
> > +        Returns:
> > +          True if an info event is ready to be read from the chip, False if the
> > +          wait timed out without any events.
> > +        """
> > +        self._check_closed()
> > +
> > +        return poll_fd(self.fd, timeout)
> > +
> > +    def read_info_event(self) -> InfoEvent:
> > +        """
> > +        Read a single line status change event from the chip.
> > +
> > +        Returns:
> > +          New gpiod.InfoEvent object.
> > +
> > +        Note:
> > +          This function may block if there are no available events in the queue.
> > +        """
> > +        self._check_closed()
> > +        return self._chip.read_info_event()
> > +
> > +    def request_lines(
> > +        self,
> > +        config: dict[tuple[Union[int, str]], Optional[LineSettings]],
> > +        consumer: Optional[str] = None,
> > +        event_buffer_size: Optional[int] = None,
> > +    ) -> LineRequest:
> > +        """
> > +        Request a set of lines for exclusive usage.
> > +
> > +        Args:
> > +          config:
> > +            Dictionary mapping offsets or names (or tuples thereof) to
> > +            LineSettings. If None is passed as the value of the mapping,
> > +            default settings are used.
> > +          consumer:
> > +            Consumer string to use for this request.
> > +          event_buffer_size:
> > +            Size of the kernel edge event buffer to configure for this request.
> > +
> > +        Returns:
> > +          New LineRequest object.
> > +        """
> > +        self._check_closed()
> > +
> > +        line_cfg = _ext.LineConfig()
> > +
> > +        for lines, settings in config.items():
> > +            offsets = list()
> > +            name_map = dict()
> > +            offset_map = dict()
>
> > +            if isinstance(lines, int) or isinstance(lines, str):
> > +                lines = (lines,)
>
> Instead of putting str and int objects into one bucket, handling exceptions,
> etc, I would rather see two methods (one per type).
>

But I very much want to leverage python's dynamic typing and allow
mixing the two.

Bartosz

> > +            for line in lines:
> > +                offset = self.map_line(line)
> > +                offsets.append(offset)
> > +                if isinstance(line, str):
> > +                    name_map[line] = offset
> > +                    offset_map[offset] = line
> > +
> > +            if settings is None:
> > +                settings = LineSettings()
> > +
> > +            line_cfg.add_line_settings(
> > +                offsets, _line_settings_to_ext_line_settings(settings)
> > +            )
> > +
> > +        req_internal = self._chip.request_lines(line_cfg, consumer, event_buffer_size)
> > +        request = LineRequest(req_internal)
> > +
> > +        request._offsets = req_internal.offsets
> > +        request._name_map = name_map
> > +        request._offset_map = offset_map
> > +
> > +        request._lines = list()
> > +        for off in request.offsets:
> > +            request._lines.append(offset_map[off] if off in offset_map else off)
> > +
> > +        return request
> > +
> > +    def __repr__(self) -> str:
> > +        """
> > +        Return a string that can be used to re-create this chip object.
> > +        """
> > +        if not self._chip:
> > +            return "<Chip CLOSED>"
> > +
> > +        return 'Chip("{}")'.format(self.path)
> > +
> > +    def __str__(self) -> str:
> > +        """
> > +        Return a user-friendly, human-readable description of this chip.
> > +        """
> > +        if not self._chip:
> > +            return "<Chip CLOSED>"
> > +
> > +        return '<Chip path="{}" fd={} info={}>'.format(
> > +            self.path, self.fd, self.get_info()
> > +        )
> > +
> > +    @property
> > +    def path(self) -> str:
> > +        """
> > +        Filesystem path used to open this chip.
> > +        """
> > +        self._check_closed()
> > +        return self._chip.path
> > +
> > +    @property
> > +    def fd(self) -> int:
> > +        """
> > +        File descriptor associated with this chip.
> > +        """
> > +        self._check_closed()
> > +        return self._chip.fd
>
> --
> With Best Regards,
> Andy Shevchenko
>
>

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [libgpiod v2][PATCH v3 0/4] bindings: implement python bindings for libgpiod v2
  2022-10-07 14:55 [libgpiod v2][PATCH v3 0/4] bindings: implement python bindings for libgpiod v2 Bartosz Golaszewski
                   ` (3 preceding siblings ...)
  2022-10-07 14:55 ` [libgpiod v2][PATCH v3 4/4] bindings: python: implement python bindings for libgpiod v2 Bartosz Golaszewski
@ 2022-10-12 12:34 ` Bartosz Golaszewski
  2022-10-12 12:41   ` Kent Gibson
  4 siblings, 1 reply; 30+ messages in thread
From: Bartosz Golaszewski @ 2022-10-12 12:34 UTC (permalink / raw)
  To: Kent Gibson, Linus Walleij, Andy Shevchenko, Viresh Kumar; +Cc: linux-gpio

On Fri, Oct 7, 2022 at 4:55 PM Bartosz Golaszewski <brgl@bgdev.pl> wrote:
>
> This is the third iteration of python bindings for libgpiod but it really has
> very little in common with the previous version.
>
> This time the code has been split into high-level python and low-level
> C layers with the latter only doing the bare minimum.
>
> The data model is mostly based on the C++ one with the main difference
> being utilizing dynamic typing and keyword arguments in place of the
> builder pattern. That allows us to reduce the number of methods and
> objects.
>
> Because python doesn't have RAII, unlike C++, we provide a module-level
> request_lines() helper as gpiod.Chip(path).request_lines(...) one-liner
> could lead to the chip left dangling even after the last reference is
> dropped.
>
> Because python forces us to dynamically allocate objects all the time even
> for fundamental types (which are also immutable) there's no point in trying
> to replicate the edge-event buffer. Instead LineRequest.read_edge_event()
> just returns a list of events.
>
> Bartosz Golaszewski (4):
>   bindings: python: remove old version
>   bindings: python: add examples
>   bindings: python: add tests
>   bindings: python: implement python bindings for libgpiod v2
>
>  bindings/python/.gitignore                   |    8 +
>  bindings/python/Makefile.am                  |   26 +-
>  bindings/python/examples/Makefile.am         |   16 +-
>  bindings/python/examples/gpiodetect.py       |   15 +-
>  bindings/python/examples/gpiofind.py         |   14 +-
>  bindings/python/examples/gpioget.py          |   34 +-
>  bindings/python/examples/gpioinfo.py         |   41 +-
>  bindings/python/examples/gpiomon.py          |   52 +-
>  bindings/python/examples/gpioset.py          |   46 +-
>  bindings/python/gpiod/Makefile.am            |   17 +
>  bindings/python/gpiod/__init__.py            |   53 +
>  bindings/python/gpiod/chip.py                |  308 ++
>  bindings/python/gpiod/chip_info.py           |   21 +
>  bindings/python/gpiod/edge_event.py          |   46 +
>  bindings/python/gpiod/exception.py           |   20 +
>  bindings/python/gpiod/ext/Makefile.am        |   11 +
>  bindings/python/gpiod/ext/chip.c             |  335 +++
>  bindings/python/gpiod/ext/common.c           |   92 +
>  bindings/python/gpiod/ext/internal.h         |   20 +
>  bindings/python/gpiod/ext/line-config.c      |  133 +
>  bindings/python/gpiod/ext/line-settings.c    |  130 +
>  bindings/python/gpiod/ext/module.c           |  193 ++
>  bindings/python/gpiod/ext/request.c          |  402 +++
>  bindings/python/gpiod/info_event.py          |   33 +
>  bindings/python/gpiod/internal.py            |   19 +
>  bindings/python/gpiod/line.py                |   56 +
>  bindings/python/gpiod/line_info.py           |   73 +
>  bindings/python/gpiod/line_request.py        |  258 ++
>  bindings/python/gpiod/line_settings.py       |   62 +
>  bindings/python/gpiodmodule.c                | 2662 ------------------
>  bindings/python/setup.py                     |   47 +
>  bindings/python/tests/Makefile.am            |   26 +-
>  bindings/python/tests/__init__.py            |   17 +
>  bindings/python/tests/__main__.py            |   16 +
>  bindings/python/tests/gpiod_py_test.py       |  832 ------
>  bindings/python/tests/gpiomockupmodule.c     |  309 --
>  bindings/python/tests/gpiosim/Makefile.am    |    7 +
>  bindings/python/tests/gpiosim/__init__.py    |    4 +
>  bindings/python/tests/gpiosim/chip.py        |   66 +
>  bindings/python/tests/gpiosim/ext.c          |  345 +++
>  bindings/python/tests/helpers.py             |   16 +
>  bindings/python/tests/tests_chip.py          |  231 ++
>  bindings/python/tests/tests_chip_info.py     |   52 +
>  bindings/python/tests/tests_edge_event.py    |  219 ++
>  bindings/python/tests/tests_info_event.py    |  189 ++
>  bindings/python/tests/tests_line_info.py     |  101 +
>  bindings/python/tests/tests_line_request.py  |  449 +++
>  bindings/python/tests/tests_line_settings.py |   79 +
>  bindings/python/tests/tests_module.py        |   59 +
>  configure.ac                                 |    3 +
>  50 files changed, 4338 insertions(+), 3925 deletions(-)
>  create mode 100644 bindings/python/.gitignore
>  create mode 100644 bindings/python/gpiod/Makefile.am
>  create mode 100644 bindings/python/gpiod/__init__.py
>  create mode 100644 bindings/python/gpiod/chip.py
>  create mode 100644 bindings/python/gpiod/chip_info.py
>  create mode 100644 bindings/python/gpiod/edge_event.py
>  create mode 100644 bindings/python/gpiod/exception.py
>  create mode 100644 bindings/python/gpiod/ext/Makefile.am
>  create mode 100644 bindings/python/gpiod/ext/chip.c
>  create mode 100644 bindings/python/gpiod/ext/common.c
>  create mode 100644 bindings/python/gpiod/ext/internal.h
>  create mode 100644 bindings/python/gpiod/ext/line-config.c
>  create mode 100644 bindings/python/gpiod/ext/line-settings.c
>  create mode 100644 bindings/python/gpiod/ext/module.c
>  create mode 100644 bindings/python/gpiod/ext/request.c
>  create mode 100644 bindings/python/gpiod/info_event.py
>  create mode 100644 bindings/python/gpiod/internal.py
>  create mode 100644 bindings/python/gpiod/line.py
>  create mode 100644 bindings/python/gpiod/line_info.py
>  create mode 100644 bindings/python/gpiod/line_request.py
>  create mode 100644 bindings/python/gpiod/line_settings.py
>  delete mode 100644 bindings/python/gpiodmodule.c
>  create mode 100644 bindings/python/setup.py
>  create mode 100644 bindings/python/tests/__init__.py
>  create mode 100644 bindings/python/tests/__main__.py
>  delete mode 100755 bindings/python/tests/gpiod_py_test.py
>  delete mode 100644 bindings/python/tests/gpiomockupmodule.c
>  create mode 100644 bindings/python/tests/gpiosim/Makefile.am
>  create mode 100644 bindings/python/tests/gpiosim/__init__.py
>  create mode 100644 bindings/python/tests/gpiosim/chip.py
>  create mode 100644 bindings/python/tests/gpiosim/ext.c
>  create mode 100644 bindings/python/tests/helpers.py
>  create mode 100644 bindings/python/tests/tests_chip.py
>  create mode 100644 bindings/python/tests/tests_chip_info.py
>  create mode 100644 bindings/python/tests/tests_edge_event.py
>  create mode 100644 bindings/python/tests/tests_info_event.py
>  create mode 100644 bindings/python/tests/tests_line_info.py
>  create mode 100644 bindings/python/tests/tests_line_request.py
>  create mode 100644 bindings/python/tests/tests_line_settings.py
>  create mode 100644 bindings/python/tests/tests_module.py
>
> --
> 2.34.1
>

I fixed the one nit from Andy. If there are no objections I'd like to
apply this and squash the entire v2 patch series into one big commit
and apply it to the master branch. This way we can stop keeping this
temporary branch and continue the development (tools, rust, possible
further tweaks to the API) on master.

Bart

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [libgpiod v2][PATCH v3 0/4] bindings: implement python bindings for libgpiod v2
  2022-10-12 12:34 ` [libgpiod v2][PATCH v3 0/4] bindings: " Bartosz Golaszewski
@ 2022-10-12 12:41   ` Kent Gibson
  2022-10-12 12:51     ` Bartosz Golaszewski
  0 siblings, 1 reply; 30+ messages in thread
From: Kent Gibson @ 2022-10-12 12:41 UTC (permalink / raw)
  To: Bartosz Golaszewski
  Cc: Linus Walleij, Andy Shevchenko, Viresh Kumar, linux-gpio

On Wed, Oct 12, 2022 at 02:34:44PM +0200, Bartosz Golaszewski wrote:
> On Fri, Oct 7, 2022 at 4:55 PM Bartosz Golaszewski <brgl@bgdev.pl> wrote:
> >
> > This is the third iteration of python bindings for libgpiod but it really has
> > very little in common with the previous version.
> >
> > This time the code has been split into high-level python and low-level
> > C layers with the latter only doing the bare minimum.
> >
> > The data model is mostly based on the C++ one with the main difference
> > being utilizing dynamic typing and keyword arguments in place of the
> > builder pattern. That allows us to reduce the number of methods and
> > objects.
> >
> > Because python doesn't have RAII, unlike C++, we provide a module-level
> > request_lines() helper as gpiod.Chip(path).request_lines(...) one-liner
> > could lead to the chip left dangling even after the last reference is
> > dropped.
> >
> > Because python forces us to dynamically allocate objects all the time even
> > for fundamental types (which are also immutable) there's no point in trying
> > to replicate the edge-event buffer. Instead LineRequest.read_edge_event()
> > just returns a list of events.
> >
> > Bartosz Golaszewski (4):
> >
> 
> I fixed the one nit from Andy. If there are no objections I'd like to
> apply this and squash the entire v2 patch series into one big commit
> and apply it to the master branch. This way we can stop keeping this
> temporary branch and continue the development (tools, rust, possible
> further tweaks to the API) on master.
> 

I'm in the process of reviewing, so hold off for a bit if you can.

If not, at the very least  IIIIpdII -> IIIIpkII in patch 4.

Otherwise you get this on 32 bit platforms:

$ gpioget.py /dev/gpiochip0 17
*** stack smashing detected ***: terminated

Cheers,
Kent.

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [libgpiod v2][PATCH v3 0/4] bindings: implement python bindings for libgpiod v2
  2022-10-12 12:41   ` Kent Gibson
@ 2022-10-12 12:51     ` Bartosz Golaszewski
  2022-10-12 13:03       ` Kent Gibson
  0 siblings, 1 reply; 30+ messages in thread
From: Bartosz Golaszewski @ 2022-10-12 12:51 UTC (permalink / raw)
  To: Kent Gibson; +Cc: Linus Walleij, Andy Shevchenko, Viresh Kumar, linux-gpio

On Wed, Oct 12, 2022 at 2:42 PM Kent Gibson <warthog618@gmail.com> wrote:
>
> On Wed, Oct 12, 2022 at 02:34:44PM +0200, Bartosz Golaszewski wrote:
> > On Fri, Oct 7, 2022 at 4:55 PM Bartosz Golaszewski <brgl@bgdev.pl> wrote:
> > >
> > > This is the third iteration of python bindings for libgpiod but it really has
> > > very little in common with the previous version.
> > >
> > > This time the code has been split into high-level python and low-level
> > > C layers with the latter only doing the bare minimum.
> > >
> > > The data model is mostly based on the C++ one with the main difference
> > > being utilizing dynamic typing and keyword arguments in place of the
> > > builder pattern. That allows us to reduce the number of methods and
> > > objects.
> > >
> > > Because python doesn't have RAII, unlike C++, we provide a module-level
> > > request_lines() helper as gpiod.Chip(path).request_lines(...) one-liner
> > > could lead to the chip left dangling even after the last reference is
> > > dropped.
> > >
> > > Because python forces us to dynamically allocate objects all the time even
> > > for fundamental types (which are also immutable) there's no point in trying
> > > to replicate the edge-event buffer. Instead LineRequest.read_edge_event()
> > > just returns a list of events.
> > >
> > > Bartosz Golaszewski (4):
> > >
> >
> > I fixed the one nit from Andy. If there are no objections I'd like to
> > apply this and squash the entire v2 patch series into one big commit
> > and apply it to the master branch. This way we can stop keeping this
> > temporary branch and continue the development (tools, rust, possible
> > further tweaks to the API) on master.
> >
>
> I'm in the process of reviewing, so hold off for a bit if you can.
>
> If not, at the very least  IIIIpdII -> IIIIpkII in patch 4.
>
> Otherwise you get this on 32 bit platforms:
>
> $ gpioget.py /dev/gpiochip0 17
> *** stack smashing detected ***: terminated
>

I'll wait in this case, thanks. Wanted to start testing the new tools
but thought about getting this behind us first.

Bart

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [libgpiod v2][PATCH v3 0/4] bindings: implement python bindings for libgpiod v2
  2022-10-12 12:51     ` Bartosz Golaszewski
@ 2022-10-12 13:03       ` Kent Gibson
  0 siblings, 0 replies; 30+ messages in thread
From: Kent Gibson @ 2022-10-12 13:03 UTC (permalink / raw)
  To: Bartosz Golaszewski
  Cc: Linus Walleij, Andy Shevchenko, Viresh Kumar, linux-gpio

On Wed, Oct 12, 2022 at 02:51:09PM +0200, Bartosz Golaszewski wrote:
> On Wed, Oct 12, 2022 at 2:42 PM Kent Gibson <warthog618@gmail.com> wrote:
> >
> > On Wed, Oct 12, 2022 at 02:34:44PM +0200, Bartosz Golaszewski wrote:
> > > On Fri, Oct 7, 2022 at 4:55 PM Bartosz Golaszewski <brgl@bgdev.pl> wrote:
> > > >
> > > > This is the third iteration of python bindings for libgpiod but it really has
> > > > very little in common with the previous version.
> > > >
> > > > This time the code has been split into high-level python and low-level
> > > > C layers with the latter only doing the bare minimum.
> > > >
> > > > The data model is mostly based on the C++ one with the main difference
> > > > being utilizing dynamic typing and keyword arguments in place of the
> > > > builder pattern. That allows us to reduce the number of methods and
> > > > objects.
> > > >
> > > > Because python doesn't have RAII, unlike C++, we provide a module-level
> > > > request_lines() helper as gpiod.Chip(path).request_lines(...) one-liner
> > > > could lead to the chip left dangling even after the last reference is
> > > > dropped.
> > > >
> > > > Because python forces us to dynamically allocate objects all the time even
> > > > for fundamental types (which are also immutable) there's no point in trying
> > > > to replicate the edge-event buffer. Instead LineRequest.read_edge_event()
> > > > just returns a list of events.
> > > >
> > > > Bartosz Golaszewski (4):
> > > >
> > >
> > > I fixed the one nit from Andy. If there are no objections I'd like to
> > > apply this and squash the entire v2 patch series into one big commit
> > > and apply it to the master branch. This way we can stop keeping this
> > > temporary branch and continue the development (tools, rust, possible
> > > further tweaks to the API) on master.
> > >
> >
> > I'm in the process of reviewing, so hold off for a bit if you can.
> >
> > If not, at the very least  IIIIpdII -> IIIIpkII in patch 4.
> >
> > Otherwise you get this on 32 bit platforms:
> >
> > $ gpioget.py /dev/gpiochip0 17
> > *** stack smashing detected ***: terminated
> >
> 
> I'll wait in this case, thanks. Wanted to start testing the new tools
> but thought about getting this behind us first.
> 

The idea of getting the tools patch out was to keep you busy while I
reviewed the python. Unfortunately I got bogged down in corner cases so
that took much longer to get out than I had intended.

I only started looking at the python today, and I don't want to commit
to a completion time at this stage, but shouldn't be too long.

So go play with the tools for a bit.

Cheers,
Kent.

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [libgpiod v2][PATCH v3 2/4] bindings: python: add examples
  2022-10-07 14:55 ` [libgpiod v2][PATCH v3 2/4] bindings: python: add examples Bartosz Golaszewski
@ 2022-10-13  3:09   ` Kent Gibson
  2022-10-17 12:00     ` Bartosz Golaszewski
  0 siblings, 1 reply; 30+ messages in thread
From: Kent Gibson @ 2022-10-13  3:09 UTC (permalink / raw)
  To: Bartosz Golaszewski
  Cc: Linus Walleij, Andy Shevchenko, Viresh Kumar, linux-gpio

On Fri, Oct 07, 2022 at 04:55:19PM +0200, Bartosz Golaszewski wrote:
> This adds the regular set of example programs implemented using libgpiod
> python bindings.
> 
> Signed-off-by: Bartosz Golaszewski <brgl@bgdev.pl>
> ---
>  bindings/python/examples/Makefile.am   | 10 +++++++
>  bindings/python/examples/gpiodetect.py | 17 ++++++++++++
>  bindings/python/examples/gpiofind.py   | 20 ++++++++++++++
>  bindings/python/examples/gpioget.py    | 31 +++++++++++++++++++++
>  bindings/python/examples/gpioinfo.py   | 35 ++++++++++++++++++++++++
>  bindings/python/examples/gpiomon.py    | 28 +++++++++++++++++++
>  bindings/python/examples/gpioset.py    | 37 ++++++++++++++++++++++++++
>  7 files changed, 178 insertions(+)
>  create mode 100644 bindings/python/examples/Makefile.am
>  create mode 100755 bindings/python/examples/gpiodetect.py
>  create mode 100755 bindings/python/examples/gpiofind.py
>  create mode 100755 bindings/python/examples/gpioget.py
>  create mode 100755 bindings/python/examples/gpioinfo.py
>  create mode 100755 bindings/python/examples/gpiomon.py
>  create mode 100755 bindings/python/examples/gpioset.py
> 
> diff --git a/bindings/python/examples/Makefile.am b/bindings/python/examples/Makefile.am
> new file mode 100644
> index 0000000..f42b80e
> --- /dev/null
> +++ b/bindings/python/examples/Makefile.am
> @@ -0,0 +1,10 @@
> +# SPDX-License-Identifier: GPL-2.0-or-later
> +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
> +
> +EXTRA_DIST = \
> +	gpiodetect.py \
> +	gpiofind.py \
> +	gpioget.py \
> +	gpioinfo.py \
> +	gpiomon.py \
> +	gpioset.py
> diff --git a/bindings/python/examples/gpiodetect.py b/bindings/python/examples/gpiodetect.py
> new file mode 100755
> index 0000000..c32014f
> --- /dev/null
> +++ b/bindings/python/examples/gpiodetect.py
> @@ -0,0 +1,17 @@
> +#!/usr/bin/env python3
> +# SPDX-License-Identifier: GPL-2.0-or-later
> +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
> +
> +"""Reimplementation of the gpiodetect tool in Python."""
> +
> +import gpiod
> +import os
> +
> +if __name__ == "__main__":
> +    for entry in os.scandir("/dev/"):
> +        if gpiod.is_gpiochip_device(entry.path):

Add a helper generator function that returns the available chip paths?
And in order might be nice too.

> +            with gpiod.Chip(entry.path) as chip:
> +                info = chip.get_info()
> +                print(
> +                    "{} [{}] ({} lines)".format(info.name, info.label, info.num_lines)
> +                )
> diff --git a/bindings/python/examples/gpiofind.py b/bindings/python/examples/gpiofind.py
> new file mode 100755
> index 0000000..2f30445
> --- /dev/null
> +++ b/bindings/python/examples/gpiofind.py
> @@ -0,0 +1,20 @@
> +#!/usr/bin/env python3
> +# SPDX-License-Identifier: GPL-2.0-or-later
> +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
> +
> +"""Reimplementation of the gpiofind tool in Python."""
> +
> +import gpiod
> +import os
> +import sys
> +
> +if __name__ == "__main__":
> +    for entry in os.scandir("/dev/"):
> +        if gpiod.is_gpiochip_device(entry.path):
> +            with gpiod.Chip(entry.path) as chip:
> +                offset = chip.map_line(sys.argv[1])

                            chip.offset_from_id(...

> +                if offset is not None:
> +                    print("{} {}".format(chip.get_info().name, offset))
> +                    sys.exit(0)
> +
> +    sys.exit(1)
> diff --git a/bindings/python/examples/gpioget.py b/bindings/python/examples/gpioget.py
> new file mode 100755
> index 0000000..d441535
> --- /dev/null
> +++ b/bindings/python/examples/gpioget.py
> @@ -0,0 +1,31 @@
> +#!/usr/bin/env python3
> +# SPDX-License-Identifier: GPL-2.0-or-later
> +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
> +
> +"""Simplified reimplementation of the gpioget tool in Python."""
> +
> +import gpiod
> +import sys
> +
> +from gpiod.line import Direction
> +
> +if __name__ == "__main__":
> +    if len(sys.argv) < 3:
> +        raise TypeError("usage: gpioget.py <gpiochip> <offset1> <offset2> ...")
> +
> +    path = sys.argv[1]
> +    lines = []
> +    for line in sys.argv[2:]:
> +        lines.append(int(line) if line.isdigit() else line)
> +

Just leave the line ids as string?

else use a list comprehension:

    lines = [ int(id) if id.isdigit() else id for id in sys.argv[2:] ]

Similarly elsewhere.

> +    request = gpiod.request_lines(
> +        path,
> +        consumer="gpioget.py",
> +        config={tuple(lines): gpiod.LineSettings(direction=Direction.INPUT)},
> +    )
> +
> +    vals = request.get_values()
> +
> +    for val in vals:
> +        print("{} ".format(val.value), end="")
> +    print()
> diff --git a/bindings/python/examples/gpioinfo.py b/bindings/python/examples/gpioinfo.py
> new file mode 100755
> index 0000000..e8c7d46
> --- /dev/null
> +++ b/bindings/python/examples/gpioinfo.py
> @@ -0,0 +1,35 @@
> +#!/usr/bin/env python3
> +# SPDX-License-Identifier: GPL-2.0-or-later
> +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
> +
> +"""Simplified reimplementation of the gpioinfo tool in Python."""
> +
> +import gpiod
> +import os
> +
> +if __name__ == "__main__":
> +    for entry in os.scandir("/dev/"):
> +        if gpiod.is_gpiochip_device(entry.path):
> +            with gpiod.Chip(entry.path) as chip:
> +                cinfo = chip.get_info()
> +                print("{} - {} lines:".format(cinfo.name, cinfo.num_lines))
> +
> +                for offset in range(0, cinfo.num_lines):
> +                    linfo = chip.get_line_info(offset)
> +                    offset = linfo.offset
> +                    name = linfo.name
> +                    consumer = linfo.consumer
> +                    direction = linfo.direction

                    is_input = linfo.direction == gpiod.line.Direction.INPUT

That is for space saving below.
Drop the others as they are only referenced once (if you follow the
suggestion below).

> +                    active_low = linfo.active_low
> +
> +                    print(
> +                        "\tline {:>3}: {:>18} {:>12} {:>8} {:>10}".format(
> +                            offset,
> +                            "unnamed" if name is None else name,
> +                            "unused" if consumer is None else consumer,
> +                            "input"
> +                            if direction == gpiod.line.Direction.INPUT
> +                            else "output",
> +                            "active-low" if active_low else "active-high",
                        linfo.offset,
                        linfo.name or "unnamed",
                        linfo.consumer or "unused",
                        is_input and "input" or "output",
                        linfo.active_low and "active_low" or "active-high",

> +                        )
> +                    )
> diff --git a/bindings/python/examples/gpiomon.py b/bindings/python/examples/gpiomon.py
> new file mode 100755
> index 0000000..e0db16f
> --- /dev/null
> +++ b/bindings/python/examples/gpiomon.py
> @@ -0,0 +1,28 @@
> +#!/usr/bin/env python3
> +# SPDX-License-Identifier: GPL-2.0-or-later
> +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
> +
> +"""Simplified reimplementation of the gpiomon tool in Python."""
> +
> +import gpiod
> +import sys
> +
> +from gpiod.line import Edge
> +
> +if __name__ == "__main__":
> +    if len(sys.argv) < 3:
> +        raise TypeError("usage: gpiomon.py <gpiochip> <offset1> <offset2> ...")
> +
> +    path = sys.argv[1]
> +    lines = []
> +    for line in sys.argv[2:]:
> +        lines.append(int(line) if line.isdigit() else line)
> +
> +    with gpiod.request_lines(
> +        path,
> +        consumer="gpiomon.py",
> +        config={tuple(lines): gpiod.LineSettings(edge_detection=Edge.BOTH)},
> +    ) as request:
> +        while True:
> +            for event in request.read_edge_event():
> +                print(event)
> diff --git a/bindings/python/examples/gpioset.py b/bindings/python/examples/gpioset.py
> new file mode 100755
> index 0000000..f0b0681
> --- /dev/null
> +++ b/bindings/python/examples/gpioset.py
> @@ -0,0 +1,37 @@
> +#!/usr/bin/env python3
> +# SPDX-License-Identifier: GPL-2.0-or-later
> +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
> +
> +"""Simplified reimplementation of the gpioset tool in Python."""
> +
> +import gpiod
> +import sys
> +
> +from gpiod.line import Direction, Value
> +
> +if __name__ == "__main__":
> +    if len(sys.argv) < 3:
> +        raise TypeError(
> +            "usage: gpioset.py <gpiochip> <offset1>=<value1> <offset2>=<value2> ..."
> +        )
> +
> +    path = sys.argv[1]
> +    values = dict()
> +    lines = []
> +    for arg in sys.argv[2:]:
> +        arg = arg.split("=")
> +        key = int(arg[0]) if arg[0].isdigit() else arg[0]
> +        val = int(arg[1])
> +
> +        lines.append(key)
> +        values[key] = Value(val)
> +

        lvs = [ arg.split('=') for arg in sys.argv[2:] ]
        lines = [ x[0] for x in lvs ]
        values = dict[lvs]

> +    request = gpiod.request_lines(
> +        path,
> +        consumer="gpioset.py",
> +        config={tuple(lines): gpiod.LineSettings(direction=Direction.OUTPUT)},
> +    )
> +
> +    vals = request.set_values(values)
> +
> +    input()
> -- 

No gpiowatch?

Add some examples for features the tools don't use, like requests with
both inputs and outputs, and reconfigure?

Cheers,
Kent.

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [libgpiod v2][PATCH v3 3/4] bindings: python: add tests
  2022-10-07 14:55 ` [libgpiod v2][PATCH v3 3/4] bindings: python: add tests Bartosz Golaszewski
@ 2022-10-13  3:09   ` Kent Gibson
  0 siblings, 0 replies; 30+ messages in thread
From: Kent Gibson @ 2022-10-13  3:09 UTC (permalink / raw)
  To: Bartosz Golaszewski
  Cc: Linus Walleij, Andy Shevchenko, Viresh Kumar, linux-gpio

On Fri, Oct 07, 2022 at 04:55:20PM +0200, Bartosz Golaszewski wrote:
> This adds a test-suite for python bindings based on the gpio-sim kernel
> module.
> 
> Signed-off-by: Bartosz Golaszewski <brgl@bgdev.pl>

<snip>

> +	for (modconst = module_constants; modconst->name; modconst++) {
> +		ret = PyModule_AddIntConstant(module,
> +					      modconst->name, modconst->val);
> +		if (ret) {
> +			Py_DECREF(module);
> +			return NULL;
> +		}
> + 	}
   ^
space before tab

<snip>

> +
> +    # TODO buffer capacity
> +    # def test_read_over_buffer_capacity(self):
> +    #     buf = gpiod.EdgeEventBuffer(2)
> +    #     self.assertTrue(self.request.wait_edge_event(datetime.timedelta(seconds=1)))
> +    #     self.assertEqual(self.request.read_edge_event(buf), 2)
> +    #     self.assertEqual(len(buf), 2)
> +

Why the TODO?  That API has been changed since, no?

So no problems with the tests, though admittedly I've only skimmed them
relative to the other patches as I'm doing them last and have reached the
limits of my attention span.  I may have another look later, but I'd
rather get the other reviews out now than wait until I'm up to that.

Cheers,
Kent.

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [libgpiod v2][PATCH v3 4/4] bindings: python: implement python bindings for libgpiod v2
  2022-10-07 14:55 ` [libgpiod v2][PATCH v3 4/4] bindings: python: implement python bindings for libgpiod v2 Bartosz Golaszewski
  2022-10-07 15:26   ` Andy Shevchenko
@ 2022-10-13  3:10   ` Kent Gibson
  2022-10-13 11:12     ` Kent Gibson
  2022-10-26 12:32     ` Bartosz Golaszewski
  1 sibling, 2 replies; 30+ messages in thread
From: Kent Gibson @ 2022-10-13  3:10 UTC (permalink / raw)
  To: Bartosz Golaszewski
  Cc: Linus Walleij, Andy Shevchenko, Viresh Kumar, linux-gpio

On Fri, Oct 07, 2022 at 04:55:21PM +0200, Bartosz Golaszewski wrote:
> This adds python bindings for libgpiod v2. As opposed to v1, they are
> mostly written in python with just low-level elements written in C and
> interfacing with libgpiod.so.
> 
> We've also added setup.py which will allow to use pip for managing the
> bindings and split them into a separate meta-openembedded recipe.
> 
> Signed-off-by: Bartosz Golaszewski <brgl@bgdev.pl>

<snipping out files with no probs>

> +++ b/bindings/python/gpiod/Makefile.am
> @@ -0,0 +1,17 @@
> +# SPDX-License-Identifier: LGPL-2.1-or-later
> +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
> +
> +SUBDIRS = ext
> +
> +EXTRA_DIST = \
> +	chip_info.py \
> +	chip.py \
> +	edge_event.py \
> +	exception.py \
> +	info_event.py \
> +	__init__.py \
> +	internal.py \
> +	line_info.py \
> +	line.py \
> +	line_request.py \
> +	line_settings.py 
                    ^
trailing whitespace

> +++ b/bindings/python/gpiod/chip.py
> @@ -0,0 +1,308 @@
> +# SPDX-License-Identifier: LGPL-2.1-or-later
> +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
> +
> +from . import _ext
> +from .chip_info import ChipInfo
> +from .exception import ChipClosedError
> +from .info_event import InfoEvent
> +from .internal import poll_fd
> +from .line_info import LineInfo
> +from .line_settings import LineSettings, _line_settings_to_ext_line_settings
> +from .line_request import LineRequest
> +from datetime import timedelta
> +from errno import ENOENT
> +from select import select
> +from typing import Union, Optional
> +
> +
> +class Chip:
> +    """
> +    Represents a GPIO chip.
> +
> +    Chip object manages all resources associated with the GPIO chip it represents.
> +
> +    The gpiochip device file is opened during the object's construction. The Chip
> +    object's constructor takes the path to the GPIO chip device file
> +    as the only argument.
> +
> +    Callers must close the chip by calling the close() method when it's no longer
> +    used.
> +
> +    Example:
> +
> +        chip = gpiod.Chip(\"/dev/gpiochip0\")
> +        do_something(chip)
> +        chip.close()
> +
> +    The gpiod.Chip class also supports controlled execution ('with' statement).
> +
> +    Example:
> +
> +        with gpiod.Chip(path="/dev/gpiochip0") as chip:
> +            do_something(chip)
> +    """
> +
> +    def __init__(self, path: str):
> +        """
> +        Open a GPIO device.
> +
> +        Args:
> +          path:
> +            Path to the GPIO character device file.
> +        """
> +        self._chip = _ext.Chip(path)
> +
> +    def __bool__(self) -> bool:
> +        """
> +        Boolean conversion for GPIO chips.
> +
> +        Returns:
> +          True if the chip is open and False if it's closed.
> +        """
> +        return True if self._chip else False
> +
> +    def __enter__(self):
> +        """
> +        Controlled execution enter callback.
> +        """
> +        self._check_closed()
> +        return self
> +
> +    def __exit__(self, exc_type, exc_value, traceback) -> None:
> +        """
> +        Controlled execution exit callback.
> +        """
> +        self.close()
> +
> +    def _check_closed(self) -> None:
> +        if not self._chip:
> +            raise ChipClosedError()
> +
> +    def close(self) -> None:
> +        """
> +        Close the associated GPIO chip descriptor. The chip object must no
> +        longer be used after this method is called.
> +        """
> +        self._check_closed()
> +        self._chip.close()
> +        self._chip = None
> +
> +    def get_info(self) -> ChipInfo:
> +        """
> +        Get the information about the chip.
> +
> +        Returns:
> +          New gpiod.ChipInfo object.
> +        """
> +        self._check_closed()
> +        return self._chip.get_info()

Could the info be cached, rather than going to the kernel every time?
It's not as though it changes, and this gets used a lot by map_line()
below for range checking.

> +
> +    def map_line(self, id: Union[str, int]) -> int:
> +        """
> +        Map a line's identifier to its offset within the chip.
> +
> +        Args:
> +          id:
> +            Name of the GPIO line, its offset as a string or its offset as an
> +            integer.
> +
> +        Returns:
> +          If id is an integer - it's returned as is (unless it's out of range
> +          for this chip). If it's a string, the method tries to interpret it as
> +          the name of the line first and tries too perform a name lookup within
> +          the chip. If it fails, it tries to convert the string to an integer
> +          and check if it represents a valid offset within the chip and if
> +          so - returns it.

map_line() is a bit vague, like find_line().  How about offset_from_id()?

Should ids fallback to being interpreted as ints if they can't be
found as strings?  Why not leave that call to the user?
If they aren't sure they can try it as a string, and if that fails try
it as an int.
(I realise this is the reverse argument of my comment in the examples
patch btw - playing devil's advocate here - and there.)

Is the range check on ints necessary? The kernel will do that when you
make the call - what is the benefit of doing it here?
In which case why accept int at all?  Which would make the function
offset_from_name() - just like the C.

> +        """
> +        self._check_closed()
> +
> +        if not isinstance(id, int):
> +            try:
> +                return self._chip.map_line(id)
> +            except OSError as ex:
> +                if ex.errno == ENOENT:
> +                    try:
> +                        offset = int(id)
> +                    except ValueError:
> +                        raise ex
> +                else:
> +                    raise ex
> +        else:
> +            offset = id
> +
> +        if offset >= self.get_info().num_lines:
> +            raise ValueError("line offset of out range")
> +
> +        return offset
> +
> +    def _get_line_info(self, line: Union[int, str], watch: bool) -> LineInfo:
> +        self._check_closed()

Wrt the _check_closed(), what is to prevent another thread closing the
chip here, and causing the next line to segfault?

And self.map_line(line) can release the GIL so all bets are off even if
you assume cooperative multitasking - not that you can assume that.

> +        return self._chip.get_line_info(self.map_line(line), watch)
> +
> +    def get_line_info(self, line: Union[int, str]) -> LineInfo:
> +        """
> +        Get the snapshot of information about the line at given offset.
> +
> +        Args:
> +          line:
> +            Offset or name of the GPIO line to get information for.
> +
> +        Returns:
> +          New LineInfo object.
> +        """
> +        return self._get_line_info(line, watch=False)
> +
> +    def watch_line_info(self, line: Union[int, str]) -> LineInfo:
> +        """
> +        Get the snapshot of information about the line at given offset and
> +        start watching it for future changes.
> +
> +        Args:
> +          line:
> +            Offset or name of the GPIO line to get information for.
> +
> +        Returns:
> +          New gpiod.LineInfo object.
> +        """
> +        return self._get_line_info(line, watch=True)
> +
> +    def unwatch_line_info(self, line: Union[int, str]) -> None:
> +        """
> +        Stop watching a line for status changes.
> +
> +        Args:
> +          line:
> +            Offset or name of the line to stop watching.
> +        """
> +        self._check_closed()
> +        return self._chip.unwatch_line_info(self.map_line(line))
> +
> +    def wait_info_event(
> +        self, timeout: Optional[Union[timedelta, float]] = None
> +    ) -> bool:
> +        """
> +        Wait for line status change events on any of the watched lines on the
> +        chip.
> +
> +        Args:
> +          timeout:
> +            Wait time limit represented as either a datetime.timedelta object
> +            or the number of seconds stored in a float.
> +
> +        Returns:
> +          True if an info event is ready to be read from the chip, False if the
> +          wait timed out without any events.
> +        """
> +        self._check_closed()
> +
> +        return poll_fd(self.fd, timeout)
> +
> +    def read_info_event(self) -> InfoEvent:
> +        """
> +        Read a single line status change event from the chip.
> +
> +        Returns:
> +          New gpiod.InfoEvent object.
> +
> +        Note:
> +          This function may block if there are no available events in the queue.
> +        """
> +        self._check_closed()
> +        return self._chip.read_info_event()
> +
> +    def request_lines(
> +        self,
> +        config: dict[tuple[Union[int, str]], Optional[LineSettings]],
> +        consumer: Optional[str] = None,
> +        event_buffer_size: Optional[int] = None,
> +    ) -> LineRequest:
> +        """
> +        Request a set of lines for exclusive usage.
> +
> +        Args:
> +          config:
> +            Dictionary mapping offsets or names (or tuples thereof) to
> +            LineSettings. If None is passed as the value of the mapping,
> +            default settings are used.

What are the semantics for lines being repeated in the dict keys (which
is possible as the keys are tuples, and also because the line can be
identified by offset or name)?

> +          consumer:
> +            Consumer string to use for this request.
> +          event_buffer_size:
> +            Size of the kernel edge event buffer to configure for this request.
> +
> +        Returns:
> +          New LineRequest object.
> +        """
> +        self._check_closed()
> +
> +        line_cfg = _ext.LineConfig()
> +
> +        for lines, settings in config.items():
> +            offsets = list()
> +            name_map = dict()
> +            offset_map = dict()
> +
> +            if isinstance(lines, int) or isinstance(lines, str):
> +                lines = (lines,)
> +
> +            for line in lines:
> +                offset = self.map_line(line)
> +                offsets.append(offset)
> +                if isinstance(line, str):
> +                    name_map[line] = offset
> +                    offset_map[offset] = line

Use list comprehensions instead of the for loop?

> +
> +            if settings is None:
> +                settings = LineSettings()
> +
               settings = settings or LineSettings()

Would use that directly below if it would fit in the line.

Rename _line_settings_to_ext_line_settings to _line_settings_to_ext,
as the second line_settings is redundant?  Then it might fit.

> +            line_cfg.add_line_settings(
> +                offsets, _line_settings_to_ext_line_settings(settings)
> +            )
> +
> +        req_internal = self._chip.request_lines(line_cfg, consumer, event_buffer_size)
> +        request = LineRequest(req_internal)
> +
> +        request._offsets = req_internal.offsets
> +        request._name_map = name_map
> +        request._offset_map = offset_map
> +
> +        request._lines = list()
> +        for off in request.offsets:
> +            request._lines.append(offset_map[off] if off in offset_map else off)

Again, prefer to use list comprehensions to translate lists like this.

> +
> +        return request
> +
> +    def __repr__(self) -> str:
> +        """
> +        Return a string that can be used to re-create this chip object.
> +        """
> +        if not self._chip:
> +            return "<Chip CLOSED>"
> +
> +        return 'Chip("{}")'.format(self.path)
> +
> +    def __str__(self) -> str:
> +        """
> +        Return a user-friendly, human-readable description of this chip.
> +        """
> +        if not self._chip:
> +            return "<Chip CLOSED>"
> +
> +        return '<Chip path="{}" fd={} info={}>'.format(
> +            self.path, self.fd, self.get_info()
> +        )
> +
> +    @property
> +    def path(self) -> str:
> +        """
> +        Filesystem path used to open this chip.
> +        """
> +        self._check_closed()
> +        return self._chip.path
> +
> +    @property
> +    def fd(self) -> int:
> +        """
> +        File descriptor associated with this chip.
> +        """
> +        self._check_closed()
> +        return self._chip.fd
> diff --git a/bindings/python/gpiod/ext/chip.c b/bindings/python/gpiod/ext/chip.c
> new file mode 100644
> index 0000000..47d5455
> --- /dev/null
> +++ b/bindings/python/gpiod/ext/chip.c
> @@ -0,0 +1,335 @@
> +// SPDX-License-Identifier: LGPL-2.1-or-later
> +// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
> +
> +#include "internal.h"
> +
> +typedef struct {
> +	PyObject_HEAD;
> +	struct gpiod_chip *chip;
> +} chip_object;
> +
> +static int
> +chip_init(chip_object *self, PyObject *args, PyObject *Py_UNUSED(ignored))
> +{
> +	char *path;
> +	int ret;
> +
> +	ret = PyArg_ParseTuple(args, "s", &path);
> +	if (!ret)
> +		return -1;
> +
> +	Py_BEGIN_ALLOW_THREADS;
> +	self->chip = gpiod_chip_open(path);
> +	Py_END_ALLOW_THREADS;

Is the assignment to self->chip while not holding the GIL (between the
BEGIN and END) safe?
To be sure, assign to a local then assign that to self->chip after the END.

Similarly other BEGIN/END blocks should only access locals.
And even then you probably need locks to prevent threads trampling each
other, but the locks need the GIL, IIUC.

So I suspect your chip._check_closed()s need to be more like
  with chip._lock()

Similarly the other object validity checks.

> +	if (!self->chip) {
> +		Py_gpiod_SetErrFromErrno();
> +		return -1;
> +	}
> +
> +	return 0;
> +}
> +
> +static void chip_finalize(chip_object *self)
> +{
> +	if (self->chip)
> +		PyObject_CallMethod((PyObject *)self, "close", "");
> +}
> +
> +static PyObject *chip_path(chip_object *self, void *Py_UNUSED(ignored))
> +{
> +	return PyUnicode_FromString(gpiod_chip_get_path(self->chip));
> +}
> +
> +static PyObject *chip_fd(chip_object *self, void *Py_UNUSED(ignored))
> +{
> +	return PyLong_FromLong(gpiod_chip_get_fd(self->chip));
> +}
> +
> +static PyGetSetDef chip_getset[] = {
> +	{
> +		.name = "path",
> +		.get = (getter)chip_path,
> +	},
> +	{
> +		.name = "fd",
> +		.get = (getter)chip_fd,
> +	},
> +	{ }
> +};
> +
> +static PyObject *chip_close(chip_object *self, PyObject *Py_UNUSED(ignored))
> +{
> +	Py_BEGIN_ALLOW_THREADS;
> +	gpiod_chip_close(self->chip);
> +	Py_END_ALLOW_THREADS;
> +	self->chip = NULL;
> +
> +	Py_RETURN_NONE;
> +}
> +
> +static PyObject *chip_get_info(chip_object *self, PyObject *Py_UNUSED(ignored))
> +{
> +	struct gpiod_chip_info *info;
> +	PyObject *type, *ret;
> +
> +	type = Py_gpiod_GetGlobalType("ChipInfo");
> +	if (!type)
> +		return NULL;
> +
> +	info = gpiod_chip_get_info(self->chip);
> +	if (!info)
> +		return PyErr_SetFromErrno(PyExc_OSError);
> +
> +	 ret = PyObject_CallFunction(type, "ssI",
> +				     gpiod_chip_info_get_name(info),
> +				     gpiod_chip_info_get_label(info),
> +				     gpiod_chip_info_get_num_lines(info));
> +	 gpiod_chip_info_free(info);
> +	 return ret;
> +}
> +
> +static PyObject *make_line_info(struct gpiod_line_info *info)
> +{
> +	PyObject *type;
> +
> +	type = Py_gpiod_GetGlobalType("LineInfo");
> +	if (!type)
> +		return NULL;
> +
> +	return PyObject_CallFunction(type, "IsOsiOiiiiOi",
                                                   ^
                                       "IsOsiOiiiiOk"

debounce_period_us is an unsigned long, not int.

> +				gpiod_line_info_get_offset(info),
> +				gpiod_line_info_get_name(info),
> +				gpiod_line_info_is_used(info) ?
> +							Py_True : Py_False,
> +				gpiod_line_info_get_consumer(info),
> +				gpiod_line_info_get_direction(info),
> +				gpiod_line_info_is_active_low(info) ?
> +							Py_True : Py_False,
> +				gpiod_line_info_get_bias(info),
> +				gpiod_line_info_get_drive(info),
> +				gpiod_line_info_get_edge_detection(info),
> +				gpiod_line_info_get_event_clock(info),
> +				gpiod_line_info_is_debounced(info) ?
> +							Py_True : Py_False,
> +				gpiod_line_info_get_debounce_period_us(info));
> +}
> +
> +static PyObject *chip_get_line_info(chip_object *self, PyObject *args)
> +{
> +	struct gpiod_line_info *info;
> +	unsigned int offset;
> +	PyObject *info_obj;
> +	int ret, watch;
> +
> +	ret = PyArg_ParseTuple(args, "Ip", &offset, &watch);
> +	if (!ret)
> +		return NULL;
> +
> +	Py_BEGIN_ALLOW_THREADS;
> +	if (watch)
> +		info = gpiod_chip_watch_line_info(self->chip, offset);
> +	else
> +		info = gpiod_chip_get_line_info(self->chip, offset);
> +	Py_END_ALLOW_THREADS;
> +	if (!info)
> +		return Py_gpiod_SetErrFromErrno();
> +
> +	info_obj = make_line_info(info);
> +	gpiod_line_info_free(info);
> +	return info_obj;
> +}
> +
> +static PyObject *
> +chip_unwatch_line_info(chip_object *self, PyObject *args)
> +{
> +	unsigned int offset;
> +	int ret;
> +
> +	ret = PyArg_ParseTuple(args, "I", &offset);
> +	if (!ret)
> +		return NULL;
> +
> +	Py_BEGIN_ALLOW_THREADS;
> +	ret = gpiod_chip_unwatch_line_info(self->chip, offset);
> +	Py_END_ALLOW_THREADS;
> +	if (ret)
> +		return Py_gpiod_SetErrFromErrno();
> +
> +	Py_RETURN_NONE;
> +}
> +
> +static PyObject *
> +chip_read_info_event(chip_object *self, PyObject *Py_UNUSED(ignored))
> +{
> +	PyObject *type, *info_obj, *event_obj;
> +	struct gpiod_info_event *event;
> +	struct gpiod_line_info *info;
> +
> +	type = Py_gpiod_GetGlobalType("InfoEvent");
> +	if (!type)
> +		return NULL;
> +
> +	Py_BEGIN_ALLOW_THREADS;
> +	event = gpiod_chip_read_info_event(self->chip);
> +	Py_END_ALLOW_THREADS;
> +	if (!event)
> +		return Py_gpiod_SetErrFromErrno();
> +
> +	info = gpiod_info_event_get_line_info(event);
> +
> +	info_obj = make_line_info(info);
> +	if (!info_obj) {
> +		gpiod_info_event_free(event);
> +		return NULL;
> +	}
> +
> +	event_obj = PyObject_CallFunction(type, "iKO",
> +				gpiod_info_event_get_event_type(event),
> +				gpiod_info_event_get_timestamp_ns(event),
> +				info_obj);
> +	Py_DECREF(info_obj);
> +	gpiod_info_event_free(event);
> +	return event_obj;
> +}
> +
> +static PyObject *chip_map_line(chip_object *self, PyObject *args)
> +{
> +	int ret, offset;
> +	char *name;
> +
> +	ret = PyArg_ParseTuple(args, "s", &name);
> +	if (!ret)
> +		return NULL;
> +
> +	Py_BEGIN_ALLOW_THREADS;
> +	offset = gpiod_chip_get_line_offset_from_name(self->chip, name);
> +	Py_END_ALLOW_THREADS;
> +	if (offset < 0)
> +		return Py_gpiod_SetErrFromErrno();
> +
> +	return PyLong_FromLong(offset);
> +}
> +
> +static struct gpiod_request_config *
> +make_request_config(PyObject *consumer_obj, PyObject *event_buffer_size_obj)
> +{
> +	struct gpiod_request_config *req_cfg;
> +	size_t event_buffer_size;
> +	const char *consumer;
> +
> +	req_cfg = gpiod_request_config_new();
> +	if (!req_cfg) {
> +		Py_gpiod_SetErrFromErrno();
> +		return NULL;
> +	}
> +
> +	if (consumer_obj != Py_None) {
> +		consumer = PyUnicode_AsUTF8(consumer_obj);
> +		if (!consumer) {
> +			gpiod_request_config_free(req_cfg);
> +			return NULL;
> +		}
> +
> +		gpiod_request_config_set_consumer(req_cfg, consumer);
> +	}
> +
> +	if (event_buffer_size_obj != Py_None) {
> +		event_buffer_size = PyLong_AsSize_t(event_buffer_size_obj);
> +		if (PyErr_Occurred()) {
> +			gpiod_request_config_free(req_cfg);
> +			return NULL;
> +		}
> +
> +		gpiod_request_config_set_event_buffer_size(req_cfg,
> +							   event_buffer_size);
> +	}
> +
> +	return req_cfg;
> +}
> +
> +static PyObject *chip_request_lines(chip_object *self, PyObject *args)
> +{
> +	PyObject *line_config, *consumer, *event_buffer_size, *req_obj;
> +	struct gpiod_request_config *req_cfg;
> +	struct gpiod_line_config *line_cfg;
> +	struct gpiod_line_request *request;
> +	int ret;
> +
> +	ret = PyArg_ParseTuple(args, "OOO",
> +			       &line_config, &consumer, &event_buffer_size);
> +	if (!ret)
> +		return NULL;
> +
> +	line_cfg = Py_gpiod_LineConfigGetData(line_config);
> +	if (!line_cfg)
> +		return NULL;
> +
> +	req_cfg = make_request_config(consumer, event_buffer_size);
> +	if (!req_cfg)
> +		return NULL;
> +
> +	Py_BEGIN_ALLOW_THREADS;
> +	request = gpiod_chip_request_lines(self->chip, req_cfg, line_cfg);
> +	Py_END_ALLOW_THREADS;
> +	gpiod_request_config_free(req_cfg);
> +	if (!request)
> +		return Py_gpiod_SetErrFromErrno();
> +
> +	req_obj = Py_gpiod_MakeRequestObject(request);
> +	if (!req_obj)
> +		gpiod_line_request_release(request);
> +
> +	return req_obj;
> +}
> +
> +static PyMethodDef chip_methods[] = {
> +	{
> +		.ml_name = "close",
> +		.ml_meth = (PyCFunction)chip_close,
> +		.ml_flags = METH_NOARGS,
> +	},
> +	{
> +		.ml_name = "get_info",
> +		.ml_meth = (PyCFunction)chip_get_info,
> +		.ml_flags = METH_NOARGS,
> +	},
> +	{
> +		.ml_name = "get_line_info",
> +		.ml_meth = (PyCFunction)chip_get_line_info,
> +		.ml_flags = METH_VARARGS,
> +	},
> +	{
> +		.ml_name = "unwatch_line_info",
> +		.ml_meth = (PyCFunction)chip_unwatch_line_info,
> +		.ml_flags = METH_VARARGS,
> +	},
> +	{
> +		.ml_name = "read_info_event",
> +		.ml_meth = (PyCFunction)chip_read_info_event,
> +		.ml_flags = METH_NOARGS,
> +	},
> +	{
> +		.ml_name = "map_line",
> +		.ml_meth = (PyCFunction)chip_map_line,
> +		.ml_flags = METH_VARARGS,
> +	},
> +	{
> +		.ml_name = "request_lines",
> +		.ml_meth = (PyCFunction)chip_request_lines,
> +		.ml_flags = METH_VARARGS,
> +	},
> +	{ }
> +};
> +
> +PyTypeObject chip_type = {
> +	PyVarObject_HEAD_INIT(NULL, 0)
> +	.tp_name = "gpiod._ext.Chip",
> +	.tp_basicsize = sizeof(chip_object),
> +	.tp_flags = Py_TPFLAGS_DEFAULT,
> +	.tp_new = PyType_GenericNew,
> +	.tp_init = (initproc)chip_init,
> +	.tp_finalize = (destructor)chip_finalize,
> +	.tp_dealloc = (destructor)Py_gpiod_dealloc,
> +	.tp_getset = chip_getset,
> +	.tp_methods = chip_methods,
> +};
> diff --git a/bindings/python/gpiod/ext/line-settings.c b/bindings/python/gpiod/ext/line-settings.c
> new file mode 100644
> index 0000000..bd2a66a
> --- /dev/null
> +++ b/bindings/python/gpiod/ext/line-settings.c
> @@ -0,0 +1,130 @@
> +// SPDX-License-Identifier: LGPL-2.1-or-later
> +// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
> +
> +#include "internal.h"
> +
> +typedef struct {
> +	PyObject_HEAD;
> +	struct gpiod_line_settings *settings;
> +} line_settings_object;
> +
> +static int set_int_prop(struct gpiod_line_settings *settings, int val,
> +			int (*func)(struct gpiod_line_settings *, int))
> +{
> +	int ret;
> +
> +	ret = func(settings, val);
> +	if (ret) {
> +		Py_gpiod_SetErrFromErrno();
> +		return -1;
> +	}
> +

Since the ret value is never used, skip the ret and just test the func
return directly?

Similarly elsewhere.

> +	return 0;
> +}
> +
> +static int
> +line_settings_init(line_settings_object *self, PyObject *args, PyObject *kwargs)
> +{
> +	static char *kwlist[] = {
> +		"direction",
> +		"edge_detection",
> +		"bias",
> +		"drive",
> +		"active_low",
> +		"debounce_period",
> +		"event_clock",
> +		"output_value",
> +		NULL
> +	};
> +
> +	int direction, edge, bias, drive, active_low, event_clock, output_value,
> +	    ret;
> +	unsigned long debounce_period;
> +
> +	ret = PyArg_ParseTupleAndKeywords(args, kwargs, "IIIIpdII", kwlist,
                                                          ^
                                                     IIIIpkII

d is a double, k is an unsigned long

> +			&direction, &edge, &bias, &drive, &active_low,
> +			&debounce_period, &event_clock, &output_value);
> +	if (!ret)
> +		return -1;
> +
> +	self->settings = gpiod_line_settings_new();
> +	if (!self->settings) {
> +		Py_gpiod_SetErrFromErrno();
> +		return -1;
> +	}
> +
> +	ret = set_int_prop(self->settings, direction,
> +			   gpiod_line_settings_set_direction);
> +	if (ret)
> +		return -1;
> +
> +	ret = set_int_prop(self->settings, edge,
> +			   gpiod_line_settings_set_edge_detection);
> +	if (ret)
> +		return -1;
> +
> +	ret = set_int_prop(self->settings, bias,
> +			   gpiod_line_settings_set_bias);
> +	if (ret)
> +		return -1;
> +
> +	ret = set_int_prop(self->settings, drive,
> +			   gpiod_line_settings_set_drive);
> +	if (ret)
> +		return -1;
> +
> +	gpiod_line_settings_set_active_low(self->settings, active_low);
> +	gpiod_line_settings_set_debounce_period_us(self->settings,
> +						   debounce_period);
> +
> +	ret = set_int_prop(self->settings, edge,
> +			   gpiod_line_settings_set_edge_detection);
> +	if (ret)
> +		return -1;
> +
> +	ret = set_int_prop(self->settings, output_value,
> +			   gpiod_line_settings_set_output_value);
> +	if (ret)
> +		return -1;
> +
> +	return 0;
> +}
> +
> +static void line_settings_finalize(line_settings_object *self)
> +{
> +	if (self->settings)
> +		gpiod_line_settings_free(self->settings);
> +}
> +
> +PyTypeObject line_settings_type = {
> +	PyVarObject_HEAD_INIT(NULL, 0)
> +	.tp_name = "gpiod._ext.LineSettings",
> +	.tp_basicsize = sizeof(line_settings_object),
> +	.tp_flags = Py_TPFLAGS_DEFAULT,
> +	.tp_new = PyType_GenericNew,
> +	.tp_init = (initproc)line_settings_init,
> +	.tp_finalize = (destructor)line_settings_finalize,
> +	.tp_dealloc = (destructor)Py_gpiod_dealloc,
> +};
> +
> +struct gpiod_line_settings *Py_gpiod_LineSettingsGetData(PyObject *obj)
> +{
> +	line_settings_object *settings;
> +	PyObject *type;
> +
> +	type = PyObject_Type(obj);
> +	if (!type)
> +		return NULL;
> +
> +	if ((PyTypeObject *)type != &line_settings_type) {
> +		PyErr_SetString(PyExc_TypeError,
> +				"not a gpiod._ext.LineSettings object");
> +		Py_DECREF(type);
> +		return NULL;
> +	}
> +	Py_DECREF(type);
> +
> +	settings = (line_settings_object *)obj;
> +
> +	return settings->settings;
> +}
> diff --git a/bindings/python/gpiod/ext/request.c b/bindings/python/gpiod/ext/request.c
> new file mode 100644
> index 0000000..36b5b48
> --- /dev/null
> +++ b/bindings/python/gpiod/ext/request.c
> @@ -0,0 +1,402 @@
> +// SPDX-License-Identifier: LGPL-2.1-or-later
> +// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
> +
> +#include "internal.h"
> +
> +typedef struct {
> +	PyObject_HEAD;
> +	struct gpiod_line_request *request;
> +	unsigned int *offsets;
> +	int *values;
> +	size_t num_lines;
> +} request_object;
> +
> +static int request_init(PyObject *Py_UNUSED(ignored0),
> +			PyObject *Py_UNUSED(ignored1),
> +			PyObject *Py_UNUSED(ignored2))
> +{
> +	PyErr_SetString(PyExc_NotImplementedError,
> +			"_ext.LineRequest cannot be instantiated");
> +
> +	return -1;
> +}
> +
> +static void request_finalize(request_object *self)
> +{
> +	if (self->request)
> +		PyObject_CallMethod((PyObject *)self, "release", "");
> +
> +	if (self->offsets)
> +		PyMem_Free(self->offsets);
> +
> +	if (self->values)
> +		PyMem_Free(self->values);
> +}
> +
> +static PyObject *
> +request_num_lines(request_object *self, void *Py_UNUSED(ignored))
> +{
> +	return PyLong_FromUnsignedLong(
> +			gpiod_line_request_get_num_lines(self->request));
> +}
> +
> +static PyObject *request_offsets(request_object *self, void *Py_UNUSED(ignored))
> +{
> +	PyObject *lines, *line;
> +	unsigned int *offsets;
> +	size_t num_lines, i;
> +	int ret;
> +
> +	num_lines = gpiod_line_request_get_num_lines(self->request);
> +
> +	offsets = PyMem_Calloc(num_lines, sizeof(unsigned int));
> +	if (!offsets)
> +		return PyErr_NoMemory();
> +
> +	gpiod_line_request_get_offsets(self->request, offsets);
> +
> +	lines = PyList_New(num_lines);
> +	if (!lines) {
> +		PyMem_Free(offsets);
> +		return NULL;
> +	}
> +
> +	for (i = 0; i < num_lines; i++) {
> +		line = PyLong_FromUnsignedLong(offsets[i]);
> +		if (!lines) {
                 ^
Should be line.

> +			Py_DECREF(lines);
> +			PyMem_Free(offsets);
> +			return NULL;
> +		}
> +
> +		ret = PyList_SetItem(lines, i, line);
> +		if (ret) {
> +			Py_DECREF(line);
> +			Py_DECREF(lines);
> +			PyMem_Free(offsets);
> +			return NULL;
> +		}
> +	}
> +
> +	PyMem_Free(offsets);
> +	return lines;
> +}
> +
> +static PyObject *request_fd(request_object *self, void *Py_UNUSED(ignored))
> +{
> +	return PyLong_FromLong(gpiod_line_request_get_fd(self->request));
> +}
> +
> +static PyGetSetDef request_getset[] = {
> +	{
> +		.name = "num_lines",
> +		.get = (getter)request_num_lines,
> +	},
> +	{
> +		.name = "offsets",
> +		.get = (getter)request_offsets,
> +	},
> +	{
> +		.name = "fd",
> +		.get = (getter)request_fd,
> +	},
> +	{ }
> +};
> +
> +static PyObject *
> +request_release(request_object *self, PyObject *Py_UNUSED(ignored))
> +{
> +	Py_BEGIN_ALLOW_THREADS;
> +	gpiod_line_request_release(self->request);
> +	Py_END_ALLOW_THREADS;
> +	self->request = NULL;
> +
> +	Py_RETURN_NONE;
> +}
> +
> +static void clear_buffers(request_object *self)
> +{
> +	memset(self->offsets, 0, self->num_lines * sizeof(unsigned int));
> +	memset(self->values, 0, self->num_lines * sizeof(int));
> +}
> +
> +static PyObject *request_get_values(request_object *self, PyObject *args)
> +{
> +	PyObject *offsets, *values, *val, *type, *iter, *next;
> +	Py_ssize_t num_offsets;
> +	unsigned int pos;
> +	int ret;
> +
> +	ret = PyArg_ParseTuple(args, "OO", &offsets, &values);
> +	if (!ret)
> +		return NULL;
> +
> +	num_offsets = PyObject_Size(offsets);
> +	if (num_offsets < 0)
> +		return NULL;
> +
> +	type = Py_gpiod_GetGlobalType("Value");
> +	if (!type)
> +		return NULL;
> +
> +	iter = PyObject_GetIter(offsets);
> +	if (!iter)
> +		return NULL;
> +
> +	clear_buffers(self);
> +
> +	for (pos = 0;; pos++) {
> +		next = PyIter_Next(iter);
> +		if (!next) {
> +			Py_DECREF(iter);
> +			break;
> +		}
> +
> +		self->offsets[pos] = Py_gpiod_PyLongAsUnsignedInt(next);
> +		Py_DECREF(next);
> +		if (PyErr_Occurred()) {
> +			Py_DECREF(iter);
> +			return NULL;
> +		}
> +	}
> +
> +	Py_BEGIN_ALLOW_THREADS;
> +	ret = gpiod_line_request_get_values_subset(self->request,
> +						   self->num_lines,
> +						   self->offsets,
> +						   self->values);
> +	Py_END_ALLOW_THREADS;
> +	if (ret)
> +		return Py_gpiod_SetErrFromErrno();
> +
> +	for (pos = 0; pos < num_offsets; pos++) {

gpiod/ext/request.c:172:20: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘Py_ssize_t’ {aka ‘int’} [-Wsign-compare]
  172 |  for (pos = 0; pos < num_offsets; pos++) {
      |                    ^

> +		val = PyObject_CallFunction(type, "i", self->values[pos]);
> +		if (!val)
> +			return NULL;
> +
> +		ret = PyList_SetItem(values, pos, val);
> +		if (ret) {
> +			Py_DECREF(val);
> +			return NULL;
> +		}
> +	}
> +
> +	Py_RETURN_NONE;
> +}
> +
> +static PyObject *request_set_values(request_object *self, PyObject *args)
> +{
> +	PyObject *values, *key, *val, *val_stripped;
> +	Py_ssize_t pos = 0;
> +	int ret;
> +
> +	ret = PyArg_ParseTuple(args, "O", &values);
> +	if (!ret)
> +		return NULL;
> +
> +	clear_buffers(self);
> +
> +	while (PyDict_Next(values, &pos, &key, &val)) {
> +		self->offsets[pos] = Py_gpiod_PyLongAsUnsignedInt(key);
> +		if (PyErr_Occurred())
> +			return NULL;
> +
> +		val_stripped = PyObject_GetAttrString(val, "value");
> +		if (!val_stripped)
> +			return NULL;
> +
> +		self->values[pos] = PyLong_AsLong(val_stripped);
> +		Py_DECREF(val_stripped);
> +		if (PyErr_Occurred())
> +			return NULL;
> +	}
> +
> +	Py_BEGIN_ALLOW_THREADS;
> +	ret = gpiod_line_request_set_values_subset(self->request,
> +						   self->num_lines,
> +						   self->offsets,
> +						   self->values);
> +	Py_END_ALLOW_THREADS;
> +	if (ret)
> +		return Py_gpiod_SetErrFromErrno();
> +
> +	Py_RETURN_NONE;
> +}
> +
> +static PyObject *request_reconfigure_lines(request_object *self, PyObject *args)
> +{
> +	struct gpiod_line_config *line_cfg;
> +	PyObject *line_cfg_obj;
> +	int ret;
> +
> +	ret = PyArg_ParseTuple(args, "O", &line_cfg_obj);
> +	if (!ret)
> +		return NULL;
> +
> +	line_cfg = Py_gpiod_LineConfigGetData(line_cfg_obj);
> +	if (!line_cfg)
> +		return NULL;
> +
> +	Py_BEGIN_ALLOW_THREADS;
> +	ret = gpiod_line_request_reconfigure_lines(self->request, line_cfg);
> +	Py_END_ALLOW_THREADS;
> +	if (ret)
> +		return Py_gpiod_SetErrFromErrno();
> +
> +	Py_RETURN_NONE;
> +}
> +
> +static PyObject *request_read_edge_event(request_object *self, PyObject *args)
> +{
> +	PyObject *max_events_obj, *event_obj, *events, *type;
> +	struct gpiod_edge_event_buffer *buffer;
> +	size_t max_events, num_events, i;
> +	struct gpiod_edge_event *event;
> +	int ret;
> +
> +	ret = PyArg_ParseTuple(args, "O", &max_events_obj);
> +	if (!ret)
> +		return NULL;
> +
> +	if (max_events_obj != Py_None) {
> +		max_events = PyLong_AsSize_t(max_events_obj);
> +		if (PyErr_Occurred())
> +			return NULL;
> +	} else {
> +		max_events = 64;
> +	}
> +
> +	type = Py_gpiod_GetGlobalType("EdgeEvent");
> +	if (!type)
> +		return NULL;
> +
> +	buffer = gpiod_edge_event_buffer_new(max_events);
> +	if (!buffer)
> +		return Py_gpiod_SetErrFromErrno();
> +

A new buffer every time?
Maybe cache it in the request_object for next time?

> +	Py_BEGIN_ALLOW_THREADS;
> +	ret = gpiod_line_request_read_edge_event(self->request,
> +						 buffer, max_events);
> +	Py_END_ALLOW_THREADS;
> +	if (ret < 0) {
> +		gpiod_edge_event_buffer_free(buffer);
> +		return NULL;
> +	}
> +
> +	num_events = ret;
> +
> +	events = PyList_New(num_events);
> +	if (!events) {
> +		gpiod_edge_event_buffer_free(buffer);
> +		return NULL;
> +	}
> +
> +	for (i = 0; i < num_events; i++) {
> +		event = gpiod_edge_event_buffer_get_event(buffer, i);
> +		if (!event) {
> +			gpiod_edge_event_buffer_free(buffer);
> +			Py_DECREF(events);
> +			return NULL;
> +		}
> +
> +		event_obj = PyObject_CallFunction(type, "iKiii",
> +				gpiod_edge_event_get_event_type(event),
> +				gpiod_edge_event_get_timestamp_ns(event),
> +				gpiod_edge_event_get_line_offset(event),
> +				gpiod_edge_event_get_global_seqno(event),
> +				gpiod_edge_event_get_line_seqno(event));
> +		if (!event_obj) {
> +			gpiod_edge_event_buffer_free(buffer);
> +			Py_DECREF(events);
> +			return NULL;
> +		}
> +
> +		ret = PyList_SetItem(events, i, event_obj);
> +		if (ret) {
> +			gpiod_edge_event_buffer_free(buffer);
> +			Py_DECREF(event_obj);
> +			Py_DECREF(events);
> +			return NULL;
> +		}
> +	}
> +
> +	gpiod_edge_event_buffer_free(buffer);
> +
> +	return events;
> +}
> +
> +static PyMethodDef request_methods[] = {
> +	{
> +		.ml_name = "release",
> +		.ml_meth = (PyCFunction)request_release,
> +		.ml_flags = METH_NOARGS,
> +	},
> +	{
> +		.ml_name = "get_values",
> +		.ml_meth = (PyCFunction)request_get_values,
> +		.ml_flags = METH_VARARGS,
> +	},
> +	{
> +		.ml_name = "set_values",
> +		.ml_meth = (PyCFunction)request_set_values,
> +		.ml_flags = METH_VARARGS,
> +	},
> +	{
> +		.ml_name = "reconfigure_lines",
> +		.ml_meth = (PyCFunction)request_reconfigure_lines,
> +		.ml_flags = METH_VARARGS,
> +	},
> +	{
> +		.ml_name = "read_edge_event",
> +		.ml_meth = (PyCFunction)request_read_edge_event,
> +		.ml_flags = METH_VARARGS,
> +	},
> +	{ }
> +};
> +
> +PyTypeObject request_type = {
> +	PyVarObject_HEAD_INIT(NULL, 0)
> +	.tp_name = "gpiod._ext.Request",
> +	.tp_basicsize = sizeof(request_object),
> +	.tp_flags = Py_TPFLAGS_DEFAULT,
> +	.tp_new = PyType_GenericNew,
> +	.tp_init = (initproc)request_init,
> +	.tp_finalize = (destructor)request_finalize,
> +	.tp_dealloc = (destructor)Py_gpiod_dealloc,
> +	.tp_getset = request_getset,
> +	.tp_methods = request_methods,
> +};
> +
> +PyObject *Py_gpiod_MakeRequestObject(struct gpiod_line_request *request)
> +{
> +	request_object *req_obj;
> +	unsigned int *offsets;
> +	size_t num_lines;
> +	int *values;
> +
> +	num_lines = gpiod_line_request_get_num_lines(request);
> +
> +	req_obj = PyObject_New(request_object, &request_type);
> +	if (!req_obj)
> +		return NULL;
> +
> +	offsets = PyMem_Calloc(num_lines, sizeof(unsigned int));
> +	if (!offsets) {
> +		Py_DECREF(req_obj);
> +		return NULL;
> +	}
> +
> +	values = PyMem_Calloc(num_lines, sizeof(int));
> +	if (!values) {
> +		PyMem_Free(offsets);
> +		Py_DECREF(req_obj);
> +		return NULL;
> +	}
> +
> +	req_obj->request = request;
> +	req_obj->offsets = offsets;
> +	req_obj->values = values;
> +	req_obj->num_lines = num_lines;
> +
> +	return (PyObject *)req_obj;
> +}
> diff --git a/bindings/python/gpiod/line_info.py b/bindings/python/gpiod/line_info.py
> new file mode 100644
> index 0000000..9a6c9bf
> --- /dev/null
> +++ b/bindings/python/gpiod/line_info.py
> @@ -0,0 +1,73 @@
> +# SPDX-License-Identifier: LGPL-2.1-or-later
> +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
> +
> +from . import _ext
> +from dataclasses import dataclass
> +from datetime import timedelta
> +from gpiod.line import Direction, Bias, Drive, Edge, Clock
> +
> +
> +@dataclass(frozen=True, init=False, repr=False)
> +class LineInfo:
> +    """
> +    Snapshot of a line's status.
> +    """
> +
> +    offset: int
> +    name: str
> +    used: bool
> +    consumer: str
> +    direction: Direction
> +    active_low: bool
> +    bias: Bias
> +    drive: Drive
> +    edge_detection: Edge
> +    event_clock: Clock
> +    debounced: bool

I've probably asked this before, but does debounced differ from
not debounce_period?

> +    debounce_period: timedelta
> +
> +    def __init__(
> +        self,
> +        offset: int,
> +        name: str,
> +        used: bool,
> +        consumer: str,
> +        direction: int,
> +        active_low: bool,
> +        bias: int,
> +        drive: int,
> +        edge_detection: int,
> +        event_clock: int,
> +        debounced: bool,
> +        debounce_period_us: int,
> +    ):
> +        object.__setattr__(self, "offset", offset)
> +        object.__setattr__(self, "name", name)
> +        object.__setattr__(self, "used", used)
> +        object.__setattr__(self, "consumer", consumer)
> +        object.__setattr__(self, "direction", Direction(direction))
> +        object.__setattr__(self, "active_low", active_low)
> +        object.__setattr__(self, "bias", Bias(bias))
> +        object.__setattr__(self, "drive", Drive(drive))
> +        object.__setattr__(self, "edge_detection", Edge(edge_detection))
> +        object.__setattr__(self, "event_clock", Clock(event_clock))
> +        object.__setattr__(self, "debounced", debounced)
> +        object.__setattr__(
> +            self, "debounce_period", timedelta(microseconds=debounce_period_us)
> +        )
> +
> +    def __str__(self):
> +        return '<LineInfo offset={} name="{}" used={} consumer="{}" direction={} active_low={} bias={} drive={} edge_detection={} event_clock={} debounced={} debounce_period={}>'.format(
> +            self.offset,
> +            self.name,
> +            self.used,
> +            self.consumer,
> +            self.direction,
> +            self.active_low,
> +            self.bias,
> +            self.drive,
> +            self.edge_detection,
> +            self.event_clock,
> +            self.debounced,
> +            self.debounce_period,
> +        )
> diff --git a/bindings/python/gpiod/line_request.py b/bindings/python/gpiod/line_request.py
> new file mode 100644
> index 0000000..a3ee392
> --- /dev/null
> +++ b/bindings/python/gpiod/line_request.py
> @@ -0,0 +1,258 @@
> +# SPDX-License-Identifier: LGPL-2.1-or-later
> +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
> +
> +from . import _ext
> +from .edge_event import EdgeEvent
> +from .exception import RequestReleasedError
> +from .internal import poll_fd
> +from .line import Value
> +from .line_settings import LineSettings, _line_settings_to_ext_line_settings
> +from collections.abc import Iterable
> +from datetime import timedelta
> +from typing import Optional, Union
> +
> +
> +class LineRequest:
> +    """
> +    Stores the context of a set of requested GPIO lines.
> +    """
> +
> +    def __init__(self, req: _ext.Request):
> +        """
> +        DON'T USE
> +
> +        LineRequest objects can only be instantiated by a Chip parent. This is
> +        not part of stable API.
> +        """
> +        self._req = req
> +
> +    def __bool__(self) -> bool:
> +        """
> +        Boolean conversion for GPIO line requests.
> +
> +        Returns:
> +          True if the request is live and False if it's been released.
> +        """
> +        return True if self._req else False

           return self._req

> +
> +    def __enter__(self):
> +        """
> +        Controlled execution enter callback.
> +        """
> +        self._check_released()
> +        return self
> +
> +    def __exit__(self, exc_type, exc_value, traceback):
> +        """
> +        Controlled execution exit callback.
> +        """
> +        self.release()
> +
> +    def _check_released(self) -> None:
> +        if not self._req:
> +            raise RequestReleasedError()
> +
> +    def release(self) -> None:
> +        """
> +        Release this request and free all associated resources. The object must
> +        not be used after a call to this method.
> +        """
> +        self._check_released()
> +        self._req.release()
> +        self._req = None
> +
> +    def get_value(self, line: Union[int, str]) -> Value:
> +        """
> +        Get a single GPIO line value.
> +
> +        Args:
> +          line:
> +            Offset or name of the line to get value for.
> +
> +        Returns:
> +          Logical value of the line.
> +        """
> +        return self.get_values([line])[0]
> +
> +    def get_values(
> +        self, lines: Optional[Iterable[Union[int, str]]] = None
> +    ) -> list[Value]:
> +        """
> +        Get values of a set of GPIO lines.
> +
> +        Args:
> +          lines:
> +            List of names or offsets of GPIO lines to get values for. Can be
> +            None in which case all requested lines will be read.
> +
> +        Returns:
> +          List of logical line values.
> +        """
> +        self._check_released()
> +
> +        if lines is None:
> +            lines = self._lines

What if lines is empty?  Is that equivalent to None, or an error?
If the former...

           lines = lines or self._lines

either here or where used below (if that changed to a list comprehension)

> +
> +        offsets = [None] * len(lines)
> +
> +        for i, line in enumerate(lines):
> +            if isinstance(line, str):
> +                if line not in self._name_map:
> +                    raise ValueError("unknown line name: {}".format(line))
> +
> +                offsets[i] = self._name_map[line]
> +            else:
> +                offsets[i] = line
> +

I would do this with a list comprehension and a helper function to do
the id to offset mapping.  i.e.

           offsets = [ offset_from_id(id) for id in lines ]

> +        buf = [None] * len(lines)
> +
> +        self._req.get_values(offsets, buf)
> +        return buf
> +
> +    def set_value(self, line: Union[int, str], value: Value) -> None:
> +        """
> +        Set the value of a single GPIO line.
> +
> +        Args:
> +          line:
> +            Offset or name of the line to set.
> +          value:
> +            New value.
> +        """
> +        self.set_values({line: value})
> +
> +    def set_values(self, values: dict[Union[int, str], Value]) -> None:
> +        """
> +        Set the values of a subset of GPIO lines.
> +
> +        Args:
> +          values:
> +            Dictionary mapping line offsets or names to desired values.
> +        """
> +        self._check_released()
> +
> +        mapped = dict()
> +        for i, line in enumerate(values):
> +            if isinstance(line, str):
> +                if line not in self._name_map:
> +                    raise ValueError("unknown line name: {}".format(line))
> +
> +                mapped[self._name_map[line]] = values[line]
> +            else:
> +                mapped[line] = values[line]
> +
> +        self._req.set_values(mapped)
> +
> +    def reconfigure_lines(
> +        self, config: dict[tuple[Union[int, str]], LineSettings]
> +    ) -> None:
> +        """
> +        Reconfigure requested lines.
> +
> +        Args:
> +          config
> +            Dictionary mapping offsets or names (or tuples thereof) to
> +            LineSettings. If None is passed as the value of the mapping,
> +            default settings are used.
> +        """
> +        self._check_released()
> +
> +        line_cfg = _ext.LineConfig()
> +
> +        for lines, settings in config.items():
> +            if isinstance(lines, int) or isinstance(lines, str):
> +                lines = [lines]
> +
> +            offsets = [None] * len(lines)
> +
> +            for i, line in enumerate(lines):
> +                if isinstance(line, str):
> +                    if line not in self._name_map:
> +                        raise ValueError("unknown line name: {}".format(line))
> +
> +                    offsets[i] = self._name_map[line]
> +                else:
> +                    offsets[i] = line

And again...
               offsets = [ offset_from_id(id) for id in lines ]

> +
> +            line_cfg.add_line_settings(
> +                offsets, _line_settings_to_ext_line_settings(settings)
> +            )
> +
> +        self._req.reconfigure_lines(line_cfg)
> +
> +    def wait_edge_event(
> +        self, timeout: Optional[Union[timedelta, float]] = None
> +    ) -> bool:
> +        """
> +        Wait for edge events on any of the requested lines.
> +
> +        Args:
> +          timeout:
> +            Wait time limit expressed as either a datetime.timedelta object
> +            or the number of seconds stored in a float.
> +
> +        Returns:
> +          True if events are ready to be read. False on timeout.
> +        """
> +        self._check_released()
> +
> +        return poll_fd(self.fd, timeout)
> +
> +    def read_edge_event(self, max_events: Optional[int] = None) -> list[EdgeEvent]:
> +        """
> +        Read a number of edge events from a line request.
> +
> +        Args:
> +          max_events:
> +            Maximum number of events to read.
> +
> +        Returns:
> +          List of read EdgeEvent objects.
> +        """
> +        self._check_released()
> +
> +        return self._req.read_edge_event(max_events)
> +
> +    def __str__(self):
> +        """
> +        Return a user-friendly, human-readable description of this request.
> +        """
> +        if not self._req:
> +            return "<LineRequest RELEASED>"
> +
> +        return "<LineRequest num_lines={} offsets={} fd={}>".format(
> +            self.num_lines, self.offsets, self.fd
> +        )
> +
> +    @property
> +    def num_lines(self) -> int:
> +        """
> +        Number of requested lines.
> +        """
> +        self._check_released()
> +        return len(self._offsets)
> +
> +    @property
> +    def offsets(self) -> list[int]:
> +        """
> +        List of requested offsets. Lines requested by name are mapped to their
> +        offsets.
> +        """
> +        self._check_released()
> +        return self._offsets
> +
> +    @property
> +    def lines(self) -> list[Union[int, str]]:
> +        """
> +        List of requested lines. Lines requested by name are listed as such.
> +        """
> +        self._check_released()
> +        return self._lines
> +
> +    @property
> +    def fd(self) -> int:
> +        """
> +        File descriptor associated with this request.
> +        """
> +        self._check_released()
> +        return self._req.fd
> diff --git a/bindings/python/gpiod/line_settings.py b/bindings/python/gpiod/line_settings.py
> new file mode 100644
> index 0000000..1315b0c
> --- /dev/null
> +++ b/bindings/python/gpiod/line_settings.py
> @@ -0,0 +1,62 @@
> +# SPDX-License-Identifier: LGPL-2.1-or-later
> +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
> +
> +from . import _ext
> +from dataclasses import dataclass
> +from datetime import timedelta
> +from gpiod.line import Direction, Bias, Drive, Edge, Clock, Value
> +
> +
> +@dataclass(repr=False)
> +class LineSettings:
> +    """
> +    Stores a set of line properties.
> +    """
> +
> +    direction: Direction = Direction.AS_IS
> +    edge_detection: Edge = Edge.NONE
> +    bias: Bias = Bias.AS_IS
> +    drive: Drive = Drive.PUSH_PULL
> +    active_low: bool = False
> +    debounce_period: timedelta = timedelta()
> +    event_clock: Clock = Clock.MONOTONIC
> +    output_value: Value = Value.INACTIVE
> +
> +    # __repr__ generated by @dataclass uses repr for enum members resulting in
> +    # an unusable representation as those are of the form: <NAME: $value>
> +    def __repr__(self):
> +        return "LineSettings(direction={}, edge_detection={} bias={} drive={} active_low={} debounce_period={} event_clock={} output_value={})".format(
> +            str(self.direction),
> +            str(self.edge_detection),
> +            str(self.bias),
> +            str(self.drive),
> +            self.active_low,
> +            repr(self.debounce_period),
> +            str(self.event_clock),
> +            str(self.output_value),
> +        )
> +
> +    def __str__(self):
> +        return "<LineSettings direction={} edge_detection={} bias={} drive={} active_low={} debounce_period={} event_clock={} output_value={}>".format(
> +            self.direction,
> +            self.edge_detection,
> +            self.bias,
> +            self.drive,
> +            self.active_low,
> +            self.debounce_period,
> +            self.event_clock,
> +            self.output_value,
> +        )
> +
> +
> +def _line_settings_to_ext_line_settings(settings: LineSettings) -> _ext.LineSettings:
> +    return _ext.LineSettings(
> +        direction=settings.direction.value,
> +        edge_detection=settings.edge_detection.value,
> +        bias=settings.bias.value,
> +        drive=settings.drive.value,
> +        active_low=settings.active_low,
> +        debounce_period=int(settings.debounce_period.total_seconds() * 1000000),
> +        event_clock=settings.event_clock.value,
> +        output_value=settings.output_value.value,
> +    )

As mentioned earlier, rename to _line_settings_to_ext??

> diff --git a/bindings/python/setup.py b/bindings/python/setup.py
> new file mode 100644
> index 0000000..ec8f99d
> --- /dev/null

Ok, the API is much nicer than the previous versions, so that is all
good, given some clarification on the config semantics.

Other than the couple of PyArg_ParseTupleAndKeywords issues, 
the other few minor bugs, and my obvious preference for using list
comprehensions to build lists, the big sticking point for me is thread
safety.
I'm not convinced your current approach is thread safe, so convince me,
either by proving me wrong or providing another solution.
Hopefully I'm wrong.

Cheers,
Kent.

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [libgpiod v2][PATCH v3 4/4] bindings: python: implement python bindings for libgpiod v2
  2022-10-13  3:10   ` Kent Gibson
@ 2022-10-13 11:12     ` Kent Gibson
  2022-10-26 12:32     ` Bartosz Golaszewski
  1 sibling, 0 replies; 30+ messages in thread
From: Kent Gibson @ 2022-10-13 11:12 UTC (permalink / raw)
  To: Bartosz Golaszewski
  Cc: Linus Walleij, Andy Shevchenko, Viresh Kumar, linux-gpio

On Thu, Oct 13, 2022 at 11:10:26AM +0800, Kent Gibson wrote:
> On Fri, Oct 07, 2022 at 04:55:21PM +0200, Bartosz Golaszewski wrote:
> > This adds python bindings for libgpiod v2. As opposed to v1, they are
> > mostly written in python with just low-level elements written in C and
> > interfacing with libgpiod.so.
> > 
> > We've also added setup.py which will allow to use pip for managing the
> > bindings and split them into a separate meta-openembedded recipe.
> > 
> > Signed-off-by: Bartosz Golaszewski <brgl@bgdev.pl>
> 
> <snipping out files with no probs>
> 
> Other than the couple of PyArg_ParseTupleAndKeywords issues, 
> the other few minor bugs, and my obvious preference for using list
> comprehensions to build lists, the big sticking point for me is thread
> safety.
> I'm not convinced your current approach is thread safe, so convince me,
> either by proving me wrong or providing another solution.
> Hopefully I'm wrong.
> 

But it isn't expected to be MT-safe, is it?

Cheers,
Kent.

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [libgpiod v2][PATCH v3 2/4] bindings: python: add examples
  2022-10-13  3:09   ` Kent Gibson
@ 2022-10-17 12:00     ` Bartosz Golaszewski
  2022-10-17 12:11       ` Kent Gibson
  0 siblings, 1 reply; 30+ messages in thread
From: Bartosz Golaszewski @ 2022-10-17 12:00 UTC (permalink / raw)
  To: Kent Gibson; +Cc: Linus Walleij, Andy Shevchenko, Viresh Kumar, linux-gpio

On Thu, Oct 13, 2022 at 5:09 AM Kent Gibson <warthog618@gmail.com> wrote:
>
> On Fri, Oct 07, 2022 at 04:55:19PM +0200, Bartosz Golaszewski wrote:
> > This adds the regular set of example programs implemented using libgpiod
> > python bindings.
> >
> > Signed-off-by: Bartosz Golaszewski <brgl@bgdev.pl>
> > ---
> >  bindings/python/examples/Makefile.am   | 10 +++++++
> >  bindings/python/examples/gpiodetect.py | 17 ++++++++++++
> >  bindings/python/examples/gpiofind.py   | 20 ++++++++++++++
> >  bindings/python/examples/gpioget.py    | 31 +++++++++++++++++++++
> >  bindings/python/examples/gpioinfo.py   | 35 ++++++++++++++++++++++++
> >  bindings/python/examples/gpiomon.py    | 28 +++++++++++++++++++
> >  bindings/python/examples/gpioset.py    | 37 ++++++++++++++++++++++++++
> >  7 files changed, 178 insertions(+)
> >  create mode 100644 bindings/python/examples/Makefile.am
> >  create mode 100755 bindings/python/examples/gpiodetect.py
> >  create mode 100755 bindings/python/examples/gpiofind.py
> >  create mode 100755 bindings/python/examples/gpioget.py
> >  create mode 100755 bindings/python/examples/gpioinfo.py
> >  create mode 100755 bindings/python/examples/gpiomon.py
> >  create mode 100755 bindings/python/examples/gpioset.py
> >
> > diff --git a/bindings/python/examples/Makefile.am b/bindings/python/examples/Makefile.am
> > new file mode 100644
> > index 0000000..f42b80e
> > --- /dev/null
> > +++ b/bindings/python/examples/Makefile.am
> > @@ -0,0 +1,10 @@
> > +# SPDX-License-Identifier: GPL-2.0-or-later
> > +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
> > +
> > +EXTRA_DIST = \
> > +     gpiodetect.py \
> > +     gpiofind.py \
> > +     gpioget.py \
> > +     gpioinfo.py \
> > +     gpiomon.py \
> > +     gpioset.py
> > diff --git a/bindings/python/examples/gpiodetect.py b/bindings/python/examples/gpiodetect.py
> > new file mode 100755
> > index 0000000..c32014f
> > --- /dev/null
> > +++ b/bindings/python/examples/gpiodetect.py
> > @@ -0,0 +1,17 @@
> > +#!/usr/bin/env python3
> > +# SPDX-License-Identifier: GPL-2.0-or-later
> > +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
> > +
> > +"""Reimplementation of the gpiodetect tool in Python."""
> > +
> > +import gpiod
> > +import os
> > +
> > +if __name__ == "__main__":
> > +    for entry in os.scandir("/dev/"):
> > +        if gpiod.is_gpiochip_device(entry.path):
>
> Add a helper generator function that returns the available chip paths?
> And in order might be nice too.
>
> > +            with gpiod.Chip(entry.path) as chip:
> > +                info = chip.get_info()
> > +                print(
> > +                    "{} [{}] ({} lines)".format(info.name, info.label, info.num_lines)
> > +                )
> > diff --git a/bindings/python/examples/gpiofind.py b/bindings/python/examples/gpiofind.py
> > new file mode 100755
> > index 0000000..2f30445
> > --- /dev/null
> > +++ b/bindings/python/examples/gpiofind.py
> > @@ -0,0 +1,20 @@
> > +#!/usr/bin/env python3
> > +# SPDX-License-Identifier: GPL-2.0-or-later
> > +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
> > +
> > +"""Reimplementation of the gpiofind tool in Python."""
> > +
> > +import gpiod
> > +import os
> > +import sys
> > +
> > +if __name__ == "__main__":
> > +    for entry in os.scandir("/dev/"):
> > +        if gpiod.is_gpiochip_device(entry.path):
> > +            with gpiod.Chip(entry.path) as chip:
> > +                offset = chip.map_line(sys.argv[1])
>
>                             chip.offset_from_id(...
>
> > +                if offset is not None:
> > +                    print("{} {}".format(chip.get_info().name, offset))
> > +                    sys.exit(0)
> > +
> > +    sys.exit(1)
> > diff --git a/bindings/python/examples/gpioget.py b/bindings/python/examples/gpioget.py
> > new file mode 100755
> > index 0000000..d441535
> > --- /dev/null
> > +++ b/bindings/python/examples/gpioget.py
> > @@ -0,0 +1,31 @@
> > +#!/usr/bin/env python3
> > +# SPDX-License-Identifier: GPL-2.0-or-later
> > +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
> > +
> > +"""Simplified reimplementation of the gpioget tool in Python."""
> > +
> > +import gpiod
> > +import sys
> > +
> > +from gpiod.line import Direction
> > +
> > +if __name__ == "__main__":
> > +    if len(sys.argv) < 3:
> > +        raise TypeError("usage: gpioget.py <gpiochip> <offset1> <offset2> ...")
> > +
> > +    path = sys.argv[1]
> > +    lines = []
> > +    for line in sys.argv[2:]:
> > +        lines.append(int(line) if line.isdigit() else line)
> > +
>
> Just leave the line ids as string?
>
> else use a list comprehension:
>
>     lines = [ int(id) if id.isdigit() else id for id in sys.argv[2:] ]
>
> Similarly elsewhere.
>
> > +    request = gpiod.request_lines(
> > +        path,
> > +        consumer="gpioget.py",
> > +        config={tuple(lines): gpiod.LineSettings(direction=Direction.INPUT)},
> > +    )
> > +
> > +    vals = request.get_values()
> > +
> > +    for val in vals:
> > +        print("{} ".format(val.value), end="")
> > +    print()
> > diff --git a/bindings/python/examples/gpioinfo.py b/bindings/python/examples/gpioinfo.py
> > new file mode 100755
> > index 0000000..e8c7d46
> > --- /dev/null
> > +++ b/bindings/python/examples/gpioinfo.py
> > @@ -0,0 +1,35 @@
> > +#!/usr/bin/env python3
> > +# SPDX-License-Identifier: GPL-2.0-or-later
> > +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
> > +
> > +"""Simplified reimplementation of the gpioinfo tool in Python."""
> > +
> > +import gpiod
> > +import os
> > +
> > +if __name__ == "__main__":
> > +    for entry in os.scandir("/dev/"):
> > +        if gpiod.is_gpiochip_device(entry.path):
> > +            with gpiod.Chip(entry.path) as chip:
> > +                cinfo = chip.get_info()
> > +                print("{} - {} lines:".format(cinfo.name, cinfo.num_lines))
> > +
> > +                for offset in range(0, cinfo.num_lines):
> > +                    linfo = chip.get_line_info(offset)
> > +                    offset = linfo.offset
> > +                    name = linfo.name
> > +                    consumer = linfo.consumer
> > +                    direction = linfo.direction
>
>                     is_input = linfo.direction == gpiod.line.Direction.INPUT
>
> That is for space saving below.
> Drop the others as they are only referenced once (if you follow the
> suggestion below).
>
> > +                    active_low = linfo.active_low
> > +
> > +                    print(
> > +                        "\tline {:>3}: {:>18} {:>12} {:>8} {:>10}".format(
> > +                            offset,
> > +                            "unnamed" if name is None else name,
> > +                            "unused" if consumer is None else consumer,
> > +                            "input"
> > +                            if direction == gpiod.line.Direction.INPUT
> > +                            else "output",
> > +                            "active-low" if active_low else "active-high",
>                         linfo.offset,
>                         linfo.name or "unnamed",
>                         linfo.consumer or "unused",
>                         is_input and "input" or "output",
>                         linfo.active_low and "active_low" or "active-high",
>
> > +                        )
> > +                    )
> > diff --git a/bindings/python/examples/gpiomon.py b/bindings/python/examples/gpiomon.py
> > new file mode 100755
> > index 0000000..e0db16f
> > --- /dev/null
> > +++ b/bindings/python/examples/gpiomon.py
> > @@ -0,0 +1,28 @@
> > +#!/usr/bin/env python3
> > +# SPDX-License-Identifier: GPL-2.0-or-later
> > +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
> > +
> > +"""Simplified reimplementation of the gpiomon tool in Python."""
> > +
> > +import gpiod
> > +import sys
> > +
> > +from gpiod.line import Edge
> > +
> > +if __name__ == "__main__":
> > +    if len(sys.argv) < 3:
> > +        raise TypeError("usage: gpiomon.py <gpiochip> <offset1> <offset2> ...")
> > +
> > +    path = sys.argv[1]
> > +    lines = []
> > +    for line in sys.argv[2:]:
> > +        lines.append(int(line) if line.isdigit() else line)
> > +
> > +    with gpiod.request_lines(
> > +        path,
> > +        consumer="gpiomon.py",
> > +        config={tuple(lines): gpiod.LineSettings(edge_detection=Edge.BOTH)},
> > +    ) as request:
> > +        while True:
> > +            for event in request.read_edge_event():
> > +                print(event)
> > diff --git a/bindings/python/examples/gpioset.py b/bindings/python/examples/gpioset.py
> > new file mode 100755
> > index 0000000..f0b0681
> > --- /dev/null
> > +++ b/bindings/python/examples/gpioset.py
> > @@ -0,0 +1,37 @@
> > +#!/usr/bin/env python3
> > +# SPDX-License-Identifier: GPL-2.0-or-later
> > +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
> > +
> > +"""Simplified reimplementation of the gpioset tool in Python."""
> > +
> > +import gpiod
> > +import sys
> > +
> > +from gpiod.line import Direction, Value
> > +
> > +if __name__ == "__main__":
> > +    if len(sys.argv) < 3:
> > +        raise TypeError(
> > +            "usage: gpioset.py <gpiochip> <offset1>=<value1> <offset2>=<value2> ..."
> > +        )
> > +
> > +    path = sys.argv[1]
> > +    values = dict()
> > +    lines = []
> > +    for arg in sys.argv[2:]:
> > +        arg = arg.split("=")
> > +        key = int(arg[0]) if arg[0].isdigit() else arg[0]
> > +        val = int(arg[1])
> > +
> > +        lines.append(key)
> > +        values[key] = Value(val)
> > +
>
>         lvs = [ arg.split('=') for arg in sys.argv[2:] ]
>         lines = [ x[0] for x in lvs ]
>         values = dict[lvs]

It must be dict(lvs) but even then values are still strings, not their
enum integer representations, so we still need to convert them to ints
before passing them to set_values(). I'd leave it as it is now.

>
> > +    request = gpiod.request_lines(
> > +        path,
> > +        consumer="gpioset.py",
> > +        config={tuple(lines): gpiod.LineSettings(direction=Direction.OUTPUT)},
> > +    )
> > +
> > +    vals = request.set_values(values)
> > +
> > +    input()
> > --
>
> No gpiowatch?
>

There's no gpiowatch yet in the main set of tools, I'll add them once they land.

> Add some examples for features the tools don't use, like requests with
> both inputs and outputs, and reconfigure?
>

These are already covered in tests. I may add them in the future but
don't want to add a lot of code that's not routinely run as part of
tests.

Bart

> Cheers,
> Kent.

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [libgpiod v2][PATCH v3 2/4] bindings: python: add examples
  2022-10-17 12:00     ` Bartosz Golaszewski
@ 2022-10-17 12:11       ` Kent Gibson
  2022-10-17 13:49         ` Andy Shevchenko
  0 siblings, 1 reply; 30+ messages in thread
From: Kent Gibson @ 2022-10-17 12:11 UTC (permalink / raw)
  To: Bartosz Golaszewski
  Cc: Linus Walleij, Andy Shevchenko, Viresh Kumar, linux-gpio

On Mon, Oct 17, 2022 at 02:00:15PM +0200, Bartosz Golaszewski wrote:
> On Thu, Oct 13, 2022 at 5:09 AM Kent Gibson <warthog618@gmail.com> wrote:
> >
> > On Fri, Oct 07, 2022 at 04:55:19PM +0200, Bartosz Golaszewski wrote:
> > > This adds the regular set of example programs implemented using libgpiod
> > > python bindings.
> > >
> > > Signed-off-by: Bartosz Golaszewski <brgl@bgdev.pl>
> > > +    path = sys.argv[1]
> > > +    values = dict()
> > > +    lines = []
> > > +    for arg in sys.argv[2:]:
> > > +        arg = arg.split("=")
> > > +        key = int(arg[0]) if arg[0].isdigit() else arg[0]
> > > +        val = int(arg[1])
> > > +
> > > +        lines.append(key)
> > > +        values[key] = Value(val)
> > > +
> >
> >         lvs = [ arg.split('=') for arg in sys.argv[2:] ]
            lvs = [ (x,int(y)) for (x,y) in lvs ]
> >         lines = [ x[0] for x in lvs ]
> >         values = dict(lvs)
> 

An extra pass to fix the int values.
You could do it in one with a more appropriate parser function.

Cheers,
Kent.


^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [libgpiod v2][PATCH v3 2/4] bindings: python: add examples
  2022-10-17 12:11       ` Kent Gibson
@ 2022-10-17 13:49         ` Andy Shevchenko
  2022-10-17 14:07           ` Kent Gibson
  0 siblings, 1 reply; 30+ messages in thread
From: Andy Shevchenko @ 2022-10-17 13:49 UTC (permalink / raw)
  To: Kent Gibson; +Cc: Bartosz Golaszewski, Linus Walleij, Viresh Kumar, linux-gpio

On Mon, Oct 17, 2022 at 08:11:28PM +0800, Kent Gibson wrote:
> On Mon, Oct 17, 2022 at 02:00:15PM +0200, Bartosz Golaszewski wrote:
> > On Thu, Oct 13, 2022 at 5:09 AM Kent Gibson <warthog618@gmail.com> wrote:
> > > On Fri, Oct 07, 2022 at 04:55:19PM +0200, Bartosz Golaszewski wrote:
> > > > This adds the regular set of example programs implemented using libgpiod
> > > > python bindings.
> > > >
> > > > Signed-off-by: Bartosz Golaszewski <brgl@bgdev.pl>
> > > > +    path = sys.argv[1]
> > > > +    values = dict()
> > > > +    lines = []
> > > > +    for arg in sys.argv[2:]:
> > > > +        arg = arg.split("=")
> > > > +        key = int(arg[0]) if arg[0].isdigit() else arg[0]
> > > > +        val = int(arg[1])
> > > > +
> > > > +        lines.append(key)
> > > > +        values[key] = Value(val)
> > > > +
> > >
> > >         lvs = [ arg.split('=') for arg in sys.argv[2:] ]
>             lvs = [ (x,int(y)) for (x,y) in lvs ]
> > >         lines = [ x[0] for x in lvs ]
> > >         values = dict(lvs)
> > 
> 
> An extra pass to fix the int values.

In Python we have map(), which I think is the best for that kind of job.

> You could do it in one with a more appropriate parser function.

It seems we need some Python guru to revisit the code, because to me
it looks a bit C:ish :-)

Maybe I can ask colleague of mine, if he has time for a such...
No guarantees, though.

-- 
With Best Regards,
Andy Shevchenko



^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [libgpiod v2][PATCH v3 2/4] bindings: python: add examples
  2022-10-17 13:49         ` Andy Shevchenko
@ 2022-10-17 14:07           ` Kent Gibson
  2022-10-17 14:19             ` Andy Shevchenko
  0 siblings, 1 reply; 30+ messages in thread
From: Kent Gibson @ 2022-10-17 14:07 UTC (permalink / raw)
  To: Andy Shevchenko
  Cc: Bartosz Golaszewski, Linus Walleij, Viresh Kumar, linux-gpio

On Mon, Oct 17, 2022 at 04:49:55PM +0300, Andy Shevchenko wrote:
> On Mon, Oct 17, 2022 at 08:11:28PM +0800, Kent Gibson wrote:
> > On Mon, Oct 17, 2022 at 02:00:15PM +0200, Bartosz Golaszewski wrote:
> > > On Thu, Oct 13, 2022 at 5:09 AM Kent Gibson <warthog618@gmail.com> wrote:
> > > > On Fri, Oct 07, 2022 at 04:55:19PM +0200, Bartosz Golaszewski wrote:
> > > > > This adds the regular set of example programs implemented using libgpiod
> > > > > python bindings.
> > > > >
> > > > > Signed-off-by: Bartosz Golaszewski <brgl@bgdev.pl>
> > > > > +    path = sys.argv[1]
> > > > > +    values = dict()
> > > > > +    lines = []
> > > > > +    for arg in sys.argv[2:]:
> > > > > +        arg = arg.split("=")
> > > > > +        key = int(arg[0]) if arg[0].isdigit() else arg[0]
> > > > > +        val = int(arg[1])
> > > > > +
> > > > > +        lines.append(key)
> > > > > +        values[key] = Value(val)
> > > > > +
> > > >
> > > >         lvs = [ arg.split('=') for arg in sys.argv[2:] ]
> >             lvs = [ (x,int(y)) for (x,y) in lvs ]
> > > >         lines = [ x[0] for x in lvs ]
> > > >         values = dict(lvs)
> > > 
> > 
> > An extra pass to fix the int values.
> 
> In Python we have map(), which I think is the best for that kind of job.
> 

My understanding is map/filter is old school and list comprehensions
have replaced map, as generators have replaced filter.

i.e.
    list(map(function, iterable))
becomes
    [function(x) for x in iterable]

Either way, what we are missing here is a parser function that gives us
exactly the (offset,value) output we want from the command line string.

Oh, and we need both the lines list and the values dict, both of which
are easily created from the interim lvs.

> > You could do it in one with a more appropriate parser function.
> 
> It seems we need some Python guru to revisit the code, because to me
> it looks a bit C:ish :-)
> 

The for loop or the list comprehension?
Last I checked only one of those is available in C.
And yeah, the for loop version reads as C, so not at all Pythonic,
which is why I suggested the list comprehension.

> Maybe I can ask colleague of mine, if he has time for a such...
> No guarantees, though.
> 

Maybe I'm wrong and they've flipped back to map/filter.
Stanger things have happened.

Cheers,
Kent.

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [libgpiod v2][PATCH v3 2/4] bindings: python: add examples
  2022-10-17 14:07           ` Kent Gibson
@ 2022-10-17 14:19             ` Andy Shevchenko
  2022-10-17 15:53               ` Bartosz Golaszewski
  0 siblings, 1 reply; 30+ messages in thread
From: Andy Shevchenko @ 2022-10-17 14:19 UTC (permalink / raw)
  To: Kent Gibson; +Cc: Bartosz Golaszewski, Linus Walleij, Viresh Kumar, linux-gpio

On Mon, Oct 17, 2022 at 10:07:17PM +0800, Kent Gibson wrote:
> On Mon, Oct 17, 2022 at 04:49:55PM +0300, Andy Shevchenko wrote:
> > On Mon, Oct 17, 2022 at 08:11:28PM +0800, Kent Gibson wrote:
> > > On Mon, Oct 17, 2022 at 02:00:15PM +0200, Bartosz Golaszewski wrote:
> > > > On Thu, Oct 13, 2022 at 5:09 AM Kent Gibson <warthog618@gmail.com> wrote:
> > > > > On Fri, Oct 07, 2022 at 04:55:19PM +0200, Bartosz Golaszewski wrote:

...

> > > > >         lvs = [ arg.split('=') for arg in sys.argv[2:] ]
> > >             lvs = [ (x,int(y)) for (x,y) in lvs ]
> > > > >         lines = [ x[0] for x in lvs ]
> > > > >         values = dict(lvs)
> > > > 
> > > 
> > > An extra pass to fix the int values.
> > 
> > In Python we have map(), which I think is the best for that kind of job.
> > 
> 
> My understanding is map/filter is old school and list comprehensions
> have replaced map, as generators have replaced filter.
> 
> i.e.
>     list(map(function, iterable))
> becomes
>     [function(x) for x in iterable]

Definitely it does not cover all the cases map() is taking care of.
So it can't be old school :-)

* Yes, in this particular case it may be map() or list comprehension.
  But I think with map() the two lines can become one.

> Either way, what we are missing here is a parser function that gives us
> exactly the (offset,value) output we want from the command line string.
> 
> Oh, and we need both the lines list and the values dict, both of which
> are easily created from the interim lvs.
> 
> > > You could do it in one with a more appropriate parser function.
> > 
> > It seems we need some Python guru to revisit the code, because to me
> > it looks a bit C:ish :-)
> 
> The for loop or the list comprehension?
> Last I checked only one of those is available in C.
> And yeah, the for loop version reads as C, so not at all Pythonic,
> which is why I suggested the list comprehension.

Yes, but I believe it does not utilize the powerfulness of the current Python.
Anyway, I'm not a Py guru, take my remarks with a grain of salt.

-- 
With Best Regards,
Andy Shevchenko



^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [libgpiod v2][PATCH v3 2/4] bindings: python: add examples
  2022-10-17 14:19             ` Andy Shevchenko
@ 2022-10-17 15:53               ` Bartosz Golaszewski
  2022-10-17 16:09                 ` Kent Gibson
  2022-10-17 16:24                 ` Andy Shevchenko
  0 siblings, 2 replies; 30+ messages in thread
From: Bartosz Golaszewski @ 2022-10-17 15:53 UTC (permalink / raw)
  To: Andy Shevchenko; +Cc: Kent Gibson, Linus Walleij, Viresh Kumar, linux-gpio

On Mon, Oct 17, 2022 at 4:19 PM Andy Shevchenko
<andriy.shevchenko@linux.intel.com> wrote:
>
> On Mon, Oct 17, 2022 at 10:07:17PM +0800, Kent Gibson wrote:
> > On Mon, Oct 17, 2022 at 04:49:55PM +0300, Andy Shevchenko wrote:
> > > On Mon, Oct 17, 2022 at 08:11:28PM +0800, Kent Gibson wrote:
> > > > On Mon, Oct 17, 2022 at 02:00:15PM +0200, Bartosz Golaszewski wrote:
> > > > > On Thu, Oct 13, 2022 at 5:09 AM Kent Gibson <warthog618@gmail.com> wrote:
> > > > > > On Fri, Oct 07, 2022 at 04:55:19PM +0200, Bartosz Golaszewski wrote:
>
> ...
>
> > > > > >         lvs = [ arg.split('=') for arg in sys.argv[2:] ]
> > > >             lvs = [ (x,int(y)) for (x,y) in lvs ]
> > > > > >         lines = [ x[0] for x in lvs ]
> > > > > >         values = dict(lvs)
> > > > >
> > > >
> > > > An extra pass to fix the int values.
> > >
> > > In Python we have map(), which I think is the best for that kind of job.
> > >
> >
> > My understanding is map/filter is old school and list comprehensions
> > have replaced map, as generators have replaced filter.
> >
> > i.e.
> >     list(map(function, iterable))
> > becomes
> >     [function(x) for x in iterable]
>
> Definitely it does not cover all the cases map() is taking care of.
> So it can't be old school :-)
>
> * Yes, in this particular case it may be map() or list comprehension.
>   But I think with map() the two lines can become one.
>
> > Either way, what we are missing here is a parser function that gives us
> > exactly the (offset,value) output we want from the command line string.
> >
> > Oh, and we need both the lines list and the values dict, both of which
> > are easily created from the interim lvs.
> >
> > > > You could do it in one with a more appropriate parser function.
> > >
> > > It seems we need some Python guru to revisit the code, because to me
> > > it looks a bit C:ish :-)
> >
> > The for loop or the list comprehension?
> > Last I checked only one of those is available in C.
> > And yeah, the for loop version reads as C, so not at all Pythonic,
> > which is why I suggested the list comprehension.
>
> Yes, but I believe it does not utilize the powerfulness of the current Python.
> Anyway, I'm not a Py guru, take my remarks with a grain of salt.
>

How about this?

    lvs = list(
        map(
            lambda val: [val[0], Value(int(val[1]))],
            [arg.split("=") for arg in sys.argv[2:]],
        )
    )
    lines = [x[0] for x in lvs]
    values = dict(lvs)

It's so much less readable but at least it's pythonic, look at those
lambdas and comprehension lists and even a map! :)

Anyway - unlike the programming interface - these are just
implementation details that can be always improved later.

Bart

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [libgpiod v2][PATCH v3 2/4] bindings: python: add examples
  2022-10-17 15:53               ` Bartosz Golaszewski
@ 2022-10-17 16:09                 ` Kent Gibson
  2022-10-17 16:20                   ` Kent Gibson
  2022-10-17 16:24                 ` Andy Shevchenko
  1 sibling, 1 reply; 30+ messages in thread
From: Kent Gibson @ 2022-10-17 16:09 UTC (permalink / raw)
  To: Bartosz Golaszewski
  Cc: Andy Shevchenko, Linus Walleij, Viresh Kumar, linux-gpio

On Mon, Oct 17, 2022 at 05:53:52PM +0200, Bartosz Golaszewski wrote:
> On Mon, Oct 17, 2022 at 4:19 PM Andy Shevchenko
> <andriy.shevchenko@linux.intel.com> wrote:
> >
> > On Mon, Oct 17, 2022 at 10:07:17PM +0800, Kent Gibson wrote:
> > > On Mon, Oct 17, 2022 at 04:49:55PM +0300, Andy Shevchenko wrote:
> > > > On Mon, Oct 17, 2022 at 08:11:28PM +0800, Kent Gibson wrote:
> > > > > On Mon, Oct 17, 2022 at 02:00:15PM +0200, Bartosz Golaszewski wrote:
> > > > > > On Thu, Oct 13, 2022 at 5:09 AM Kent Gibson <warthog618@gmail.com> wrote:
> > > > > > > On Fri, Oct 07, 2022 at 04:55:19PM +0200, Bartosz Golaszewski wrote:
> >
> > ...
> >
> > > > > > >         lvs = [ arg.split('=') for arg in sys.argv[2:] ]
> > > > >             lvs = [ (x,int(y)) for (x,y) in lvs ]
> > > > > > >         lines = [ x[0] for x in lvs ]
> > > > > > >         values = dict(lvs)
> > > > > >
> > > > >
> > > > > An extra pass to fix the int values.
> > > >
> > > > In Python we have map(), which I think is the best for that kind of job.
> > > >
> > >
> > > My understanding is map/filter is old school and list comprehensions
> > > have replaced map, as generators have replaced filter.
> > >
> > > i.e.
> > >     list(map(function, iterable))
> > > becomes
> > >     [function(x) for x in iterable]
> >
> > Definitely it does not cover all the cases map() is taking care of.
> > So it can't be old school :-)
> >
> > * Yes, in this particular case it may be map() or list comprehension.
> >   But I think with map() the two lines can become one.
> >
> > > Either way, what we are missing here is a parser function that gives us
> > > exactly the (offset,value) output we want from the command line string.
> > >
> > > Oh, and we need both the lines list and the values dict, both of which
> > > are easily created from the interim lvs.
> > >
> > > > > You could do it in one with a more appropriate parser function.
> > > >
> > > > It seems we need some Python guru to revisit the code, because to me
> > > > it looks a bit C:ish :-)
> > >
> > > The for loop or the list comprehension?
> > > Last I checked only one of those is available in C.
> > > And yeah, the for loop version reads as C, so not at all Pythonic,
> > > which is why I suggested the list comprehension.
> >
> > Yes, but I believe it does not utilize the powerfulness of the current Python.
> > Anyway, I'm not a Py guru, take my remarks with a grain of salt.
> >
> 
> How about this?
> 
>     lvs = list(
>         map(
>             lambda val: [val[0], Value(int(val[1]))],
>             [arg.split("=") for arg in sys.argv[2:]],
>         )
>     )

which is the same as

    lvs = [ (x,Value(int(y))) for (x,y) in [ arg.split("=") for arg in sys.argv[2:]] ]

which is the same as my two liner, just nested - though it may only
iterate through the list once if the inner list comprehension is
treated as a generator.  Not sure.

Either way, not too fussed - it is only example code.
As long as it isn't a for loop ;-).

Cheers,
Kent.

>     lines = [x[0] for x in lvs]
>     values = dict(lvs)
> 
> It's so much less readable but at least it's pythonic, look at those
> lambdas and comprehension lists and even a map! :)
> 
> Anyway - unlike the programming interface - these are just
> implementation details that can be always improved later.
> 
> Bart

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [libgpiod v2][PATCH v3 2/4] bindings: python: add examples
  2022-10-17 16:09                 ` Kent Gibson
@ 2022-10-17 16:20                   ` Kent Gibson
  2022-10-17 16:55                     ` Andy Shevchenko
  0 siblings, 1 reply; 30+ messages in thread
From: Kent Gibson @ 2022-10-17 16:20 UTC (permalink / raw)
  To: Bartosz Golaszewski
  Cc: Andy Shevchenko, Linus Walleij, Viresh Kumar, linux-gpio

On Tue, Oct 18, 2022 at 12:09:10AM +0800, Kent Gibson wrote:
> On Mon, Oct 17, 2022 at 05:53:52PM +0200, Bartosz Golaszewski wrote:
> > On Mon, Oct 17, 2022 at 4:19 PM Andy Shevchenko
> > <andriy.shevchenko@linux.intel.com> wrote:
> > >
> > > On Mon, Oct 17, 2022 at 10:07:17PM +0800, Kent Gibson wrote:
> > > > On Mon, Oct 17, 2022 at 04:49:55PM +0300, Andy Shevchenko wrote:
> > > > > On Mon, Oct 17, 2022 at 08:11:28PM +0800, Kent Gibson wrote:
> > > > > > On Mon, Oct 17, 2022 at 02:00:15PM +0200, Bartosz Golaszewski wrote:
> > > > > > > On Thu, Oct 13, 2022 at 5:09 AM Kent Gibson <warthog618@gmail.com> wrote:
> > > > > > > > On Fri, Oct 07, 2022 at 04:55:19PM +0200, Bartosz Golaszewski wrote:
> > >
> > > ...
> > >
> > > > > > > >         lvs = [ arg.split('=') for arg in sys.argv[2:] ]
> > > > > >             lvs = [ (x,int(y)) for (x,y) in lvs ]
> > > > > > > >         lines = [ x[0] for x in lvs ]
> > > > > > > >         values = dict(lvs)
> > > > > > >
> > > > > >
> > > > > > An extra pass to fix the int values.
> > > > >
> > > > > In Python we have map(), which I think is the best for that kind of job.
> > > > >
> > > >
> > > > My understanding is map/filter is old school and list comprehensions
> > > > have replaced map, as generators have replaced filter.
> > > >
> > > > i.e.
> > > >     list(map(function, iterable))
> > > > becomes
> > > >     [function(x) for x in iterable]
> > >
> > > Definitely it does not cover all the cases map() is taking care of.
> > > So it can't be old school :-)
> > >
> > > * Yes, in this particular case it may be map() or list comprehension.
> > >   But I think with map() the two lines can become one.
> > >
> > > > Either way, what we are missing here is a parser function that gives us
> > > > exactly the (offset,value) output we want from the command line string.
> > > >
> > > > Oh, and we need both the lines list and the values dict, both of which
> > > > are easily created from the interim lvs.
> > > >
> > > > > > You could do it in one with a more appropriate parser function.
> > > > >
> > > > > It seems we need some Python guru to revisit the code, because to me
> > > > > it looks a bit C:ish :-)
> > > >
> > > > The for loop or the list comprehension?
> > > > Last I checked only one of those is available in C.
> > > > And yeah, the for loop version reads as C, so not at all Pythonic,
> > > > which is why I suggested the list comprehension.
> > >
> > > Yes, but I believe it does not utilize the powerfulness of the current Python.
> > > Anyway, I'm not a Py guru, take my remarks with a grain of salt.
> > >
> > 
> > How about this?
> > 
> >     lvs = list(
> >         map(
> >             lambda val: [val[0], Value(int(val[1]))],
> >             [arg.split("=") for arg in sys.argv[2:]],
> >         )
> >     )
> 
> which is the same as
> 
>     lvs = [ (x,Value(int(y))) for (x,y) in [ arg.split("=") for arg in sys.argv[2:]] ]
> 
> which is the same as my two liner, just nested - though it may only
> iterate through the list once if the inner list comprehension is
> treated as a generator.  Not sure.
> 
> Either way, not too fussed - it is only example code.
> As long as it isn't a for loop ;-).
> 

Oh, btw, the parser fn version would be something like:

def parse_value(arg):
     (x,y) = arg.split("=")
     return (x, Value(int(y)))

lvs = [ parse_value(arg) for arg in sys.argv[2:]
...

Is that clearer?

Cheers,
Kent.


^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [libgpiod v2][PATCH v3 2/4] bindings: python: add examples
  2022-10-17 15:53               ` Bartosz Golaszewski
  2022-10-17 16:09                 ` Kent Gibson
@ 2022-10-17 16:24                 ` Andy Shevchenko
  2022-10-17 16:39                   ` Kent Gibson
  1 sibling, 1 reply; 30+ messages in thread
From: Andy Shevchenko @ 2022-10-17 16:24 UTC (permalink / raw)
  To: Bartosz Golaszewski; +Cc: Kent Gibson, Linus Walleij, Viresh Kumar, linux-gpio

On Mon, Oct 17, 2022 at 05:53:52PM +0200, Bartosz Golaszewski wrote:
> On Mon, Oct 17, 2022 at 4:19 PM Andy Shevchenko
> <andriy.shevchenko@linux.intel.com> wrote:

...

> How about this?
> 
>     lvs = list(
>         map(
>             lambda val: [val[0], Value(int(val[1]))],
>             [arg.split("=") for arg in sys.argv[2:]],
>         )
>     )

Yeah, this looks ugly... So initial variant with two lines looks to me
like this:

  lvs = [arg.split("=") for arg in sys.argv[2:]] # btw, needs handling 2 exceptions
  values = dict((x, Value(int(y))) for (x,y) in lvs) # needs to handle an exception
  # Perhaps you need ordered dict?
  lines = values.keys()

>     lines = [x[0] for x in lvs]
>     values = dict(lvs)

> It's so much less readable but at least it's pythonic, look at those
> lambdas and comprehension lists and even a map! :)


-- 
With Best Regards,
Andy Shevchenko



^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [libgpiod v2][PATCH v3 2/4] bindings: python: add examples
  2022-10-17 16:24                 ` Andy Shevchenko
@ 2022-10-17 16:39                   ` Kent Gibson
  0 siblings, 0 replies; 30+ messages in thread
From: Kent Gibson @ 2022-10-17 16:39 UTC (permalink / raw)
  To: Andy Shevchenko
  Cc: Bartosz Golaszewski, Linus Walleij, Viresh Kumar, linux-gpio

On Mon, Oct 17, 2022 at 07:24:40PM +0300, Andy Shevchenko wrote:
> On Mon, Oct 17, 2022 at 05:53:52PM +0200, Bartosz Golaszewski wrote:
> > On Mon, Oct 17, 2022 at 4:19 PM Andy Shevchenko
> > <andriy.shevchenko@linux.intel.com> wrote:
> 
> ...
> 
> > How about this?
> > 
> >     lvs = list(
> >         map(
> >             lambda val: [val[0], Value(int(val[1]))],
> >             [arg.split("=") for arg in sys.argv[2:]],
> >         )
> >     )
> 
> Yeah, this looks ugly... So initial variant with two lines looks to me
> like this:
> 
>   lvs = [arg.split("=") for arg in sys.argv[2:]] # btw, needs handling 2 exceptions
>   values = dict((x, Value(int(y))) for (x,y) in lvs) # needs to handle an exception
>   # Perhaps you need ordered dict?
>   lines = values.keys()
> 

Indeed, an OrderedDict keys would provide the lines in argv order, so values.keys()
could be used in place of lines.

And if you use a parser function then it can deal with the parsing exceptions.

   values = OrderedDict([ parse(arg) for args in sys.argv[2:] ])

Cheers,
Kent.

> >     lines = [x[0] for x in lvs]
> >     values = dict(lvs)
> 
> > It's so much less readable but at least it's pythonic, look at those
> > lambdas and comprehension lists and even a map! :)
> 
> 
> -- 
> With Best Regards,
> Andy Shevchenko
> 
> 

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [libgpiod v2][PATCH v3 2/4] bindings: python: add examples
  2022-10-17 16:20                   ` Kent Gibson
@ 2022-10-17 16:55                     ` Andy Shevchenko
  2022-10-17 16:57                       ` Andy Shevchenko
  0 siblings, 1 reply; 30+ messages in thread
From: Andy Shevchenko @ 2022-10-17 16:55 UTC (permalink / raw)
  To: Kent Gibson; +Cc: Bartosz Golaszewski, Linus Walleij, Viresh Kumar, linux-gpio

On Tue, Oct 18, 2022 at 12:20:18AM +0800, Kent Gibson wrote:
> On Tue, Oct 18, 2022 at 12:09:10AM +0800, Kent Gibson wrote:

...

> Oh, btw, the parser fn version would be something like:
> 
> def parse_value(arg):
>      (x,y) = arg.split("=")
>      return (x, Value(int(y)))

Not a lisp, no need for too many parentheses. Also, we could use splices:

	eqidx = arg.index('=')
	return arg[:eqidx], Value(int(arg[eqidx + 1:]))

or with split()

	l, v = arg.split('=')
	return l, Value(int(v))

> lvs = [ parse_value(arg) for arg in sys.argv[2:]

Dunno why you put spaces inside outer [], but okay.

> Is that clearer?

-- 
With Best Regards,
Andy Shevchenko



^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [libgpiod v2][PATCH v3 2/4] bindings: python: add examples
  2022-10-17 16:55                     ` Andy Shevchenko
@ 2022-10-17 16:57                       ` Andy Shevchenko
  2022-10-17 17:26                         ` Bartosz Golaszewski
  0 siblings, 1 reply; 30+ messages in thread
From: Andy Shevchenko @ 2022-10-17 16:57 UTC (permalink / raw)
  To: Kent Gibson; +Cc: Bartosz Golaszewski, Linus Walleij, Viresh Kumar, linux-gpio

On Mon, Oct 17, 2022 at 07:55:41PM +0300, Andy Shevchenko wrote:
> On Tue, Oct 18, 2022 at 12:20:18AM +0800, Kent Gibson wrote:
> > On Tue, Oct 18, 2022 at 12:09:10AM +0800, Kent Gibson wrote:

...

> > Oh, btw, the parser fn version would be something like:
> > 
> > def parse_value(arg):
> >      (x,y) = arg.split("=")
> >      return (x, Value(int(y)))
> 
> Not a lisp, no need for too many parentheses. Also, we could use splices:
> 
> 	eqidx = arg.index('=')
> 	return arg[:eqidx], Value(int(arg[eqidx + 1:]))
> 
> or with split()
> 
> 	l, v = arg.split('=')
> 	return l, Value(int(v))
> 
> > lvs = [ parse_value(arg) for arg in sys.argv[2:]
> 
> Dunno why you put spaces inside outer [], but okay.

and this actually can be directly put to the dict constructor:

	values = OrderedDict(parse_value(arg) for arg in sys.argv[2:])

This looks short enough and readable.

-- 
With Best Regards,
Andy Shevchenko



^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [libgpiod v2][PATCH v3 2/4] bindings: python: add examples
  2022-10-17 16:57                       ` Andy Shevchenko
@ 2022-10-17 17:26                         ` Bartosz Golaszewski
  0 siblings, 0 replies; 30+ messages in thread
From: Bartosz Golaszewski @ 2022-10-17 17:26 UTC (permalink / raw)
  To: Andy Shevchenko; +Cc: Kent Gibson, Linus Walleij, Viresh Kumar, linux-gpio

On Mon, Oct 17, 2022 at 6:57 PM Andy Shevchenko
<andriy.shevchenko@linux.intel.com> wrote:
>
> On Mon, Oct 17, 2022 at 07:55:41PM +0300, Andy Shevchenko wrote:
> > On Tue, Oct 18, 2022 at 12:20:18AM +0800, Kent Gibson wrote:
> > > On Tue, Oct 18, 2022 at 12:09:10AM +0800, Kent Gibson wrote:
>
> ...
>
> > > Oh, btw, the parser fn version would be something like:
> > >
> > > def parse_value(arg):
> > >      (x,y) = arg.split("=")
> > >      return (x, Value(int(y)))
> >
> > Not a lisp, no need for too many parentheses. Also, we could use splices:
> >
> >       eqidx = arg.index('=')
> >       return arg[:eqidx], Value(int(arg[eqidx + 1:]))
> >
> > or with split()
> >
> >       l, v = arg.split('=')
> >       return l, Value(int(v))
> >
> > > lvs = [ parse_value(arg) for arg in sys.argv[2:]
> >
> > Dunno why you put spaces inside outer [], but okay.
>
> and this actually can be directly put to the dict constructor:
>
>         values = OrderedDict(parse_value(arg) for arg in sys.argv[2:])
>
> This looks short enough and readable.
>

Let's stop bikeshedding, I'll move on to stuff that really matters now. :)

Bartosz

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [libgpiod v2][PATCH v3 4/4] bindings: python: implement python bindings for libgpiod v2
  2022-10-13  3:10   ` Kent Gibson
  2022-10-13 11:12     ` Kent Gibson
@ 2022-10-26 12:32     ` Bartosz Golaszewski
  2022-10-26 12:56       ` Kent Gibson
  1 sibling, 1 reply; 30+ messages in thread
From: Bartosz Golaszewski @ 2022-10-26 12:32 UTC (permalink / raw)
  To: Kent Gibson; +Cc: Linus Walleij, Andy Shevchenko, Viresh Kumar, linux-gpio

On Thu, Oct 13, 2022 at 5:10 AM Kent Gibson <warthog618@gmail.com> wrote:
>
> On Fri, Oct 07, 2022 at 04:55:21PM +0200, Bartosz Golaszewski wrote:
> > This adds python bindings for libgpiod v2. As opposed to v1, they are
> > mostly written in python with just low-level elements written in C and
> > interfacing with libgpiod.so.
> >
> > We've also added setup.py which will allow to use pip for managing the
> > bindings and split them into a separate meta-openembedded recipe.
> >
> > Signed-off-by: Bartosz Golaszewski <brgl@bgdev.pl>
>

<snip - all those cut are done>

> Should ids fallback to being interpreted as ints if they can't be
> found as strings?  Why not leave that call to the user?
> If they aren't sure they can try it as a string, and if that fails try
> it as an int.
> (I realise this is the reverse argument of my comment in the examples
> patch btw - playing devil's advocate here - and there.)
>

I've thought about it and I think that the current behavior makes
sense in Python where things should "just work" out of the box. If
anything, we can add a flag no_implicit_conv or something.

> Is the range check on ints necessary? The kernel will do that when you
> make the call - what is the benefit of doing it here?

That way we won't make it seem like we correctly mapped the name?

> In which case why accept int at all?  Which would make the function
> offset_from_name() - just like the C.
>

This was your idea in the first place. :)

The reasoning was - it's python, so let's use its dynamic typing and
make the interface pretty lax.

> > +        """
> > +        self._check_closed()
> > +
> > +        if not isinstance(id, int):
> > +            try:
> > +                return self._chip.map_line(id)
> > +            except OSError as ex:
> > +                if ex.errno == ENOENT:
> > +                    try:
> > +                        offset = int(id)
> > +                    except ValueError:
> > +                        raise ex
> > +                else:
> > +                    raise ex
> > +        else:
> > +            offset = id
> > +
> > +        if offset >= self.get_info().num_lines:
> > +            raise ValueError("line offset of out range")
> > +
> > +        return offset
> > +
> > +    def _get_line_info(self, line: Union[int, str], watch: bool) -> LineInfo:
> > +        self._check_closed()
>
> Wrt the _check_closed(), what is to prevent another thread closing the
> chip here, and causing the next line to segfault?
>

Nothing, but we don't guarantee thread-safety like you mentioned in
your response below.

> And self.map_line(line) can release the GIL so all bets are off even if
> you assume cooperative multitasking - not that you can assume that.
>

Yes but that's up to the user to assure correct concurrent access to
libgpiod objects. Are there any benefits of trying to assure
thread-safety in the library code?

<snip>

> > +
> > +    def request_lines(
> > +        self,
> > +        config: dict[tuple[Union[int, str]], Optional[LineSettings]],
> > +        consumer: Optional[str] = None,
> > +        event_buffer_size: Optional[int] = None,
> > +    ) -> LineRequest:
> > +        """
> > +        Request a set of lines for exclusive usage.
> > +
> > +        Args:
> > +          config:
> > +            Dictionary mapping offsets or names (or tuples thereof) to
> > +            LineSettings. If None is passed as the value of the mapping,
> > +            default settings are used.
>
> What are the semantics for lines being repeated in the dict keys (which
> is possible as the keys are tuples, and also because the line can be
> identified by offset or name)?
>

Good point, I will clear that up in the docs and add test cases.

> > +          consumer:
> > +            Consumer string to use for this request.
> > +          event_buffer_size:
> > +            Size of the kernel edge event buffer to configure for this request.
> > +
> > +        Returns:
> > +          New LineRequest object.
> > +        """
> > +        self._check_closed()
> > +
> > +        line_cfg = _ext.LineConfig()
> > +
> > +        for lines, settings in config.items():
> > +            offsets = list()
> > +            name_map = dict()
> > +            offset_map = dict()
> > +
> > +            if isinstance(lines, int) or isinstance(lines, str):
> > +                lines = (lines,)
> > +
> > +            for line in lines:
> > +                offset = self.map_line(line)
> > +                offsets.append(offset)
> > +                if isinstance(line, str):
> > +                    name_map[line] = offset
> > +                    offset_map[offset] = line
>
> Use list comprehensions instead of the for loop?
>

Not sure it's worth it - we would essentially iterate thrice to fill
in every container here?

> > +
> > +            if settings is None:
> > +                settings = LineSettings()
> > +
>                settings = settings or LineSettings()
>
> Would use that directly below if it would fit in the line.
>
> Rename _line_settings_to_ext_line_settings to _line_settings_to_ext,
> as the second line_settings is redundant?  Then it might fit.
>

Done

> > +            line_cfg.add_line_settings(
> > +                offsets, _line_settings_to_ext_line_settings(settings)
> > +            )
> > +
> > +        req_internal = self._chip.request_lines(line_cfg, consumer, event_buffer_size)
> > +        request = LineRequest(req_internal)
> > +
> > +        request._offsets = req_internal.offsets
> > +        request._name_map = name_map
> > +        request._offset_map = offset_map
> > +
> > +        request._lines = list()
> > +        for off in request.offsets:
> > +            request._lines.append(offset_map[off] if off in offset_map else off)
>
> Again, prefer to use list comprehensions to translate lists like this.
>

Done

<snip>

> > +
> > +static int
> > +chip_init(chip_object *self, PyObject *args, PyObject *Py_UNUSED(ignored))
> > +{
> > +     char *path;
> > +     int ret;
> > +
> > +     ret = PyArg_ParseTuple(args, "s", &path);
> > +     if (!ret)
> > +             return -1;
> > +
> > +     Py_BEGIN_ALLOW_THREADS;
> > +     self->chip = gpiod_chip_open(path);
> > +     Py_END_ALLOW_THREADS;
>
> Is the assignment to self->chip while not holding the GIL (between the
> BEGIN and END) safe?
> To be sure, assign to a local then assign that to self->chip after the END.
>

Good point.

> Similarly other BEGIN/END blocks should only access locals.
> And even then you probably need locks to prevent threads trampling each
> other, but the locks need the GIL, IIUC.
>
> So I suspect your chip._check_closed()s need to be more like
>   with chip._lock()
>

I agree that the C code should not crash - so it makes sense to not
assign self->chip with GIL released. But for thread-safety in general
- I don't think the module should care about it. Just like C and C++
libs leave it to the user. The only thing we should care about is not
keeping any global state that could cause problems in multithreaded
apps.

> Similarly the other object validity checks.
>
> > +     if (!self->chip) {
> > +             Py_gpiod_SetErrFromErrno();
> > +             return -1;
> > +     }
> > +
> > +     return 0;
> > +}
> > +
> > +static void chip_finalize(chip_object *self)
> > +{
> > +     if (self->chip)
> > +             PyObject_CallMethod((PyObject *)self, "close", "");
> > +}
> > +
> > +static PyObject *chip_path(chip_object *self, void *Py_UNUSED(ignored))
> > +{
> > +     return PyUnicode_FromString(gpiod_chip_get_path(self->chip));
> > +}
> > +
> > +static PyObject *chip_fd(chip_object *self, void *Py_UNUSED(ignored))
> > +{
> > +     return PyLong_FromLong(gpiod_chip_get_fd(self->chip));
> > +}
> > +
> > +static PyGetSetDef chip_getset[] = {
> > +     {
> > +             .name = "path",
> > +             .get = (getter)chip_path,
> > +     },
> > +     {
> > +             .name = "fd",
> > +             .get = (getter)chip_fd,
> > +     },
> > +     { }
> > +};
> > +
> > +static PyObject *chip_close(chip_object *self, PyObject *Py_UNUSED(ignored))
> > +{
> > +     Py_BEGIN_ALLOW_THREADS;
> > +     gpiod_chip_close(self->chip);
> > +     Py_END_ALLOW_THREADS;
> > +     self->chip = NULL;
> > +
> > +     Py_RETURN_NONE;
> > +}
> > +
> > +static PyObject *chip_get_info(chip_object *self, PyObject *Py_UNUSED(ignored))
> > +{
> > +     struct gpiod_chip_info *info;
> > +     PyObject *type, *ret;
> > +
> > +     type = Py_gpiod_GetGlobalType("ChipInfo");
> > +     if (!type)
> > +             return NULL;
> > +
> > +     info = gpiod_chip_get_info(self->chip);
> > +     if (!info)
> > +             return PyErr_SetFromErrno(PyExc_OSError);
> > +
> > +      ret = PyObject_CallFunction(type, "ssI",
> > +                                  gpiod_chip_info_get_name(info),
> > +                                  gpiod_chip_info_get_label(info),
> > +                                  gpiod_chip_info_get_num_lines(info));
> > +      gpiod_chip_info_free(info);
> > +      return ret;
> > +}
> > +
> > +static PyObject *make_line_info(struct gpiod_line_info *info)
> > +{
> > +     PyObject *type;
> > +
> > +     type = Py_gpiod_GetGlobalType("LineInfo");
> > +     if (!type)
> > +             return NULL;
> > +
> > +     return PyObject_CallFunction(type, "IsOsiOiiiiOi",
>                                                    ^
>                                        "IsOsiOiiiiOk"
>
> debounce_period_us is an unsigned long, not int.
>

Good catch.

<snip>

> > +
> > +static int set_int_prop(struct gpiod_line_settings *settings, int val,
> > +                     int (*func)(struct gpiod_line_settings *, int))
> > +{
> > +     int ret;
> > +
> > +     ret = func(settings, val);
> > +     if (ret) {
> > +             Py_gpiod_SetErrFromErrno();
> > +             return -1;
> > +     }
> > +
>
> Since the ret value is never used, skip the ret and just test the func
> return directly?
>
> Similarly elsewhere.
>

This is just for consistency. It looks better and clearer IMO.

> > +     return 0;
> > +}
> > +
> > +static int
> > +line_settings_init(line_settings_object *self, PyObject *args, PyObject *kwargs)
> > +{
> > +     static char *kwlist[] = {
> > +             "direction",
> > +             "edge_detection",
> > +             "bias",
> > +             "drive",
> > +             "active_low",
> > +             "debounce_period",
> > +             "event_clock",
> > +             "output_value",
> > +             NULL
> > +     };
> > +
> > +     int direction, edge, bias, drive, active_low, event_clock, output_value,
> > +         ret;
> > +     unsigned long debounce_period;
> > +
> > +     ret = PyArg_ParseTupleAndKeywords(args, kwargs, "IIIIpdII", kwlist,
>                                                           ^
>                                                      IIIIpkII
>
> d is a double, k is an unsigned long
>

Done

<snip!>

> > +static PyObject *request_offsets(request_object *self, void *Py_UNUSED(ignored))
> > +{
> > +     PyObject *lines, *line;
> > +     unsigned int *offsets;
> > +     size_t num_lines, i;
> > +     int ret;
> > +
> > +     num_lines = gpiod_line_request_get_num_lines(self->request);
> > +
> > +     offsets = PyMem_Calloc(num_lines, sizeof(unsigned int));
> > +     if (!offsets)
> > +             return PyErr_NoMemory();
> > +
> > +     gpiod_line_request_get_offsets(self->request, offsets);
> > +
> > +     lines = PyList_New(num_lines);
> > +     if (!lines) {
> > +             PyMem_Free(offsets);
> > +             return NULL;
> > +     }
> > +
> > +     for (i = 0; i < num_lines; i++) {
> > +             line = PyLong_FromUnsignedLong(offsets[i]);
> > +             if (!lines) {
>                  ^
> Should be line.
>

Thanks!

<snip>

>
> A new buffer every time?
> Maybe cache it in the request_object for next time?
>

I was thinking about it and then forgot, thanks!

> > +     Py_BEGIN_ALLOW_THREADS;
> > +     ret = gpiod_line_request_read_edge_event(self->request,
> > +                                              buffer, max_events);
> > +     Py_END_ALLOW_THREADS;
> > +     if (ret < 0) {
> > +             gpiod_edge_event_buffer_free(buffer);
> > +             return NULL;
> > +     }
> > +
> > +     num_events = ret;
> > +
> > +     events = PyList_New(num_events);
> > +     if (!events) {
> > +             gpiod_edge_event_buffer_free(buffer);
> > +             return NULL;
> > +     }
> > +
> > +     for (i = 0; i < num_events; i++) {
> > +             event = gpiod_edge_event_buffer_get_event(buffer, i);
> > +             if (!event) {
> > +                     gpiod_edge_event_buffer_free(buffer);
> > +                     Py_DECREF(events);
> > +                     return NULL;
> > +             }
> > +
> > +             event_obj = PyObject_CallFunction(type, "iKiii",
> > +                             gpiod_edge_event_get_event_type(event),
> > +                             gpiod_edge_event_get_timestamp_ns(event),
> > +                             gpiod_edge_event_get_line_offset(event),
> > +                             gpiod_edge_event_get_global_seqno(event),
> > +                             gpiod_edge_event_get_line_seqno(event));
> > +             if (!event_obj) {
> > +                     gpiod_edge_event_buffer_free(buffer);
> > +                     Py_DECREF(events);
> > +                     return NULL;
> > +             }
> > +
> > +             ret = PyList_SetItem(events, i, event_obj);
> > +             if (ret) {
> > +                     gpiod_edge_event_buffer_free(buffer);
> > +                     Py_DECREF(event_obj);
> > +                     Py_DECREF(events);
> > +                     return NULL;
> > +             }
> > +     }
> > +
> > +     gpiod_edge_event_buffer_free(buffer);
> > +
> > +     return events;
> > +}
> > +
> > +static PyMethodDef request_methods[] = {
> > +     {
> > +             .ml_name = "release",
> > +             .ml_meth = (PyCFunction)request_release,
> > +             .ml_flags = METH_NOARGS,
> > +     },
> > +     {
> > +             .ml_name = "get_values",
> > +             .ml_meth = (PyCFunction)request_get_values,
> > +             .ml_flags = METH_VARARGS,
> > +     },
> > +     {
> > +             .ml_name = "set_values",
> > +             .ml_meth = (PyCFunction)request_set_values,
> > +             .ml_flags = METH_VARARGS,
> > +     },
> > +     {
> > +             .ml_name = "reconfigure_lines",
> > +             .ml_meth = (PyCFunction)request_reconfigure_lines,
> > +             .ml_flags = METH_VARARGS,
> > +     },
> > +     {
> > +             .ml_name = "read_edge_event",
> > +             .ml_meth = (PyCFunction)request_read_edge_event,
> > +             .ml_flags = METH_VARARGS,
> > +     },
> > +     { }
> > +};
> > +
> > +PyTypeObject request_type = {
> > +     PyVarObject_HEAD_INIT(NULL, 0)
> > +     .tp_name = "gpiod._ext.Request",
> > +     .tp_basicsize = sizeof(request_object),
> > +     .tp_flags = Py_TPFLAGS_DEFAULT,
> > +     .tp_new = PyType_GenericNew,
> > +     .tp_init = (initproc)request_init,
> > +     .tp_finalize = (destructor)request_finalize,
> > +     .tp_dealloc = (destructor)Py_gpiod_dealloc,
> > +     .tp_getset = request_getset,
> > +     .tp_methods = request_methods,
> > +};
> > +
> > +PyObject *Py_gpiod_MakeRequestObject(struct gpiod_line_request *request)
> > +{
> > +     request_object *req_obj;
> > +     unsigned int *offsets;
> > +     size_t num_lines;
> > +     int *values;
> > +
> > +     num_lines = gpiod_line_request_get_num_lines(request);
> > +
> > +     req_obj = PyObject_New(request_object, &request_type);
> > +     if (!req_obj)
> > +             return NULL;
> > +
> > +     offsets = PyMem_Calloc(num_lines, sizeof(unsigned int));
> > +     if (!offsets) {
> > +             Py_DECREF(req_obj);
> > +             return NULL;
> > +     }
> > +
> > +     values = PyMem_Calloc(num_lines, sizeof(int));
> > +     if (!values) {
> > +             PyMem_Free(offsets);
> > +             Py_DECREF(req_obj);
> > +             return NULL;
> > +     }
> > +
> > +     req_obj->request = request;
> > +     req_obj->offsets = offsets;
> > +     req_obj->values = values;
> > +     req_obj->num_lines = num_lines;
> > +
> > +     return (PyObject *)req_obj;
> > +}
> > diff --git a/bindings/python/gpiod/line_info.py b/bindings/python/gpiod/line_info.py
> > new file mode 100644
> > index 0000000..9a6c9bf
> > --- /dev/null
> > +++ b/bindings/python/gpiod/line_info.py
> > @@ -0,0 +1,73 @@
> > +# SPDX-License-Identifier: LGPL-2.1-or-later
> > +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
> > +
> > +from . import _ext
> > +from dataclasses import dataclass
> > +from datetime import timedelta
> > +from gpiod.line import Direction, Bias, Drive, Edge, Clock
> > +
> > +
> > +@dataclass(frozen=True, init=False, repr=False)
> > +class LineInfo:
> > +    """
> > +    Snapshot of a line's status.
> > +    """
> > +
> > +    offset: int
> > +    name: str
> > +    used: bool
> > +    consumer: str
> > +    direction: Direction
> > +    active_low: bool
> > +    bias: Bias
> > +    drive: Drive
> > +    edge_detection: Edge
> > +    event_clock: Clock
> > +    debounced: bool
>
> I've probably asked this before, but does debounced differ from
> not debounce_period?
>

Nope, just consistency with the rest of tree. I'll think whether it's
really useful to not have it.

<snip>

> > +    def __init__(self, req: _ext.Request):
> > +        """
> > +        DON'T USE
> > +
> > +        LineRequest objects can only be instantiated by a Chip parent. This is
> > +        not part of stable API.
> > +        """
> > +        self._req = req
> > +
> > +    def __bool__(self) -> bool:
> > +        """
> > +        Boolean conversion for GPIO line requests.
> > +
> > +        Returns:
> > +          True if the request is live and False if it's been released.
> > +        """
> > +        return True if self._req else False
>
>            return self._req

But this will not convert to bool. The typing hint is just it - a
hint. I would be returning the internal request object.

>
> > +
> > +    def __enter__(self):
> > +        """
> > +        Controlled execution enter callback.
> > +        """
> > +        self._check_released()
> > +        return self
> > +
> > +    def __exit__(self, exc_type, exc_value, traceback):
> > +        """
> > +        Controlled execution exit callback.
> > +        """
> > +        self.release()
> > +
> > +    def _check_released(self) -> None:
> > +        if not self._req:
> > +            raise RequestReleasedError()
> > +
> > +    def release(self) -> None:
> > +        """
> > +        Release this request and free all associated resources. The object must
> > +        not be used after a call to this method.
> > +        """
> > +        self._check_released()
> > +        self._req.release()
> > +        self._req = None
> > +
> > +    def get_value(self, line: Union[int, str]) -> Value:
> > +        """
> > +        Get a single GPIO line value.
> > +
> > +        Args:
> > +          line:
> > +            Offset or name of the line to get value for.
> > +
> > +        Returns:
> > +          Logical value of the line.
> > +        """
> > +        return self.get_values([line])[0]
> > +
> > +    def get_values(
> > +        self, lines: Optional[Iterable[Union[int, str]]] = None
> > +    ) -> list[Value]:
> > +        """
> > +        Get values of a set of GPIO lines.
> > +
> > +        Args:
> > +          lines:
> > +            List of names or offsets of GPIO lines to get values for. Can be
> > +            None in which case all requested lines will be read.
> > +
> > +        Returns:
> > +          List of logical line values.
> > +        """
> > +        self._check_released()
> > +
> > +        if lines is None:
> > +            lines = self._lines
>
> What if lines is empty?  Is that equivalent to None, or an error?
> If the former...
>
>            lines = lines or self._lines
>
> either here or where used below (if that changed to a list comprehension)
>
> > +
> > +        offsets = [None] * len(lines)
> > +
> > +        for i, line in enumerate(lines):
> > +            if isinstance(line, str):
> > +                if line not in self._name_map:
> > +                    raise ValueError("unknown line name: {}".format(line))
> > +
> > +                offsets[i] = self._name_map[line]
> > +            else:
> > +                offsets[i] = line
> > +
>
> I would do this with a list comprehension and a helper function to do
> the id to offset mapping.  i.e.
>

Used list and dict comprehensions where possible.

<snip>

>
> Ok, the API is much nicer than the previous versions, so that is all
> good, given some clarification on the config semantics.
>
> Other than the couple of PyArg_ParseTupleAndKeywords issues,
> the other few minor bugs, and my obvious preference for using list
> comprehensions to build lists, the big sticking point for me is thread
> safety.
> I'm not convinced your current approach is thread safe, so convince me,
> either by proving me wrong or providing another solution.
> Hopefully I'm wrong.
>

It's not thread-safe. Why would it be? What if you use gpiod and a
module_x which also uses gpiod internally? Then you lock some mutex in
gpiod and module_x deadlocks with the same mutex? It should be left to
the user to handle threads.

Thanks a lot for the thorough review. v4 will be going out shortly!

Bart

> Cheers,
> Kent.

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [libgpiod v2][PATCH v3 4/4] bindings: python: implement python bindings for libgpiod v2
  2022-10-26 12:32     ` Bartosz Golaszewski
@ 2022-10-26 12:56       ` Kent Gibson
  0 siblings, 0 replies; 30+ messages in thread
From: Kent Gibson @ 2022-10-26 12:56 UTC (permalink / raw)
  To: Bartosz Golaszewski
  Cc: Linus Walleij, Andy Shevchenko, Viresh Kumar, linux-gpio

On Wed, Oct 26, 2022 at 02:32:35PM +0200, Bartosz Golaszewski wrote:
> On Thu, Oct 13, 2022 at 5:10 AM Kent Gibson <warthog618@gmail.com> wrote:
> >
> > On Fri, Oct 07, 2022 at 04:55:21PM +0200, Bartosz Golaszewski wrote:
> 
> I agree that the C code should not crash - so it makes sense to not
> assign self->chip with GIL released. But for thread-safety in general
> - I don't think the module should care about it. Just like C and C++
> libs leave it to the user. The only thing we should care about is not
> keeping any global state that could cause problems in multithreaded
> apps.
> 

Yeah, agreed - no locking or thread safety guarantees - we don't want to
add the overhead of locking, and the user can provide their own locking
wrapper if they need it, probably at request scope.
As long we we don't go messing with Python objects outside the GIL, or
sharing state between requests (and we don't) then we should be fine.

Sorry for the confusion there on my part - I think I was flipping back
and forth between Python and Rust and carried some of my Rust Sync
concerns over to the Python.

Cheers,
Kent.

^ permalink raw reply	[flat|nested] 30+ messages in thread

end of thread, other threads:[~2022-10-26 12:56 UTC | newest]

Thread overview: 30+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2022-10-07 14:55 [libgpiod v2][PATCH v3 0/4] bindings: implement python bindings for libgpiod v2 Bartosz Golaszewski
2022-10-07 14:55 ` [libgpiod v2][PATCH v3 1/4] bindings: python: remove old version Bartosz Golaszewski
2022-10-07 14:55 ` [libgpiod v2][PATCH v3 2/4] bindings: python: add examples Bartosz Golaszewski
2022-10-13  3:09   ` Kent Gibson
2022-10-17 12:00     ` Bartosz Golaszewski
2022-10-17 12:11       ` Kent Gibson
2022-10-17 13:49         ` Andy Shevchenko
2022-10-17 14:07           ` Kent Gibson
2022-10-17 14:19             ` Andy Shevchenko
2022-10-17 15:53               ` Bartosz Golaszewski
2022-10-17 16:09                 ` Kent Gibson
2022-10-17 16:20                   ` Kent Gibson
2022-10-17 16:55                     ` Andy Shevchenko
2022-10-17 16:57                       ` Andy Shevchenko
2022-10-17 17:26                         ` Bartosz Golaszewski
2022-10-17 16:24                 ` Andy Shevchenko
2022-10-17 16:39                   ` Kent Gibson
2022-10-07 14:55 ` [libgpiod v2][PATCH v3 3/4] bindings: python: add tests Bartosz Golaszewski
2022-10-13  3:09   ` Kent Gibson
2022-10-07 14:55 ` [libgpiod v2][PATCH v3 4/4] bindings: python: implement python bindings for libgpiod v2 Bartosz Golaszewski
2022-10-07 15:26   ` Andy Shevchenko
2022-10-07 18:19     ` Bartosz Golaszewski
2022-10-13  3:10   ` Kent Gibson
2022-10-13 11:12     ` Kent Gibson
2022-10-26 12:32     ` Bartosz Golaszewski
2022-10-26 12:56       ` Kent Gibson
2022-10-12 12:34 ` [libgpiod v2][PATCH v3 0/4] bindings: " Bartosz Golaszewski
2022-10-12 12:41   ` Kent Gibson
2022-10-12 12:51     ` Bartosz Golaszewski
2022-10-12 13:03       ` Kent Gibson

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).