9

Is it possible to to make Django's QueryDict preserve the ordering from the original query string?

>>> from django.http import QueryDict
>>> q = QueryDict(u'x=foo³&y=bar(potato),z=hello world')
>>> q.urlencode(safe='()')
u'y=bar(potato)%2Cz%3Dhello%20world&x=foo%C2%B3'
wim
  • 338,267
  • 99
  • 616
  • 750

2 Answers2

3

QueryDict inherits from Django's MultiValueDict which inherits from dict which is implemented as a hash table. Thus, you can't guarantee it will stay ordered.

I'm not sure if this will be relevant to what you need, but an ordering that QueryDict does preserve is the order of "lists" (multiple values for the same key) passed in to them. Using this, you could do:

>>> from django.http import QueryDict
>>> q = QueryDict(u'x=foo³&x=bar(potato),x=hello world')
>>> q.lists()
[('x', ['foo³', 'bar(potato)', 'hello world'])]
>>> q.urlencode(safe='()')
u'x=foo%C2%B3&x=bar(potato)&x=hello%20world'
Community
  • 1
  • 1
pcoronel
  • 3,833
  • 22
  • 25
  • this behavior changed after python 3.7, could you update your answer? https://stackoverflow.com/a/47837132/13953019 – abtinmo Oct 20 '21 at 10:32
2

QueryDict class is based on MultiValueDict class that is based on regular python dict, which is an unordered collection as you know.

According to the source code, QueryDict internally uses urlparse.parse_qsl() method, which preserves the order of query parameters, outputs a list of tuples:

>>> from urlparse import parse_qsl
>>> parse_qsl('x=foo³&y=bar(potato),z=hello world')
[('x', 'foo\xc2\xb3'), ('y', 'bar(potato),z=hello world')]

What you can do, is to use the order of keys given by the parse_qsl() for sorting:

>>> order = [key for key, _ in parse_qsl('x=foo³&y=bar(potato),z=hello world')]
>>> order
['x', 'y']

Then, subclass QueryDict and override lists() method used in urlencode():

>>> class MyQueryDict(QueryDict):
...     def __init__(self, query_string, mutable=False, encoding=None, order=None):
...         super(MyQueryDict, self).__init__(query_string, mutable=False, encoding=None)
...         self.order = order
...     def lists(self):
...         return [(key, self.getlist(key)) for key in self.order]
... 
>>> q = MyQueryDict(u'x=foo³&y=bar(potato),z=hello world', order=order)
>>> q.urlencode(safe='()')
u'x=foo%C2%B3&y=bar(potato)%2Cz%3Dhello%20world'

The approach is a bit ugly and may need further improvement, but hope at least it'll give you an idea of what is happening and what you can do about it.

wim
  • 338,267
  • 99
  • 616
  • 750
alecxe
  • 462,703
  • 120
  • 1,088
  • 1,195