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