11# SPDX-License-Identifier: MIT
22
33import enum
4+ import sys
45
5- from typing import Any , ClassVar , Optional
6+ from collections .abc import Collection
7+ from typing import Any , Generic , Optional
8+
9+
10+ if sys .version_info >= (3 , 13 ):
11+ from typing import TypeVar
12+ else :
13+ from typing_extensions import TypeVar
14+
15+
16+ if sys .version_info >= (3 , 10 ):
17+ from types import NoneType
18+ else :
19+ NoneType = type (None )
620
721
822class _DataMeta (type ):
@@ -42,15 +56,15 @@ class _DataMeta(type):
4256 This metaclass also does some verification to prevent duplicated data.
4357 """
4458
45- def __new__ (mcs , name : str , bases : tuple [Any ], dic : dict [str , Any ]): # type: ignore[no-untyped-def] # noqa: C901
46- dic ['_single' ] = {}
47- dic ['_range' ] = []
59+ def __new__ (cls , name : str , bases : tuple [Any ], obj_dict : dict [str , Any ]): # type: ignore[no-untyped-def] # noqa: C901
60+ obj_dict ['_single' ] = {}
61+ obj_dict ['_range' ] = []
4862
49- # allow constructing data via a data dictionary as opposed to directly in the object body
50- if 'data' in dic :
51- data = dic .pop ('data' )
63+ # allow constructing data via a data obj_dicttionary as opposed to directly in the object body
64+ if 'data' in obj_dict :
65+ data = obj_dict .pop ('data' )
5266 else :
53- data = dic
67+ data = obj_dict
5468
5569 for attr in data :
5670 if not attr .startswith ('_' ) and isinstance (data [attr ], tuple ):
@@ -67,17 +81,17 @@ def __new__(mcs, name: str, bases: tuple[Any], dic: dict[str, Any]): # type: ig
6781 msg = f"Second element of '{ attr } ' should be a string"
6882 raise TypeError (msg )
6983
70- if num in dic ['_single' ]:
84+ if num in obj_dict ['_single' ]:
7185 msg = f"Duplicated value in '{ attr } ' ({ num } )"
7286 raise ValueError (msg )
7387
74- for nmin , nmax , _ in dic ['_range' ]:
88+ for nmin , nmax , _ in obj_dict ['_range' ]:
7589 if nmin <= num <= nmax :
7690 msg = f"Duplicated value in '{ attr } ' ({ num } )"
7791 raise ValueError (msg )
7892
79- dic [attr ] = num
80- dic ['_single' ][num ] = desc , sub
93+ obj_dict [attr ] = num
94+ obj_dict ['_single' ][num ] = desc , sub
8195 elif len (data [attr ]) == 5 : # range
8296 nmin , el , nmax , desc , sub = data [attr ]
8397
@@ -94,33 +108,36 @@ def __new__(mcs, name: str, bases: tuple[Any], dic: dict[str, Any]): # type: ig
94108 msg = f"Fourth element of '{ attr } ' should be a string"
95109 raise TypeError (msg )
96110
97- for num in dic ['_single' ]:
111+ for num in obj_dict ['_single' ]:
98112 if nmin <= num <= nmax :
99113 msg = f"Duplicated value in '{ attr } ' ({ num } )"
100114 raise ValueError (msg )
101115
102- dic [attr ] = range (nmin , nmax + 1 )
103- dic ['_range' ].append ((nmin , nmax , (desc , sub )))
116+ obj_dict [attr ] = range (nmin , nmax + 1 )
117+ obj_dict ['_range' ].append ((nmin , nmax , (desc , sub )))
104118
105119 else :
106120 msg = f'Invalid field: { attr } '
107121 raise ValueError (msg )
108122
109- return super ().__new__ (mcs , name , bases , dic )
123+ return super ().__new__ (cls , name , bases , obj_dict )
110124
111125
112- class _Data (metaclass = _DataMeta ):
126+ _SubdataType = TypeVar ('_SubdataType' , default = NoneType )
127+
128+
129+ class _Data (Generic [_SubdataType ], metaclass = _DataMeta ):
113130 """
114131 This class provides a get_description method to get data out of _single and _range.
115132 See the _DataMeta documentation for more information.
116133 """
117134
118- _DATA = tuple [str , Optional [ Any ]]
119- _single : dict [ int , _DATA ]
120- _range : list [ tuple [int , int , _DATA ]]
135+ _single : dict [ int , tuple [str , _SubdataType ]]
136+ _range : list [ tuple [ int , int , tuple [ str , _SubdataType ]] ]
137+ data : dict [ str , tuple [int , str , _SubdataType ]]
121138
122139 @classmethod
123- def _get_data (cls , num : Optional [int ]) -> _DATA :
140+ def _get_data (cls , num : Optional [int ]) -> tuple [ str , _SubdataType ] :
124141 if num is None :
125142 msg = 'Data index is not an int'
126143 raise KeyError (msg )
@@ -140,7 +157,7 @@ def get_description(cls, num: Optional[int]) -> str:
140157 return cls ._get_data (num )[0 ]
141158
142159 @classmethod
143- def get_subdata (cls , num : Optional [int ]) -> Any :
160+ def get_subdata (cls , num : Optional [int ]) -> _SubdataType :
144161 subdata = cls ._get_data (num )[1 ]
145162
146163 if not subdata :
@@ -210,7 +227,11 @@ class Collections(_Data):
210227 VENDOR = 0x80 , ..., 0xFF , 'Vendor'
211228
212229
213- class GenericDesktopControls (_Data ):
230+ class UsagePage (_Data [Collection [UsageTypes ]]):
231+ pass
232+
233+
234+ class GenericDesktopControls (UsagePage ):
214235 POINTER = 0x01 , 'Pointer' , UsageTypes .CP
215236 MOUSE = 0x02 , 'Mouse' , UsageTypes .CA
216237 JOYSTICK = 0x04 , 'Joystick' , UsageTypes .CA
@@ -282,7 +303,7 @@ class GenericDesktopControls(_Data):
282303 SYSTEM_DISPLAY_LCD_AUTOSCALE = 0xB7 , 'System Display LCD Autoscale' , UsageTypes .OSC
283304
284305
285- class KeyboardKeypad (_Data ):
306+ class KeyboardKeypad (UsagePage ):
286307 NO_EVENT = 0x00 , 'No event indicated' , UsageTypes .SEL
287308 KEYBOARD_ERROR_ROLL_OVER = 0x01 , 'Keyboard ErrorRollOver' , UsageTypes .SEL
288309 KEYBOARD_POST = 0x02 , 'Keyboard POSTFail' , UsageTypes .SEL
@@ -504,7 +525,7 @@ class KeyboardKeypad(_Data):
504525 KEYBOARD_RIGHT_GUI = 0xE7 , 'Keyboard Right GUI' , UsageTypes .DV
505526
506527
507- class Led (_Data ):
528+ class Led (UsagePage ):
508529 NUM_LOCK = 0x01 , 'Num Lock' , UsageTypes .OOC
509530 CAPS_LOCK = 0x02 , 'Caps Lock' , UsageTypes .OOC
510531 SCROLL_LOCK = 0x03 , 'Scroll Lock' , UsageTypes .OOC
@@ -584,15 +605,19 @@ class Led(_Data):
584605 EXTERNAL_POWER_CONNECTED = 0x4D , 'External Power Connected' , UsageTypes .OOC
585606
586607
587- class Button (_Data ):
608+ class Button (UsagePage ):
588609 _USAGE_TYPES = (
589610 UsageTypes .SEL ,
590611 UsageTypes .OOC ,
591612 UsageTypes .MC ,
592613 UsageTypes .OSC ,
593614 )
594615
595- data : ClassVar [dict [str , tuple [int , str , UsageTypes ]]] = {
616+ # XXX: The type of the 'data' variable includes a generic type bound to the
617+ # class, which currently isn't supported in ClassVar, so we don't
618+ # define 'data' as a ClassVar. Ruff complains about it in RUF012, but
619+ # since we can't define `data` as ClassVar, let's just ignore it.
620+ data = { # noqa: RUF012
596621 'NO_BUTTON' : (0x0000 , 'Button 1 (primary/trigger)' , _USAGE_TYPES ),
597622 'BUTTON_1' : (0x0001 , 'Button 1 (primary/trigger)' , _USAGE_TYPES ),
598623 'BUTTON_2' : (0x0002 , 'Button 2 (secondary)' , _USAGE_TYPES ),
@@ -603,7 +628,7 @@ class Button(_Data):
603628 data [f'BUTTON_{ _i } ' ] = _i , f'Button { _i } ' , _USAGE_TYPES
604629
605630
606- class Consumer (_Data ):
631+ class Consumer (UsagePage ):
607632 CONSUMER_CONTROL = 0x0001 , 'Consumer Control' , UsageTypes .CA
608633 NUMERIC_KEY_PAD = 0x0002 , 'Numeric Key Pad' , UsageTypes .NARY
609634 PROGRAMMABLE_BUTTONS = 0x0003 , 'Programmable Buttons' , UsageTypes .NARY
@@ -970,7 +995,7 @@ class Consumer(_Data):
970995 AC_DISTRIBUTE_VERTICALLY = 0x029C , 'AC Distribute Vertically' , UsageTypes .SEL
971996
972997
973- class PowerDevice (_Data ):
998+ class PowerDevice (UsagePage ):
974999 INAME = 0x01 , 'iName' , UsageTypes .SV
9751000 PRESENT_STATUS = 0x02 , 'PresentStatus' , UsageTypes .CL
9761001 CHARGED_STATUS = 0x03 , 'ChangedStatus' , UsageTypes .CL
@@ -1050,13 +1075,13 @@ class PowerDevice(_Data):
10501075 ISERIALNUMBER = 0xFF , 'iSerialNumber' , UsageTypes .SV
10511076
10521077
1053- class FIDO (_Data ):
1078+ class FIDO (UsagePage ):
10541079 U2F_AUTHENTICATOR_DEVICEM = 0x01 , 'U2F Authenticator Device'
10551080 INPUT_REPORT_DATA = 0x20 , 'Input Report Data'
10561081 OUTPUT_REPORT_DATA = 0x21 , 'Output Report Data'
10571082
10581083
1059- class UsagePages (_Data ):
1084+ class UsagePages (_Data [ UsagePage ] ):
10601085 GENERIC_DESKTOP_CONTROLS_PAGE = 0x01 , 'Generic Desktop Controls' , GenericDesktopControls
10611086 SIMULATION_CONTROLS_PAGE = 0x02 , 'Simulation Controls'
10621087 VR_CONTROLS_PAGE = 0x03 , 'VR Controls'
0 commit comments