Skip to content

Commit

Permalink
Generate ecodes.py at build time
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
gvalkov committed Jan 22, 2025
1 parent 83f9360 commit 90c43d3
Show file tree
Hide file tree
Showing 9 changed files with 180 additions and 118 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ TAGS
.#*
__pycache__
.pytest_cache
.ruff_cache

evdev/*.so
evdev/ecodes.c
Expand Down
2 changes: 1 addition & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 0 additions & 1 deletion evdev/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
105 changes: 4 additions & 101 deletions evdev/ecodes.py
Original file line number Diff line number Diff line change
@@ -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 *
102 changes: 102 additions & 0 deletions evdev/ecodes_runtime.py
Original file line number Diff line number Diff line change
@@ -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
File renamed without changes.
53 changes: 53 additions & 0 deletions evdev/genecodes_py.py
Original file line number Diff line number Diff line change
@@ -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()
3 changes: 0 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ classifiers = [
[tool.setuptools]
packages = ["evdev"]

[tool.setuptools.data-files]
"data" = ["evdev/*.pyi"]

[tool.ruff]
line-length = 120

Expand Down
31 changes: 19 additions & 12 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
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


curdir = Path(__file__).resolve().parent
ecodes_c_path = curdir / "evdev/ecodes.c"
ecodes_pyi_path = curdir / "evdev/ecodes.pyi"


def create_ecodes(headers=None):
Expand Down Expand Up @@ -49,24 +50,17 @@ 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.
"""

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)


Expand All @@ -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


Expand Down

0 comments on commit 90c43d3

Please sign in to comment.