[Checkins] SVN: zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/ snapshot of cookies API (ATM largely for discussion with John J. Lee)

Gary Poster gary at modernsongs.com
Tue Oct 28 06:23:45 EDT 2008


Log message for revision 92653:
  snapshot of cookies API (ATM largely for discussion with John J. Lee)

Changed:
  U   zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/README.txt
  U   zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/browser.py
  A   zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/cookies.py
  U   zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/ftests/__init__.py
  U   zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/ftests/ftesting.zcml
  U   zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/testing.py

-=-
Modified: zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/README.txt
===================================================================
--- zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/README.txt	2008-10-28 08:20:44 UTC (rev 92652)
+++ zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/README.txt	2008-10-28 10:23:44 UTC (rev 92653)
@@ -148,6 +148,502 @@
     'text/html;charset=utf-8'
 
 
+Cookies
+-------
+
+When a Set-Cookie header is available, it can be found in the headers, as seen
+above.  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;'
+
+You can also examine cookies using the browser's ``cookies`` attribute.  It has
+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.
+
+    >>> 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'}
+
+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
+    >>> 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
+
+You can see the header 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'
+
+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
+
+#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
 ---------------------------
 
@@ -1229,7 +1725,7 @@
 
 When the testbrowser is raising HttpErrors, the errors still hit the test.
 Sometimes we don't want that to happen, in situations where there are edge
-cases that will cause the error to be predictabley but infrequently raised.
+cases that will cause the error to be predictably but infrequently raised.
 Time is a primary cause of this.
 
 To get around this, one can set the raiseHttpErrors to False.
@@ -1247,7 +1743,7 @@
     True
 
 If we don't handle the errors, and allow internal ones to propagate, however,
-this flage doesn't affect things.
+this flag doesn't affect things.
 
     >>> browser.handleErrors = False
     >>> browser.open('http://localhost/invalid')

Modified: zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/browser.py
===================================================================
--- zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/browser.py	2008-10-28 08:20:44 UTC (rev 92652)
+++ zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/browser.py	2008-10-28 10:23:44 UTC (rev 92653)
@@ -16,18 +16,22 @@
 $Id$
 """
 __docformat__ = "reStructuredText"
-from zope.testbrowser import interfaces
-import ClientForm
-from cStringIO import StringIO
-import mechanize
+import datetime
 import operator
 import re
+from cStringIO import StringIO
 import sys
 import time
 import urllib2
 
+import mechanize
+import ClientForm
+
 from zope import interface
 
+from zope.testbrowser import interfaces, cookies
+
+
 RegexType = type(re.compile(''))
 _compress_re = re.compile(r"\s+")
 compressText = lambda text: _compress_re.sub(' ', text.strip())
@@ -164,6 +168,7 @@
         self.mech_browser = mech_browser
         self.timer = PystoneTimer()
         self.raiseHttpErrors = True
+        self.cookies = cookies.Cookies(self.mech_browser)
         self._enable_setattr_errors = True
 
         if url is not None:

Added: zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/cookies.py
===================================================================
--- zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/cookies.py	                        (rev 0)
+++ zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/cookies.py	2008-10-28 10:23:44 UTC (rev 92653)
@@ -0,0 +1,334 @@
+
+import Cookie
+import cookielib
+import datetime
+import urllib
+import urlparse
+import UserDict
+
+import mechanize
+try:
+    from pytz import UTC
+except ImportError:
+
+    ZERO = datetime.timedelta(0)
+    HOUR = datetime.timedelta(hours=1)
+
+
+    class UTC(datetime.tzinfo):
+        """UTC
+
+        The reference UTC implementation given in Python docs.
+        """
+        zone = "UTC"
+
+        def utcoffset(self, dt):
+            return ZERO
+
+        def tzname(self, dt):
+            return "UTC"
+
+        def dst(self, dt):
+            return ZERO
+
+        def localize(self, dt, is_dst=False):
+            '''Convert naive time to local time'''
+            if dt.tzinfo is not None:
+                raise ValueError, 'Not naive datetime (tzinfo is already set)'
+            return dt.replace(tzinfo=self)
+
+        def normalize(self, dt, is_dst=False):
+            '''Correct the timezone information on the given datetime'''
+            if dt.tzinfo is None:
+                raise ValueError, 'Naive time - no tzinfo set'
+            return dt.replace(tzinfo=self)
+
+        def __repr__(self):
+            return "<UTC>"
+
+        def __str__(self):
+            return "UTC"
+
+# Cookies class helpers
+
+
+class _StubHTTPMessage(object):
+    def __init__(self, cookies):
+        self._cookies = cookies
+
+    def getheaders(self, name):
+        if name.lower() != 'set-cookie':
+            return []
+        else:
+            return self._cookies
+
+
+class _StubResponse(object):
+    def __init__(self, cookies):
+        self.message = _StubHTTPMessage(cookies)
+
+    def info(self):
+        return self.message
+
+def expiration_string(expires): # this is not protected so usable in tests.
+    if isinstance(expires, datetime.datetime):
+        if expires.tzinfo is not None:
+            expires = expires.astimezone(UTC)
+        expires = expires.strftime('%a, %d %b %Y %H:%M:%S GMT')
+    return expires
+
+# end Cookies class helpers
+
+
+class Cookies(UserDict.DictMixin):
+    """Cookies for mechanize browser.
+    """
+
+    def __init__(self, mech_browser, url=None):
+        self.mech_browser = mech_browser
+        self._url = url
+        for handler in self.mech_browser.handlers:
+            if getattr(handler, 'cookiejar', None) is not None:
+                self._jar = handler.cookiejar
+                break
+        else:
+            raise RuntimeError('no cookiejar found')
+
+    def forURL(self, url):
+        return self.__class__(self.mech_browser, url)
+
+    @property
+    def url(self):
+        if self._url is not None:
+            return self._url
+        else:
+            return self.mech_browser.geturl()
+
+    @property
+    def _request(self):
+        if self._url is not None:
+            return self.mech_browser.request_class(self._url)
+        else:
+            request = self.mech_browser.request
+            if request is None:
+                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)
+        self._jar.add_cookie_header(request)
+        return request.get_header('Cookie')
+
+    def __str__(self):
+        return self.header
+
+    def __repr__(self):
+        # get the cookies for the current url
+        return '<%s.%s object at %r for %s (%s)>' % (
+            self.__class__.__module__, self.__class__.__name__,
+            id(self), self.url, self.header)
+
+    def _raw_cookies(self):
+        # uses protected method of clientcookie, after agonizingly trying not
+        # to. XXX
+        res = self._jar._cookies_for_request(self._request)
+        # _cookies_for_request does not sort by path, as specified by RFC2109
+        # (page 9, section 4.3.4) and RFC2965 (page 12, section 3.3.4).
+        # We sort by path match, and then, just for something stable, we sort
+        # by domain match and by whether the cookie specifies a port.
+        # This maybe should be fixed in clientcookie.
+        res.sort(key = lambda ck:
+            ((ck.path is not None and -(len(ck.path)) or 0),
+             (ck.domain is not None and -(len(ck.domain)) or 0),
+             ck.port is None))
+        return res
+
+    def _get_cookies(self, key=None):
+        if key is None:
+            seen = set()
+            for ck in self._raw_cookies():
+                if ck.name not in seen:
+                    yield ck
+                    seen.add(ck.name)
+        else:
+            for ck in self._raw_cookies():
+                if ck.name == key:
+                    yield ck
+
+    _marker = object()
+
+    def _get(self, key, default=_marker):
+        for ck in self._raw_cookies():
+            if ck.name == key:
+                return ck
+        if default is self._marker:
+            raise KeyError(key)
+        return default
+
+    def __getitem__(self, key):
+        return self._get(key).value
+
+    def getinfo(self, key):
+        return self._getinfo(self._get(key))
+
+    def _getinfo(self, ck):
+        res = {'name': ck.name,
+               'value': ck.value,
+               'port': ck.port,
+               'domain': ck.domain,
+               'path': ck.path,
+               'secure': ck.secure,
+               'expires': None,
+               'comment': ck.comment,
+               'commenturl': ck.comment_url}
+        if ck.expires is not None:
+            res['expires'] = datetime.datetime.fromtimestamp(
+                ck.expires, UTC)
+        return res
+
+    def keys(self):
+        return [ck.name for ck in self._get_cookies()]
+
+    def __iter__(self):
+        return (ck.name for ck in self._get_cookies())
+
+    iterkeys = __iter__
+
+    def iterinfo(self, key=None):
+        return (self._getinfo(ck) for ck in self._get_cookies(key))
+
+    def iteritems(self):
+        return ((ck.name, ck.value) for ck in self._get_cookies())
+
+    def has_key(self, key):
+        return self._get(key, None) is not None
+
+    __contains__ = has_key
+
+    def __len__(self):
+        return len(list(self._get_cookies()))
+
+    def __delitem__(self, key):
+        ck = self._get(key)
+        self._jar.clear(ck.domain, ck.path, ck.name)
+
+    def set(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:
+            path = ck.path
+        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.
+        c = Cookie.SimpleCookie()
+        name = str(name)
+        c[name] = value.encode('utf8')
+        if secure:
+            c[name]['secure'] = True
+        if domain:
+            c[name]['domain'] = domain
+        if path:
+            c[name]['path'] = path
+        if expires:
+            c[name]['expires'] = expiration_string(expires)
+        if comment:
+            c[name]['comment'] = urllib.quote(
+                comment.encode('utf-8'), safe="/?:@&+")
+        if port:
+            c[name]['port'] = port
+        if commenturl:
+            c[name]['commenturl'] = commenturl
+        if version:
+            c[name]['version'] = version
+        # this use of objects like _StubResponse and _StubHTTPMessage is in
+        # fact supported by the documented client cookie API.
+        cookies = self._jar.make_cookies(
+            _StubResponse([c.output(header='').strip()]), request)
+        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)
+
+    def expire(self, name, expires=None):
+        if expires is None:
+            del self[name]
+        else:
+            ck = self._get(name)
+            self.set(ck.name, ck.value, expires, ck.domain, ck.path, ck.secure,
+                     ck.comment)
+
+    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():
+            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
+        self._jar.clear_session_cookies()
+
+    def clearAll(self): # XXX could add optional "domain" filter or similar
+        self._jar.clear()

Modified: zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/ftests/__init__.py
===================================================================
--- zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/ftests/__init__.py	2008-10-28 08:20:44 UTC (rev 92652)
+++ zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/ftests/__init__.py	2008-10-28 10:23:44 UTC (rev 92653)
@@ -12,12 +12,30 @@
 #
 ##############################################################################
 
-class Echo:
-    """Simply echo the contents of the request"""
+class View:
 
     def __init__(self, context, request):
+        self.context = context
         self.request = request
 
+class Echo(View):
+    """Simply echo the contents of the request"""
+
     def __call__(self):
         return ('\n'.join('%s: %s' % x for x in self.request.items()) +
             '\nBody: %r' % self.request.bodyStream.read())
+
+class GetCookie(View):
+    """Gets cookie value"""
+
+    def __call__(self):
+        return '\n'.join(
+            ('%s: %s' % (k, v)) for k, v in sorted(
+                self.request.cookies.items()))
+
+class SetCookie(View):
+    """Gets cookie value"""
+
+    def __call__(self):
+        self.request.response.setCookie(
+            **dict((str(k), str(v)) for k, v in self.request.form.items()))

Modified: zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/ftests/ftesting.zcml
===================================================================
--- zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/ftests/ftesting.zcml	2008-10-28 08:20:44 UTC (rev 92652)
+++ zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/ftests/ftesting.zcml	2008-10-28 10:23:44 UTC (rev 92653)
@@ -37,6 +37,20 @@
      permission="zope.Public"
      />
 
+  <browser:page
+     name="set_cookie.html"
+     for="*"
+     class=".ftests.SetCookie"
+     permission="zope.Public"
+     />
+
+  <browser:page
+     name="get_cookie.html"
+     for="*"
+     class=".ftests.GetCookie"
+     permission="zope.Public"
+     />
+
   <browser:resourceDirectory
       name="testbrowser"
       directory="ftests" />

Modified: zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/testing.py
===================================================================
--- zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/testing.py	2008-10-28 08:20:44 UTC (rev 92652)
+++ zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/testing.py	2008-10-28 10:23:44 UTC (rev 92653)
@@ -134,6 +134,8 @@
                                             data['content-type'])
         return urllib2.AbstractHTTPHandler.do_request_(self, req)
 
+    https_request = http_request
+
     def http_open(self, req):
         """Open an HTTP connection having a ``urllib2`` request."""
         # Here we connect to the publisher.
@@ -143,7 +145,9 @@
             req.timeout = socket._GLOBAL_DEFAULT_TIMEOUT
         return self.do_open(PublisherConnection, req)
 
+    https_open = http_open
 
+
 class PublisherMechanizeBrowser(mechanize.Browser):
     """Special ``mechanize`` browser using the Zope Publisher HTTP handler."""
 



More information about the Checkins mailing list