Commit 76e155a1 authored by Georg Brandl's avatar Georg Brandl

#3788: more tests for http.cookies, now at 95% coverage. Also bring coding...

#3788: more tests for http.cookies, now at 95% coverage.  Also bring coding style in the module up to PEP 8, where it does not break backwards compatibility.
parent 7b280e91
...@@ -46,7 +46,7 @@ At the moment, this is the only documentation. ...@@ -46,7 +46,7 @@ At the moment, this is the only documentation.
The Basics The Basics
---------- ----------
Importing is easy.. Importing is easy...
>>> from http import cookies >>> from http import cookies
...@@ -127,19 +127,14 @@ the value to a string, when the values are set dictionary-style. ...@@ -127,19 +127,14 @@ the value to a string, when the values are set dictionary-style.
'Set-Cookie: number=7\r\nSet-Cookie: string=seven' 'Set-Cookie: number=7\r\nSet-Cookie: string=seven'
Finis. Finis.
""" #" """
# ^
# |----helps out font-lock
# #
# Import our required modules # Import our required modules
# #
import re
import string import string
from pickle import dumps, loads
import re, warnings
__all__ = ["CookieError", "BaseCookie", "SimpleCookie"] __all__ = ["CookieError", "BaseCookie", "SimpleCookie"]
_nulljoin = ''.join _nulljoin = ''.join
...@@ -235,7 +230,7 @@ def _quote(str, LegalChars=_LegalChars): ...@@ -235,7 +230,7 @@ def _quote(str, LegalChars=_LegalChars):
if all(c in LegalChars for c in str): if all(c in LegalChars for c in str):
return str return str
else: else:
return '"' + _nulljoin( map(_Translator.get, str, str) ) + '"' return '"' + _nulljoin(map(_Translator.get, str, str)) + '"'
_OctalPatt = re.compile(r"\\[0-3][0-7][0-7]") _OctalPatt = re.compile(r"\\[0-3][0-7][0-7]")
...@@ -244,7 +239,7 @@ _QuotePatt = re.compile(r"[\\].") ...@@ -244,7 +239,7 @@ _QuotePatt = re.compile(r"[\\].")
def _unquote(str): def _unquote(str):
# If there aren't any doublequotes, # If there aren't any doublequotes,
# then there can't be any special characters. See RFC 2109. # then there can't be any special characters. See RFC 2109.
if len(str) < 2: if len(str) < 2:
return str return str
if str[0] != '"' or str[-1] != '"': if str[0] != '"' or str[-1] != '"':
return str return str
...@@ -263,31 +258,32 @@ def _unquote(str): ...@@ -263,31 +258,32 @@ def _unquote(str):
n = len(str) n = len(str)
res = [] res = []
while 0 <= i < n: while 0 <= i < n:
Omatch = _OctalPatt.search(str, i) o_match = _OctalPatt.search(str, i)
Qmatch = _QuotePatt.search(str, i) q_match = _QuotePatt.search(str, i)
if not Omatch and not Qmatch: # Neither matched if not o_match and not q_match: # Neither matched
res.append(str[i:]) res.append(str[i:])
break break
# else: # else:
j = k = -1 j = k = -1
if Omatch: j = Omatch.start(0) if o_match:
if Qmatch: k = Qmatch.start(0) j = o_match.start(0)
if Qmatch and ( not Omatch or k < j ): # QuotePatt matched if q_match:
k = q_match.start(0)
if q_match and (not o_match or k < j): # QuotePatt matched
res.append(str[i:k]) res.append(str[i:k])
res.append(str[k+1]) res.append(str[k+1])
i = k+2 i = k + 2
else: # OctalPatt matched else: # OctalPatt matched
res.append(str[i:j]) res.append(str[i:j])
res.append( chr( int(str[j+1:j+4], 8) ) ) res.append(chr(int(str[j+1:j+4], 8)))
i = j+4 i = j + 4
return _nulljoin(res) return _nulljoin(res)
# The _getdate() routine is used to set the expiration time in # The _getdate() routine is used to set the expiration time in the cookie's HTTP
# the cookie's HTTP header. By default, _getdate() returns the # header. By default, _getdate() returns the current time in the appropriate
# current time in the appropriate "expires" format for a # "expires" format for a Set-Cookie header. The one optional argument is an
# Set-Cookie header. The one optional argument is an offset from # offset from now, in seconds. For example, an offset of -3600 means "one hour
# now, in seconds. For example, an offset of -3600 means "one hour ago". # ago". The offset may be a floating point number.
# The offset may be a floating point number.
# #
_weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] _weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
...@@ -305,7 +301,7 @@ def _getdate(future=0, weekdayname=_weekdayname, monthname=_monthname): ...@@ -305,7 +301,7 @@ def _getdate(future=0, weekdayname=_weekdayname, monthname=_monthname):
class Morsel(dict): class Morsel(dict):
"""A class to hold ONE key,value pair. """A class to hold ONE (key, value) pair.
In a cookie, each such pair may have several attributes, so this class is In a cookie, each such pair may have several attributes, so this class is
used to keep the attributes associated with the appropriate key,value pair. used to keep the attributes associated with the appropriate key,value pair.
...@@ -326,23 +322,24 @@ class Morsel(dict): ...@@ -326,23 +322,24 @@ class Morsel(dict):
# This dictionary provides a mapping from the lowercase # This dictionary provides a mapping from the lowercase
# variant on the left to the appropriate traditional # variant on the left to the appropriate traditional
# formatting on the right. # formatting on the right.
_reserved = { "expires" : "expires", _reserved = {
"path" : "Path", "expires" : "expires",
"comment" : "Comment", "path" : "Path",
"domain" : "Domain", "comment" : "Comment",
"max-age" : "Max-Age", "domain" : "Domain",
"secure" : "secure", "max-age" : "Max-Age",
"httponly" : "httponly", "secure" : "secure",
"version" : "Version", "httponly" : "httponly",
} "version" : "Version",
}
def __init__(self): def __init__(self):
# Set defaults # Set defaults
self.key = self.value = self.coded_value = None self.key = self.value = self.coded_value = None
# Set default attributes # Set default attributes
for K in self._reserved: for key in self._reserved:
dict.__setitem__(self, K, "") dict.__setitem__(self, key, "")
def __setitem__(self, K, V): def __setitem__(self, K, V):
K = K.lower() K = K.lower()
...@@ -362,18 +359,18 @@ class Morsel(dict): ...@@ -362,18 +359,18 @@ class Morsel(dict):
raise CookieError("Illegal key value: %s" % key) raise CookieError("Illegal key value: %s" % key)
# It's a good key, so save it. # It's a good key, so save it.
self.key = key self.key = key
self.value = val self.value = val
self.coded_value = coded_val self.coded_value = coded_val
def output(self, attrs=None, header = "Set-Cookie:"): def output(self, attrs=None, header="Set-Cookie:"):
return "%s %s" % ( header, self.OutputString(attrs) ) return "%s %s" % (header, self.OutputString(attrs))
__str__ = output __str__ = output
def __repr__(self): def __repr__(self):
return '<%s: %s=%s>' % (self.__class__.__name__, return '<%s: %s=%s>' % (self.__class__.__name__,
self.key, repr(self.value) ) self.key, repr(self.value))
def js_output(self, attrs=None): def js_output(self, attrs=None):
# Print javascript # Print javascript
...@@ -383,34 +380,36 @@ class Morsel(dict): ...@@ -383,34 +380,36 @@ class Morsel(dict):
document.cookie = \"%s\"; document.cookie = \"%s\";
// end hiding --> // end hiding -->
</script> </script>
""" % ( self.OutputString(attrs).replace('"',r'\"')) """ % (self.OutputString(attrs).replace('"', r'\"'))
def OutputString(self, attrs=None): def OutputString(self, attrs=None):
# Build up our result # Build up our result
# #
result = [] result = []
RA = result.append append = result.append
# First, the key=value pair # First, the key=value pair
RA("%s=%s" % (self.key, self.coded_value)) append("%s=%s" % (self.key, self.coded_value))
# Now add any defined attributes # Now add any defined attributes
if attrs is None: if attrs is None:
attrs = self._reserved attrs = self._reserved
items = sorted(self.items()) items = sorted(self.items())
for K,V in items: for key, value in items:
if V == "": continue if value == "":
if K not in attrs: continue continue
if K == "expires" and type(V) == type(1): if key not in attrs:
RA("%s=%s" % (self._reserved[K], _getdate(V))) continue
elif K == "max-age" and type(V) == type(1): if key == "expires" and isinstance(value, int):
RA("%s=%d" % (self._reserved[K], V)) append("%s=%s" % (self._reserved[key], _getdate(value)))
elif K == "secure": elif key == "max-age" and isinstance(value, int):
RA(str(self._reserved[K])) append("%s=%d" % (self._reserved[key], value))
elif K == "httponly": elif key == "secure":
RA(str(self._reserved[K])) append(str(self._reserved[key]))
elif key == "httponly":
append(str(self._reserved[key]))
else: else:
RA("%s=%s" % (self._reserved[K], V)) append("%s=%s" % (self._reserved[key], value))
# Return the result # Return the result
return _semispacejoin(result) return _semispacejoin(result)
...@@ -426,24 +425,23 @@ class Morsel(dict): ...@@ -426,24 +425,23 @@ class Morsel(dict):
# #
_LegalCharsPatt = r"[\w\d!#%&'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\=]" _LegalCharsPatt = r"[\w\d!#%&'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\=]"
_CookiePattern = re.compile( _CookiePattern = re.compile(r"""
r"(?x)" # This is a Verbose pattern (?x) # This is a verbose pattern
r"(?P<key>" # Start of group 'key' (?P<key> # Start of group 'key'
""+ _LegalCharsPatt +"+?" # Any word of at least one letter, nongreedy """ + _LegalCharsPatt + r"""+? # Any word of at least one letter
r")" # End of group 'key' ) # End of group 'key'
r"\s*=\s*" # Equal Sign \s*=\s* # Equal Sign
r"(?P<val>" # Start of group 'val' (?P<val> # Start of group 'val'
r'"(?:[^\\"]|\\.)*"' # Any doublequoted string "(?:[^\\"]|\\.)*" # Any doublequoted string
r"|" # or | # or
""+ _LegalCharsPatt +"*" # Any word or empty string """ + _LegalCharsPatt + r"""* # Any word or empty string
r")" # End of group 'val' ) # End of group 'val'
r"\s*;?" # Probably ending in a semi-colon \s*;? # Probably ending in a semi-colon
, re.ASCII) # May be removed if safe. """, re.ASCII) # May be removed if safe.
# At long last, here is the cookie class. # At long last, here is the cookie class. Using this class is almost just like
# Using this class is almost just like using a dictionary. # using a dictionary. See this module's docstring for example usage.
# See this module's docstring for example usage.
# #
class BaseCookie(dict): class BaseCookie(dict):
"""A container class for a set of Morsels.""" """A container class for a set of Morsels."""
...@@ -467,7 +465,8 @@ class BaseCookie(dict): ...@@ -467,7 +465,8 @@ class BaseCookie(dict):
return strval, strval return strval, strval
def __init__(self, input=None): def __init__(self, input=None):
if input: self.load(input) if input:
self.load(input)
def __set(self, key, real_value, coded_value): def __set(self, key, real_value, coded_value):
"""Private method for setting a cookie's value""" """Private method for setting a cookie's value"""
...@@ -484,25 +483,25 @@ class BaseCookie(dict): ...@@ -484,25 +483,25 @@ class BaseCookie(dict):
"""Return a string suitable for HTTP.""" """Return a string suitable for HTTP."""
result = [] result = []
items = sorted(self.items()) items = sorted(self.items())
for K,V in items: for key, value in items:
result.append( V.output(attrs, header) ) result.append(value.output(attrs, header))
return sep.join(result) return sep.join(result)
__str__ = output __str__ = output
def __repr__(self): def __repr__(self):
L = [] l = []
items = sorted(self.items()) items = sorted(self.items())
for K,V in items: for key, value in items:
L.append( '%s=%s' % (K,repr(V.value) ) ) l.append('%s=%s' % (key, repr(value.value)))
return '<%s: %s>' % (self.__class__.__name__, _spacejoin(L)) return '<%s: %s>' % (self.__class__.__name__, _spacejoin(l))
def js_output(self, attrs=None): def js_output(self, attrs=None):
"""Return a string suitable for JavaScript.""" """Return a string suitable for JavaScript."""
result = [] result = []
items = sorted(self.items()) items = sorted(self.items())
for K,V in items: for key, value in items:
result.append( V.js_output(attrs) ) result.append(value.js_output(attrs))
return _nulljoin(result) return _nulljoin(result)
def load(self, rawdata): def load(self, rawdata):
...@@ -511,15 +510,15 @@ class BaseCookie(dict): ...@@ -511,15 +510,15 @@ class BaseCookie(dict):
is equivalent to calling: is equivalent to calling:
map(Cookie.__setitem__, d.keys(), d.values()) map(Cookie.__setitem__, d.keys(), d.values())
""" """
if type(rawdata) == type(""): if isinstance(rawdata, str):
self.__ParseString(rawdata) self.__parse_string(rawdata)
else: else:
# self.update() wouldn't call our custom __setitem__ # self.update() wouldn't call our custom __setitem__
for k, v in rawdata.items(): for key, value in rawdata.items():
self[k] = v self[key] = value
return return
def __ParseString(self, str, patt=_CookiePattern): def __parse_string(self, str, patt=_CookiePattern):
i = 0 # Our starting point i = 0 # Our starting point
n = len(str) # Length of string n = len(str) # Length of string
M = None # current morsel M = None # current morsel
...@@ -527,25 +526,27 @@ class BaseCookie(dict): ...@@ -527,25 +526,27 @@ class BaseCookie(dict):
while 0 <= i < n: while 0 <= i < n:
# Start looking for a cookie # Start looking for a cookie
match = patt.search(str, i) match = patt.search(str, i)
if not match: break # No more cookies if not match:
# No more cookies
break
K,V = match.group("key"), match.group("val") key, value = match.group("key"), match.group("val")
i = match.end(0) i = match.end(0)
# Parse the key, value in case it's metainfo # Parse the key, value in case it's metainfo
if K[0] == "$": if key[0] == "$":
# We ignore attributes which pertain to the cookie # We ignore attributes which pertain to the cookie
# mechanism as a whole. See RFC 2109. # mechanism as a whole. See RFC 2109.
# (Does anyone care?) # (Does anyone care?)
if M: if M:
M[ K[1:] ] = V M[key[1:]] = value
elif K.lower() in Morsel._reserved: elif key.lower() in Morsel._reserved:
if M: if M:
M[ K ] = _unquote(V) M[key] = _unquote(value)
else: else:
rval, cval = self.value_decode(V) rval, cval = self.value_decode(value)
self.__set(K, rval, cval) self.__set(key, rval, cval)
M = self[K] M = self[key]
class SimpleCookie(BaseCookie): class SimpleCookie(BaseCookie):
...@@ -556,16 +557,8 @@ class SimpleCookie(BaseCookie): ...@@ -556,16 +557,8 @@ class SimpleCookie(BaseCookie):
received from HTTP are kept as strings. received from HTTP are kept as strings.
""" """
def value_decode(self, val): def value_decode(self, val):
return _unquote( val ), val return _unquote(val), val
def value_encode(self, val): def value_encode(self, val):
strval = str(val) strval = str(val)
return strval, _quote( strval ) return strval, _quote(strval)
###########################################################
def _test():
import doctest, http.cookies
return doctest.testmod(http.cookies)
if __name__ == "__main__":
_test()
...@@ -19,24 +19,21 @@ class CookieTests(unittest.TestCase): ...@@ -19,24 +19,21 @@ class CookieTests(unittest.TestCase):
def test_basic(self): def test_basic(self):
cases = [ cases = [
{ 'data': 'chips=ahoy; vienna=finger', {'data': 'chips=ahoy; vienna=finger',
'dict': {'chips':'ahoy', 'vienna':'finger'}, 'dict': {'chips':'ahoy', 'vienna':'finger'},
'repr': "<SimpleCookie: chips='ahoy' vienna='finger'>", 'repr': "<SimpleCookie: chips='ahoy' vienna='finger'>",
'output': 'Set-Cookie: chips=ahoy\nSet-Cookie: vienna=finger', 'output': 'Set-Cookie: chips=ahoy\nSet-Cookie: vienna=finger'},
},
{'data': 'keebler="E=mc2; L=\\"Loves\\"; fudge=\\012;"',
{ 'data': 'keebler="E=mc2; L=\\"Loves\\"; fudge=\\012;"', 'dict': {'keebler' : 'E=mc2; L="Loves"; fudge=\012;'},
'dict': {'keebler' : 'E=mc2; L="Loves"; fudge=\012;'}, 'repr': '''<SimpleCookie: keebler='E=mc2; L="Loves"; fudge=\\n;'>''',
'repr': '''<SimpleCookie: keebler='E=mc2; L="Loves"; fudge=\\n;'>''', 'output': 'Set-Cookie: keebler="E=mc2; L=\\"Loves\\"; fudge=\\012;"'},
'output': 'Set-Cookie: keebler="E=mc2; L=\\"Loves\\"; fudge=\\012;"',
},
# Check illegal cookies that have an '=' char in an unquoted value # Check illegal cookies that have an '=' char in an unquoted value
{ 'data': 'keebler=E=mc2', {'data': 'keebler=E=mc2',
'dict': {'keebler' : 'E=mc2'}, 'dict': {'keebler' : 'E=mc2'},
'repr': "<SimpleCookie: keebler='E=mc2'>", 'repr': "<SimpleCookie: keebler='E=mc2'>",
'output': 'Set-Cookie: keebler=E=mc2', 'output': 'Set-Cookie: keebler=E=mc2'},
}
] ]
for case in cases: for case in cases:
...@@ -72,6 +69,26 @@ class CookieTests(unittest.TestCase): ...@@ -72,6 +69,26 @@ class CookieTests(unittest.TestCase):
</script> </script>
""") """)
def test_special_attrs(self):
# 'expires'
C = cookies.SimpleCookie('Customer="WILE_E_COYOTE"')
C['Customer']['expires'] = 0
# can't test exact output, it always depends on current date/time
self.assertTrue(C.output().endswith('GMT'))
# 'max-age'
C = cookies.SimpleCookie('Customer="WILE_E_COYOTE"')
C['Customer']['max-age'] = 10
self.assertEqual(C.output(),
'Set-Cookie: Customer="WILE_E_COYOTE"; Max-Age=10')
# others
C = cookies.SimpleCookie('Customer="WILE_E_COYOTE"')
C['Customer']['secure'] = True
C['Customer']['httponly'] = True
self.assertEqual(C.output(),
'Set-Cookie: Customer="WILE_E_COYOTE"; httponly; secure')
def test_quoted_meta(self): def test_quoted_meta(self):
# Try cookie with quoted meta-data # Try cookie with quoted meta-data
C = cookies.SimpleCookie() C = cookies.SimpleCookie()
...@@ -80,8 +97,72 @@ class CookieTests(unittest.TestCase): ...@@ -80,8 +97,72 @@ class CookieTests(unittest.TestCase):
self.assertEqual(C['Customer']['version'], '1') self.assertEqual(C['Customer']['version'], '1')
self.assertEqual(C['Customer']['path'], '/acme') self.assertEqual(C['Customer']['path'], '/acme')
self.assertEqual(C.output(['path']),
'Set-Cookie: Customer="WILE_E_COYOTE"; Path=/acme')
self.assertEqual(C.js_output(), r"""
<script type="text/javascript">
<!-- begin hiding
document.cookie = "Customer=\"WILE_E_COYOTE\"; Path=/acme; Version=1";
// end hiding -->
</script>
""")
self.assertEqual(C.js_output(['path']), r"""
<script type="text/javascript">
<!-- begin hiding
document.cookie = "Customer=\"WILE_E_COYOTE\"; Path=/acme";
// end hiding -->
</script>
""")
class MorselTests(unittest.TestCase):
"""Tests for the Morsel object."""
def test_reserved_keys(self):
M = cookies.Morsel()
# tests valid and invalid reserved keys for Morsels
for i in M._reserved:
# Test that all valid keys are reported as reserved and set them
self.assertTrue(M.isReservedKey(i))
M[i] = '%s_value' % i
for i in M._reserved:
# Test that valid key values come out fine
self.assertEqual(M[i], '%s_value' % i)
for i in "the holy hand grenade".split():
# Test that invalid keys raise CookieError
self.assertRaises(cookies.CookieError,
M.__setitem__, i, '%s_value' % i)
def test_setter(self):
M = cookies.Morsel()
# tests the .set method to set keys and their values
for i in M._reserved:
# Makes sure that all reserved keys can't be set this way
self.assertRaises(cookies.CookieError,
M.set, i, '%s_value' % i, '%s_value' % i)
for i in "thou cast _the- !holy! ^hand| +*grenade~".split():
# Try typical use case. Setting decent values.
# Check output and js_output.
M['path'] = '/foo' # Try a reserved key as well
M.set(i, "%s_val" % i, "%s_coded_val" % i)
self.assertEqual(
M.output(),
"Set-Cookie: %s=%s; Path=/foo" % (i, "%s_coded_val" % i))
expected_js_output = """
<script type="text/javascript">
<!-- begin hiding
document.cookie = "%s=%s; Path=/foo";
// end hiding -->
</script>
""" % (i, "%s_coded_val" % i)
self.assertEqual(M.js_output(), expected_js_output)
for i in ["foo bar", "foo@bar"]:
# Try some illegal characters
self.assertRaises(cookies.CookieError,
M.set, i, '%s_value' % i, '%s_value' % i)
def test_main(): def test_main():
run_unittest(CookieTests) run_unittest(CookieTests, MorselTests)
run_doctest(cookies) run_doctest(cookies)
if __name__ == '__main__': if __name__ == '__main__':
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment