From: Kent Gibson <warthog618@gmail.com>
To: Bartosz Golaszewski <brgl@bgdev.pl>
Cc: Linus Walleij <linus.walleij@linaro.org>,
Andy Shevchenko <andriy.shevchenko@linux.intel.com>,
Darrien <darrien@freenet.de>,
Viresh Kumar <viresh.kumar@linaro.org>, Jiri Benc <jbenc@upir.cz>,
Joel Savitz <joelsavitz@gmail.com>,
linux-gpio@vger.kernel.org
Subject: Re: [libgpiod v2][PATCH v2 5/5] bindings: python: add the implementation for v2 API
Date: Tue, 5 Jul 2022 10:09:37 +0800 [thread overview]
Message-ID: <20220705020937.GB6652@sol> (raw)
In-Reply-To: <20220628084226.472035-6-brgl@bgdev.pl>
On Tue, Jun 28, 2022 at 10:42:26AM +0200, Bartosz Golaszewski wrote:
> This is the implementation of the new python API for libgpiod v2.
>
> Signed-off-by: Bartosz Golaszewski <brgl@bgdev.pl>
> ---
> bindings/python/.gitignore | 1 +
> bindings/python/Makefile.am | 40 +
> bindings/python/chip-info.c | 126 +++
> bindings/python/chip.c | 606 ++++++++++++
> bindings/python/edge-event-buffer.c | 330 +++++++
> bindings/python/edge-event.c | 191 ++++
> bindings/python/exception.c | 182 ++++
> bindings/python/info-event.c | 175 ++++
> bindings/python/line-config.c | 1373 +++++++++++++++++++++++++++
> bindings/python/line-info.c | 286 ++++++
> bindings/python/line-request.c | 803 ++++++++++++++++
> bindings/python/line.c | 239 +++++
> bindings/python/module.c | 557 +++++++++++
> bindings/python/module.h | 58 ++
> bindings/python/request-config.c | 320 +++++++
> configure.ac | 3 +-
> 16 files changed, 5289 insertions(+), 1 deletion(-)
> create mode 100644 bindings/python/.gitignore
> create mode 100644 bindings/python/Makefile.am
> create mode 100644 bindings/python/chip-info.c
> create mode 100644 bindings/python/chip.c
> create mode 100644 bindings/python/edge-event-buffer.c
> create mode 100644 bindings/python/edge-event.c
> create mode 100644 bindings/python/exception.c
> create mode 100644 bindings/python/info-event.c
> create mode 100644 bindings/python/line-config.c
> create mode 100644 bindings/python/line-info.c
> create mode 100644 bindings/python/line-request.c
> create mode 100644 bindings/python/line.c
> create mode 100644 bindings/python/module.c
> create mode 100644 bindings/python/module.h
> create mode 100644 bindings/python/request-config.c
>
> diff --git a/bindings/python/.gitignore b/bindings/python/.gitignore
> new file mode 100644
> index 0000000..bee8a64
> --- /dev/null
> +++ b/bindings/python/.gitignore
> @@ -0,0 +1 @@
> +__pycache__
> diff --git a/bindings/python/Makefile.am b/bindings/python/Makefile.am
> new file mode 100644
> index 0000000..3f7ee5f
> --- /dev/null
> +++ b/bindings/python/Makefile.am
> @@ -0,0 +1,40 @@
> +# SPDX-License-Identifier: GPL-2.0-or-later
> +# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
> +
It's 2022?
> +pyexec_LTLIBRARIES = gpiod.la
> +
> +gpiod_la_SOURCES = \
> + chip.c \
> + chip-info.c \
> + edge-event.c \
> + edge-event-buffer.c \
> + exception.c \
> + info-event.c \
> + line.c \
> + line-config.c \
> + line-info.c \
> + line-request.c \
> + module.c \
> + module.h \
> + request-config.c
> +
> +gpiod_la_CFLAGS = -I$(top_srcdir)/include/
> +gpiod_la_CFLAGS += -Wall -Wextra -g -std=gnu89 $(PYTHON_CPPFLAGS)
> +gpiod_la_CFLAGS += -include $(top_builddir)/config.h
> +gpiod_la_LDFLAGS = -module -avoid-version
> +gpiod_la_LIBADD = $(top_builddir)/lib/libgpiod.la $(PYTHON_LIBS)
> +gpiod_la_LIBADD += $(top_builddir)/bindings/python/enum/libpycenum.la
> +
> +SUBDIRS = enum .
> +
> +if WITH_TESTS
> +
> +SUBDIRS += tests
> +
> +endif
> +
> +if WITH_EXAMPLES
> +
> +SUBDIRS += examples
> +
> +endif
> diff --git a/bindings/python/chip-info.c b/bindings/python/chip-info.c
> new file mode 100644
> index 0000000..e48cf74
> --- /dev/null
> +++ b/bindings/python/chip-info.c
> @@ -0,0 +1,126 @@
> +// SPDX-License-Identifier: LGPL-2.1-or-later
> +// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
> +
> +#include "module.h"
> +
> +typedef struct {
> + PyObject_HEAD;
> + struct gpiod_chip_info *info;
> +} chip_info_object;
> +
> +static int chip_info_init(PyObject *Py_UNUSED(self),
> + PyObject *Py_UNUSED(ignored0),
> + PyObject *Py_UNUSED(ignored1))
> +{
> + PyErr_SetString(PyExc_TypeError,
> + "cannot create 'gpiod.ChipInfo' instances");
> + return -1;
> +}
> +
As you may've noticed, I tend to make use of the 100 character wrap
limit these days, so wrapping at 80 feels premature.
> +static void chip_info_finalize(chip_info_object *self)
> +{
> + if (self->info)
> + gpiod_chip_info_free(self->info);
> +}
> +
> +PyDoc_STRVAR(chip_info_name_doc,
> +"Name of the chip as represented in the kernel.");
> +
> +static PyObject *chip_info_name(chip_info_object *self,
> + void *Py_UNUSED(ignored))
> +{
> + return PyUnicode_FromString(gpiod_chip_info_get_name(self->info));
> +}
> +
> +PyDoc_STRVAR(chip_info_label_doc,
> +"Label of the chip as represented in the kernel.");
> +
> +static PyObject *chip_info_label(chip_info_object *self,
> + void *Py_UNUSED(ignored))
> +{
> + return PyUnicode_FromString(gpiod_chip_info_get_label(self->info));
> +}
> +
> +PyDoc_STRVAR(chip_info_num_lines_doc,
> +"Number of GPIO lines exposed by the chip.");
> +
> +static PyObject *chip_info_num_lines(chip_info_object *self,
> + void *Py_UNUSED(ignored))
> +{
> + return PyLong_FromUnsignedLong(
> + gpiod_chip_info_get_num_lines(self->info));
> +}
> +
> +static PyGetSetDef chip_info_getset[] = {
> + {
> + .name = "name",
> + .get = (getter)chip_info_name,
> + .doc = chip_info_name_doc,
> + },
> + {
> + .name = "label",
> + .get = (getter)chip_info_label,
> + .doc = chip_info_label_doc,
> + },
> + {
> + .name = "num_lines",
> + .get = (getter)chip_info_num_lines,
> + .doc = chip_info_num_lines_doc
> + },
> + { }
> +};
> +
> +static PyObject *chip_info_str(PyObject *self)
> +{
> + PyObject *name, *label, *num_lines, *str = NULL;
> +
> + name = PyObject_GetAttrString(self, "name");
> + label = PyObject_GetAttrString(self, "label");
> + num_lines = PyObject_GetAttrString(self, "num_lines");
> + if (!name || !label || !num_lines)
> + goto out;
> +
> + str = PyUnicode_FromFormat("<gpiod.ChipInfo name=\"%S\" label=\"%S\" num_lines=%S>",
> + name, label, num_lines);
> +
> +out:
> + Py_XDECREF(name);
> + Py_XDECREF(label);
> + Py_XDECREF(num_lines);
> + return str;
> +}
> +
> +PyDoc_STRVAR(chip_info_type_doc,
> +"Chip info object contains an immutable snapshot of a chip's status.");
Either "ChipInfo" or "Immutable object containing..." as you use
elsewhere (I'd go with the latter for consistency).
[snip]
> +
> +PyDoc_STRVAR(chip_wait_info_event_doc,
> +"Wait for line status change events on any of the watched lines on the chip.\n"
> +"\n"
> +"Args:\n"
> +" timeout:\n"
> +" Wait time limit stored represented as a datetime.timedelta object.\n"
> +"\n"
As discussed in earlier mails, consider accepting int nanoseconds and/or
float secs as well. Forcing the user to build a timedelta is a PITA.
Same applies for all time periods.
[snip]
> +PyDoc_STRVAR(chip_get_line_offset_from_name_doc,
> +"Map a line's name to its offset within the chip.\n"
> +"\n"
> +"Args:\n"
> +" name:\n"
> +" Name of the GPIO line to map.\n"
> +"\n"
> +"Returns:\n"
> +" Line offset corresponding with the name or None if a line with given name\n"
> +" is not exposed by this chip.");
> +
It should throw if the name search fails.
If name is already an int then just return the int.
(to allow the method to be used as a mapping function on a mixed
list.) Though ironically the name isn't the best then.
Perhaps just get_line_offset() or map_line_offset()?
[snip]
> +static PyGetSetDef edge_event_getset[] = {
> + {
> + .name = "type",
> + .get = (getter)edge_event_get_type,
> + .doc = edge_event_get_type_doc,
> + },
> + {
> + .name = "timestamp_ns",
> + .get = (getter)edge_event_timestamp_ns,
> + .doc = edge_event_timestamp_ns_doc,
> + },
> + {
> + .name = "line_offset",
> + .get = (getter)edge_event_line_offset,
> + .doc = edge_event_line_offset_doc,
> + },
> + {
> + .name = "global_seqno",
> + .get = (getter)edge_event_global_seqno,
> + .doc = edge_event_global_seqno_doc,
> + },
> + {
> + .name = "line_seqno",
> + .get = (getter)edge_event_line_seqno,
> + .doc = edge_event_line_seqno_doc,
> + },
> + { }
> +};
> +
Provide a helper to convert the timestamp_ns into a time.datetime.
(for event_clock_realtime)
[snip]
> +static const struct exception_desc exceptions[] = {
> + {
> + .name = "ChipClosedError",
> + .base = "Exception",
> + .doc = "Error raised when an already closed chip is used.",
> + },
> + {
> + .name = "RequestReleasedError",
> + .base = "Exception",
> + .doc = "Error raised when a released request is used.",
> + },
> + {
> + .name = "BadMappingError",
> + .base = "Exception",
> + .doc = "Exception thrown when the core C library returns an invalid value for any of the line properties.",
> + },
Name is too vague - a bad mapping could mean anything - including its own
name ;-).
How about "UnknownPropertyValueError"? "unknown" rather than "invalid"
as the likely cause is an updated C library.
Or even just a ValueError might work.
[snip]
> +
> +static PyGetSetDef info_event_getset[] = {
> + {
> + .name = "type",
> + .get = (getter)info_event_get_type,
> + .doc = info_event_get_type_doc,
> + },
> + {
> + .name = "timestamp_ns",
> + .get = (getter)info_event_timestamp_ns,
> + .doc = info_event_timestamp_ns_doc,
> + },
> + {
> + .name = "line_info",
> + .get = (getter)info_event_line_info,
> + .doc = info_event_line_info_doc,
> + },
> + { }
> +};
> +
Provide a helper to convert timestamp_ns to time.datetime.
This one is a bit trickier as the kernel only ever provides monotonic
clock, so need to perform the monotonic -> realtime conversion.
(for reference my proposed gpiowatch tool does this)
[snip]
> +PyDoc_STRVAR(line_config_set_props_default_doc,
> +"Set the defaults for properties.\n"
> +"\n"
> +"Args:\n"
> +" direction:\n"
> +" Default direction.\n"
> +" edge_detection:\n"
> +" Default edge detection.\n"
> +" bias:\n"
> +" Default bias.\n"
> +" drive:\n"
> +" Default drive.\n"
> +" active_low:\n"
> +" Default active-low setting.\n"
> +" debounce_period:\n"
> +" Default debounce period.\n"
> +" event_clock:\n"
> +" Default event clock.\n"
> +" output_value:\n"
> +" Default output value.");
> +
How about merging the _default and _offset forms by adding an offsets
kwarg?
offsets=None (or unspecified) -> default
offsets=int -> offset
offsets=iterable -> offsets
Off on a bit of a tangent... why should the end user care about
defaults and overrides?
For a higher level abstraction I'd prefer to see the whole "default"
concept disappear in favour of the config for each line. That would
remove a lot of the complexity from the LineConfig interface.
Though it would add complexity to the binding internals.
[snip]
> +PyDoc_STRVAR(line_config_get_props_default_doc,
> +"Get default values for a set of line properties.\n"
> +"\n"
> +"Args:\n"
> +" properties:\n"
> +" List of properties (gpiod.LineConfig.Property) for which to get default\n"
> +" values.\n"
> +"\n"
> +"Returns:\n"
> +" List of default values for properties specified in the argument list and\n"
> +" in the same order");
> +
As per the set, consider merging the _default and _offset forms by
adding an offset kwarg.
[snip]
> +PyDoc_STRVAR(line_info_type_doc,
> +"Line info object contains an immutable snapshot of a line's status.");
> +
Either "LineInfo" or "Immutable object containing..." as you use
elsewhere (I'd go with the latter for consistency).
[snip]
> + } else {
> + for (i = 0; i < num_values; i++) {
> + offset = PyList_GetItem(offsets_obj, i);
> + if (!offset) {
> + PyMem_Free(values);
> + PyMem_Free(offsets);
> + return NULL;
> + }
> +
> + offsets[i] = Py_gpiod_PyLongAsUnsignedInt(offset);
> + if (PyErr_Occurred()){
^ missing whitespace.
> + PyMem_Free(values);
> + PyMem_Free(offsets);
> + return NULL;
> + }
> + }
> + }
> +
> + Py_BEGIN_ALLOW_THREADS;
> + ret = gpiod_line_request_get_values_subset(self->request, num_values,
> + offsets, values);
[snip]
> +static const PyCEnum_EnumDef line_enums[] = {
> + {
> + .name = "Value",
> + .values = value_enum_vals
> + },
> + {
> + .name = "Direction",
> + .values = direction_enum_vals
> + },
> + {
> + .name = "Bias",
> + .values = bias_enum_vals
> + },
> + {
> + .name = "Drive",
> + .values = drive_enum_vals
> + },
> + {
> + .name = "Edge",
> + .values = edge_enum_vals
> + },
> + {
> + .name = "Clock",
> + .values = event_clock_enum_vals
> + },
> + { }
> +};
> +
Clock -> EventClock here and elsewhere
[snip]
> +static PyObject *
> +make_line_cfg_kwargs(PyObject *direction, PyObject *edge_detection,
> + PyObject *bias, PyObject *drive, PyObject *active_low,
> + PyObject *debounce_period, PyObject *event_clock,
> + PyObject *output_value, PyObject *output_values)
> +{
> + static const char *const keys[] = {
> + "direction",
> + "edge_detection",
> + "bias",
> + "drive",
> + "active_low",
> + "debounce_period",
> + "event_clock",
> + "output_value",
> + "output_values",
> + };
> +
> + PyObject *kwargs, *vals[9];
> + int ret, i;
> +
> + vals[0] = direction;
> + vals[1] = edge_detection;
> + vals[2] = bias;
> + vals[3] = drive;
> + vals[4] = active_low;
> + vals[5] = debounce_period;
> + vals[6] = event_clock;
> + vals[7] = output_value;
> + vals[8] = output_values;
> +
> + if (memcmp(vals, "\0\0\0\0\0\0\0\0\0", 9) == 0)
> + return NULL;
> +
> + kwargs = PyDict_New();
> + if (!kwargs)
> + return NULL;
> +
> + for (i = 0; i < 9; i ++) {
^ extra whitespace
> + if (!vals[i])
> + continue;
> +
> + ret = PyDict_SetItemString(kwargs, keys[i], vals[i]);
> + if (ret) {
> + Py_DECREF(kwargs);
> + return NULL;
> + }
> + }
> +
> + return kwargs;
> +}
> +
[snip]
> + res = PyObject_Call(method, args, line_cfg_kwargs);
> + Py_DECREF(args);
> + Py_DECREF(method);
> + if (!Py_IsNone(res)) {
> + Py_DECREF(res);
> + return NULL;
> + }
> +
As mentioned in a separate mail, Py_IsNone() requires Python 3.10, while
the configure.ac allows 3.9.
> + Py_DECREF(res);
> +
> + return line_cfg;
> +}
> +
> +static PyObject *
> +module_request_lines(PyObject *self, PyObject *args, PyObject *kwargs)
> +{
> + static char *kwlist[] = {
> + "path",
> + "req_cfg",
> + "line_cfg",
> + "lines",
> + "direction",
> + "edge_detection",
> + "bias",
> + "drive",
> + "active_low",
> + "debounce_period",
> + "event_clock",
> + "output_value",
> + "output_values",
> + NULL
> + };
> +
My suggestion to provide a lines parameter here was actually a poor one,
given the LineConfig only deals with offsets - which is totally reasonable
as supporting line names in LineConfig would be complicated.
I would prefer the two to be consistent, and so use offsets.
If get_line_offset_from_name() was better for mapping (in the Python
sense) then you could just use a list comprehension to convert a list of
names/offsets into a list of offsets to pass in here.
So I would change lines to offsets here and make
get_line_offset_from_name() more useful for mapping (more on that where
it is defined).
> + PyObject *path, *req_cfg = NULL, *line_cfg = NULL, *lines = NULL,
> + *direction = NULL, *edge_detection = NULL, *bias = NULL,
> + *drive = NULL, *active_low = NULL, *debounce_period = NULL,
> + *event_clock = NULL, *output_value = NULL,
> + *output_values = NULL, *dict, *chip, *req, *line_cfg_kwargs;
> + int ret;
> +
> + ret = PyArg_ParseTupleAndKeywords(args, kwargs, "O|OO$OOOOOOOOOO",
> + kwlist, &path, &req_cfg, &line_cfg,
> + &lines, &direction, &edge_detection,
> + &bias, &drive, &active_low,
> + &debounce_period, &event_clock,
> + &output_value, &output_values);
> + if (!ret)
> + return NULL;
> +
> + dict = PyModule_GetDict(self);
> + if (!dict)
> + return NULL;
> +
> + chip = make_chip(dict, path);
> + if (!chip)
> + return NULL;
> +
> + req_cfg = make_req_cfg(dict, chip, req_cfg, lines);
> + if (!req_cfg) {
> + close_chip(chip);
> + return NULL;
> + }
> +
What if lines is None or empty?
A failed name -> offset mapping in make_req_cfg() and set_lines() results
in a returned NULL here? Shouldn't it provide a meaningful error or throw
an exception?
Change to passing in offsets and this problem goes away.
[snip]
> --- a/configure.ac
> +++ b/configure.ac
> @@ -198,7 +198,7 @@ AM_CONDITIONAL([WITH_BINDINGS_PYTHON], [test "x$with_bindings_python" = xtrue])
>
> if test "x$with_bindings_python" = xtrue
> then
> - AM_PATH_PYTHON([3.0], [],
> + AM_PATH_PYTHON([3.9], [],
Given this requirement, make sure it compiles with Python 3.9.
> [AC_MSG_ERROR([python3 not found - needed for python bindings])])
> AC_CHECK_PROG([has_python_config], [python3-config], [true], [false])
> if test "x$has_python_config" = xfalse
> @@ -243,6 +243,7 @@ AC_CONFIG_FILES([Makefile
> bindings/cxx/examples/Makefile
> bindings/cxx/tests/Makefile
> bindings/python/Makefile
> + bindings/python/enum/Makefile
> bindings/python/examples/Makefile
> bindings/python/tests/Makefile
> man/Makefile])
> --
> 2.34.1
>
Nothing major really.
I would personally like to have a slightly higher level of abstraction,
but given you are going for a minimalist wrapper around libgpiod, it
seems totally reasonable.
Cheers,
Kent.
next prev parent reply other threads:[~2022-07-05 2:09 UTC|newest]
Thread overview: 35+ messages / expand[flat|nested] mbox.gz Atom feed top
2022-06-28 8:42 [libgpiod v2][PATCH v2 0/5] bindings: implement python bindings for libgpiod v2 Bartosz Golaszewski
2022-06-28 8:42 ` [libgpiod v2][PATCH v2 1/5] bindings: python: remove old version Bartosz Golaszewski
2022-06-28 8:42 ` [libgpiod v2][PATCH v2 2/5] bindings: python: enum: add a piece of common code for using python's enums from C Bartosz Golaszewski
2022-06-28 8:42 ` [libgpiod v2][PATCH v2 3/5] bindings: python: add examples for v2 API Bartosz Golaszewski
2022-06-28 8:42 ` [libgpiod v2][PATCH v2 4/5] bindings: python: add tests " Bartosz Golaszewski
2022-07-05 2:08 ` Kent Gibson
2022-07-07 10:17 ` Bartosz Golaszewski
2022-07-07 12:22 ` Kent Gibson
2022-06-28 8:42 ` [libgpiod v2][PATCH v2 5/5] bindings: python: add the implementation " Bartosz Golaszewski
2022-06-30 2:25 ` Kent Gibson
2022-06-30 6:54 ` Bartosz Golaszewski
2022-06-30 8:14 ` Kent Gibson
2022-06-30 8:38 ` Kent Gibson
2022-07-01 6:07 ` Kent Gibson
2022-07-01 7:21 ` Bartosz Golaszewski
2022-07-01 7:26 ` Kent Gibson
2022-07-01 7:29 ` Bartosz Golaszewski
2022-07-01 7:33 ` Kent Gibson
2022-07-01 8:02 ` Kent Gibson
2022-07-01 8:18 ` Bartosz Golaszewski
2022-07-01 8:32 ` Bartosz Golaszewski
2022-07-01 8:52 ` Kent Gibson
2022-07-01 9:28 ` Bartosz Golaszewski
2022-07-01 8:32 ` Kent Gibson
2022-07-05 2:09 ` Kent Gibson [this message]
2022-07-07 12:19 ` Bartosz Golaszewski
2022-07-07 13:09 ` Kent Gibson
2022-07-07 20:09 ` Bartosz Golaszewski
2022-07-08 1:38 ` Kent Gibson
2022-07-08 9:49 ` Bartosz Golaszewski
2022-07-08 10:56 ` Kent Gibson
2022-07-08 11:28 ` Bartosz Golaszewski
2022-07-08 15:26 ` Bartosz Golaszewski
2022-07-08 15:58 ` Kent Gibson
2022-06-28 8:47 ` [libgpiod v2][PATCH v2 0/5] bindings: implement python bindings for libgpiod v2 Bartosz Golaszewski
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20220705020937.GB6652@sol \
--to=warthog618@gmail.com \
--cc=andriy.shevchenko@linux.intel.com \
--cc=brgl@bgdev.pl \
--cc=darrien@freenet.de \
--cc=jbenc@upir.cz \
--cc=joelsavitz@gmail.com \
--cc=linus.walleij@linaro.org \
--cc=linux-gpio@vger.kernel.org \
--cc=viresh.kumar@linaro.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
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).