From: Vincent Fazio <vfazio@gmail.com>
To: linux-gpio@vger.kernel.org
Cc: Vincent Fazio <vfazio@gmail.com>
Subject: [libgpiod][PATCH] bindings: python: drop python 3.9 support
Date: Sun, 29 Mar 2026 13:28:32 -0500 [thread overview]
Message-ID: <20260329182832.39824-1-vfazio@gmail.com> (raw)
* Update pyproject.toml to require Python 3.10+ for runtime
* Update mypy's target version to 3.10 syntax
* Drop ruff's target version as it's inferred by the project's
requires-python value
* Remove the linter settings that ignored UP007 & UP0045
* Update type annotations to conform to active linter rules
Closes: https://github.com/brgl/libgpiod/issues/151
Signed-off-by: Vincent Fazio <vfazio@gmail.com>
---
bindings/python/gpiod/__init__.py | 9 +++---
bindings/python/gpiod/_ext.pyi | 10 +++----
bindings/python/gpiod/_internal.py | 10 +++----
bindings/python/gpiod/chip.py | 36 ++++++++++-------------
bindings/python/gpiod/line_request.py | 36 ++++++++++-------------
bindings/python/pyproject.toml | 10 ++-----
bindings/python/tests/gpiosim/chip.py | 9 +++---
bindings/python/tests/helpers.py | 8 ++---
bindings/python/tests/tests_edge_event.py | 5 ++--
bindings/python/tests/tests_info_event.py | 3 +-
configure.ac | 2 +-
11 files changed, 59 insertions(+), 79 deletions(-)
diff --git a/bindings/python/gpiod/__init__.py b/bindings/python/gpiod/__init__.py
index 854e41f..be1b6b0 100644
--- a/bindings/python/gpiod/__init__.py
+++ b/bindings/python/gpiod/__init__.py
@@ -8,7 +8,6 @@ This module wraps the native C API of libgpiod in a set of python classes.
"""
from collections.abc import Iterable
-from typing import Optional, Union
from . import (
_ext,
@@ -87,10 +86,10 @@ def is_gpiochip_device(path: str) -> bool:
def request_lines(
path: str,
- config: dict[Union[Iterable[Union[int, str]], int, str], Optional[LineSettings]],
- consumer: Optional[str] = None,
- event_buffer_size: Optional[int] = None,
- output_values: Optional[dict[Union[int, str], line.Value]] = None,
+ config: dict[Iterable[int | str] | int | str, LineSettings | None],
+ consumer: str | None = None,
+ event_buffer_size: int | None = None,
+ output_values: dict[int | str, line.Value] | None = None,
) -> LineRequest:
"""
Open a GPIO chip pointed to by 'path', request lines according to the
diff --git a/bindings/python/gpiod/_ext.pyi b/bindings/python/gpiod/_ext.pyi
index 31fd352..873c23f 100644
--- a/bindings/python/gpiod/_ext.pyi
+++ b/bindings/python/gpiod/_ext.pyi
@@ -1,8 +1,6 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# SPDX-FileCopyrightText: 2024 Vincent Fazio <vfazio@gmail.com>
-from typing import Optional
-
from .chip_info import ChipInfo
from .edge_event import EdgeEvent
from .info_event import InfoEvent
@@ -32,7 +30,7 @@ class Request:
def get_values(self, offsets: list[int], values: list[Value]) -> None: ...
def set_values(self, values: dict[int, Value]) -> None: ...
def reconfigure_lines(self, line_cfg: LineConfig) -> None: ...
- def read_edge_events(self, max_events: Optional[int]) -> list[EdgeEvent]: ...
+ def read_edge_events(self, max_events: int | None) -> list[EdgeEvent]: ...
@property
def chip_name(self) -> str: ...
@property
@@ -47,12 +45,12 @@ class Chip:
def get_info(self) -> ChipInfo: ...
def line_offset_from_id(self, id: str) -> int: ...
def get_line_info(self, offset: int, watch: bool) -> LineInfo: ...
- def get_line_name(self, offset: int) -> Optional[str]: ...
+ def get_line_name(self, offset: int) -> str | None: ...
def request_lines(
self,
line_cfg: LineConfig,
- consumer: Optional[str],
- event_buffer_size: Optional[int],
+ consumer: str | None,
+ event_buffer_size: int | None,
) -> Request: ...
def read_info_event(self) -> InfoEvent: ...
def close(self) -> None: ...
diff --git a/bindings/python/gpiod/_internal.py b/bindings/python/gpiod/_internal.py
index ee15796..b81f970 100644
--- a/bindings/python/gpiod/_internal.py
+++ b/bindings/python/gpiod/_internal.py
@@ -5,7 +5,7 @@ from __future__ import annotations
from datetime import timedelta
from select import select
-from typing import TYPE_CHECKING, Optional, Union
+from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Generator, Iterable
@@ -15,8 +15,8 @@ if TYPE_CHECKING:
__all__ = ["poll_fd", "config_iter"]
-def poll_fd(fd: int, timeout: Optional[Union[timedelta, float]] = None) -> bool:
- sec: Union[float, None]
+def poll_fd(fd: int, timeout: timedelta | float | None = None) -> bool:
+ sec: float | None
if isinstance(timeout, timedelta):
sec = timeout.total_seconds()
else:
@@ -27,8 +27,8 @@ def poll_fd(fd: int, timeout: Optional[Union[timedelta, float]] = None) -> bool:
def config_iter(
- config: dict[Union[Iterable[Union[int, str]], int, str], Optional[LineSettings]],
-) -> Generator[tuple[Union[int, str], Optional[LineSettings]]]:
+ config: dict[Iterable[int | str] | int | str, LineSettings | None],
+) -> Generator[tuple[int | str, LineSettings | None]]:
for key, settings in config.items():
if isinstance(key, int) or isinstance(key, str):
yield key, settings
diff --git a/bindings/python/gpiod/chip.py b/bindings/python/gpiod/chip.py
index a98fce6..8113fa9 100644
--- a/bindings/python/gpiod/chip.py
+++ b/bindings/python/gpiod/chip.py
@@ -4,7 +4,7 @@
from __future__ import annotations
from errno import ENOENT
-from typing import TYPE_CHECKING, Optional, Union, cast
+from typing import TYPE_CHECKING, cast
from . import _ext
from ._internal import config_iter, poll_fd
@@ -60,8 +60,8 @@ class Chip:
path:
Path to the GPIO character device file.
"""
- self._chip: Union[_ext.Chip, None] = _ext.Chip(path)
- self._info: Union[ChipInfo, None] = None
+ self._chip: _ext.Chip | None = _ext.Chip(path)
+ self._info: ChipInfo | None = None
def __bool__(self) -> bool:
"""
@@ -81,9 +81,9 @@ class Chip:
def __exit__(
self,
- exc_type: Optional[type[BaseException]],
- exc_value: Optional[BaseException],
- traceback: Optional[TracebackType],
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ traceback: TracebackType | None,
) -> None:
"""
Controlled execution exit callback.
@@ -117,7 +117,7 @@ class Chip:
return self._info
- def line_offset_from_id(self, id: Union[str, int]) -> int:
+ def line_offset_from_id(self, id: str | int) -> int:
"""
Map a line's identifier to its offset within the chip.
@@ -155,13 +155,13 @@ class Chip:
return offset
- def _get_line_info(self, line: Union[int, str], watch: bool) -> LineInfo:
+ def _get_line_info(self, line: int | str, watch: bool) -> LineInfo:
self._check_closed()
return cast("_ext.Chip", self._chip).get_line_info(
self.line_offset_from_id(line), watch
)
- def get_line_info(self, line: Union[int, str]) -> LineInfo:
+ def get_line_info(self, line: int | str) -> LineInfo:
"""
Get the snapshot of information about the line at given offset.
@@ -174,7 +174,7 @@ class Chip:
"""
return self._get_line_info(line, watch=False)
- def watch_line_info(self, line: Union[int, str]) -> LineInfo:
+ def watch_line_info(self, line: int | str) -> LineInfo:
"""
Get the snapshot of information about the line at given offset and
start watching it for future changes.
@@ -188,7 +188,7 @@ class Chip:
"""
return self._get_line_info(line, watch=True)
- def unwatch_line_info(self, line: Union[int, str]) -> None:
+ def unwatch_line_info(self, line: int | str) -> None:
"""
Stop watching a line for status changes.
@@ -201,9 +201,7 @@ class Chip:
self.line_offset_from_id(line)
)
- def wait_info_event(
- self, timeout: Optional[Union[timedelta, float]] = None
- ) -> bool:
+ def wait_info_event(self, timeout: timedelta | float | None = None) -> bool:
"""
Wait for line status change events on any of the watched lines on the
chip.
@@ -237,12 +235,10 @@ class Chip:
def request_lines(
self,
- config: dict[
- Union[Iterable[Union[int, str]], int, str], Optional[LineSettings]
- ],
- consumer: Optional[str] = None,
- event_buffer_size: Optional[int] = None,
- output_values: Optional[dict[Union[int, str], Value]] = None,
+ config: dict[Iterable[int | str] | int | str, LineSettings | None],
+ consumer: str | None = None,
+ event_buffer_size: int | None = None,
+ output_values: dict[int | str, Value] | None = None,
) -> LineRequest:
"""
Request a set of lines for exclusive usage.
diff --git a/bindings/python/gpiod/line_request.py b/bindings/python/gpiod/line_request.py
index deb48a7..0287791 100644
--- a/bindings/python/gpiod/line_request.py
+++ b/bindings/python/gpiod/line_request.py
@@ -4,7 +4,7 @@
from __future__ import annotations
import warnings
-from typing import TYPE_CHECKING, Optional, Union, cast
+from typing import TYPE_CHECKING, cast
from . import _ext
from ._internal import config_iter, poll_fd
@@ -33,11 +33,11 @@ class LineRequest:
Note: LineRequest objects can only be instantiated by a Chip parent.
LineRequest.__init__() is not part of stable API.
"""
- self._req: Union[_ext.Request, None] = req
+ self._req: _ext.Request | None = req
self._chip_name: str
self._offsets: list[int]
self._name_map: dict[str, int]
- self._lines: list[Union[int, str]]
+ self._lines: list[int | str]
def __bool__(self) -> bool:
"""
@@ -57,9 +57,9 @@ class LineRequest:
def __exit__(
self,
- exc_type: Optional[type[BaseException]],
- exc_value: Optional[BaseException],
- traceback: Optional[TracebackType],
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ traceback: TracebackType | None,
) -> None:
"""
Controlled execution exit callback.
@@ -79,7 +79,7 @@ class LineRequest:
cast("_ext.Request", self._req).release()
self._req = None
- def get_value(self, line: Union[int, str]) -> Value:
+ def get_value(self, line: int | str) -> Value:
"""
Get a single GPIO line value.
@@ -92,16 +92,14 @@ class LineRequest:
"""
return self.get_values([line])[0]
- def _line_to_offset(self, line: Union[int, str]) -> int:
+ def _line_to_offset(self, line: int | str) -> int:
if isinstance(line, int):
return line
if (_line := self._name_map.get(line)) is None:
raise ValueError(f"unknown line name: {line}")
return _line
- def get_values(
- self, lines: Optional[Iterable[Union[int, str]]] = None
- ) -> list[Value]:
+ def get_values(self, lines: Iterable[int | str] | None = None) -> list[Value]:
"""
Get values of a set of GPIO lines.
@@ -124,7 +122,7 @@ class LineRequest:
cast("_ext.Request", self._req).get_values(offsets, buf)
return buf
- def set_value(self, line: Union[int, str], value: Value) -> None:
+ def set_value(self, line: int | str, value: Value) -> None:
"""
Set the value of a single GPIO line.
@@ -136,7 +134,7 @@ class LineRequest:
"""
self.set_values({line: value})
- def set_values(self, values: dict[Union[int, str], Value]) -> None:
+ def set_values(self, values: dict[int | str, Value]) -> None:
"""
Set the values of a subset of GPIO lines.
@@ -152,9 +150,7 @@ class LineRequest:
def reconfigure_lines(
self,
- config: dict[
- Union[Iterable[Union[int, str]], int, str], Optional[LineSettings]
- ],
+ config: dict[Iterable[int | str] | int | str, LineSettings | None],
) -> None:
"""
Reconfigure requested lines.
@@ -196,9 +192,7 @@ class LineRequest:
cast("_ext.Request", self._req).reconfigure_lines(line_cfg)
- def wait_edge_events(
- self, timeout: Optional[Union[timedelta, float]] = None
- ) -> bool:
+ def wait_edge_events(self, timeout: timedelta | float | None = None) -> bool:
"""
Wait for edge events on any of the requested lines.
@@ -215,7 +209,7 @@ class LineRequest:
return poll_fd(self.fd, timeout)
- def read_edge_events(self, max_events: Optional[int] = None) -> list[EdgeEvent]:
+ def read_edge_events(self, max_events: int | None = None) -> list[EdgeEvent]:
"""
Read a number of edge events from a line request.
@@ -271,7 +265,7 @@ class LineRequest:
return self._offsets
@property
- def lines(self) -> list[Union[int, str]]:
+ def lines(self) -> list[int | str]:
"""
List of requested lines. Lines requested by name are listed as such.
"""
diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml
index 1c3549c..d919b88 100644
--- a/bindings/python/pyproject.toml
+++ b/bindings/python/pyproject.toml
@@ -11,7 +11,7 @@ dynamic = ["version"]
description = "Python bindings for libgpiod"
readme = "README.md"
license = "LGPL-2.1-or-later"
-requires-python = ">=3.9.0"
+requires-python = ">=3.10"
authors = [
{name = "Bartosz Golaszewski", email = "brgl@bgdev.pl"},
]
@@ -22,7 +22,6 @@ classifiers = [
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
- "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
@@ -46,7 +45,7 @@ include = ["gpiod"]
namespaces = false
[tool.mypy]
-python_version = "3.9"
+python_version = "3.10"
files = [
"gpiod/",
"tests/",
@@ -57,7 +56,7 @@ module = "gpiod.line.*"
strict_equality = false # Ignore Enum comparison-overlap: https://github.com/python/mypy/issues/17317
[tool.ruff]
-target-version = "py39"
+target-version = "py310"
include = [
"gpiod/**/*.py",
"gpiod/**/*.pyi",
@@ -72,9 +71,6 @@ ignore=[
"B904",
# Never enforce line length violations. Let the formatter handle it: https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules
"E501",
- # Ignore new Union (|) syntax until we require 3.10+
- "UP007",
- "UP045",
]
[tool.ruff.lint.per-file-ignores]
diff --git a/bindings/python/tests/gpiosim/chip.py b/bindings/python/tests/gpiosim/chip.py
index 691bfe1..7fd0042 100644
--- a/bindings/python/tests/gpiosim/chip.py
+++ b/bindings/python/tests/gpiosim/chip.py
@@ -2,7 +2,6 @@
# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
from enum import Enum
-from typing import Optional
from . import _ext
@@ -27,10 +26,10 @@ class Chip:
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,
+ label: str | None = None,
+ num_lines: int | None = None,
+ line_names: dict[int, str] | None = None,
+ hogs: dict[int, tuple[str, Direction]] | None = None,
):
self._chip = _ext.Chip()
diff --git a/bindings/python/tests/helpers.py b/bindings/python/tests/helpers.py
index ad272a1..4abd8b2 100644
--- a/bindings/python/tests/helpers.py
+++ b/bindings/python/tests/helpers.py
@@ -4,7 +4,7 @@
from __future__ import annotations
import os
-from typing import TYPE_CHECKING, Optional
+from typing import TYPE_CHECKING
if TYPE_CHECKING:
from types import TracebackType
@@ -20,8 +20,8 @@ class LinkGuard:
def __exit__(
self,
- type: Optional[type[BaseException]],
- val: Optional[BaseException],
- tb: Optional[TracebackType],
+ type: type[BaseException] | None,
+ val: BaseException | None,
+ tb: TracebackType | None,
) -> None:
os.unlink(self.dst)
diff --git a/bindings/python/tests/tests_edge_event.py b/bindings/python/tests/tests_edge_event.py
index bf1685c..4efed2a 100644
--- a/bindings/python/tests/tests_edge_event.py
+++ b/bindings/python/tests/tests_edge_event.py
@@ -6,7 +6,6 @@ from datetime import timedelta
from functools import partial
from select import select
from threading import Thread
-from typing import Optional
from unittest import TestCase
import gpiod
@@ -56,7 +55,7 @@ class EdgeEventInvalidConfig(TestCase):
class WaitingForEdgeEvents(TestCase):
def setUp(self) -> None:
self.sim = gpiosim.Chip(num_lines=8)
- self.thread: Optional[Thread] = None
+ self.thread: Thread | None = None
def tearDown(self) -> None:
if self.thread:
@@ -208,7 +207,7 @@ class PollLineRequestObject(TestCase):
self.request = gpiod.request_lines(
self.sim.dev_path, {2: gpiod.LineSettings(edge_detection=Edge.BOTH)}
)
- self.thread: Optional[Thread] = None
+ self.thread: Thread | None = None
def tearDown(self) -> None:
if self.thread:
diff --git a/bindings/python/tests/tests_info_event.py b/bindings/python/tests/tests_info_event.py
index 31dc952..e32ef19 100644
--- a/bindings/python/tests/tests_info_event.py
+++ b/bindings/python/tests/tests_info_event.py
@@ -8,7 +8,6 @@ import time
from dataclasses import FrozenInstanceError
from functools import partial
from select import select
-from typing import Optional
from unittest import TestCase
import gpiod
@@ -53,7 +52,7 @@ class WatchingInfoEventWorks(TestCase):
def setUp(self) -> None:
self.sim = gpiosim.Chip(num_lines=8, line_names={4: "foobar"})
self.chip = gpiod.Chip(self.sim.dev_path)
- self.thread: Optional[threading.Thread] = None
+ self.thread: threading.Thread | None = None
def tearDown(self) -> None:
if self.thread:
diff --git a/configure.ac b/configure.ac
index 61a010f..c1bae2a 100644
--- a/configure.ac
+++ b/configure.ac
@@ -234,7 +234,7 @@ AM_CONDITIONAL([WITH_BINDINGS_PYTHON], [test "x$with_bindings_python" = xtrue])
if test "x$with_bindings_python" = xtrue
then
- AM_PATH_PYTHON([3.9], [],
+ AM_PATH_PYTHON([3.10], [],
[AC_MSG_ERROR([python3 not found - needed for python bindings])])
fi
--
2.43.0
next reply other threads:[~2026-03-29 18:28 UTC|newest]
Thread overview: 2+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-03-29 18:28 Vincent Fazio [this message]
2026-03-30 9:04 ` [libgpiod][PATCH] bindings: python: drop python 3.9 support 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=20260329182832.39824-1-vfazio@gmail.com \
--to=vfazio@gmail.com \
--cc=linux-gpio@vger.kernel.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