summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--pyramid/compat.py18
-rw-r--r--pyramid/config/testing.py10
-rw-r--r--pyramid/config/util.py6
-rw-r--r--pyramid/traversal.py90
-rw-r--r--pyramid/urldispatch.py19
5 files changed, 94 insertions, 49 deletions
diff --git a/pyramid/compat.py b/pyramid/compat.py
index 12b8c9f37..7376278ac 100644
--- a/pyramid/compat.py
+++ b/pyramid/compat.py
@@ -213,3 +213,21 @@ except ImportError: # pragma: no cover
import json
+if PY3: # pragma: no cover
+ # see PEP 3333 for why we encode WSGI PATH_INFO to latin-1 before
+ # decoding it to utf-8
+ def decode_path_info(path):
+ return path.encode('latin-1').decode('utf-8')
+else:
+ def decode_path_info(path):
+ return path.decode('utf-8')
+
+if PY3: # pragma: no cover
+ # see PEP 3333 for why we decode the path to latin-1
+ from urllib.parse import unquote_to_bytes
+ def unquote_bytes_to_wsgi(bytestring):
+ return unquote_to_bytes(bytestring).decode('latin-1')
+else:
+ from urlparse import unquote as unquote_to_bytes
+ def unquote_bytes_to_wsgi(bytestring):
+ return unquote_to_bytes(bytestring)
diff --git a/pyramid/config/testing.py b/pyramid/config/testing.py
index 3cdc1aa24..f40cf25a7 100644
--- a/pyramid/config/testing.py
+++ b/pyramid/config/testing.py
@@ -8,7 +8,11 @@ from pyramid.interfaces import (
)
from pyramid.renderers import RendererHelper
-from pyramid.traversal import traversal_path_info
+
+from pyramid.traversal import (
+ decode_path_info,
+ split_path_info,
+ )
from pyramid.config.util import action_method
@@ -66,9 +70,9 @@ class TestingConfiguratorMixin(object):
self.context = context
def __call__(self, request):
- path = request.environ['PATH_INFO']
+ path = decode_path_info(request.environ['PATH_INFO'])
ob = resources[path]
- traversed = traversal_path_info(path)
+ traversed = split_path_info(path)
return {'context':ob, 'view_name':'','subpath':(),
'traversed':traversed, 'virtual_root':ob,
'virtual_root_path':(), 'root':ob}
diff --git a/pyramid/config/util.py b/pyramid/config/util.py
index b0e873de3..79f13e4a0 100644
--- a/pyramid/config/util.py
+++ b/pyramid/config/util.py
@@ -15,7 +15,7 @@ from pyramid.exceptions import ConfigurationError
from pyramid.traversal import (
find_interface,
- traversal_path_info,
+ traversal_path,
)
from hashlib import md5
@@ -268,8 +268,8 @@ def make_predicates(xhr=None, request_method=None, path_info=None,
if 'traverse' in context:
return True
m = context['match']
- tvalue = tgenerate(m)
- m['traverse'] = traversal_path_info(tvalue)
+ tvalue = tgenerate(m) # tvalue will be urlquoted string
+ m['traverse'] = traversal_path(tvalue) # will be seq of unicode
return True
# This isn't actually a predicate, it's just a infodict
# modifier that injects ``traverse`` into the matchdict. As a
diff --git a/pyramid/traversal.py b/pyramid/traversal.py
index cd624fd30..ffc40fa60 100644
--- a/pyramid/traversal.py
+++ b/pyramid/traversal.py
@@ -16,12 +16,12 @@ from pyramid.compat import (
PY3,
native_,
text_,
- bytes_,
ascii_native_,
text_type,
binary_type,
- url_unquote_native,
is_nonstr_iter,
+ decode_path_info,
+ unquote_bytes_to_wsgi,
)
from pyramid.encode import url_quote
@@ -429,33 +429,46 @@ def virtual_root(resource, request):
def traversal_path(path):
""" Variant of :func:`pyramid.traversal.traversal_path_info` suitable for
- decoding paths that are URL-encoded."""
- path = ascii_native_(path)
- path = url_unquote_native(path, 'latin-1', 'strict')
- return traversal_path_info(path)
+ decoding paths that are URL-encoded.
+
+ If this function is passed a Unicode object instead of a sequence of
+ bytes as ``path``, that Unicode object *must* directly encodeable to
+ ASCII. For example, u'/foo' will work but u'/<unprintable unicode>' (a
+ Unicode object with characters that cannot be encoded to ascii) will
+ not. A :exc:`UnicodeEncodeError` will be raised if the Unicode cannot be
+ encoded directly to ASCII.
+ """
+ if isinstance(path, text_type):
+ # must not possess characters outside ascii
+ path = path.encode('ascii')
+ # we unquote this path exactly like a PEP 3333 server would
+ path = unquote_bytes_to_wsgi(path) # result will be a native string
+ return traversal_path_info(path) # result will be a tuple of unicode
@lru_cache(1000)
def traversal_path_info(path):
- """ Given a ``PATH_INFO`` environ value (slash-separated path segments),
- return a tuple representing that path which can be used to traverse a
- resource tree.
-
- ``PATH_INFO`` is assumed to already be URL-decoded. It is encoded to
- bytes using the Latin-1 encoding; the resulting set of bytes is
- subsequently decoded to text using the UTF-8 encoding; a
- :exc:`pyramid.exc.URLDecodeError` is raised if a the URL cannot be
- decoded.
-
- The ``PATH_INFO`` is split on slashes, creating a list of segments. Each
- segment subsequently decoded into Unicode. If a segment name is empty or
- if it is ``.``, it is ignored. If a segment name is ``..``, the previous
- segment is deleted, and the ``..`` is ignored.
-
- If this function is passed a Unicode object instead of a string, that
- Unicode object *must* directly encodeable to ASCII. For example, u'/foo'
- will work but u'/<unprintable unicode>' (a Unicode object with characters
- that cannot be encoded to ascii) will not. A :exc:`UnicodeError` will be
- raised if the Unicode cannot be encoded directly to ASCII.
+ """ Given``path``, return a tuple representing that path which can be
+ used to traverse a resource tree. ``path`` is assumed to be an
+ already-URL-decoded ``str`` type as if it had come to us from an upstream
+ WSGI server as the ``PATH_INFO`` environ variable.
+
+ The ``path`` is first decoded to from its WSGI representation to Unicode;
+ it is decoded differently depending on platform:
+
+ - On Python 2, ``path`` is decoded to Unicode from bytes using the UTF-8
+ decoding directly; a :exc:`pyramid.exc.URLDecodeError` is raised if a the
+ URL cannot be decoded.
+
+ - On Python 3, as per the PEP 3333 spec, ``path`` is first encoded to
+ bytes using the Latin-1 encoding; the resulting set of bytes is
+ subsequently decoded to text using the UTF-8 encoding; a
+ :exc:`pyramid.exc.URLDecodeError` is raised if a the URL cannot be
+ decoded.
+
+ The ``path`` is split on slashes, creating a list of segments. If a
+ segment name is empty or if it is ``.``, it is ignored. If a segment
+ name is ``..``, the previous segment is deleted, and the ``..`` is
+ ignored.
Examples:
@@ -504,9 +517,15 @@ def traversal_path_info(path):
applications in :app:`Pyramid`.
"""
try:
- path = bytes_(path, 'latin-1').decode('utf-8')
+ path = decode_path_info(path) # result will be Unicode
except UnicodeDecodeError as e:
raise URLDecodeError(e.encoding, e.object, e.start, e.end, e.reason)
+ return split_path_info(path) # result will be tuple of Unicode
+
+@lru_cache(1000)
+def split_path_info(path):
+ # suitable for splitting an already-unquoted-already-decoded (unicode)
+ # path value
path = path.strip('/')
clean = []
for segment in path.split('/'):
@@ -622,23 +641,32 @@ class ResourceTreeTraverser(object):
path = matchdict.get('traverse', '/') or '/'
if is_nonstr_iter(path):
# this is a *traverse stararg (not a {traverse})
- path = '/'.join([quote_path_segment(x) for x in path]) or '/'
+ # routing has already decoded these elements, so we just
+ # need to join them
+ path = '/'.join(path) or '/'
subpath = matchdict.get('subpath', ())
if not is_nonstr_iter(subpath):
# this is not a *subpath stararg (just a {subpath})
- subpath = traversal_path_info(subpath)
+ # routing has already decoded this string, so we just need
+ # to split it
+ subpath = split_path_info(subpath)
else:
# this request did not match a route
subpath = ()
try:
- path = environ['PATH_INFO'] or '/'
+ # empty if mounted under a path in mod_wsgi, for example
+ path = decode_path_info(environ['PATH_INFO'] or '/')
except KeyError:
path = '/'
+ except UnicodeDecodeError as e:
+ raise URLDecodeError(e.encoding, e.object, e.start, e.end,
+ e.reason)
if VH_ROOT_KEY in environ:
- vroot_path = environ[VH_ROOT_KEY]
+ # HTTP_X_VHM_ROOT
+ vroot_path = decode_path_info(environ[VH_ROOT_KEY])
vroot_tuple = traversal_path_info(vroot_path)
vpath = vroot_path + path
vroot_idx = len(vroot_tuple) -1
diff --git a/pyramid/urldispatch.py b/pyramid/urldispatch.py
index c7520b8d2..73875b675 100644
--- a/pyramid/urldispatch.py
+++ b/pyramid/urldispatch.py
@@ -18,8 +18,9 @@ from pyramid.compat import (
from pyramid.exceptions import URLDecodeError
from pyramid.traversal import (
- traversal_path_info,
quote_path_segment,
+ decode_path_info,
+ split_path_info,
)
_marker = object()
@@ -70,9 +71,11 @@ class RoutesMapper(object):
environ = request.environ
try:
# empty if mounted under a path in mod_wsgi, for example
- path = environ['PATH_INFO'] or '/'
+ path = decode_path_info(environ['PATH_INFO'] or '/')
except KeyError:
path = '/'
+ except UnicodeDecodeError as e:
+ raise URLDecodeError(e.encoding, e.object, e.start, e.end, e.reason)
for route in self.routelist:
match = route.match(path)
@@ -147,17 +150,9 @@ def _compile_route(route):
d = {}
for k, v in m.groupdict().items():
if k == star:
- d[k] = traversal_path_info(v)
+ d[k] = split_path_info(v)
else:
- try:
- val = bytes_(v).decode('utf-8', 'strict')
- d[k] = val
- except UnicodeDecodeError as e:
- raise URLDecodeError(
- e.encoding, e.object, e.start, e.end, e.reason
- )
-
-
+ d[k] = v
return d