Mercurial > public > mercurial-scm > hg
comparison mercurial/hgweb/request.py @ 36862:ec0af9c59270
hgweb: use a multidict for holding query string parameters
My intention with refactoring the WSGI code was to make it easier
to read. I initially wanted to vendor and use WebOb, because it seems
to be a pretty reasonable abstraction layer for WSGI. However, it isn't
using relative imports and I didn't want to deal with the hassle of
patching it. But that doesn't mean we can't use good ideas from WebOb.
WebOb has a "multidict" data structure for holding parsed query string
and POST form data. It quacks like a dict but allows you to store
multiple values for each key. It offers mechanisms to return just one
value, all values, or return 1 value asserting that only 1 value is
set. I quite like its API.
This commit implements a read-only "multidict" in the spirit of
WebOb's multidict.
We replace the query string attributes of our parsed request with
an instance of it.
Differential Revision: https://phab.mercurial-scm.org/D2776
author | Gregory Szorc <gregory.szorc@gmail.com> |
---|---|
date | Sat, 10 Mar 2018 12:35:38 -0800 |
parents | a88d68dc3ee8 |
children | cf69df7ea385 |
comparison
equal
deleted
inserted
replaced
36861:a88d68dc3ee8 | 36862:ec0af9c59270 |
---|---|
25 from .. import ( | 25 from .. import ( |
26 error, | 26 error, |
27 pycompat, | 27 pycompat, |
28 util, | 28 util, |
29 ) | 29 ) |
30 | |
31 class multidict(object): | |
32 """A dict like object that can store multiple values for a key. | |
33 | |
34 Used to store parsed request parameters. | |
35 | |
36 This is inspired by WebOb's class of the same name. | |
37 """ | |
38 def __init__(self): | |
39 # Stores (key, value) 2-tuples. This isn't the most efficient. But we | |
40 # don't rely on parameters that much, so it shouldn't be a perf issue. | |
41 # we can always add dict for fast lookups. | |
42 self._items = [] | |
43 | |
44 def __getitem__(self, key): | |
45 """Returns the last set value for a key.""" | |
46 for k, v in reversed(self._items): | |
47 if k == key: | |
48 return v | |
49 | |
50 raise KeyError(key) | |
51 | |
52 def __setitem__(self, key, value): | |
53 """Replace a values for a key with a new value.""" | |
54 try: | |
55 del self[key] | |
56 except KeyError: | |
57 pass | |
58 | |
59 self._items.append((key, value)) | |
60 | |
61 def __delitem__(self, key): | |
62 """Delete all values for a key.""" | |
63 oldlen = len(self._items) | |
64 | |
65 self._items[:] = [(k, v) for k, v in self._items if k != key] | |
66 | |
67 if oldlen == len(self._items): | |
68 raise KeyError(key) | |
69 | |
70 def __contains__(self, key): | |
71 return any(k == key for k, v in self._items) | |
72 | |
73 def __len__(self): | |
74 return len(self._items) | |
75 | |
76 def get(self, key, default=None): | |
77 try: | |
78 return self.__getitem__(key) | |
79 except KeyError: | |
80 return default | |
81 | |
82 def add(self, key, value): | |
83 """Add a new value for a key. Does not replace existing values.""" | |
84 self._items.append((key, value)) | |
85 | |
86 def getall(self, key): | |
87 """Obtains all values for a key.""" | |
88 return [v for k, v in self._items if k == key] | |
89 | |
90 def getone(self, key): | |
91 """Obtain a single value for a key. | |
92 | |
93 Raises KeyError if key not defined or it has multiple values set. | |
94 """ | |
95 vals = self.getall(key) | |
96 | |
97 if not vals: | |
98 raise KeyError(key) | |
99 | |
100 if len(vals) > 1: | |
101 raise KeyError('multiple values for %r' % key) | |
102 | |
103 return vals[0] | |
104 | |
105 def asdictoflists(self): | |
106 d = {} | |
107 for k, v in self._items: | |
108 if k in d: | |
109 d[k].append(v) | |
110 else: | |
111 d[k] = [v] | |
112 | |
113 return d | |
30 | 114 |
31 @attr.s(frozen=True) | 115 @attr.s(frozen=True) |
32 class parsedrequest(object): | 116 class parsedrequest(object): |
33 """Represents a parsed WSGI request. | 117 """Represents a parsed WSGI request. |
34 | 118 |
54 # Whether there is a path component to this request. This can be true | 138 # Whether there is a path component to this request. This can be true |
55 # when ``dispatchpath`` is empty due to REPO_NAME muckery. | 139 # when ``dispatchpath`` is empty due to REPO_NAME muckery. |
56 havepathinfo = attr.ib() | 140 havepathinfo = attr.ib() |
57 # Raw query string (part after "?" in URL). | 141 # Raw query string (part after "?" in URL). |
58 querystring = attr.ib() | 142 querystring = attr.ib() |
59 # List of 2-tuples of query string arguments. | 143 # multidict of query string parameters. |
60 querystringlist = attr.ib() | 144 qsparams = attr.ib() |
61 # Dict of query string arguments. Values are lists with at least 1 item. | |
62 querystringdict = attr.ib() | |
63 # wsgiref.headers.Headers instance. Operates like a dict with case | 145 # wsgiref.headers.Headers instance. Operates like a dict with case |
64 # insensitive keys. | 146 # insensitive keys. |
65 headers = attr.ib() | 147 headers = attr.ib() |
66 # Request body input stream. | 148 # Request body input stream. |
67 bodyfh = attr.ib() | 149 bodyfh = attr.ib() |
155 | 237 |
156 querystring = env.get('QUERY_STRING', '') | 238 querystring = env.get('QUERY_STRING', '') |
157 | 239 |
158 # We store as a list so we have ordering information. We also store as | 240 # We store as a list so we have ordering information. We also store as |
159 # a dict to facilitate fast lookup. | 241 # a dict to facilitate fast lookup. |
160 querystringlist = util.urlreq.parseqsl(querystring, keep_blank_values=True) | 242 qsparams = multidict() |
161 | 243 for k, v in util.urlreq.parseqsl(querystring, keep_blank_values=True): |
162 querystringdict = {} | 244 qsparams.add(k, v) |
163 for k, v in querystringlist: | |
164 if k in querystringdict: | |
165 querystringdict[k].append(v) | |
166 else: | |
167 querystringdict[k] = [v] | |
168 | 245 |
169 # HTTP_* keys contain HTTP request headers. The Headers structure should | 246 # HTTP_* keys contain HTTP request headers. The Headers structure should |
170 # perform case normalization for us. We just rewrite underscore to dash | 247 # perform case normalization for us. We just rewrite underscore to dash |
171 # so keys match what likely went over the wire. | 248 # so keys match what likely went over the wire. |
172 headers = [] | 249 headers = [] |
195 advertisedbaseurl=advertisedbaseurl, | 272 advertisedbaseurl=advertisedbaseurl, |
196 apppath=apppath, | 273 apppath=apppath, |
197 dispatchparts=dispatchparts, dispatchpath=dispatchpath, | 274 dispatchparts=dispatchparts, dispatchpath=dispatchpath, |
198 havepathinfo='PATH_INFO' in env, | 275 havepathinfo='PATH_INFO' in env, |
199 querystring=querystring, | 276 querystring=querystring, |
200 querystringlist=querystringlist, | 277 qsparams=qsparams, |
201 querystringdict=querystringdict, | |
202 headers=headers, | 278 headers=headers, |
203 bodyfh=bodyfh) | 279 bodyfh=bodyfh) |
204 | 280 |
205 class wsgiresponse(object): | 281 class wsgiresponse(object): |
206 """Represents a response to a WSGI request. | 282 """Represents a response to a WSGI request. |
348 self.threaded = wsgienv[r'wsgi.multithread'] | 424 self.threaded = wsgienv[r'wsgi.multithread'] |
349 self.multiprocess = wsgienv[r'wsgi.multiprocess'] | 425 self.multiprocess = wsgienv[r'wsgi.multiprocess'] |
350 self.run_once = wsgienv[r'wsgi.run_once'] | 426 self.run_once = wsgienv[r'wsgi.run_once'] |
351 self.env = wsgienv | 427 self.env = wsgienv |
352 self.req = parserequestfromenv(wsgienv, inp) | 428 self.req = parserequestfromenv(wsgienv, inp) |
353 self.form = self.req.querystringdict | 429 self.form = self.req.qsparams.asdictoflists() |
354 self.res = wsgiresponse(self.req, start_response) | 430 self.res = wsgiresponse(self.req, start_response) |
355 self._start_response = start_response | 431 self._start_response = start_response |
356 self.server_write = None | 432 self.server_write = None |
357 self.headers = [] | 433 self.headers = [] |
358 | 434 |