[Python-checkins] r83361 - in python/branches/py3k/Lib: http/cookies.py test/test_http_cookies.py

georg.brandl python-checkins at python.org
Sat Jul 31 23:04:00 CEST 2010


Author: georg.brandl
Date: Sat Jul 31 23:04:00 2010
New Revision: 83361

Log:
#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.

Modified:
   python/branches/py3k/Lib/http/cookies.py
   python/branches/py3k/Lib/test/test_http_cookies.py

Modified: python/branches/py3k/Lib/http/cookies.py
==============================================================================
--- python/branches/py3k/Lib/http/cookies.py	(original)
+++ python/branches/py3k/Lib/http/cookies.py	Sat Jul 31 23:04:00 2010
@@ -46,7 +46,7 @@
 The Basics
 ----------
 
-Importing is easy..
+Importing is easy...
 
    >>> from http import cookies
 
@@ -127,19 +127,14 @@
    'Set-Cookie: number=7\r\nSet-Cookie: string=seven'
 
 Finis.
-"""  #"
-#     ^
-#     |----helps out font-lock
+"""
 
 #
 # Import our required modules
 #
+import re
 import string
 
-from pickle import dumps, loads
-
-import re, warnings
-
 __all__ = ["CookieError", "BaseCookie", "SimpleCookie"]
 
 _nulljoin = ''.join
@@ -235,7 +230,7 @@
     if all(c in LegalChars for c in str):
         return str
     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]")
@@ -244,7 +239,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 len(str) < 2:
         return str
     if str[0] != '"' or str[-1] != '"':
         return str
@@ -263,31 +258,32 @@
     n = len(str)
     res = []
     while 0 <= i < n:
-        Omatch = _OctalPatt.search(str, i)
-        Qmatch = _QuotePatt.search(str, i)
-        if not Omatch and not Qmatch:              # Neither matched
+        o_match = _OctalPatt.search(str, i)
+        q_match = _QuotePatt.search(str, i)
+        if not o_match and not q_match:              # Neither matched
             res.append(str[i:])
             break
         # else:
         j = k = -1
-        if Omatch: j = Omatch.start(0)
-        if Qmatch: k = Qmatch.start(0)
-        if Qmatch and ( not Omatch or k < j ):     # QuotePatt matched
+        if o_match:
+            j = o_match.start(0)
+        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[k+1])
-            i = k+2
+            i = k + 2
         else:                                      # OctalPatt matched
             res.append(str[i:j])
-            res.append( chr( int(str[j+1:j+4], 8) ) )
-            i = j+4
+            res.append(chr(int(str[j+1:j+4], 8)))
+            i = j + 4
     return _nulljoin(res)
 
-# The _getdate() routine is used to set the expiration time in
-# the cookie's HTTP header.      By default, _getdate() returns the
-# current time in the appropriate "expires" format for a
-# Set-Cookie header.     The one optional argument is an offset from
-# now, in seconds.      For example, an offset of -3600 means "one hour ago".
-# The offset may be a floating point number.
+# The _getdate() routine is used to set the expiration time in the cookie's HTTP
+# header.  By default, _getdate() returns the current time in the appropriate
+# "expires" format for a Set-Cookie header.  The one optional argument is an
+# offset from now, in seconds.  For example, an offset of -3600 means "one hour
+# ago".  The offset may be a floating point number.
 #
 
 _weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
@@ -305,7 +301,7 @@
 
 
 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
     used to keep the attributes associated with the appropriate key,value pair.
@@ -326,23 +322,24 @@
     # This dictionary provides a mapping from the lowercase
     # variant on the left to the appropriate traditional
     # formatting on the right.
-    _reserved = { "expires" : "expires",
-                   "path"        : "Path",
-                   "comment" : "Comment",
-                   "domain"      : "Domain",
-                   "max-age" : "Max-Age",
-                   "secure"      : "secure",
-                   "httponly"  : "httponly",
-                   "version" : "Version",
-                   }
+    _reserved = {
+        "expires"  : "expires",
+        "path"     : "Path",
+        "comment"  : "Comment",
+        "domain"   : "Domain",
+        "max-age"  : "Max-Age",
+        "secure"   : "secure",
+        "httponly" : "httponly",
+        "version"  : "Version",
+    }
 
     def __init__(self):
         # Set defaults
         self.key = self.value = self.coded_value = None
 
         # Set default attributes
-        for K in self._reserved:
-            dict.__setitem__(self, K, "")
+        for key in self._reserved:
+            dict.__setitem__(self, key, "")
 
     def __setitem__(self, K, V):
         K = K.lower()
@@ -362,18 +359,18 @@
             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
+        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) )
+    def output(self, attrs=None, header="Set-Cookie:"):
+        return "%s %s" % (header, self.OutputString(attrs))
 
     __str__ = output
 
     def __repr__(self):
         return '<%s: %s=%s>' % (self.__class__.__name__,
-                                self.key, repr(self.value) )
+                                self.key, repr(self.value))
 
     def js_output(self, attrs=None):
         # Print javascript
