From 03e634eaa7837209dc60065ece85bb3e54b8770b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 17 Oct 2024 21:01:26 -1000 Subject: [PATCH] Avoid multiple conversions in `URL.update_query` with passing a `Mapping` or `Sequence` (#1327) --- CHANGES/1327.misc.rst | 1 + yarl/_url.py | 54 +++++++++++++++++++++++++++++++++---------- 2 files changed, 43 insertions(+), 12 deletions(-) create mode 120000 CHANGES/1327.misc.rst diff --git a/CHANGES/1327.misc.rst b/CHANGES/1327.misc.rst new file mode 120000 index 000000000..213232627 --- /dev/null +++ b/CHANGES/1327.misc.rst @@ -0,0 +1 @@ +1309.misc.rst \ No newline at end of file diff --git a/yarl/_url.py b/yarl/_url.py index 78f81d229..a631f0093 100644 --- a/yarl/_url.py +++ b/yarl/_url.py @@ -1365,9 +1365,8 @@ def _get_str_query(cls, *args: Any, **kwargs: Any) -> Union[str, None]: query: Union[str, Mapping[str, QueryVariable], None] if kwargs: if args: - raise ValueError( - "Either kwargs or single query parameter must be present" - ) + msg = "Either kwargs or single query parameter must be present" + raise ValueError(msg) query = kwargs elif len(args) == 1: query = args[0] @@ -1383,16 +1382,14 @@ def _get_str_query(cls, *args: Any, **kwargs: Any) -> Union[str, None]: if isinstance(query, str): return cls._QUERY_QUOTER(query) if isinstance(query, (bytes, bytearray, memoryview)): - raise TypeError( - "Invalid query type: bytes, bytearray and memoryview are forbidden" - ) + msg = "Invalid query type: bytes, bytearray and memoryview are forbidden" + raise TypeError(msg) if isinstance(query, Sequence): # We don't expect sequence values if we're given a list of pairs # already; only mappings like builtin `dict` which can't have the # same key pointing to multiple values are allowed to use # `_query_seq_pairs`. return cls._get_str_query_from_iterable(query) - raise TypeError( "Invalid query type: only str, mapping or " "sequence of (key, value) pairs is allowed" @@ -1469,13 +1466,46 @@ def update_query(self, *args: Any, **kwargs: Any) -> "URL": >>> url.update_query(a=3, c=4) URL('http://example.com/?a=3&b=2&c=4') """ - scheme, netloc, path, _, fragment = self._val - if (s := self._get_str_query(*args, **kwargs)) is None: + in_query: Union[str, Mapping[str, QueryVariable], None] + if kwargs: + if args: + msg = "Either kwargs or single query parameter must be present" + raise ValueError(msg) + in_query = kwargs + elif len(args) == 1: + in_query = args[0] + else: + raise ValueError("Either kwargs or single query parameter must be present") + + scheme, netloc, path, query, fragment = self._val + if in_query is None: query = "" + elif not in_query: + pass + elif isinstance(in_query, Mapping): + qm: MultiDict[QueryVariable] = MultiDict(self._parsed_query) + qm.update(in_query) + query = self._get_str_query_from_sequence_iterable(qm.items()) + elif isinstance(in_query, str): + qstr: MultiDict[str] = MultiDict(self._parsed_query) + qstr.update(parse_qsl(in_query, keep_blank_values=True)) + query = self._get_str_query_from_iterable(qstr.items()) + elif isinstance(in_query, (bytes, bytearray, memoryview)): + msg = "Invalid query type: bytes, bytearray and memoryview are forbidden" + raise TypeError(msg) + elif isinstance(in_query, Sequence): + # We don't expect sequence values if we're given a list of pairs + # already; only mappings like builtin `dict` which can't have the + # same key pointing to multiple values are allowed to use + # `_query_seq_pairs`. + qs: MultiDict[SimpleQuery] = MultiDict(self._parsed_query) + qs.update(in_query) + query = self._get_str_query_from_iterable(qs.items()) else: - q_dict = MultiDict(self._parsed_query) - q_dict.update(parse_qsl(s, keep_blank_values=True)) - query = self._get_str_query_from_iterable(q_dict.items()) + raise TypeError( + "Invalid query type: only str, mapping or " + "sequence of (key, value) pairs is allowed" + ) return self._from_val( tuple.__new__(SplitResult, (scheme, netloc, path, query, fragment)) )