diff -r 4285d13fd3dc Lib/http/cookiejar.py --- a/Lib/http/cookiejar.py Sat Feb 23 15:44:46 2013 -0800 +++ b/Lib/http/cookiejar.py Wed Feb 27 07:28:31 2013 -0800 @@ -7,26 +7,16 @@ attributes of the HTTP cookie system as cookie-attributes, to distinguish them clearly from Python attributes. -Class diagram (note that BSDDBCookieJar and the MSIE* classes are not -distributed with the Python standard library, but are available from -http://wwwsearch.sf.net/): - CookieJar____ - / \ \ - FileCookieJar \ \ - / | \ \ \ - MozillaCookieJar | LWPCookieJar \ \ - | | \ - | ---MSIEBase | \ - | / | | \ - | / MSIEDBCookieJar BSDDBCookieJar - |/ - MSIECookieJar - + FileCookieProcessor CookieJar + / \ + MozillaCookieProcessor LWPCookieProcessor + """ __all__ = ['Cookie', 'CookieJar', 'CookiePolicy', 'DefaultCookiePolicy', - 'FileCookieJar', 'LWPCookieJar', 'LoadError', 'MozillaCookieJar'] + 'FileCookieProcessor', 'LWPCookieProcessor', 'LoadError', + 'MozillaCookieProcessor'] import copy import datetime @@ -38,6 +28,7 @@ except ImportError: import dummy_threading as _threading import http.client # only for the default HTTP port +from abc import ABCMeta, abstractmethod from calendar import timegm debug = False # set to True to enable debugging via the logging module @@ -1733,62 +1724,64 @@ # derives from OSError for backwards-compatibility with Python 2.4.0 class LoadError(OSError): pass -class FileCookieJar(CookieJar): - """CookieJar that can be loaded from and saved to a file.""" +class FileCookieProcessor(metaclass=ABCMeta): + """Abstract Base Class for file-based cookie processor + """ - def __init__(self, filename=None, delayload=False, policy=None): + def __init__(self, filename=None, cookiejar=None): """ Cookies are NOT loaded from the named file until either the .load() or .revert() method is called. """ - CookieJar.__init__(self, policy) + if cookiejar is None: + cookiejar = CookieJar() + self.cookiejar = cookiejar + self.filename = filename + + @abstractmethod + def save(self, filename=None, ignore_discard=False, ignore_expires=False): + """Save cookies to a file. + + Concrete implementations should call FileCookieJar.load first in order + to ensure self.filename is set correctly. + """ if filename is not None: - try: - filename+"" - except: - raise ValueError("filename must be string-like") - self.filename = filename - self.delayload = bool(delayload) + self.filename = filename - def save(self, filename=None, ignore_discard=False, ignore_expires=False): - """Save cookies to a file.""" - raise NotImplementedError() + @abstractmethod + def load(self, filename, ignore_discard=False, ignore_expires=False): + """Load cookies from a file. + + Concrete implementations should call FileCookieJar.load first in order + to ensure self.filename is set correctly. + """ + if filename is not None: + self.filename = filename - def load(self, filename=None, ignore_discard=False, ignore_expires=False): - """Load cookies from a file.""" - if filename is None: - if self.filename is not None: filename = self.filename - else: raise ValueError(MISSING_FILENAME_TEXT) - - with open(filename) as f: - self._really_load(f, filename, ignore_discard, ignore_expires) - - def revert(self, filename=None, - ignore_discard=False, ignore_expires=False): + def revert(self, filename=None, ignore_discard=False, + ignore_expires=False): """Clear all cookies and reload cookies from a saved file. Raises LoadError (or OSError) if reversion is not successful; the object's state will not be altered if this happens. """ - if filename is None: - if self.filename is not None: filename = self.filename - else: raise ValueError(MISSING_FILENAME_TEXT) + if filename is not None: + self.filename = filename - self._cookies_lock.acquire() + self.cookiejar._cookies_lock.acquire() try: - - old_state = copy.deepcopy(self._cookies) - self._cookies = {} + old_state = copy.deepcopy(self.cookiejar._cookies) + self.cookiejar._cookies = {} try: self.load(filename, ignore_discard, ignore_expires) except OSError: - self._cookies = old_state + self.cookiejar._cookies = old_state raise finally: - self._cookies_lock.release() + self.cookiejar._cookies_lock.release() def lwp_cookie_str(cookie): @@ -1819,7 +1812,7 @@ return join_header_words([h]) -class LWPCookieJar(FileCookieJar): +class LWPCookieProcessor(FileCookieProcessor): """ The LWPCookieJar saves a sequence of "Set-Cookie3" lines. "Set-Cookie3" is the format used by the libwww-perl libary, not known @@ -1831,6 +1824,9 @@ as_lwp_str(ignore_discard=True, ignore_expired=True) """ + def __init__(self, filename=None, cookiejar=None): + FileCookieProcessor.__init__(self, cookiejar=cookiejar) + self.filename = filename def as_lwp_str(self, ignore_discard=True, ignore_expires=True): """Return cookies as a string of "\\n"-separated "Set-Cookie3" headers. @@ -1840,7 +1836,7 @@ """ now = time.time() r = [] - for cookie in self: + for cookie in self.cookiejar: if not ignore_discard and cookie.discard: continue if not ignore_expires and cookie.is_expired(now): @@ -1849,97 +1845,100 @@ return "\n".join(r+[""]) def save(self, filename=None, ignore_discard=False, ignore_expires=False): - if filename is None: - if self.filename is not None: filename = self.filename - else: raise ValueError(MISSING_FILENAME_TEXT) + if filename is not None: + self.filename = filename - with open(filename, "w") as f: + with open(self.filename, "w") as f: # There really isn't an LWP Cookies 2.0 format, but this indicates # that there is extra information in here (domain_dot and # port_spec) while still being compatible with libwww-perl, I hope. f.write("#LWP-Cookies-2.0\n") f.write(self.as_lwp_str(ignore_discard, ignore_expires)) - def _really_load(self, f, filename, ignore_discard, ignore_expires): - magic = f.readline() - if not self.magic_re.search(magic): - msg = ("%r does not look like a Set-Cookie3 (LWP) format " - "file" % filename) - raise LoadError(msg) + def load(self, filename=None, ignore_discard=False, ignore_expires=None): + FileCookieProcessor.load(self, filename=filename, + ignore_discard=ignore_discard, ignore_expires=ignore_expires) - now = time.time() + with open(self.filename) as f: + magic = f.readline() + if not self.cookiejar.magic_re.search(magic): + msg = ("%r does not look like a Set-Cookie3 (LWP) format " + "file" % filename) + raise LoadError(msg) - header = "Set-Cookie3:" - boolean_attrs = ("port_spec", "path_spec", "domain_dot", - "secure", "discard") - value_attrs = ("version", - "port", "path", "domain", - "expires", - "comment", "commenturl") + now = time.time() - try: - while 1: - line = f.readline() - if line == "": break - if not line.startswith(header): - continue - line = line[len(header):].strip() + header = "Set-Cookie3:" + boolean_attrs = ("port_spec", "path_spec", "domain_dot", + "secure", "discard") + value_attrs = ("version", + "port", "path", "domain", + "expires", + "comment", "commenturl") - for data in split_header_words([line]): - name, value = data[0] - standard = {} - rest = {} - for k in boolean_attrs: - standard[k] = False - for k, v in data[1:]: - if k is not None: - lc = k.lower() - else: - lc = None - # don't lose case distinction for unknown fields - if (lc in value_attrs) or (lc in boolean_attrs): - k = lc - if k in boolean_attrs: - if v is None: v = True - standard[k] = v - elif k in value_attrs: - standard[k] = v - else: - rest[k] = v + try: + while 1: + line = f.readline() + if line == "": break + if not line.startswith(header): + continue + line = line[len(header):].strip() - h = standard.get - expires = h("expires") - discard = h("discard") - if expires is not None: - expires = iso2time(expires) - if expires is None: - discard = True - domain = h("domain") - domain_specified = domain.startswith(".") - c = Cookie(h("version"), name, value, - h("port"), h("port_spec"), - domain, domain_specified, h("domain_dot"), - h("path"), h("path_spec"), - h("secure"), - expires, - discard, - h("comment"), - h("commenturl"), - rest) - if not ignore_discard and c.discard: - continue - if not ignore_expires and c.is_expired(now): - continue - self.set_cookie(c) - except OSError: - raise - except Exception: - _warn_unhandled_exception() - raise LoadError("invalid Set-Cookie3 format file %r: %r" % - (filename, line)) + for data in split_header_words([line]): + name, value = data[0] + standard = {} + rest = {} + for k in boolean_attrs: + standard[k] = False + for k, v in data[1:]: + if k is not None: + lc = k.lower() + else: + lc = None + # don't lose case distinction for unknown fields + if (lc in value_attrs) or (lc in boolean_attrs): + k = lc + if k in boolean_attrs: + if v is None: v = True + standard[k] = v + elif k in value_attrs: + standard[k] = v + else: + rest[k] = v + h = standard.get + expires = h("expires") + discard = h("discard") + if expires is not None: + expires = iso2time(expires) + if expires is None: + discard = True + domain = h("domain") + domain_specified = domain.startswith(".") + c = Cookie(h("version"), name, value, + h("port"), h("port_spec"), + domain, domain_specified, h("domain_dot"), + h("path"), h("path_spec"), + h("secure"), + expires, + discard, + h("comment"), + h("commenturl"), + rest) + if not ignore_discard and c.discard: + continue + if not ignore_expires and c.is_expired(now): + continue + self.cookiejar.set_cookie(c) + except OSError: + raise + except Exception: + _warn_unhandled_exception() + raise LoadError("invalid Set-Cookie3 format file %r: %r" % + (filename, line)) -class MozillaCookieJar(FileCookieJar): + +class MozillaCookieProcessor(FileCookieProcessor): """ WARNING: you may want to backup your browser's cookies file if you use @@ -1977,72 +1976,77 @@ # This is a generated file! Do not edit. """ + def __init__(self, filename=None, cookiejar=None): + FileCookieProcessor.__init__(self, filename=filename, + cookiejar=cookiejar) - def _really_load(self, f, filename, ignore_discard, ignore_expires): + def load(self, filename=None, ignore_discard=False, ignore_expires=False): + FileCookieProcessor.load(self, filename=filename, + ignore_discard=ignore_discard, ignore_expires=ignore_expires) now = time.time() - magic = f.readline() - if not self.magic_re.search(magic): - f.close() - raise LoadError( - "%r does not look like a Netscape format cookies file" % - filename) + with open(self.filename) as f: + magic = f.readline() + if not self.magic_re.search(magic): + raise LoadError( + "%r does not look like a Netscape format cookies file" % + filename) - try: - while 1: - line = f.readline() - if line == "": break + try: + while 1: + line = f.readline() + if line == "": break - # last field may be absent, so keep any trailing tab - if line.endswith("\n"): line = line[:-1] + # last field may be absent, so keep any trailing tab + if line.endswith("\n"): line = line[:-1] - # skip comments and blank lines XXX what is $ for? - if (line.strip().startswith(("#", "$")) or - line.strip() == ""): - continue + # skip comments and blank lines XXX what is $ for? + if (line.strip().startswith(("#", "$")) or + line.strip() == ""): + continue - domain, domain_specified, path, secure, expires, name, value = \ - line.split("\t") - secure = (secure == "TRUE") - domain_specified = (domain_specified == "TRUE") - if name == "": - # cookies.txt regards 'Set-Cookie: foo' as a cookie - # with no name, whereas http.cookiejar regards it as a - # cookie with no value. - name = value - value = None + domain, domain_specified, path, secure, expires, name, \ + value = line.split("\t") + secure = (secure == "TRUE") + domain_specified = (domain_specified == "TRUE") + if name == "": + # cookies.txt regards 'Set-Cookie: foo' as a cookie + # with no name, whereas http.cookiejar regards it as a + # cookie with no value. + name = value + value = None - initial_dot = domain.startswith(".") - assert domain_specified == initial_dot + initial_dot = domain.startswith(".") + assert domain_specified == initial_dot - discard = False - if expires == "": - expires = None - discard = True + discard = False + if expires == "": + expires = None + discard = True - # assume path_specified is false - c = Cookie(0, name, value, - None, False, - domain, domain_specified, initial_dot, - path, False, - secure, - expires, - discard, - None, - None, - {}) - if not ignore_discard and c.discard: - continue - if not ignore_expires and c.is_expired(now): - continue - self.set_cookie(c) + # assume path_specified is false + c = Cookie(0, name, value, + None, False, + domain, domain_specified, initial_dot, + path, False, + secure, + expires, + discard, + None, + None, + {}) + if not ignore_discard and c.discard: + continue + if not ignore_expires and c.is_expired(now): + continue + self.cookiejar.set_cookie(c) - except OSError: - raise - except Exception: - _warn_unhandled_exception() - raise LoadError("invalid Netscape format cookies file %r: %r" % - (filename, line)) + except OSError: + raise + except Exception: + _warn_unhandled_exception() + raise LoadError("invalid Netscape format cookies file %r: %r" % + (filename, line)) def save(self, filename=None, ignore_discard=False, ignore_expires=False): if filename is None: @@ -2052,7 +2056,7 @@ with open(filename, "w") as f: f.write(self.header) now = time.time() - for cookie in self: + for cookie in self.cookiejar: if not ignore_discard and cookie.discard: continue if not ignore_expires and cookie.is_expired(now): diff -r 4285d13fd3dc Lib/test/test_http_cookiejar.py --- a/Lib/test/test_http_cookiejar.py Sat Feb 23 15:44:46 2013 -0800 +++ b/Lib/test/test_http_cookiejar.py Wed Feb 27 07:28:31 2013 -0800 @@ -9,10 +9,11 @@ from http.cookiejar import (time2isoz, http2time, time2netscape, parse_ns_headers, join_header_words, split_header_words, Cookie, - CookieJar, DefaultCookiePolicy, LWPCookieJar, MozillaCookieJar, - LoadError, lwp_cookie_str, DEFAULT_HTTP_PORT, escape_path, - reach, is_HDN, domain_match, user_domain_match, request_path, - request_port, request_host) + CookieJar, DefaultCookiePolicy, LWPCookieProcessor, + MozillaCookieProcessor, LoadError, lwp_cookie_str, + DEFAULT_HTTP_PORT, escape_path, reach, is_HDN, domain_match, + user_domain_match, request_path, request_port, request_host, + FileCookieProcessor) class DateTimeTests(unittest.TestCase): @@ -235,23 +236,25 @@ def test_lwp_valueless_cookie(self): # cookies with no value should be saved and loaded consistently filename = test.support.TESTFN - c = LWPCookieJar() - interact_netscape(c, "http://www.acme.com/", 'boo') - self.assertEqual(c._cookies["www.acme.com"]["/"]["boo"].value, None) + proc = LWPCookieProcessor() + interact_netscape(proc.cookiejar, "http://www.acme.com/", 'boo') + self.assertEqual(proc.cookiejar._cookies["www.acme.com"]["/"][ + "boo"].value, None) try: - c.save(filename, ignore_discard=True) - c = LWPCookieJar() - c.load(filename, ignore_discard=True) + proc.save(filename, ignore_discard=True) + proc = LWPCookieProcessor() + proc.load(filename, ignore_discard=True) finally: try: os.unlink(filename) except OSError: pass - self.assertEqual(c._cookies["www.acme.com"]["/"]["boo"].value, None) + self.assertEqual(proc.cookiejar._cookies["www.acme.com"]["/"][ + "boo"].value, None) def test_bad_magic(self): # OSErrors (eg. file doesn't exist) are allowed to propagate filename = test.support.TESTFN - for cookiejar_class in LWPCookieJar, MozillaCookieJar: - c = cookiejar_class() + for cookie_proc in LWPCookieProcessor, MozillaCookieProcessor: + c = cookie_proc() try: c.load(filename="for this test to work, a file with this " "filename should not exist") @@ -266,13 +269,31 @@ try: with open(filename, "w") as f: f.write("oops\n") - for cookiejar_class in LWPCookieJar, MozillaCookieJar: - c = cookiejar_class() + for cookie_proc in LWPCookieProcessor, MozillaCookieProcessor: + c = cookie_proc() self.assertRaises(LoadError, c.load, filename) finally: try: os.unlink(filename) except OSError: pass + def test_invalid_interface(self): + # ensure that instantiation of an invalid implementation will raise a + # TypeError + class InvalidFileCookieJar(FileCookieProcessor): + pass + + try: + InvalidFileCookieJar() + except TypeError: + pass + except Exception as e: + self.fail('Unexpected exception {}'.format(e)) + + def test_concrete_lookup(self): + # expected implementations of FileCookieJar + self.assertEqual(set(FileCookieProcessor.__subclasses__()), + set([LWPCookieProcessor, MozillaCookieProcessor])) + class CookieTests(unittest.TestCase): # XXX # Get rid of string comparisons where not actually testing str / repr. @@ -354,33 +375,34 @@ # missing = sign in Cookie: header is regarded by Mozilla as a missing # name, and by http.cookiejar as a missing value filename = test.support.TESTFN - c = MozillaCookieJar(filename) - interact_netscape(c, "http://www.acme.com/", 'eggs') - interact_netscape(c, "http://www.acme.com/", '"spam"; path=/foo/') - cookie = c._cookies["www.acme.com"]["/"]["eggs"] + proc = MozillaCookieProcessor(filename) + interact_netscape(proc.cookiejar, "http://www.acme.com/", 'eggs') + interact_netscape(proc.cookiejar, "http://www.acme.com/", + '"spam"; path=/foo/') + cookie = proc.cookiejar._cookies["www.acme.com"]["/"]["eggs"] self.assertTrue(cookie.value is None) self.assertEqual(cookie.name, "eggs") - cookie = c._cookies["www.acme.com"]['/foo/']['"spam"'] + cookie = proc.cookiejar._cookies["www.acme.com"]['/foo/']['"spam"'] self.assertTrue(cookie.value is None) self.assertEqual(cookie.name, '"spam"') self.assertEqual(lwp_cookie_str(cookie), ( r'"spam"; path="/foo/"; domain="www.acme.com"; ' 'path_spec; discard; version=0')) - old_str = repr(c) - c.save(ignore_expires=True, ignore_discard=True) + old_str = repr(proc.cookiejar) + proc.save(ignore_expires=True, ignore_discard=True) try: - c = MozillaCookieJar(filename) - c.revert(ignore_expires=True, ignore_discard=True) + proc = MozillaCookieProcessor(filename) + proc.revert(ignore_expires=True, ignore_discard=True) finally: - os.unlink(c.filename) + os.unlink(proc.filename) # cookies unchanged apart from lost info re. whether path was specified self.assertEqual( - repr(c), + repr(proc.cookiejar), re.sub("path_specified=%s" % True, "path_specified=%s" % False, old_str) ) - self.assertEqual(interact_netscape(c, "http://www.acme.com/foo/"), - '"spam"; eggs') + self.assertEqual(interact_netscape(proc.cookiejar, + "http://www.acme.com/foo/"), '"spam"; eggs') def test_rfc2109_handling(self): # RFC 2109 cookies are handled as RFC 2965 or Netscape cookies, @@ -1375,66 +1397,66 @@ # Test rejection of Set-Cookie2 responses based on domain, path, port. pol = DefaultCookiePolicy(rfc2965=True) - c = LWPCookieJar(policy=pol) + proc = LWPCookieProcessor(cookiejar=CookieJar(policy=pol)) max_age = "max-age=3600" # illegal domain (no embedded dots) - cookie = interact_2965(c, "http://www.acme.com", + cookie = interact_2965(proc.cookiejar, "http://www.acme.com", 'foo=bar; domain=".com"; version=1') - self.assertTrue(not c) + self.assertTrue(not proc.cookiejar) # legal domain - cookie = interact_2965(c, "http://www.acme.com", + cookie = interact_2965(proc.cookiejar, "http://www.acme.com", 'ping=pong; domain="acme.com"; version=1') - self.assertEqual(len(c), 1) + self.assertEqual(len(proc.cookiejar), 1) # illegal domain (host prefix "www.a" contains a dot) - cookie = interact_2965(c, "http://www.a.acme.com", + cookie = interact_2965(proc.cookiejar, "http://www.a.acme.com", 'whiz=bang; domain="acme.com"; version=1') - self.assertEqual(len(c), 1) + self.assertEqual(len(proc.cookiejar), 1) # legal domain - cookie = interact_2965(c, "http://www.a.acme.com", + cookie = interact_2965(proc.cookiejar, "http://www.a.acme.com", 'wow=flutter; domain=".a.acme.com"; version=1') - self.assertEqual(len(c), 2) + self.assertEqual(len(proc.cookiejar), 2) # can't partially match an IP-address - cookie = interact_2965(c, "http://125.125.125.125", + cookie = interact_2965(proc.cookiejar, "http://125.125.125.125", 'zzzz=ping; domain="125.125.125"; version=1') - self.assertEqual(len(c), 2) + self.assertEqual(len(proc.cookiejar), 2) # illegal path (must be prefix of request path) - cookie = interact_2965(c, "http://www.sol.no", + cookie = interact_2965(proc.cookiejar, "http://www.sol.no", 'blah=rhubarb; domain=".sol.no"; path="/foo"; ' 'version=1') - self.assertEqual(len(c), 2) + self.assertEqual(len(proc.cookiejar), 2) # legal path - cookie = interact_2965(c, "http://www.sol.no/foo/bar", + cookie = interact_2965(proc.cookiejar, "http://www.sol.no/foo/bar", 'bing=bong; domain=".sol.no"; path="/foo"; ' 'version=1') - self.assertEqual(len(c), 3) + self.assertEqual(len(proc.cookiejar), 3) # illegal port (request-port not in list) - cookie = interact_2965(c, "http://www.sol.no", + cookie = interact_2965(proc.cookiejar, "http://www.sol.no", 'whiz=ffft; domain=".sol.no"; port="90,100"; ' 'version=1') - self.assertEqual(len(c), 3) + self.assertEqual(len(proc.cookiejar), 3) # legal port cookie = interact_2965( - c, "http://www.sol.no", + proc.cookiejar, "http://www.sol.no", r'bang=wallop; version=1; domain=".sol.no"; ' r'port="90,100, 80,8080"; ' r'max-age=100; Comment = "Just kidding! (\"|\\\\) "') - self.assertEqual(len(c), 4) + self.assertEqual(len(proc.cookiejar), 4) # port attribute without any value (current port) - cookie = interact_2965(c, "http://www.sol.no", + cookie = interact_2965(proc.cookiejar, "http://www.sol.no", 'foo9=bar; version=1; domain=".sol.no"; port; ' 'max-age=100;') - self.assertEqual(len(c), 5) + self.assertEqual(len(proc.cookiejar), 5) # encoded path # LWP has this test, but unescaping allowed path characters seems @@ -1443,24 +1465,24 @@ ## r'foo8=bar; version=1; path="/%66oo"') # but this is OK, because '<' is not an allowed HTTP URL path # character: - cookie = interact_2965(c, "http://www.sol.no/