From 90c43d3469f574638e8315d40a5cb3bbe602ba02 Mon Sep 17 00:00:00 2001 From: Georgi Valkov Date: Wed, 22 Jan 2025 00:43:21 +0100 Subject: [PATCH] Generate ecodes.py at build time - The existing ecodes.py is renamed to ecodes_runtime.py. - An ecodes.py is generated at build time (in build_ext) with the genecodes_py.py script, after the extension modules are built. The script essentially does a repr() on vars(ecodes_runtime) and adds type annotations. - If something goes wrong in the process of generating ecodes.py, ecodes_runtime.py is copied to ecodes.py. - Stop generating ecodes.pyi as the generated ecodes.py is fully annotated. --- .gitignore | 1 + MANIFEST.in | 2 +- evdev/__init__.py | 1 - evdev/ecodes.py | 105 +------------------------ evdev/ecodes_runtime.py | 102 ++++++++++++++++++++++++ evdev/{genecodes.py => genecodes_c.py} | 0 evdev/genecodes_py.py | 53 +++++++++++++ pyproject.toml | 3 - setup.py | 31 +++++--- 9 files changed, 180 insertions(+), 118 deletions(-) create mode 100644 evdev/ecodes_runtime.py rename evdev/{genecodes.py => genecodes_c.py} (100%) create mode 100644 evdev/genecodes_py.py diff --git a/.gitignore b/.gitignore index 6548086..3e244aa 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ TAGS .#* __pycache__ .pytest_cache +.ruff_cache evdev/*.so evdev/ecodes.c diff --git a/MANIFEST.in b/MANIFEST.in index 1b5a7b6..bcbbd6c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,4 +2,4 @@ # evdev headers of the running kernel. Refer to the 'build_ecodes' distutils # command in setup.py. exclude evdev/ecodes.c -include evdev/ecodes.pyi +include evdev/ecodes.py diff --git a/evdev/__init__.py b/evdev/__init__.py index 6aa6ef2..c453e4b 100644 --- a/evdev/__init__.py +++ b/evdev/__init__.py @@ -2,7 +2,6 @@ # Gather everything into a single, convenient namespace. # -------------------------------------------------------------------------- -from . import ecodes, ff from .device import AbsInfo, DeviceInfo, EvdevError, InputDevice from .events import AbsEvent, InputEvent, KeyEvent, RelEvent, SynEvent, event_factory from .uinput import UInput, UInputError diff --git a/evdev/ecodes.py b/evdev/ecodes.py index 3a6c3d0..a19dcba 100644 --- a/evdev/ecodes.py +++ b/evdev/ecodes.py @@ -1,102 +1,5 @@ -# pylint: disable=undefined-variable -""" -This modules exposes the integer constants defined in ``linux/input.h`` and -``linux/input-event-codes.h``. +# When installed, this module is replaced by an ecodes.py generated at +# build time by genecodes_py.py (see build_ext in setup.py). -Exposed constants:: - - KEY, ABS, REL, SW, MSC, LED, BTN, REP, SND, ID, EV, - BUS, SYN, FF, FF_STATUS, INPUT_PROP - -This module also provides reverse and forward mappings of the names and values -of the above mentioned constants:: - - >>> evdev.ecodes.KEY_A - 30 - - >>> evdev.ecodes.ecodes['KEY_A'] - 30 - - >>> evdev.ecodes.KEY[30] - 'KEY_A' - - >>> evdev.ecodes.REL[0] - 'REL_X' - - >>> evdev.ecodes.EV[evdev.ecodes.EV_KEY] - 'EV_KEY' - - >>> evdev.ecodes.bytype[evdev.ecodes.EV_REL][0] - 'REL_X' - -Keep in mind that values in reverse mappings may point to one or more event -codes. For example:: - - >>> evdev.ecodes.FF[80] - ['FF_EFFECT_MIN', 'FF_RUMBLE'] - - >>> evdev.ecodes.FF[81] - 'FF_PERIODIC' -""" - -from inspect import getmembers - -from . import _ecodes - -#: Mapping of names to values. -ecodes = {} - -prefixes = "KEY ABS REL SW MSC LED BTN REP SND ID EV BUS SYN FF_STATUS FF INPUT_PROP" -prev_prefix = "" -g = globals() - -# eg. code: 'REL_Z', val: 2 -for code, val in getmembers(_ecodes): - for prefix in prefixes.split(): # eg. 'REL' - if code.startswith(prefix): - ecodes[code] = val - # FF_STATUS codes should not appear in the FF reverse mapping - if not code.startswith(prev_prefix): - d = g.setdefault(prefix, {}) - # codes that share the same value will be added to a list. eg: - # >>> ecodes.FF_STATUS - # {0: 'FF_STATUS_STOPPED', 1: ['FF_STATUS_MAX', 'FF_STATUS_PLAYING']} - if val in d: - if isinstance(d[val], list): - d[val].append(code) - else: - d[val] = [d[val], code] - else: - d[val] = code - - prev_prefix = prefix - -#: Keys are a combination of all BTN and KEY codes. -keys = {} -keys.update(BTN) -keys.update(KEY) - -# make keys safe to use for the default list of uinput device -# capabilities -del keys[_ecodes.KEY_MAX] -del keys[_ecodes.KEY_CNT] - -#: Mapping of event types to other value/name mappings. -bytype = { - _ecodes.EV_KEY: keys, - _ecodes.EV_ABS: ABS, - _ecodes.EV_REL: REL, - _ecodes.EV_SW: SW, - _ecodes.EV_MSC: MSC, - _ecodes.EV_LED: LED, - _ecodes.EV_REP: REP, - _ecodes.EV_SND: SND, - _ecodes.EV_SYN: SYN, - _ecodes.EV_FF: FF, - _ecodes.EV_FF_STATUS: FF_STATUS, -} - -from evdev._ecodes import * - -# cheaper than whitelisting in an __all__ -del code, val, prefix, getmembers, g, d, prefixes, prev_prefix +# This stub exists to make development of evdev itself more convenient. +from . ecodes_runtime import * diff --git a/evdev/ecodes_runtime.py b/evdev/ecodes_runtime.py new file mode 100644 index 0000000..3a6c3d0 --- /dev/null +++ b/evdev/ecodes_runtime.py @@ -0,0 +1,102 @@ +# pylint: disable=undefined-variable +""" +This modules exposes the integer constants defined in ``linux/input.h`` and +``linux/input-event-codes.h``. + +Exposed constants:: + + KEY, ABS, REL, SW, MSC, LED, BTN, REP, SND, ID, EV, + BUS, SYN, FF, FF_STATUS, INPUT_PROP + +This module also provides reverse and forward mappings of the names and values +of the above mentioned constants:: + + >>> evdev.ecodes.KEY_A + 30 + + >>> evdev.ecodes.ecodes['KEY_A'] + 30 + + >>> evdev.ecodes.KEY[30] + 'KEY_A' + + >>> evdev.ecodes.REL[0] + 'REL_X' + + >>> evdev.ecodes.EV[evdev.ecodes.EV_KEY] + 'EV_KEY' + + >>> evdev.ecodes.bytype[evdev.ecodes.EV_REL][0] + 'REL_X' + +Keep in mind that values in reverse mappings may point to one or more event +codes. For example:: + + >>> evdev.ecodes.FF[80] + ['FF_EFFECT_MIN', 'FF_RUMBLE'] + + >>> evdev.ecodes.FF[81] + 'FF_PERIODIC' +""" + +from inspect import getmembers + +from . import _ecodes + +#: Mapping of names to values. +ecodes = {} + +prefixes = "KEY ABS REL SW MSC LED BTN REP SND ID EV BUS SYN FF_STATUS FF INPUT_PROP" +prev_prefix = "" +g = globals() + +# eg. code: 'REL_Z', val: 2 +for code, val in getmembers(_ecodes): + for prefix in prefixes.split(): # eg. 'REL' + if code.startswith(prefix): + ecodes[code] = val + # FF_STATUS codes should not appear in the FF reverse mapping + if not code.startswith(prev_prefix): + d = g.setdefault(prefix, {}) + # codes that share the same value will be added to a list. eg: + # >>> ecodes.FF_STATUS + # {0: 'FF_STATUS_STOPPED', 1: ['FF_STATUS_MAX', 'FF_STATUS_PLAYING']} + if val in d: + if isinstance(d[val], list): + d[val].append(code) + else: + d[val] = [d[val], code] + else: + d[val] = code + + prev_prefix = prefix + +#: Keys are a combination of all BTN and KEY codes. +keys = {} +keys.update(BTN) +keys.update(KEY) + +# make keys safe to use for the default list of uinput device +# capabilities +del keys[_ecodes.KEY_MAX] +del keys[_ecodes.KEY_CNT] + +#: Mapping of event types to other value/name mappings. +bytype = { + _ecodes.EV_KEY: keys, + _ecodes.EV_ABS: ABS, + _ecodes.EV_REL: REL, + _ecodes.EV_SW: SW, + _ecodes.EV_MSC: MSC, + _ecodes.EV_LED: LED, + _ecodes.EV_REP: REP, + _ecodes.EV_SND: SND, + _ecodes.EV_SYN: SYN, + _ecodes.EV_FF: FF, + _ecodes.EV_FF_STATUS: FF_STATUS, +} + +from evdev._ecodes import * + +# cheaper than whitelisting in an __all__ +del code, val, prefix, getmembers, g, d, prefixes, prev_prefix diff --git a/evdev/genecodes.py b/evdev/genecodes_c.py similarity index 100% rename from evdev/genecodes.py rename to evdev/genecodes_c.py diff --git a/evdev/genecodes_py.py b/evdev/genecodes_py.py new file mode 100644 index 0000000..f33c7a1 --- /dev/null +++ b/evdev/genecodes_py.py @@ -0,0 +1,53 @@ +import sys +from unittest import mock +from pprint import PrettyPrinter + +sys.modules["evdev.ecodes"] = mock.Mock() +from evdev import ecodes_runtime as ecodes + +pprint = PrettyPrinter(indent=2, sort_dicts=True, width=120).pprint + + +print("# Automatically generated by evdev.genecodes_py") +print() +print('"""') +print(ecodes.__doc__.strip()) +print('"""') + +print() +print("from typing import Final, Dict, List, Union") +print() + +for name, value in ecodes.ecodes.items(): + print(f"{name}: Final = {value}") +print() + +entries = [ + ("ecodes", "Dict[str, int]", "#: Mapping of names to values."), + ("bytype", "Dict[int, Dict[int, Union[str, List[str]]", "#: Mapping of event types to other value/name mappings."), + ("keys", "Dict[int, Union[str, List[str]]", "#: Keys are a combination of all BTN and KEY codes."), + ("KEY", "Dict[int, Union[str, List[str]]", None), + ("ABS", "Dict[int, Union[str, List[str]]", None), + ("REL", "Dict[int, Union[str, List[str]]", None), + ("SW", "Dict[int, Union[str, List[str]]", None), + ("MSC", "Dict[int, Union[str, List[str]]", None), + ("LED", "Dict[int, Union[str, List[str]]", None), + ("BTN", "Dict[int, Union[str, List[str]]", None), + ("REP", "Dict[int, Union[str, List[str]]", None), + ("SND", "Dict[int, Union[str, List[str]]", None), + ("ID", "Dict[int, Union[str, List[str]]", None), + ("EV", "Dict[int, Union[str, List[str]]", None), + ("BUS", "Dict[int, Union[str, List[str]]", None), + ("SYN", "Dict[int, Union[str, List[str]]", None), + ("FF", "Dict[int, Union[str, List[str]]", None), + ("FF_STATUS", "Dict[int, Union[str, List[str]]", None), + ("INPUT_PROP", "Dict[int, Union[str, List[str]]", None) +] + +for key, annotation, doc in entries: + if doc: + print(doc) + + print(f"{key}: {annotation} = ", end="") + pprint(getattr(ecodes, key)) + print() diff --git a/pyproject.toml b/pyproject.toml index 5f56454..e7ea393 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,9 +32,6 @@ classifiers = [ [tool.setuptools] packages = ["evdev"] -[tool.setuptools.data-files] -"data" = ["evdev/*.pyi"] - [tool.ruff] line-length = 120 diff --git a/setup.py b/setup.py index 0990554..4b42869 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,9 @@ import os import sys +import shutil import textwrap from pathlib import Path +from subprocess import run from setuptools import setup, Extension, Command from setuptools.command import build_ext as _build_ext @@ -9,7 +11,6 @@ curdir = Path(__file__).resolve().parent ecodes_c_path = curdir / "evdev/ecodes.c" -ecodes_pyi_path = curdir / "evdev/ecodes.pyi" def create_ecodes(headers=None): @@ -49,7 +50,7 @@ def create_ecodes(headers=None): build_ext --include-dirs path/ \\ install - If you prefer to avoid building this package from source, then please consider + If you want to avoid building this package from source, then please consider installing the `evdev-binary` package instead. Keep in mind that it may not be fully compatible with, or support all the features of your current kernel. """ @@ -57,16 +58,9 @@ def create_ecodes(headers=None): sys.stderr.write(textwrap.dedent(msg)) sys.exit(1) - from subprocess import run - print("writing %s (using %s)" % (ecodes_c_path, " ".join(headers))) with ecodes_c_path.open("w") as fh: - cmd = [sys.executable, "evdev/genecodes.py", "--ecodes", *headers] - run(cmd, check=True, stdout=fh) - - print("writing %s (using %s)" % (ecodes_pyi_path, " ".join(headers))) - with ecodes_pyi_path.open("w") as fh: - cmd = [sys.executable, "evdev/genecodes.py", "--stubs", *headers] + cmd = [sys.executable, "evdev/genecodes_c.py", "--ecodes", *headers] run(cmd, check=True, stdout=fh) @@ -90,16 +84,29 @@ def run(self): class build_ext(_build_ext.build_ext): def has_ecodes(self): - if ecodes_c_path.exists() and ecodes_pyi_path.exists(): - print("ecodes.c and ecodes.pyi already exist ... skipping build_ecodes") + if ecodes_c_path.exists(): + print("ecodes.c already exists ... skipping build_ecodes") return False return True + def generate_ecodes_py(self): + ecodes_py = Path(self.build_lib) / "evdev/ecodes.py" + print(f"writing {ecodes_py}") + with ecodes_py.open("w") as fh: + cmd = [sys.executable, "-B", "evdev/genecodes_py.py"] + res = run(cmd, env={"PYTHONPATH": self.build_lib}, stdout=fh) + + if res.returncode != 0: + print(f"failed to generate static {ecodes_py} - will use ecodes_runtime.py") + shutil.copy("evdev/ecodes_runtime.py", ecodes_py) + def run(self): for cmd_name in self.get_sub_commands(): self.run_command(cmd_name) _build_ext.build_ext.run(self) + self.generate_ecodes_py() + sub_commands = [("build_ecodes", has_ecodes)] + _build_ext.build_ext.sub_commands