summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.travis.yml2
-rw-r--r--CHANGES.txt49
-rw-r--r--HACKING.txt32
-rw-r--r--docs/narr/i18n.rst6
-rw-r--r--docs/whatsnew-1.5.rst78
-rw-r--r--pyramid/authentication.py86
-rw-r--r--pyramid/config/views.py18
-rw-r--r--pyramid/encode.py23
-rw-r--r--pyramid/i18n.py33
-rw-r--r--pyramid/scripts/pshell.py29
-rw-r--r--pyramid/session.py113
-rw-r--r--pyramid/testing.py1
-rw-r--r--pyramid/tests/test_authentication.py129
-rw-r--r--pyramid/tests/test_config/test_views.py21
-rw-r--r--pyramid/tests/test_encode.py5
-rw-r--r--pyramid/tests/test_scripts/test_pshell.py64
-rw-r--r--pyramid/tests/test_session.py27
-rw-r--r--pyramid/tests/test_url.py55
-rw-r--r--pyramid/url.py167
-rw-r--r--setup.py4
20 files changed, 603 insertions, 339 deletions
diff --git a/.travis.yml b/.travis.yml
index bc82c8faf..29e499e76 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,6 +8,8 @@ python:
- 3.2
- 3.3
+install: python setup.py dev
+
script: python setup.py test -q
notifications:
diff --git a/CHANGES.txt b/CHANGES.txt
index c05c4561e..1ae2ebbd9 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,5 @@
-Unreleased
-==========
+1.5a3 (2013-12-10)
+==================
Features
--------
@@ -35,11 +35,13 @@ Features
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
@@ -48,6 +50,27 @@ Features
timeouts, and conformance with the ``ISession`` API.
See https://github.com/Pylons/pyramid/pull/1142
+- The anchor argument to ``pyramid.request.Request.route_url`` and
+ ``pyramid.request.Request.resource_url`` and their derivatives will now be
+ escaped via URL quoting to ensure minimal conformance. See
+ https://github.com/Pylons/pyramid/pull/1183
+
+- Allow sending of ``_query`` and ``_anchor`` options to
+ ``pyramid.request.Request.static_url`` when an external URL is being
+ generated.
+ See https://github.com/Pylons/pyramid/pull/1183
+
+- You can now send a string as the ``_query`` argument to
+ ``pyramid.request.Request.route_url`` and
+ ``pyramid.request.Request.resource_url`` and their derivatives. When a
+ string is sent instead of a list or dictionary. it is URL-quoted however it
+ 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
---------
@@ -122,12 +145,6 @@ Deprecations
- The ``pyramid.security.has_permission`` API is now deprecated. Instead, use
the newly-added ``has_permission`` method of the request object.
-- The ``pyramid.security.forget`` API is now deprecated. Instead, use
- the newly-added ``forget_userid`` method of the request object.
-
-- The ``pyramid.security.remember`` API is now deprecated. Instead, use
- the newly-added ``remember_userid`` method of the request object.
-
- The ``pyramid.security.effective_principals`` API is now deprecated.
Instead, use the newly-added ``effective_principals`` attribute of the
request object.
@@ -140,6 +157,12 @@ Deprecations
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)
==================
diff --git a/HACKING.txt b/HACKING.txt
index b32a8a957..12f2d68e2 100644
--- a/HACKING.txt
+++ b/HACKING.txt
@@ -6,9 +6,9 @@ Here are some guidelines about hacking on Pyramid.
Using a Development Checkout
----------------------------
-You'll have to create a development environment to hack on Pyramid, using a
-Pyramid checkout. You can either do this by hand or, if you have ``tox``
-installed (it's on PyPI), you can (ab)use tox to get a working development
+You'll have to create a development environment to hack on Pyramid, using a
+Pyramid checkout. You can either do this by hand or, if you have ``tox``
+installed (it's on PyPI), you can (ab)use tox to get a working development
environment. Each installation method is described below.
By Hand
@@ -25,15 +25,15 @@ By Hand
$ cd ~/hack-on-pyramid
$ virtualenv -ppython2.7 env
- Note that very old versions of virtualenv (virtualenv versions below, say,
+ Note that very old versions of virtualenv (virtualenv versions below, say,
1.10 or thereabouts) require you to pass a ``--no-site-packages`` flag to
get a completely isolated environment.
- You can choose which Python version you want to use by passing a ``-p``
+ You can choose which Python version you want to use by passing a ``-p``
flag to ``virtualenv``. For example, ``virtualenv -ppython2.7``
chooses the Python 2.7 interpreter to be installed.
- From here on in within these instructions, the ``~/hack-on-pyramid/env``
+ From here on in within these instructions, the ``~/hack-on-pyramid/env``
virtual environment you created above will be referred to as ``$VENV``.
To use the instructions in the steps that follow literally, use the
``export VENV=~/hack-on-pyramid/env`` command.
@@ -132,7 +132,7 @@ Coding Style
- PEP8 compliance. Whitespace rules are relaxed: not necessary to put
2 newlines between classes. But 80-column lines, in particular, are
- mandatory. See
+ mandatory. See
http://docs.pylonsproject.org/en/latest/community/codestyle.html for more
information.
@@ -142,14 +142,14 @@ Coding Style
Running Tests
--------------
-- To run all tests for Pyramid on a single Python version, run ``nosetests``
+- To run all tests for Pyramid on a single Python version, run ``nosetests``
from your development virtualenv (See *Using a Development Checkout* above).
- To run individual tests (i.e. during development) you can use a regular
expression with the ``-t`` parameter courtesy of the `nose-selecttests
- <https://pypi.python.org/pypi/nose-selecttests/>`_ plugin that's been
- installed (along with nose itself) via ``python setup.py dev``. The
- easiest usage is to simply provide the verbatim name of the test you're
+ <https://pypi.python.org/pypi/nose-selecttests/>`_ plugin that's been
+ installed (along with nose itself) via ``python setup.py dev``. The
+ easiest usage is to simply provide the verbatim name of the test you're
working on.
- To run the full set of Pyramid tests on all platforms, install ``tox``
@@ -191,8 +191,8 @@ or adds the feature.
To build and review docs (where ``$VENV`` refers to the virtualenv you're
using to develop Pyramid):
-1. After following the steps above in "Using a Development Checkout", cause
- Sphinx and all development requirements to be installed in your
+1. After following the steps above in "Using a Development Checkout", cause
+ Sphinx and all development requirements to be installed in your
virtualenv::
$ cd ~/hack-on-pyramid
@@ -212,9 +212,9 @@ using to develop Pyramid):
$ cd ~/hack-on-pyramid/pyramid/docs
$ make clean html SPHINXBUILD=$VENV/bin/sphinx-build
- The ``SPHINXBUILD=...`` hair is there in order to tell it to use the
- virtualenv Python, which will have both Sphinx and Pyramid (for API
- documentation generation) installed.
+ The ``SPHINXBUILD=...`` argument tells Sphinx to use the virtualenv Python,
+ which will have both Sphinx and Pyramid (for API documentation generation)
+ installed.
4. Open the ``docs/_build/html/index.html`` file to see the resulting HTML
rendering.
diff --git a/docs/narr/i18n.rst b/docs/narr/i18n.rst
index c9b782c08..5f50ca212 100644
--- a/docs/narr/i18n.rst
+++ b/docs/narr/i18n.rst
@@ -607,10 +607,8 @@ object, but the domain and mapping information attached is ignored.
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.
diff --git a/docs/whatsnew-1.5.rst b/docs/whatsnew-1.5.rst
index 57f93cbff..9ccf097a8 100644
--- a/docs/whatsnew-1.5.rst
+++ b/docs/whatsnew-1.5.rst
@@ -316,6 +316,51 @@ The feature additions in Pyramid 1.5 follow.
- :func:`pyramid.path.package_name` no longer thows an exception when resolving
the package name for namespace packages that have no ``__file__`` attribute.
+- An authorization API has been added as a method of the request:
+ :meth:`pyramid.request.Request.has_permission`. It is a method-based
+ alternative to the :func:`pyramid.security.has_permission` API and works
+ exactly the same. The older API is now deprecated.
+
+- Property API attributes have been added to the request for easier access to
+ authentication data: :attr:`pyramid.request.Request.authenticated_userid`,
+ :attr:`pyramid.request.Request.unauthenticated_userid`, and
+ :attr:`pyramid.request.Request.effective_principals`. These are analogues,
+ respectively, of :func:`pyramid.security.authenticated_userid`,
+ :func:`pyramid.security.unauthenticated_userid`, and
+ :func:`pyramid.security.effective_principals`. They operate exactly the
+ same, except they are attributes of the request instead of functions
+ accepting a request. They are properties, so they cannot be assigned to.
+ The older function-based APIs are now deprecated.
+
+- Pyramid's console scripts (``pserve``, ``pviews``, etc) can now be run
+ directly, allowing custom arguments to be sent to the python interpreter
+ at runtime. For example::
+
+ python -3 -m pyramid.scripts.pserve development.ini
+
+- Added a specific subclass of :class:`pyramid.httpexceptions.HTTPBadRequest`
+ named :class:`pyramid.exceptions.BadCSRFToken` which will now be raised in
+ response to failures in the ``check_csrf_token`` view predicate. 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 . 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
+ session implementations. It provides a reusable API which focuses strictly
+ on providing a dictionary-like object that properly handles renewals,
+ timeouts, and conformance with the ``ISession`` API.
+ See https://github.com/Pylons/pyramid/pull/1142
+
+
Other Backwards Incompatibilities
---------------------------------
@@ -404,6 +449,13 @@ Other Backwards Incompatibilities
Pyramid narrative documentation instead of providing renderer globals values
to the configurator.
+- The key/values in the ``_query`` parameter of
+ :meth:`pyramid.request.Request.route_url` and the ``query`` parameter of
+ :meth:`pyramid.request.Request.resource_url` (and their variants), used to
+ encode a value of ``None`` as the string ``'None'``, leaving the resulting
+ query string to be ``a=b&key=None``. The value is now dropped in this
+ situation, leaving a query string of ``a=b&key=``. See
+ https://github.com/Pylons/pyramid/issues/1119
Deprecations
------------
@@ -417,12 +469,36 @@ Deprecations
a deprecation warning when used. It had been docs-deprecated in 1.4
but did not issue a deprecation warning when used.
+- :func:`pyramid.security.has_permission` is now deprecated in favor of using
+ :meth:`pyramid.request.Request.has_permission`.
+
+- The :func:`pyramid.security.authenticated_userid`,
+ :func:`pyramid.security.unauthenticated_userid`, and
+ :func:`pyramid.security.effective_principals` functions have been
+ deprecated. Use :attr:`pyramid.request.Request.authenticated_userid`,
+ :attr:`pyramid.request.Request.unauthenticated_userid` and
+ :attr:`pyramid.request.Request.effective_principals` instead.
+
+- Deprecate the ``pyramid.interfaces.ITemplateRenderer`` interface. It was
+ ill-defined and became unused when Mako and Chameleon template bindings were
+ split into their own packages.
+
+- The ``pyramid.session.UnencryptedCookieSessionFactoryConfig`` API has been
+ deprecated and is superseded by the
+ ``pyramid.session.SignedCookieSessionFactory``. Note that while the cookies
+ generated by the ``UnencryptedCookieSessionFactoryConfig``
+ are compatible with cookies generated by old releases, cookies generated by
+ the SignedCookieSessionFactory are not. See
+ https://github.com/Pylons/pyramid/pull/1142
+
Documentation Enhancements
--------------------------
- A new documentation chapter named :ref:`quick_tour` was added. It describes
starting out with Pyramid from a high level.
+- Added a :ref:`quick_tutorial` to go with the Quick Tour
+
- Many other enhancements.
@@ -431,3 +507,5 @@ Dependency Changes
- Pyramid no longer depends upon ``Mako`` or ``Chameleon``.
+- Pyramid now depends on WebOb>=1.3 (it uses ``webob.cookies.CookieProfile``
+ from 1.3+).
diff --git a/pyramid/authentication.py b/pyramid/authentication.py
index 2c301bd29..ba7b864f9 100644
--- a/pyramid/authentication.py
+++ b/pyramid/authentication.py
@@ -10,6 +10,8 @@ import warnings
from zope.interface import implementer
+from webob.cookies import CookieProfile
+
from pyramid.compat import (
long,
text_type,
@@ -18,6 +20,7 @@ from pyramid.compat import (
url_quote,
bytes_,
ascii_native_,
+ native_,
)
from pyramid.interfaces import (
@@ -798,8 +801,6 @@ def encode_ip_timestamp(ip, timestamp):
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 @@ class AuthTktCookieHelper(object):
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 @@ class AuthTktCookieHelper(object):
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 @@ class AuthTktCookieHelper(object):
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 @@ class AuthTktCookieHelper(object):
)
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 @@ class BasicAuthAuthenticationPolicy(CallbackAuthenticationPolicy):
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)
+
diff --git a/pyramid/config/views.py b/pyramid/config/views.py
index a3f885504..72dc3f414 100644
--- a/pyramid/config/views.py
+++ b/pyramid/config/views.py
@@ -44,6 +44,11 @@ from pyramid.compat import (
is_nonstr_iter
)
+from pyramid.encode import (
+ quote_plus,
+ urlencode,
+)
+
from pyramid.exceptions import (
ConfigurationError,
PredicateMismatch,
@@ -65,6 +70,8 @@ from pyramid.security import NO_PERMISSION_REQUIRED
from pyramid.static import static_view
from pyramid.threadlocal import get_current_registry
+from pyramid.url import parse_url_overrides
+
from pyramid.view import (
render_view_to_response,
AppendSlashNotFoundViewFactory,
@@ -1895,14 +1902,15 @@ class StaticURLInfo(object):
kw['subpath'] = subpath
return request.route_url(route_name, **kw)
else:
+ app_url, scheme, host, port, qs, anchor = \
+ parse_url_overrides(kw)
parsed = url_parse(url)
if not parsed.scheme:
- # parsed.scheme is readonly, so we have to parse again
- # to change the scheme, sigh.
- url = urlparse.urlunparse(url_parse(
- url, scheme=request.environ['wsgi.url_scheme']))
+ url = urlparse.urlunparse(parsed._replace(
+ scheme=request.environ['wsgi.url_scheme']))
subpath = url_quote(subpath)
- return urljoin(url, subpath)
+ result = urljoin(url, subpath)
+ return result + qs + anchor
raise ValueError('No static URL definition matching %s' % path)
diff --git a/pyramid/encode.py b/pyramid/encode.py
index 9e190bc21..0be0107b3 100644
--- a/pyramid/encode.py
+++ b/pyramid/encode.py
@@ -3,11 +3,16 @@ from pyramid.compat import (
binary_type,
is_nonstr_iter,
url_quote as _url_quote,
- url_quote_plus as quote_plus, # bw compat api (dnr)
+ url_quote_plus as _quote_plus,
)
-def url_quote(s, safe=''): # bw compat api
- return _url_quote(s, safe=safe)
+def url_quote(val, safe=''): # bw compat api
+ cls = val.__class__
+ if cls is text_type:
+ val = val.encode('utf-8')
+ elif cls is not binary_type:
+ val = str(val).encode('utf-8')
+ return _url_quote(val, safe=safe)
def urlencode(query, doseq=True):
"""
@@ -47,28 +52,28 @@ def urlencode(query, doseq=True):
prefix = ''
for (k, v) in query:
- k = _enc(k)
+ k = quote_plus(k)
if is_nonstr_iter(v):
for x in v:
- x = _enc(x)
+ x = quote_plus(x)
result += '%s%s=%s' % (prefix, k, x)
prefix = '&'
elif v is None:
result += '%s%s=' % (prefix, k)
else:
- v = _enc(v)
+ v = quote_plus(v)
result += '%s%s=%s' % (prefix, k, v)
prefix = '&'
return result
-def _enc(val):
+# bw compat api (dnr)
+def quote_plus(val, safe=''):
cls = val.__class__
if cls is text_type:
val = val.encode('utf-8')
elif cls is not binary_type:
val = str(val).encode('utf-8')
- return quote_plus(val)
-
+ return _quote_plus(val, safe=safe)
diff --git a/pyramid/i18n.py b/pyramid/i18n.py
index 6ffd93e8f..aaba769c6 100644
--- a/pyramid/i18n.py
+++ b/pyramid/i18n.py
@@ -75,16 +75,16 @@ class Localizer(object):
: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 @@ class Localizer(object):
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:
diff --git a/pyramid/scripts/pshell.py b/pyramid/scripts/pshell.py
index dd09bf457..12b078677 100644
--- a/pyramid/scripts/pshell.py
+++ b/pyramid/scripts/pshell.py
@@ -153,16 +153,12 @@ class PShellCommand(object):
shell = None
user_shell = self.options.python_shell.lower()
if not user_shell:
- shell = self.make_ipython_v0_11_shell()
- if shell is None:
- shell = self.make_ipython_v0_10_shell()
+ shell = self.make_ipython_shell()
if shell is None:
shell = self.make_bpython_shell()
elif user_shell == 'ipython':
- shell = self.make_ipython_v0_11_shell()
- if shell is None:
- shell = self.make_ipython_v0_10_shell()
+ shell = self.make_ipython_shell()
elif user_shell == 'bpython':
shell = self.make_bpython_shell()
@@ -191,6 +187,27 @@ class PShellCommand(object):
BPShell(locals_=env, banner=help + '\n')
return shell
+ def make_ipython_shell(self):
+ shell = self.make_ipython_v1_1_shell()
+ if shell is None:
+ shell = self.make_ipython_v0_11_shell()
+ if shell is None:
+ shell = self.make_ipython_v0_10_shell()
+ return shell
+
+ def make_ipython_v1_1_shell(self, IPShellFactory=None):
+ if IPShellFactory is None: # pragma: no cover
+ try:
+ from IPython.terminal.embed import (
+ InteractiveShellEmbed)
+ IPShellFactory = InteractiveShellEmbed
+ except ImportError:
+ return None
+ def shell(env, help):
+ IPShell = IPShellFactory(banner2=help + '\n', user_ns=env)
+ IPShell()
+ return shell
+
def make_ipython_v0_11_shell(self, IPShellFactory=None):
if IPShellFactory is None: # pragma: no cover
try:
diff --git a/pyramid/session.py b/pyramid/session.py
index d3a4113b9..8c9900975 100644
--- a/pyramid/session.py
+++ b/pyramid/session.py
@@ -8,6 +8,8 @@ import time
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 @@ def check_csrf_token(request,
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 @@ def BaseCookieSessionFactory(
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 @@ def BaseCookieSessionFactory(
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 @@ def BaseCookieSessionFactory(
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 @@ def UnencryptedCookieSessionFactoryConfig(
"""
.. 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 @@ def UnencryptedCookieSessionFactoryConfig(
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 @@ def UnencryptedCookieSessionFactoryConfig(
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 @@ def SignedCookieSessionFactory(
reissue_time=0,
hashalg='sha512',
salt='pyramid.session.',
- serialize=None,
- deserialize=None,
+ serializer=None,
):
"""
.. versionadded:: 1.5
@@ -546,53 +571,27 @@ def SignedCookieSessionFactory(
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,
diff --git a/pyramid/testing.py b/pyramid/testing.py
index b3460d8aa..91dc41dd5 100644
--- a/pyramid/testing.py
+++ b/pyramid/testing.py
@@ -320,6 +320,7 @@ class DummyRequest(
method = 'GET'
application_url = 'http://example.com'
host = 'example.com:80'
+ domain = 'example.com'
content_length = 0
query_string = ''
charset = 'UTF-8'
diff --git a/pyramid/tests/test_authentication.py b/pyramid/tests/test_authentication.py
index 3ac8f2d61..79d2a5923 100644
--- a/pyramid/tests/test_authentication.py
+++ b/pyramid/tests/test_authentication.py
@@ -572,7 +572,12 @@ class TestAuthTktCookieHelper(unittest.TestCase):
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 @@ class TestAuthTktCookieHelper(unittest.TestCase):
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 @@ class TestAuthTktCookieHelper(unittest.TestCase):
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 @@ class TestAuthTktCookieHelper(unittest.TestCase):
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 @@ class TestAuthTktCookieHelper(unittest.TestCase):
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 @@ class TestAuthTktCookieHelper(unittest.TestCase):
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 @@ class TestAuthTktCookieHelper(unittest.TestCase):
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 @@ class TestAuthTktCookieHelper(unittest.TestCase):
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 @@ class TestAuthTktCookieHelper(unittest.TestCase):
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 @@ class TestAuthTktCookieHelper(unittest.TestCase):
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 @@ class TestAuthTktCookieHelper(unittest.TestCase):
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 @@ class TestAuthTktCookieHelper(unittest.TestCase):
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 @@ class TestBasicAuthAuthenticationPolicy(unittest.TestCase):
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 @@ class DummyCookies(object):
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 @@ class DummyAuthTktModule(object):
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
diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py
index 051961d25..57bb5e9d0 100644
--- a/pyramid/tests/test_config/test_views.py
+++ b/pyramid/tests/test_config/test_views.py
@@ -3820,6 +3820,27 @@ class TestStaticURLInfo(unittest.TestCase):
result = inst.generate('package:path/abc def', request, a=1)
self.assertEqual(result, 'http://example.com/abc%20def')
+ def test_generate_url_with_custom_query(self):
+ inst = self._makeOne()
+ registrations = [('http://example.com/', 'package:path/', None)]
+ inst._get_registrations = lambda *x: registrations
+ request = self._makeRequest()
+ result = inst.generate('package:path/abc def', request, a=1,
+ _query='(openlayers)')
+ self.assertEqual(result,
+ 'http://example.com/abc%20def?(openlayers)')
+
+ def test_generate_url_with_custom_anchor(self):
+ inst = self._makeOne()
+ registrations = [('http://example.com/', 'package:path/', None)]
+ inst._get_registrations = lambda *x: registrations
+ request = self._makeRequest()
+ uc = text_(b'La Pe\xc3\xb1a', 'utf-8')
+ result = inst.generate('package:path/abc def', request, a=1,
+ _anchor=uc)
+ self.assertEqual(result,
+ 'http://example.com/abc%20def#La%20Pe%C3%B1a')
+
def test_add_already_exists(self):
inst = self._makeOne()
config = self._makeConfig(
diff --git a/pyramid/tests/test_encode.py b/pyramid/tests/test_encode.py
index 908249877..8fb766d88 100644
--- a/pyramid/tests/test_encode.py
+++ b/pyramid/tests/test_encode.py
@@ -72,3 +72,8 @@ class URLQuoteTests(unittest.TestCase):
la = b'La/Pe\xc3\xb1a'
result = self._callFUT(la, '/')
self.assertEqual(result, 'La/Pe%C3%B1a')
+
+ def test_it_with_nonstr_nonbinary(self):
+ la = None
+ result = self._callFUT(la, '/')
+ self.assertEqual(result, 'None')
diff --git a/pyramid/tests/test_scripts/test_pshell.py b/pyramid/tests/test_scripts/test_pshell.py
index 8f9f3abfb..7cb130c41 100644
--- a/pyramid/tests/test_scripts/test_pshell.py
+++ b/pyramid/tests/test_scripts/test_pshell.py
@@ -42,6 +42,15 @@ class TestPShellCommand(unittest.TestCase):
self.assertEqual(bpython.locals_, {'foo': 'bar'})
self.assertTrue('a help message' in bpython.banner)
+ def test_make_ipython_v1_1_shell(self):
+ command = self._makeOne()
+ ipshell_factory = dummy.DummyIPShellFactory()
+ shell = command.make_ipython_v1_1_shell(ipshell_factory)
+ shell({'foo': 'bar'}, 'a help message')
+ self.assertEqual(ipshell_factory.kw['user_ns'], {'foo': 'bar'})
+ self.assertTrue('a help message' in ipshell_factory.kw['banner2'])
+ self.assertTrue(ipshell_factory.shell.called)
+
def test_make_ipython_v0_11_shell(self):
command = self._makeOne()
ipshell_factory = dummy.DummyIPShellFactory()
@@ -64,8 +73,7 @@ class TestPShellCommand(unittest.TestCase):
def test_command_loads_default_shell(self):
command = self._makeOne()
shell = dummy.DummyShell()
- command.make_ipython_v0_11_shell = lambda: None
- command.make_ipython_v0_10_shell = lambda: None
+ command.make_ipython_shell = lambda: None
command.make_bpython_shell = lambda: None
command.make_default_shell = lambda: shell
command.run()
@@ -86,8 +94,7 @@ class TestPShellCommand(unittest.TestCase):
command = self._makeOne()
shell = dummy.DummyShell()
bad_shell = dummy.DummyShell()
- command.make_ipython_v0_11_shell = lambda: bad_shell
- command.make_ipython_v0_10_shell = lambda: bad_shell
+ command.make_ipython_shell = lambda: bad_shell
command.make_bpython_shell = lambda: bad_shell
command.make_default_shell = lambda: shell
command.options.python_shell = 'unknow_python_shell'
@@ -106,9 +113,33 @@ class TestPShellCommand(unittest.TestCase):
self.assertTrue(self.bootstrap.closer.called)
self.assertTrue(shell.help)
+ def test_command_loads_ipython_v1_1(self):
+ command = self._makeOne()
+ shell = dummy.DummyShell()
+ command.make_ipython_v1_1_shell = lambda: shell
+ command.make_ipython_v0_11_shell = lambda: None
+ command.make_ipython_v0_10_shell = lambda: None
+ command.make_bpython_shell = lambda: None
+ command.make_default_shell = lambda: None
+ command.options.python_shell = 'ipython'
+ command.run()
+ self.assertTrue(self.config_factory.parser)
+ self.assertEqual(self.config_factory.parser.filename,
+ '/foo/bar/myapp.ini')
+ self.assertEqual(self.bootstrap.a[0], '/foo/bar/myapp.ini#myapp')
+ self.assertEqual(shell.env, {
+ 'app':self.bootstrap.app, 'root':self.bootstrap.root,
+ 'registry':self.bootstrap.registry,
+ 'request':self.bootstrap.request,
+ 'root_factory':self.bootstrap.root_factory,
+ })
+ self.assertTrue(self.bootstrap.closer.called)
+ self.assertTrue(shell.help)
+
def test_command_loads_ipython_v0_11(self):
command = self._makeOne()
shell = dummy.DummyShell()
+ command.make_ipython_v1_1_shell = lambda: None
command.make_ipython_v0_11_shell = lambda: shell
command.make_ipython_v0_10_shell = lambda: None
command.make_bpython_shell = lambda: None
@@ -131,6 +162,7 @@ class TestPShellCommand(unittest.TestCase):
def test_command_loads_ipython_v0_10(self):
command = self._makeOne()
shell = dummy.DummyShell()
+ command.make_ipython_v1_1_shell = lambda: None
command.make_ipython_v0_11_shell = lambda: None
command.make_ipython_v0_10_shell = lambda: shell
command.make_bpython_shell = lambda: None
@@ -153,8 +185,7 @@ class TestPShellCommand(unittest.TestCase):
def test_command_loads_bpython_shell(self):
command = self._makeOne()
shell = dummy.DummyBPythonShell()
- command.make_ipython_v0_11_shell = lambda: None
- command.make_ipython_v0_10_shell = lambda: None
+ command.make_ipython_shell = lambda: None
command.make_bpython_shell = lambda: shell
command.options.python_shell = 'bpython'
command.run()
@@ -173,25 +204,34 @@ class TestPShellCommand(unittest.TestCase):
def test_shell_ipython_ordering(self):
command = self._makeOne()
+ shell1_1 = dummy.DummyShell()
shell0_11 = dummy.DummyShell()
shell0_10 = dummy.DummyShell()
+ command.make_ipython_v1_1_shell = lambda: shell1_1
+ shell = command.make_shell()
+ self.assertEqual(shell, shell1_1)
+
+ command.make_ipython_v1_1_shell = lambda: None
command.make_ipython_v0_11_shell = lambda: shell0_11
- command.make_ipython_v0_10_shell = lambda: shell0_10
- command.make_bpython_shell = lambda: None
shell = command.make_shell()
self.assertEqual(shell, shell0_11)
+ command.make_ipython_v0_11_shell = lambda: None
+ command.make_ipython_v0_10_shell = lambda: shell0_10
+ shell = command.make_shell()
+ self.assertEqual(shell, shell0_10)
+
command.options.python_shell = 'ipython'
+ command.make_ipython_v1_1_shell = lambda: shell1_1
shell = command.make_shell()
- self.assertEqual(shell, shell0_11)
+ self.assertEqual(shell, shell1_1)
def test_shell_ordering(self):
command = self._makeOne()
ipshell = dummy.DummyShell()
bpshell = dummy.DummyShell()
dshell = dummy.DummyShell()
- command.make_ipython_v0_11_shell = lambda: None
- command.make_ipython_v0_10_shell = lambda: None
+ command.make_ipython_shell = lambda: None
command.make_bpython_shell = lambda: None
command.make_default_shell = lambda: dshell
@@ -206,7 +246,7 @@ class TestPShellCommand(unittest.TestCase):
shell = command.make_shell()
self.assertEqual(shell, dshell)
- command.make_ipython_v0_11_shell = lambda: ipshell
+ command.make_ipython_shell = lambda: ipshell
command.make_bpython_shell = lambda: bpshell
command.options.python_shell = 'ipython'
shell = command.make_shell()
diff --git a/pyramid/tests/test_session.py b/pyramid/tests/test_session.py
index a9f70d6a0..1ad0729b3 100644
--- a/pyramid/tests/test_session.py
+++ b/pyramid/tests/test_session.py
@@ -264,8 +264,8 @@ class SharedCookieSessionTests(object):
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 @@ class TestSignedCookieSession(SharedCookieSessionTests, unittest.TestCase):
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 @@ class TestSignedCookieSession(SharedCookieSessionTests, unittest.TestCase):
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 @@ class TestSignedCookieSession(SharedCookieSessionTests, unittest.TestCase):
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 @@ class TestUnencryptedCookieSession(SharedCookieSessionTests, unittest.TestCase):
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 @@ class Test_check_csrf_token(unittest.TestCase):
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
diff --git a/pyramid/tests/test_url.py b/pyramid/tests/test_url.py
index f6117777f..0a788ba97 100644
--- a/pyramid/tests/test_url.py
+++ b/pyramid/tests/test_url.py
@@ -6,7 +6,6 @@ from pyramid import testing
from pyramid.compat import (
text_,
- native_,
WIN,
)
@@ -93,6 +92,14 @@ class TestURLMethodsMixin(unittest.TestCase):
result = request.resource_url(context, 'a b c')
self.assertEqual(result, 'http://example.com:5432/context/a%20b%20c')
+ def test_resource_url_with_query_str(self):
+ request = self._makeOne()
+ self._registerResourceURL(request.registry)
+ context = DummyContext()
+ result = request.resource_url(context, 'a', query='(openlayers)')
+ self.assertEqual(result,
+ 'http://example.com:5432/context/a?(openlayers)')
+
def test_resource_url_with_query_dict(self):
request = self._makeOne()
self._registerResourceURL(request.registry)
@@ -149,23 +156,18 @@ class TestURLMethodsMixin(unittest.TestCase):
request = self._makeOne()
self._registerResourceURL(request.registry)
context = DummyContext()
- uc = text_(b'La Pe\xc3\xb1a', 'utf-8')
+ uc = text_(b'La Pe\xc3\xb1a', 'utf-8')
result = request.resource_url(context, anchor=uc)
- self.assertEqual(
- result,
- native_(
- text_(b'http://example.com:5432/context/#La Pe\xc3\xb1a',
- 'utf-8'),
- 'utf-8')
- )
+ self.assertEqual(result,
+ 'http://example.com:5432/context/#La%20Pe%C3%B1a')
- def test_resource_url_anchor_is_not_urlencoded(self):
+ def test_resource_url_anchor_is_urlencoded_safe(self):
request = self._makeOne()
self._registerResourceURL(request.registry)
context = DummyContext()
- result = request.resource_url(context, anchor=' /#')
+ result = request.resource_url(context, anchor=' /#?&+')
self.assertEqual(result,
- 'http://example.com:5432/context/# /#')
+ 'http://example.com:5432/context/#%20/%23?&+')
def test_resource_url_no_IResourceURL_registered(self):
# falls back to ResourceURL
@@ -448,14 +450,8 @@ class TestURLMethodsMixin(unittest.TestCase):
request.registry.registerUtility(mapper, IRoutesMapper)
result = request.route_url('flub', _anchor=b"La Pe\xc3\xb1a")
- self.assertEqual(
- result,
- native_(
- text_(
- b'http://example.com:5432/1/2/3#La Pe\xc3\xb1a',
- 'utf-8'),
- 'utf-8')
- )
+ self.assertEqual(result,
+ 'http://example.com:5432/1/2/3#La%20Pe%C3%B1a')
def test_route_url_with_anchor_unicode(self):
from pyramid.interfaces import IRoutesMapper
@@ -465,14 +461,8 @@ class TestURLMethodsMixin(unittest.TestCase):
anchor = text_(b'La Pe\xc3\xb1a', 'utf-8')
result = request.route_url('flub', _anchor=anchor)
- self.assertEqual(
- result,
- native_(
- text_(
- b'http://example.com:5432/1/2/3#La Pe\xc3\xb1a',
- 'utf-8'),
- 'utf-8')
- )
+ self.assertEqual(result,
+ 'http://example.com:5432/1/2/3#La%20Pe%C3%B1a')
def test_route_url_with_query(self):
from pyramid.interfaces import IRoutesMapper
@@ -483,6 +473,15 @@ class TestURLMethodsMixin(unittest.TestCase):
self.assertEqual(result,
'http://example.com:5432/1/2/3?q=1')
+ def test_route_url_with_query_str(self):
+ from pyramid.interfaces import IRoutesMapper
+ request = self._makeOne()
+ mapper = DummyRoutesMapper(route=DummyRoute('/1/2/3'))
+ request.registry.registerUtility(mapper, IRoutesMapper)
+ result = request.route_url('flub', _query='(openlayers)')
+ self.assertEqual(result,
+ 'http://example.com:5432/1/2/3?(openlayers)')
+
def test_route_url_with_empty_query(self):
from pyramid.interfaces import IRoutesMapper
request = self._makeOne()
diff --git a/pyramid/url.py b/pyramid/url.py
index fda2c72c7..78dd297d5 100644
--- a/pyramid/url.py
+++ b/pyramid/url.py
@@ -12,12 +12,13 @@ from pyramid.interfaces import (
)
from pyramid.compat import (
- native_,
bytes_,
- text_type,
- url_quote,
+ string_types,
)
-from pyramid.encode import urlencode
+from pyramid.encode import (
+ url_quote,
+ urlencode,
+)
from pyramid.path import caller_package
from pyramid.threadlocal import get_current_registry
@@ -27,6 +28,48 @@ from pyramid.traversal import (
)
PATH_SAFE = '/:@&+$,' # from webob
+QUERY_SAFE = '/?:@!$&\'()*+,;=' # RFC 3986
+ANCHOR_SAFE = QUERY_SAFE
+
+def parse_url_overrides(kw):
+ """Parse special arguments passed when generating urls.
+
+ The supplied dictionary is mutated, popping arguments as necessary.
+ Returns a 6-tuple of the format ``(app_url, scheme, host, port,
+ qs, anchor)``.
+ """
+ anchor = ''
+ qs = ''
+ app_url = None
+ host = None
+ scheme = None
+ port = None
+
+ if '_query' in kw:
+ query = kw.pop('_query')
+ if isinstance(query, string_types):
+ qs = '?' + url_quote(query, QUERY_SAFE)
+ elif query:
+ qs = '?' + urlencode(query, doseq=True)
+
+ if '_anchor' in kw:
+ anchor = kw.pop('_anchor')
+ anchor = url_quote(anchor, ANCHOR_SAFE)
+ anchor = '#' + anchor
+
+ if '_app_url' in kw:
+ app_url = kw.pop('_app_url')
+
+ if '_host' in kw:
+ host = kw.pop('_host')
+
+ if '_scheme' in kw:
+ scheme = kw.pop('_scheme')
+
+ if '_port' in kw:
+ port = kw.pop('_port')
+
+ return app_url, scheme, host, port, qs, anchor
class URLMethodsMixin(object):
""" Request methods mixin for BaseRequest having to do with URL
@@ -124,18 +167,22 @@ class URLMethodsMixin(object):
``*remainder`` replacement value, it is tacked on to the URL
after being URL-quoted-except-for-embedded-slashes.
- If no ``_query`` keyword argument is provided, the request
- query string will be returned in the URL. If it is present, it
- will be used to compose a query string that will be tacked on
- to the end of the URL, replacing any request query string.
- The value of ``_query`` must be a sequence of two-tuples *or*
- a data structure with an ``.items()`` method that returns a
- sequence of two-tuples (presumably a dictionary). This data
- structure will be turned into a query string per the
- documentation of :func:`pyramid.encode.urlencode` function.
- After the query data is turned into a query string, a leading
- ``?`` is prepended, and the resulting string is appended to
- the generated URL.
+ If no ``_query`` keyword argument is provided, the request query string
+ will be returned in the URL. If it is present, it will be used to
+ compose a query string that will be tacked on to the end of the URL,
+ replacing any request query string. The value of ``_query`` may be a
+ sequence of two-tuples *or* a data structure with an ``.items()``
+ method that returns a sequence of two-tuples (presumably a dictionary).
+ This data structure will be turned into a query string per the
+ documentation of :func:`pyramid.url.urlencode` function. This will
+ produce a query string in the ``x-www-form-urlencoded`` format. A
+ non-``x-www-form-urlencoded`` query string may be used by passing a
+ *string* value as ``_query`` in which case it will be URL-quoted
+ (e.g. query="foo bar" will become "foo%20bar"). However, the result
+ will not need to be in ``k=v`` form as required by
+ ``x-www-form-urlencoded``. After the query data is turned into a query
+ string, a leading ``?`` is prepended, and the resulting string is
+ appended to the generated URL.
.. note::
@@ -146,8 +193,13 @@ class URLMethodsMixin(object):
as values, and a k=v pair will be placed into the query string for
each value.
+ .. versionchanged:: 1.5
+ Allow the ``_query`` option to be a string to enable alternative
+ encodings.
+
If a keyword argument ``_anchor`` is present, its string
- representation will be used as a named anchor in the generated URL
+ representation will be quoted per :rfc:`3986#section-3.5` and used as
+ a named anchor in the generated URL
(e.g. if ``_anchor`` is passed as ``foo`` and the route URL is
``http://example.com/route/url``, the resulting generated URL will
be ``http://example.com/route/url#foo``).
@@ -156,8 +208,11 @@ class URLMethodsMixin(object):
If ``_anchor`` is passed as a string, it should be UTF-8 encoded. If
``_anchor`` is passed as a Unicode object, it will be converted to
- UTF-8 before being appended to the URL. The anchor value is not
- quoted in any way before being appended to the generated URL.
+ UTF-8 before being appended to the URL.
+
+ .. versionchanged:: 1.5
+ The ``_anchor`` option will be escaped instead of using
+ its raw string representation.
If both ``_anchor`` and ``_query`` are specified, the anchor
element will always follow the query element,
@@ -213,34 +268,7 @@ class URLMethodsMixin(object):
if route.pregenerator is not None:
elements, kw = route.pregenerator(self, elements, kw)
- anchor = ''
- qs = ''
- app_url = None
- host = None
- scheme = None
- port = None
-
- if '_query' in kw:
- query = kw.pop('_query')
- if query:
- qs = '?' + urlencode(query, doseq=True)
-
- if '_anchor' in kw:
- anchor = kw.pop('_anchor')
- anchor = native_(anchor, 'utf-8')
- anchor = '#' + anchor
-
- if '_app_url' in kw:
- app_url = kw.pop('_app_url')
-
- if '_host' in kw:
- host = kw.pop('_host')
-
- if '_scheme' in kw:
- scheme = kw.pop('_scheme')
-
- if '_port' in kw:
- port = kw.pop('_port')
+ app_url, scheme, host, port, qs, anchor = parse_url_overrides(kw)
if app_url is None:
if (scheme is not None or host is not None or port is not None):
@@ -331,17 +359,22 @@ class URLMethodsMixin(object):
.. 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.
-
- 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 of ``query`` must be a sequence of two-tuples *or* a data
- structure with an ``.items()`` method that returns a sequence of
- two-tuples (presumably a dictionary). This data structure will be
- turned into a query string per the documentation of
- ``pyramid.url.urlencode`` function. After the query data is turned
- into a query string, a leading ``?`` is prepended, and the resulting
- string is appended to the generated URL.
+ 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
+ of ``query`` may be a sequence of two-tuples *or* a data structure with
+ an ``.items()`` method that returns a sequence of two-tuples
+ (presumably a dictionary). This data structure will be turned into a
+ query string per the documentation of :func:``pyramid.url.urlencode``
+ function. This will produce a query string in the
+ ``x-www-form-urlencoded`` encoding. A non-``x-www-form-urlencoded``
+ query string may be used by passing a *string* value as ``query`` in
+ which case it will be URL-quoted (e.g. query="foo bar" will become
+ "foo%20bar"). However, the result will not need to be in ``k=v`` form
+ as required by ``x-www-form-urlencoded``. After the query data is
+ turned into a query string, a leading ``?`` is prepended, and the
+ resulting string is appended to the generated URL.
.. note::
@@ -352,6 +385,10 @@ class URLMethodsMixin(object):
as values, and a k=v pair will be placed into the query string for
each value.
+ .. versionchanged:: 1.5
+ Allow the ``query`` option to be a string to enable alternative
+ encodings.
+
If a keyword argument ``anchor`` is present, its string
representation will be used as a named anchor in the generated URL
(e.g. if ``anchor`` is passed as ``foo`` and the resource URL is
@@ -362,8 +399,11 @@ class URLMethodsMixin(object):
If ``anchor`` is passed as a string, it should be UTF-8 encoded. If
``anchor`` is passed as a Unicode object, it will be converted to
- UTF-8 before being appended to the URL. The anchor value is not
- quoted in any way before being appended to the generated URL.
+ UTF-8 before being appended to the URL.
+
+ .. versionchanged:: 1.5
+ The ``anchor`` option will be escaped instead of using
+ its raw string representation.
If both ``anchor`` and ``query`` are specified, the anchor element
will always follow the query element,
@@ -580,13 +620,14 @@ class URLMethodsMixin(object):
if 'query' in kw:
query = kw['query']
- if query:
+ if isinstance(query, string_types):
+ qs = '?' + url_quote(query, QUERY_SAFE)
+ elif query:
qs = '?' + urlencode(query, doseq=True)
if 'anchor' in kw:
anchor = kw['anchor']
- if isinstance(anchor, text_type):
- anchor = native_(anchor, 'utf-8')
+ anchor = url_quote(anchor, ANCHOR_SAFE)
anchor = '#' + anchor
if elements:
diff --git a/setup.py b/setup.py
index 2d49717b7..e6c9a490a 100644
--- a/setup.py
+++ b/setup.py
@@ -39,7 +39,7 @@ except IOError:
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 @@ testing_extras = tests_require + [
]
setup(name='pyramid',
- version='1.5a2',
+ version='1.5a3',
description='The Pyramid Web Framework, a Pylons project',
long_description=README + '\n\n' + CHANGES,
classifiers=[