[Checkins] SVN: zope.testbrowser/trunk/ Add cookie mapping attribute to testbrowser. ``svn merge -r 92644:94306 svn+ssh://svn.zope.org/repos/main/zope.testbrowser/branches/gary-cookie`` plus conflict resolution
Gary Poster
gary at modernsongs.com
Wed Dec 24 09:47:49 EST 2008
Log message for revision 94307:
Add cookie mapping attribute to testbrowser. ``svn merge -r 92644:94306 svn+ssh://svn.zope.org/repos/main/zope.testbrowser/branches/gary-cookie`` plus conflict resolution
Changed:
U zope.testbrowser/trunk/CHANGES.txt
U zope.testbrowser/trunk/buildout.cfg
U zope.testbrowser/trunk/setup.py
U zope.testbrowser/trunk/src/zope/testbrowser/README.txt
U zope.testbrowser/trunk/src/zope/testbrowser/browser.py
A zope.testbrowser/trunk/src/zope/testbrowser/cookies.py
A zope.testbrowser/trunk/src/zope/testbrowser/cookies.txt
U zope.testbrowser/trunk/src/zope/testbrowser/ftests/__init__.py
U zope.testbrowser/trunk/src/zope/testbrowser/ftests/ftesting.zcml
U zope.testbrowser/trunk/src/zope/testbrowser/interfaces.py
U zope.testbrowser/trunk/src/zope/testbrowser/testing.py
U zope.testbrowser/trunk/src/zope/testbrowser/tests.py
-=-
Modified: zope.testbrowser/trunk/CHANGES.txt
===================================================================
--- zope.testbrowser/trunk/CHANGES.txt 2008-12-24 14:09:40 UTC (rev 94306)
+++ zope.testbrowser/trunk/CHANGES.txt 2008-12-24 14:47:46 UTC (rev 94307)
@@ -2,15 +2,19 @@
CHANGES
=======
-3.5.2 (Unreleased)
-------------------
+Next release (Unreleased)
+-------------------------
- Author e-mail to zope-dev rather than zope3-dev.
- New lines are no longer stripped in XML and HTML code contained in a
textarea; fix requires ClientForm >= 0.2.10 (LP #268139).
+- Added ``cookies`` attribute to browser for easy manipulation of browser
+ cookies. See brief example in main documentation, plus new ``cookies.txt``
+ documentation.
+
3.5.1 (2008-10-10)
------------------
Modified: zope.testbrowser/trunk/buildout.cfg
===================================================================
--- zope.testbrowser/trunk/buildout.cfg 2008-12-24 14:09:40 UTC (rev 94306)
+++ zope.testbrowser/trunk/buildout.cfg 2008-12-24 14:47:46 UTC (rev 94307)
@@ -11,6 +11,8 @@
zope.testbrowser =
zope.publisher = 3.5.4
ClientForm = 0.2.10
+mechanize = 0.1.10
+setuptools = 0.6c9
[test]
recipe = zc.recipe.testrunner
Modified: zope.testbrowser/trunk/setup.py
===================================================================
--- zope.testbrowser/trunk/setup.py 2008-12-24 14:09:40 UTC (rev 94306)
+++ zope.testbrowser/trunk/setup.py 2008-12-24 14:47:46 UTC (rev 94307)
@@ -55,6 +55,7 @@
'setuptools',
'zope.interface',
'zope.schema',
+ 'pytz',
],
extras_require = {
'test': [
Modified: zope.testbrowser/trunk/src/zope/testbrowser/README.txt
===================================================================
--- zope.testbrowser/trunk/src/zope/testbrowser/README.txt 2008-12-24 14:09:40 UTC (rev 94306)
+++ zope.testbrowser/trunk/src/zope/testbrowser/README.txt 2008-12-24 14:47:46 UTC (rev 94307)
@@ -148,6 +148,59 @@
'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'].replace(';', '')
+ 'foo=bar'
+
+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*. Here are
+a few examples.
+
+ >>> 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'}
+ >>> browser.cookies['sha'] = 'zam'
+ >>> len(browser.cookies)
+ 2
+ >>> 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
+ >>> sorted(browser.cookies.items())
+ [('foo', 'bar'), ('sha', 'zam')]
+ >>> browser.cookies.clearAll()
+ >>> len(browser.cookies)
+ 0
+
+Many more examples, and a discussion of the additional methods available, can
+be found in cookies.txt.
+
+
Navigation and Link Objects
---------------------------
@@ -305,6 +358,7 @@
>>> browser.open('http://localhost/@@/testbrowser/controls.html')
+
Obtaining a Control
~~~~~~~~~~~~~~~~~~~
@@ -442,6 +496,7 @@
Characteristics of controls and subcontrols are discussed below.
+
Control Objects
~~~~~~~~~~~~~~~
@@ -550,6 +605,7 @@
argument, which is a tuple of (x, y). These submit the forms, and are
demonstrated below as we examine each control individually.
+
ItemControl Objects
~~~~~~~~~~~~~~~~~~~
@@ -581,6 +637,7 @@
Controls with subcontrols--
+
Various Controls
~~~~~~~~~~~~~~~~
@@ -914,6 +971,7 @@
>>> ctrl.multiple
False
+
Using Submitting Controls
~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1175,6 +1233,7 @@
Here, the body is left in place because it isn't form data.
+
Performance Testing
-------------------
@@ -1229,7 +1288,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 +1306,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')
@@ -1258,6 +1317,7 @@
>>> browser.raiseHttpErrors = True
+
Hand-Holding
------------
Modified: zope.testbrowser/trunk/src/zope/testbrowser/browser.py
===================================================================
--- zope.testbrowser/trunk/src/zope/testbrowser/browser.py 2008-12-24 14:09:40 UTC (rev 94306)
+++ zope.testbrowser/trunk/src/zope/testbrowser/browser.py 2008-12-24 14:47:46 UTC (rev 94307)
@@ -19,17 +19,22 @@
__docformat__ = "reStructuredText"
-import ClientForm
import cStringIO
-import mechanize
+import datetime
import operator
import re
import sys
import time
import urllib2
+
+import ClientForm
+import mechanize
import zope.interface
+
+import zope.testbrowser.cookies
import zope.testbrowser.interfaces
+
RegexType = type(re.compile(''))
_compress_re = re.compile(r"\s+")
compressText = lambda text: _compress_re.sub(' ', text.strip())
@@ -166,6 +171,7 @@
self.mech_browser = mech_browser
self.timer = PystoneTimer()
self.raiseHttpErrors = True
+ self.cookies = zope.testbrowser.cookies.Cookies(self.mech_browser)
self._enable_setattr_errors = True
if url is not None:
@@ -288,6 +294,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):
Copied: zope.testbrowser/trunk/src/zope/testbrowser/cookies.py (from rev 94306, zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/cookies.py)
===================================================================
--- zope.testbrowser/trunk/src/zope/testbrowser/cookies.py (rev 0)
+++ zope.testbrowser/trunk/src/zope/testbrowser/cookies.py 2008-12-24 14:47:46 UTC (rev 94307)
@@ -0,0 +1,379 @@
+##############################################################################
+#
+# Copyright (c) 2008 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+
+import Cookie
+import datetime
+import time
+import urllib
+import urlparse
+import UserDict
+
+import mechanize
+import pytz
+import zope.interface
+from zope.testbrowser import interfaces
+
+# 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(pytz.UTC)
+ expires = expires.strftime('%a, %d %b %Y %H:%M:%S GMT')
+ return expires
+
+if getattr(property, 'setter', None) is None:
+ # hack on Python 2.6 spelling of the only part we use here
+ class property(property):
+ __slots__ = ()
+ def setter(self, f):
+ return property(self.fget, f, self.fdel, self.__doc__)
+
+# end Cookies class helpers
+
+
+class Cookies(object, UserDict.DictMixin):
+ """Cookies for mechanize browser.
+ """
+
+ zope.interface.implements(interfaces.ICookies)
+
+ 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')
+
+ @property
+ def strict_domain_policy(self):
+ policy = self._jar.get_policy()
+ flags = (policy.DomainStrictNoDots | policy.DomainRFC2965Match |
+ policy.DomainStrictNonDomain)
+ return policy.strict_ns_domain & flags == flags
+
+ @strict_domain_policy.setter
+ def strict_domain_policy(self, value):
+ jar = self._jar
+ policy = jar.get_policy()
+ flags = (policy.DomainStrictNoDots | policy.DomainRFC2965Match |
+ policy.DomainStrictNonDomain)
+ policy.strict_ns_domain |= flags
+ if not value:
+ policy.strict_ns_domain ^= flags
+
+ 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
+
+ @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):
+ return self._jar.cookies_for_request(self._request)
+
+ 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, pytz.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 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)
+ now = int(time.time())
+ if expires is not None and self._is_expired(expires, now):
+ 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, now=now)
+
+ def change(self, name, value=None,
+ domain=None, expires=None, path=None, secure=None, comment=None,
+ commenturl=None, port=None):
+ now = int(time.time())
+ if expires is not None and self._is_expired(expires, now):
+ # shortcut
+ del self[name]
+ else:
+ self._change(self._get(name), value, domain, expires, path, secure,
+ comment, commenturl, port, now)
+
+ def _change(self, ck, value=None,
+ domain=None, expires=None, path=None, secure=None,
+ comment=None, commenturl=None, port=None, now=None):
+ if value is None:
+ value = ck.value
+ if domain is None:
+ domain = ck.domain
+ else:
+ self._verifyDomain(domain, None)
+ if expires is None:
+ expires = ck.expires
+ if path is None:
+ path = ck.path
+ else:
+ self._verifyPath(domain, None)
+ 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, now=now)
+
+ def _verifyDomain(self, domain, ck):
+ tmp_domain = domain
+ if domain is not None and domain.startswith('.'):
+ tmp_domain = domain[1:]
+ self_host = mechanize.effective_request_host(self._request)
+ if (self_host != tmp_domain and
+ not self_host.endswith('.' + tmp_domain)):
+ raise ValueError('current url must match given domain')
+ if (ck is not None and ck.domain != tmp_domain and
+ ck.domain.endswith(tmp_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, now=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 and not domain.startswith('.'):
+ # 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').
+ 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)
+ 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)
+ assert len(cookies) == 1, (
+ 'programmer error: %d cookies made' % (len(cookies),))
+ policy = self._jar._policy
+ if now is None:
+ now = int(time.time())
+ policy._now = self._jar._now = now # TODO get mechanize to expose this
+ if not policy.set_ok(cookies[0], request):
+ raise ValueError('policy does not allow this cookie')
+ if ck is not None:
+ self._jar.clear(ck.domain, ck.path, ck.name)
+ self._jar.set_cookie(cookies[0])
+
+ def __setitem__(self, 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, now): # now = int(time.time())
+ dnow = datetime.datetime.fromtimestamp(now, pytz.UTC)
+ if isinstance(value, datetime.datetime):
+ if value.tzinfo is None:
+ if value <= dnow.replace(tzinfo=None):
+ return True
+ elif value <= dnow:
+ return True
+ elif isinstance(value, basestring):
+ if datetime.datetime.fromtimestamp(
+ mechanize.str2time(value),
+ pytz.UTC) <= dnow:
+ return True
+ return False
+
+ def clear(self):
+ # to give expected mapping behavior of resulting in an empty dict,
+ # we use _raw_cookies rather than _get_cookies.
+ for ck in self._raw_cookies():
+ self._jar.clear(ck.domain, ck.path, ck.name)
+
+ def clearAllSession(self):
+ self._jar.clear_session_cookies()
+
+ def clearAll(self):
+ self._jar.clear()
Copied: zope.testbrowser/trunk/src/zope/testbrowser/cookies.txt (from rev 94306, zope.testbrowser/branches/gary-cookie/src/zope/testbrowser/cookies.txt)
===================================================================
--- zope.testbrowser/trunk/src/zope/testbrowser/cookies.txt (rev 0)
+++ zope.testbrowser/trunk/src/zope/testbrowser/cookies.txt 2008-12-24 14:47:46 UTC (rev 94307)
@@ -0,0 +1,868 @@
+=======
+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'].replace(';', '')
+ '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 pytz 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.
+
+ >>> browser.cookies.strict_domain_policy
+ False
+
+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.
+
+Here's an example. If we go to dev.example.org, we should only see the
+"tweedle" cookie if we are using strict rules. But right now we are using
+loose rules, so 'boo' is around too.
+
+ >>> browser.open('https://dev.example.org/get_cookie.html')
+ >>> sorted(browser.cookies)
+ ['boo', 'tweedle']
+ >>> print browser.contents
+ boo: yah
+ tweedle: dee
+
+If we set strict_domain_policy to True, then only tweedle is included.
+
+ >>> browser.cookies.strict_domain_policy = True
+ >>> sorted(browser.cookies)
+ ['tweedle']
+ >>> browser.open('https://dev.example.org/get_cookie.html')
+ >>> print browser.contents
+ tweedle: dee
+
+If we set the "boo" domain to ".example.org" (as it would be set under the more
+recent Cookie RFC if a server sent the value) then maybe we get the "boo" value
+again.
+
+ >>> browser.cookies.forURL('https://example.org').change(
+ ... 'boo', domain=".example.org")
+ Traceback (most recent call last):
+ ...
+ ValueError: policy does not allow this cookie
+
+Whoa! Why couldn't we do that?
+
+Well, the strict_domain_policy affects what cookies we can set also. With
+strict rules, ".example.org" can only be set by "*.example.org" domains, *not*
+example.org itself.
+
+OK, we'll create a new cookie then.
+
+ >>> browser.cookies.forURL('https://snoo.example.org').create(
+ ... 'snoo', 'kums', domain=".example.org")
+
+ >>> sorted(browser.cookies)
+ ['snoo', 'tweedle']
+ >>> browser.open('https://dev.example.org/get_cookie.html')
+ >>> print browser.contents
+ snoo: kums
+ tweedle: dee
+
+Let's set things back to the way they were.
+
+ >>> del browser.cookies['snoo']
+ >>> browser.cookies.strict_domain_policy = False
+ >>> browser.open('https://dev.example.org/get_cookie.html')
+ >>> sorted(browser.cookies)
+ ['boo', 'tweedle']
+ >>> print browser.contents
+ boo: yah
+ tweedle: dee
+
+Now back to the the examples of masking by domain. First, unless you pass an
+explicit domain, you will be modifying the existing cookie.
+
+ >>> 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
Modified: zope.testbrowser/trunk/src/zope/testbrowser/ftests/__init__.py
===================================================================
--- zope.testbrowser/trunk/src/zope/testbrowser/ftests/__init__.py 2008-12-24 14:09:40 UTC (rev 94306)
+++ zope.testbrowser/trunk/src/zope/testbrowser/ftests/__init__.py 2008-12-24 14:47:46 UTC (rev 94307)
@@ -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/trunk/src/zope/testbrowser/ftests/ftesting.zcml
===================================================================
--- zope.testbrowser/trunk/src/zope/testbrowser/ftests/ftesting.zcml 2008-12-24 14:09:40 UTC (rev 94306)
+++ zope.testbrowser/trunk/src/zope/testbrowser/ftests/ftesting.zcml 2008-12-24 14:47:46 UTC (rev 94307)
@@ -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/trunk/src/zope/testbrowser/interfaces.py
===================================================================
--- zope.testbrowser/trunk/src/zope/testbrowser/interfaces.py 2008-12-24 14:09:40 UTC (rev 94306)
+++ zope.testbrowser/trunk/src/zope/testbrowser/interfaces.py 2008-12-24 14:47:46 UTC (rev 94307)
@@ -19,11 +19,95 @@
import zope.interface
import zope.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 = zope.schema.URI(
+ title=u"URL",
+ description=u"The URL the mapping is currently exposing.",
+ required=True)
+
+ header = zope.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(zope.interface.Interface):
"""A Programmatic Web Browser."""
+ cookies = zope.schema.Field(
+ title=u"Cookies",
+ description=(u"An ICookies mapping for the browser's current URL."),
+ required=True)
+
url = zope.schema.URI(
title=u"URL",
description=u"The URL the browser is currently showing.",
Modified: zope.testbrowser/trunk/src/zope/testbrowser/testing.py
===================================================================
--- zope.testbrowser/trunk/src/zope/testbrowser/testing.py 2008-12-24 14:09:40 UTC (rev 94306)
+++ zope.testbrowser/trunk/src/zope/testbrowser/testing.py 2008-12-24 14:47:46 UTC (rev 94307)
@@ -131,6 +131,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.
@@ -140,7 +142,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."""
Modified: zope.testbrowser/trunk/src/zope/testbrowser/tests.py
===================================================================
--- zope.testbrowser/trunk/src/zope/testbrowser/tests.py 2008-12-24 14:09:40 UTC (rev 94306)
+++ zope.testbrowser/trunk/src/zope/testbrowser/tests.py 2008-12-24 14:47:46 UTC (rev 94307)
@@ -383,6 +383,7 @@
(re.compile(r'User-Agent: Python-urllib/2.6'), 'User-agent: Python-urllib/2.4'),
(re.compile(r'Host: localhost'), 'Connection: close'),
(re.compile(r'Content-Type: '), 'Content-type: '),
+ (re.compile(r'Content-Disposition: '), 'Content-disposition: '),
])
TestBrowserLayer = zope.app.testing.functional.ZCMLLayer(
@@ -396,6 +397,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 +410,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