device: Handle composite IntFlag.name on Python < 3.11 (#3187)

* Handle composite IntFlag.name on Python < 3.11

On Python < 3.11, IntFlag.name returns None for composite flags
(e.g. NotificationFlag(0x000900) = SOFTWARE_PRESENT|WIRELESS).

The previous fix (#3184) returns an empty list in this case, losing
the flag names. This commit decomposes composite flags manually by
iterating over enum members, producing correct results on all
supported Python versions (>= 3.8 as declared in setup.py).

Add regression tests covering composite flags and None input.

Tested on:
- Python 3.9.2 (Debian 11): 56/56 passed
- Python 3.11.15 (pyenv): 56/56 passed

Refs #3184

* Fix line length style violation (E501)

Move comment above the parametrize entry to stay under 127 chars.

---------

Co-authored-by: avercelli <avercelli@vulog.com>
This commit is contained in:
Alessio85 2026-04-16 17:38:25 +02:00 committed by GitHub
parent 0a6421ef82
commit 8b023115d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 31 additions and 2 deletions

View File

@ -91,9 +91,12 @@ class NotificationFlag(IntFlag):
@classmethod
def flag_names(cls, flags) -> List[str]:
"""Extract the names of the flags from the integer."""
if flags is None or flags.name is None:
if flags is None:
return []
return flags.name.replace("_", " ").lower().split("|")
if flags.name is not None:
return flags.name.replace("_", " ").lower().split("|")
# Python < 3.11: .name is None for composite flags, decompose manually
return [m.name.replace("_", " ").lower() for m in cls if m.value and m in flags]
NUMPAD_NUMERICAL_KEYS = 0x800000
F_LOCK_STATUS = 0x400000

View File

@ -296,6 +296,32 @@ def test_notification_flag_str(flag_bits, expected_names):
assert flag_names == expected_names
@pytest.mark.parametrize(
"flags, expected",
[
(None, []),
# composite flag, .name is None on Python < 3.11
(hidpp10_constants.NotificationFlag(0x000900), ["software present", "wireless"]),
(hidpp10_constants.NotificationFlag(0x100000), ["battery status"]),
(hidpp10_constants.NotificationFlag(0x080000), ["mouse extra buttons"]),
],
)
def test_notification_flag_names(flags, expected):
result = hidpp10_constants.NotificationFlag.flag_names(flags)
assert result == expected
def test_notification_flag_names_none_does_not_crash():
"""Regression test for #3184: flag_names crashes with AttributeError when
flags.name is None, which happens with certain receiver firmware versions
(e.g. Nano C52F reporting 0x000900)."""
flags_with_none_name = hidpp10_constants.NotificationFlag(0x000900)
result = hidpp10_constants.NotificationFlag.flag_names(flags_with_none_name)
assert "software present" in result
assert "wireless" in result
assert isinstance(result, list)
def test_get_device_features():
result = _hidpp10.get_device_features(device_standard)