├── .gitignore ├── .travis.yml ├── LICENSE ├── README.rst ├── querystring_parser ├── __init__.py ├── builder.py ├── parser.py └── tests.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | *.egg-info 4 | MANIFEST 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.3" 5 | install: 6 | script: 7 | - python querystring_parser/tests.py 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2011 Bernard Kobos 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =================== 2 | querystring-parser 3 | =================== 4 | 5 | .. image:: https://travis-ci.org/bernii/querystring-parser.svg?branch=master 6 | :target: https://travis-ci.org/bernii/querystring-parser 7 | 8 | This repository hosts the query string parser for Python/Django projects that correctly creates nested dictionaries from sent form/querystring data. 9 | 10 | When to use it? 11 | ================ 12 | 13 | Lets say you have some textfields on your webpage that you wish to get as dictionary on the backend. The querystring could look like: 14 | :: 15 | section[1]['words'][2]=a§ion[0]['words'][2]=a§ion[0]['words'][2]=b 16 | 17 | Standard django REQUEST (QueryDict) variable will contain: 18 | :: 19 | 20 | 21 | As you see it doesn't really convert it to dict. Instead of elegant dictionary you have a string called "section[1]['words'][2]" and "section[0]['words'][2]" and if you want to do something with it, you'll need to parse it (sic!). 22 | 23 | When using querystring-parser the output will look like: 24 | :: 25 | {u'section': {0: {u'words': {2: [u'a', u'b']}}, 1: {u'words': {2: u'a'}}}} 26 | 27 | Tadam! Everything is much simpler and more beautiful now :) 28 | 29 | Efficiency: 30 | ============ 31 | 32 | Test made using timeit show that in most cases speed of created library is similar to standard Django QueryDict parsing speed. For query string containing multidimensional complicated arrays querystring-parser is significantly slower. This is totally understandable as created library creates nested dictionaries in contrary to standard Django function which only tokenizes data. You can see results below. 33 | Edit: Actually parsing is done by urlparse.parse_qs so I've added it to tests. 34 | 35 | :: 36 | 37 | Test string nr querystring-parser Django QueryDict parse_qs 38 | 0 2.75077319145 3.44334220886 0.582501888275 39 | Test string nr querystring-parser Django QueryDict parse_qs 40 | 1 10.1889920235 10.2983090878 2.08930182457 41 | Test string nr querystring-parser Django QueryDict parse_qs 42 | 2 0.613747119904 1.21649289131 0.283004999161 43 | Test string nr querystring-parser Django QueryDict parse_qs 44 | 3 0.107316017151 0.459388017654 0.0687718391418 45 | Test string nr querystring-parser Django QueryDict parse_qs 46 | 4 0.00291299819946 0.169251918793 0.0170118808746 47 | 48 | 49 | Test #1 Is most interesting as is contains nested dictionaries in query string. 50 | 51 | Installation: 52 | ============ 53 | 54 | Install using pip. 55 | :: 56 | pip install querystring-parser 57 | 58 | How to use: 59 | ============ 60 | 61 | Just add it to your Django project and start using it. 62 | :: 63 | from querystring_parser import parser 64 | post_dict = parser.parse(request.POST.urlencode()) 65 | 66 | License: 67 | ========= 68 | 69 | * MIT License 70 | -------------------------------------------------------------------------------- /querystring_parser/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernii/querystring-parser/e711e70a5234820f778342ba75c8e8f30322f22c/querystring_parser/__init__.py -------------------------------------------------------------------------------- /querystring_parser/builder.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | ''' 3 | Created on 2012-03-28 4 | 5 | @author: Tomasz 'Doppler' Najdek 6 | 7 | Updated 2012-04-01 Bernard 'berni' Kobos 8 | ''' 9 | 10 | 11 | try: 12 | # for Python3 13 | import urllib.parse as urllib 14 | except ImportError: 15 | # for Python2 16 | import urllib 17 | 18 | try: 19 | unicode 20 | except NameError: 21 | # for Python3 22 | unicode = str 23 | 24 | 25 | def build(item, encoding=None): 26 | def recursion(item, base=None): 27 | pairs = list() 28 | if(hasattr(item, 'values')): 29 | for key, value in item.items(): 30 | if encoding: 31 | quoted_key = urllib.quote(unicode(key).encode(encoding)) 32 | else: 33 | quoted_key = urllib.quote(unicode(key)) 34 | if(base): 35 | new_base = "%s[%s]" % (base, quoted_key) 36 | pairs += recursion(value, new_base) 37 | else: 38 | new_base = quoted_key 39 | pairs += recursion(value, new_base) 40 | elif(isinstance(item, list)): 41 | for (index, value) in enumerate(item): 42 | if(base): 43 | new_base = "%s" % (base) 44 | pairs += recursion(value, new_base) 45 | else: 46 | pairs += recursion(value) 47 | else: 48 | if encoding: 49 | quoted_item = urllib.quote(unicode(item).encode(encoding)) 50 | else: 51 | quoted_item = urllib.quote(unicode(item)) 52 | if(base): 53 | pairs.append("%s=%s" % (base, quoted_item)) 54 | else: 55 | pairs.append(quoted_item) 56 | return pairs 57 | return '&'.join(recursion(item)) 58 | -------------------------------------------------------------------------------- /querystring_parser/parser.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | ''' 3 | Created on 2011-05-12 4 | 5 | @author: berni 6 | ''' 7 | 8 | import sys 9 | import six 10 | 11 | try: 12 | # for Python3 13 | import urllib.parse as urllib 14 | except ImportError: 15 | # for Python2 16 | import urllib 17 | 18 | try: 19 | unicode 20 | DEFAULT_ENCODING = 'utf-8' 21 | except NameError: 22 | # for Python3 23 | unicode = str 24 | DEFAULT_ENCODING = None 25 | 26 | 27 | 28 | def has_variable_name(s): 29 | ''' 30 | Variable name before [ 31 | @param s: 32 | ''' 33 | if s.find("[") > 0: 34 | return True 35 | 36 | 37 | def more_than_one_index(s, brackets=2): 38 | ''' 39 | Search for two sets of [] [] 40 | @param s: string to search 41 | ''' 42 | start = 0 43 | brackets_num = 0 44 | while start != -1 and brackets_num < brackets: 45 | start = s.find("[", start) 46 | if start == -1: 47 | break 48 | start = s.find("]", start) 49 | brackets_num += 1 50 | if start != -1: 51 | return True 52 | return False 53 | 54 | 55 | def get_key(s): 56 | ''' 57 | Get data between [ and ] remove ' if exist 58 | @param s: string to process 59 | ''' 60 | start = s.find("[") 61 | end = s.find("]") 62 | if start == -1 or end == -1: 63 | return None 64 | if s[start + 1] == "'": 65 | start += 1 66 | if s[end - 1] == "'": 67 | end -= 1 68 | return s[start + 1:end] # without brackets 69 | 70 | 71 | def is_number(s): 72 | ''' 73 | Check if s is an int (for indexes in dict) 74 | @param s: string to check 75 | ''' 76 | if len(s) > 0 and s[0] in ('-', '+'): 77 | return s[1:].isdigit() 78 | return s.isdigit() 79 | 80 | 81 | class MalformedQueryStringError(Exception): 82 | ''' 83 | Query string is malformed, can't parse it :( 84 | ''' 85 | pass 86 | 87 | 88 | def parser_helper(key, val): 89 | ''' 90 | Helper for parser function 91 | @param key: 92 | @param val: 93 | ''' 94 | start_bracket = key.find("[") 95 | end_bracket = key.find("]") 96 | pdict = {} 97 | if has_variable_name(key): # var['key'][3] 98 | pdict[key[:key.find("[")]] = parser_helper(key[start_bracket:], val) 99 | elif more_than_one_index(key): # ['key'][3] 100 | newkey = get_key(key) 101 | newkey = int(newkey) if is_number(newkey) else newkey 102 | pdict[newkey] = parser_helper(key[end_bracket + 1:], val) 103 | else: # key = val or ['key'] 104 | newkey = key 105 | if start_bracket != -1: # ['key'] 106 | newkey = get_key(key) 107 | if newkey is None: 108 | raise MalformedQueryStringError 109 | newkey = int(newkey) if is_number(newkey) else newkey 110 | if key == u'[]': # val is the array key 111 | val = int(val) if is_number(val) else val 112 | pdict[newkey] = val 113 | return pdict 114 | 115 | def parse(query_string, unquote=True, normalized=False, encoding=DEFAULT_ENCODING): 116 | ''' 117 | Main parse function 118 | @param query_string: 119 | @param unquote: unquote html query string ? 120 | @param encoding: An optional encoding used to decode the keys and values. Defaults to utf-8, which the W3C declares as a default in the W3C algorithm for encoding. 121 | @see http://www.w3.org/TR/html5/forms.html#application/x-www-form-urlencoded-encoding-algorithm 122 | 123 | @param normalized: parse number key in dict to proper list ? 124 | ''' 125 | 126 | mydict = {} 127 | plist = [] 128 | if query_string == "": 129 | return mydict 130 | 131 | if type(query_string) == bytes: 132 | query_string = query_string.decode() 133 | 134 | for element in query_string.split("&"): 135 | try: 136 | if unquote: 137 | (var, val) = element.split("=") 138 | if sys.version_info[0] == 2: 139 | var = var.encode('ascii') 140 | val = val.encode('ascii') 141 | var = urllib.unquote_plus(var) 142 | val = urllib.unquote_plus(val) 143 | else: 144 | (var, val) = element.split("=") 145 | except ValueError: 146 | raise MalformedQueryStringError 147 | if encoding: 148 | var = var.decode(encoding) 149 | val = val.decode(encoding) 150 | plist.append(parser_helper(var, val)) 151 | for di in plist: 152 | (k, v) = di.popitem() 153 | tempdict = mydict 154 | while k in tempdict and type(v) is dict: 155 | tempdict = tempdict[k] 156 | (k, v) = v.popitem() 157 | if k in tempdict and type(tempdict[k]).__name__ == 'list': 158 | tempdict[k].append(v) 159 | elif k in tempdict: 160 | tempdict[k] = [tempdict[k], v] 161 | else: 162 | tempdict[k] = v 163 | 164 | if normalized == True: 165 | return _normalize(mydict) 166 | return mydict 167 | 168 | 169 | def _normalize(d): 170 | ''' 171 | The above parse function generates output of list in dict form 172 | i.e. {'abc' : {0: 'xyz', 1: 'pqr'}}. This function normalize it and turn 173 | them into proper data type, i.e. {'abc': ['xyz', 'pqr']} 174 | 175 | Note: if dict has element starts with 10, 11 etc.. this function won't fill 176 | blanks. 177 | for eg: {'abc': {10: 'xyz', 12: 'pqr'}} will convert to 178 | {'abc': ['xyz', 'pqr']} 179 | ''' 180 | newd = {} 181 | if isinstance(d, dict) == False: 182 | return d 183 | # if dictionary. iterate over each element and append to newd 184 | for k, v in six.iteritems(d): 185 | if isinstance(v, dict): 186 | first_key = next(iter(six.viewkeys(v))) 187 | if isinstance(first_key, int): 188 | temp_new = [] 189 | for k1, v1 in v.items(): 190 | temp_new.append(_normalize(v1)) 191 | newd[k] = temp_new 192 | elif first_key == '': 193 | newd[k] = v.values()[0] 194 | else: 195 | newd[k] = _normalize(v) 196 | else: 197 | newd[k] = v 198 | return newd 199 | 200 | 201 | if __name__ == '__main__': 202 | """Compare speed with Django QueryDict""" 203 | from timeit import Timer 204 | from tests import KnownValues 205 | import os 206 | import sys 207 | from django.core.management import setup_environ 208 | # Add project dir so Djnago project settings is in the scope 209 | LIB_PATH = os.path.abspath('..') 210 | sys.path.append(LIB_PATH) 211 | import settings 212 | setup_environ(settings) 213 | 214 | i = 0 215 | for key, val in KnownValues.knownValues: 216 | statement = "parse(\"%s\")" % key 217 | statementd = "http.QueryDict(\"%s\")" % key 218 | statementqs = "parse_qs(\"%s\")" % key 219 | t = Timer(statement, "from __main__ import parse") 220 | td = Timer(statementd, "from django import http") 221 | tqs = Timer(statementqs, "from urlparse import parse_qs") 222 | print ("Test string nr ".ljust(15), "querystring-parser".ljust(22), "Django QueryDict".ljust(22), "parse_qs") 223 | print (str(i).ljust(15), str(min(t.repeat(3, 10000))).ljust(22), str(min(td.repeat(3, 10000))).ljust(22), min(tqs.repeat(3, 10000))) 224 | i += 1 225 | -------------------------------------------------------------------------------- /querystring_parser/tests.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | ''' 3 | Created on 2011-05-13 4 | 5 | @author: berni 6 | 7 | Updated 2012-03-28 Tomasz 'Doppler' Najdek 8 | Updated 2012-09-24 Bernard 'berni' Kobos 9 | ''' 10 | 11 | import sys 12 | from parser import parse, MalformedQueryStringError 13 | from builder import build 14 | import unittest 15 | 16 | 17 | class KnownValues(unittest.TestCase): 18 | ''' 19 | Test output for known query string values 20 | ''' 21 | knownValuesClean = ( 22 | ({u'omg': {0: u'0001212'}}), 23 | ( 24 | # "packetname=fd§ion[0]['words'][2]=§ion[0]['words'][2]=&language=1&packetdesc=sdfsd&newlanguage=proponowany jezyk..&newsectionname=§ion[0]['words'][1]=§ion[0]['words'][1]=&packettype=radio§ion[0]['words'][0]=sdfsd§ion[0]['words'][0]=ds", 25 | {u"packetname": u"fd", u"section": {0: {u"words": {0: [u"sdfsd", u"ds"], 1: [u"", u""], 2: [u"", u""]}}}, u"language": u"1", u"packetdesc": u"sdfsd", u"newlanguage": u"proponowany jezyk..", u"newsectionname": u"", u"packettype": u"radio"} 26 | ), 27 | ( 28 | # "language=1&newlanguage=proponowany jezyk..&newsectionname=&packetdesc=Zajebiste slowka na jutrzejszy sprawdzian z chemii&packetid=4&packetname=Chemia spr&packettype=1§ion[10]['name']=sekcja siatkarska§ion[10]['words'][-1]=§ion[10]['words'][-1]=§ion[10]['words'][-2]=§ion[10]['words'][-2]=§ion[10]['words'][30]=noga§ion[10]['words'][30]=leg§ion[11]['del_words'][32]=kciuk§ion[11]['del_words'][32]=thimb§ion[11]['del_words'][33]=oko§ion[11]['del_words'][33]=an eye§ion[11]['name']=sekcja siatkarska1§ion[11]['words'][-1]=§ion[11]['words'][-1]=§ion[11]['words'][-2]=§ion[11]['words'][-2]=§ion[11]['words'][31]=renca§ion[11]['words'][31]=rukka§ion[12]['name']=sekcja siatkarska2§ion[12]['words'][-1]=§ion[12]['words'][-1]=§ion[12]['words'][-2]=§ion[12]['words'][-2]=§ion[12]['words'][34]=wlos§ion[12]['words'][34]=a hair§ionnew=sekcja siatkarska§ionnew=sekcja siatkarska1§ionnew=sekcja siatkarska2&tags=dance, angielski, taniec", 29 | {u"packetdesc": u"Zajebiste slowka na jutrzejszy sprawdzian z chemii", u"packetid": u"4", u"packetname": u"Chemia spr", 30 | u"section": {10: {u"words": {-1: [u"", u""], -2: [u"", u""], 30: [u"noga", u"leg"]}, u"name": u"sekcja siatkarska"}, 31 | 11: {u"words": {-1: [u"", u""], -2: [u"", u""], 31: [u"renca", u"rukka"]}, 32 | u"del_words": {32: [u"kciuk", u"thimb"], 33: [u"oko", u"an eye"]}, 33 | u"name": u"sekcja siatkarska1"}, 34 | 12: {u"words": {-1: [u"", u""], -2: [u"", u""], 34: [u"wlos", u"a hair"]}, 35 | u"name": u"sekcja siatkarska2"}}, 36 | u"language": u"1", u"newlanguage": u"proponowany jezyk..", u"packettype": u"1", 37 | u"tags": u"dance, angielski, taniec", u"newsectionname": u"", 38 | u"sectionnew": [u"sekcja siatkarska", u"sekcja siatkarska1", u"sekcja siatkarska2"]} 39 | ), 40 | ( 41 | # "f=a hair§ionnew[]=sekcja siatkarska§ionnew[]=sekcja siatkarska1§ionnew[]=sekcja siatkarska2", 42 | {u"f": u"a hair", u"sectionnew": {u"": [u"sekcja siatkarska", u"sekcja siatkarska1", u"sekcja siatkarska2"]}} 43 | ), 44 | # f = a 45 | ({u"f": u"a"}), 46 | # "" 47 | ({}), 48 | ) 49 | 50 | knownValues = ( 51 | ({u'omg': {0: u'0001212'}}), 52 | ( 53 | # "packetname=f%26d§ion%5B0%5D%5B%27words%27%5D%5B2%5D=§ion%5B0%5D%5B%27words%27%5D%5B2%5D=&language=1&packetdesc=sdfsd&newlanguage=proponowany+jezyk..&newsectionname=§ion%5B0%5D%5B%27words%27%5D%5B1%5D=§ion%5B0%5D%5B%27words%27%5D%5B1%5D=&packettype=radio§ion%5B0%5D%5B%27words%27%5D%5B0%5D=sdfsd§ion%5B0%5D%5B%27words%27%5D%5B0%5D=ds", 54 | {u"packetname": u"f&d", u"section": {0: {u"words": {0: [u"sdfsd", u"ds"], 1: [u"", u""], 2: [u"", u""]}}}, u"language": u"1", u"packetdesc": u"sdfsd", u"newlanguage": u"proponowany jezyk..", u"newsectionname": u"", u"packettype": u"radio"} 55 | ), 56 | ( 57 | # "language=1&newlanguage=proponowany+jezyk..&newsectionname=&packetdesc=Zajebiste+slowka+na+jutrzejszy+sprawdzian+z+chemii&packetid=4&packetname=Chemia+spr&packettype=1§ion%5B10%5D%5B%27name%27%5D=sekcja+siatkarska§ion%5B10%5D%5B%27words%27%5D%5B-1%5D=§ion%5B10%5D%5B%27words%27%5D%5B-1%5D=§ion%5B10%5D%5B%27words%27%5D%5B-2%5D=§ion%5B10%5D%5B%27words%27%5D%5B-2%5D=§ion%5B10%5D%5B%27words%27%5D%5B30%5D=noga§ion%5B10%5D%5B%27words%27%5D%5B30%5D=leg§ion%5B11%5D%5B%27del_words%27%5D%5B32%5D=kciuk§ion%5B11%5D%5B%27del_words%27%5D%5B32%5D=thimb§ion%5B11%5D%5B%27del_words%27%5D%5B33%5D=oko§ion%5B11%5D%5B%27del_words%27%5D%5B33%5D=an+eye§ion%5B11%5D%5B%27name%27%5D=sekcja+siatkarska1§ion%5B11%5D%5B%27words%27%5D%5B-1%5D=§ion%5B11%5D%5B%27words%27%5D%5B-1%5D=§ion%5B11%5D%5B%27words%27%5D%5B-2%5D=§ion%5B11%5D%5B%27words%27%5D%5B-2%5D=§ion%5B11%5D%5B%27words%27%5D%5B31%5D=renca§ion%5B11%5D%5B%27words%27%5D%5B31%5D=rukka§ion%5B12%5D%5B%27name%27%5D=sekcja+siatkarska2§ion%5B12%5D%5B%27words%27%5D%5B-1%5D=§ion%5B12%5D%5B%27words%27%5D%5B-1%5D=§ion%5B12%5D%5B%27words%27%5D%5B-2%5D=§ion%5B12%5D%5B%27words%27%5D%5B-2%5D=§ion%5B12%5D%5B%27words%27%5D%5B34%5D=wlos§ion%5B12%5D%5B%27words%27%5D%5B34%5D=a+hair§ionnew=sekcja%3Dsiatkarska§ionnew=sekcja+siatkarska1§ionnew=sekcja+siatkarska2&tags=dance%2C+angielski%2C+taniec", 58 | {u"packetdesc": u"Zajebiste slowka na jutrzejszy sprawdzian z chemii", u"packetid": u"4", u"packetname": u"Chemia spr", 59 | u"section": {10: {u"words": {-1: [u"", u""], -2: [u"", u""], 30: [u"noga", u"leg"]}, u"name": u"sekcja siatkarska"}, 60 | 11: {u"words": {-1: [u"", u""], -2: [u"", u""], 31: [u"renca", u"rukka"]}, 61 | u"del_words": {32: [u"kciuk", u"thimb"], 33: [u"oko", u"an eye"]}, 62 | u"name": u"sekcja siatkarska1"}, 63 | 12: {u"words": {-1: [u"", u""], -2: [u"", u""], 34: [u"wlos", u"a hair"]}, 64 | u"name": u"sekcja siatkarska2"}}, 65 | u"language": u"1", u"newlanguage": u"proponowany jezyk..", u"packettype": u"1", 66 | u"tags": u"dance, angielski, taniec", u"newsectionname": "", 67 | u"sectionnew": [u"sekcja=siatkarska", u"sekcja siatkarska1", u"sekcja siatkarska2"]} 68 | ), 69 | ( 70 | # "f=a+hair§ionnew%5B%5D=sekcja+siatkarska§ionnew%5B%5D=sekcja+siatkarska1§ionnew%5B%5D=sekcja+siatkarska2", 71 | {u"f": u"a hair", u"sectionnew": {u"": [u"sekcja siatkarska", u"sekcja siatkarska1", u"sekcja siatkarska2"]}} 72 | ), 73 | # f = a 74 | ({u"f": u"a"}), 75 | # "" 76 | ({}), 77 | ) 78 | 79 | knownValuesCleanWithUnicode = ( 80 | # f = some unicode 81 | ({u"f": u"\u9017"}), 82 | ) 83 | 84 | knownValuesWithUnicode = ( 85 | # f = some unicode 86 | ({u"f": u"\u9017"}), 87 | ) 88 | 89 | def test_parse_known_values_clean(self): 90 | """parse should give known result with known input""" 91 | self.maxDiff = None 92 | for dic in self.knownValuesClean: 93 | result = parse(build(dic), unquote=True) 94 | self.assertEqual(dic, result) 95 | 96 | def test_parse_known_values(self): 97 | """parse should give known result with known input (quoted)""" 98 | self.maxDiff = None 99 | for dic in self.knownValues: 100 | result = parse(build(dic)) 101 | self.assertEqual(dic, result) 102 | 103 | def test_parse_known_values_clean_with_unicode(self): 104 | """parse should give known result with known input""" 105 | self.maxDiff = None 106 | encoding = 'utf-8' if sys.version_info[0] == 2 else None 107 | for dic in self.knownValuesClean + self.knownValuesCleanWithUnicode: 108 | result = parse(build(dic, encoding=encoding), unquote=True, encoding=encoding) 109 | self.assertEqual(dic, result) 110 | 111 | def test_parse_known_values_with_unicode(self): 112 | """parse should give known result with known input (quoted)""" 113 | self.maxDiff = None 114 | 115 | encoding = 'utf-8' if sys.version_info[0] == 2 else None 116 | for dic in self.knownValues + self.knownValuesWithUnicode: 117 | result = parse(build(dic, encoding=encoding), encoding=encoding) 118 | self.assertEqual(dic, result) 119 | 120 | def test_parse_unicode_input_string(self): 121 | """https://github.com/bernii/querystring-parser/issues/15""" 122 | qs = u'first_name=%D8%B9%D9%84%DB%8C' 123 | expected = {u'first_name': u'\u0639\u0644\u06cc'} 124 | self.assertEqual(parse(qs.encode('ascii')), expected) 125 | self.assertEqual(parse(qs), expected) 126 | 127 | class ParseBadInput(unittest.TestCase): 128 | ''' 129 | Test for exceptions when bad input is provided 130 | ''' 131 | badQueryStrings = ( 132 | "f&a hair§ionnew[]=sekcja siatkarska§ionnew[]=sekcja siatkarska1§ionnew[]=sekcja siatkarska2", 133 | "f=a hair§ionnew[=sekcja siatkarska§ionnew[]=sekcja siatkarska1§ionnew[]=sekcja siatkarska2", 134 | "packetname==fd&newsectionname=", 135 | "packetname=fd&newsectionname=§ion[0]['words'][1", 136 | "packetname=fd&newsectionname=&", 137 | ) 138 | 139 | def test_bad_input(self): 140 | """parse should fail with malformed querystring""" 141 | for qstr in self.badQueryStrings: 142 | self.assertRaises(MalformedQueryStringError, parse, qstr, False) 143 | 144 | 145 | class BuildUrl(unittest.TestCase): 146 | ''' 147 | Basic test to verify builder's functionality 148 | ''' 149 | request_data = { 150 | u"word": u"easy", 151 | u"more_words": [u"medium", u"average"], 152 | u"words_with_translation": {u"hard": u"trudny", u"tough": u"twardy"}, 153 | u"words_nested": {u"hard": [u"trudny", u"twardy"]} 154 | } 155 | 156 | def test_build(self): 157 | result = build(self.request_data) 158 | self.assertEquals(parse(result), self.request_data) 159 | 160 | def test_end_to_end(self): 161 | self.maxDiff = None 162 | querystring = build(self.request_data) 163 | result = parse(querystring) 164 | self.assertEquals(result, self.request_data) 165 | 166 | 167 | class BuilderAndParser(unittest.TestCase): 168 | ''' 169 | Testing both builder and parser 170 | ''' 171 | def test_end_to_end(self): 172 | parsed = parse('a[]=1&a[]=2') 173 | result = build(parsed) 174 | self.assertEquals(result, "a[]=1&a[]=2") 175 | 176 | 177 | class NormalizedParse(unittest.TestCase): 178 | ''' 179 | ''' 180 | knownValues = {"section": {10: {u"words": {-1: [u"", u""], -2: [u"", u""], 181 | 30: [u"noga", u"leg"]}, 182 | "name": u"sekcja siatkarska"}, 183 | 11: {u"words": {-1: [u"", u""], 184 | -2: [u"", u""], 185 | 31: [u"renca", u"rukka"]}, 186 | u"del_words": {32: [u"kciuk", u"thimb"], 187 | 33: [u"oko", u"an eye"]}, 188 | u"name": u"sekcja siatkarska1"}, 189 | 12: {u"words": {-1: [u"", u""], 190 | -2: [u"", u""], 191 | 34: [u"wlos", u"a hair"]}, 192 | u"name": u"sekcja siatkarska2"}}} 193 | knownValuesNormalized = {'section': 194 | [{'name': 'sekcja siatkarska', 195 | 'words': [['', ''], ['', ''], 196 | ['noga', 'leg']]}, 197 | {'del_words': [['kciuk', 'thimb'], 198 | ['oko', 'an eye']], 199 | 'name': 'sekcja siatkarska1', 200 | 'words': [['', ''], ['', ''], 201 | ['renca', 'rukka']]}, 202 | {'name': 'sekcja siatkarska2', 203 | 'words': [['wlos', 'a hair'], ['', ''], 204 | ['', '']]}]} 205 | 206 | def test_parse_normalized(self): 207 | result = parse(build(self.knownValues), normalized=True) 208 | self.assertEqual(self.knownValuesNormalized, result) 209 | 210 | if __name__ == "__main__": 211 | unittest.main() 212 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [metadata] 5 | license_files = LICENSE 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="querystring_parser", 5 | version="1.2.4", 6 | description="QueryString parser for Python/Django that correctly handles nested dictionaries", 7 | author="bernii", 8 | author_email="berni@extensa.pl", 9 | url="https://github.com/bernii/querystring-parser", 10 | packages=["querystring_parser"], 11 | install_requires=["six"], 12 | classifiers=[ 13 | "Development Status :: 6 - Mature", 14 | "License :: OSI Approved :: MIT License", 15 | "Operating System :: OS Independent", 16 | "Programming Language :: Python :: 2.7", 17 | "Programming Language :: Python :: 3", 18 | ], 19 | ) 20 | --------------------------------------------------------------------------------