public inbox for linux-gpio@vger.kernel.org
 help / color / mirror / Atom feed
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


             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