/
/
/
1"""Tests for collection type handling in API argument parsing."""
2
3from __future__ import annotations
4
5from typing import Any
6
7import pytest
8from music_assistant_models.enums import DashboardType
9
10from music_assistant.helpers.api import parse_value
11
12
13def test_parse_value_none_for_optional_dict() -> None:
14 """A None value is returned as-is when the annotation is dict[str, Any] | None."""
15 result = parse_value("supported_types", None, dict[str, Any] | None)
16 assert result is None
17
18
19def test_parse_value_none_for_optional_list() -> None:
20 """A None value is returned as-is when the annotation is list[str] | None."""
21 result = parse_value("values", None, list[str] | None)
22 assert result is None
23
24
25def test_parse_value_dict_for_optional_dict() -> None:
26 """A real dict is still parsed correctly when the annotation is dict[str, Any] | None."""
27 result = parse_value("supported_types", {"a": 1}, dict[str, Any] | None)
28 assert result == {"a": 1}
29
30
31def test_parse_value_set_of_enum() -> None:
32 """A json list is parsed into a set when the annotation is set[Enum]."""
33 result = parse_value("supported_types", ["party"], set[DashboardType])
34 assert result == {DashboardType.PARTY}
35
36
37def test_parse_value_optional_set_of_enum() -> None:
38 """A json list is parsed into a set for an optional set[Enum] annotation."""
39 result = parse_value("supported_types", ["party", "now_playing"], set[DashboardType] | None)
40 assert result == {DashboardType.PARTY, DashboardType.NOW_PLAYING}
41
42
43def test_parse_value_frozenset_of_str() -> None:
44 """A json list is parsed into a frozenset when the annotation is frozenset[str]."""
45 result = parse_value("values", ["a", "b"], frozenset[str])
46 assert result == frozenset({"a", "b"})
47
48
49def test_parse_value_none_members_dropped_from_list() -> None:
50 """None members are dropped when the element type of a list does not admit None."""
51 result = parse_value("values", ["a", None, "b"], list[str])
52 assert result == ["a", "b"]
53
54
55def test_parse_value_none_members_kept_for_optional_element_type() -> None:
56 """None members are kept when the element type of a list admits None."""
57 result = parse_value("values", ["a", None, "b"], list[str | None])
58 assert result == ["a", None, "b"]
59
60
61def test_parse_value_none_member_kept_in_set_of_optional() -> None:
62 """A None member is kept for a set with an element type that admits None."""
63 result = parse_value("values", ["a", None], set[str | None])
64 assert result == {"a", None}
65
66
67def test_parse_value_none_member_kept_in_variadic_tuple_of_optional() -> None:
68 """A None member is kept for a variadic tuple with an element type that admits None."""
69 result = parse_value("values", ["a", None, "b"], tuple[str | None, ...])
70 assert result == ("a", None, "b")
71
72
73def test_parse_value_fixed_length_tuple_per_position_types() -> None:
74 """Each member of a fixed-length tuple is parsed against the type of its own position."""
75 result = parse_value("values", ["a", 1, "party"], tuple[str, int, DashboardType])
76 assert result == ("a", 1, DashboardType.PARTY)
77
78
79def test_parse_value_fixed_length_tuple_keeps_none_members() -> None:
80 """A None member of a fixed-length tuple is kept, so the tuple keeps its length."""
81 result = parse_value("values", ["a", "name", None], tuple[str, str, str | None])
82 assert result == ("a", "name", None)
83
84
85def test_parse_value_list_of_fixed_length_tuples() -> None:
86 """A json list of lists is parsed into tuples that each keep their None members."""
87 result = parse_value(
88 "stations",
89 [["t1", "Name A", "http://img"], ["t2", "Name B", None]],
90 list[tuple[str, str, str | None]],
91 )
92 assert result == [("t1", "Name A", "http://img"), ("t2", "Name B", None)]
93
94
95def test_parse_value_variadic_tuple() -> None:
96 """A json list is parsed into a tuple of arbitrary length for a variadic annotation."""
97 result = parse_value("values", ["a", "b", "c"], tuple[str, ...])
98 assert result == ("a", "b", "c")
99
100
101def test_parse_value_fixed_length_tuple_wrong_length() -> None:
102 """A value with the wrong number of members is rejected for a fixed-length tuple."""
103 with pytest.raises(TypeError, match="is invalid for values"):
104 parse_value("values", ["a"], tuple[str, str])
105
106
107def test_parse_value_fixed_length_tuple_names_the_failing_member() -> None:
108 """A member that does not fit its type is reported with its position in the tuple."""
109 with pytest.raises(TypeError, match=r"invalid for values\[1\]"):
110 parse_value("values", ["a", {}], tuple[str, str])
111
112
113def test_parse_value_fixed_length_tuple_in_union() -> None:
114 """A union of fixed-length tuples falls through to the member that fits the value."""
115 result = parse_value("values", ["a", "b", "c"], tuple[str, str] | tuple[str, str, str])
116 assert result == ("a", "b", "c")
117
118
119def test_parse_value_dict_names_the_failing_value() -> None:
120 """A dict value that does not fit its type is reported with its argument and key."""
121 with pytest.raises(TypeError, match=r"invalid for supported_types\[a\]"):
122 parse_value("supported_types", {"a": {}}, dict[str, int])
123
124
125def test_parse_value_dict_names_the_failing_key() -> None:
126 """A dict key that does not fit its type is reported with the argument it belongs to."""
127 with pytest.raises(TypeError, match="invalid for supported_types key"):
128 parse_value("supported_types", {1: "a"}, dict[str, str])
129