/
/
/
1"""Tests for value conversion and union fallthrough in API argument parsing."""
2
3from __future__ import annotations
4
5import inspect
6import logging
7from typing import get_type_hints
8
9import pytest
10
11from music_assistant.helpers.api import parse_arguments, parse_value
12
13
14def test_numeric_string_converted_to_int() -> None:
15 """A numeric string is accepted for an int annotation when conversion is allowed."""
16 assert parse_value("volume", "5", int, allow_value_convert=True) == 5
17
18
19def test_int_converted_to_float() -> None:
20 """An int is accepted for a float annotation when conversion is allowed."""
21 result = parse_value("gain", 1, float, allow_value_convert=True)
22 assert isinstance(result, float)
23 assert result == 1.0
24
25
26def test_numeric_string_converted_to_float() -> None:
27 """A numeric string is accepted for a float annotation when conversion is allowed."""
28 assert parse_value("gain", "5", float, allow_value_convert=True) == 5.0
29
30
31def test_float_converted_to_int() -> None:
32 """A float is truncated for an int annotation when conversion is allowed."""
33 assert parse_value("volume", 1.9, int, allow_value_convert=True) == 1
34
35
36@pytest.mark.parametrize(
37 ("value", "expected"),
38 [("true", True), ("1", True), (1, True), ("false", False), ("nope", False), (0, False)],
39)
40def test_string_and_int_converted_to_bool(value: str | int, expected: bool) -> None:
41 """A string or int is accepted for a bool annotation when conversion is allowed."""
42 assert parse_value("enabled", value, bool, allow_value_convert=True) is expected
43
44
45def test_no_conversion_without_allow_value_convert() -> None:
46 """A value of the wrong type is rejected while conversion is not allowed."""
47 with pytest.raises(TypeError, match="is invalid for volume"):
48 parse_value("volume", "5", int)
49
50
51def test_value_without_a_conversion_is_still_rejected() -> None:
52 """A value that no conversion applies to is rejected even when conversion is allowed."""
53 with pytest.raises(TypeError, match="is invalid for name"):
54 parse_value("name", 5, str, allow_value_convert=True)
55
56
57def test_parse_arguments_retries_with_conversion() -> None:
58 """Arguments that only fit after conversion are accepted by the retry."""
59
60 def _handler(volume: int, gain: float) -> None: ...
61
62 result = parse_arguments(
63 inspect.signature(_handler),
64 get_type_hints(_handler),
65 {"volume": "5", "gain": 1},
66 )
67 assert result == {"volume": 5, "gain": 1.0}
68
69
70def test_union_that_fits_no_member_raises() -> None:
71 """A value that fits no member of a union without None is rejected."""
72 with pytest.raises(TypeError, match="is invalid for values"):
73 parse_value("values", {}, int | str)
74
75
76def test_optional_union_that_fits_no_member_warns_and_returns_none(
77 caplog: pytest.LogCaptureFixture,
78) -> None:
79 """A value that fits no member of an optional union is logged and dropped."""
80 with caplog.at_level(logging.WARNING):
81 assert parse_value("values", {}, int | str | None) is None
82 assert "is invalid for values" in caplog.text
83