diff --git a/tests/test_uritemplate.py b/tests/test_uritemplate.py index dfe19f5..e23a4e0 100644 --- a/tests/test_uritemplate.py +++ b/tests/test_uritemplate.py @@ -665,5 +665,68 @@ def test_expand(self) -> None: self.assertEqual(expand("{/zero,a_float}", self.context), "/0/3.1415") +class TestNoneListValues(unittest.TestCase): + def test_expansion_ignores_none_list_items_for_all_operators( + self, + ) -> None: + values = {"x": ["a", None, "b"]} + expected = { + "{x}": "a,b", + "{x*}": "a,b", + "{+x}": "a,b", + "{+x*}": "a,b", + "{#x}": "#a,b", + "{#x*}": "#a,b", + "{.x}": ".a,b", + "{.x*}": ".a.b", + "{/x}": "/a,b", + "{/x*}": "/a/b", + "{;x}": ";x=a,b", + "{;x*}": ";x=a;x=b", + "{?x}": "?x=a,b", + "{?x*}": "?x=a&x=b", + "{&x}": "&x=a,b", + "{&x*}": "&x=a&x=b", + } + + for template, result in expected.items(): + with self.subTest(template=template): + self.assertEqual(URITemplate(template).expand(values), result) + + def test_expansion_preserves_defined_empty_and_zero_list_items( + self, + ) -> None: + values = {"x": ["", None, 0, None, "b"]} + expected = { + "{x}": ",0,b", + "{x*}": ",0,b", + "{+x}": ",0,b", + "{+x*}": ",0,b", + "{#x}": "#,0,b", + "{#x*}": "#,0,b", + "{.x}": ".,0,b", + "{.x*}": "..0.b", + "{/x}": "/,0,b", + "{/x*}": "//0/b", + "{;x}": ";x=,0,b", + "{;x*}": ";x=;x=0;x=b", + "{?x}": "?x=,0,b", + "{?x*}": "?x=&x=0&x=b", + "{&x}": "&x=,0,b", + "{&x*}": "&x=&x=0&x=b", + } + + for template, result in expected.items(): + with self.subTest(template=template): + self.assertEqual(URITemplate(template).expand(values), result) + + def test_expansion_does_not_mutate_none_containing_lists(self) -> None: + values = {"x": ["a", None, "b"]} + for template in ("{x}", "{.x}", "{;x}", "{?x}"): + with self.subTest(template=template): + URITemplate(template).expand(values) + self.assertEqual(values, {"x": ["a", None, "b"]}) + + if __name__ == "__main__": unittest.main() diff --git a/uritemplate/variable.py b/uritemplate/variable.py index 1f7993c..638a866 100644 --- a/uritemplate/variable.py +++ b/uritemplate/variable.py @@ -284,9 +284,10 @@ def _query_expansion( safe = self.operator.reserved_characters() _quote = self.operator.quote if list_test(value) and not tuples: + value = t.cast(t.Sequence[ScalarVariableValue], value) + value = [v for v in value if v is not None] if not value: return None - value = t.cast(t.Sequence[ScalarVariableValue], value) if explode: return self.operator.expansion_separator().join( f"{name}={_quote(v)}" for v in value @@ -340,13 +341,12 @@ def _label_path_expansion( tuples, items = is_list_of_tuples(value) if list_test(value) and not tuples: + value = t.cast(t.Sequence[ScalarVariableValue], value) + value = [v for v in value if v is not None] if not explode: join_str = "," - value = t.cast(t.Sequence[ScalarVariableValue], value) - fragments = [ - self.operator.quote(v) for v in value if v is not None - ] + fragments = [self.operator.quote(v) for v in value] return join_str.join(fragments) if fragments else None if dict_test(value) or tuples: @@ -386,9 +386,10 @@ def _semi_path_expansion( if list_test(value) and not tuples: value = t.cast(t.Sequence[ScalarVariableValue], value) + value = [v for v in value if v is not None] if explode: expanded = join_str.join( - f"{name}={quote(v, safe)}" for v in value if v is not None + f"{name}={quote(v, safe)}" for v in value ) return expanded if expanded else None else: @@ -434,6 +435,7 @@ def _string_expansion( if list_test(value) and not tuples: value = t.cast(t.Sequence[ScalarVariableValue], value) + value = [v for v in value if v is not None] return ",".join(self.operator.quote(v) for v in value) if dict_test(value) or tuples: