1# -*- coding: utf-8 -*-
2"""Implementation of nested form-data encoding function(s)."""
3from .._compat import basestring
4from .._compat import urlencode as _urlencode
5
6
7__all__ = ('urlencode',)
8
9
10def urlencode(query, *args, **kwargs):
11    """Handle nested form-data queries and serialize them appropriately.
12
13    There are times when a website expects a nested form data query to be sent
14    but, the standard library's urlencode function does not appropriately
15    handle the nested structures. In that case, you need this function which
16    will flatten the structure first and then properly encode it for you.
17
18    When using this to send data in the body of a request, make sure you
19    specify the appropriate Content-Type header for the request.
20
21    .. code-block:: python
22
23        import requests
24        from requests_toolbelt.utils import formdata
25
26        query = {
27           'my_dict': {
28               'foo': 'bar',
29               'biz': 'baz",
30            },
31            'a': 'b',
32        }
33
34        resp = requests.get(url, params=formdata.urlencode(query))
35        # or
36        resp = requests.post(
37            url,
38            data=formdata.urlencode(query),
39            headers={
40                'Content-Type': 'application/x-www-form-urlencoded'
41            },
42        )
43
44    Similarly, you can specify a list of nested tuples, e.g.,
45
46    .. code-block:: python
47
48        import requests
49        from requests_toolbelt.utils import formdata
50
51        query = [
52            ('my_list', [
53                ('foo', 'bar'),
54                ('biz', 'baz'),
55            ]),
56            ('a', 'b'),
57        ]
58
59        resp = requests.get(url, params=formdata.urlencode(query))
60        # or
61        resp = requests.post(
62            url,
63            data=formdata.urlencode(query),
64            headers={
65                'Content-Type': 'application/x-www-form-urlencoded'
66            },
67        )
68
69    For additional parameter and return information, see the official
70    `urlencode`_ documentation.
71
72    .. _urlencode:
73        https://docs.python.org/3/library/urllib.parse.html#urllib.parse.urlencode
74    """
75    expand_classes = (dict, list, tuple)
76    original_query_list = _to_kv_list(query)
77
78    if not all(_is_two_tuple(i) for i in original_query_list):
79        raise ValueError("Expected query to be able to be converted to a "
80                         "list comprised of length 2 tuples.")
81
82    query_list = original_query_list
83    while any(isinstance(v, expand_classes) for _, v in query_list):
84        query_list = _expand_query_values(query_list)
85
86    return _urlencode(query_list, *args, **kwargs)
87
88
89def _to_kv_list(dict_or_list):
90    if hasattr(dict_or_list, 'items'):
91        return list(dict_or_list.items())
92    return dict_or_list
93
94
95def _is_two_tuple(item):
96    return isinstance(item, (list, tuple)) and len(item) == 2
97
98
99def _expand_query_values(original_query_list):
100    query_list = []
101    for key, value in original_query_list:
102        if isinstance(value, basestring):
103            query_list.append((key, value))
104        else:
105            key_fmt = key + '[%s]'
106            value_list = _to_kv_list(value)
107            query_list.extend((key_fmt % k, v) for k, v in value_list)
108    return query_list
109