python3-10.qs

 1from .qs import *
 2
 3__all__ = [
 4    "list_to_dict",
 5    "is_valid_php_query_name",
 6    "merge_dict_in_list",
 7    "merge",
 8    "qs_parse",
 9    "build_qs",
10    "qs_parse_pairs",
11]
def list_to_dict(lst: list[typing.Any]) -> dict[int, typing.Any]:
12def list_to_dict(lst: list[Any]) -> dict[int, Any]:
13    """Maps a list to an equivalent dictionary
14
15    e.g: ["a","b","c"] -> {0:"a",1:"b",2:"c"}
16
17    Used to convert lists to PHP-style arrays
18
19    Args:
20        lst (list[Any]): The list to transform
21
22    Returns:
23        dict[int, Any]: The "PHP-style array" dict
24    """
25
26    return {i: value for i, value in enumerate(lst)}

Maps a list to an equivalent dictionary

e.g: ["a","b","c"] -> {0:"a",1:"b",2:"c"}

Used to convert lists to PHP-style arrays

Args: lst (list[Any]): The list to transform

Returns: dict[int, Any]: The "PHP-style array" dict

def is_valid_php_query_name(name: str) -> bool:
29def is_valid_php_query_name(name: str) -> bool:
30    """
31    Check if a given name follows PHP query string syntax.
32    This implementation assumes that names will be structured like:
33    field
34    field[key]
35    field[key1][key2]
36    field[]
37    """
38    pattern = r"""
39    ^               # Asserts the start of the line, it means to start matching from the beginning of the string.
40    [^\[\]&]+       # Matches one or more characters that are not `[`, `]`, or `&`. It describes the base key.
41    (               # Opens a group. This group is used to match any subsequent keys within brackets.
42    \[              # Matches a literal `[`, which is the start of a key.
43    [^\[\]&]*       # Matches zero or more characters that are not `[`, `]`, or `&`, which is the content of a key.
44    \]              # Matches a literal `]`, which is the end of a key.
45    )*              # Closes the group and asserts that the group can appear zero or more times, for nested keys.
46    $               # Asserts the end of the line, meaning the string should end with the preceding group.
47    """
48    return bool(re.match(pattern, name, re.VERBOSE))

Check if a given name follows PHP query string syntax. This implementation assumes that names will be structured like: field field[key] field[key1][key2] field[]

def merge_dict_in_list(source: dict, destination: list) -> list | dict:
125def merge_dict_in_list(source: dict, destination: list) -> list | dict:
126    """
127    Merge a dictionary into a list.
128
129    Only the values of integer keys from the dictionary are merged into the list.
130
131    If the dictionary contains only integer keys, returns a merged list.
132    If the dictionary contains other keys as well, returns a merged dict.
133
134    Args:
135        source (dict): The dictionary to merge.
136        destination (list): The list to merge.
137
138    Returns:
139        list | dict: Merged data.
140    """
141    # Retain only integer keys:
142    int_keys = sorted([key for key in source.keys() if isinstance(key, int)])
143    array_values = [source[key] for key in int_keys]
144    merged_array = array_values + destination
145
146    if len(int_keys) == len(source.keys()):
147        return merged_array
148
149    return merge(source, list_to_dict(merged_array))

Merge a dictionary into a list.

Only the values of integer keys from the dictionary are merged into the list.

If the dictionary contains only integer keys, returns a merged list. If the dictionary contains other keys as well, returns a merged dict.

Args: source (dict): The dictionary to merge. destination (list): The list to merge.

Returns: list | dict: Merged data.

def merge(source: dict | list, destination: dict | list, shallow: bool = True):
152def merge(source: dict | list, destination: dict | list, shallow: bool = True):
153    """
154    Merge the `source` and `destination`.
155    Performs a shallow or deep merge based on the `shallow` flag.
156    Args:
157        source (Any): The source data to merge.
158        destination (Any): The destination data to merge into.
159        shallow (bool): If True, perform a shallow merge. Defaults to True.
160    Returns:
161        Any: Merged data.
162    """
163    if not shallow:
164        source = deepcopy(source)
165        destination = deepcopy(destination)
166
167    match (source, destination):
168        case (list(), list()):
169            return source + destination
170        case (dict(), list()):
171            return merge_dict_in_list(source, destination)
172
173    items = cast(Mapping, source).items()
174    for key, value in items:
175        if isinstance(value, dict) and isinstance(destination, dict):
176            # get node or create one
177            node = destination.setdefault(key, {})
178            node = merge(value, node)
179            destination[key] = node
180        else:
181            if (
182                isinstance(value, list) or isinstance(value, tuple)
183            ) and key in destination:
184                value = merge(destination[key], list(value))
185
186            if isinstance(key, str) and isinstance(destination, list):
187                destination = list_to_dict(
188                    destination
189                )  # << WRITE TEST THAT WILL REACH THIS LINE
190
191            cast(dict, destination)[key] = value
192    return destination

Merge the source and destination. Performs a shallow or deep merge based on the shallow flag. Args: source (Any): The source data to merge. destination (Any): The destination data to merge into. shallow (bool): If True, perform a shallow merge. Defaults to True. Returns: Any: Merged data.

def qs_parse( qs: str, keep_blank_values: bool = True, strict_parsing: bool = False) -> dict:
195def qs_parse(
196    qs: str, keep_blank_values: bool = True, strict_parsing: bool = False
197) -> dict:
198    """
199    Parses a query string using PHP's nesting syntax, and returns a dict.
200
201    Args:
202        qs (str): The query string to parse.
203        keep_blank_values (bool): If True, includes keys with blank values. Defaults to True.
204        strict_parsing (bool): If True, raises ValueError on any errors. Defaults to False.
205
206    Returns:
207        dict: A dictionary representing the parsed query string.
208    """
209
210    tokens = {}
211    pairs = [
212        pair for query_segment in qs.split("&") for pair in query_segment.split(";")
213    ]
214
215    for name_val in pairs:
216        if not name_val and not strict_parsing:
217            continue
218        nv = name_val.split("=")
219
220        if len(nv) != 2:
221            if strict_parsing:
222                raise ValueError(f"Bad query field: {name_val}")
223            # Handle case of a control-name with no equal sign
224            if keep_blank_values:
225                nv.append("")
226            else:
227                continue
228
229        if len(nv[1]) or keep_blank_values:
230            _get_name_value(tokens, nv[0], nv[1], urlencoded=True)
231
232    return tokens

Parses a query string using PHP's nesting syntax, and returns a dict.

Args: qs (str): The query string to parse. keep_blank_values (bool): If True, includes keys with blank values. Defaults to True. strict_parsing (bool): If True, raises ValueError on any errors. Defaults to False.

Returns: dict: A dictionary representing the parsed query string.

def build_qs(query: Mapping) -> str:
235def build_qs(query: Mapping) -> str:
236    """
237    Build a query string from a dictionary or list of 2-tuples.
238    Coerces data types before serialization.
239    Args:
240        query (Mapping): The query data to build the string from.
241    Returns:
242        str: A query string.
243    """
244
245    def dict_generator(indict, pre=None):
246        pre = pre[:] if pre else []
247        if isinstance(indict, dict):
248            for key, value in indict.items():
249                if isinstance(value, dict):
250                    for d in dict_generator(value, pre + [key]):
251                        yield d
252                else:
253                    yield pre + [key, value]
254        else:
255            yield indict
256
257    paths = [i for i in dict_generator(query)]
258    qs = []
259
260    for path in paths:
261        names = path[:-1]
262        value = path[-1]
263        s: list[str] = []
264        for i, n in enumerate(names):
265            n = f"[{n}]" if i > 0 else str(n)
266            s.append(n)
267
268        match value:
269            case list() | tuple():
270                for v in value:
271                    multi = s[:]
272                    if not s[-1].endswith("[]"):
273                        multi.append("[]")
274                    multi.append("=")
275                    # URLEncode value
276                    multi.append(quote_plus(str(v)))
277                    qs.append("".join(multi))
278            case _:
279                s.append("=")
280                # URLEncode value
281                s.append(quote_plus(str(value)))
282                qs.append("".join(s))
283
284    return "&".join(qs)

Build a query string from a dictionary or list of 2-tuples. Coerces data types before serialization. Args: query (Mapping): The query data to build the string from. Returns: str: A query string.

def qs_parse_pairs( pairs: Sequence[tuple[str, str] | tuple[str]], keep_blank_values: bool = True, strict_parsing: bool = False) -> dict:
287def qs_parse_pairs(
288    pairs: Sequence[tuple[str, str] | tuple[str]],
289    keep_blank_values: bool = True,
290    strict_parsing: bool = False,
291) -> dict:
292    """
293    Parses a list of key/value pairs and returns a dict.
294
295    Args:
296        pairs (list[tuple[str, str]]): The list of key/value pairs.
297        keep_blank_values (bool): If True, includes keys with blank values. Defaults to True.
298        strict_parsing (bool): If True, raises ValueError on any errors. Defaults to False.
299
300    Returns:
301        dict: A dictionary representing the parsed pairs.
302    """
303
304    tokens = {}
305
306    for name_val in pairs:
307        if not name_val and not strict_parsing:
308            continue
309        nv = name_val
310
311        if len(nv) != 2:
312            if strict_parsing:
313                raise ValueError(f"Bad query field: {name_val}")
314            # Handle case of a control-name with no equal sign
315            if keep_blank_values:
316                nv = (nv[0], "")
317            else:
318                continue
319
320        if len(nv[1]) or keep_blank_values:
321            _get_name_value(tokens, nv[0], nv[1], False)
322
323    return tokens

Parses a list of key/value pairs and returns a dict.

Args: pairs (list[tuple[str, str]]): The list of key/value pairs. keep_blank_values (bool): If True, includes keys with blank values. Defaults to True. strict_parsing (bool): If True, raises ValueError on any errors. Defaults to False.

Returns: dict: A dictionary representing the parsed pairs.