Refactor: Use dataclasses and enums

Replace NamedTuples with more flexible dataclass, which also support
type hints. Introduce enums to replace constant strings, which need to
be kept in sync. Also enhances interfaces by limiting it to the enum
values. Remove unused variables.
This commit is contained in:
Matthias Hagmann 2024-05-10 13:27:18 +02:00 committed by Peter F. Patel-Schneider
parent 500b9998c5
commit 7d171b1d09
4 changed files with 56 additions and 67 deletions

18
lib/hidapi/common.py Normal file
View File

@ -0,0 +1,18 @@
import dataclasses
@dataclasses.dataclass
class DeviceInfo:
path: str
bus_id: str
vendor_id: str
product_id: str
interface: str
driver: str
manufacturer: str
product: str
serial: str
release: str
isDevice: bool
hidpp_short: str
hidpp_long: str

View File

@ -28,38 +28,18 @@ import ctypes
import logging import logging
import platform import platform
from collections import namedtuple
from threading import Thread from threading import Thread
from time import sleep from time import sleep
import gi import gi
from hidapi.common import DeviceInfo
gi.require_version("Gdk", "3.0") gi.require_version("Gdk", "3.0")
from gi.repository import GLib # NOQA: E402 from gi.repository import GLib # NOQA: E402
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
native_implementation = "hidapi"
# Device info as expected by Solaar
DeviceInfo = namedtuple(
"DeviceInfo",
[
"path",
"bus_id",
"vendor_id",
"product_id",
"interface",
"driver",
"manufacturer",
"product",
"serial",
"release",
"isDevice",
"hidpp_short",
"hidpp_long",
],
)
# Global handle to hidapi # Global handle to hidapi
_hidapi = None _hidapi = None

View File

@ -29,7 +29,6 @@ import warnings
# the tuple object we'll expose when enumerating devices # the tuple object we'll expose when enumerating devices
from collections import namedtuple
from select import select from select import select
from time import sleep from time import sleep
from time import time from time import time
@ -37,33 +36,15 @@ from time import time
import gi import gi
import pyudev import pyudev
from hidapi.common import DeviceInfo
gi.require_version("Gdk", "3.0") gi.require_version("Gdk", "3.0")
from gi.repository import GLib # NOQA: E402 from gi.repository import GLib # NOQA: E402
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
native_implementation = "udev"
fileopen = open fileopen = open
DeviceInfo = namedtuple(
"DeviceInfo",
[
"path",
"bus_id",
"vendor_id",
"product_id",
"interface",
"driver",
"manufacturer",
"product",
"serial",
"release",
"isDevice",
"hidpp_short",
"hidpp_long",
],
)
# #
# exposed API # exposed API
# docstrings mostly copied from hidapi.h # docstrings mostly copied from hidapi.h

View File

