Add merge and update operator support to `MultiValueDict` / `QueryDict`

In Python 3.9, PEP 584 added the | (merge) and |= (update) operators to dict and related mapping types.

I propose that we extend MultiValueDict to support these operators, which will be picked up by its subclass QueryDict too. That will enable more succinct merge/update patterns, for example to copy and modify request.GET in a view:

next_page_params = request.GET | {"page": page + 1}

Currently, the operators work due to inheritance from dict, but with flaws:

  1. | (merge) always returns a plain dict.
  2. |= (update) works even for immutable QueryDicts, bypassing the mutation block that exists in other methods like update().
4 Likes

This feels like a bug to me, so :+1: to fixing it.

That would be a +1 from me

So after a bit of hacking on this branch, I found that it’s a bit tricky to balance backwards compatiblity with correctness for |.

The reference implementation suggests implementing the operators using .update() to merge in values. If we do that, it will use MultiValueDict.update() which extends lists, rather than replacing them. This is counter to the current behaviour which replaces values, so we would break existing code, plus it’s less useful as the example above won’t work: page would be set to 1 and 2, not replaced.

However, I did find that |= is actually completely broken right now, as it leaves MultiValueDict (and QueryDict) in a broken state:

In [1]: from django.utils.datastructures import MultiValueDict

In [2]: mvd = MultiValueDict({"page": [1]})

In [3]: mvd |= {"page": 2}

In [4]: mvd["page"]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[4], line 1
----> 1 mvd["page"]

File /.../django/utils/datastructures.py:90, in MultiValueDict.__getitem__(self, key)
     88     raise MultiValueDictKeyError(key)
     89 try:
---> 90     return list_[-1]
     91 except IndexError:
     92     return []

TypeError: 'int' object is not subscriptable

So I’ll limit this to a bug fix for |=.

Ticket, and PR.