@@ -383,34 +380,36 @@
         document.cookie = \"%s\";
         // end hiding -->
         </script>
-        """ % ( self.OutputString(attrs).replace('"',r'\"'))
+        """ % (self.OutputString(attrs).replace('"', r'\"'))
 
     def OutputString(self, attrs=None):
         # Build up our result
         #
         result = []
-        RA = result.append
+        append = result.append
 
         # 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
         if attrs is None:
             attrs = self._reserved
         items = sorted(self.items())
-        for K,V in items:
-            if V == "": continue
-            if K not in attrs: continue
-            if K == "expires" and type(V) == type(1):
-                RA("%s=%s" % (self._reserved[K], _getdate(V)))
-            elif K == "max-age" and type(V) == type(1):
-                RA("%s=%d" % (self._reserved[K], V))
-            elif K == "secure":
-                RA(str(self._reserved[K]))
-            elif K == "httponly":
-                RA(str(self._reserved[K]))
+        for key, value in items:
+            if value == "":
+                continue
+            if key not in attrs:
+                continue
+            if key == "expires" and isinstance(value, int):
+                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":
+                append(str(self._reserved[key]))
             else:
-                RA("%s=%s" % (self._reserved[K], V))
+                append("%s=%s" % (self._reserved[key], value))
 
         # Return the result
         return _semispacejoin(result)
@@ -426,24 +425,23 @@
 #
 
 _LegalCharsPatt  = r"[\w\d!#%&'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\=]"
-_CookiePattern = re.compile(
-    r"(?x)"                       # This is a Verbose pattern
-    r"(?P<key>"                   # Start of group 'key'
-    ""+ _LegalCharsPatt +"+?"     # Any word of at least one letter, nongreedy
-    r")"                          # End of group 'key'
-    r"\s*=\s*"                    # Equal Sign
-    r"(?P<val>"                   # Start of group 'val'
-    r'"(?:[^\\"]|\\.)*"'            # Any doublequoted string
-    r"|"                            # or
-    ""+ _LegalCharsPatt +"*"        # Any word or empty string
-    r")"                          # End of group 'val'
-    r"\s*;?"                      # Probably ending in a semi-colon
-    , re.ASCII)                   # May be removed if safe.
-
-
-# At long last, here is the cookie class.
-#   Using this class is almost just like using a dictionary.
-# See this module's docstring for example usage.
+_CookiePattern = re.compile(r"""
+    (?x)                           # This is a verbose pattern
+    (?P<key>                       # Start of group 'key'
+    """ + _LegalCharsPatt + r"""+?   # Any word of at least one letter
+    )                              # End of group 'key'
+    \s*=\s*                        # Equal Sign
+    (?P<val>                       # Start of group 'val'
+    "(?:[^\\"]|\\.)*"                # Any doublequoted string
+    |                                # or
+    """ + _LegalCharsPatt + r"""*    # Any word or empty string
+    )                              # End of group 'val'
+    \s*;?                          # Probably ending in a semi-colon
+    """, re.ASCII)                 # May be removed if safe.
+
+
+# At long last, here is the cookie class.  Using this class is almost just like
+# using a dictionary.  See this module's docstring for example usage.
 #
 class BaseCookie(dict):
     """A container class for a set of Morsels."""
@@ -467,7 +465,8 @@
         return strval, strval
 
     def __init__(self, input=None):
-        if input: self.load(input)
+        if input:
+            self.load(input)
 
     def __set(self, key, real_value, coded_value):
         """Private method for setting a cookie's value"""
@@ -484,25 +483,25 @@
         """Return a string suitable for HTTP."""
         result = []
         items = sorted(self.items())
-        for K,V in items:
-            result.append( V.output(attrs, header) )
+        for key, value in items:
+            result.append(value.output(attrs, header))
         return sep.join(result)
 
     __str__ = output
 
     def __repr__(self):
-        L = []
+        l = []
         items = sorted(self.items())
-        for K,V in items:
-            L.append( '%s=%s' % (K,repr(V.value) ) )
-        return '<%s: %s>' % (self.__class__.__name__, _spacejoin(L))
+        for key, value in items:
+            l.append('%s=%s' % (key, repr(value.value)))
+        return '<%s: %s>' % (self.__class__.__name__, _spacejoin(l))
 
     def js_output(self, attrs=None):
         """Return a string suitable for JavaScript."""
         result = []
         items = sorted(self.items())
-        for K,V in items:
-            result.append( V.js_output(attrs) )
+        for key, value in items:
+            result.append(value.js_output(attrs))
         return _nulljoin(result)
 
     def load(self, rawdata):
