diff -r 5a87fb8df5fe Lib/http/cookies.py --- a/Lib/http/cookies.py Mon Mar 09 10:37:59 2015 -0400 +++ b/Lib/http/cookies.py Tue Mar 10 16:23:24 2015 -0700 @@ -131,6 +131,7 @@ # import re import string +import warnings __all__ = ["CookieError", "BaseCookie", "SimpleCookie"] @@ -138,6 +139,10 @@ _semispacejoin = '; '.join _spacejoin = ' '.join +_SETTER_DEPRECATED = ( + 'The .%s setter is deprecated. The attribute will be read-only in future ' + 'releases. Please use the set() method instead.') + # # Define an exception visible to External modules # @@ -157,82 +162,64 @@ # _Translator hash-table for fast quoting # _LegalChars = string.ascii_letters + string.digits + "!#$%&'*+-.^_`|~:" -_Translator = { - '\000' : '\\000', '\001' : '\\001', '\002' : '\\002', - '\003' : '\\003', '\004' : '\\004', '\005' : '\\005', - '\006' : '\\006', '\007' : '\\007', '\010' : '\\010', - '\011' : '\\011', '\012' : '\\012', '\013' : '\\013', - '\014' : '\\014', '\015' : '\\015', '\016' : '\\016', - '\017' : '\\017', '\020' : '\\020', '\021' : '\\021', - '\022' : '\\022', '\023' : '\\023', '\024' : '\\024', - '\025' : '\\025', '\026' : '\\026', '\027' : '\\027', - '\030' : '\\030', '\031' : '\\031', '\032' : '\\032', - '\033' : '\\033', '\034' : '\\034', '\035' : '\\035', - '\036' : '\\036', '\037' : '\\037', +_LegalMorselKeyPatt = re.compile('[%s]' % _LegalChars) - # Because of the way browsers really handle cookies (as opposed - # to what the RFC says) we also encode , and ; +_Translator = { + 0: '\\000', 1: '\\001', 2: '\\002', 3: '\\003', + 4: '\\004', 5: '\\005', 6: '\\006', 7: '\\007', + 8: '\\010', 9: '\\011', 10: '\\012', 11: '\\013', + 12: '\\014', 13: '\\015', 14: '\\016', 15: '\\017', + 16: '\\020', 17: '\\021', 18: '\\022', 19: '\\023', + 20: '\\024', 21: '\\025', 22: '\\026', 23: '\\027', + 24: '\\030', 25: '\\031', 26: '\\032', 27: '\\033', + 28: '\\034', 29: '\\035', 30: '\\036', 31: '\\037', + 34: '\\042', 44: '\\054', 59: '\\073', 92: '\\134', + 127: '\\177', 128: '\\200', 129: '\\201', 130: '\\202', + 131: '\\203', 132: '\\204', 133: '\\205', 134: '\\206', + 135: '\\207', 136: '\\210', 137: '\\211', 138: '\\212', + 139: '\\213', 140: '\\214', 141: '\\215', 142: '\\216', + 143: '\\217', 144: '\\220', 145: '\\221', 146: '\\222', + 147: '\\223', 148: '\\224', 149: '\\225', 150: '\\226', + 151: '\\227', 152: '\\230', 153: '\\231', 154: '\\232', + 155: '\\233', 156: '\\234', 157: '\\235', 158: '\\236', + 159: '\\237', 160: '\\240', 161: '\\241', 162: '\\242', + 163: '\\243', 164: '\\244', 165: '\\245', 166: '\\246', + 167: '\\247', 168: '\\250', 169: '\\251', 170: '\\252', + 171: '\\253', 172: '\\254', 173: '\\255', 174: '\\256', + 175: '\\257', 176: '\\260', 177: '\\261', 178: '\\262', + 179: '\\263', 180: '\\264', 181: '\\265', 182: '\\266', + 183: '\\267', 184: '\\270', 185: '\\271', 186: '\\272', + 187: '\\273', 188: '\\274', 189: '\\275', 190: '\\276', + 191: '\\277', 192: '\\300', 193: '\\301', 194: '\\302', + 195: '\\303', 196: '\\304', 197: '\\305', 198: '\\306', + 199: '\\307', 200: '\\310', 201: '\\311', 202: '\\312', + 203: '\\313', 204: '\\314', 205: '\\315', 206: '\\316', + 207: '\\317', 208: '\\320', 209: '\\321', 210: '\\322', + 211: '\\323', 212: '\\324', 213: '\\325', 214: '\\326', + 215: '\\327', 216: '\\330', 217: '\\331', 218: '\\332', + 219: '\\333', 220: '\\334', 221: '\\335', 222: '\\336', + 223: '\\337', 224: '\\340', 225: '\\341', 226: '\\342', + 227: '\\343', 228: '\\344', 229: '\\345', 230: '\\346', + 231: '\\347', 232: '\\350', 233: '\\351', 234: '\\352', + 235: '\\353', 236: '\\354', 237: '\\355', 238: '\\356', + 239: '\\357', 240: '\\360', 241: '\\361', 242: '\\362', + 243: '\\363', 244: '\\364', 245: '\\365', 246: '\\366', + 247: '\\367', 248: '\\370', 249: '\\371', 250: '\\372', + 251: '\\373', 252: '\\374', 253: '\\375', 254: '\\376', + 255: '\\377', +} - ',' : '\\054', ';' : '\\073', - - '"' : '\\"', '\\' : '\\\\', - - '\177' : '\\177', '\200' : '\\200', '\201' : '\\201', - '\202' : '\\202', '\203' : '\\203', '\204' : '\\204', - '\205' : '\\205', '\206' : '\\206', '\207' : '\\207', - '\210' : '\\210', '\211' : '\\211', '\212' : '\\212', - '\213' : '\\213', '\214' : '\\214', '\215' : '\\215', - '\216' : '\\216', '\217' : '\\217', '\220' : '\\220', - '\221' : '\\221', '\222' : '\\222', '\223' : '\\223', - '\224' : '\\224', '\225' : '\\225', '\226' : '\\226', - '\227' : '\\227', '\230' : '\\230', '\231' : '\\231', - '\232' : '\\232', '\233' : '\\233', '\234' : '\\234', - '\235' : '\\235', '\236' : '\\236', '\237' : '\\237', - '\240' : '\\240', '\241' : '\\241', '\242' : '\\242', - '\243' : '\\243', '\244' : '\\244', '\245' : '\\245', - '\246' : '\\246', '\247' : '\\247', '\250' : '\\250', - '\251' : '\\251', '\252' : '\\252', '\253' : '\\253', - '\254' : '\\254', '\255' : '\\255', '\256' : '\\256', - '\257' : '\\257', '\260' : '\\260', '\261' : '\\261', - '\262' : '\\262', '\263' : '\\263', '\264' : '\\264', - '\265' : '\\265', '\266' : '\\266', '\267' : '\\267', - '\270' : '\\270', '\271' : '\\271', '\272' : '\\272', - '\273' : '\\273', '\274' : '\\274', '\275' : '\\275', - '\276' : '\\276', '\277' : '\\277', '\300' : '\\300', - '\301' : '\\301', '\302' : '\\302', '\303' : '\\303', - '\304' : '\\304', '\305' : '\\305', '\306' : '\\306', - '\307' : '\\307', '\310' : '\\310', '\311' : '\\311', - '\312' : '\\312', '\313' : '\\313', '\314' : '\\314', - '\315' : '\\315', '\316' : '\\316', '\317' : '\\317', - '\320' : '\\320', '\321' : '\\321', '\322' : '\\322', - '\323' : '\\323', '\324' : '\\324', '\325' : '\\325', - '\326' : '\\326', '\327' : '\\327', '\330' : '\\330', - '\331' : '\\331', '\332' : '\\332', '\333' : '\\333', - '\334' : '\\334', '\335' : '\\335', '\336' : '\\336', - '\337' : '\\337', '\340' : '\\340', '\341' : '\\341', - '\342' : '\\342', '\343' : '\\343', '\344' : '\\344', - '\345' : '\\345', '\346' : '\\346', '\347' : '\\347', - '\350' : '\\350', '\351' : '\\351', '\352' : '\\352', - '\353' : '\\353', '\354' : '\\354', '\355' : '\\355', - '\356' : '\\356', '\357' : '\\357', '\360' : '\\360', - '\361' : '\\361', '\362' : '\\362', '\363' : '\\363', - '\364' : '\\364', '\365' : '\\365', '\366' : '\\366', - '\367' : '\\367', '\370' : '\\370', '\371' : '\\371', - '\372' : '\\372', '\373' : '\\373', '\374' : '\\374', - '\375' : '\\375', '\376' : '\\376', '\377' : '\\377' - } - -def _quote(str, LegalChars=_LegalChars): +def _quote(str_): r"""Quote a string for use in a cookie header. If the string does not need to be double-quoted, then just return the string. Otherwise, surround the string in doublequotes and quote (with a \) special characters. """ - if all(c in LegalChars for c in str): - return str + if str_ is None or _LegalMorselKeyPatt.fullmatch(str_): + return str_ else: - return '"' + _nulljoin(_Translator.get(s, s) for s in str) + '"' + return '"' + str_.translate(_Translator) + '"' _OctalPatt = re.compile(r"\\[0-3][0-7][0-7]") @@ -241,7 +228,7 @@ def _unquote(str): # If there aren't any doublequotes, # then there can't be any special characters. See RFC 2109. - if len(str) < 2: + if str is None or len(str) < 2: return str if str[0] != '"' or str[-1] != '"': return str @@ -339,11 +326,38 @@ def __init__(self): # Set defaults - self.key = self.value = self.coded_value = None + self._key = self._value = self._coded_value = None - # Set default attributes - for key in self._reserved: - dict.__setitem__(self, key, "") + # Set required flags + for flag in self._flags: + self[flag] = False + + @property + def key(self): + return self._key + + @key.setter + def key(self, key): + warnings.warn(_SETTER_DEPRECATED % 'key') + self._key = key + + @property + def value(self): + return self._value + + @value.setter + def value(self, value): + warnings.warn(_SETTER_DEPRECATED % 'value') + self._value = value + + @property + def coded_value(self): + return self._coded_value + + @coded_value.setter + def coded_value(self, coded_value): + warnings.warn(_SETTER_DEPRECATED % 'coded_value') + self._coded_value = coded_value def __setitem__(self, K, V): K = K.lower() @@ -351,21 +365,43 @@ raise CookieError("Invalid Attribute %s" % K) dict.__setitem__(self, K, V) + def __eq__(self, morsel): + return dict.__eq__(self, morsel) and \ + self.value == morsel.value and self.key == morsel.key and \ + self.coded_value == morsel.coded_value + + def __copy__(self): + morsel = Morsel() + #morsel.update(self) + dict.update(morsel, self) + morsel.__dict__.update(self.__dict__) + return morsel + + def update(self, values): + invalid_attrs = [ + key for key in values.keys() if key.lower() not in self._reserved] + + if len(invalid_attrs) > 0: + # only report the first invalid attribute in the event that attrs + # is very long + raise CookieError( + 'Invalid attribute: {}'.format(invalid_attrs[0])) + + dict.update(self, values) + def isReservedKey(self, K): return K.lower() in self._reserved - def set(self, key, val, coded_val, LegalChars=_LegalChars): - # First we verify that the key isn't a reserved word - # Second we make sure it only contains legal characters + def set(self, key, val, coded_val): if key.lower() in self._reserved: - raise CookieError("Attempt to set a reserved key: %s" % key) - if any(c not in LegalChars for c in key): - raise CookieError("Illegal key value: %s" % key) + raise CookieError('Attempt to set a reserved key: %s' % key) + if any(c not in _LegalChars for c in key): + raise CookieError('Illegal key value: %s' % key) - # It's a good key, so save it. - self.key = key - self.value = val - self.coded_value = coded_val + # It's a good key, so save it + self._key = key + self._value = val + self._coded_value = coded_val def output(self, attrs=None, header="Set-Cookie:"): return "%s %s" % (header, self.OutputString(attrs)) @@ -373,8 +409,7 @@ __str__ = output def __repr__(self): - return '<%s: %s=%s>' % (self.__class__.__name__, - self.key, repr(self.value)) + return '<%s: %s>' % (self.__class__.__name__, self.OutputString()) def js_output(self, attrs=None): # Print javascript @@ -408,9 +443,9 @@ append("%s=%s" % (self._reserved[key], _getdate(value))) elif key == "max-age" and isinstance(value, int): append("%s=%d" % (self._reserved[key], value)) - elif key == "secure": - append(str(self._reserved[key])) - elif key == "httponly": + elif key in self._flags: + if not value: + continue append(str(self._reserved[key])) else: append("%s=%s" % (self._reserved[key], value)) diff -r 5a87fb8df5fe Lib/test/test_http_cookies.py --- a/Lib/test/test_http_cookies.py Mon Mar 09 10:37:59 2015 -0400 +++ b/Lib/test/test_http_cookies.py Tue Mar 10 16:23:24 2015 -0700 @@ -1,9 +1,11 @@ # Simple test suite for http/cookies.py +import copy from test.support import run_unittest, run_doctest, check_warnings import unittest from http import cookies import pickle +import re import warnings class CookieTests(unittest.TestCase): @@ -243,6 +245,81 @@ self.assertRaises(cookies.CookieError, M.set, i, '%s_value' % i, '%s_value' % i) + def test_deprecation(self): + morsel = cookies.Morsel() + + exp = re.escape(cookies._SETTER_DEPRECATED).replace('\\%s', '\w{1,}') + with check_warnings((exp, UserWarning)): + morsel.key = 'foo' + morsel.value = 'bar' + morsel.coded_value = 'baz' + + def test_eq(self): + base_case = ('key', 'value', 'value') + cases = ( + (base_case, ('key', 'value', 'mismatch')), + (base_case, ('key', 'mismatch', 'value')), + (base_case, ('mismatch', 'value', 'value')), + ) + for case_a, case_b in cases: + morsel_a = cookies.Morsel() + morsel_b = cookies.Morsel() + + morsel_a.set(*case_a) + morsel_b.set(*case_b) + + self.assertEqual(morsel_a == morsel_b, case_a == case_b) + + def test_copy(self): + morsel_a = cookies.Morsel() + morsel_b = copy.copy(morsel_a) + + self.assertTrue(isinstance(morsel_b, cookies.Morsel)) + self.assertNotEqual(id(morsel_a), id(morsel_b)) + self.assertEqual(morsel_a, morsel_b) + + def test_update(self): + morsel = cookies.Morsel() + morsel['expires'] = 0 + self.assertEqual(morsel['expires'], 0) + + morsel.update({'expires': 1}) + self.assertEqual(morsel['expires'], 1) + + # invalid keys should be treated consistently between .__setitem__() + # and .update() + self.assertRaises( + cookies.CookieError, morsel.__setitem__, 'foo', 'bar') + self.assertRaises( + cookies.CookieError, morsel.update, {'foo': 'bar'}) + + def test_repr(self): + morsel = cookies.Morsel() + morsel.update({ + 'expires': 0, + 'path': '/', + 'comment': 'foo', + 'domain': 'example.com', + 'max-age': 0, + 'secure': 1, + 'httponly': 1, + 'version': 1, + }) + morsel.set('key', 'val', 'coded_val') + self.assertEqual( + repr(morsel), '<{}: {}>'.format( + morsel.__class__.__name__, morsel.OutputString())) + + def test_len(self): + morsel = cookies.Morsel() + # morsel flags are set by default + base_len = len(morsel._flags) + self.assertEqual(len(morsel), base_len) + morsel['expires'] = 0 + self.assertEqual(len(morsel), base_len + 1) + + morsel.set('foo', 'bar', 'bar') + self.assertEqual(len(morsel), base_len + 1) def test_main(): run_unittest(CookieTests, MorselTests)