Index: Doc/library/urllib.request.rst =================================================================== --- Doc/library/urllib.request.rst (revision 65358) +++ Doc/library/urllib.request.rst (working copy) @@ -470,7 +470,9 @@ .. method:: Request.add_header(key, val) - Add another header to the request. Headers are currently ignored by all + Add another header to the request.This method adds the key in .capitalize() + format, but when headers are passed to HTTP Handlers, they are internally + converted to the required .title() format. Headers are currently ignored by all handlers except HTTP handlers, where they are added to the list of headers sent to the server. Note that there cannot be more than one header with the same name, and later calls will overwrite previous calls in case the *key* collides. Index: Lib/urllib/request.py =================================================================== --- Lib/urllib/request.py (revision 65358) +++ Lib/urllib/request.py (working copy) @@ -158,6 +158,37 @@ host = _cut_port_re.sub("", host, 1) return host.lower() +class CaseInsensitiveDict(dict): + def __init__(self, *args, **kwargs): + self.keystore = {} + d = dict(*args, **kwargs) + for k in d.keys(): + self.keystore[self._get_lower(k)] = k + return super(CaseInsensitiveDict, self).__init__(*args, **kwargs) + def __setitem__(self, k, v): + if hasattr(self,'keystore'): + self.keystore[self._get_lower(k)] = k + return super(CaseInsensitiveDict, self).__setitem__(k, v) + def __getitem__(self, k): + if hasattr(self,'keystore') and self._get_lower(k) in self.keystore: + k = self.keystore[self._get_lower(k)] + return super(CaseInsensitiveDict, self).__getitem__(k) + def __contains__(self, k): + if hasattr(self,'keystore') and self._get_lower(k) in self.keystore: + k = self.keystore[self._get_lower(k)] + return super(CaseInsensitiveDict, self).__contains__(k) + def get(self, k, failobj=None): + if hasattr(self,'keystore') and self._get_lower(k) in self.keystore: + k = self.keystore[self._get_lower(k)] + return super(CaseInsensitiveDict, self).get(k, failobj) + + @staticmethod + def _get_lower(k): + if isinstance(k, str): + return k.lower() + else: + return k + class Request: def __init__(self, url, data=None, headers={}, @@ -169,10 +200,10 @@ self.host = None self.port = None self.data = data - self.headers = {} + self.headers = CaseInsensitiveDict() for key, value in headers.items(): self.add_header(key, value) - self.unredirected_hdrs = {} + self.unredirected_hdrs = CaseInsensitiveDict() if origin_req_host is None: origin_req_host = request_host(self) self.origin_req_host = origin_req_host Index: Lib/test/test_urllib2.py =================================================================== --- Lib/test/test_urllib2.py (revision 65358) +++ Lib/test/test_urllib2.py (working copy) @@ -52,7 +52,9 @@ stay that way, because the complete set of headers are only accessible through the .get_header(), .has_header(), .header_items() interface. However, .headers pre-dates those methods, and so real code will be using - the dictionary. + the dictionary. The methods .get_header() and .has_header() and .headers + attribute suppot case-insensitive dict lookup for header keys. + .header_items() returns them in .capitalize() form. The introduction in 2.4 of those methods was a mistake for the same reason: code that previously saw all (urllib2 user)-provided headers in .headers @@ -71,9 +73,14 @@ >>> Request(url, headers={"spam-EggS": "blah"}).headers["Spam-eggs"] 'blah' - Currently, Request(url, "Spam-eggs").headers["Spam-Eggs"] raises KeyError, - but that could be changed in future. + Check case-insensitive dict lookup. + >>> Request(url, headers={"spam-EggS": "blah"}).headers["Spam-Eggs"] + 'blah' + >>> Request(url, headers={"spam-EggS": "blah"}).headers["spam-eggs"] + 'blah' + >>> Request(url, headers={"spam-EggS": "blah"}).headers["SPAM-EGGS"] + 'blah' """ def test_request_headers_methods(): @@ -81,12 +88,20 @@ Note the case normalization of header names here, to .capitalize()-case. This should be preserved for backwards-compatibility. (In the HTTP case, normalization to .title()-case is done by urllib2 before sending headers to - http.client). + http.client). The methods .has_header and .get_header support + case-insensitive lookup to support the cases where Headers can be looked up + (mostly in) .title() form. >>> url = "http://example.com" >>> r = Request(url, headers={"Spam-eggs": "blah"}) >>> r.has_header("Spam-eggs") True + >>> r.has_header("Spam-Eggs") + True + >>> r.has_header("spam-eggs") + True + >>> r.has_header("SpaM-EggS") + True >>> r.header_items() [('Spam-eggs', 'blah')] >>> r.add_header("Foo-Bar", "baz") @@ -94,9 +109,13 @@ >>> items [('Foo-bar', 'baz'), ('Spam-eggs', 'blah')] - Note that e.g. r.has_header("spam-EggS") is currently False, and - r.get_header("spam-EggS") returns None, but that could be changed in - future. + >>> req = Request(url) + >>> req.add_header("spam-eggs", "eggs") + >>> req.add_header("SPAM-eggs", "ham") # Later headers will be over-written + >>> req.header_items() + [('Spam-eggs', 'ham')] + >>> list(req.headers.items()) + [('Spam-eggs', 'ham')] >>> r.has_header("Not-there") False