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 Sat Feb 23 17:16:22 2013 -0800 @@ -38,6 +38,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,8 +1734,8 @@ # 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 FileCookieJar(CookieJar, metaclass=ABCMeta): + """Abstract Base Class for any file-based CookieJar.""" def __init__(self, filename=None, delayload=False, policy=None): """ @@ -1751,18 +1752,23 @@ self.filename = filename self.delayload = bool(delayload) + @abstractmethod def save(self, filename=None, ignore_discard=False, ignore_expires=False): """Save cookies to a file.""" - raise NotImplementedError() + pass + @abstractmethod 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) + """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 - with open(filename) as f: - self._really_load(f, filename, ignore_discard, ignore_expires) + if self.filename is None: + raise ValueError(MISSING_FILENAME_TEXT) def revert(self, filename=None, ignore_discard=False, ignore_expires=False): @@ -1860,83 +1866,87 @@ 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): + FileCookieJar.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.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.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): @@ -1978,71 +1988,74 @@ """ - def _really_load(self, f, filename, ignore_discard, ignore_expires): + def load(self, filename=None, ignore_discard=False, ignore_expires=False): + FileCookieJar.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): + f.close() + 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.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: 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 Sat Feb 23 17:16:22 2013 -0800 @@ -12,7 +12,7 @@ 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) + request_port, request_host, FileCookieJar) class DateTimeTests(unittest.TestCase): @@ -273,6 +273,24 @@ 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(FileCookieJar): + 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(FileCookieJar.__subclasses__()), set([LWPCookieJar, + MozillaCookieJar])) + class CookieTests(unittest.TestCase): # XXX # Get rid of string comparisons where not actually testing str / repr. diff -r 4285d13fd3dc Misc/ACKS --- a/Misc/ACKS Sat Feb 23 15:44:46 2013 -0800 +++ b/Misc/ACKS Sat Feb 23 17:16:22 2013 -0800 @@ -1351,3 +1351,4 @@ Kai Zhu Tarek Ziadé Peter Åstrand +Demian Brecht