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 Mon Mar 16 07:23:09 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 Mon Mar 16 07:23:09 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 Mon Mar 16 07:23:09 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,87 @@ 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 __eq__(self, morsel): + if not isinstance(morsel, Morsel): + return NotImplemented + + 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 +386,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 +420,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 Mon Mar 16 07:23:09 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,135 @@ 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 + self.assertEqual(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)

AltStyle によって変換されたページ (->オリジナル) /