[Checkins] SVN: zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/ completion of cookies API and tests prior to review. mechanize changes pending to remove the one XXX.

Gary Poster gary at modernsongs.com
Wed Nov 5 16:48:16 EST 2008


Log message for revision 92802:
  completion of cookies API and tests prior to review.  mechanize changes pending to remove the one XXX.

Changed:
  U   zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/README.txt
  U   zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/browser.py
  U   zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/cookies.py
  A   zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/cookies.txt
  U   zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/interfaces.py
  U   zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/tests.py

-=-
Modified: zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/README.txt
===================================================================
--- zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/README.txt	2008-11-05 16:38:52 UTC (rev 92801)
+++ zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/README.txt	2008-11-05 21:48:13 UTC (rev 92802)
@@ -159,10 +159,10 @@
     >>> browser.headers['set-cookie']
     'foo=bar;'
 
-You can also examine cookies using the browser's ``cookies`` attribute.  It has
+It is also available in the browser's ``cookies`` attribute.  This is
 an extended mapping interface that allows getting, setting, and deleting the
-cookies that the browser is remembering for the current url.  These are
-examples of just the accessor operators and methods.
+cookies that the browser is remembering *for the current url*.  Here are
+a few examples.
 
     >>> browser.cookies['foo']
     'bar'
@@ -180,22 +180,6 @@
     1
     >>> print(dict(browser.cookies))
     {'foo': 'bar'}
-
-It can also be used to examine cookies that have already been set in a
-previous request.  To demonstrate this, we use another view that does not set
-cookies but reports on the cookies it receives from the browser.
-
-    >>> browser.open('http://localhost/get_cookie.html')
-    >>> print browser.headers.get('set-cookie')
-    None
-    >>> browser.contents
-    'foo: bar'
-    >>> browser.cookies['foo']
-    'bar'
-
-The standard mapping mutation methods and operators are also available, as
-seen here.
-
     >>> browser.cookies['sha'] = 'zam'
     >>> len(browser.cookies)
     2
@@ -208,487 +192,15 @@
     >>> print browser.contents # server got the cookie change
     foo: bar
     sha: zam
-    >>> browser.cookies.update({'va': 'voom', 'tweedle': 'dee'})
     >>> pprint.pprint(sorted(browser.cookies.items()))
-    [('foo', 'bar'), ('sha', 'zam'), ('tweedle', 'dee'), ('va', 'voom')]
-    >>> browser.open('http://localhost/get_cookie.html')
-    >>> print browser.headers.get('set-cookie')
-    None
-    >>> print browser.contents
-    foo: bar
-    sha: zam
-    tweedle: dee
-    va: voom
-    >>> del browser.cookies['foo']
-    >>> del browser.cookies['tweedle']
-    >>> browser.open('http://localhost/get_cookie.html')
-    >>> print browser.contents
-    sha: zam
-    va: voom
+    [('foo', 'bar'), ('sha', 'zam')]
+    >>> browser.cookies.clearAll()
+    >>> len(browser.cookies)
+    0
 
-You can see the header in the ``header`` attribute and the repr and str.
+Many more examples, and a discussion of the additional methods available, can
+be found in cookies.txt.
 
