diff -r 9332a545ad85 Doc/library/http.cookies.rst --- a/Doc/library/http.cookies.rst Thu Mar 12 22:01:30 2015 +0200 +++ b/Doc/library/http.cookies.rst Fri Mar 13 14:17:57 2015 -0700 @@ -148,16 +148,28 @@ The value of the cookie. + .. deprecated:: 3.5 + Setting :attr:`~Morsel.value` directly has been deprecated in favour of + using :func:`~Morsel.set` + .. attribute:: Morsel.coded_value The encoded value of the cookie --- this is what should be sent. + .. deprecated:: 3.5 + Setting :attr:`~Morsel.coded_value` directly has been deprecated in + favour of using :func:`~Morsel.set` + .. attribute:: Morsel.key The name of the cookie. + .. deprecated:: 3.5 + Setting :attr:`~Morsel.key` directly has been deprecated in + favour of using :func:`~Morsel.set` + .. method:: Morsel.set(key, value, coded_value) diff -r 9332a545ad85 Doc/whatsnew/3.5.rst --- a/Doc/whatsnew/3.5.rst Thu Mar 12 22:01:30 2015 +0200 +++ b/Doc/whatsnew/3.5.rst Fri Mar 13 14:17:57 2015 -0700 @@ -491,6 +491,12 @@ ``True``, but this default is deprecated. Specify the *decode_data* keyword with an appropriate value to avoid the deprecation warning. +* :class:`~http.cookies.Morsel` has previously allowed for setting attributes + :attr:`~http.cookies.Morsel.key`, :attr:`~http.cookies.Morsel.value` and + :attr:`~http.cookies.Morsel.coded_value`. Use the preferred + :func:`~http.cookies.Morsel.set` method in order to avoid the deprecation + warning. + Deprecated functions and types of the C API ------------------------------------------- diff -r 9332a545ad85 Lib/http/cookies.py --- a/Lib/http/cookies.py Thu Mar 12 22:01:30 2015 +0200 +++ b/Lib/http/cookies.py Fri Mar 13 14:17:57 2015 -0700 @@ -138,6 +138,15 @@ _semispacejoin = '; '.join _spacejoin = ' '.join +_DEPRECATED_SETTER = ( + 'The .%s setter is deprecated. The attribute will be read-only in future ' + 'releases. Please use the set() method instead.') + +def _warn_deprecated_setter(setter): + import warnings + warnings.warn( + _DEPRECATED_SETTER % setter, DeprecationWarning, stacklevel=3) + # # Define an exception visible to External modules # @@ -156,83 +165,33 @@ # _LegalChars is the list of chars which don't require "'s # _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', +_LegalChars = string.ascii_letters + string.digits + "!#$%&'*+-.^_`|~:" +_UnquotedChars = _LegalChars + ' ()/<=>?@[]{}' - # Because of the way browsers really handle cookies (as opposed - # to what the RFC says) we also encode , and ; +_Translator = { + n: '\\{:03o}'.format(n) + for n in set(range(256)) - set(map(ord, _UnquotedChars))} - ',' : '\\054', ';' : '\\073', +# (3.5 Backwards compatibility) Because of the way browsers really handle +# cookies (as opposed to what the RFC says) we also encode , and ; +_Translator.update({ + ord('"'): '\\"', + ord('\\'): '\\\\', +}) - '"' : '\\"', '\\' : '\\\\', +_LegalMorselKeyPatt = re.compile('[%s]+' % _LegalChars) - '\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): + 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 +200,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,33 +298,95 @@ 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, "") + @property + def key(self): + return self._key + + @key.setter + def key(self, key): + _warn_deprecated_setter('key') + self._key = key + + @property + def value(self): + return self._value + + @value.setter + def value(self, value): + _warn_deprecated_setter('value') + self._value = value + + @property + def coded_value(self): + return self._coded_value + + @coded_value.setter + def coded_value(self, coded_value): + _warn_deprecated_setter('coded_value') + self._coded_value = coded_value + def __setitem__(self, K, V): K = K.lower() if not K in self._reserved: - raise CookieError("Invalid Attribute %s" % K) + raise CookieError("Invalid Attribute '%r'" % K) dict.__setitem__(self, K, V) + def setdefault(self, key, val=None): + key = key.lower() + if key not in self._reserved: + raise CookieError("Invalid Attribute '%r'" % key) + return dict.setdefault(self, key, val) + + def __ne__(self, morsel): + if not isinstance(morsel, Morsel): + return True + + return dict.__ne__(self, morsel) or \ + self.value != morsel.value or self.key != morsel.key or \ + self.coded_value != morsel.coded_value + + def __eq__(self, morsel): + if not isinstance(morsel, Morsel): + return False + + 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() + dict.update(morsel, self) + morsel.__dict__.update(self.__dict__) + return morsel + + def update(self, values): + data = {} + for key, val in dict(values).items(): + key = key.lower() + if key not in self._reserved: + raise CookieError('Invalid attribute: {}'.format(key)) + data[key] = val + dict.update(self, data) + 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 not _LegalMorselKeyPatt.fullmatch(key): + raise CookieError('Illegal key: %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 +394,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 +428,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 9332a545ad85 Lib/test/test_http_cookies.py --- a/Lib/test/test_http_cookies.py Thu Mar 12 22:01:30 2015 +0200 +++ b/Lib/test/test_http_cookies.py Fri Mar 13 14:17:57 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,136 @@ self.assertRaises(cookies.CookieError, M.set, i, '%s_value' % i, '%s_value' % i) + def test_deprecation(self): + morsel = cookies.Morsel() + + exp = re.escape(cookies._DEPRECATED_SETTER).replace('\\%s', '\w{1,}') + for attr in ('key', 'value', 'coded_value'): + with self.assertWarns( + DeprecationWarning, msg='Illegal key: %s' % attr): + setattr(morsel, attr, '') + + def test_eq(self): + base_case = ('key', 'value', 'value') + attribs = { + 'path': '/', + 'comment': 'foo', + 'domain': 'example.com', + 'version': 2, + } + cases = ( + (base_case, base_case), + (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_a.update(attribs) + morsel_b = cookies.Morsel() + morsel_b.update(attribs) + + morsel_a.set(*case_a) + morsel_b.set(*case_b) + + with self.subTest((morsel_a, morsel_b)): + self.assertEqual(morsel_a == morsel_b, case_a == case_b) + + # test mismatched types + self.assertNotEqual(cookies.Morsel(), 1) + self.assertNotEqual(cookies.Morsel(), '') + + # morsel/dict + morsel = cookies.Morsel() + morsel.set('foo', 'bar', 'baz') + morsel['version'] == 2 + # test both __eq__ and __ne__ + self.assertNotEqual(morsel, dict(morsel)) + self.assertFalse(morsel == dict(morsel)) + + def test_copy(self): + morsel_a = cookies.Morsel() + morsel_a.set('foo', 'bar', 'baz') + morsel_a.update({ + 'version': 2, + 'comment': 'foo', + }) + + morsel_b = copy.copy(morsel_a) + + self.assertIsInstance(morsel_b, cookies.Morsel) + self.assertNotEqual(id(morsel_a), id(morsel_b)) + self.assertEqual(morsel_a, morsel_b) + + def test_setitem(self): + morsel = cookies.Morsel() + morsel['expires'] = 0 + self.assertEqual(morsel['expires'], 0) + + with self.assertRaises(cookies.CookieError): + morsel['foo'] = 'bar' + self.assertNotIn('foo', morsel) + + def test_update(self): + morsel = cookies.Morsel() + + # test dict update + morsel.update({'expires': 1}) + self.assertEqual(morsel['expires'], 1) + + # test iterator/iterable update + morsel.update( + (k, v) for k, v in (('path', '/'), ('comment', 'foo'))) + self.assertEqual(morsel['path'], '/') + self.assertEqual(morsel['comment'], 'foo') + morsel.update((('path', '/foo'), ('comment', 'bar'))) + self.assertEqual(morsel['path'], '/foo') + self.assertEqual(morsel['comment'], 'bar') + + with self.assertRaises(cookies.CookieError): + morsel.update({'foo': 'bar'}) + self.assertNotIn('foo', morsel) + + # ensure that __setitem__ and update yield the same translated + # key values + morsel = cookies.Morsel() + morsel.update({'Expires': 0}) + morsel['Version'] = 0 + self.assertIn('expires', morsel) + self.assertIn('version', morsel) + + 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() + self.assertEqual(len(morsel), len(morsel._reserved)) + + def test_setdefault(self): + morsel = cookies.Morsel() + with self.assertRaises(cookies.CookieError): + morsel.setdefault('invalid', 'value') + + self.assertEqual(morsel.setdefault('Version', 'value'), '') + self.assertEqual(morsel.setdefault('DOMAIN', 'value'), '') + + # this shouldn't override the default value + self.assertEqual(morsel.setdefault('expires', 'value'), '') def test_main(): run_unittest(CookieTests, MorselTests)