src/pyams_utils/traversing.py
changeset 289 c8e21d7dd685
child 292 b338586588ad
equal deleted inserted replaced
-1:000000000000 289:c8e21d7dd685
       
     1 #
       
     2 # Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
       
     3 # All Rights Reserved.
       
     4 #
       
     5 # This software is subject to the provisions of the Zope Public License,
       
     6 # Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
       
     7 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
       
     8 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
       
     9 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
       
    10 # FOR A PARTICULAR PURPOSE.
       
    11 #
       
    12 
       
    13 __docformat__ = 'restructuredtext'
       
    14 
       
    15 
       
    16 # import standard library
       
    17 
       
    18 # import interfaces
       
    19 from pyams_utils.interfaces.traversing import IPathElements
       
    20 from pyramid.interfaces import VH_ROOT_KEY
       
    21 from zope.intid.interfaces import IIntIds
       
    22 from zope.location.interfaces import IContained
       
    23 from zope.traversing.interfaces import ITraversable, BeforeTraverseEvent
       
    24 
       
    25 # import packages
       
    26 from pyams_utils.adapter import adapter_config, ContextAdapter
       
    27 from pyams_utils.registry import query_utility
       
    28 from pyramid.compat import decode_path_info, is_nonstr_iter
       
    29 from pyramid.exceptions import URLDecodeError, NotFound
       
    30 from pyramid.location import lineage
       
    31 from pyramid.threadlocal import get_current_registry
       
    32 from pyramid.traversal import ResourceTreeTraverser, slash, split_path_info, empty
       
    33 from zope.interface import Interface
       
    34 
       
    35 
       
    36 class NamespaceTraverser(ResourceTreeTraverser):
       
    37     """Custom traverser handling views and namespaces
       
    38 
       
    39     This is an upgraded version of native Pyramid traverser.
       
    40     It adds:
       
    41     - a new BeforeTraverseEvent before traversing each object in the path
       
    42     - support for namespaces with "++" notation
       
    43     """
       
    44 
       
    45     NAMESPACE_SELECTOR = '++'
       
    46 
       
    47     def __call__(self, request):
       
    48 
       
    49         environ = request.environ
       
    50         matchdict = request.matchdict
       
    51 
       
    52         if matchdict is not None:
       
    53             path = matchdict.get('traverse', slash) or slash
       
    54             if is_nonstr_iter(path):
       
    55                 # this is a *traverse stararg (not a {traverse})
       
    56                 # routing has already decoded these elements, so we just
       
    57                 # need to join them
       
    58                 path = '/' + slash.join(path) or slash
       
    59 
       
    60             subpath = matchdict.get('subpath', ())
       
    61             if not is_nonstr_iter(subpath):
       
    62                 # this is not a *subpath stararg (just a {subpath})
       
    63                 # routing has already decoded this string, so we just need
       
    64                 # to split it
       
    65                 subpath = split_path_info(subpath)
       
    66 
       
    67         else:
       
    68             subpath = ()
       
    69             try:
       
    70                 # empty if mounted under a path in mod_wsgi, for example
       
    71                 path = request.path_info or slash
       
    72             except KeyError:
       
    73                 # if environ['PATH_INFO'] is just not there
       
    74                 path = slash
       
    75             except UnicodeDecodeError as e:
       
    76                 raise URLDecodeError(e.encoding, e.object, e.start, e.end, e.reason)
       
    77 
       
    78         if VH_ROOT_KEY in environ:
       
    79             # HTTP_X_VHM_ROOT
       
    80             vroot_path = decode_path_info(environ[VH_ROOT_KEY])
       
    81             vroot_tuple = split_path_info(vroot_path)
       
    82             vpath = vroot_path + path
       
    83             vroot_idx = len(vroot_tuple) - 1
       
    84         else:
       
    85             vroot_tuple = ()
       
    86             vpath = path
       
    87             vroot_idx = -1
       
    88 
       
    89         root = self.root
       
    90         ob = vroot = root
       
    91 
       
    92         request.registry.notify(BeforeTraverseEvent(root, request))
       
    93 
       
    94         if vpath == slash:
       
    95             # invariant: vpath must not be empty
       
    96             # prevent a call to traversal_path if we know it's going
       
    97             # to return the empty tuple
       
    98             vpath_tuple = ()
       
    99 
       
   100         else:
       
   101             # we do dead reckoning here via tuple slicing instead of
       
   102             # pushing and popping temporary lists for speed purposes
       
   103             # and this hurts readability; apologies
       
   104             i = 0
       
   105             view_selector = self.VIEW_SELECTOR
       
   106             ns_selector = self.NAMESPACE_SELECTOR
       
   107             vpath_tuple = split_path_info(vpath)
       
   108 
       
   109             for segment in vpath_tuple:
       
   110                 if ob is not root:
       
   111                     request.registry.notify(BeforeTraverseEvent(ob, request))
       
   112 
       
   113                 if segment[:2] == view_selector:
       
   114                     # check for view name prefixed by '@@'
       
   115                     return {'context': ob,
       
   116                             'view_name': segment[2:],
       
   117                             'subpath': vpath_tuple[i + 1:],
       
   118                             'traversed': vpath_tuple[:vroot_idx + i + 1],
       
   119                             'virtual_root': vroot,
       
   120                             'virtual_root_path': vroot_tuple,
       
   121                             'root': root}
       
   122 
       
   123                 elif segment[:2] == ns_selector:
       
   124                     # check for namespace prefixed by '++'
       
   125                     # when a namespace is detected, named "ITraversable" multi-adapters are searched for
       
   126                     # context and request, for context and for request, sequentially; a NotFound exception is
       
   127                     # raised if traverser can't be found, otherwise it's "traverse" method is called to get new
       
   128                     # context
       
   129                     ns, name = segment[2:].split(ns_selector, 1)
       
   130                     registry = get_current_registry()
       
   131                     traverser = registry.queryMultiAdapter((ob, request), ITraversable, ns)
       
   132                     if traverser is None:
       
   133                         traverser = registry.queryAdapter(ob, ITraversable, ns)
       
   134                     if traverser is None:
       
   135                         traverser = registry.queryAdapter(request, ITraversable, ns)
       
   136                     if traverser is None:
       
   137                         raise NotFound()
       
   138                     ob = traverser.traverse(name, vpath_tuple[vroot_idx + i + 1:])
       
   139                     i += 1
       
   140                     continue
       
   141 
       
   142                 try:
       
   143                     getitem = ob.__getitem__
       
   144                 except AttributeError:
       
   145                     return {'context': ob,
       
   146                             'view_name': segment,
       
   147                             'subpath': vpath_tuple[i + 1:],
       
   148                             'traversed': vpath_tuple[:vroot_idx + i + 1],
       
   149                             'virtual_root': vroot,
       
   150                             'virtual_root_path': vroot_tuple,
       
   151                             'root': root}
       
   152 
       
   153                 try:
       
   154                     next = getitem(segment)
       
   155                 except KeyError:
       
   156                     return {'context': ob,
       
   157                             'view_name': segment,
       
   158                             'subpath': vpath_tuple[i + 1:],
       
   159                             'traversed': vpath_tuple[:vroot_idx + i + 1],
       
   160                             'virtual_root': vroot,
       
   161                             'virtual_root_path': vroot_tuple,
       
   162                             'root': root}
       
   163                 if i == vroot_idx:
       
   164                     vroot = next
       
   165                 ob = next
       
   166                 i += 1
       
   167 
       
   168         if ob is not root:
       
   169             request.registry.notify(BeforeTraverseEvent(ob, request))
       
   170 
       
   171         return {'context': ob,
       
   172                 'view_name': empty,
       
   173                 'subpath': subpath,
       
   174                 'traversed': vpath_tuple,
       
   175                 'virtual_root': vroot,
       
   176                 'virtual_root_path': vroot_tuple,
       
   177                 'root': root}
       
   178 
       
   179 
       
   180 def get_parent(context, interface=Interface, allow_context=True, condition=None):
       
   181     """Get first parent of the context that implements given interface
       
   182 
       
   183     :param object context: base element
       
   184     :param Interface interface: the interface that parend should implement
       
   185     :param boolean allow_context: if 'True' (the default), traversing is done starting with context; otherwise,
       
   186         traversing is done starting from context's parent
       
   187     :param callable condition: an optional function that should return a 'True' result when called with parent
       
   188         as first argument
       
   189     """
       
   190     if allow_context:
       
   191         parent = context
       
   192     else:
       
   193         parent = getattr(context, '__parent__', None)
       
   194     while parent is not None:
       
   195         if interface.providedBy(parent):
       
   196             target = interface(parent)
       
   197             if (not condition) or condition(target):
       
   198                 return target
       
   199         parent = getattr(parent, '__parent__', None)
       
   200     return None
       
   201 
       
   202 
       
   203 @adapter_config(context=IContained, provides=IPathElements)
       
   204 class PathElementsAdapter(ContextAdapter):
       
   205     """Contained object path elements adapter
       
   206 
       
   207     This interface is intended to be used inside a keyword index to
       
   208     be able to search object based on a given parent
       
   209     """
       
   210 
       
   211     @property
       
   212     def parents(self):
       
   213         intids = query_utility(IIntIds)
       
   214         if intids is None:
       
   215             return []
       
   216         return [intids.register(parent) for parent in lineage(self.context)]