Ran some AI CR grilling:
- pid: normalize to a string (wpid is str, product_id is int for USB / hex str for Bluetooth) so consumers see one type. - battery.status: decompose combined flags into single-bit member names so output is identical on Python < 3.11 and 3.11+. - close open device handles on the --json path even when output raises (try/finally), matching the text path. - level_kind: rename "reported" to approximation" to contrast with "level". - tests: restore py3.8 import compatibility via __future__ annotations, assert the exact combined-flag rendering, cover the close-on-raise path.
This commit is contained in:
parent
93a0ef8e7f
commit
53d9c0c145
|
|
@ -156,12 +156,21 @@ def _battery_json(battery):
|
|||
level = battery.level
|
||||
if isinstance(level, BatteryLevelApproximation):
|
||||
# A qualitative level (e.g. "good"), not an actual percentage
|
||||
level_kind = "reported"
|
||||
level_kind = "approximation"
|
||||
elif isinstance(level, int):
|
||||
level_kind = "level"
|
||||
else:
|
||||
level_kind = None
|
||||
status = getattr(battery.status, "name", None)
|
||||
status = getattr(battery.status, "name", None) # canonical member name; combined flags have none on Python < 3.11
|
||||
if status is None and battery.status is not None:
|
||||
# Decompose combined flags into single-bit members so the output
|
||||
# is identical on every Python version.
|
||||
status = (
|
||||
"|".join(
|
||||
m.name for m in type(battery.status) if m.value and m.value & (m.value - 1) == 0 and (battery.status & m) == m
|
||||
)
|
||||
or None
|
||||
)
|
||||
return {
|
||||
"level": int(level) if level is not None else None,
|
||||
"level_kind": level_kind,
|
||||
|
|
@ -184,7 +193,8 @@ def _receiver_json(receiver):
|
|||
|
||||
def _device_json(dev):
|
||||
"""Serialize a device as a JSON-friendly dict, or None if the device is gone."""
|
||||
# Save the protocol before the ping, as it overrides it
|
||||
# Save the descriptor-known protocol; the ping below may update it, and
|
||||
# for an offline device the ping fails and would leave it unset.
|
||||
protocol = float(dev.protocol) if dev.protocol else None
|
||||
try:
|
||||
online = dev.ping()
|
||||
|
|
@ -201,7 +211,7 @@ def _device_json(dev):
|
|||
"name": dev.name,
|
||||
"number": dev.number,
|
||||
"receiver": _receiver_json(receiver),
|
||||
"pid": dev.wpid or dev.product_id,
|
||||
"pid": str(dev.wpid or dev.product_id),
|
||||
"path": dev.path,
|
||||
"bluetooth": dev.bluetooth,
|
||||
"mac": dev.hid_serial if dev.bluetooth else None,
|
||||
|
|
@ -562,9 +572,11 @@ def run(devices, args, find_receiver, find_device):
|
|||
assert args.device
|
||||
|
||||
if args.json:
|
||||
_json_output(devices, args.device.lower(), find_receiver, find_device)
|
||||
for d in Device.instances:
|
||||
d.close()
|
||||
try:
|
||||
_json_output(devices, args.device.lower(), find_receiver, find_device)
|
||||
finally:
|
||||
for d in Device.instances:
|
||||
d.close()
|
||||
return
|
||||
|
||||
print(f"{NAME.lower()} version {__version__}")
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@
|
|||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
|
@ -136,13 +138,13 @@ def test_battery_json_percentage():
|
|||
def test_battery_json_approximation():
|
||||
info = _battery_json(_battery(BatteryLevelApproximation.GOOD))
|
||||
assert info["level"] == 50
|
||||
assert info["level_kind"] == "reported"
|
||||
assert info["level_kind"] == "approximation"
|
||||
|
||||
|
||||
def test_battery_json_full_approximation():
|
||||
info = _battery_json(_battery(BatteryLevelApproximation.FULL, next_level=BatteryLevelApproximation.LOW))
|
||||
assert info["level"] == 90
|
||||
assert info["level_kind"] == "reported"
|
||||
assert info["level_kind"] == "approximation"
|
||||
assert info["next_level"] == 20
|
||||
|
||||
|
||||
|
|
@ -166,7 +168,7 @@ def test_battery_json_no_status():
|
|||
def test_battery_json_combined_status_flag():
|
||||
combined = BatteryStatus(0x07) # bits from multiple flags (no canonical name on Python < 3.11)
|
||||
status = _battery_json(_battery(50, combined))["status"]
|
||||
assert status is None or isinstance(status, str)
|
||||
assert status == "RECHARGING|ALMOST_FULL|SLOW_RECHARGE"
|
||||
|
||||
|
||||
def test_receiver_json_none():
|
||||
|
|
@ -225,12 +227,12 @@ def test_device_json_bluetooth_device():
|
|||
assert info["online"] is True
|
||||
|
||||
|
||||
def test_device_json_reported_battery(mocker):
|
||||
def test_device_json_approximation_battery(mocker):
|
||||
dev = _keyboard_device()
|
||||
mocker.patch.object(dev, "battery", return_value=_battery(BatteryLevelApproximation.GOOD))
|
||||
info = _device_json(dev)
|
||||
assert info["battery"]["level"] == 50
|
||||
assert info["battery"]["level_kind"] == "reported"
|
||||
assert info["battery"]["level_kind"] == "approximation"
|
||||
|
||||
|
||||
def test_device_json_offline_battery_unavailable():
|
||||
|
|
@ -309,3 +311,18 @@ def test_run_json_output(mocker):
|
|||
output = _capture_json(mocker, run, [dev], args, None, None)
|
||||
|
||||
assert [d["name"] for d in output["devices"]] == ["Craft Advanced Keyboard"]
|
||||
|
||||
|
||||
def test_run_json_closes_devices_when_output_raises(mocker):
|
||||
from solaar.cli.show import Device
|
||||
from solaar.cli.show import run
|
||||
|
||||
dev = _keyboard_device()
|
||||
args = mocker.Mock(json=True, device="all")
|
||||
mocker.patch("solaar.cli.show._json_output", side_effect=RuntimeError("boom"))
|
||||
close = mocker.patch.object(Device, "close")
|
||||
|
||||
with pytest.raises(RuntimeError):
|
||||
run([dev], args, None, None)
|
||||
|
||||
close.assert_called()
|
||||
|
|
|
|||
Loading…
Reference in New Issue