From c4dcd448d0579aeab4e740aa295c577ef19a6a01 Mon Sep 17 00:00:00 2001 From: oyeong011 Date: Tue, 8 Sep 2026 11:51:41 +0900 Subject: [PATCH] Append new query parameters after existing parameters Existing parameter replacements retain their first position; new names no longer use a negative insertion index. Constraint: Preserve Python 2-compatible source style and both URL types Confidence: high Scope-risk: narrow Tested: 137 tests and doctests; actual URL and DecodedURL calls including 10000 query entries Not-tested: Historical Python interpreters --- src/hyperlink/_url.py | 4 ++-- src/hyperlink/test/test_url.py | 31 ++++++++++++++++++++++++++----- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/hyperlink/_url.py b/src/hyperlink/_url.py index 8797b5cc..452cc673 100644 --- a/src/hyperlink/_url.py +++ b/src/hyperlink/_url.py @@ -1904,7 +1904,7 @@ def set(self, name, value=None): # Preserve the original position of the query key in the list q = [(k, v) for (k, v) in self.query if k != name] idx = next( - (i for (i, (k, v)) in enumerate(self.query) if k == name), -1 + (i for (i, (k, v)) in enumerate(self.query) if k == name), len(q) ) q[idx:idx] = [(name, value)] return self.replace(query=q) @@ -2315,7 +2315,7 @@ def set(self, name, value=None): "Return a new DecodedURL with query parameter *name* set to *value*" query = self.query q = [(k, v) for (k, v) in query if k != name] - idx = next((i for (i, (k, v)) in enumerate(query) if k == name), -1) + idx = next((i for (i, (k, v)) in enumerate(query) if k == name), len(q)) q[idx:idx] = [(name, value)] return self.replace(query=q) diff --git a/src/hyperlink/test/test_url.py b/src/hyperlink/test/test_url.py index 37c91726..9dd8b8a6 100644 --- a/src/hyperlink/test/test_url.py +++ b/src/hyperlink/test/test_url.py @@ -10,7 +10,7 @@ from typing import Any, Iterable, Optional, Text, Tuple, cast from .common import HyperlinkTestCase -from .. import URL, URLParseError +from .. import URL, DecodedURL, URLParseError from .._url import inet_pton, SCHEME_PORT_MAP @@ -1317,13 +1317,34 @@ def test_twisted_compat(self): def test_set_ordering(self): # type: () -> None - # TODO url = URL.from_text("http://example.com/?a=b&c") url = url.set("x", "x") url = url.add("x", "y") - assert url.to_text() == "http://example.com/?a=b&x=x&c&x=y" - # Would expect: - # assert url.to_text() == u'http://example.com/?a=b&c&x=x&x=y' + assert url.to_text() == "http://example.com/?a=b&c&x=x&x=y" + + def test_set_new_query_parameter(self): + # type: () -> None + """Both URL types append new parameters without moving existing ones.""" + for url_type in (URL, DecodedURL): + for query in ((), (("a", "1"),), (("a", "1"), ("b", None))): + url = url_type.from_text("https://example.com/").replace( + query=query + ) + result = url.set("new", "value") + self.assertEqual(result.query, query + (("new", "value"),)) + self.assertEqual(url.query, query) + + def test_set_existing_query_parameter(self): + # type: () -> None + """Replacing duplicate parameters retains the first occurrence's position.""" + for url_type in (URL, DecodedURL): + url = url_type.from_text( + "https://example.com/?a=1&x=old&b=2&x=other" + ) + result = url.set("x", "new") + self.assertEqual( + result.query, (("a", "1"), ("x", "new"), ("b", "2")) + ) def test_schemeless_path(self): # type: () -> None