@@ -511,15 +510,15 @@
         is equivalent to calling:
             map(Cookie.__setitem__, d.keys(), d.values())
         """
-        if type(rawdata) == type(""):
-            self.__ParseString(rawdata)
+        if isinstance(rawdata, str):
+            self.__parse_string(rawdata)
         else:
             # self.update() wouldn't call our custom __setitem__
-            for k, v in rawdata.items():
-                self[k] = v
+            for key, value in rawdata.items():
+                self[key] = value
         return
 
-    def __ParseString(self, str, patt=_CookiePattern):
+    def __parse_string(self, str, patt=_CookiePattern):
         i = 0            # Our starting point
         n = len(str)     # Length of string
         M = None         # current morsel
@@ -527,25 +526,27 @@
         while 0 <= i < n:
             # Start looking for a cookie
             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)
 
             # Parse the key, value in case it's metainfo
-            if K[0] == "$":
+            if key[0] == "$":
                 # We ignore attributes which pertain to the cookie
                 # mechanism as a whole.  See RFC 2109.
                 # (Does anyone care?)
                 if M:
-                    M[ K[1:] ] = V
-            elif K.lower() in Morsel._reserved:
+                    M[key[1:]] = value
+            elif key.lower() in Morsel._reserved:
                 if M:
-                    M[ K ] = _unquote(V)
+                    M[key] = _unquote(value)
             else:
-                rval, cval = self.value_decode(V)
-                self.__set(K, rval, cval)
-                M = self[K]
+                rval, cval = self.value_decode(value)
+                self.__set(key, rval, cval)
+                M = self[key]
 
 
 class SimpleCookie(BaseCookie):
@@ -556,16 +557,8 @@
     received from HTTP are kept as strings.
     """
     def value_decode(self, val):
-        return _unquote( val ), val
+        return _unquote(val), val
+
     def value_encode(self, val):
         strval = str(val)
-        return strval, _quote( strval )
-
-###########################################################
-
-def _test():
-    import doctest, http.cookies
-    return doctest.testmod(http.cookies)
-
-if __name__ == "__main__":
-    _test()
+        return strval, _quote(strval)

Modified: python/branches/py3k/Lib/test/test_http_cookies.py
==============================================================================
--- python/branches/py3k/Lib/test/test_http_cookies.py	(original)
+++ python/branches/py3k/Lib/test/test_http_cookies.py	Sat Jul 31 23:04:00 2010
@@ -19,24 +19,21 @@
 
     def test_basic(self):
         cases = [
-            { 'data': 'chips=ahoy; vienna=finger',
-              'dict': {'chips':'ahoy', 'vienna':'finger'},
-              'repr': "<SimpleCookie: chips='ahoy' vienna='finger'>",
-              'output': 'Set-Cookie: chips=ahoy\nSet-Cookie: vienna=finger',
-            },
-
-            { 'data': 'keebler="E=mc2; L=\\"Loves\\"; fudge=\\012;"',
-              'dict': {'keebler' : 'E=mc2; L="Loves"; fudge=\012;'},
-              'repr': '''<SimpleCookie: keebler='E=mc2; L="Loves"; fudge=\\n;'>''',
-              'output': 'Set-Cookie: keebler="E=mc2; L=\\"Loves\\"; fudge=\\012;"',
-            },
+            {'data': 'chips=ahoy; vienna=finger',
+             'dict': {'chips':'ahoy', 'vienna':'finger'},
+             'repr': "<SimpleCookie: chips='ahoy' vienna='finger'>",
+             'output': 'Set-Cookie: chips=ahoy\nSet-Cookie: vienna=finger'},
+
+            {'data': 'keebler="E=mc2; L=\\"Loves\\"; fudge=\\012;"',
+             'dict': {'keebler' : 'E=mc2; L="Loves"; fudge=\012;'},
+             'repr': '''<SimpleCookie: keebler='E=mc2; L="Loves"; fudge=\\n;'>''',
+             'output': 'Set-Cookie: keebler="E=mc2; L=\\"Loves\\"; fudge=\\012;"'},
 
             # Check illegal cookies that have an '=' char in an unquoted value
-            { 'data': 'keebler=E=mc2',
-              'dict': {'keebler' : 'E=mc2'},
-              'repr': "<SimpleCookie: keebler='E=mc2'>",
-              'output': 'Set-Cookie: keebler=E=mc2',
-            }
+            {'data': 'keebler=E=mc2',
+             'dict': {'keebler' : 'E=mc2'},
+             'repr': "<SimpleCookie: keebler='E=mc2'>",
+             'output': 'Set-Cookie: keebler=E=mc2'},
         ]
 
         for case in cases:
@@ -72,6 +69,26 @@
         </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):
         # Try cookie with quoted meta-data
         C = cookies.SimpleCookie()
@@ -80,8 +97,72 @@
         self.assertEqual(C['Customer']['version'], '1')
         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 at bar"]:
+            # Try some illegal characters
+            self.assertRaises(cookies.CookieError,
+                              M.set, i, '%s_value' % i, '%s_value' % i)
+
+
 def test_main():
-    run_unittest(CookieTests)
+    run_unittest(CookieTests, MorselTests)
     run_doctest(cookies)
 
 if __name__ == '__main__':


More information about the Python-checkins mailing list