Skip to content

Commit 9fc042e

Browse files
committed
Modernize typing and require Python 3.10+
Drop Python 3.9 support and overhaul typing throughout the package. - Bump minimum Python to 3.10 in `pyproject.toml` and CI matrix. - Add `from __future__ import annotations` across the package. - Replace `Union`/`Optional`/`Dict`/`List`/`Tuple` with PEP 604/585 syntax. - Add precise `@overload` sets for `capabilities`, `_capabilities`, `input_props`, `leds`, `active_keys`, and `resolve_ecodes`. - Introduce `_Capabilities`, `_AbsInfoCapabilities`, and `_VerboseAbsInfoCapabilities` for richer return types on `capabilities()`. - Generate `_CapabilitiesKeys`, `_CapabilitiesAbsKeys`, and verbose variants from `genecodes_py.py` and emit `_ecodes.pyi` from `genecodes_c.py` during build. - Add `.pyi` stubs for the `_input` and `_uinput` C extensions. - Type `UInput.from_device` kwargs with `TypedDict` + `Unpack`. - Type the `need_write` decorator with `Concatenate`/`ParamSpec`. - Add `HasEvent` protocol and `is_has_event` type guard for `write_event`. - Annotate `ctypes.Structure` fields in `ff.py`. - Enable ruff `UP` and `TC` rule sets and replace `%`/`.format()` with f-strings.
1 parent a47b5b5 commit 9fc042e

20 files changed

Lines changed: 638 additions & 200 deletions

.github/workflows/install.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ jobs:
1111
fail-fast: false
1212
matrix:
1313
os: [ubuntu-latest]
14-
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
14+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
1515
include:
1616
- os: ubuntu-latest
17-
python-version: "3.9"
17+
python-version: "3.10"
1818

1919
steps:
2020
- uses: actions/checkout@v6

.github/workflows/lint.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ jobs:
2222

2323
- name: Check for pylint errors
2424
run: |
25-
python -m pip install pylint setuptools
25+
python -m pip install pylint setuptools ruff
2626
python setup.py build
2727
python -m pylint --verbose -E build/lib*/evdev
28+
python -m ruff check build/lib*/evdev

examples/udev-example.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@
3737
# should adapt this to your needs
3838
if "py-evdev-uinput" in name:
3939
if udev.action == "add":
40-
print("Device added: %s" % udev)
40+
print(f"Device added: {udev}")
4141
fds[dev.fd] = InputDevice(udev.device_node)
4242
break
4343
if udev.action == "remove":
44-
print("Device removed: %s" % udev)
44+
print(f"Device removed: {udev}")
4545

4646
def helper():
4747
global fds

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ description = "Bindings to the Linux input handling subsystem"
99
keywords = ["evdev", "input", "uinput"]
1010
readme = "README.md"
1111
license = "BSD-3-Clause"
12-
requires-python = ">=3.9"
12+
requires-python = ">=3.10"
1313
authors = [
1414
{ name="Georgi Valkov", email="georgi.t.valkov@gmail.com" },
1515
]
@@ -32,6 +32,7 @@ classifiers = [
3232
line-length = 120
3333

3434
[tool.ruff.lint]
35+
select = ["UP", "TC"]
3536
ignore = ["E265", "E241", "F403", "F401", "E401", "E731"]
3637

3738
[tool.bumpversion]

setup.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99
from setuptools import setup, Extension, Command
1010
from setuptools.command import build_ext as _build_ext
1111

12+
from evdev import genecodes_c
13+
1214

1315
curdir = Path(__file__).resolve().parent
1416
ecodes_c_path = curdir / "src/evdev/ecodes.c"
17+
ecodes_pyi_path = curdir / "src/evdev/_ecodes.pyi"
1518

1619

1720
def create_ecodes(headers=None, reproducible=False):
@@ -63,13 +66,14 @@ def create_ecodes(headers=None, reproducible=False):
6366
sys.stderr.write(textwrap.dedent(msg))
6467
sys.exit(1)
6568

66-
print("writing %s (using %s)" % (ecodes_c_path, " ".join(headers)))
67-
with ecodes_c_path.open("w") as fh:
68-
cmd = [sys.executable, "src/evdev/genecodes_c.py"]
69-
if reproducible:
70-
cmd.append("--reproducible")
71-
cmd.extend(["--ecodes", *headers])
72-
run(cmd, check=True, stdout=fh)
69+
for path, arg in [(ecodes_c_path, "--ecodes"), (ecodes_pyi_path, "--stubs")]:
70+
print("writing %s (using %s)" % (path, " ".join(headers)))
71+
with path.open("w") as fh:
72+
cmd = [sys.executable, "src/evdev/genecodes_c.py"]
73+
if reproducible:
74+
cmd.append("--reproducible")
75+
cmd.extend([arg, *headers])
76+
run(cmd, check=True, stdout=fh)
7377

7478

7579
class build_ecodes(Command):
@@ -96,8 +100,8 @@ def run(self):
96100

97101
class build_ext(_build_ext.build_ext):
98102
def has_ecodes(self):
99-
if ecodes_c_path.exists():
100-
print("ecodes.c already exists ... skipping build_ecodes")
103+
if ecodes_c_path.exists() and ecodes_pyi_path.exists():
104+
print("ecodes.c and _ecodes.pyi already exist ... skipping build_ecodes")
101105
return False
102106
return True
103107

src/evdev/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
# Gather everything into a single, convenient namespace.
33
# --------------------------------------------------------------------------
44

5-
# The superfluous "import name as name" syntax is here to satisfy mypy's attrs-defined rule.
6-
# Alternatively all exported objects can be listed in __all__.
5+
# The "import name as name" syntax is here to satisfy Python's type system
6+
# import conventions:
7+
# https://typing.python.org/en/latest/spec/distributing.html#import-conventions
8+
9+
from __future__ import annotations
710

811
from . import (
912
ecodes as ecodes,

src/evdev/_input.pyi

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""Python bindings to certain linux input subsystem functions"""
2+
3+
def ioctl_devinfo(fd: int, /) -> tuple[int, int, int, int, str, str, str]:
4+
"""fetch input device info"""
5+
6+
def ioctl_capabilities(
7+
fd: int, /
8+
) -> dict[int, list[int | tuple[int, tuple[int, int, int, int, int, int]]]]:
9+
"""fetch input device capabilities"""
10+
11+
def ioctl_EVIOCGABS(fd: int, ev_code: int, /) -> tuple[int, int, int, int, int, int]:
12+
"""get input device absinfo"""
13+
14+
def ioctl_EVIOCSABS(
15+
fd: int,
16+
ev_code: int,
17+
absinfo: tuple[int, int, int, int, int, int],
18+
/,
19+
) -> None:
20+
"""set input device absinfo"""
21+
22+
def ioctl_EVIOCGREP(fd: int, /) -> tuple[int, int]: ...
23+
def ioctl_EVIOCSREP(fd: int, delay: int, period: int, /) -> int: ...
24+
def ioctl_EVIOCGVERSION(fd: int, /) -> int: ...
25+
def ioctl_EVIOCGRAB(fd: int, flag: int, /) -> None: ...
26+
def ioctl_EVIOCGEFFECTS(fd: int, /) -> int:
27+
"""fetch the number of effects the device can keep in its memory."""
28+
29+
def ioctl_EVIOCG_bits(fd: int, evtype: int, /) -> list[int]:
30+
"""get state of KEY|LED|SND|SW"""
31+
32+
def ioctl_EVIOCGPROP(fd: int, /) -> list[int]:
33+
"""get device properties"""
34+
35+
def device_read(fd: int, /) -> tuple[int, int, int, int, int] | None:
36+
"""read an input event from a device"""
37+
38+
def device_read_many(fd: int, /) -> tuple[tuple[int, int, int, int, int], ...]:
39+
"""read all available input events from a device"""
40+
41+
def upload_effect(fd: int, effect_data: bytes, /) -> int: ...
42+
def erase_effect(fd: int, ff_id: int, /) -> None: ...

src/evdev/_uinput.pyi

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""Python bindings for parts of linux/uinput.c."""
2+
3+
from typing import Final
4+
5+
maxnamelen: Final[int]
6+
7+
def open(devnode: str, /) -> int:
8+
"""Open uinput device node."""
9+
10+
def setup(
11+
fd: int,
12+
name: str,
13+
vendor: int,
14+
product: int,
15+
version: int,
16+
bustype: int,
17+
absinfo: list[list[int]],
18+
max_effects: int,
19+
/,
20+
) -> None:
21+
"""Set an uinput device up."""
22+
23+
def create(fd: int, /) -> None:
24+
"""Create an uinput device."""
25+
26+
def close(fd: int, /) -> None:
27+
"""Destroy uinput device."""
28+
29+
def write(fd: int, type: int, code: int, value: int, /) -> None:
30+
"""Write event to uinput device."""
31+
32+
def enable(fd: int, type: int, code: int, /) -> None:
33+
"""Enable a type of event."""
34+
35+
def set_phys(fd: int, phys: str, /) -> None:
36+
"""Set physical path"""
37+
38+
def get_sysname(fd: int, /) -> str:
39+
"""Obtain the sysname of the uinput device."""
40+
41+
def set_prop(fd: int, prop: int, /) -> None:
42+
"""Set device input property"""

0 commit comments

Comments
 (0)