summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@plope.com>2011-10-10 12:00:59 -0400
committerChris McDonough <chrism@plope.com>2011-10-10 12:00:59 -0400
commit4b29bdaf6af8161320eb86ef25031afc11c4c083 (patch)
tree6aef2b2fee3496563aa793beb3271d2f481281cc
parent7c963af17947612b3b0a67ca09bd60da12594e13 (diff)
downloadwebob-feature.cookie-bug-for-bug.tar.gz
* Mutating the ``request.cookies`` property now reflects the mutations intofeature.cookie-bug-for-bug
the ``HTTP_COOKIES`` environ header.
-rw-r--r--docs/news.txt5
-rw-r--r--tests/test_cookies.py183
-rw-r--r--tests/test_request.py15
-rw-r--r--webob/cookies.py134
-rw-r--r--webob/request.py19
5 files changed, 340 insertions, 16 deletions
diff --git a/docs/news.txt b/docs/news.txt
index b8fdca6..488733c 100644
--- a/docs/news.txt
+++ b/docs/news.txt
@@ -4,9 +4,8 @@ News
master
---------
-* Mutating the ``request.cookies`` property now retains the mutated value
- during the course of a single request (bug-for-bug compatibility with
- previous versions; in-the-wild codebases depend upon this behavior).
+* Mutating the ``request.cookies`` property now reflects the mutations into
+ the ``HTTP_COOKIES`` environ header.
* ``Response.etag = (tag, False)`` sets weak etag.
diff --git a/tests/test_cookies.py b/tests/test_cookies.py
index 923978d..6a4c163 100644
--- a/tests/test_cookies.py
+++ b/tests/test_cookies.py
@@ -3,6 +3,9 @@ from datetime import timedelta
from webob import cookies
from webob.compat import text_
from nose.tools import eq_
+import unittest
+from webob.compat import native_
+from webob.compat import PY3
def test_cookie_empty():
c = cookies.Cookie() # empty cookie
@@ -140,3 +143,183 @@ def test_morsel_repr():
result = repr(v)
eq_(result, "<Morsel: a='b'>")
+class TestRequestCookies(unittest.TestCase):
+ def _makeOne(self, environ):
+ from webob.cookies import RequestCookies
+ return RequestCookies(environ)
+
+ def test_get_no_cache_key_in_environ_no_http_cookie_header(self):
+ environ = {}
+ inst = self._makeOne(environ)
+ self.assertEqual(inst.get('a'), None)
+ parsed = environ['webob._parsed_cookies']
+ self.assertEqual(parsed, ({}, ''))
+
+ def test_get_no_cache_key_in_environ_has_http_cookie_header(self):
+ header ='a=1; b=2'
+ environ = {'HTTP_COOKIE':header}
+ inst = self._makeOne(environ)
+ self.assertEqual(inst.get('a'), '1')
+ parsed = environ['webob._parsed_cookies'][0]
+ self.assertEqual(parsed['a'], '1')
+ self.assertEqual(parsed['b'], '2')
+ self.assertEqual(environ['HTTP_COOKIE'], header) # no change
+
+ def test_get_cache_key_in_environ_no_http_cookie_header(self):
+ environ = {'webob._parsed_cookies':({}, '')}
+ inst = self._makeOne(environ)
+ self.assertEqual(inst.get('a'), None)
+ parsed = environ['webob._parsed_cookies']
+ self.assertEqual(parsed, ({}, ''))
+
+ def test_get_cache_key_in_environ_has_http_cookie_header(self):
+ header ='a=1; b=2'
+ environ = {'HTTP_COOKIE':header, 'webob._parsed_cookies':({}, '')}
+ inst = self._makeOne(environ)
+ self.assertEqual(inst.get('a'), '1')
+ parsed = environ['webob._parsed_cookies'][0]
+ self.assertEqual(parsed['a'], '1')
+ self.assertEqual(parsed['b'], '2')
+ self.assertEqual(environ['HTTP_COOKIE'], header) # no change
+
+ def test___setitem__name_not_string_type(self):
+ inst = self._makeOne({})
+ self.assertRaises(TypeError, inst.__setitem__, None, 1)
+
+ def test___setitem__name_not_encodeable_to_ascii(self):
+ name = native_(b'La Pe\xc3\xb1a', 'utf-8')
+ inst = self._makeOne({})
+ self.assertRaises(TypeError, inst.__setitem__, name, 'abc')
+
+ def test___setitem__name_not_rfc2109_valid(self):
+ name = '$a'
+ inst = self._makeOne({})
+ self.assertRaises(TypeError, inst.__setitem__, name, 'abc')
+
+ def test___setitem__value_not_string_type(self):
+ inst = self._makeOne({})
+ self.assertRaises(ValueError, inst.__setitem__, 'a', None)
+
+ def test___setitem__value_not_utf_8_decodeable(self):
+ value = text_(b'La Pe\xc3\xb1a', 'utf-8')
+ value = value.encode('utf-16')
+ inst = self._makeOne({})
+ self.assertRaises(ValueError, inst.__setitem__, 'a', value)
+
+ def test__setitem__success_no_existing_headers(self):
+ value = native_(b'La Pe\xc3\xb1a', 'utf-8')
+ environ = {}
+ inst = self._makeOne(environ)
+ inst['a'] = value
+ self.assertEqual(environ['HTTP_COOKIE'], 'a="La Pe\\303\\261a"')
+
+ def test__setitem__success_append(self):
+ value = native_(b'La Pe\xc3\xb1a', 'utf-8')
+ environ = {'HTTP_COOKIE':'a=1; b=2'}
+ inst = self._makeOne(environ)
+ inst['c'] = value
+ self.assertEqual(
+ environ['HTTP_COOKIE'], 'a=1; b=2; c="La Pe\\303\\261a"')
+
+ def test__setitem__success_replace(self):
+ environ = {'HTTP_COOKIE':'a=1; b="La Pe\\303\\261a"; c=3'}
+ inst = self._makeOne(environ)
+ inst['b'] = 'abc'
+ self.assertEqual(environ['HTTP_COOKIE'], 'a=1; b=abc; c=3')
+ inst['c'] = '4'
+ self.assertEqual(environ['HTTP_COOKIE'], 'a=1; b=abc; c=4')
+
+ def test__delitem__fail_no_http_cookie(self):
+ environ = {}
+ inst = self._makeOne(environ)
+ self.assertRaises(KeyError, inst.__delitem__, 'a')
+ self.assertEqual(environ, {})
+
+ def test__delitem__fail_with_http_cookie(self):
+ environ = {'HTTP_COOKIE':''}
+ inst = self._makeOne(environ)
+ self.assertRaises(KeyError, inst.__delitem__, 'a')
+ self.assertEqual(environ, {'HTTP_COOKIE':''})
+
+ def test__delitem__success(self):
+ environ = {'HTTP_COOKIE':'a=1'}
+ inst = self._makeOne(environ)
+ del inst['a']
+ self.assertEqual(environ['HTTP_COOKIE'], '')
+ self.assertEqual(inst._cache, {})
+
+ def test_keys(self):
+ environ = {'HTTP_COOKIE':'a=1; b="La Pe\\303\\261a"; c=3'}
+ inst = self._makeOne(environ)
+ self.assertEqual(sorted(list(inst.keys())), ['a', 'b', 'c'])
+
+ def test_values(self):
+ val = text_(b'La Pe\xc3\xb1a', 'utf-8')
+ environ = {'HTTP_COOKIE':'a=1; b="La Pe\\303\\261a"; c=3'}
+ inst = self._makeOne(environ)
+ self.assertEqual(sorted(list(inst.values())), ['1', '3', val])
+
+ def test_items(self):
+ val = text_(b'La Pe\xc3\xb1a', 'utf-8')
+ environ = {'HTTP_COOKIE':'a=1; b="La Pe\\303\\261a"; c=3'}
+ inst = self._makeOne(environ)
+ self.assertEqual(sorted(list(inst.items())),
+ [('a', '1'), ('b', val), ('c', '3')])
+
+ if not PY3:
+ def test_iterkeys(self):
+ environ = {'HTTP_COOKIE':'a=1; b="La Pe\\303\\261a"; c=3'}
+ inst = self._makeOne(environ)
+ self.assertEqual(sorted(list(inst.iterkeys())), ['a', 'b', 'c'])
+
+ def test_itervalues(self):
+ val = text_(b'La Pe\xc3\xb1a', 'utf-8')
+ environ = {'HTTP_COOKIE':'a=1; b="La Pe\\303\\261a"; c=3'}
+ inst = self._makeOne(environ)
+ self.assertEqual(sorted(list(inst.itervalues())), ['1', '3', val])
+
+ def test_iteritems(self):
+ val = text_(b'La Pe\xc3\xb1a', 'utf-8')
+ environ = {'HTTP_COOKIE':'a=1; b="La Pe\\303\\261a"; c=3'}
+ inst = self._makeOne(environ)
+ self.assertEqual(sorted(list(inst.iteritems())),
+ [('a', '1'), ('b', val), ('c', '3')])
+
+ def test___contains__(self):
+ environ = {'HTTP_COOKIE':'a=1'}
+ inst = self._makeOne(environ)
+ self.assertTrue('a' in inst)
+ self.assertFalse('b' in inst)
+
+ def test___iter__(self):
+ environ = {'HTTP_COOKIE':'a=1; b=2; c=3'}
+ inst = self._makeOne(environ)
+ self.assertEqual(sorted(list(iter(inst))), ['a', 'b', 'c'])
+
+ def test___len__(self):
+ environ = {'HTTP_COOKIE':'a=1; b=2; c=3'}
+ inst = self._makeOne(environ)
+ self.assertEqual(len(inst), 3)
+ del inst['a']
+ self.assertEqual(len(inst), 2)
+
+ def test_clear(self):
+ environ = {'HTTP_COOKIE':'a=1; b=2; c=3'}
+ inst = self._makeOne(environ)
+ inst.clear()
+ self.assertEqual(environ['HTTP_COOKIE'], '')
+ self.assertEqual(inst.get('a'), None)
+
+ def test___repr__(self):
+ environ = {'HTTP_COOKIE':'a=1; b=2; c=3'}
+ inst = self._makeOne(environ)
+ r = repr(inst)
+ self.assertTrue(r.startswith(
+ '<RequestCookies (dict-like) with values '))
+ self.assertTrue(r.endswith('>'))
+
+
+
+
+
+
diff --git a/tests/test_request.py b/tests/test_request.py
index 9120425..70956db 100644
--- a/tests/test_request.py
+++ b/tests/test_request.py
@@ -1,3 +1,4 @@
+import collections
import unittest, warnings
from webob.request import Request
from webob.request import BaseRequest
@@ -790,6 +791,16 @@ class BaseRequestTests(unittest.TestCase):
req = BaseRequest(environ)
self.assertEqual(req.cookies, {'a': 'b'})
+ def test_set_cookies(self):
+ environ = {
+ 'HTTP_COOKIE': 'a=b',
+ }
+ req = BaseRequest(environ)
+ req.cookies = {'a':'1', 'b': '2'}
+ self.assertEqual(req.cookies, {'a': '1', 'b':'2'})
+ rcookies = [x.strip() for x in environ['HTTP_COOKIE'].split(';')]
+ self.assertEqual(sorted(rcookies), ['a=1', 'b=2'])
+
def test_is_xhr_no_header(self):
req = BaseRequest({})
self.assert_(not req.is_xhr)
@@ -1531,7 +1542,7 @@ class RequestTests_functional(unittest.TestCase):
'datetime.datetime(1994, 10, 29, 19, 43, 31, tzinfo=UTC)',
"user_agent: 'Mozilla",
'is_xhr: True',
- "cookies is {",
+ "cookies is <RequestCookies",
'var1',
'value1',
'params is NestedMultiDict',
@@ -2237,7 +2248,7 @@ class RequestTests_functional(unittest.TestCase):
# Cookies
req.headers['Cookie'] = 'test=value'
- self.assert_(isinstance(req.cookies, dict))
+ self.assert_(isinstance(req.cookies, collections.MutableMapping))
self.assertEqual(list(req.cookies.items()), [('test', 'value')])
req.charset = None
self.assertEqual(req.cookies, {'test': 'value'})
diff --git a/webob/cookies.py b/webob/cookies.py
index aa6f65e..1b5d907 100644
--- a/webob/cookies.py
+++ b/webob/cookies.py
@@ -1,3 +1,5 @@
+import collections
+
from datetime import (
date,
datetime,
@@ -11,11 +13,143 @@ from webob.compat import (
PY3,
text_type,
bytes_,
+ text_,
native_,
+ string_types,
)
__all__ = ['Cookie']
+_marker = object()
+
+class RequestCookies(collections.MutableMapping):
+
+ _cache_key = 'webob._parsed_cookies'
+
+ def __init__(self, environ):
+ self._environ = environ
+
+ @property
+ def _cache(self):
+ env = self._environ
+ header = env.get('HTTP_COOKIE', '')
+ cache, cache_header = env.get(self._cache_key, ({}, None))
+ if cache_header == header:
+ return cache
+ d = lambda b: b.decode('utf8')
+ cache = dict((d(k), d(v)) for k,v in parse_cookie(header))
+ env[self._cache_key] = (cache, header)
+ return cache
+
+ def _mutate_header(self, name, value):
+ header = self._environ.get('HTTP_COOKIE')
+ had_header = header is not None
+ header = header or ''
+ if PY3: # pragma: no cover
+ header = header.encode('latin-1')
+ bytes_name = bytes_(name, 'ascii')
+ if value is None:
+ replacement = None
+ else:
+ bytes_val = _quote(bytes_(value, 'utf-8'))
+ replacement = bytes_name + b'=' + bytes_val
+ matches = _rx_cookie.finditer(header)
+ found = False
+ for match in matches:
+ start, end = match.span()
+ match_name = match.group(1)
+ if match_name == bytes_name:
+ found = True
+ if replacement is None: # remove value
+ header = header[:start].rstrip(b' ;') + header[end:]
+ else: # replace value
+ header = header[:start] + replacement + header[end:]
+ break
+ else:
+ if replacement is not None:
+ if header:
+ header += b'; ' + replacement
+ else:
+ header = replacement
+
+ if header:
+ self._environ['HTTP_COOKIE'] = native_(header, 'latin-1')
+ elif had_header:
+ self._environ['HTTP_COOKIE'] = ''
+
+ return found
+
+ def _valid_cookie_name(self, name):
+ if not isinstance(name, string_types):
+ raise TypeError(name, 'cookie name must be a string')
+ if not isinstance(name, text_type):
+ name = text_(name, 'utf-8')
+ try:
+ bytes_cookie_name = bytes_(name, 'ascii')
+ except UnicodeEncodeError:
+ raise TypeError('cookie name must be encodable to ascii')
+ if not _valid_cookie_name(bytes_cookie_name):
+ raise TypeError('cookie name must be valid according to RFC 2109')
+ return name
+
+ def __setitem__(self, name, value):
+ name = self._valid_cookie_name(name)
+ if not isinstance(value, string_types):
+ raise ValueError(value, 'cookie value must be a string')
+ if not isinstance(value, text_type):
+ try:
+ value = text_(value, 'utf-8')
+ except UnicodeDecodeError:
+ raise ValueError(
+ value, 'cookie value must be utf-8 binary or unicode')
+ self._mutate_header(name, value)
+
+ def __getitem__(self, name):
+ return self._cache[name]
+
+ def get(self, name, default=None):
+ return self._cache.get(name)
+
+ def __delitem__(self, name):
+ name = self._valid_cookie_name(name)
+ found = self._mutate_header(name, None)
+ if not found:
+ raise KeyError(name)
+
+ def keys(self):
+ return self._cache.keys()
+
+ def values(self):
+ return self._cache.values()
+
+ def items(self):
+ return self._cache.items()
+
+ if not PY3:
+ def iterkeys(self):
+ return self._cache.iterkeys()
+
+ def itervalues(self):
+ return self._cache.itervalues()
+
+ def iteritems(self):
+ return self._cache.iteritems()
+
+ def __contains__(self, name):
+ return name in self._cache
+
+ def __iter__(self):
+ return self._cache.__iter__()
+
+ def __len__(self):
+ return len(self._cache)
+
+ def clear(self):
+ self._environ['HTTP_COOKIE'] = ''
+
+ def __repr__(self):
+ return '<RequestCookies (dict-like) with values %r>' % (self._cache,)
+
class Cookie(dict):
def __init__(self, input=None):
if input:
diff --git a/webob/request.py b/webob/request.py
index bf6e826..f81cf9e 100644
--- a/webob/request.py
+++ b/webob/request.py
@@ -35,7 +35,7 @@ from webob.compat import (
urlparse,
)
-from webob.cookies import parse_cookie
+from webob.cookies import RequestCookies
from webob.descriptors import (
CHARSET_RE,
@@ -682,16 +682,13 @@ class BaseRequest(object):
"""
Return a dictionary of cookies as found in the request.
"""
- env = self.environ
- data = self.environ.get('HTTP_COOKIE', '')
- if 'webob._parsed_cookies' in env:
- vars, var_source = env['webob._parsed_cookies']
- if var_source == data:
- return vars
- d = lambda b: b.decode('utf8')
- vars = dict((d(k), d(v)) for k,v in parse_cookie(data))
- env['webob._parsed_cookies'] = (vars, data)
- return vars
+ return RequestCookies(self.environ)
+
+ @cookies.setter
+ def cookies(self, val):
+ self.environ.pop('HTTP_COOKIE', None)
+ r = RequestCookies(self.environ)
+ r.update(val)
def copy(self):
"""