Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement type stubs for ecodes #216

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions evdev/genpyi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#! /usr/bin/env python3
"""
Generate a Python extension module with the constants defined in linux/input.h.
"""

from __future__ import print_function
import os, sys, re


# -----------------------------------------------------------------------------
# The default header file locations to try.
headers = [
"/usr/include/linux/input.h",
"/usr/include/linux/input-event-codes.h",
"/usr/include/linux/uinput.h",
]

if sys.argv[1:]:
headers = sys.argv[1:]

uname = list(os.uname())
del uname[1]
uname = " ".join(uname)
print(f"# used_linux_headers: {headers}")


# -----------------------------------------------------------------------------
macro_regex = r"#define +((?:KEY|ABS|REL|SW|MSC|LED|BTN|REP|SND|ID|EV|BUS|SYN|FF|UI_FF|INPUT_PROP)_\w+)"
macro_regex = re.compile(macro_regex)

# -----------------------------------------------------------------------------
template = rf"""
# Automatically generated by evdev.genecodes
# Generated on {uname}
# Headers: {headers}

"""

def parse_header(header):
lines = ""
for line in open(header):
macro = macro_regex.search(line)
if macro:
#lines += f" {macro.group(1)}: int{os.linesep}"
lines += f"{macro.group(1)}: int{os.linesep}"

return lines


for header in headers:
try:
fh = open(header)
except (IOError, OSError):
print(f"Unable to read header file: {header}")
continue
template += f"{parse_header(header)}"

print(template)
Comment on lines +1 to +58
Copy link
Collaborator

@sezanzeb sezanzeb Aug 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't tested my suggested code yet. Anyhow

  • I think it's good practice to avoid code that gets executed when importing a file. So this should be warpped in if __name__ == '__main__':. (I think the same way about genecodes.py, but refactoring this now is not important)
  • Also, I split this into a few separate specialized functions. I think that makes it a bit more readable.

I wouldn't want to bother you with somewhat opinionated review comments, so I just made the refactor myself:

Suggested change
#! /usr/bin/env python3
"""
Generate a Python extension module with the constants defined in linux/input.h.
"""
from __future__ import print_function
import os, sys, re
# -----------------------------------------------------------------------------
# The default header file locations to try.
headers = [
"/usr/include/linux/input.h",
"/usr/include/linux/input-event-codes.h",
"/usr/include/linux/uinput.h",
]
if sys.argv[1:]:
headers = sys.argv[1:]
uname = list(os.uname())
del uname[1]
uname = " ".join(uname)
print(f"# used_linux_headers: {headers}")
# -----------------------------------------------------------------------------
macro_regex = r"#define +((?:KEY|ABS|REL|SW|MSC|LED|BTN|REP|SND|ID|EV|BUS|SYN|FF|UI_FF|INPUT_PROP)_\w+)"
macro_regex = re.compile(macro_regex)
# -----------------------------------------------------------------------------
template = rf"""
# Automatically generated by evdev.genecodes
# Generated on {uname}
# Headers: {headers}
"""
def parse_header(header):
lines = ""
for line in open(header):
macro = macro_regex.search(line)
if macro:
#lines += f" {macro.group(1)}: int{os.linesep}"
lines += f"{macro.group(1)}: int{os.linesep}"
return lines
for header in headers:
try:
fh = open(header)
except (IOError, OSError):
print(f"Unable to read header file: {header}")
continue
template += f"{parse_header(header)}"
print(template)
#! /usr/bin/env python3
"""
Generate a Python extension module with the constants defined in linux/input.h.
"""
from __future__ import print_function
import os
import sys
import re
from typing import List
def get_headers() -> List[str]:
# The default header file locations to try.
header_files = [
"/usr/include/linux/input.h",
"/usr/include/linux/input-event-codes.h",
"/usr/include/linux/uinput.h",
]
if sys.argv[1:]:
header_files = sys.argv[1:]
print(f"# used_linux_headers: {header_files}")
return header_files
def get_uname() -> str:
uname = list(os.uname())
del uname[1]
return " ".join(uname)
def parse_header(header_file: str) -> str:
macro_regex = r"#define +((?:KEY|ABS|REL|SW|MSC|LED|BTN|REP|SND|ID|EV|BUS|SYN|FF|UI_FF|INPUT_PROP)_\w+)"
macro_regex = re.compile(macro_regex)
lines = ""
for line in open(header_file):
macro = macro_regex.search(line)
if macro:
# lines += f" {macro.group(1)}: int{os.linesep}"
lines += f"{macro.group(1)}: int{os.linesep}"
return lines
def generate_template(uname: str, headers: List[str]) -> str:
template = rf"""
# Automatically generated by evdev.genecodes
# Generated on {uname}
# Headers: {headers}
"""
for header in headers:
try:
fh = open(header)
except (IOError, OSError):
print(f"Unable to read header file: {header}")
continue
template += f"{parse_header(header)}"
return template
if __name__ == '__main__':
template = generate_template(
get_uname(),
get_headers()
)
print(template)

