Michael Merickel
2018-10-15 0c29cf2df41600d3906d521c72991c7686018b71
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__``
372        attribute, its name will be the first element in the generated path tuple
373        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):
CM 382     """ Implementation detail shared by resource_path and resource_path_tuple"""
383     path = [loc.__name__ or '' for loc in lineage(resource)]
eb9fbf 384     path.reverse()
a6b51e 385     path.extend(elements)
e0be0c 386     return path
e62e47 387
0c29cf 388
MM 389 _model_path_list = _resource_path_list  # b/w compat, not an API
390
fb6a5c 391
CM 392 def virtual_root(resource, request):
7b75a8 393     """
fb6a5c 394     Provided any :term:`resource` and a :term:`request` object, return
CM 395     the resource object representing the :term:`virtual root` of the
8b1f6e 396     current :term:`request`.  Using a virtual root in a
fd5ae9 397     :term:`traversal` -based :app:`Pyramid` application permits
7ba907 398     rooting. For example, the resource at the traversal path ``/cms`` will
MM 399     be found at ``http://example.com/`` instead of rooting it at
7b75a8 400     ``http://example.com/cms/``.
e62e47 401
fb6a5c 402     If the ``resource`` passed in is a context obtained via
7b75a8 403     :term:`traversal`, and if the ``HTTP_X_VHM_ROOT`` key is in the
CM 404     WSGI environment, the value of this key will be treated as a
fb6a5c 405     'virtual root path': the :func:`pyramid.traversal.find_resource`
b32bfd 406     API will be used to find the virtual root resource using this path;
CM 407     if the resource is found, it will be returned.  If the
043ccd 408     ``HTTP_X_VHM_ROOT`` key is not present in the WSGI environment,
bac5b3 409     the physical :term:`root` of the resource tree will be returned instead.
e62e47 410
7b75a8 411     Virtual roots are not useful at all in applications that use
CM 412     :term:`URL dispatch`. Contexts obtained via URL dispatch don't
413     really support being virtually rooted (each URL dispatch context
414     is both its own physical and virtual root).  However if this API
fb6a5c 415     is called with a ``resource`` argument which is a context obtained
CM 416     via URL dispatch, the resource passed in will be returned
b74cd4 417     unconditionally."""
6fec21 418     try:
CM 419         reg = request.registry
420     except AttributeError:
7ba907 421         reg = get_current_registry()
MM 422     url_adapter = reg.queryMultiAdapter((resource, request), IResourceURL)
423     if url_adapter is None:
424         url_adapter = ResourceURL(resource, request)
425
426     vpath, rpath = url_adapter.virtual_path, url_adapter.physical_path
427     if rpath != vpath and rpath.endswith(vpath):
0c29cf 428         vroot_path = rpath[: -len(vpath)]
7ba907 429         return find_resource(resource, vroot_path)
MM 430
431     try:
432         return request.root
433     except AttributeError:
434         return find_root(resource)
0c29cf 435
6d4015 436
e6e0ea 437 def traversal_path(path):
f84147 438     """ Variant of :func:`pyramid.traversal.traversal_path_info` suitable for
5c43d5 439     decoding paths that are URL-encoded.
CM 440
441     If this function is passed a Unicode object instead of a sequence of
442     bytes as ``path``, that Unicode object *must* directly encodeable to
443     ASCII.  For example, u'/foo' will work but u'/<unprintable unicode>' (a
444     Unicode object with characters that cannot be encoded to ascii) will
445     not. A :exc:`UnicodeEncodeError` will be raised if the Unicode cannot be
446     encoded directly to ASCII.
447     """
448     if isinstance(path, text_type):
95df3a 449         # must not possess characters outside ascii
5c43d5 450         path = path.encode('ascii')
95df3a 451     # we unquote this path exactly like a PEP 3333 server would
0c29cf 452     path = unquote_bytes_to_wsgi(path)  # result will be a native string
MM 453     return traversal_path_info(path)  # result will be a tuple of unicode
454
841313 455
6212db 456 @lru_cache(1000)
f84147 457 def traversal_path_info(path):
5c43d5 458     """ Given``path``, return a tuple representing that path which can be
CM 459     used to traverse a resource tree.  ``path`` is assumed to be an
460     already-URL-decoded ``str`` type as if it had come to us from an upstream
6212db 461     WSGI server as the ``PATH_INFO`` environ variable.
841313 462
5c43d5 463     The ``path`` is first decoded to from its WSGI representation to Unicode;
CM 464     it is decoded differently depending on platform:
f84147 465
5c43d5 466     - On Python 2, ``path`` is decoded to Unicode from bytes using the UTF-8
CM 467       decoding directly; a :exc:`pyramid.exc.URLDecodeError` is raised if a the
468       URL cannot be decoded.
f84147 469
6212db 470     - On Python 3, as per the PEP 3333 spec, ``path`` is first encoded to
CM 471       bytes using the Latin-1 encoding; the resulting set of bytes is
472       subsequently decoded to text using the UTF-8 encoding; a
5c43d5 473       :exc:`pyramid.exc.URLDecodeError` is raised if a the URL cannot be
CM 474       decoded.
475
476     The ``path`` is split on slashes, creating a list of segments.  If a
477     segment name is empty or if it is ``.``, it is ignored.  If a segment
478     name is ``..``, the previous segment is deleted, and the ``..`` is
479     ignored.
841313 480
CM 481     Examples:
e6e0ea 482
CM 483     ``/``
484
485         ()
486
487     ``/foo/bar/baz``
488
489         (u'foo', u'bar', u'baz')
490
491     ``foo/bar/baz``
492
493         (u'foo', u'bar', u'baz')
494
495     ``/foo/bar/baz/``
496
497         (u'foo', u'bar', u'baz')
498
499     ``/foo//bar//baz/``
500
501         (u'foo', u'bar', u'baz')
502
503     ``/foo/bar/baz/..``
504
505         (u'foo', u'bar')
506
507     ``/my%20archives/hello``
508
509         (u'my archives', u'hello')
510
511     ``/archives/La%20Pe%C3%B1a``
512
513         (u'archives', u'<unprintable unicode>')
514
012b97 515     .. note::
M 516
517       This function does not generate the same type of tuples that
518       :func:`pyramid.traversal.resource_path_tuple` does.  In particular, the
519       leading empty string is not present in the tuple it returns, unlike tuples
520       returned by :func:`pyramid.traversal.resource_path_tuple`.  As a result,
521       tuples generated by ``traversal_path`` are not resolveable by the
522       :func:`pyramid.traversal.find_resource` API.  ``traversal_path`` is a
523       function mostly used by the internals of :app:`Pyramid` and by people
524       writing their own traversal machinery, as opposed to users writing
525       applications in :app:`Pyramid`.
e6e0ea 526     """
f84147 527     try:
0c29cf 528         path = decode_path_info(path)  # result will be Unicode
f84147 529     except UnicodeDecodeError as e:
CM 530         raise URLDecodeError(e.encoding, e.object, e.start, e.end, e.reason)
0c29cf 531     return split_path_info(path)  # result will be tuple of Unicode
MM 532
5c43d5 533
CM 534 @lru_cache(1000)
535 def split_path_info(path):
95df3a 536     # suitable for splitting an already-unquoted-already-decoded (unicode)
CM 537     # path value
bcd781 538     path = path.strip('/')
62267e 539     clean = []
CM 540     for segment in path.split('/'):
83faa0 541         if not segment or segment == '.':
62267e 542             continue
CM 543         elif segment == '..':
f2ef79 544             if clean:
CM 545                 del clean[-1]
62267e 546         else:
CM 547             clean.append(segment)
e281fa 548     return tuple(clean)
0c29cf 549
df1c6f 550
a97a70 551 _segment_cache = {}
CM 552
5942ec 553 quote_path_segment_doc = """ \
CM 554 Return a quoted representation of a 'path segment' (such as
555 the string ``__name__`` attribute of a resource) as a string.  If the
556 ``segment`` passed in is a unicode object, it is converted to a
557 UTF-8 string, then it is URL-quoted using Python's
558 ``urllib.quote``.  If the ``segment`` passed in is a string, it is
559 URL-quoted using Python's :mod:`urllib.quote`.  If the segment
560 passed in is not a string or unicode object, an error will be
561 raised.  The return value of ``quote_path_segment`` is always a
562 string, never Unicode.
8cf91a 563
5942ec 564 You may pass a string of characters that need not be encoded as
CM 565 the ``safe`` argument to this function.  This corresponds to the
566 ``safe`` argument to :mod:`urllib.quote`.
e0be0c 567
5942ec 568 .. note::
62267e 569
5942ec 570    The return value for each segment passed to this
CM 571    function is cached in a module-scope dictionary for
572    speed: the cached version is returned when possible
573    rather than recomputing the quoted version.  No cache
574    emptying is ever done for the lifetime of an
575    application, however.  If you pass arbitrary
576    user-supplied strings to this function (as opposed to
577    some bounded set of values from a 'working set' known to
578    your application), it may become a memory leak.
579 """
580
581
bc37a5 582 if PY2:
5942ec 583     # special-case on Python 2 for speed?  unchecked
f52759 584     def quote_path_segment(segment, safe=PATH_SEGMENT_SAFE):
5942ec 585         """ %s """ % quote_path_segment_doc
CM 586         # The bit of this code that deals with ``_segment_cache`` is an
587         # optimization: we cache all the computation of URL path segments
588         # in this module-scope dictionary with the original string (or
589         # unicode value) as the key, so we can look it up later without
590         # needing to reencode or re-url-quote it
591         try:
592             return _segment_cache[(segment, safe)]
593         except KeyError:
0c29cf 594             if (
MM 595                 segment.__class__ is text_type
596             ):  # isinstance slighly slower (~15%)
5942ec 597                 result = url_quote(segment.encode('utf-8'), safe)
CM 598             else:
599                 result = url_quote(str(segment), safe)
600             # we don't need a lock to mutate _segment_cache, as the below
601             # will generate exactly one Python bytecode (STORE_SUBSCR)
602             _segment_cache[(segment, safe)] = result
603             return result
0c29cf 604
MM 605
bc37a5 606 else:
0c29cf 607
f52759 608     def quote_path_segment(segment, safe=PATH_SEGMENT_SAFE):
bc37a5 609         """ %s """ % quote_path_segment_doc
MM 610         # The bit of this code that deals with ``_segment_cache`` is an
611         # optimization: we cache all the computation of URL path segments
612         # in this module-scope dictionary with the original string (or
613         # unicode value) as the key, so we can look it up later without
614         # needing to reencode or re-url-quote it
615         try:
616             return _segment_cache[(segment, safe)]
617         except KeyError:
618             if segment.__class__ not in (text_type, binary_type):
619                 segment = str(segment)
620             result = url_quote(native_(segment, 'utf-8'), safe)
621             # we don't need a lock to mutate _segment_cache, as the below
622             # will generate exactly one Python bytecode (STORE_SUBSCR)
623             _segment_cache[(segment, safe)] = result
624             return result
625
0c29cf 626
ebcdc7 627 slash = text_('/')
0c29cf 628
62267e 629
3b7334 630 @implementer(ITraverser)
fb6a5c 631 class ResourceTreeTraverser(object):
CM 632     """ A resource tree traverser that should be used (for speed) when
b32bfd 633     every resource in the tree supplies a ``__name__`` and
CM 634     ``__parent__`` attribute (ie. every resource in the tree is
c6895b 635     :term:`location` aware) ."""
077c3c 636
7ba907 637     VH_ROOT_KEY = VH_ROOT_KEY
427859 638     VIEW_SELECTOR = '@@'
CM 639
62267e 640     def __init__(self, root):
CM 641         self.root = root
3bfa35 642
acc776 643     def __call__(self, request):
def68d 644         environ = request.environ
ebcdc7 645         matchdict = request.matchdict
acc776 646
def68d 647         if matchdict is not None:
25cbe1 648
ebcdc7 649             path = matchdict.get('traverse', slash) or slash
8e606d 650             if is_nonstr_iter(path):
1db5c4 651                 # this is a *traverse stararg (not a {traverse})
5c43d5 652                 # routing has already decoded these elements, so we just
CM 653                 # need to join them
ece96f 654                 path = '/' + slash.join(path) or slash
25cbe1 655
CM 656             subpath = matchdict.get('subpath', ())
8e606d 657             if not is_nonstr_iter(subpath):
1db5c4 658                 # this is not a *subpath stararg (just a {subpath})
5c43d5 659                 # routing has already decoded this string, so we just need
CM 660                 # to split it
661                 subpath = split_path_info(subpath)
077c3c 662
dfc2b6 663         else:
ef8a8c 664             # this request did not match a route
6ecdbc 665             subpath = ()
dfc2b6 666             try:
6212db 667                 # empty if mounted under a path in mod_wsgi, for example
ebcdc7 668                 path = request.path_info or slash
dfc2b6 669             except KeyError:
ebcdc7 670                 # if environ['PATH_INFO'] is just not there
CM 671                 path = slash
6212db 672             except UnicodeDecodeError as e:
0c29cf 673                 raise URLDecodeError(
MM 674                     e.encoding, e.object, e.start, e.end, e.reason
675                 )
1cd598 676
7ba907 677         if self.VH_ROOT_KEY in environ:
5c43d5 678             # HTTP_X_VHM_ROOT
7ba907 679             vroot_path = decode_path_info(environ[self.VH_ROOT_KEY])
92dcb5 680             vroot_tuple = split_path_info(vroot_path)
0c29cf 681             vpath = (
MM 682                 vroot_path + path
683             )  # both will (must) be unicode or asciistr
25c64c 684             vroot_idx = len(vroot_tuple) - 1
160f01 685         else:
CM 686             vroot_tuple = ()
687             vpath = path
688             vroot_idx = -1
62267e 689
487651 690         root = self.root
CM 691         ob = vroot = root
1cd598 692
0c29cf 693         if vpath == slash:  # invariant: vpath must not be empty
7fc6a3 694             # prevent a call to traversal_path if we know it's going
CM 695             # to return the empty tuple
696             vpath_tuple = ()
d70fd9 697         else:
7fc6a3 698             # we do dead reckoning here via tuple slicing instead of
CM 699             # pushing and popping temporary lists for speed purposes
700             # and this hurts readability; apologies
701             i = 0
3901fc 702             view_selector = self.VIEW_SELECTOR
92dcb5 703             vpath_tuple = split_path_info(vpath)
7fc6a3 704             for segment in vpath_tuple:
3901fc 705                 if segment[:2] == view_selector:
0c29cf 706                     return {
MM 707                         'context': ob,
708                         'view_name': segment[2:],
709                         'subpath': vpath_tuple[i + 1 :],
710                         'traversed': vpath_tuple[: vroot_idx + i + 1],
711                         'virtual_root': vroot,
712                         'virtual_root_path': vroot_tuple,
713                         'root': root,
714                     }
d70fd9 715                 try:
CM 716                     getitem = ob.__getitem__
717                 except AttributeError:
0c29cf 718                     return {
MM 719                         'context': ob,
720                         'view_name': segment,
721                         'subpath': vpath_tuple[i + 1 :],
722                         'traversed': vpath_tuple[: vroot_idx + i + 1],
723                         'virtual_root': vroot,
724                         'virtual_root_path': vroot_tuple,
725                         'root': root,
726                     }
1cd598 727
d70fd9 728                 try:
CM 729                     next = getitem(segment)
730                 except KeyError:
0c29cf 731                     return {
MM 732                         'context': ob,
733                         'view_name': segment,
734                         'subpath': vpath_tuple[i + 1 :],
735                         'traversed': vpath_tuple[: vroot_idx + i + 1],
736                         'virtual_root': vroot,
737                         'virtual_root_path': vroot_tuple,
738                         'root': root,
739                     }
51c305 740                 if i == vroot_idx:
7fc6a3 741                     vroot = next
d70fd9 742                 ob = next
CM 743                 i += 1
013c4b 744
0c29cf 745         return {
MM 746             'context': ob,
747             'view_name': empty,
748             'subpath': subpath,
749             'traversed': vpath_tuple,
750             'virtual_root': vroot,
751             'virtual_root_path': vroot_tuple,
752             'root': root,
753         }
217070 754
0c29cf 755
MM 756 ModelGraphTraverser = (
757     ResourceTreeTraverser
758 )  # b/w compat, not API, used in wild
759
fb6a5c 760
7ba907 761 @implementer(IResourceURL)
c51896 762 class ResourceURL(object):
7ba907 763     VH_ROOT_KEY = VH_ROOT_KEY
e62e47 764
c51896 765     def __init__(self, resource, request):
db0185 766         physical_path_tuple = resource_path_tuple(resource)
CM 767         physical_path = _join_path_tuple(physical_path_tuple)
768
769         if physical_path_tuple != ('',):
770             physical_path_tuple = physical_path_tuple + ('',)
c51896 771             physical_path = physical_path + '/'
CM 772
773         virtual_path = physical_path
db0185 774         virtual_path_tuple = physical_path_tuple
c51896 775
CM 776         environ = request.environ
7ba907 777         vroot_path = environ.get(self.VH_ROOT_KEY)
c51896 778
CM 779         # if the physical path starts with the virtual root path, trim it out
780         # of the virtual path
781         if vroot_path is not None:
db0185 782             vroot_path = vroot_path.rstrip('/')
CM 783             if vroot_path and physical_path.startswith(vroot_path):
784                 vroot_path_tuple = tuple(vroot_path.split('/'))
785                 numels = len(vroot_path_tuple)
786                 virtual_path_tuple = ('',) + physical_path_tuple[numels:]
0c29cf 787                 virtual_path = physical_path[len(vroot_path) :]
c51896 788
0c29cf 789         self.virtual_path = virtual_path  # IResourceURL attr
44f585 790         self.physical_path = physical_path  # IResourceURL attr
0c29cf 791         self.virtual_path_tuple = virtual_path_tuple  # IResourceURL attr (1.5)
MM 792         self.physical_path_tuple = (
793             physical_path_tuple
794         )  # IResourceURL attr (1.5)
795
118ea0 796
eb9fbf 797 @lru_cache(1000)
CM 798 def _join_path_tuple(tuple):
799     return tuple and '/'.join([quote_path_segment(x) for x in tuple]) or '/'
e62e47 800
0c29cf 801
eac7c4 802 class DefaultRootFactory:
CM 803     __parent__ = None
804     __name__ = None
0c29cf 805
eac7c4 806     def __init__(self, request):
95e971 807         pass