-    >>> browser.cookies.header
-    'sha=zam; va=voom'
-    >>> browser.cookies # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
-    <zope.testbrowser.cookies.Cookies object at ... for
-     http://localhost/get_cookie.html (sha=zam; va=voom)>
-    >>> str(browser.cookies)
-    'sha=zam; va=voom'
-
-The ``cookies`` mapping also has an extended interface to get and set extra
-information about each cookie.  ``getinfo`` returns a dictionary.  Here is the
-interface description.
-
-::
-
-    def getinfo(name):
-       """returns dict of settings for the given cookie name.
-
-       This includes only the following cookie values: 
-
-       - name (str)
-       - value (str),
-       - port (int or None),
-       - domain (str),
-       - path (str or None),
-       - secure (bool), and
-       - expires (datetime.datetime with pytz.UTC timezone or None),
-       - comment (str or None),
-       - commenturl (str or None).
-       """
-
-Here are some examples.
-
-    >>> browser.open('http://localhost/set_cookie.html?name=foo&value=bar')
-    >>> pprint.pprint(browser.cookies.getinfo('foo'))
-    {'comment': None,
-     'commenturl': None,
-     'domain': 'localhost.local',
-     'expires': None,
-     'name': 'foo',
-     'path': '/',
-     'port': None,
-     'secure': False,
-     'value': 'bar'}
-    >>> pprint.pprint(browser.cookies.getinfo('sha'))
-    {'comment': None,
-     'commenturl': None,
-     'domain': 'localhost.local',
-     'expires': None,
-     'name': 'sha',
-     'path': '/',
-     'port': None,
-     'secure': False,
-     'value': 'zam'}
-    >>> import datetime
-    >>> expires = datetime.datetime(2030, 1, 1).strftime(
-    ...     '%a, %d %b %Y %H:%M:%S GMT')
-    >>> browser.open(
-    ...     'http://localhost/set_cookie.html?name=wow&value=wee&'
-    ...     'expires=%s' %
-    ...     (expires,))
-    >>> pprint.pprint(browser.cookies.getinfo('wow'))
-    {'comment': None,
-     'commenturl': None,
-     'domain': 'localhost.local',
-     'expires': datetime.datetime(2030, 1, 1, 0, 0, tzinfo=<UTC>),
-     'name': 'wow',
-     'path': '/',
-     'port': None,
-     'secure': False,
-     'value': 'wee'}
-
-Max-age is converted to an "expires" value.
-
-    >>> browser.open(
-    ...     'http://localhost/set_cookie.html?name=max&value=min&'
-    ...     'max-age=3000&&comment=silly+billy')
-    >>> pprint.pprint(browser.cookies.getinfo('max')) # doctest: +ELLIPSIS
-    {'comment': 'silly%20billy',
-     'commenturl': None,
-     'domain': 'localhost.local',
-     'expires': datetime.datetime(..., tzinfo=<UTC>),
-     'name': 'max',
-     'path': '/',
-     'port': None,
-     'secure': False,
-     'value': 'min'}
-
-You can iterate over all of the information about the cookies for the current
-page using the ``iterinfo`` method.
-
-    >>> pprint.pprint(sorted(browser.cookies.iterinfo(),
-    ...                      key=lambda info: info['name']))
-    ... # doctest: +ELLIPSIS
-    [{'comment': None,
-      'commenturl': None,
-      'domain': 'localhost.local',
-      'expires': None,
-      'name': 'foo',
-      'path': '/',
-      'port': None,
-      'secure': False,
-      'value': 'bar'},
-     {'comment': 'silly%20billy',
-      'commenturl': None,
-      'domain': 'localhost.local',
-      'expires': datetime.datetime(..., tzinfo=<UTC>),
-      'name': 'max',
-      'path': '/',
-      'port': None,
-      'secure': False,
-      'value': 'min'},
-     {'comment': None,
-      'commenturl': None,
-      'domain': 'localhost.local',
-      'expires': None,
-      'name': 'sha',
-      'path': '/',
-      'port': None,
-      'secure': False,
-      'value': 'zam'},
-     {'comment': None,
-      'commenturl': None,
-      'domain': 'localhost.local',
-      'expires': None,
-      'name': 'va',
-      'path': '/',
-      'port': None,
-      'secure': False,
-      'value': 'voom'},
-     {'comment': None,
-      'commenturl': None,
-      'domain': 'localhost.local',
-      'expires': datetime.datetime(2030, 1, 1, 0, 0, tzinfo=<UTC>),
-      'name': 'wow',
-      'path': '/',
-      'port': None,
-      'secure': False,
-      'value': 'wee'}]
-
-If you want to look at the cookies for another page, you can either navigate to
-the other page in the browser, or use the ``forURL`` method, which returns an
-ICookies instance for the new URL.
-
-    >>> sorted(browser.cookies.forURL(
-    ...     'http://localhost/inner/set_cookie.html').keys())
-    ['foo', 'max', 'sha', 'va', 'wow']
-    >>> extra_cookie = browser.cookies.forURL(
-    ...     'http://localhost/inner/set_cookie.html')
-    >>> extra_cookie['gew'] = 'gaw'
-    >>> extra_cookie.getinfo('gew')['path']
-    '/inner'
-    >>> sorted(extra_cookie.keys())
-    ['foo', 'gew', 'max', 'sha', 'va', 'wow']
-    >>> sorted(browser.cookies.keys())
-    ['foo', 'max', 'sha', 'va', 'wow']
-
-    >>> import zope.app.folder.folder
-    >>> getRootFolder()['inner'] = zope.app.folder.folder.Folder()
-    >>> getRootFolder()['inner']['path'] = zope.app.folder.folder.Folder()
-    >>> import transaction
-    >>> transaction.commit()
-    >>> browser.open('http://localhost/inner/get_cookie.html')
-    >>> print browser.contents # has gewgaw
-    foo: bar
-    gew: gaw
-    max: min
-    sha: zam
-    va: voom
-    wow: wee
-    >>> browser.open('http://localhost/inner/path/get_cookie.html')
-    >>> print browser.contents # has gewgaw
-    foo: bar
-    gew: gaw
-    max: min
-    sha: zam
-    va: voom
-    wow: wee
-    >>> browser.open('http://localhost/get_cookie.html')
-    >>> print browser.contents # NO gewgaw
-    foo: bar
-    max: min
-    sha: zam
-    va: voom
-    wow: wee
-
-Here's an example of the server setting a cookie that is only available on an
-inner page.
-
-    >>> browser.open(
-    ...     'http://localhost/inner/path/set_cookie.html?name=big&value=kahuna'
-    ...     )
-    >>> browser.cookies['big']
-    'kahuna'
-    >>> browser.cookies.getinfo('big')['path']
-    '/inner/path'
-    >>> browser.cookies.getinfo('gew')['path']
-    '/inner'
-    >>> browser.cookies.getinfo('foo')['path']
-    '/'
-    >>> print browser.cookies.forURL('http://localhost/').get('big')
-    None
-
-The basic mapping API only allows setting values.  If a cookie already exists
-for the given name, it will be changed; or else a new cookie will be created
-for the current request's domain and a path of '/', set to last for only this
-browser session (a "session" cookie).  To create or set cookies with different
-additional information, use the ``set`` method.  Here is an example.
-
-    >>> from zope.testbrowser.cookies import UTC
-    >>> browser.cookies.set(
-    ...     'bling', value='blang', path='/inner',
-    ...     expires=datetime.datetime(2020, 1, 1, tzinfo=UTC),
-    ...     comment='follow swallow')
-    >>> pprint.pprint(browser.cookies.getinfo('bling'))
-    {'comment': 'follow%20swallow',
-     'commenturl': None,
-     'domain': 'localhost.local',
-     'expires': datetime.datetime(2020, 1, 1, 0, 0, tzinfo=<UTC>),
-     'name': 'bling',
-     'path': '/inner',
-     'port': None,
-     'secure': False,
-     'value': 'blang'}
-
-In these further examples, note that the testbrowser sends all domains to Zope,
-and both http and https.
-
-    >>> browser.open('https://dev.example.com/inner/path/get_cookie.html')
-    >>> browser.cookies.keys() # a different domain
-    []
-    >>> browser.cookies.set('tweedle', 'dee')
-    >>> pprint.pprint(browser.cookies.getinfo('tweedle'))
-    {'comment': None,
-     'commenturl': None,
-     'domain': 'dev.example.com',
-     'expires': None,
-     'name': 'tweedle',
-     'path': '/inner/path',
-     'port': None,
-     'secure': False,
-     'value': 'dee'}
-    >>> browser.cookies.set('boo', 'yah', domain='.example.com', path='/inner',
-    ...             secure=True)
-    >>> pprint.pprint(browser.cookies.getinfo('boo'))
-    {'comment': None,
-     'commenturl': None,
-     'domain': '.example.com',
-     'expires': None,
-     'name': 'boo',
-     'path': '/inner',
-     'port': None,
-     'secure': True,
-     'value': 'yah'}
-    >>> sorted(browser.cookies.keys())
-    ['boo', 'tweedle']
-    >>> browser.open('https://dev.example.com/inner/path/get_cookie.html')
-    >>> print browser.contents
-    boo: yah
-    tweedle: dee
-    >>> browser.open( # not https, so not secure, so not 'boo'
-    ...     'http://dev.example.com/inner/path/get_cookie.html')
-    >>> sorted(browser.cookies.keys())
-    ['tweedle']
-    >>> print browser.contents
-    tweedle: dee
-    >>> browser.open( # not tweedle's domain
-    ...     'https://prod.example.com/inner/path/get_cookie.html')
-    >>> sorted(browser.cookies.keys())
-    ['boo']
-    >>> print browser.contents
-    boo: yah
-    >>> browser.open( # not tweedle's domain
-    ...     'https://example.com/inner/path/get_cookie.html')
-    >>> sorted(browser.cookies.keys())
-    ['boo']
-    >>> print browser.contents
-    boo: yah
-    >>> browser.open( # not tweedle's path
-    ...     'https://dev.example.com/inner/get_cookie.html')
-    >>> sorted(browser.cookies.keys())
-    ['boo']
-    >>> print browser.contents
-    boo: yah
-
-The API allows creation of cookies that mask existing cookies, but it does not
-allow creating a cookie that will be immediately masked upon creation. Having
-multiple cookies with the same name for a given URL is rare, and is a
-pathological case for using a mapping API to work with cookies, but it is
-supported to some degree, as demonstrated below.  Note that the Cookie RFCs
-(2109, 2965) specify that all matching cookies be sent to the server, but with
-an ordering so that more specific paths come first. We also prefer more
-specific domains, though the RFCs state that the ordering of cookies with the
-same path is indeterminate.  The best-matching cookie is the one that the
-mapping API uses.
-
-Also note that ports, as sent by RFC 2965's Cookie2 and Set-Cookie2 headers,
-are parsed and stored by this API but are not used for filtering as of this
-writing.
-
-This is an example of making one cookie that masks another because of path.
-First, unless you pass an explicit path, you will be modifying the existing
-cookie.
-
-    >>> browser.open('https://dev.example.com/inner/path/get_cookie.html')
-    >>> print browser.contents
-    boo: yah
-    tweedle: dee
-    >>> browser.cookies.getinfo('boo')['path']
-    '/inner'
-    >>> browser.cookies['boo'] = 'hoo'
-    >>> browser.cookies.getinfo('boo')['path']
-    '/inner'
-    >>> browser.cookies.getinfo('boo')['secure']
-    True
-
-Now we mask the cookie, using the path.
-
-    >>> browser.cookies.set('boo', 'boo', path='/inner/path')
-    >>> browser.cookies['boo']
-    'boo'
-    >>> browser.cookies.getinfo('boo')['path']
-    '/inner/path'
-    >>> browser.cookies.getinfo('boo')['secure']
-    False
-    >>> browser.cookies['boo']
-    'boo'
-    >>> sorted(browser.cookies.keys())
-    ['boo', 'tweedle']
-
-To identify the additional cookies, you can change the URL...
-
-    >>> extra_cookies = browser.cookies.forURL(
-    ...     'https://dev.example.com/inner/get_cookie.html')
-    >>> extra_cookies['boo']
-    'hoo'
-    >>> extra_cookies.getinfo('boo')['path']
-    '/inner'
-    >>> extra_cookies.getinfo('boo')['secure']
-    True
-
-...or use ``iterinfo`` and pass in a name.
-
-    >>> pprint.pprint(list(browser.cookies.iterinfo('boo')))
-    [{'comment': None,
-      'commenturl': None,
-      'domain': 'dev.example.com',
-      'expires': None,
-      'name': 'boo',
-      'path': '/inner/path',
-      'port': None,
-      'secure': False,
-      'value': 'boo'},
-     {'comment': None,
-      'commenturl': None,
-      'domain': '.example.com',
-      'expires': None,
-      'name': 'boo',
-      'path': '/inner',
-      'port': None,
-      'secure': True,
-      'value': 'hoo'}]
-
-An odd situation in this case is that deleting a cookie can sometimes reveal
-another one.
-
-    >>> browser.open('https://dev.example.com/inner/path/get_cookie.html')
-    >>> browser.cookies['boo']
-    'boo'
-    >>> del browser.cookies['boo']
-    >>> browser.cookies['boo']
-    'hoo'
-
-Setting a cookie that will be immediately masked within the current url is not
-allowed.
-
-    >>> browser.cookies.getinfo('tweedle')['path']
-    '/inner/path'
-    >>> browser.cookies.set('tweedle', 'dum', path='/inner')
-    ... # doctest: +NORMALIZE_WHITESPACE
-    Traceback (most recent call last):
-    ...
-    ValueError: cannot set a cookie that will be hidden by another cookie for
-    this url (https://dev.example.com/inner/path/get_cookie.html)
-    >>> browser.cookies['tweedle']
-    'dee'
-
-    XXX then show for domain.
-
-XXX show can't set hidden cookie, but can hide another cookie
-
-The ``expire`` method is really just a convenience.  Here are some examples.
-XXX this pretty much straight from an email with John.  Adjust to put in
-context.
-
-    >>> import zope.testbrowser.cookies
-    >>> import mechanize
-    >>> import datetime
-    >>> br = mechanize.Browser()
-    >>> cookies = zope.testbrowser.cookies.Cookies(br, 'http://example.com')
-    >>> cookies.set('foo', 'bar', domain='example.com')
-    >>> cookies.expire('foo', datetime.datetime(2021, 1, 1))
-    >>> cookies.getinfo('foo')['expires']
-    datetime.datetime(2021, 1, 1, 0, 0, tzinfo=<UTC>)
-    >>> cookies.expire('foo')
-    >>> len(cookies)
-    0
-
-That's the main story.  Now here are some edge cases.
-
-    >>> cookies.set('foo', 'bar', domain='example.com')
-    >>> cookies.expire(
-    ...     'foo',
-    ...     zope.testbrowser.cookies.expiration_string(
-    ...         datetime.datetime(2020, 1, 1)))
-    >>> cookies.getinfo('foo')['expires']
-    datetime.datetime(2020, 1, 1, 0, 0, tzinfo=<UTC>)
-    >>> cookies.forURL('http://example.com').expire(
-    ...     'foo',
-    ...     zope.testbrowser.cookies.expiration_string(
-    ...         datetime.datetime(2019, 1, 1)))
-    >>> cookies.getinfo('foo')['expires']
-    datetime.datetime(2019, 1, 1, 0, 0, tzinfo=<UTC>)
-    >>> cookies['foo']
-    'bar'
-    >>> cookies.expire('foo', datetime.datetime(1999, 1, 1))
-    >>> len(cookies)
-    0
-    >>> cookies.expire(
-    ...     'foo',
-    ...     zope.testbrowser.cookies.expiration_string(
-    ...         datetime.datetime(1999, 1, 1)))
-    >>> len(cookies)
-    0
-
-
-#Note that explicitly setting a Cookie header is an error if the ``cookies``
-#mapping has any values; and adding a new cookie to the ``cookies`` mapping
-#is an error if the Cookie header is already set.
-#
-#    >>> browser.addHeader('Cookie', 'gee=gaw') # XXX Cookie or Cookie2
-#    Traceback (most recent call last):
-#    ...
-#    ValueError: cookies are already set in `cookies` attribute
-#
-#    >>> new_browser = Browser('http://localhost/get_cookie.html')
-#    >>> print new_browser.headers.get('set-cookie')
-#    None
-#    >>> print new_browser.contents
-#    <BLANKLINE>
-#    >>> new_browser.addHeader('Cookie', 'gee=gaw')
-#    Traceback (most recent call last):
-#    ...
-#    ValueError: cookies are already set in `Cookie` header
-#    >>> new_browser.cookies['fee'] = 'fi'
-#    >>> del new_browser # clean up
-
-XXX show path example, and how masking works; lots of other stuff to show like
-popinfo, update, update from cookies, expire, clear, clearAll, clearAllSession.
-
-    >>> browser.cookies.clearAll() # clean out cookies for subsequent tests
-
 Navigation and Link Objects
 ---------------------------
 

Modified: zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/browser.py
===================================================================
--- zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/browser.py	2008-11-05 16:38:52 UTC (rev 92801)
+++ zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/browser.py	2008-11-05 21:48:13 UTC (rev 92802)
@@ -291,6 +291,12 @@
 
     def addHeader(self, key, value):
         """See zope.testbrowser.interfaces.IBrowser"""
+        if (self.mech_browser.request is not None and
+            key.lower() in ('cookie', 'cookie2') and
+            self.cookies.header):
+            # to prevent unpleasant intermittent errors, only set cookies with
+            # the browser headers OR the cookies mapping.
+            raise ValueError('cookies are already set in `cookies` attribute')
         self.mech_browser.addheaders.append( (str(key), str(value)) )
 
     def getLink(self, text=None, url=None, id=None, index=0):

Modified: zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/cookies.py
===================================================================
--- zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/cookies.py	2008-11-05 16:38:52 UTC (rev 92801)
+++ zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/cookies.py	2008-11-05 21:48:13 UTC (rev 92802)
@@ -51,6 +51,9 @@
 
     UTC = _UTC()
 
+import zope.interface
+from zope.testbrowser import interfaces
+
 # Cookies class helpers
 
 
@@ -86,6 +89,8 @@
     """Cookies for mechanize browser.
     """
 
+    zope.interface.implements(interfaces.ICookies)
+
     def __init__(self, mech_browser, url=None):
         self.mech_browser = mech_browser
         self._url = url
@@ -116,9 +121,6 @@
                 raise RuntimeError('no request found')
             return request
 
-    def __str__(self):
-        return self.header
-
     @property
     def header(self):
         request = self.mech_browser.request_class(self.url)
@@ -218,57 +220,114 @@
         ck = self._get(key)
         self._jar.clear(ck.domain, ck.path, ck.name)
 
-    def set(self, name, value=None,
+    def create(self, name, value,
+               domain=None, expires=None, path=None, secure=None, comment=None,
+               commenturl=None, port=None):
+        if value is None:
+            raise ValueError('must provide value')
+        ck = self._get(name, None)
+        if (ck is not None and
+            (path is None or ck.path == path) and
+            (domain is None or ck.domain == domain or
+             ck.domain == domain) and
+            (port is None or ck.port == port)):
+            # cookie already exists
+            raise ValueError('cookie already exists')
+        if domain is not None:
+            self._verifyDomain(domain, ck)
+        if path is not None:
+            self._verifyPath(path, ck)
+        if expires is not None and self._is_expired(expires):
+            raise zope.testbrowser.interfaces.AlreadyExpiredError(
+                'May not create a cookie that is immediately expired')
+        self._setCookie(name, value, domain, expires, path, secure, comment,
+                        commenturl, port)
+
+    def change(self, name, value=None,
             domain=None, expires=None, path=None, secure=None, comment=None,
             commenturl=None, port=None):
-        request = self._request
-        if request is None:
-            raise mechanize.BrowserStateError(
-                'cannot create cookie without request')
-        ck = self._get(name, None)
-        use_ck = (ck is not None and
-                  (path is None or ck.path == path) and
-                  (domain is None or ck.domain == domain))
-        if path is not None:
-            self_path = urlparse.urlparse(self.url)[2]
-            if not self_path.startswith(path):
-                raise ValueError('current url must start with path, if given')
-            if ck is not None and ck.path != path and ck.path.startswith(path):
-                raise ValueError(
-                    'cannot set a cookie that will be hidden by another '
-                    'cookie for this url (%s)' % (self.url,))
-            # you CAN hide an existing cookie, by passing an explicit path
-        elif use_ck:
+        if expires is not None and self._is_expired(expires):
+            # shortcut
+            del self[name]
+        else:
+            self._change(self._get(name), value, domain, expires, path, secure,
+                         comment, commenturl, port)
+
+    def _change(self, ck, value=None,
+                domain=None, expires=None, path=None, secure=None,
+                comment=None, commenturl=None, port=None):
+        if value is None:
+            value = ck.value
+        if domain is None:
+            domain = ck.domain
+        else:
+            self._verifyDomain(domain, ck)
+        if expires is None:
+            expires = ck.expires
+        if path is None:
             path = ck.path
-        if expires is not None and self._is_expired(expires):
-            if use_ck:
-                raise ValueError('May not delete a cookie using ``set``')
+        else:
+            self._verifyPath(domain, ck)
+        if secure is None:
+            secure = ck.secure
+        if comment is None:
+            comment = ck.comment
+        if commenturl is None:
+            commenturl = ck.comment_url
+        if port is None:
+            port = ck.port
+        self._setCookie(ck.name, value, domain, expires, path, secure, comment,
+                        commenturl, port, ck.version, ck=ck)
+
+    def _verifyDomain(self, domain, ck):
+        if domain is not None and domain.startswith('.'):
+            domain = domain[1:]
+        self_host = cookielib.eff_request_host(self._request)[1]
+        if (self_host != domain and
+            not self_host.endswith('.' + domain)):
+            raise ValueError('current url must match given domain')
+        if (ck is not None and ck.domain != domain and
+            ck.domain.endswith(domain)):
+            raise ValueError(
+                'cannot set a cookie that will be hidden by another '
+                'cookie for this url (%s)' % (self.url,))
+
+    def _verifyPath(self, path, ck):
+        self_path = urlparse.urlparse(self.url)[2]
+        if not self_path.startswith(path):
+            raise ValueError('current url must start with path, if given')
+        if ck is not None and ck.path != path and ck.path.startswith(path):
+            raise ValueError(
+                'cannot set a cookie that will be hidden by another '
+                'cookie for this url (%s)' % (self.url,))
+
+    def _setCookie(self, name, value, domain, expires, path, secure, comment,
+                   commenturl, port, version=None, ck=None):
+        for nm, val in self.mech_browser.addheaders:
+            if nm.lower() in ('cookie', 'cookie2'):
+                raise ValueError('cookies are already set in `Cookie` header')
+        if domain:
+            # we do a dance here so that we keep names that have been passed
+            # in consistent (i.e., if we get an explicit 'example.com' it stays
+            # 'example.com', rather than converting to '.example.com').
+            if domain.startswith('.'):
+                tmp_domain = domain[1:]
             else:
-                raise ValueError(
-                    'May not create a cookie that is immediately ignored')
-        version = None
-        if use_ck:
-            # keep unchanged existing cookie values
-            if domain is None:
-                domain = ck.domain
-            if value is None:
-                value = ck.value
-            if port is None:
-                port = ck.port
-            if comment is None:
-                comment = ck.comment
-            if commenturl is None:
-                commenturl = ck.comment_url
-            if secure is None:
-                secure = ck.secure
-            if expires is None and ck.expires is not None:
-                expires = datetime.datetime.fromtimestamp(ck.expires, UTC)
-            version = ck.version
-        # else...if the domain is bad, set_cookie_if_ok should catch it.
+                tmp_domain = domain
+                domain = None
+            if secure:
+                protocol = 'https'
+            else:
+                protocol = 'http'
+            url = '%s://%s%s' % (protocol, tmp_domain, path or '/')
+            request = self.mech_browser.request_class(url)
+        else:
+            request = self._request
+            if request is None:
+                raise mechanize.BrowserStateError(
+                    'cannot create cookie without request or domain')
         c = Cookie.SimpleCookie()
         name = str(name)
-        if value is None:
-            raise ValueError('if cookie does not exist, must provide value')
         c[name] = value.encode('utf8')
         if secure:
             c[name]['secure'] = True
@@ -293,25 +352,16 @@
             _StubResponse([c.output(header='').strip()]), request)
         assert len(cookies) == 1, (
             'programmer error: %d cookies made' % (len(cookies),))
+        if ck is not None:
+            self._jar.clear(ck.domain, ck.path, ck.name)
         self._jar.set_cookie_if_ok(cookies[0], request)
 
-    def update(self, source=None, **kwargs):
-        if isinstance(source, Cookies): # XXX change to ICookies.providedBy
-            if self.url != source.url:
-                raise ValueError('can only update from another ICookies '
-                                 'instance if it shares the identical url')
-            elif self is source:
-                return
-            else:
-                for info in source.iterInfo():
-                    self.set(info['name'], info['value'], info['expires'],
-                             info['domain'], info['path'], info['secure'],
-                             info['comment'])
-            source = None # to support kwargs
-        UserDict.DictMixin.update(self, source, **kwargs)
-
     def __setitem__(self, key, value):
-        self.set(key, value)
+        ck = self._get(key, None)
+        if ck is None:
+            self.create(key, value)
+        else:
+            self._change(ck, value)
 
     def _is_expired(self, value):
         if isinstance(value, datetime.datetime):
@@ -327,32 +377,14 @@
                 return True
         return False
 
-    def expire(self, name, expires=None):
-        if expires is None or self._is_expired(expires):
-            del self[name]
-        else:
-            res = self.set(name, expires=expires)
-
     def clear(self):
         # to give expected mapping behavior of resulting in an empty dict,
         # we use _raw_cookies rather than _get_cookies.
-        for cookies in self._raw_cookies():
+        for ck in self._raw_cookies():
             self._jar.clear(ck.domain, ck.path, ck.name)
 
-    def popinfo(self, key, *args):
-        if len(args) > 1:
-            raise TypeError, "popinfo expected at most 2 arguments, got "\
-                              + repr(1 + len(args))
-        ck = self._get(key, None)
-        if ck is None:
-            if args:
-                return args[0]
-            raise KeyError(key)
-        self._jar.clear(ck.domain, ck.path, ck.name)
-        return self._getinfo(ck)
-
-    def clearAllSession(self): # XXX could add optional "domain" filter or similar
+    def clearAllSession(self):
         self._jar.clear_session_cookies()
 
-    def clearAll(self): # XXX could add optional "domain" filter or similar
+    def clearAll(self):
         self._jar.clear()

Added: zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/cookies.txt
===================================================================
--- zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/cookies.txt	                        (rev 0)
+++ zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/cookies.txt	2008-11-05 21:48:13 UTC (rev 92802)
@@ -0,0 +1,800 @@
+=======
+Cookies
+=======
+
+Getting started
+===============
+
+The cookies mapping has an extended mapping interface that allows getting,
+setting, and deleting the cookies that the browser is remembering for the
+current url, or for an explicitly provided URL.
+
+    >>> from zope.testbrowser.testing import Browser
+    >>> browser = Browser()
+
+Initially the browser does not point to a URL, and the cookies cannot be used.
+
+    >>> len(browser.cookies)
+    Traceback (most recent call last):
+    ...
+    RuntimeError: no request found
+    >>> browser.cookies.keys()
+    Traceback (most recent call last):
+    ...
+    RuntimeError: no request found
+
+Once you send the browser to a URL, the cookies attribute can be used.
+
+    >>> browser.open('http://localhost/')
+    >>> len(browser.cookies)
+    0
+    >>> browser.cookies.keys()
+    []
+    >>> browser.url
+    'http://localhost/'
+    >>> browser.cookies.url
+    'http://localhost/'
+    >>> import zope.testbrowser.interfaces
+    >>> from zope.interface.verify import verifyObject
+    >>> verifyObject(zope.testbrowser.interfaces.ICookies, browser.cookies)
+    True
+
+Alternatively, you can use the ``forURL`` method to get another instance of
+the cookies mapping for the given URL.
+
+    >>> len(browser.cookies.forURL('http://www.example.com'))
+    0
+    >>> browser.cookies.forURL('http://www.example.com').keys()
+    []
+    >>> browser.cookies.forURL('http://www.example.com').url
+    'http://www.example.com'
+    >>> browser.url
+    'http://localhost/'
+    >>> browser.cookies.url
+    'http://localhost/'
+
+Here, we use a view that will make the server set cookies with the
+values we provide.
+
+    >>> browser.open('http://localhost/set_cookie.html?name=foo&value=bar')
+    >>> browser.headers['set-cookie']
+    'foo=bar;'
+
+
+Basic Mapping Interface
+=======================
+
+Now the cookies for localhost have a value.  These are examples of just the
+basic accessor operators and methods.
+ 
+    >>> browser.cookies['foo']
+    'bar'
+    >>> browser.cookies.keys()
+    ['foo']
+    >>> browser.cookies.values()
+    ['bar']
+    >>> browser.cookies.items()
+    [('foo', 'bar')]
+    >>> 'foo' in browser.cookies
+    True
+    >>> 'bar' in browser.cookies
+    False
+    >>> len(browser.cookies)
+    1
+    >>> print(dict(browser.cookies))
+    {'foo': 'bar'}
+
+As you would expect, the cookies attribute can also be used to examine cookies
+that have already been set in a previous request.  To demonstrate this, we use
+another view that does not set cookies but reports on the cookies it receives
+from the browser.
+
+    >>> browser.open('http://localhost/get_cookie.html')
+    >>> print browser.headers.get('set-cookie')
+    None
+    >>> browser.contents
+    'foo: bar'
+    >>> browser.cookies['foo']
+    'bar'
+
+The standard mapping mutation methods and operators are also available, as
+seen here.
+
+    >>> browser.cookies['sha'] = 'zam'
+    >>> len(browser.cookies)
+    2
+    >>> import pprint
+    >>> pprint.pprint(sorted(browser.cookies.items()))
+    [('foo', 'bar'), ('sha', 'zam')]
+    >>> browser.open('http://localhost/get_cookie.html')
+    >>> print browser.headers.get('set-cookie')
+    None
+    >>> print browser.contents # server got the cookie change
+    foo: bar
+    sha: zam
+    >>> browser.cookies.update({'va': 'voom', 'tweedle': 'dee'})
+    >>> pprint.pprint(sorted(browser.cookies.items()))
+    [('foo', 'bar'), ('sha', 'zam'), ('tweedle', 'dee'), ('va', 'voom')]
+    >>> browser.open('http://localhost/get_cookie.html')
+    >>> print browser.headers.get('set-cookie')
+    None
+    >>> print browser.contents
+    foo: bar
+    sha: zam
+    tweedle: dee
+    va: voom
+    >>> del browser.cookies['foo']
+    >>> del browser.cookies['tweedle']
+    >>> browser.open('http://localhost/get_cookie.html')
+    >>> print browser.contents
+    sha: zam
+    va: voom
+
+Headers
+=======
+
+You can see the Cookies header that will be sent to the browser in the
+``header`` attribute and the repr and str.
+
+    >>> browser.cookies.header
+    'sha=zam; va=voom'
+    >>> browser.cookies # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
+    <zope.testbrowser.cookies.Cookies object at ... for
+     http://localhost/get_cookie.html (sha=zam; va=voom)>
+    >>> str(browser.cookies)
+    'sha=zam; va=voom'
+
+Extended Mapping Interface
+==========================
+
+------------------------------------------
+Read Methods: ``getinfo`` and ``iterinfo``
+------------------------------------------
+
+``getinfo``
+-----------
+
+The ``cookies`` mapping also has an extended interface to get and set extra
+information about each cookie.  ``getinfo`` returns a dictionary.  Here is the
+interface description.
+
+::
+
+    def getinfo(name):
+       """returns dict of settings for the given cookie name.
+
+       This includes only the following cookie values: 
+
+       - name (str)
+       - value (str),
+       - port (int or None),
+       - domain (str),
+       - path (str or None),
+       - secure (bool), and
+       - expires (datetime.datetime with pytz.UTC timezone or None),
+       - comment (str or None),
+       - commenturl (str or None).
+       """
+
+Here are some examples.
+
+    >>> browser.open('http://localhost/set_cookie.html?name=foo&value=bar')
+    >>> pprint.pprint(browser.cookies.getinfo('foo'))
+    {'comment': None,
+     'commenturl': None,
+     'domain': 'localhost.local',
+     'expires': None,
+     'name': 'foo',
+     'path': '/',
+     'port': None,
+     'secure': False,
+     'value': 'bar'}
+    >>> pprint.pprint(browser.cookies.getinfo('sha'))
+    {'comment': None,
+     'commenturl': None,
+     'domain': 'localhost.local',
+     'expires': None,
+     'name': 'sha',
+     'path': '/',
+     'port': None,
+     'secure': False,
+     'value': 'zam'}
+    >>> import datetime
+    >>> expires = datetime.datetime(2030, 1, 1).strftime(
+    ...     '%a, %d %b %Y %H:%M:%S GMT')
+    >>> browser.open(
+    ...     'http://localhost/set_cookie.html?name=wow&value=wee&'
+    ...     'expires=%s' %
+    ...     (expires,))
+    >>> pprint.pprint(browser.cookies.getinfo('wow'))
+    {'comment': None,
+     'commenturl': None,
+     'domain': 'localhost.local',
+     'expires': datetime.datetime(2030, 1, 1, 0, 0, tzinfo=<UTC>),
+     'name': 'wow',
+     'path': '/',
+     'port': None,
+     'secure': False,
+     'value': 'wee'}
+
+Max-age is converted to an "expires" value.
+
+    >>> browser.open(
+    ...     'http://localhost/set_cookie.html?name=max&value=min&'
+    ...     'max-age=3000&&comment=silly+billy')
+    >>> pprint.pprint(browser.cookies.getinfo('max')) # doctest: +ELLIPSIS
+    {'comment': 'silly%20billy',
+     'commenturl': None,
+     'domain': 'localhost.local',
+     'expires': datetime.datetime(..., tzinfo=<UTC>),
+     'name': 'max',
+     'path': '/',
+     'port': None,
+     'secure': False,
+     'value': 'min'}
+
+``iterinfo``
+------------
+
+You can iterate over all of the information about the cookies for the current
+page using the ``iterinfo`` method.
+
+    >>> pprint.pprint(sorted(browser.cookies.iterinfo(),
+    ...                      key=lambda info: info['name']))
+    ... # doctest: +ELLIPSIS
+    [{'comment': None,
+      'commenturl': None,
+      'domain': 'localhost.local',
+      'expires': None,
+      'name': 'foo',
+      'path': '/',
+      'port': None,
+      'secure': False,
+      'value': 'bar'},
+     {'comment': 'silly%20billy',
+      'commenturl': None,
+      'domain': 'localhost.local',
+      'expires': datetime.datetime(..., tzinfo=<UTC>),
+      'name': 'max',
+      'path': '/',
+      'port': None,
+      'secure': False,
+      'value': 'min'},
+     {'comment': None,
+      'commenturl': None,
+      'domain': 'localhost.local',
+      'expires': None,
+      'name': 'sha',
+      'path': '/',
+      'port': None,
+      'secure': False,
+      'value': 'zam'},
+     {'comment': None,
+      'commenturl': None,
+      'domain': 'localhost.local',
+      'expires': None,
+      'name': 'va',
+      'path': '/',
+      'port': None,
+      'secure': False,
+      'value': 'voom'},
+     {'comment': None,
+      'commenturl': None,
+      'domain': 'localhost.local',
+      'expires': datetime.datetime(2030, 1, 1, 0, 0, tzinfo=<UTC>),
+      'name': 'wow',
+      'path': '/',
+      'port': None,
+      'secure': False,
+      'value': 'wee'}]
+
+Extended Examples
+-----------------
+
+If you want to look at the cookies for another page, you can either navigate to
+the other page in the browser, or, as already mentioned, you can use the
+``forURL`` method, which returns an ICookies instance for the new URL.
+
+    >>> sorted(browser.cookies.forURL(
+    ...     'http://localhost/inner/set_cookie.html').keys())
+    ['foo', 'max', 'sha', 'va', 'wow']
+    >>> extra_cookie = browser.cookies.forURL(
+    ...     'http://localhost/inner/set_cookie.html')
+    >>> extra_cookie['gew'] = 'gaw'
+    >>> extra_cookie.getinfo('gew')['path']
+    '/inner'
+    >>> sorted(extra_cookie.keys())
+    ['foo', 'gew', 'max', 'sha', 'va', 'wow']
+    >>> sorted(browser.cookies.keys())
+    ['foo', 'max', 'sha', 'va', 'wow']
+
+    >>> import zope.app.folder.folder
+    >>> getRootFolder()['inner'] = zope.app.folder.folder.Folder()
+    >>> getRootFolder()['inner']['path'] = zope.app.folder.folder.Folder()
+    >>> import transaction
+    >>> transaction.commit()
+    >>> browser.open('http://localhost/inner/get_cookie.html')
+    >>> print browser.contents # has gewgaw
+    foo: bar
+    gew: gaw
+    max: min
+    sha: zam
+    va: voom
+    wow: wee
+    >>> browser.open('http://localhost/inner/path/get_cookie.html')
+    >>> print browser.contents # has gewgaw
+    foo: bar
+    gew: gaw
+    max: min
+    sha: zam
+    va: voom
+    wow: wee
+    >>> browser.open('http://localhost/get_cookie.html')
+    >>> print browser.contents # NO gewgaw
+    foo: bar
+    max: min
+    sha: zam
+    va: voom
+    wow: wee
+
+Here's an example of the server setting a cookie that is only available on an
+inner page.
+
+    >>> browser.open(
+    ...     'http://localhost/inner/path/set_cookie.html?name=big&value=kahuna'
+    ...     )
+    >>> browser.cookies['big']
+    'kahuna'
+    >>> browser.cookies.getinfo('big')['path']
+    '/inner/path'
+    >>> browser.cookies.getinfo('gew')['path']
+    '/inner'
+    >>> browser.cookies.getinfo('foo')['path']
+    '/'
+    >>> print browser.cookies.forURL('http://localhost/').get('big')
+    None
+
+----------------------------------------
+Write Methods: ``create`` and ``change``
+----------------------------------------
+
+The basic mapping API only allows setting values.  If a cookie already exists
+for the given name, it's value will be changed; or else a new cookie will be
+created for the current request's domain and a path of '/', set to last for
+only this browser session (a "session" cookie).
+
+To create or change cookies with different additional information, use the
+``create`` and ``change`` methods, respectively.  Here is an example of
+``create``.
+
+    >>> from zope.testbrowser.cookies import UTC
+    >>> browser.cookies.create(
+    ...     'bling', value='blang', path='/inner',
+    ...     expires=datetime.datetime(2020, 1, 1, tzinfo=UTC),
+    ...     comment='follow swallow')
+    >>> pprint.pprint(browser.cookies.getinfo('bling'))
+    {'comment': 'follow%20swallow',
+     'commenturl': None,
+     'domain': 'localhost.local',
+     'expires': datetime.datetime(2020, 1, 1, 0, 0, tzinfo=<UTC>),
+     'name': 'bling',
+     'path': '/inner',
+     'port': None,
+     'secure': False,
+     'value': 'blang'}
+
+In these further examples of ``create``, note that the testbrowser sends all
+domains to Zope, and both http and https.
+
+    >>> browser.open('https://dev.example.com/inner/path/get_cookie.html')
+    >>> browser.cookies.keys() # a different domain
+    []
+    >>> browser.cookies.create('tweedle', 'dee')
+    >>> pprint.pprint(browser.cookies.getinfo('tweedle'))
+    {'comment': None,
+     'commenturl': None,
+     'domain': 'dev.example.com',
+     'expires': None,
+     'name': 'tweedle',
+     'path': '/inner/path',
+     'port': None,
+     'secure': False,
+     'value': 'dee'}
+    >>> browser.cookies.create(
+    ...     'boo', 'yah', domain='.example.com', path='/inner', secure=True)
+    >>> pprint.pprint(browser.cookies.getinfo('boo'))
+    {'comment': None,
+     'commenturl': None,
+     'domain': '.example.com',
+     'expires': None,
+     'name': 'boo',
+     'path': '/inner',
+     'port': None,
+     'secure': True,
+     'value': 'yah'}
+    >>> sorted(browser.cookies.keys())
+    ['boo', 'tweedle']
+    >>> browser.open('https://dev.example.com/inner/path/get_cookie.html')
+    >>> print browser.contents
+    boo: yah
+    tweedle: dee
+    >>> browser.open( # not https, so not secure, so not 'boo'
+    ...     'http://dev.example.com/inner/path/get_cookie.html')
+    >>> sorted(browser.cookies.keys())
+    ['tweedle']
+    >>> print browser.contents
+    tweedle: dee
+    >>> browser.open( # not tweedle's domain
+    ...     'https://prod.example.com/inner/path/get_cookie.html')
+    >>> sorted(browser.cookies.keys())
+    ['boo']
+    >>> print browser.contents
+    boo: yah
+    >>> browser.open( # not tweedle's domain
+    ...     'https://example.com/inner/path/get_cookie.html')
+    >>> sorted(browser.cookies.keys())
+    ['boo']
+    >>> print browser.contents
+    boo: yah
+    >>> browser.open( # not tweedle's path
+    ...     'https://dev.example.com/inner/get_cookie.html')
+    >>> sorted(browser.cookies.keys())
+    ['boo']
+    >>> print browser.contents
+    boo: yah
+
+Masking by Path
+---------------
+
+The API allows creation of cookies that mask existing cookies, but it does not
+allow creating a cookie that will be immediately masked upon creation. Having
+multiple cookies with the same name for a given URL is rare, and is a
+pathological case for using a mapping API to work with cookies, but it is
+supported to some degree, as demonstrated below.  Note that the Cookie RFCs
+(2109, 2965) specify that all matching cookies be sent to the server, but with
+an ordering so that more specific paths come first. We also prefer more
+specific domains, though the RFCs state that the ordering of cookies with the
+same path is indeterminate.  The best-matching cookie is the one that the
+mapping API uses.
+
+Also note that ports, as sent by RFC 2965's Cookie2 and Set-Cookie2 headers,
+are parsed and stored by this API but are not used for filtering as of this
+writing.
+
+This is an example of making one cookie that masks another because of path.
+First, unless you pass an explicit path, you will be modifying the existing
+cookie.
+
+    >>> browser.open('https://dev.example.com/inner/path/get_cookie.html')
+    >>> print browser.contents
+    boo: yah
+    tweedle: dee
+    >>> browser.cookies.getinfo('boo')['path']
+    '/inner'
+    >>> browser.cookies['boo'] = 'hoo'
+    >>> browser.cookies.getinfo('boo')['path']
+    '/inner'
+    >>> browser.cookies.getinfo('boo')['secure']
+    True
+
+Now we mask the cookie, using the path.
+
+    >>> browser.cookies.create('boo', 'boo', path='/inner/path')
+    >>> browser.cookies['boo']
+    'boo'
+    >>> browser.cookies.getinfo('boo')['path']
+    '/inner/path'
+    >>> browser.cookies.getinfo('boo')['secure']
+    False
+    >>> browser.cookies['boo']
+    'boo'
+    >>> sorted(browser.cookies.keys())
+    ['boo', 'tweedle']
+
+To identify the additional cookies, you can change the URL...
+
+    >>> extra_cookies = browser.cookies.forURL(
+    ...     'https://dev.example.com/inner/get_cookie.html')
+    >>> extra_cookies['boo']
+    'hoo'
+    >>> extra_cookies.getinfo('boo')['path']
+    '/inner'
+    >>> extra_cookies.getinfo('boo')['secure']
+    True
+
+...or use ``iterinfo`` and pass in a name.
+
+    >>> pprint.pprint(list(browser.cookies.iterinfo('boo')))
+    [{'comment': None,
+      'commenturl': None,
+      'domain': 'dev.example.com',
+      'expires': None,
+      'name': 'boo',
+      'path': '/inner/path',
+      'port': None,
+      'secure': False,
+      'value': 'boo'},
+     {'comment': None,
+      'commenturl': None,
+      'domain': '.example.com',
+      'expires': None,
+      'name': 'boo',
+      'path': '/inner',
+      'port': None,
+      'secure': True,
+      'value': 'hoo'}]
+
+An odd situation in this case is that deleting a cookie can sometimes reveal
+another one.
+
+    >>> browser.open('https://dev.example.com/inner/path/get_cookie.html')
+    >>> browser.cookies['boo']
+    'boo'
+    >>> del browser.cookies['boo']
+    >>> browser.cookies['boo']
+    'hoo'
+
+Creating a cookie that will be immediately masked within the current url is not
+allowed.
+
+    >>> browser.cookies.getinfo('tweedle')['path']
+    '/inner/path'
+    >>> browser.cookies.create('tweedle', 'dum', path='/inner')
+    ... # doctest: +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+    ...
+    ValueError: cannot set a cookie that will be hidden by another cookie for
+    this url (https://dev.example.com/inner/path/get_cookie.html)
+    >>> browser.cookies['tweedle']
+    'dee'
+
+Masking by Domain
+-----------------
+
+All of the same behavior is also true for domains.  The only difference is a
+theoretical one: while the behavior of masking cookies via paths is defined by
+the relevant IRCs, it is not defined for domains.  Here, we simply follow a
+"best match" policy.
+
+We initialize by setting some cookies for example.org.
+
+    >>> browser.open('https://dev.example.org/get_cookie.html')
+    >>> browser.cookies.keys() # a different domain
+    []
+    >>> browser.cookies.create('tweedle', 'dee')
+    >>> browser.cookies.create('boo', 'yah', domain='example.org',
+    ...                     secure=True)
+
+Before we look at the examples, note that the default behavior of the cookies
+is to be liberal in the matching of domains.  According to the RFCs,
+a domain of 'example.com' can only be set implicitly from the server, and
+implies an exact match, so example.com URLs will get the cookie, but not
+*.example.com (i.e., dev.example.com).  Real browsers vary in their behavior
+in this regard.  The cookies collection, by default, has a looser
+interpretation of this, such that domains are always interpreted as
+effectively beginning with a ".", so dev.example.com will include a cookie from
+the "example.com" domain filter as if it were a ".example.com" filter.
+
+Now for the examples.  First, unless you pass an explicit domain, you will be
+modifying the existing cookie.
+
+    >>> browser.open('https://dev.example.org/get_cookie.html')
+    >>> print browser.contents
+    boo: yah
+    tweedle: dee
+    >>> browser.cookies.getinfo('boo')['domain']
+    'example.org'
+    >>> browser.cookies['boo'] = 'hoo'
+    >>> browser.cookies.getinfo('boo')['domain']
+    'example.org'
+    >>> browser.cookies.getinfo('boo')['secure']
+    True
+
+Now we mask the cookie, using the domain.
+
+    >>> browser.cookies.create('boo', 'boo', domain='dev.example.org')
+    >>> browser.cookies['boo']
+    'boo'
+    >>> browser.cookies.getinfo('boo')['domain']
+    'dev.example.org'
+    >>> browser.cookies.getinfo('boo')['secure']
+    False
+    >>> browser.cookies['boo']
+    'boo'
+    >>> sorted(browser.cookies.keys())
+    ['boo', 'tweedle']
+
+To identify the additional cookies, you can change the URL...
+
+    >>> extra_cookies = browser.cookies.forURL(
+    ...     'https://example.org/get_cookie.html')
+    >>> extra_cookies['boo']
+    'hoo'
+    >>> extra_cookies.getinfo('boo')['domain']
+    'example.org'
+    >>> extra_cookies.getinfo('boo')['secure']
+    True
+
+...or use ``iterinfo`` and pass in a name.
+
+    >>> pprint.pprint(list(browser.cookies.iterinfo('boo')))
+    [{'comment': None,
+      'commenturl': None,
+      'domain': 'dev.example.org',
+      'expires': None,
+      'name': 'boo',
+      'path': '/',
+      'port': None,
+      'secure': False,
+      'value': 'boo'},
+     {'comment': None,
+      'commenturl': None,
+      'domain': 'example.org',
+      'expires': None,
+      'name': 'boo',
+      'path': '/',
+      'port': None,
+      'secure': True,
+      'value': 'hoo'}]
+
+An odd situation in this case is that deleting a cookie can sometimes reveal
+another one.
+
+    >>> browser.open('https://dev.example.org/get_cookie.html')
+    >>> browser.cookies['boo']
+    'boo'
+    >>> del browser.cookies['boo']
+    >>> browser.cookies['boo']
+    'hoo'
+
+Setting a cookie with a foreign domain from the current URL is not allowed (use
+forURL to get around this).
+
+    >>> browser.cookies.create('tweedle', 'dum', domain='locahost.local')
+    Traceback (most recent call last):
+    ...
+    ValueError: current url must match given domain
+    >>> browser.cookies['tweedle']
+    'dee'
+
+Setting a cookie that will be immediately masked within the current url is also
+not allowed.
+
+    >>> browser.cookies.getinfo('tweedle')['domain']
+    'dev.example.org'
+    >>> browser.cookies.create('tweedle', 'dum', domain='.example.org')
+    ... # doctest: +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+    ...
+    ValueError: cannot set a cookie that will be hidden by another cookie for
+    this url (https://dev.example.org/get_cookie.html)
+    >>> browser.cookies['tweedle']
+    'dee'
+
+``change``
+----------
+
+So far all of our examples in this section have centered on ``create``.
+``change`` allows making changes to existing cookies.  Changing expiration
+is a good example.
+
+    >>> browser.open("http://example.net")
+    >>> browser.cookies['foo'] = 'bar'
+    >>> browser.cookies.change('foo', expires=datetime.datetime(2021, 1, 1))
+    >>> browser.cookies.getinfo('foo')['expires']
+    datetime.datetime(2021, 1, 1, 0, 0, tzinfo=<UTC>)
+
+That's the main story.  Now here are some edge cases.
+
+    >>> browser.cookies.change(
+    ...     'foo',
+    ...     expires=zope.testbrowser.cookies.expiration_string(
+    ...         datetime.datetime(2020, 1, 1)))
+    >>> browser.cookies.getinfo('foo')['expires']
+    datetime.datetime(2020, 1, 1, 0, 0, tzinfo=<UTC>)
+
+    >>> browser.cookies.forURL('http://example.net').change(
+    ...     'foo',
+    ...     expires=zope.testbrowser.cookies.expiration_string(
+    ...         datetime.datetime(2019, 1, 1)))
+    >>> browser.cookies.getinfo('foo')['expires']
+    datetime.datetime(2019, 1, 1, 0, 0, tzinfo=<UTC>)
+    >>> browser.cookies['foo']
+    'bar'
+    >>> browser.cookies.change('foo', expires=datetime.datetime(1999, 1, 1))
+    >>> len(browser.cookies)
+    0
+
+While we are at it, it is worth noting that trying to create a cookie that has
+already expired raises an error.
+
+    >>> browser.cookies.create('foo', 'bar',
+    ...                        expires=datetime.datetime(1999, 1, 1))
+    Traceback (most recent call last):
+    ...
+    AlreadyExpiredError: May not create a cookie that is immediately expired
+
+Clearing cookies
+----------------
+
+clear, clearAll, clearAllSession allow various clears of the cookies.
+
+The ``clear`` method clears all of the cookies for the current page.
+
+    >>> browser.open('https://dev.example.com/inner/path')
+    >>> pprint.pprint(list(browser.cookies.iterinfo()))
+    [{'comment': None,
+      'commenturl': None,
+      'domain': 'dev.example.com',
+      'expires': None,
+      'name': 'tweedle',
+      'path': '/inner/path',
+      'port': None,
+      'secure': False,
+      'value': 'dee'},
+     {'comment': None,
+      'commenturl': None,
+      'domain': '.example.com',
+      'expires': None,
+      'name': 'boo',
+      'path': '/inner',
+      'port': None,
+      'secure': True,
+      'value': 'hoo'}]
+    >>> browser.open('https://dev.example.com/inner')
+    >>> len(browser.cookies)
+    1
+    >>> browser.cookies.clear()
+    >>> len(browser.cookies)
+    0
+    >>> browser.open('https://dev.example.com/inner/path')
+    >>> len(browser.cookies)
+    1
+
+The ``clearAllSession`` method clears *all* session cookies (for all domains
+and paths, not just the current URL), as if the browser had been restarted.
+
+    >>> browser.open('http://localhost/inner/path')
+    >>> len(browser.cookies)
+    8
+    >>> len([info for info in browser.cookies.iterinfo()
+    ...      if info['expires'] is not None])
+    3
+    >>> browser.open('https://dev.example.org/inner/path')
+    >>> len(browser.cookies)
+    2
+    >>> len([info for info in browser.cookies.iterinfo()
+    ...      if info['expires'] is not None])
+    0
+    >>> browser.cookies.clearAllSession()
+    >>> len(browser.cookies)
+    0
+    >>> browser.open('http://localhost/inner/path')
+    >>> len(browser.cookies)
+    3
+
+The ``clearAll`` removes all cookies for the browser.
+
+    >>> browser.open('http://example.org/')
+    >>> browser.cookies.clearAll()
+    >>> browser.open('http://localhost/inner/path')
+    >>> len(browser.cookies)
+    0
+
+Note that explicitly setting a Cookie header is an error if the ``cookies``
+mapping has any values; and adding a new cookie to the ``cookies`` mapping
+is an error if the Cookie header is already set.  This is to prevent hard-to-
+diagnose intermittent errors when one header or the other wins.
+
+    >>> browser.cookies['boo'] = 'yah'
+    >>> browser.addHeader('Cookie', 'gee=gaw')
+    Traceback (most recent call last):
+    ...
+    ValueError: cookies are already set in `cookies` attribute
+
+    >>> browser.cookies.clearAll()
+    >>> browser.addHeader('Cookie', 'gee=gaw')
+    >>> browser.cookies['fee'] = 'fi'
+    Traceback (most recent call last):
+    ...
+    ValueError: cookies are already set in `Cookie` header


Property changes on: zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/cookies.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Modified: zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/interfaces.py
===================================================================
--- zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/interfaces.py	2008-11-05 16:38:52 UTC (rev 92801)
+++ zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/interfaces.py	2008-11-05 21:48:13 UTC (rev 92802)
@@ -18,11 +18,95 @@
 __docformat__ = "reStructuredText"
 
 from zope import interface, schema
+import zope.interface.common.mapping
 
 
+class AlreadyExpiredError(ValueError):
+    pass
+
+
+class ICookies(zope.interface.common.mapping.IExtendedReadMapping,
+               zope.interface.common.mapping.IExtendedWriteMapping,
+               zope.interface.common.mapping.IMapping): # NOT copy
+    """A mapping of cookies for a given url"""
+
+    url = schema.URI(
+        title=u"URL",
+        description=u"The URL the mapping is currently exposing.",
+        required=True)
+
+    header = schema.TextLine(
+        title=u"Header",
+        description=u"The current value for the Cookie header for the URL",
+        required=True)
+
+    def forURL(url):
+        """Returns another ICookies instance for the given URL."""
+
+    def getinfo(name):
+       """returns dict of settings for the given cookie name.
+
+       This includes only the following cookie values: 
+
+       - name (str)
+       - value (str),
+       - port (int or None),
+       - domain (str),
+       - path (str or None),
+       - secure (bool), and
+       - expires (datetime.datetime with pytz.UTC timezone or None),
+       - comment (str or None),
+       - commenturl (str or None).
+       """
+
+    def iterinfo(name=None):
+        """iterate over the information about all the cookies for the URL.
+        
+        Each result is a dictionary as described for ``getinfo``.
+        
+        If name is given, iterates over all cookies for given name.
+        """
+
+    def create(name, value,
+               domain=None, expires=None, path=None, secure=None, comment=None,
+               commenturl=None, port=None):
+        """Create a new cookie with the given values.
+        
+        If cookie of the same name, domain, and path exists, raises a
+        ValueError.
+        
+        Expires is a string or a datetime.datetime.  timezone-naive datetimes
+        are interpreted as in UTC.  If expires is before now, raises
+        AlreadyExpiredError.
+        
+        If the domain or path do not generally match the current URL, raises
+        ValueError.
+        """
+
+    def change(name, value=None,
+            domain=None, expires=None, path=None, secure=None, comment=None,
+            commenturl=None, port=None):
+        """Change an attribute of an existing cookie.
+        
+        If cookie does not exist, raises a KeyError."""
+
+    def clearAll():
+        """Clear all cookies for the associated browser, irrespective of URL
+        """
+
+    def clearAllSession():
+        """Clear session cookies for associated browser, irrespective of URL
+        """
+
+
 class IBrowser(interface.Interface):
     """A Programmatic Web Browser."""
 
+    cookies = schema.Field(
+        title=u"Cookies",
+        description=(u"An ICookies mapping for the browser's current URL."),
+        required=True)
+
     url = schema.URI(
         title=u"URL",
         description=u"The URL the browser is currently showing.",

Modified: zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/tests.py
===================================================================
--- zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/tests.py	2008-11-05 16:38:52 UTC (rev 92801)
+++ zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/tests.py	2008-11-05 21:48:13 UTC (rev 92802)
@@ -396,6 +396,10 @@
         checker=checker)
     readme.layer = TestBrowserLayer
 
+    cookies = FunctionalDocFileSuite('cookies.txt', optionflags=flags,
+        checker=checker)
+    cookies.layer = TestBrowserLayer
+
     fixed_bugs = FunctionalDocFileSuite('fixed-bugs.txt', optionflags=flags)
     fixed_bugs.layer = TestBrowserLayer
 
@@ -405,7 +409,7 @@
 
     this_file = doctest.DocTestSuite(checker=checker)
 
-    return unittest.TestSuite((this_file, readme, fixed_bugs, wire))
+    return unittest.TestSuite((this_file, readme, fixed_bugs, wire, cookies))
 
 if __name__ == '__main__':
     unittest.main(defaultTest='test_suite')



More information about the Checkins mailing list