Michael Merickel
2018-10-15 a54bc1ccac17625991e26eb5d4577f893803c683
commit | author | age
3b7334 1 from zope.interface import implementer
0f5d8b 2 from zope.interface.interfaces import IInterface
77e32d 3
0c1c39 4 from pyramid.interfaces import (
c51896 5     IResourceURL,
0c1c39 6     IRequestFactory,
CM 7     ITraverser,
8     VH_ROOT_KEY,
0c29cf 9 )
e6e0ea 10
0c1c39 11 from pyramid.compat import (
bc37a5 12     PY2,
0c1c39 13     native_,
CM 14     text_,
15     ascii_native_,
16     text_type,
17     binary_type,
18     is_nonstr_iter,
5c43d5 19     decode_path_info,
CM 20     unquote_bytes_to_wsgi,
e80491 21     lru_cache,
0c29cf 22 )
0c1c39 23
b60bdb 24 from pyramid.encode import url_quote
CM 25 from pyramid.exceptions import URLDecodeError
26 from pyramid.location import lineage
27 from pyramid.threadlocal import get_current_registry
e6c2d2 28
0c29cf 29 PATH_SEGMENT_SAFE = "~!$&'()*+,;=:@"  # from webob
f52759 30 PATH_SAFE = PATH_SEGMENT_SAFE + "/"
MK 31
e6c2d2 32 empty = text_('')
0c29cf 33
423b85 34
fb6a5c 35 def find_root(resource):
bac5b3 36     """ Find the root node in the resource tree to which ``resource``
fb6a5c 37     belongs. Note that ``resource`` should be :term:`location`-aware.
CM 38     Note that the root resource is available in the request object by
67b8a1 39     accessing the ``request.root`` attribute.
FCN 40     """
fb6a5c 41     for location in lineage(resource):
cc5e49 42         if location.__parent__ is None:
fb6a5c 43             resource = location
cc5e49 44             break
fb6a5c 45     return resource
0c29cf 46
cc5e49 47
fb6a5c 48 def find_resource(resource, path):
CM 49     """ Given a resource object and a string or tuple representing a path
bac5b3 50     (such as the return value of :func:`pyramid.traversal.resource_path` or
CM 51     :func:`pyramid.traversal.resource_path_tuple`), return a resource in this
52     application's resource tree at the specified path.  The resource passed
53     in *must* be :term:`location`-aware.  If the path cannot be resolved (if
54     the respective node in the resource tree does not exist), a
55     :exc:`KeyError` will be raised.
a97a70 56
c6895b 57     This function is the logical inverse of
fb6a5c 58     :func:`pyramid.traversal.resource_path` and
CM 59     :func:`pyramid.traversal.resource_path_tuple`; it can resolve any
c6895b 60     path string or tuple generated by either of those functions.
a97a70 61
CM 62     Rules for passing a *string* as the ``path`` argument: if the
eaf1b4 63     first character in the path string is the ``/``
CDLG 64     character, the path is considered absolute and the resource tree
b32bfd 65     traversal will start at the root resource.  If the first character
a97a70 66     of the path string is *not* the ``/`` character, the path is
bac5b3 67     considered relative and resource tree traversal will begin at the resource
fb6a5c 68     object supplied to the function as the ``resource`` argument.  If an
CM 69     empty string is passed as ``path``, the ``resource`` passed in will
70     be returned.  Resource path strings must be escaped in the following
a97a70 71     manner: each Unicode path segment must be encoded as UTF-8 and as
c6895b 72     each path segment must escaped via Python's :mod:`urllib.quote`.
CM 73     For example, ``/path/to%20the/La%20Pe%C3%B1a`` (absolute) or
74     ``to%20the/La%20Pe%C3%B1a`` (relative).  The
fb6a5c 75     :func:`pyramid.traversal.resource_path` function generates strings
c6895b 76     which follow these rules (albeit only absolute ones).
a97a70 77
f84147 78     Rules for passing *text* (Unicode) as the ``path`` argument are the same
CM 79     as those for a string.  In particular, the text may not have any nonascii
80     characters in it.
81
a97a70 82     Rules for passing a *tuple* as the ``path`` argument: if the first
CM 83     element in the path tuple is the empty string (for example ``('',
bac5b3 84     'a', 'b', 'c')``, the path is considered absolute and the resource tree
CM 85     traversal will start at the resource tree root object.  If the first
a97a70 86     element in the path tuple is not the empty string (for example
bac5b3 87     ``('a', 'b', 'c')``), the path is considered relative and resource tree
fb6a5c 88     traversal will begin at the resource object supplied to the function
CM 89     as the ``resource`` argument.  If an empty sequence is passed as
90     ``path``, the ``resource`` passed in itself will be returned.  No
dbab77 91     URL-quoting or UTF-8-encoding of individual path segments within
CM 92     the tuple is required (each segment may be any string or unicode
fb6a5c 93     object representing a resource name).  Resource path tuples generated by
CM 94     :func:`pyramid.traversal.resource_path_tuple` can always be
95     resolved by ``find_resource``.
dbab77 96     """
f84147 97     if isinstance(path, text_type):
CM 98         path = ascii_native_(path)
fb6a5c 99     D = traverse(resource, path)
916f88 100     view_name = D['view_name']
CM 101     context = D['context']
102     if view_name:
103         raise KeyError('%r has no subelement %s' % (context, view_name))
104     return context
cc5e49 105
0c29cf 106
MM 107 find_model = find_resource  # b/w compat (forever)
108
fb6a5c 109
CM 110 def find_interface(resource, class_or_interface):
7ae0c2 111     """
b32bfd 112     Return the first resource found in the :term:`lineage` of ``resource``
CM 113     which, a) if ``class_or_interface`` is a Python class object, is an
114     instance of the class or any subclass of that class or b) if
115     ``class_or_interface`` is a :term:`interface`, provides the specified
116     interface.  Return ``None`` if no resource providing ``interface_or_class``
117     can be found in the lineage.  The ``resource`` passed in *must* be
118     :term:`location`-aware.
7ae0c2 119     """
0f5d8b 120     if IInterface.providedBy(class_or_interface):
CM 121         test = class_or_interface.providedBy
122     else:
123         test = lambda arg: isinstance(arg, class_or_interface)
fb6a5c 124     for location in lineage(resource):
0f5d8b 125         if test(location):
bb5d64 126             return location
0c29cf 127
35ff8e 128
fb6a5c 129 def resource_path(resource, *elements):
CM 130     """ Return a string object representing the absolute physical path of the
131     resource object based on its position in the resource tree, e.g
132     ``/foo/bar``.  Any positional arguments passed in as ``elements`` will be
133     appended as path segments to the end of the resource path.  For instance,
134     if the resource's path is ``/foo/bar`` and ``elements`` equals ``('a',
135     'b')``, the returned string will be ``/foo/bar/a/b``.  The first
136     character in the string will always be the ``/`` character (a leading
137     ``/`` character in a path string represents that the path is absolute).
a97a70 138
fb6a5c 139     Resource path strings returned will be escaped in the following
358821 140     manner: each unicode path segment will be encoded as UTF-8 and
c6895b 141     each path segment will be escaped via Python's :mod:`urllib.quote`.
a97a70 142     For example, ``/path/to%20the/La%20Pe%C3%B1a``.
CM 143
c6895b 144     This function is a logical inverse of
fb6a5c 145     :mod:`pyramid.traversal.find_resource`: it can be used to generate
c6895b 146     path references that can later be resolved via that function.
a97a70 147
fb6a5c 148     The ``resource`` passed in *must* be :term:`location`-aware.
a97a70 149
012b97 150     .. note::
8e037f 151
012b97 152        Each segment in the path string returned will use the ``__name__``
M 153        attribute of the resource it represents within the resource tree.  Each
154        of these segments *should* be a unicode or string object (as per the
155        contract of :term:`location`-awareness).  However, no conversion or
156        safety checking of resource names is performed.  For instance, if one of
157        the resources in your tree has a ``__name__`` which (by error) is a
158        dictionary, the :func:`pyramid.traversal.resource_path` function will
159        attempt to append it to a string and it will cause a
160        :exc:`pyramid.exceptions.URLDecodeError`.
92c3e5 161
012b97 162     .. note::
M 163
164        The :term:`root` resource *must* have a ``__name__`` attribute with a
165        value of either ``None`` or the empty string for paths to be generated
166        properly.  If the root resource has a non-null ``__name__`` attribute,
167        its name will be prepended to the generated path rather than a single
168        leading '/' character.
a97a70 169     """
eb9fbf 170     # joining strings is a bit expensive so we delegate to a function
CM 171     # which caches the joined result for us
fb6a5c 172     return _join_path_tuple(resource_path_tuple(resource, *elements))
eb9fbf 173
0c29cf 174
MM 175 model_path = resource_path  # b/w compat (forever)
176
fb6a5c 177
CM 178 def traverse(resource, path):
179     """Given a resource object as ``resource`` and a string or tuple
916f88 180     representing a path as ``path`` (such as the return value of
fb6a5c 181     :func:`pyramid.traversal.resource_path` or
CM 182     :func:`pyramid.traversal.resource_path_tuple` or the value of
916f88 183     ``request.environ['PATH_INFO']``), return a dictionary with the
CM 184     keys ``context``, ``root``, ``view_name``, ``subpath``,
185     ``traversed``, ``virtual_root``, and ``virtual_root_path``.
186
187     A definition of each value in the returned dictionary:
188
fb6a5c 189     - ``context``: The :term:`context` (a :term:`resource` object) found
8b1f6e 190       via traversal or url dispatch.  If the ``path`` passed in is the
fb6a5c 191       empty string, the value of the ``resource`` argument passed to this
8b1f6e 192       function is returned.
916f88 193
fb6a5c 194     - ``root``: The resource object at which :term:`traversal` begins.
CM 195       If the ``resource`` passed in was found via url dispatch or if the
7f1243 196       ``path`` passed in was relative (non-absolute), the value of the
fb6a5c 197       ``resource`` argument passed to this function is returned.
916f88 198
8b1f6e 199     - ``view_name``: The :term:`view name` found during
fb6a5c 200       :term:`traversal` or :term:`url dispatch`; if the ``resource`` was
8b1f6e 201       found via traversal, this is usually a representation of the
CM 202       path segment which directly follows the path to the ``context``
203       in the ``path``.  The ``view_name`` will be a Unicode object or
204       the empty string.  The ``view_name`` will be the empty string if
205       there is no element which follows the ``context`` path.  An
b32bfd 206       example: if the path passed is ``/foo/bar``, and a resource
8b1f6e 207       object is found at ``/foo`` (but not at ``/foo/bar``), the 'view
fb6a5c 208       name' will be ``u'bar'``.  If the ``resource`` was found via
8b1f6e 209       urldispatch, the view_name will be the name the route found was
CM 210       registered with.
916f88 211
fb6a5c 212     - ``subpath``: For a ``resource`` found via :term:`traversal`, this
8b1f6e 213       is a sequence of path segments found in the ``path`` that follow
CM 214       the ``view_name`` (if any).  Each of these items is a Unicode
916f88 215       object.  If no path segments follow the ``view_name``, the
8b1f6e 216       subpath will be the empty sequence.  An example: if the path
b32bfd 217       passed is ``/foo/bar/baz/buz``, and a resource object is found at
916f88 218       ``/foo`` (but not ``/foo/bar``), the 'view name' will be
8b1f6e 219       ``u'bar'`` and the :term:`subpath` will be ``[u'baz', u'buz']``.
fb6a5c 220       For a ``resource`` found via url dispatch, the subpath will be a
8b1f6e 221       sequence of values discerned from ``*subpath`` in the route
CM 222       pattern matched or the empty sequence.
916f88 223
7f1243 224     - ``traversed``: The sequence of path elements traversed from the
8b1f6e 225       root to find the ``context`` object during :term:`traversal`.
CM 226       Each of these items is a Unicode object.  If no path segments
227       were traversed to find the ``context`` object (e.g. if the
228       ``path`` provided is the empty string), the ``traversed`` value
fb6a5c 229       will be the empty sequence.  If the ``resource`` is a resource found
8b1f6e 230       via :term:`url dispatch`, traversed will be None.
916f88 231
fb6a5c 232     - ``virtual_root``: A resource object representing the 'virtual' root
bac5b3 233       of the resource tree being traversed during :term:`traversal`.
8b1f6e 234       See :ref:`vhosting_chapter` for a definition of the virtual root
7f1243 235       object.  If no virtual hosting is in effect, and the ``path``
CM 236       passed in was absolute, the ``virtual_root`` will be the
b32bfd 237       *physical* root resource object (the object at which :term:`traversal`
fb6a5c 238       begins).  If the ``resource`` passed in was found via :term:`URL
8b1f6e 239       dispatch` or if the ``path`` passed in was relative, the
CM 240       ``virtual_root`` will always equal the ``root`` object (the
fb6a5c 241       resource passed in).
916f88 242
8b1f6e 243     - ``virtual_root_path`` -- If :term:`traversal` was used to find
fb6a5c 244       the ``resource``, this will be the sequence of path elements
b32bfd 245       traversed to find the ``virtual_root`` resource.  Each of these
8b1f6e 246       items is a Unicode object.  If no path segments were traversed
b32bfd 247       to find the ``virtual_root`` resource (e.g. if virtual hosting is
8b1f6e 248       not in effect), the ``traversed`` value will be the empty list.
fb6a5c 249       If url dispatch was used to find the ``resource``, this will be
c6895b 250       ``None``.
916f88 251
c6895b 252     If the path cannot be resolved, a :exc:`KeyError` will be raised.
916f88 253
CM 254     Rules for passing a *string* as the ``path`` argument: if the
255     first character in the path string is the with the ``/``
bac5b3 256     character, the path will considered absolute and the resource tree
b32bfd 257     traversal will start at the root resource.  If the first character
916f88 258     of the path string is *not* the ``/`` character, the path is
bac5b3 259     considered relative and resource tree traversal will begin at the resource
fb6a5c 260     object supplied to the function as the ``resource`` argument.  If an
CM 261     empty string is passed as ``path``, the ``resource`` passed in will
262     be returned.  Resource path strings must be escaped in the following
a66593 263     manner: each Unicode path segment must be encoded as UTF-8 and
c6895b 264     each path segment must escaped via Python's :mod:`urllib.quote`.
CM 265     For example, ``/path/to%20the/La%20Pe%C3%B1a`` (absolute) or
266     ``to%20the/La%20Pe%C3%B1a`` (relative).  The
fb6a5c 267     :func:`pyramid.traversal.resource_path` function generates strings
c6895b 268     which follow these rules (albeit only absolute ones).
916f88 269
CM 270     Rules for passing a *tuple* as the ``path`` argument: if the first
271     element in the path tuple is the empty string (for example ``('',
bac5b3 272     'a', 'b', 'c')``, the path is considered absolute and the resource tree
CM 273     traversal will start at the resource tree root object.  If the first
916f88 274     element in the path tuple is not the empty string (for example
bac5b3 275     ``('a', 'b', 'c')``), the path is considered relative and resource tree
fb6a5c 276     traversal will begin at the resource object supplied to the function
CM 277     as the ``resource`` argument.  If an empty sequence is passed as
278     ``path``, the ``resource`` passed in itself will be returned.  No
916f88 279     URL-quoting or UTF-8-encoding of individual path segments within
CM 280     the tuple is required (each segment may be any string or unicode
fb6a5c 281     object representing a resource name).
916f88 282
CM 283     Explanation of the conversion of ``path`` segment values to
284     Unicode during traversal: Each segment is URL-unquoted, and
285     decoded into Unicode. Each segment is assumed to be encoded using
c6895b 286     the UTF-8 encoding (or a subset, such as ASCII); a
c81aad 287     :exc:`pyramid.exceptions.URLDecodeError` is raised if a segment
0482bd 288     cannot be decoded.  If a segment name is empty or if it is ``.``,
CM 289     it is ignored.  If a segment name is ``..``, the previous segment
290     is deleted, and the ``..`` is ignored.  As a result of this
291     process, the return values ``view_name``, each element in the
292     ``subpath``, each element in ``traversed``, and each element in
293     the ``virtual_root_path`` will be Unicode as opposed to a string,
294     and will be URL-decoded.
916f88 295     """
487651 296
8e606d 297     if is_nonstr_iter(path):
916f88 298         # the traverser factory expects PATH_INFO to be a string, not
CM 299         # unicode and it expects path segments to be utf-8 and
300         # urlencoded (it's the same traverser which accepts PATH_INFO
301         # from user agents; user agents always send strings).
302         if path:
391afe 303             path = _join_path_tuple(tuple(path))
916f88 304         else:
CM 305             path = ''
306
a66593 307     # The user is supposed to pass us a string object, never Unicode.  In
CM 308     # practice, however, users indeed pass Unicode to this API.  If they do
309     # pass a Unicode object, its data *must* be entirely encodeable to ASCII,
310     # so we encode it here as a convenience to the user and to prevent
311     # second-order failures from cropping up (all failures will occur at this
312     # step rather than later down the line as the result of calling
313     # ``traversal_path``).
314
f84147 315     path = ascii_native_(path)
a66593 316
916f88 317     if path and path[0] == '/':
fb6a5c 318         resource = find_root(resource)
916f88 319
6fec21 320     reg = get_current_registry()
012b97 321
4eafaa 322     request_factory = reg.queryUtility(IRequestFactory)
CM 323     if request_factory is None:
0c29cf 324         from pyramid.request import Request  # avoid circdep
MM 325
4eafaa 326         request_factory = Request
CM 327
81a833 328     request = request_factory.blank(path)
6fec21 329     request.registry = reg
fb6a5c 330     traverser = reg.queryAdapter(resource, ITraverser)
0a0ffe 331     if traverser is None:
fb6a5c 332         traverser = ResourceTreeTraverser(resource)
5e829c 333
acc776 334     return traverser(request)
0c29cf 335
a97a70 336
fb6a5c 337 def resource_path_tuple(resource, *elements):
eec9cf 338     """
CM 339     Return a tuple representing the absolute physical path of the
bac5b3 340     ``resource`` object based on its position in a resource tree, e.g
8b1f6e 341     ``('', 'foo', 'bar')``.  Any positional arguments passed in as
dbab77 342     ``elements`` will be appended as elements in the tuple
fb6a5c 343     representing the resource path.  For instance, if the resource's
dbab77 344     path is ``('', 'foo', 'bar')`` and elements equals ``('a', 'b')``,
f8d20a 345     the returned tuple will be ``('', 'foo', 'bar', 'a', 'b')``.  The
a97a70 346     first element of this tuple will always be the empty string (a
CM 347     leading empty string element in a path tuple represents that the
348     path is absolute).
349
c6895b 350     This function is a logical inverse of
fb6a5c 351     :func:`pyramid.traversal.find_resource`: it can be used to
f8d20a 352     generate path references that can later be resolved by that function.
a97a70 353
fb6a5c 354     The ``resource`` passed in *must* be :term:`location`-aware.
c8a91c 355
012b97 356     .. note::
8e037f 357
012b97 358        Each segment in the path tuple returned will equal the ``__name__``
M 359        attribute of the resource it represents within the resource tree.  Each
360        of these segments *should* be a unicode or string object (as per the
361        contract of :term:`location`-awareness).  However, no conversion or
362        safety checking of resource names is performed.  For instance, if one of
363        the resources in your tree has a ``__name__`` which (by error) is a
364        dictionary, that dictionary will be placed in the path tuple; no warning
365        or error will be given.
92c3e5 366
012b97 367     .. note::
92c3e5 368
012b97 369        The :term:`root` resource *must* have a ``__name__`` attribute with a
M 370        value of either ``None`` or the empty string for path tuples to be
371        generated properly.  If the root resource has a non-null ``__name__``
a54bc1 372        attribute, its name will be the first element in the generated path
MM 373        tuple rather than the empty string.
7ae0c2 374     """
fb6a5c 375     return tuple(_resource_path_list(resource, *elements))
dbab77 376
0c29cf 377
33516a 378 model_path_tuple = resource_path_tuple  # b/w compat (forever)
0c29cf 379
182210 380
fb6a5c 381 def _resource_path_list(resource, *elements):
a54bc1 382     """ Implementation detail shared by resource_path and
MM 383     resource_path_tuple"""
fb6a5c 384     path = [loc.__name__ or '' for loc in lineage(resource)]
eb9fbf 385     path.reverse()
a6b51e 386     path.extend(elements)
e0be0c 387     return path
e62e47 388
0c29cf 389
MM 390 _model_path_list = _resource_path_list  # b/w compat, not an API
391
fb6a5c 392
CM 393 def virtual_root(resource, request):
7b75a8 394     """
fb6a5c 395     Provided any :term:`resource` and a :term:`request` object, return
CM 396     the resource object representing the :term:`virtual root` of the
8b1f6e 397     current :term:`request`.  Using a virtual root in a
fd5ae9 398     :term:`traversal` -based :app:`Pyramid` application permits
7ba907 399     rooting. For example, the resource at the traversal path ``/cms`` will
MM 400     be found at ``http://example.com/`` instead of rooting it at
7b75a8 401     ``http://example.com/cms/``.
e62e47 402
fb6a5c 403     If the ``resource`` passed in is a context obtained via
7b75a8 404     :term:`traversal`, and if the ``HTTP_X_VHM_ROOT`` key is in the
CM 405     WSGI environment, the value of this key will be treated as a
fb6a5c 406     'virtual root path': the :func:`pyramid.traversal.find_resource`
b32bfd 407     API will be used to find the virtual root resource using this path;
CM 408     if the resource is found, it will be returned.  If the
043ccd 409     ``HTTP_X_VHM_ROOT`` key is not present in the WSGI environment,
bac5b3 410     the physical :term:`root` of the resource tree will be returned instead.
e62e47 411
7b75a8 412     Virtual roots are not useful at all in applications that use
CM 413     :term:`URL dispatch`. Contexts obtained via URL dispatch don't
414     really support being virtually rooted (each URL dispatch context
415     is both its own physical and virtual root).  However if this API
fb6a5c 416     is called with a ``resource`` argument which is a context obtained
CM 417     via URL dispatch, the resource passed in will be returned
b74cd4 418     unconditionally."""
6fec21 419     try:
CM 420         reg = request.registry
421     except AttributeError:
7ba907 422         reg = get_current_registry()
MM 423     url_adapter = reg.queryMultiAdapter((resource, request), IResourceURL)
424     if url_adapter is None:
425         url_adapter = ResourceURL(resource, request)
426
427     vpath, rpath = url_adapter.virtual_path, url_adapter.physical_path
428     if rpath != vpath and rpath.endswith(vpath):
0c29cf 429         vroot_path = rpath[: -len(vpath)]
7ba907 430         return find_resource(resource, vroot_path)
MM 431
432     try:
433         return request.root
434     except AttributeError:
435         return find_root(resource)
0c29cf 436
6d4015 437
e6e0ea 438 def traversal_path(path):
f84147 439     """ Variant of :func:`pyramid.traversal.traversal_path_info` suitable for
5c43d5 440     decoding paths that are URL-encoded.
CM 441
442     If this function is passed a Unicode object instead of a sequence of
443     bytes as ``path``, that Unicode object *must* directly encodeable to
444     ASCII.  For example, u'/foo' will work but u'/<unprintable unicode>' (a
445     Unicode object with characters that cannot be encoded to ascii) will
446     not. A :exc:`UnicodeEncodeError` will be raised if the Unicode cannot be
447     encoded directly to ASCII.
448     """
449     if isinstance(path, text_type):
95df3a 450         # must not possess characters outside ascii
5c43d5 451         path = path.encode('ascii')
95df3a 452     # we unquote this path exactly like a PEP 3333 server would
0c29cf 453     path = unquote_bytes_to_wsgi(path)  # result will be a native string
MM 454     return traversal_path_info(path)  # result will be a tuple of unicode
455
841313 456
6212db 457 @lru_cache(1000)
f84147 458 def traversal_path_info(path):
5c43d5 459     """ Given``path``, return a tuple representing that path which can be
CM 460     used to traverse a resource tree.  ``path`` is assumed to be an
461     already-URL-decoded ``str`` type as if it had come to us from an upstream
6212db 462     WSGI server as the ``PATH_INFO`` environ variable.
841313 463
5c43d5 464     The ``path`` is first decoded to from its WSGI representation to Unicode;
CM 465     it is decoded differently depending on platform:
f84147 466
5c43d5 467     - On Python 2, ``path`` is decoded to Unicode from bytes using the UTF-8
CM 468       decoding directly; a :exc:`pyramid.exc.URLDecodeError` is raised if a the
469       URL cannot be decoded.
f84147 470
6212db 471     - On Python 3, as per the PEP 3333 spec, ``path`` is first encoded to
CM 472       bytes using the Latin-1 encoding; the resulting set of bytes is
473       subsequently decoded to text using the UTF-8 encoding; a
5c43d5 474       :exc:`pyramid.exc.URLDecodeError` is raised if a the URL cannot be
CM 475       decoded.
476
477     The ``path`` is split on slashes, creating a list of segments.  If a
478     segment name is empty or if it is ``.``, it is ignored.  If a segment
479     name is ``..``, the previous segment is deleted, and the ``..`` is
480     ignored.
841313 481
CM 482     Examples:
e6e0ea 483
CM 484     ``/``
485
486         ()
487
488     ``/foo/bar/baz``
489
490         (u'foo', u'bar', u'baz')
491
492     ``foo/bar/baz``
493
494         (u'foo', u'bar', u'baz')
495
496     ``/foo/bar/baz/``
497
498         (u'foo', u'bar', u'baz')
499
500     ``/foo//bar//baz/``
501
502         (u'foo', u'bar', u'baz')
503
504     ``/foo/bar/baz/..``
505
506         (u'foo', u'bar')
507
508     ``/my%20archives/hello``
509
510         (u'my archives', u'hello')
511
512     ``/archives/La%20Pe%C3%B1a``
513
514         (u'archives', u'<unprintable unicode>')
515
012b97 516     .. note::
M 517
518       This function does not generate the same type of tuples that
519       :func:`pyramid.traversal.resource_path_tuple` does.  In particular, the
a54bc1 520       leading empty string is not present in the tuple it returns, unlike
MM 521       tuples returned by :func:`pyramid.traversal.resource_path_tuple`.  As a
522       result, tuples generated by ``traversal_path`` are not resolveable by
523       the :func:`pyramid.traversal.find_resource` API.  ``traversal_path`` is
524       a function mostly used by the internals of :app:`Pyramid` and by people
012b97 525       writing their own traversal machinery, as opposed to users writing
M 526       applications in :app:`Pyramid`.
e6e0ea 527     """
f84147 528     try:
0c29cf 529         path = decode_path_info(path)  # result will be Unicode
f84147 530     except UnicodeDecodeError as e:
CM 531         raise URLDecodeError(e.encoding, e.object, e.start, e.end, e.reason)
0c29cf 532     return split_path_info(path)  # result will be tuple of Unicode
MM 533
5c43d5 534
CM 535 @lru_cache(1000)
536 def split_path_info(path):
95df3a 537     # suitable for splitting an already-unquoted-already-decoded (unicode)
CM 538     # path value
bcd781 539     path = path.strip('/')
62267e 540     clean = []
CM 541     for segment in path.split('/'):
83faa0 542         if not segment or segment == '.':
62267e 543             continue
CM 544         elif segment == '..':
f2ef79 545             if clean:
CM 546                 del clean[-1]
62267e 547         else:
CM 548             clean.append(segment)
e281fa 549     return tuple(clean)
0c29cf 550
df1c6f 551
a97a70 552 _segment_cache = {}
CM 553
5942ec 554 quote_path_segment_doc = """ \
CM 555 Return a quoted representation of a 'path segment' (such as
556 the string ``__name__`` attribute of a resource) as a string.  If the
557 ``segment`` passed in is a unicode object, it is converted to a
558 UTF-8 string, then it is URL-quoted using Python's
559 ``urllib.quote``.  If the ``segment`` passed in is a string, it is
560 URL-quoted using Python's :mod:`urllib.quote`.  If the segment
561 passed in is not a string or unicode object, an error will be
562 raised.  The return value of ``quote_path_segment`` is always a
563 string, never Unicode.
8cf91a 564
5942ec 565 You may pass a string of characters that need not be encoded as
CM 566 the ``safe`` argument to this function.  This corresponds to the
567 ``safe`` argument to :mod:`urllib.quote`.
e0be0c 568
5942ec 569 .. note::
62267e 570
5942ec 571    The return value for each segment passed to this
CM 572    function is cached in a module-scope dictionary for
573    speed: the cached version is returned when possible
574    rather than recomputing the quoted version.  No cache
575    emptying is ever done for the lifetime of an
576    application, however.  If you pass arbitrary
577    user-supplied strings to this function (as opposed to
578    some bounded set of values from a 'working set' known to
579    your application), it may become a memory leak.
580 """
581
582
bc37a5 583 if PY2:
5942ec 584     # special-case on Python 2 for speed?  unchecked
f52759 585     def quote_path_segment(segment, safe=PATH_SEGMENT_SAFE):
5942ec 586         """ %s """ % quote_path_segment_doc
CM 587         # The bit of this code that deals with ``_segment_cache`` is an
588         # optimization: we cache all the computation of URL path segments
589         # in this module-scope dictionary with the original string (or
590         # unicode value) as the key, so we can look it up later without
591         # needing to reencode or re-url-quote it
592         try:
593             return _segment_cache[(segment, safe)]
594         except KeyError:
0c29cf 595             if (
MM 596                 segment.__class__ is text_type
597             ):  # isinstance slighly slower (~15%)
5942ec 598                 result = url_quote(segment.encode('utf-8'), safe)
CM 599             else:
600                 result = url_quote(str(segment), safe)
601             # we don't need a lock to mutate _segment_cache, as the below
602             # will generate exactly one Python bytecode (STORE_SUBSCR)
603             _segment_cache[(segment, safe)] = result
604             return result
0c29cf 605
MM 606
bc37a5 607 else:
0c29cf 608
f52759 609     def quote_path_segment(segment, safe=PATH_SEGMENT_SAFE):
bc37a5 610         """ %s """ % quote_path_segment_doc
MM 611         # The bit of this code that deals with ``_segment_cache`` is an
612         # optimization: we cache all the computation of URL path segments
613         # in this module-scope dictionary with the original string (or
614         # unicode value) as the key, so we can look it up later without
615         # needing to reencode or re-url-quote it
616         try:
617             return _segment_cache[(segment, safe)]
618         except KeyError:
619             if segment.__class__ not in (text_type, binary_type):
620                 segment = str(segment)
621             result = url_quote(native_(segment, 'utf-8'), safe)
622             # we don't need a lock to mutate _segment_cache, as the below
623             # will generate exactly one Python bytecode (STORE_SUBSCR)
624             _segment_cache[(segment, safe)] = result
625             return result
626
0c29cf 627
ebcdc7 628 slash = text_('/')
0c29cf 629
62267e 630
3b7334 631 @implementer(ITraverser)
fb6a5c 632 class ResourceTreeTraverser(object):
CM 633     """ A resource tree traverser that should be used (for speed) when
b32bfd 634     every resource in the tree supplies a ``__name__`` and
CM 635     ``__parent__`` attribute (ie. every resource in the tree is
c6895b 636     :term:`location` aware) ."""
077c3c 637
7ba907 638     VH_ROOT_KEY = VH_ROOT_KEY
427859 639     VIEW_SELECTOR = '@@'
CM 640
62267e 641     def __init__(self, root):
CM 642         self.root = root
3bfa35 643
acc776 644     def __call__(self, request):
def68d 645         environ = request.environ
ebcdc7 646         matchdict = request.matchdict
acc776 647
def68d 648         if matchdict is not None:
25cbe1 649
ebcdc7 650             path = matchdict.get('traverse', slash) or slash
8e606d 651             if is_nonstr_iter(path):
1db5c4 652                 # this is a *traverse stararg (not a {traverse})
5c43d5 653                 # routing has already decoded these elements, so we just
CM 654                 # need to join them
ece96f 655                 path = '/' + slash.join(path) or slash
25cbe1 656
CM 657             subpath = matchdict.get('subpath', ())
8e606d 658             if not is_nonstr_iter(subpath):
1db5c4 659                 # this is not a *subpath stararg (just a {subpath})
5c43d5 660                 # routing has already decoded this string, so we just need
CM 661                 # to split it
662                 subpath = split_path_info(subpath)
077c3c 663
dfc2b6 664         else:
ef8a8c 665             # this request did not match a route
6ecdbc 666             subpath = ()
dfc2b6 667             try:
6212db 668                 # empty if mounted under a path in mod_wsgi, for example
ebcdc7 669                 path = request.path_info or slash
dfc2b6 670             except KeyError:
ebcdc7 671                 # if environ['PATH_INFO'] is just not there
CM 672                 path = slash
6212db 673             except UnicodeDecodeError as e:
0c29cf 674                 raise URLDecodeError(
MM 675                     e.encoding, e.object, e.start, e.end, e.reason
676                 )
1cd598 677
7ba907 678         if self.VH_ROOT_KEY in environ:
5c43d5 679             # HTTP_X_VHM_ROOT
7ba907 680             vroot_path = decode_path_info(environ[self.VH_ROOT_KEY])
92dcb5 681             vroot_tuple = split_path_info(vroot_path)
0c29cf 682             vpath = (
MM 683                 vroot_path + path
684             )  # both will (must) be unicode or asciistr
25c64c 685             vroot_idx = len(vroot_tuple) - 1
160f01 686         else:
CM 687             vroot_tuple = ()
688             vpath = path
689             vroot_idx = -1
62267e 690
487651 691         root = self.root
CM 692         ob = vroot = root
1cd598 693
0c29cf 694         if vpath == slash:  # invariant: vpath must not be empty
7fc6a3 695             # prevent a call to traversal_path if we know it's going
CM 696             # to return the empty tuple
697             vpath_tuple = ()
d70fd9 698         else:
7fc6a3 699             # we do dead reckoning here via tuple slicing instead of
CM 700             # pushing and popping temporary lists for speed purposes
701             # and this hurts readability; apologies
702             i = 0
3901fc 703             view_selector = self.VIEW_SELECTOR
92dcb5 704             vpath_tuple = split_path_info(vpath)
7fc6a3 705             for segment in vpath_tuple:
3901fc 706                 if segment[:2] == view_selector:
0c29cf 707                     return {
MM 708                         'context': ob,
709                         'view_name': segment[2:],
710                         'subpath': vpath_tuple[i + 1 :],
711                         'traversed': vpath_tuple[: vroot_idx + i + 1],
712                         'virtual_root': vroot,
713                         'virtual_root_path': vroot_tuple,
714                         'root': root,
715                     }
d70fd9 716                 try:
CM 717                     getitem = ob.__getitem__
718                 except AttributeError:
0c29cf 719                     return {
MM 720                         'context': ob,
721                         'view_name': segment,
722                         'subpath': vpath_tuple[i + 1 :],
723                         'traversed': vpath_tuple[: vroot_idx + i + 1],
724                         'virtual_root': vroot,
725                         'virtual_root_path': vroot_tuple,
726                         'root': root,
727                     }
1cd598 728
d70fd9 729                 try:
CM 730                     next = getitem(segment)
731                 except KeyError:
0c29cf 732                     return {
MM 733                         'context': ob,
734                         'view_name': segment,
735                         'subpath': vpath_tuple[i + 1 :],
736                         'traversed': vpath_tuple[: vroot_idx + i + 1],
737                         'virtual_root': vroot,
738                         'virtual_root_path': vroot_tuple,
739                         'root': root,
740                     }
51c305 741                 if i == vroot_idx:
7fc6a3 742                     vroot = next
d70fd9 743                 ob = next
CM 744                 i += 1
013c4b 745
0c29cf 746         return {
MM 747             'context': ob,
748             'view_name': empty,
749             'subpath': subpath,
750             'traversed': vpath_tuple,
751             'virtual_root': vroot,
752             'virtual_root_path': vroot_tuple,
753             'root': root,
754         }
217070 755
0c29cf 756
MM 757 ModelGraphTraverser = (
758     ResourceTreeTraverser
759 )  # b/w compat, not API, used in wild
760
fb6a5c 761
7ba907 762 @implementer(IResourceURL)
c51896 763 class ResourceURL(object):
7ba907 764     VH_ROOT_KEY = VH_ROOT_KEY
e62e47 765
c51896 766     def __init__(self, resource, request):
db0185 767         physical_path_tuple = resource_path_tuple(resource)
CM 768         physical_path = _join_path_tuple(physical_path_tuple)
769
770         if physical_path_tuple != ('',):
771             physical_path_tuple = physical_path_tuple + ('',)
c51896 772             physical_path = physical_path + '/'
CM 773
774         virtual_path = physical_path
db0185 775         virtual_path_tuple = physical_path_tuple
c51896 776
CM 777         environ = request.environ
7ba907 778         vroot_path = environ.get(self.VH_ROOT_KEY)
c51896 779
CM 780         # if the physical path starts with the virtual root path, trim it out
781         # of the virtual path
782         if vroot_path is not None:
db0185 783             vroot_path = vroot_path.rstrip('/')
CM 784             if vroot_path and physical_path.startswith(vroot_path):
785                 vroot_path_tuple = tuple(vroot_path.split('/'))
786                 numels = len(vroot_path_tuple)
787                 virtual_path_tuple = ('',) + physical_path_tuple[numels:]
0c29cf 788                 virtual_path = physical_path[len(vroot_path) :]
c51896 789
0c29cf 790         self.virtual_path = virtual_path  # IResourceURL attr
44f585 791         self.physical_path = physical_path  # IResourceURL attr
0c29cf 792         self.virtual_path_tuple = virtual_path_tuple  # IResourceURL attr (1.5)
MM 793         self.physical_path_tuple = (
794             physical_path_tuple
795         )  # IResourceURL attr (1.5)
796
118ea0 797
eb9fbf 798 @lru_cache(1000)
CM 799 def _join_path_tuple(tuple):
800     return tuple and '/'.join([quote_path_segment(x) for x in tuple]) or '/'
e62e47 801
0c29cf 802
eac7c4 803 class DefaultRootFactory:
CM 804     __parent__ = None
805     __name__ = None
0c29cf 806
eac7c4 807     def __init__(self, request):
95e971 808         pass