@ -21,6 +21,7 @@ from collections import defaultdict
from copy import copy from copy import copy
from dataclasses import dataclass from dataclasses import dataclass
from dataclasses import field from dataclasses import field
from enum import Enum
from shlex import quote as shlex_quote from shlex import quote as shlex_quote
from typing import Any from typing import Any
from typing import Dict from typing import Dict
@ -932,6 +933,12 @@ class DeviceInfo:
codename: str = "" codename: str = ""
settings: Dict[str, _Setting] = field(default_factory=dict) settings: Dict[str, _Setting] = field(default_factory=dict)
def __post_init__(self):
if self.serial is None or self.serial == "?":
self.serial = ""
if self.unitId is None or self.unitId == "?":
self.unitId = ""
@property @property
def id(self): def id(self):
return self.serial or self.unitId or "" return self.serial or self.unitId or ""
@ -944,12 +951,6 @@ class DeviceInfo:
def display_name(self): def display_name(self):
return f"{self.codename} ({self.id})" return f"{self.codename} ({self.id})"
def __post_init__(self):
if self.serial is None or self.serial == "?":
self.serial = ""
if self.unitId is None or self.unitId == "?":
self.unitId = ""
def matches(self, search): def matches(self, search):
return search and search in (self.serial, self.unitId, self.display_name) return search and search in (self.serial, self.unitId, self.display_name)
@ -960,8 +961,10 @@ class DeviceInfo:
if v and v != "?": if v and v != "?":
setattr(self, k, copy(v) if k != "settings" else {s.name: s for s in v}) setattr(self, k, copy(v) if k != "settings" else {s.name: s for s in v})
class DeviceInfoFactory:
@classmethod @classmethod
def from_device(cls, device): def create_device_info(cls, device) -> DeviceInfo:
d = DeviceInfo() d = DeviceInfo()
d.update(device) d.update(device)
return d return d
@ -992,7 +995,8 @@ class AllDevicesInfo:
existing = self[device.serial] or self[device.unitId] existing = self[device.serial] or self[device.unitId]
if not existing: if not existing:
updated = True updated = True
self._devices.append(DeviceInfo.from_device(device)) device_info = DeviceInfoFactory.create_device_info(device)
self._devices.append(device_info)
elif not existing.settings and device.settings: elif not existing.settings and device.settings:
updated = True updated = True
existing.update(device) existing.update(device)
@ -1076,7 +1080,6 @@ class LaterUI(RuleComponentUI):
self.field.set_halign(Gtk.Align.CENTER) self.field.set_halign(Gtk.Align.CENTER)
self.field.set_valign(Gtk.Align.CENTER) self.field.set_valign(Gtk.Align.CENTER)
self.field.set_hexpand(True) self.field.set_hexpand(True)
# self.field.set_vexpand(True)
self.field.connect("value-changed", self._on_update) self.field.connect("value-changed", self._on_update)
self.widgets[self.field] = (0, 1, 1, 1) self.widgets[self.field] = (0, 1, 1, 1)
@ -1126,6 +1129,13 @@ def _from_named_ints(v, all_values):
return v return v
class SetValueControlKinds(Enum):
TOGGLE = "toggle"
RANGE = "range"
RANGE_WITH_KEY = "range_with_key"
CHOICE = "choice"
class SetValueControl(Gtk.HBox): class SetValueControl(Gtk.HBox):
def __init__(self, on_change, *args, accept_toggle=True, **kwargs): def __init__(self, on_change, *args, accept_toggle=True, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -1156,17 +1166,17 @@ class SetValueControl(Gtk.HBox):
self.pack_end(w, True, True, 0) self.pack_end(w, True, True, 0)
w.hide() w.hide()
self.unsupp_value = None self.unsupp_value = None
self.current_kind = None self.current_kind: Optional[SetValueControlKinds] = None
self.sub_key_range_items = None self.sub_key_range_items = None
def _changed(self, widget, *args): def _changed(self, widget, *args):
if widget.get_visible(): if widget.get_visible():
value = self.get_value() value = self.get_value()
if self.current_kind == "choice": if self.current_kind == SetValueControlKinds.CHOICE:
value = widget.get_value() value = widget.get_value()
icon = "dialog-warning" if widget._allowed_values and (value not in widget._allowed_values) else "" icon = "dialog-warning" if widget._allowed_values and (value not in widget._allowed_values) else ""
widget.get_child().set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon) widget.get_child().set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
elif self.current_kind == "range_with_key" and widget == self.sub_key_widget: elif self.current_kind == SetValueControlKinds.RANGE_WITH_KEY and widget == self.sub_key_widget:
key = self.sub_key_widget.get_value() key = self.sub_key_widget.get_value()
selected_item = ( selected_item = (
next((item for item in self.sub_key_range_items if key == item.id), None) next((item for item in self.sub_key_range_items if key == item.id), None)
@ -1182,27 +1192,27 @@ class SetValueControl(Gtk.HBox):
w.hide() w.hide()
def get_value(self): def get_value(self):
if self.current_kind == "toggle": if self.current_kind == SetValueControlKinds.TOGGLE:
return self.toggle_widget.get_value() return self.toggle_widget.get_value()
if self.current_kind == "range": if self.current_kind == SetValueControlKinds.RANGE:
return int(self.range_widget.get_value()) return int(self.range_widget.get_value())
if self.current_kind == "range_with_key": if self.current_kind == SetValueControlKinds.RANGE_WITH_KEY:
return {self.sub_key_widget.get_value(): int(self.range_widget.get_value())} return {self.sub_key_widget.get_value(): int(self.range_widget.get_value())}
if self.current_kind == "choice": if self.current_kind == SetValueControlKinds.CHOICE:
return self.choice_widget.get_value() return self.choice_widget.get_value()
return self.unsupp_value return self.unsupp_value
def set_value(self, value): def set_value(self, value):
if self.current_kind == "toggle": if self.current_kind == SetValueControlKinds.TOGGLE:
self.toggle_widget.set_value(value if value is not None else "") self.toggle_widget.set_value(value if value is not None else "")
elif self.current_kind == "range": elif self.current_kind == SetValueControlKinds.RANGE:
minimum, maximum = self.range_widget.get_range() minimum, maximum = self.range_widget.get_range()
try: try:
v = round(float(value)) v = round(float(value))
except (ValueError, TypeError): except (ValueError, TypeError):
v = minimum v = minimum
self.range_widget.set_value(max(minimum, min(maximum, v))) self.range_widget.set_value(max(minimum, min(maximum, v)))
elif self.current_kind == "range_with_key": elif self.current_kind == SetValueControlKinds.RANGE_WITH_KEY:
if not (isinstance(value, dict) and len(value) == 1): if not (isinstance(value, dict) and len(value) == 1):
value = {None: None} value = {None: None}
key = next(iter(value.keys())) key = next(iter(value.keys()))
@ -1216,7 +1226,7 @@ class SetValueControl(Gtk.HBox):
v = minimum v = minimum
self.sub_key_widget.set_value(key or "") self.sub_key_widget.set_value(key or "")
self.range_widget.set_value(max(minimum, min(maximum, v))) self.range_widget.set_value(max(minimum, min(maximum, v)))
elif self.current_kind == "choice": elif self.current_kind == SetValueControlKinds.CHOICE:
self.choice_widget.set_value(value) self.choice_widget.set_value(value)
else: else:
self.unsupp_value = value self.unsupp_value = value
@ -1228,18 +1238,18 @@ class SetValueControl(Gtk.HBox):
self.choice_widget.set_value("") self.choice_widget.set_value("")
def make_toggle(self): def make_toggle(self):
self.current_kind = "toggle" self.current_kind = SetValueControlKinds.TOGGLE
self._hide_all() self._hide_all()
self.toggle_widget.show() self.toggle_widget.show()
def make_range(self, minimum, maximum): def make_range(self, minimum, maximum):
self.current_kind = "range" self.current_kind = SetValueControlKinds.RANGE
self._hide_all() self._hide_all()
self.range_widget.set_range(minimum, maximum) self.range_widget.set_range(minimum, maximum)
self.range_widget.show() self.range_widget.show()
def make_range_with_key(self, items, labels=None): def make_range_with_key(self, items, labels=None):
self.current_kind = "range_with_key" self.current_kind = SetValueControlKinds.RANGE_WITH_KEY
self._hide_all() self._hide_all()
self.sub_key_range_items = items or None self.sub_key_range_items = items or None
if not labels: if not labels:
@ -1252,7 +1262,7 @@ class SetValueControl(Gtk.HBox):
def make_choice(self, values, extra=None): def make_choice(self, values, extra=None):
# if extra is not in values, it is ignored # if extra is not in values, it is ignored
self.current_kind = "choice" self.current_kind = SetValueControlKinds.CHOICE
self._hide_all() self._hide_all()
sort_key = int if all((v == extra or str(v).isdigit()) for v in values) else str sort_key = int if all((v == extra or str(v).isdigit()) for v in values) else str
if extra is not None and extra in values: if extra is not None and extra in values: