Chris McDonough
2013-12-12 28c26c17d66b563140e60b95ca1ea7e6dc3ccb6c
Merge branch 'master' of github.com:Pylons/pyramid
11 files modified
433 ■■■■ changed files
CHANGES.txt 25 ●●●● patch | view | raw | blame | history
docs/narr/i18n.rst 6 ●●●●● patch | view | raw | blame | history
docs/whatsnew-1.5.rst 7 ●●●● patch | view | raw | blame | history
pyramid/authentication.py 86 ●●●●● patch | view | raw | blame | history
pyramid/i18n.py 33 ●●●●● patch | view | raw | blame | history
pyramid/session.py 113 ●●●● patch | view | raw | blame | history
pyramid/testing.py 1 ●●●● patch | view | raw | blame | history
pyramid/tests/test_authentication.py 129 ●●●●● patch | view | raw | blame | history
pyramid/tests/test_session.py 27 ●●●●● patch | view | raw | blame | history
pyramid/url.py 2 ●●● patch | view | raw | blame | history
setup.py 4 ●●●● patch | view | raw | blame | history
CHANGES.txt
@@ -1,5 +1,5 @@
Unreleased
==========
1.5a3 (2013-12-10)
==================
Features
--------
@@ -35,11 +35,13 @@
  See https://github.com/Pylons/pyramid/pull/1149
- Added a new ``SignedCookieSessionFactory`` which is very similar to the
  ``UnencryptedCookieSessionFactoryConfig`` but with a clearer focus on
  signing content. The custom serializer arguments to this function should
  only focus on serializing, unlike its predecessor which required the
  serializer to also perform signing.
  See https://github.com/Pylons/pyramid/pull/1142
  ``UnencryptedCookieSessionFactoryConfig`` but with a clearer focus on signing
  content. The custom serializer arguments to this function should only focus
  on serializing, unlike its predecessor which required the serializer to also
  perform signing.  See https://github.com/Pylons/pyramid/pull/1142 .  Note
  that cookies generated using ``SignedCookieSessionFactory`` are not
  compatible with cookies generated using ``UnencryptedCookieSessionFactory``,
  so existing user session data will be destroyed if you switch to it.
- Added a new ``BaseCookieSessionFactory`` which acts as a generic cookie
  factory that can be used by framework implementors to create their own
@@ -65,6 +67,9 @@
  does not need to be in ``k=v`` form.  This is useful if you want to be able
  to use a different query string format than ``x-www-form-urlencoded``.  See
  https://github.com/Pylons/pyramid/pull/1183
- ``pyramid.testing.DummyRequest`` now has a ``domain`` attribute to match the
  new WebOb 1.3 API.  Its value is ``example.com``.
Bug Fixes
---------
@@ -149,6 +154,12 @@
  Instead, use the newly-added ``unauthenticated_userid`` attribute of the
  request object.
Dependencies
------------
- Pyramid now depends on WebOb>=1.3 (it uses ``webob.cookies.CookieProfile``
  from 1.3+).
1.5a2 (2013-09-22)
==================
docs/narr/i18n.rst
@@ -607,10 +607,8 @@
   def aview(request):
       localizer = request.localizer
       num = 1
       translated = localizer.pluralize(
                          _('item_plural', default="${number} items"),
                          None, num, 'mydomain', mapping={'number':num}
                          )
       translated = localizer.pluralize('item_plural', '${number} items',
           num, 'mydomain', mapping={'number':num})
The corresponding message catalog must have language plural definitions and
plural alternatives set.
docs/whatsnew-1.5.rst
@@ -348,7 +348,10 @@
  signing content. The custom serializer arguments to this function should
  only focus on serializing, unlike its predecessor which required the
  serializer to also perform signing.
  See https://github.com/Pylons/pyramid/pull/1142
  See https://github.com/Pylons/pyramid/pull/1142 . Note
  that cookies generated using ``SignedCookieSessionFactory`` are not
  compatible with cookies generated using ``UnencryptedCookieSessionFactory``,
  so existing user session data will be destroyed if you switch to it.
- Added a new ``BaseCookieSessionFactory`` which acts as a generic cookie
  factory that can be used by framework implementors to create their own
@@ -504,3 +507,5 @@
- Pyramid no longer depends upon ``Mako`` or ``Chameleon``.
- Pyramid now depends on WebOb>=1.3 (it uses ``webob.cookies.CookieProfile``
  from 1.3+).
pyramid/authentication.py
@@ -10,6 +10,8 @@
from zope.interface import implementer
from webob.cookies import CookieProfile
from pyramid.compat import (
    long,
    text_type,
@@ -18,6 +20,7 @@
    url_quote,
    bytes_,
    ascii_native_,
    native_,
    )
from pyramid.interfaces import (
@@ -798,8 +801,6 @@
    ts_chars = ''.join(map(chr, ts))
    return bytes_(ip_chars + ts_chars)
EXPIRE = object()
class AuthTktCookieHelper(object):
    """
    A helper class for use in third-party authentication policy
@@ -830,55 +831,32 @@
                 include_ip=False, timeout=None, reissue_time=None,
                 max_age=None, http_only=False, path="/", wild_domain=True,
                 hashalg='md5', parent_domain=False, domain=None):
        serializer = _SimpleSerializer()
        self.cookie_profile = CookieProfile(
            cookie_name = cookie_name,
            secure = secure,
            max_age = max_age,
            httponly = http_only,
            path = path,
            serializer=serializer
            )
        self.secret = secret
        self.cookie_name = cookie_name
        self.include_ip = include_ip
        self.secure = secure
        self.include_ip = include_ip
        self.timeout = timeout
        self.reissue_time = reissue_time
        self.max_age = max_age
        self.http_only = http_only
        self.path = path
        self.wild_domain = wild_domain
        self.parent_domain = parent_domain
        self.domain = domain
        self.hashalg = hashalg
        static_flags = []
        if self.secure:
            static_flags.append('; Secure')
        if self.http_only:
            static_flags.append('; HttpOnly')
        self.static_flags = "".join(static_flags)
    def _get_cookies(self, environ, value, max_age=None):
        if max_age is EXPIRE:
            max_age = "; Max-Age=0; Expires=Wed, 31-Dec-97 23:59:59 GMT"
        elif max_age is not None:
            later = datetime.datetime.utcnow() + datetime.timedelta(
                seconds=int(max_age))
            # Wdy, DD-Mon-YY HH:MM:SS GMT
            expires = later.strftime('%a, %d %b %Y %H:%M:%S GMT')
            # the Expires header is *required* at least for IE7 (IE7 does
            # not respect Max-Age)
            max_age = "; Max-Age=%s; Expires=%s" % (max_age, expires)
        else:
            max_age = ''
        cur_domain = environ.get('HTTP_HOST', environ.get('SERVER_NAME'))
        # While Chrome, IE, and Firefox can cope, Opera (at least) cannot
        # cope with a port number in the cookie domain when the URL it
        # receives the cookie from does not also have that port number in it
        # (e.g via a proxy).  In the meantime, HTTP_HOST is sent with port
        # number, and neither Firefox nor Chrome do anything with the
        # information when it's provided in a cookie domain except strip it
        # out.  So we strip out any port number from the cookie domain
        # aggressively to avoid problems.  See also
        # https://github.com/Pylons/pyramid/issues/131
        if ':' in cur_domain:
            cur_domain = cur_domain.split(':', 1)[0]
    def _get_cookies(self, request, value, max_age=None):
        cur_domain = request.domain
        domains = []
        if self.domain:
@@ -892,14 +870,15 @@
                if self.wild_domain:
                    domains.append('.' + cur_domain)
        cookies = []
        base_cookie = '%s="%s"; Path=%s%s%s' % (self.cookie_name, value,
                self.path, max_age, self.static_flags)
        for domain in domains:
            domain = '; Domain=%s' % domain if domain is not None else ''
            cookies.append(('Set-Cookie', '%s%s' % (base_cookie, domain)))
        profile = self.cookie_profile(request)
        return cookies
        kw = {}
        kw['domains'] = domains
        if max_age is not None:
            kw['max_age'] = max_age
        headers = profile.get_headers(value, **kw)
        return headers
    def identify(self, request):
        """ Return a dictionary with authentication information, or ``None``
@@ -968,9 +947,8 @@
    def forget(self, request):
        """ Return a set of expires Set-Cookie headers, which will destroy
        any existing auth_tkt cookie when attached to a response"""
        environ = request.environ
        request._authtkt_reissue_revoked = True
        return self._get_cookies(environ, '', max_age=EXPIRE)
        return self._get_cookies(request, None)
    def remember(self, request, userid, max_age=None, tokens=()):
        """ Return a set of Set-Cookie headers; when set into a response,
@@ -1037,7 +1015,7 @@
            )
        cookie_value = ticket.cookie_value()
        return self._get_cookies(environ, cookie_value, max_age)
        return self._get_cookies(request, cookie_value, max_age)
@implementer(IAuthenticationPolicy)
class SessionAuthenticationPolicy(CallbackAuthenticationPolicy):
@@ -1196,3 +1174,11 @@
        except ValueError: # not enough values to unpack
            return None
        return username, password
class _SimpleSerializer(object):
    def loads(self, bstruct):
        return native_(bstruct)
    def dumps(self, appstruct):
        return bytes_(appstruct)
pyramid/i18n.py
@@ -75,16 +75,16 @@
        :term:`message identifier` objects as a singular/plural pair
        and an ``n`` value representing the number that appears in the
        message using gettext plural forms support.  The ``singular``
        and ``plural`` objects passed may be translation strings or
        unicode strings.  ``n`` represents the number of elements.
        ``domain`` is the translation domain to use to do the
        pluralization, and ``mapping`` is the interpolation mapping
        that should be used on the result.  Note that if the objects
        passed are translation strings, their domains and mappings are
        ignored.  The domain and mapping arguments must be used
        instead.  If the ``domain`` is not supplied, a default domain
        is used (usually ``messages``).
        and ``plural`` objects should be unicode strings. There is no
        reason to use translation string objects as arguments as all
        metadata is ignored.
        ``n`` represents the number of elements. ``domain`` is the
        translation domain to use to do the pluralization, and ``mapping``
        is the interpolation mapping that should be used on the result. If
        the ``domain`` is not supplied, a default domain is used (usually
        ``messages``).
        Example::
           num = 1
@@ -93,6 +93,19 @@
                                            num,
                                            mapping={'num':num})
        If using the gettext plural support, which is required for
        languages that have pluralisation rules other than n != 1, the
        ``singular`` argument must be the message_id defined in the
        translation file. The plural argument is not used in this case.
        Example::
           num = 1
           translated = localizer.pluralize('item_plural',
                                            '',
                                            num,
                                            mapping={'num':num})
        
        """
        if self.pluralizer is None:
pyramid/session.py
@@ -8,6 +8,8 @@
from zope.deprecation import deprecated
from zope.interface import implementer
from webob.cookies import SignedSerializer
from pyramid.compat import (
    pickle,
    PY3,
@@ -119,9 +121,17 @@
        return False
    return True
class PickleSerializer(object):
    """ A Webob cookie serializer that uses the pickle protocol to dump Python
    data to bytes."""
    def loads(self, bstruct):
        return pickle.loads(bstruct)
    def dumps(self, appstruct):
        return pickle.dumps(appstruct, pickle.HIGHEST_PROTOCOL)
def BaseCookieSessionFactory(
    serialize,
    deserialize,
    serializer,
    cookie_name='session',
    max_age=None,
    path='/',
@@ -154,13 +164,11 @@
    Parameters:
    ``serialize``
      A callable accepting a Python object and returning a bytestring. A
      ``ValueError`` should be raised for malformed inputs.
    ``deserialize``
      A callable accepting a bytestring and returning a Python object. A
      ``ValueError`` should be raised for malformed inputs.
    ``serializer``
      An object with two methods: `loads`` and ``dumps``.  The ``loads`` method
      should accept bytes and return a Python object.  The ``dumps`` method
      should accept a Python object and return bytes.  A ``ValueError`` should
      be raised for malformed inputs.
    ``cookie_name``
      The name of the cookie used for sessioning. Default: ``'session'``.
@@ -238,7 +246,7 @@
            cookieval = request.cookies.get(self._cookie_name)
            if cookieval is not None:
                try:
                    value = deserialize(bytes_(cookieval))
                    value = serializer.loads(bytes_(cookieval))
                except ValueError:
                    # the cookie failed to deserialize, dropped
                    value = None
@@ -336,7 +344,7 @@
                exception = getattr(self.request, 'exception', None)
                if exception is not None: # dont set a cookie during exceptions
                    return False
            cookieval = native_(serialize(
            cookieval = native_(serializer.dumps(
                (self.accessed, self.created, dict(self))
                ))
            if len(cookieval) > 4064:
@@ -374,6 +382,10 @@
    """
    .. deprecated:: 1.5
       Use :func:`pyramid.session.SignedCookieSessionFactory` instead.
       Caveat: Cookies generated using ``SignedCookieSessionFactory`` are not
       compatible with cookies generated using
       ``UnencryptedCookieSessionFactory``, so existing user session data will
       be destroyed if you switch to it.
    
    Configure a :term:`session factory` which will provide unencrypted
    (but signed) cookie-based sessions.  The return value of this
@@ -430,9 +442,20 @@
      is valid. Default: ``signed_deserialize`` (using pickle).
    """
    class SerializerWrapper(object):
        def __init__(self, secret):
            self.secret = secret
        def loads(self, bstruct):
            return signed_deserialize(bstruct, secret)
        def dumps(self, appstruct):
            return signed_serialize(appstruct, secret)
    serializer = SerializerWrapper(secret)
    return BaseCookieSessionFactory(
        lambda v: signed_serialize(v, secret),
        lambda v: signed_deserialize(v, secret),
        serializer,
        cookie_name=cookie_name,
        max_age=cookie_max_age,
        path=cookie_path,
@@ -447,7 +470,10 @@
deprecated(
    'UnencryptedCookieSessionFactoryConfig',
    'The UnencryptedCookieSessionFactoryConfig callable is deprecated as of '
    'Pyramid 1.5.  Use ``pyramid.session.SignedCookieSessionFactory`` instead.'
    'Pyramid 1.5.  Use ``pyramid.session.SignedCookieSessionFactory`` instead. '
    'Caveat: Cookies generated using SignedCookieSessionFactory are not '
    'compatible with cookies generated using UnencryptedCookieSessionFactory, '
    'so existing user session data will be destroyed if you switch to it.'
    )
def SignedCookieSessionFactory(
@@ -463,8 +489,7 @@
    reissue_time=0,
    hashalg='sha512',
    salt='pyramid.session.',
    serialize=None,
    deserialize=None,
    serializer=None,
    ):
    """
    .. versionadded:: 1.5
@@ -546,53 +571,27 @@
      If ``True``, set a session cookie even if an exception occurs
      while rendering a view. Default: ``True``.
    ``serialize``
      A callable accepting a Python object and returning a bytestring. A
      ``ValueError`` should be raised for malformed inputs.
      Default: :func:`pickle.dumps`.
    ``deserialize``
      A callable accepting a bytestring and returning a Python object. A
      ``ValueError`` should be raised for malformed inputs.
      Default: :func:`pickle.loads`.
    ``serializer``
      An object with two methods: `loads`` and ``dumps``.  The ``loads`` method
      should accept bytes and return a Python object.  The ``dumps`` method
      should accept a Python object and return bytes.  A ``ValueError`` should
      be raised for malformed inputs.  If a serializer is not passed, the
      :class:`pyramid.session.PickleSerializer` serializer will be used.
    .. versionadded: 1.5a3
    """
    if serializer is None:
        serializer = PickleSerializer()
    if serialize is None:
        serialize = lambda v: pickle.dumps(v, pickle.HIGHEST_PROTOCOL)
    if deserialize is None:
        deserialize = pickle.loads
    digestmod = lambda string=b'': hashlib.new(hashalg, string)
    digest_size = digestmod().digest_size
    salted_secret = bytes_(salt or '') + bytes_(secret)
    def signed_serialize(appstruct):
        cstruct = serialize(appstruct)
        sig = hmac.new(salted_secret, cstruct, digestmod).digest()
        return base64.b64encode(cstruct + sig)
    def signed_deserialize(bstruct):
        try:
            fstruct = base64.b64decode(bstruct)
        except (binascii.Error, TypeError) as e:
            raise ValueError('Badly formed base64 data: %s' % e)
        cstruct = fstruct[:-digest_size]
        expected_sig = fstruct[-digest_size:]
        sig = hmac.new(salted_secret, cstruct, digestmod).digest()
        if strings_differ(sig, expected_sig):
            raise ValueError('Invalid signature')
        return deserialize(cstruct)
    signed_serializer = SignedSerializer(
        secret,
        salt,
        hashalg,
        serializer=serializer,
        )
    return BaseCookieSessionFactory(
        signed_serialize,
        signed_deserialize,
        signed_serializer,
        cookie_name=cookie_name,
        max_age=max_age,
        path=path,
pyramid/testing.py
@@ -320,6 +320,7 @@
    method = 'GET'
    application_url = 'http://example.com'
    host = 'example.com:80'
    domain = 'example.com'
    content_length = 0
    query_string = ''
    charset = 'UTF-8'
pyramid/tests/test_authentication.py
@@ -572,7 +572,12 @@
        return DummyRequest(environ, cookie=cookie)
    def _cookieValue(self, cookie):
        return eval(cookie.value)
        items = cookie.value.split('/')
        D = {}
        for item in items:
            k, v = item.split('=', 1)
            D[k] = v
        return D
    def _parseHeaders(self, headers):
        return [ self._parseHeader(header) for header in headers ]
@@ -838,7 +843,7 @@
        request.callbacks[0](None, response)
        self.assertEqual(len(response.headerlist), 3)
        self.assertEqual(response.headerlist[0][0], 'Set-Cookie')
        self.assertTrue("'tokens': ()" in response.headerlist[0][1])
        self.assertTrue("/tokens=/" in response.headerlist[0][1])
    def test_remember(self):
        helper = self._makeOne('secret')
@@ -851,11 +856,11 @@
        self.assertTrue(result[0][1].startswith('auth_tkt='))
        self.assertEqual(result[1][0], 'Set-Cookie')
        self.assertTrue(result[1][1].endswith('; Path=/; Domain=localhost'))
        self.assertTrue(result[1][1].endswith('; Domain=localhost; Path=/'))
        self.assertTrue(result[1][1].startswith('auth_tkt='))
        self.assertEqual(result[2][0], 'Set-Cookie')
        self.assertTrue(result[2][1].endswith('; Path=/; Domain=.localhost'))
        self.assertTrue(result[2][1].endswith('; Domain=.localhost; Path=/'))
        self.assertTrue(result[2][1].startswith('auth_tkt='))
    def test_remember_include_ip(self):
@@ -869,11 +874,11 @@
        self.assertTrue(result[0][1].startswith('auth_tkt='))
        self.assertEqual(result[1][0], 'Set-Cookie')
        self.assertTrue(result[1][1].endswith('; Path=/; Domain=localhost'))
        self.assertTrue(result[1][1].endswith('; Domain=localhost; Path=/'))
        self.assertTrue(result[1][1].startswith('auth_tkt='))
        self.assertEqual(result[2][0], 'Set-Cookie')
        self.assertTrue(result[2][1].endswith('; Path=/; Domain=.localhost'))
        self.assertTrue(result[2][1].endswith('; Domain=.localhost; Path=/'))
        self.assertTrue(result[2][1].startswith('auth_tkt='))
    def test_remember_path(self):
@@ -889,12 +894,12 @@
        self.assertEqual(result[1][0], 'Set-Cookie')
        self.assertTrue(result[1][1].endswith(
            '; Path=/cgi-bin/app.cgi/; Domain=localhost'))
            '; Domain=localhost; Path=/cgi-bin/app.cgi/'))
        self.assertTrue(result[1][1].startswith('auth_tkt='))
        self.assertEqual(result[2][0], 'Set-Cookie')
        self.assertTrue(result[2][1].endswith(
            '; Path=/cgi-bin/app.cgi/; Domain=.localhost'))
            '; Domain=.localhost; Path=/cgi-bin/app.cgi/'))
        self.assertTrue(result[2][1].startswith('auth_tkt='))
    def test_remember_http_only(self):
@@ -922,15 +927,15 @@
        self.assertEqual(len(result), 3)
        self.assertEqual(result[0][0], 'Set-Cookie')
        self.assertTrue('; Secure' in result[0][1])
        self.assertTrue('; secure' in result[0][1])
        self.assertTrue(result[0][1].startswith('auth_tkt='))
        self.assertEqual(result[1][0], 'Set-Cookie')
        self.assertTrue('; Secure' in result[1][1])
        self.assertTrue('; secure' in result[1][1])
        self.assertTrue(result[1][1].startswith('auth_tkt='))
        self.assertEqual(result[2][0], 'Set-Cookie')
        self.assertTrue('; Secure' in result[2][1])
        self.assertTrue('; secure' in result[2][1])
        self.assertTrue(result[2][1].startswith('auth_tkt='))
    def test_remember_wild_domain_disabled(self):
@@ -944,62 +949,49 @@
        self.assertTrue(result[0][1].startswith('auth_tkt='))
        self.assertEqual(result[1][0], 'Set-Cookie')
        self.assertTrue(result[1][1].endswith('; Path=/; Domain=localhost'))
        self.assertTrue(result[1][1].endswith('; Domain=localhost; Path=/'))
        self.assertTrue(result[1][1].startswith('auth_tkt='))
    def test_remember_parent_domain(self):
        helper = self._makeOne('secret', parent_domain=True)
        request = self._makeRequest()
        request.environ['HTTP_HOST'] = 'www.example.com'
        request.domain = 'www.example.com'
        result = helper.remember(request, 'other')
        self.assertEqual(len(result), 1)
        self.assertEqual(result[0][0], 'Set-Cookie')
        self.assertTrue(result[0][1].endswith('; Path=/; Domain=.example.com'))
        self.assertTrue(result[0][1].endswith('; Domain=.example.com; Path=/'))
        self.assertTrue(result[0][1].startswith('auth_tkt='))
    def test_remember_parent_domain_supercedes_wild_domain(self):
        helper = self._makeOne('secret', parent_domain=True, wild_domain=True)
        request = self._makeRequest()
        request.environ['HTTP_HOST'] = 'www.example.com'
        request.domain = 'www.example.com'
        result = helper.remember(request, 'other')
        self.assertEqual(len(result), 1)
        self.assertTrue(result[0][1].endswith('; Domain=.example.com'))
        self.assertTrue(result[0][1].endswith('; Domain=.example.com; Path=/'))
    def test_remember_explicit_domain(self):
        helper = self._makeOne('secret', domain='pyramid.bazinga')
        request = self._makeRequest()
        request.environ['HTTP_HOST'] = 'www.example.com'
        request.domain = 'www.example.com'
        result = helper.remember(request, 'other')
        self.assertEqual(len(result), 1)
        self.assertEqual(result[0][0], 'Set-Cookie')
        self.assertTrue(result[0][1].endswith('; Path=/; Domain=pyramid.bazinga'))
        self.assertTrue(result[0][1].endswith(
                '; Domain=pyramid.bazinga; Path=/'))
        self.assertTrue(result[0][1].startswith('auth_tkt='))
    def test_remember_domain_supercedes_parent_and_wild_domain(self):
        helper = self._makeOne('secret', domain='pyramid.bazinga',
                parent_domain=True, wild_domain=True)
        request = self._makeRequest()
        request.environ['HTTP_HOST'] = 'www.example.com'
        request.domain = 'www.example.com'
        result = helper.remember(request, 'other')
        self.assertEqual(len(result), 1)
        self.assertTrue(result[0][1].endswith('; Path=/; Domain=pyramid.bazinga'))
    def test_remember_domain_has_port(self):
        helper = self._makeOne('secret', wild_domain=False)
        request = self._makeRequest()
        request.environ['HTTP_HOST'] = 'example.com:80'
        result = helper.remember(request, 'other')
        self.assertEqual(len(result), 2)
        self.assertEqual(result[0][0], 'Set-Cookie')
        self.assertTrue(result[0][1].endswith('; Path=/'))
        self.assertTrue(result[0][1].startswith('auth_tkt='))
        self.assertEqual(result[1][0], 'Set-Cookie')
        self.assertTrue(result[1][1].endswith('; Path=/; Domain=example.com'))
        self.assertTrue(result[1][1].startswith('auth_tkt='))
        self.assertTrue(result[0][1].endswith(
                '; Domain=pyramid.bazinga; Path=/'))
    def test_remember_binary_userid(self):
        import base64
@@ -1010,7 +1002,7 @@
        self.assertEqual(len(result), 3)
        val = self._cookieValue(values[0])
        self.assertEqual(val['userid'],
                         bytes_(base64.b64encode(b'userid').strip()))
                         text_(base64.b64encode(b'userid').strip()))
        self.assertEqual(val['user_data'], 'userid_type:b64str')
    def test_remember_int_userid(self):
@@ -1044,7 +1036,7 @@
        self.assertEqual(len(result), 3)
        val = self._cookieValue(values[0])
        self.assertEqual(val['userid'],
                         base64.b64encode(userid.encode('utf-8')))
                         text_(base64.b64encode(userid.encode('utf-8'))))
        self.assertEqual(val['user_data'], 'userid_type:b64unicode')
    def test_remember_insane_userid(self):
@@ -1074,13 +1066,13 @@
        self.assertEqual(len(result), 3)
        self.assertEqual(result[0][0], 'Set-Cookie')
        self.assertTrue("'tokens': ('foo', 'bar')" in result[0][1])
        self.assertTrue("/tokens=foo|bar/" in result[0][1])
        self.assertEqual(result[1][0], 'Set-Cookie')
        self.assertTrue("'tokens': ('foo', 'bar')" in result[1][1])
        self.assertTrue("/tokens=foo|bar/" in result[1][1])
        self.assertEqual(result[2][0], 'Set-Cookie')
        self.assertTrue("'tokens': ('foo', 'bar')" in result[2][1])
        self.assertTrue("/tokens=foo|bar/" in result[2][1])
    def test_remember_unicode_but_ascii_token(self):
        helper = self._makeOne('secret')
@@ -1088,7 +1080,7 @@
        la = text_(b'foo', 'utf-8')
        result = helper.remember(request, 'other', tokens=(la,))
        # tokens must be str type on both Python 2 and 3
        self.assertTrue("'tokens': ('foo',)" in result[0][1])
        self.assertTrue("/tokens=foo/" in result[0][1])
    def test_remember_nonascii_token(self):
        helper = self._makeOne('secret')
@@ -1112,18 +1104,25 @@
        self.assertEqual(len(headers), 3)
        name, value = headers[0]
        self.assertEqual(name, 'Set-Cookie')
        self.assertEqual(value,
          'auth_tkt=""; Path=/; Max-Age=0; Expires=Wed, 31-Dec-97 23:59:59 GMT')
        self.assertEqual(
            value,
            'auth_tkt=; Max-Age=0; Path=/; '
            'expires=Wed, 31-Dec-97 23:59:59 GMT'
            )
        name, value = headers[1]
        self.assertEqual(name, 'Set-Cookie')
        self.assertEqual(value,
                         'auth_tkt=""; Path=/; Max-Age=0; '
                         'Expires=Wed, 31-Dec-97 23:59:59 GMT; Domain=localhost')
        self.assertEqual(
            value,
            'auth_tkt=; Domain=localhost; Max-Age=0; Path=/; '
            'expires=Wed, 31-Dec-97 23:59:59 GMT'
            )
        name, value = headers[2]
        self.assertEqual(name, 'Set-Cookie')
        self.assertEqual(value,
                         'auth_tkt=""; Path=/; Max-Age=0; '
                         'Expires=Wed, 31-Dec-97 23:59:59 GMT; Domain=.localhost')
        self.assertEqual(
            value,
            'auth_tkt=; Domain=.localhost; Max-Age=0; Path=/; '
            'expires=Wed, 31-Dec-97 23:59:59 GMT'
            )
class TestAuthTicket(unittest.TestCase):
    def _makeOne(self, *arg, **kw):
@@ -1417,7 +1416,19 @@
        self.assertEqual(policy.forget(None), [
            ('WWW-Authenticate', 'Basic realm="SomeRealm"')])
class TestSimpleSerializer(unittest.TestCase):
    def _makeOne(self):
        from pyramid.authentication import _SimpleSerializer
        return _SimpleSerializer()
    def test_loads(self):
        inst = self._makeOne()
        self.assertEqual(inst.loads(b'abc'), text_('abc'))
    def test_dumps(self):
        inst = self._makeOne()
        self.assertEqual(inst.dumps('abc'), bytes_('abc'))
class DummyContext:
    pass
@@ -1429,6 +1440,7 @@
        return self.cookie
class DummyRequest:
    domain = 'localhost'
    def __init__(self, environ=None, session=None, registry=None, cookie=None):
        self.environ = environ or {}
        self.session = session or {}
@@ -1486,10 +1498,23 @@
                self.kw = kw
            def cookie_value(self):
                result = {'secret':self.secret, 'userid':self.userid,
                          'remote_addr':self.remote_addr}
                result = {
                    'secret':self.secret,
                    'userid':self.userid,
                    'remote_addr':self.remote_addr
                    }
                result.update(self.kw)
                result = repr(result)
                tokens = result.pop('tokens', None)
                if tokens is not None:
                    tokens = '|'.join(tokens)
                    result['tokens'] = tokens
                items = sorted(result.items())
                new_items = []
                for k, v in items:
                    if isinstance(v, bytes):
                        v = text_(v)
                    new_items.append((k,v))
                result = '/'.join(['%s=%s' % (k, v) for k,v in new_items ])
                return result
        self.AuthTicket = AuthTicket
pyramid/tests/test_session.py
@@ -264,8 +264,8 @@
class TestBaseCookieSession(SharedCookieSessionTests, unittest.TestCase):
    def _makeOne(self, request, **kw):
        from pyramid.session import BaseCookieSessionFactory
        return BaseCookieSessionFactory(
            dummy_serialize, dummy_deserialize, **kw)(request)
        serializer = DummySerializer()
        return BaseCookieSessionFactory(serializer, **kw)(request)
    def _serialize(self, value):
        return json.dumps(value)
@@ -294,7 +294,7 @@
        digestmod = lambda: hashlib.new(hashalg)
        cstruct = pickle.dumps(value, pickle.HIGHEST_PROTOCOL)
        sig = hmac.new(salt + b'secret', cstruct, digestmod).digest()
        return base64.b64encode(cstruct + sig)
        return base64.urlsafe_b64encode(sig + cstruct).rstrip(b'=')
    def test_reissue_not_triggered(self):
        import time
@@ -353,11 +353,12 @@
        import hmac
        import time
        request = testing.DummyRequest()
        cstruct = dummy_serialize((time.time(), 0, {'state': 1}))
        serializer = DummySerializer()
        cstruct = serializer.dumps((time.time(), 0, {'state': 1}))
        sig = hmac.new(b'pyramid.session.secret', cstruct, sha512).digest()
        cookieval = base64.b64encode(cstruct + sig)
        cookieval = base64.urlsafe_b64encode(sig + cstruct).rstrip(b'=')
        request.cookies['session'] = cookieval
        session = self._makeOne(request, deserialize=dummy_deserialize)
        session = self._makeOne(request, serializer=serializer)
        self.assertEqual(session['state'], 1)
    def test_invalid_data_size(self):
@@ -382,7 +383,7 @@
        try:
            result = callbacks[0](request, response)
        except TypeError as e: # pragma: no cover
        except TypeError: # pragma: no cover
            self.fail('HMAC failed to initialize due to key length.')
        self.assertEqual(result, None)
@@ -413,8 +414,9 @@
            kw.setdefault(dest, kw.pop(src))
    def _serialize(self, value):
        from pyramid.compat import bytes_
        from pyramid.session import signed_serialize
        return signed_serialize(value, 'secret')
        return bytes_(signed_serialize(value, 'secret'))
    def test_serialize_option(self):
        from pyramid.response import Response
@@ -596,11 +598,12 @@
        result = self._callFUT(request, 'csrf_token', raises=False)
        self.assertEqual(result, False)
def dummy_serialize(value):
    return json.dumps(value).encode('utf-8')
class DummySerializer(object):
    def dumps(self, value):
        return json.dumps(value).encode('utf-8')
def dummy_deserialize(value):
    return json.loads(value.decode('utf-8'))
    def loads(self, value):
        return json.loads(value.decode('utf-8'))
class DummySessionFactory(dict):
    _dirty = False
pyramid/url.py
@@ -359,7 +359,7 @@
        .. warning:: if no ``elements`` arguments are specified, the resource
                     URL will end with a trailing slash.  If any
                     ``elements`` are used, the generated URL will *not*
                     end in trailing a slash.
                     end in a trailing slash.
        If a keyword argument ``query`` is present, it will be used to compose
        a query string that will be tacked on to the end of the URL.  The value
setup.py
@@ -39,7 +39,7 @@
install_requires=[
    'setuptools',
    'WebOb >= 1.2b3', # request.path_info is unicode
    'WebOb >= 1.3', # request.domain and CookieProfile
    'repoze.lru >= 0.4', # py3 compat
    'zope.interface >= 3.8.0',  # has zope.interface.registry
    'zope.deprecation >= 3.5.0', # py3 compat
@@ -69,7 +69,7 @@
    ]
setup(name='pyramid',
      version='1.5a2',
      version='1.5a3',
      description='The Pyramid Web Framework, a Pylons project',
      long_description=README + '\n\n' +  CHANGES,
      classifiers=[