37 changes: 30 additions & 7 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@

from setuptools import setup, Extension, Command
from setuptools.command import build_ext as _build_ext

from setuptools.command import install as _install

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


def create_ecodes(headers=None):
Expand Down Expand Up @@ -54,11 +55,16 @@ def create_ecodes(headers=None):

from subprocess import run

print("writing %s (using %s)" % (ecodes_path, " ".join(headers)))
with ecodes_path.open("w") as fh:
print("writing %s (using %s)" % (ecodes_c_path, " ".join(headers)))
with ecodes_c_path.open("w") as fh:
cmd = [sys.executable, "evdev/genecodes.py", *headers]
Copy link
Collaborator

@sezanzeb sezanzeb Aug 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder why this was done using the subprocess module. Wouldn't just calling a method that does this be much cleaner?

For example in the case of the pyi stub, what would be a reason to not call generate_template and write the result into a file, without spawning a subprocess?

Anyhow, I'm fine with doing it as a subprocess since it has already been working like that in the past.

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/genpyi.py", *headers]
run(cmd, check=True, stdout=fh)


class build_ecodes(Command):
description = "generate ecodes.c"
Expand All @@ -80,20 +86,36 @@ def run(self):

class build_ext(_build_ext.build_ext):
def has_ecodes(self):
if ecodes_path.exists():
if ecodes_c_path.exists():
print("ecodes.c already exists ... skipping build_ecodes")
return not ecodes_path.exists()
return not ecodes_c_path.exists()

def run(self):
for cmd_name in self.get_sub_commands():
self.run_command(cmd_name)
_build_ext.build_ext.run(self)

sub_commands = [("build_ecodes", has_ecodes)] + _build_ext.build_ext.sub_commands
sub_commands = [("build_ecodes", has_ecodes)] + _build_ext.build_ext.sub_commands # type: ignore
use_stubs = True

# I've been reading through setuptools docs, but i can't for the lofe of me figure out how to bring the pyi stubs into the package "cleanly"
class install(_install.install):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generation of ecodes.c doesn't seem to require such a step, why does ecodes.pyi?

def run(self):
print(os.listdir(os.getenv("src")))
Copy link
Collaborator

@sezanzeb sezanzeb Aug 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where does the "src" environment variable come from? Is it something from when you were developing the change for debugging or something?

#_install.install.copy_file(self, "evdev/ecodes.pyi", "evdev/ecodes.pyi")
installdir = "build/lib.linux-x86_64-cpython-311/evdev"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

installdir should probably be os.path.join(self.build_lib, 'evdev')

Path(f"{installdir}/ecodes.pyi").write_bytes(Path("evdev/ecodes.pyi").read_bytes())
print("CUSTOM INSTALLER")
print(self.build_lib)
_install.install.run(self)


cflags = ["-std=c99", "-Wno-error=declaration-after-statement"]
setup(
#include_package_data=True,
packages=["evdev"],
package_dir={'evdev': 'evdev'},
package_data={'evdev': ["*.pyi"]},
ext_modules=[
Extension("evdev._input", sources=["evdev/input.c"], extra_compile_args=cflags),
Extension("evdev._uinput", sources=["evdev/uinput.c"], extra_compile_args=cflags),
Expand All @@ -102,5 +124,6 @@ def run(self):
cmdclass={
"build_ext": build_ext,
"build_ecodes": build_ecodes,
"install": install,
},
)